1.前言
通用异步收发传输器(Universal Asynchronous Receiver/Transmitter,通常称作UART) 是一种串行异步收发协议,应用十分广泛。UART工作原理是将数据的二进制位一位一位的进行传输,在UART通讯协议中信号线上的状态位高电平代表’1’,低电平代表’0’。
2.通信协议
UART帧格式如下:
- 空闲位:
UART协议规定,当总线处于空闲状态时信号线的状态为‘1’即高电平 - 起始位:
开始进行数据传输时发送方要先发出一个低电平’0’来表示传输字符的开始。因为空闲位一直是高电平所以开始第一次通讯时先发送一个明显区别于空闲状态的信号即为低电平。 - 数据位:
起始位之后就是要传输的数据,数据可以是5,6,7,8,9位,构成一个字符,一般都是8位。先发送最低位最后发送最高位。 - 奇偶校验位:
数据位传送完成后,要进行奇偶校验,校验位其实是调整个数,串口校验分几种方式:
1.无校验(no parity)
2.奇校验(odd parity):如果数据位中’1’的数目是偶数,则校验位为’1’,如果’1’的数目是奇数,校验位为’0’。
3.偶校验(even parity):如果数据为中’1’的数目是偶数,则校验位为’0’,如果为奇数,校验位为’1’。
4.mark parity:校验位始终为1
5.space parity:校验位始终为0 - 停止位:
数据结束标志,可以是1位,1.5位,2位的高电平。 - 波特率:
数据传输速率使用波特率来表示,单位bps(bits per second),常见的波特率9600bps,115200bps等等,其他标准的波特率是1200,2400,4800,19200,38400,57600。举个例子,如果串口波特率设置为9600bps,那么传输一个比特需要的时间是1/9600≈104.2us。
3.基本操作
在LINUX编程中需要通过UART通信,对其基本操作包括:打开UART设备、设置基本属性、读写IO、关闭UART设备。
针号 功能 缩写
1 数据载波检测 DCD
2 接收数据 RXD
3 发送数据 TXD
4 数据端准备 DTR
5 信号地 GND
6 数据设备准备好 DSR
7 请求发送 RTS
8 清除发送 CTS
9 振铃提示 DELL
其中RXD和TXD与GND比较常用,DTR和RTS这种在加有流控的串口设备上回使用
3.1 打开串口
fd = open("/dev/ttyS0", O_RDWR|O_NOCTTY|O_NDELAY);
- O_NOCTTY:表示程序不会成为这个端口上的“控制终端”,若不这样做的话,所有的输入会影响到你的进程,如键盘Ctrl+C中止信号可停止进程。
- O_NDELAY:表示程序并不关心DCD信号线的状态,即不关心端口另一端是否已经连接。
3.2 基本属性
设置串口属性包括基本的波特率,校验位,停止位等,其中最重要的一个结构体 struct termios ,每个选项都是16位数,包含了串口全部属性。
#include <termios.h>
struct termios{
tcflag_t c_iflag; //输入模式标志
tcflag_t c_oflag; //输出模式标志
tcflag_t c_cflag; //控制选项
tcflag_t c_lflag; //行选项
cc_t c_cc[NCCS]; //控制字符
};
(1) c_iflag:输入模式标志,控制终端输入方式
键 值 说 明
IGNBRK 忽略BREAK键输入
BRKINT 如果设置了IGNBRK,BREAK键输入将被忽略.如果设置了BRKINT,将产生SIGINT中断
IGNPAR 忽略奇偶校验错误
PARMRK 标识奇偶校验错误
INPCK 允许输入奇偶校验
ISTRIP 去除字符的第8个比特
INLCR 将输入的NL(换行)转换成CR(回车)
IGNCR 忽略输入的回车
ICRNL 将输入的回车转化成换行(如果IGNCR未设置的情况下)
IUCLC 将输入的大写字符转换成小写字符(非POSIX)
IXON 允许输入时对XON/XOFF流进行控制
IXANY 输入任何字符将重启停止的输出
IXOFF 允许输入时对XON/XOFF流进行控制
IMAXBEL 当输入队列满的时候开始响铃,Linux在使用该参数而是认为该参数总是已经设置
(2) c_oflag:输出模式标志,控制终端输出方式
键 值 说 明
OPOST 处理后输出
OLCUC 将输入的小写字符转换成大写字符(非POSIX)
ONLCR 将输入的NL(换行)转换成CR(回车)及NL(换行)
OCRNL 将输入的CR(回车)转换成NL(换行)
ONOCR 第一行不输出回车符
ONLRET 不输出回车符
OFILL 发送填充字符以延迟终端输出
OFDEL 以ASCII码DEL作为填充字符,如果未设置该参数,填充字符将是NUL('\0')(非POSIX)
NLDLY 换行输出延时,可以取NL0(不延迟)或NL1(延迟0.1s)
CRDLY 回车延迟,取值范围为:CR0、CR1、CR2和 CR3
TABDLY 水平制表符输出延迟,取值范围为:TAB0、TAB1、TAB2和TAB3
BSDLY 空格输出延迟,可以取BS0或BS1
VTDLY 垂直制表符输出延迟,可以取VT0或VT1
FFDLY 换页延迟,可以取FF0或FF1
(3) c_cflag:控制模式标志,指定终端硬件控制信息
键 值 说 明
CBAUD 波特率(4+1位)(非POSIX)
CBAUDEX 附加波特率(1位)(非POSIX)
CSIZE 字符长度,取值范围为CS5、CS6、CS7或CS8
CSTOPB 设置两个停止位
CREAD 使用接收器
PARENB 使用奇偶校验
PARODD 对输入使用奇偶校验,对输出使用偶校验
HUPCL 关闭设备时挂起
CLOCAL 忽略调制解调器线路状态
CRTSCTS 使用RTS/CTS流控制
(4) c_lflag:本地模式标志,控制终端编辑功能
键 值 说 明
ISIG 当输入INTR、QUIT、SUSP或DSUSP时,产生相应的信号
ICANON 使用标准输入模式
XCASE 在ICANON和XCASE同时设置的情况下,终端只使用大写。如果只设置了XCASE,则输入字符将被转换为小写字符,除非字符使用了转义字符(非POSIX,且Linux不支持该参数)
ECHO 显示输入字符
ECHOE 如果ICANON同时设置,ERASE将删除输入的字符,WERASE将删除输入的单词
ECHOK 如果ICANON同时设置,KILL将删除当前行
ECHONL 如果ICANON同时设置,即使ECHO没有设置依然显示换行符
ECHOPRT 如果ECHO和ICANON同时设置,将删除打印出的字符(非POSIX)
TOSTOP 向后台输出发送SIGTTOU信号
(5) c_cc[NCCS]:控制字符,用于保存终端驱动程序中的特殊字符
只有在本地模式标志c_lflag中设置了IEXITEN时,POSIX没有定义的控制字符才能在Linux中使用。每个控制字符都对应一个按键组合(^C和^H等)。 VMIN和VTIME这两个控制字符除外,它们不对应控制符。这两个控制字符只在原始模式下才有效。
键 值 说 明
c_cc[VMIN] 原始模式(非标准模式)读的最小字符数
c_cc[VTIME] 原始模式(非标准模式)读时的延时,以十分之一秒为单位
c_cc[VINTR] 默认对应的控制符是^C,作用是清空输入和输出队列的数据并且向tty设备的前台进程组中的每一个程序发送一个SIGINT信号,对SIGINT信号没有定义处理程序的进程会马上退出。
c_cc[VQUIT] 默认对应的控制符是^/,作用是清空输入和输出队列的数据并向tty设备的前台进程组中的每一个程序发送一个SIGQUIT信号,对SIGQUIT 信号没有定义处理程序的进程会马上退出。
c_cc[verase] 默认对应的控制符是^H或^?,作用是在标准模式下,删除本行前一个字符,该字符在原始模式下没有作用。
c_cc[VKILL] 默认对应的控制符是^U,在标准模式下,删除整行字符,该字符在原始模式下没有作用。
c_cc[VEOF] 默认对应的控制符是^D,在标准模式下,使用read()返回0,标志一个文件结束。
c_cc[VSTOP] 默认对应的控制字符是^S,作用是使用tty设备暂停输出直到接收到VSTART控制字符。或者,如果设备了IXANY,则等收到任何字符就开始输出。
c_cc[VSTART] 默认对应的控制字符是^Q,作用是重新开始被暂停的tty设备的输出。
c_cc[VSUSP] 默认对应的控制字符是^Z,使当前的前台进程接收到一个SIGTSTP信号。
c_cc[VEOL]
c_cc[VEOL2] 在标准模式下,这两个下标在行的末尾加上一个换行符('/n'),标志一个行的结束,从而使用缓冲区中的数据被发送,并开始新的一行。POSIX中没有定义VEOL2。
c_cc[VREPRINT] 默认对应的控制符是^R,在标准模式下,如果设置了本地模式标志ECHO,使用VERPRINT对应的控制符和换行符在本地显示,并且重新打印当前缓冲区中的字符。POSIX中没有定义VERPRINT。
c_cc[VWERASE] 默认对应的控制字符是^W,在标准模式下,删除缓冲区末端的所有空格符,然后删除与之相邻的非空格符,从而起到在一行中删除前一个单词的效果。 POSIX中没有定义VWERASE。
c_cc[VLNEXT] 默认对应的控制符是^V,作用是让下一个字符原封不动地进入缓冲区。如果要让^V字符进入缓冲区,需要按两下^V。POSIX中没有定义 VLNEXT。
3.3 操作函数
(1)获取属性
int tcgetattr(int fd, struct termios *termios_p);
一般的在设置属性之前先读出原来的串口信息保存前,因为有些参数我们不用修改使用原值就行。
(2)设置属性
int tcsetattr(int fd, int optional_actions,const struct termios *termios_p);
功能:设置与终端相关的参数 (除非需要底层支持却无法满足),使用termios_p 引用的termios 结构。optional_actions指定了什么时候改变会起作用,具体见下。
- TCSANOW:改变立即发生
- TCSADRAIN:改变在所有写入fd 的输出都被传输后生效。这个函数应当用于修改影响输出的参数时使用(当前输出完成时将值改变)
- TCSAFLUSH :改变在所有写入fd 引用的对象的输出都被传输后生效,所有已接受但未读入的输入都在改变发生前丢弃(同TCSADRAIN,但会舍弃当前所有值)
(3)等待所有输出都被传输
int tcdrain(int fd);
(4)刷新IO
int tcflush(int fd, int queue_selector);
功能:丢弃要写入引用的对象,对象由queue_selector 选择:
- TCIFLUSH:刷新收到的数据但是不读
- TCOFLUSH:刷新写入的数据但是不传送
- TCIOFLUSH:同时刷新收到的数据但是不读,并且刷新写入的数据但是不传送
(5)获取输入速度
speed_t cfgetispeed(const struct termios *termios_p);
(6)获取输出速度
speed_t cfgetospeed(const struct termios *termios_p);
3.4 关闭串口
close(fd);
4 代码演示
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <termios.h>
#include <errno.h>
int set_opt(int,int,int,char,int);
int uart_send(int fd,void *buf, int len);
int uart_recv_timeout(void *buf, int len, int timeout_ms);
void main()
{
int fd,ret,i=10;
char *uart3 = "/dev/ttySAC3";
char *buffer = "hello world!\n";
printf("\r\nitop4412 uart3 writetest start\r\n");
if((fd = open(uart3, O_RDWR | O_NOCTTY | O_NDELAY)) < 0)
{
printf("open %s is failed",uart3);
}
else
{
printf("open %s is success\n",uart3);
set_opt(fd, 115200, 8, 'N', 1);
while(i--)
{
ret = uart_send(fd,buffer, strlen(buffer));
if(ret < 0)
printf("write failed\n");
else
{
printf("wr_static is %d\n",ret);
}
sleep(1);
}
}
close(fd);
}
int set_opt(int fd,int nSpeed, int nBits, char nEvent, int nStop)
{
struct termios newtio,oldtio;
/*获取原有串口配置*/
if ( tcgetattr( fd,&oldtio) != 0) {
perror("SetupSerial 1");
return -1;
}
memset( &newtio, 0, sizeof(newtio) );
/*CREAD 开启串行数据接收,CLOCAL并打开本地连接模式*/
newtio.c_cflag |= CLOCAL | CREAD;
/*设置数据位*/
newtio.c_cflag &= ~CSIZE;
switch( nBits )
{
case 7:
newtio.c_cflag |= CS7;
break;
case 8:
newtio.c_cflag |= CS8;
break;
}
/* 设置奇偶校验位 */
switch( nEvent )
{
case 'O':
newtio.c_cflag |= PARENB;
newtio.c_cflag |= PARODD;
newtio.c_iflag |= (INPCK | ISTRIP);
break;
case 'E':
newtio.c_iflag |= (INPCK | ISTRIP);
newtio.c_cflag |= PARENB;
newtio.c_cflag &= ~PARODD;
break;
case 'N':
newtio.c_cflag &= ~PARENB;
break;
}
/* 设置波特率 */
switch( nSpeed )
{
case 2400:
cfsetispeed(&newtio, B2400);
cfsetospeed(&newtio, B2400);
break;
case 4800:
cfsetispeed(&newtio, B4800);
cfsetospeed(&newtio, B4800);
break;
case 9600:
cfsetispeed(&newtio, B9600);
cfsetospeed(&newtio, B9600);
break;
case 115200:
cfsetispeed(&newtio, B115200);
cfsetospeed(&newtio, B115200);
break;
case 460800:
cfsetispeed(&newtio, B460800);
cfsetospeed(&newtio, B460800);
break;
default:
cfsetispeed(&newtio, B9600);
cfsetospeed(&newtio, B9600);
break;
}
/*设置停止位*/
if( nStop == 1 )/*设置停止位;若停止位为1,则清除CSTOPB,若停止位为2,则激活CSTOPB*/
newtio.c_cflag &= ~CSTOPB;/*默认为一位停止位; */
else if ( nStop == 2 )
newtio.c_cflag |= CSTOPB;
/*设置最少字符和等待时间,对于接收字符和等待时间没有特别的要求时*/
newtio.c_cc[VTIME] = 0;/*非规范模式读取时的超时时间;*/
newtio.c_cc[VMIN] = 0;/*非规范模式读取时的最小字符数*/
/*tcflush清空终端未完成的输入/输出请求及数据;TCIFLUSH表示清空正收到的数据,且不读取出来 */
tcflush(fd,TCIFLUSH);
if((tcsetattr(fd,TCSANOW,&newtio))!=0)
{
perror("com set error");
return -1;
}
// printf("set done!\n\r");
return 0;
}
int uart_send(int fd,void *buf, int len)
{
int ret = 0;
int count = 0;
tcflush(fd, TCIFLUSH);
while (len > 0)
{
ret = write(fd, (char*)buf + count, len);
if (ret < 1)
{
break;
}
count += ret;
len = len - ret;
}
return count;
}
int uart_recv_timeout(void *buf, int len, int timeout_ms)
{
int ret;
size_t rsum = 0;
ret = 0;
fd_set rset;
struct timeval t;
while (rsum < len)
{
t.tv_sec = timeout_ms/1000;
t.tv_usec = (timeout_ms - t.tv_sec*1000)*1000;
FD_ZERO(&rset);
FD_SET(uart_fd, &rset);
ret = select(uart_fd+1, &rset, NULL, NULL, timeout);
if (ret <= 0) {
if (ret == 0)
{
//timeout
return -1;
}
if (errno == EINTR)
{
// 信号中断
continue;
}
return -errno;
}
else
{
ret = read(uart_fd, (char *)buf + rsum, len - rsum);
if (ret < 0)
{
return ret;
}
else
{
rsum += ret;
}
}
}
return rsum;
}