串口通信
基础知识:
- 什么是串口?
串口全称串行通信接口,是一种常用于电子设备之间通信的异步,全双工接口,典型的串口通信只需要 3 根线,分别是地线 (GND),发送线(TX),接收线(RX)。如下图:
2.什么是波特率?
串口的通信速率称之为波特率(bandrate),波特率也可以叫码元速率,定义: 单位时间内通过信道传输的码元个数就是波特率,单位: 波特
在数字信道中,一个脉冲信号就是一个码元
,码元速率指的是在 1 秒钟内能发送多少个码元
也就是说在数字信道中 1 秒钟内可以发送多少个脉冲信号
需要注意的是,串口的波特率并不能随意设置,因为通信的双方必须设置相同的波特率才可
以成功通信。如果双方的波特率设置不一样则不能通信成功。另外波特率的值一般在常用的
里面选择,约定俗成。而特殊的波特率有可能需要额外设置。
- 什么是比特率?
比特率:每秒钟传送的比特数,单位是 bit/s
比特率和波特率的关系:
比特率=波特率*log2M,M 表示每个码元承载的信息量。
M 如何理解?
一个码元就是一个脉冲信号!一个脉冲信号有可能携带 1bit 数据,也有可能携带 2bit 数据,4bit 数据。在二进制系统中,比特率就等于波特率
问题: 🤚
假如串口的波特率是 9600,在二进制系统中一秒钟可以传送多少个字节。
分析:一个字节等于 8 个 bit,也就是 8 个高低电平。在二进制系统中,比特率就等于波特率所以就是 9600/8=1200 个字节
串口通信的格式
- 格式 📌
串口通信的格式分为两种:标准格式和非标准格式。
标准格式:起始位(1bit) + 数据位(8bit) + 奇偶校验位(1bit) + 停止位(1bit)
起始位:数据线上空闲时为 1,拉低代表开始传输数据
数据位:需要发送或者接收的数据。
奇偶校验位:通过对数据中的 1的个数(奇数/偶数) 来校验数据传输是否准确。
停止位:数据传输完成。数据线恢复成 1的状态。
- 校验 🖇
奇校验 (odd parity): 如果数据中有奇数个 1,校验位为 1,否则为 0
偶校验 (even parity): 如果数据中有偶数个 1,校验位为 1,否则为 0
0 校验(space parity): 校验位恒为 0,如果为 1表述错误。
1校验(mark parity):校验位恒为 1,如果为0表示错误
串口的通讯接口
- 串口只对数据格式有定义,并没有规定接口的电器特性,
- 如果用高电平代表 1,用低电平代表0,那高电平是多少v呢?低电平又是多少v 呢?所以串口的通信接口类型有很多。
- 在举个例子:🍐
如果在串口通信中直接使用处理器引出的接口,电平是 TTL 电平。但是处理器的电平也有可能存在差异,所有某些情况下并不能直接连接。这时就要进行电平转换。
TTL:transistor transistor logic
(我们学习时用的单片机时常常用的TTL电平的,就是那些普通的杜邦线做连接)但是 TTL 的抗干扰能力比较弱,在数据传输的时候很容易出错,所以通信距离也短。往往只用在一个电路板中的俩个不同的芯片通信。既然串口没有规定电器特性,那是不是就可以通过电器特性入手来解决 TTL 的缺点呢?
RS232/协议
RS232 协议是 1970 年美国电子工业协会联合各个厂家共同制定的串行通信标准。这个标准
规定了在串口通信中采用一个标准的连接器,如下图所示,并且在标准中对每个连接器的引
脚和电平也做了规定。
DB9 引脚说明👆
1脚: 载波检测(DCD)
2脚: 接收数据(RXD)
3 脚: 发出数据(TXD)
4 脚: 数据终端准备好(DTR)
5 脚:信号地线(SG)
6脚: 数据准备好(DSR)
7 脚: 请求发送(RTS)
8 脚: 清除发送(CTS)
9 脚: 振铃指示(RI)
注 : Recommand Standard 即 RS,推荐标准
特点:逻辑1的电平为-5v 到-15v,逻辑 0的电平为+5v 到+15v。所以抗干扰能力有所增强。通信距离一般可达 15m。
RS485/协议
RS232 通信速度并不快,而且传输距离也不是很远(15m),相对于 TTL 电平来说确实提高
了抗干扰性,但是容易产生共模干扰。
RS485 标准是由电信行业协会和电子工业联盟制定。主要是用来解决超远距离(1200m)以
及更好的抗干扰性能。并且 RS485 具有多站能力,可以利用 RS485 组网。
电平特性
RS485 使用差分信号(抗共模干扰能力强) 进行数据传输,俩线之间的电压差为+2v 到+6v
表示逻辑 1,俩线之间的电压差为-2v 到-6v 表示逻辑 0。
差分信号是用俩跟线来描述是 0 还是 1,所以本质虽然也是串口,但是 485 是半双工,在软件编程中多了一个切换接收或者发送的操作,其他同串口编程一样。
串口子系统框架
使能内核驱动程序
bash
#默认都是开着的
Device Drivers --->
Character devices --->
Serial drivers --->
8250/16550 and compatible serial support
串口驱动8250🏎,8250通用的串口程序,不光在在ARM中使用,在x86中也有使用,是一个非常完善的驱动程序,相当于一个"汽车轮子",而我们在造车时就不需要再重复造车轮了。
串口的应用编程
串口不像SPI
或IIC
的注册流程,不需要在driver层注册驱动,完善控制器的相关结构体和必要的配置信息,而是直接在应用层使用驱动函数即可。
串口的操作流程
了解一下串口的配置结构体: termios
用于控制非同步通信端口。 这个结构包含了至少下列成员:
cpp
tcflag_t c_iflag; /* 输入模式 */
tcflag_t c_oflag; /* 输出模式 */
tcflag_t c_cflag; /* 控制模式 */
tcflag_t c_lflag; /* 本地模式 */
cc_t c_cc[NCCS]; /* 控制字符 */
struct termios
{
unsigned short c_iflag; /* 输入模式标志*/
unsigned short c_oflag; /* 输出模式标志*/
unsigned short c_cflag; /* 控制模式标志*/
unsigned short c_lflag; /*区域模式标志或本地模式标志或局部模式*/
unsigned char c_line; /*行控制line discipline */
unsigned char c_cc[NCC]; /* 控制字符特性*/
};
- 步骤一:
保存原来的串口配置,使用 tcgetattr 函数获取原来的 termio .结构体。
cpp
struct termios newtio, oldtio;
tcgetattr(fd, &oldtio);
- 步骤二:
设置c_cflag
,打开CLOCAL
(使能本地连接) 和CREAD
(使能接收) 并清空CSIZE
(数据位) - 步骤三:
设置c_cflag
中的数据位。
- 步骤四:
设置奇偶校验位。
奇验位:
cpp
newtio.c_cflag|= PARENB //使能奇偶校验
newtio.c_cflag|= PARODD; //使能奇校验
newtio.c_iflag|=(INPCK|ISTRIP);
ISTRIP 的作用是在串口接收数据时,自动剥离每个字符的最高位(第八位),以确保接收到的数据在传递给应用程序之前是正确的七位表示形式。
偶验位:
cpp
newtio.c_iflag =(INPCK|ISTRIP)
newtio.c_cflag|= PARENB;
newtio.c_cflag &= ~PARODD; //清零
break;
无校验:
cpp
newtio.c cflag &= ~PARENB;
-
步骤五:
设置波特率,使用函数 cfsetispeed,cfsetospeed。注意波特率前需要加 B,如 B9600
-
步骤六:
设置停止位,如设置 1 位停止位
cpp
newtio.c_cflag &= ~CSTOPB;
- 步骤七:
刷新输入队列:使用函数tcflush(fd,TCIFLUSH);
fd 是使用open函数打开文件得到的串口句柄
TCIFLUSH: 刷新输入队列
TCOFLUSH: 刷新输出队列
TCIOFLUSH:刷新输入输出队列
- 步骤八:
使用函数tcsetattr(fd,TCSANOW, &newtio);
设置配置
TCSANOW: 设置立刻生效
TCSADRIN:发送了所有输出以后设置才生效。
- 步骤九:
使用open
函数打开串口
cpp
open("串口节点", O_RDWR | O_NOCTTY | O_NDELAY);
O_RDWR: 读写权限
O_NOCTTY: 表示不占用终端,即这个程序不会成为这个串口的控制终端
O_NDELAY: 表示非阻塞
源Code
cpp
#include <stdio.h>
#include <stdlib.h>
#include <termios.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#define UART_NAME / dev / ttyS3
int set_uart(int fd, int speed, int bits, char check, int stop)
{
struct termios newtio, oldtio;
if (tcgetattr(fd, &oldtio) != 0)
{
printf("tcgetattr oldtio is error\n");
return -1;
}
bzero(&newtio, sizeof(newtio));
newtio.c_cflag |= CLOCAL | CREAD;
newtio.c_cflag &= ~CSIZE;
switch (bits)
{
case 7:
newtio.c_cflag |= CS7;
break;
case 8:
newtio.c_cflag |= CS8;
break;
}
switch (check)
{
case 'N':
newtio.c_cflag &= ~PARENB; // 失能
// newtio.c_iflag &= ~INPCK; // 不使用
break;
case 'E':
newtio.c_cflag |= PARENB; // 使能
newtio.c_cflag &= ~PARODD; // 使用偶校验
newtio.c_iflag |= (INPCK | ISTRIP); // 去除输入字符第八位
break;
case 'O':
newtio.c_cflag |= PARENB;
newtio.c_cflag |= PARODD;
newtio.c_iflag |= (INPCK | ISTRIP);
break;
}
switch (speed)
{
case 9600:
cfsetispeed(&newtio, B9600);
cfsetospeed(&newtio, B9600);
break;
case 115200:
cfsetispeed(&newtio, B115200);
cfsetospeed(&newtio, B115200);
break;
default:
break;
}
switch (stop)
{
case 1:
newtio.c_cflag &= ~CSTOPB;
break;
case 2:
newtio.c_cflag |= CSTOPB;
break;
}
tcflush(fd, TCIFLUSH);
if (tcsetattr(fd, TCSANOW, &newtio) != 0)
{
printf("tcsetattr newtio is error\n");
return -2;
}
return 0;
}
int main()
{
int fd;
char buf[128];
int count;
fd = open("/dev/ttyS3", O_RDWR | O_NOCTTY | O_NDELAY);
if (fd < 0)
{
printf("open error\n");
return -1;
}
set_uart(fd, 115200, 8, 'N', 1);
while (1)
{
memset(buf, 0, sizeof(buf));
count = read(fd, buf, sizeof(buf));
// printf("Having a receive\n");
buf[count] = '\0';
if (count > 0)
{
printf("read data is %s\n", buf);
}
}
return 0;
}
使用多线程方式进行读写
cpp
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <termios.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <pthread.h>
int uartOpen(const char *device, const int baud)
{
struct termios options;
speed_t myBaud;
int status, fd;
switch (baud)
{
case 9600:
myBaud = B9600;
break;
case 115200:
myBaud = B115200;
break;
default:
return -2;
}
fd = open(device, O_RDWR | O_NOCTTY | O_NDELAY);
if (fd == -1)
{
perror("open");
return -1;
}
fcntl(fd, F_SETFL, O_RDWR); // 设置串口阻塞办法
// 获取和修改当前选项
tcgetattr(fd, &options);
cfmakeraw(&options); // 将终端设置为原始模式8N1无流控
cfsetispeed(&options, myBaud); // 设置输入波特率
cfsetospeed(&options, myBaud); // 设置输出波特率
options.c_cflag |= (CLOCAL | CREAD);
options.c_cflag &= ~PARENB;
options.c_cflag &= ~CSTOPB; // 设置1位的停止位
options.c_cflag &= ~CSIZE; // 用数据位掩码清空设置
options.c_cflag |= CS8; // 设置8位的数据位
tcsetattr(fd, TCSANOW, &options); // 使上面新的设置生效
usleep(10000); // 10ms
return fd;
}
int uartfd;
/* 向串口发送数据的线程 */
void *sendDatas()
{
int cnt;
char *buffer = (char *)malloc(64);
while (1)
{
memset(buffer, '\0', sizeof(buffer));
printf("send -> ");
scanf("%s", buffer);
/*向串口1对应的设备文件写入buffer的数据*/
cnt = write(uartfd, buffer, strlen(buffer));
if (cnt < 0)
printf("Serial send datas error\n");
}
}
/* 读取串口数据的线程 */
void *recvDatas()
{
int cnt, readSize;
char *buffer = (char *)malloc(64);
while (1)
{
/* 判断串口是否有数据 */
if (ioctl(uartfd, FIONREAD, &cnt) == -1)
{
perror("ioctl");
return 0;
}
else
{
readSize = read(uartfd, buffer, cnt); // 读取数据到buffer中
if (readSize > 0)
printf("recv -> %s", buffer); // 读取成功再打印
}
memset(buffer, '\0', sizeof(buffer));
}
}
int main(int argc, char **argv)
{
pthread_t sendThread, recvThread;
uartfd = uartOpen("/dev/ttyS3", 115200);
if (uartfd == -1)
{
printf(" open error\n");
return -1;
}
else
{
printf("open succeed.\n");
}
/* 主函数中定义出两个线程用于接收和发送数据 */
pthread_create(&sendThread, NULL, sendDatas, NULL);
pthread_create(&recvThread, NULL, recvDatas, NULL);
/* 主线程每10秒发送心跳包 */
while (1)
{
char alive[] = "I am alive\r\n";
write(uartfd, alive, strlen(alive));
sleep(10);
}
return 0;
}