按照对linux系统的理解,串口编程的顺序无非就是open,read,write,close,而串口有波特率、数据位等重要参数需要设置,因此还应该用到设置函数,那么接下来就带着这几个问题去学习linux下的串口编程。
1、open
linux串口编程其实也是文件编程,首先要用open函数打开串口设备,获得文件描述符,open函数的简介参照:http://blog.sina.com.cn/s/blog_54f82cc201010oow.html
首先需要关心的是需要打开的文件名,它肯定是/dev路径下的某个设备,串口设备一般叫做ttyS*,ttySAC*,ttyUSB*,我以前就知道这些,后来发现竟然还有叫ttyO*的(当时调试的时候看见/dev目录下有ttyS*的,我就不假思索的,理所当然的代码中写的是ttyS*,结果程序运行时这玩意还成功打开了,但设置的时候就出问题,万万没想到啊,当时用的那个平台ttyS*不是串口,相同目录下还有叫ttyO*的文件,那才是串口)。
还有i.MX6UL上面的串口是ttymxc*
串口设备明显是可读写的,因此传入的第二个参数为O_RDWR。
因此打开串口1的操作为:
fd = open("/dev/ttySAC1", O_RDWR);
如果得到的fd不等于-1则表示成功打开串口设备了。
我在使用过程中遇到过串口无法打开的问题,open返回值为-1。查阅资料后使用命令:sudo chmod 666 /dev/ttyUSB0 修改串口设备权限后就能成功打开了。
open函数传入的第二个参数一般会用到O_NOCTTY这个标志,它表示阻止操作系统将打开的文件指定为进程的控制终端,如果没有指定这个标志,那么任何一个输入都将会影响用户的进程。我不知道它具体是怎么影响的,但是最好还是加上这个标志。
此外O_NONBLOCK这个标志也比较常用,它表示以非阻塞模式打开文件,当调用read的时候,如果没有数据也会立即返回-1。有些人会用O_NDELAY这个标志,关于这个标志的解释网上就有很多说法了,我看到就有三种说法:
1、与O_NONBLOCK一样也是以非阻塞模式打开,但如果没有读取到数据,O_NDELAY返回的是0,而O_NONBLOCK返回的是-1,并且会设置errno为EAGAIN。
2、O_NDELAY表示这个程序不关心DCD信号线所处的状态,端口的另一端是否激活或者停止。如果用户不指定了这个标志,则进程将会一直处在睡眠状态,直到DCD信号线被激活。
3、与O_NONBLOCK等价。
为了探究答案,我在一份linux3.14.38的源代码中搜索,发现这两个标志的值与平台相关。我在Ubuntu中直接用printf打印出来两个标志的值是完全一样的,并且read函数返回的值为都是-1,并不是第一条说的那样O_NDELAY返回的是0。
我暂时也没有更多的平台去验证,暂且认为网上的那些说法都是基于作者自己正在使用的平台上说的,而在我使用的Ubuntu中O_NDELAY与O_NONBLOCK是完全相同的。
所以打开串口的操作应为下列2句中的一句:
fd = open("/dev/ttySAC1", O_RDWR | O_NOCTTY);
fd = open("/dev/ttySAC1", O_RDWR | O_NOCTTY | O_NONBLOCK);
2、串口设置
打开串口设备之后还需要对串口进行设置。使用tcsetattr函数设置串口,函数原型为:
int tcsetattr(int fd, int optional_actions, const struct termios *termios_p);
成功返回0,失败返回-1。
第一个参数fd表示打开的串口文件描述符。
第二个参数optional_actions用于控制修改起作用的时间。可以取下列值:
TCSANOW:不等数据传输完毕就立即改变属性。
TCSADRAIN:等待所有数据传输结束才改变属性。
TCSAFLUSH:清空输入输出缓冲区才改变属性。
第三个参数struct termios中包含了串口属性,struct termios定义为:
1 #define NCCS 19
2 struct termios {
3 tcflag_t c_iflag; /* input mode flags */
4 tcflag_t c_oflag; /* output mode flags */
5 tcflag_t c_cflag; /* control mode flags */
6 tcflag_t c_lflag; /* local mode flags */
7 cc_t c_line; /* line discipline */
8 cc_t c_cc[NCCS]; /* control characters */
9 };
看到这种不认识的数据类型就头大,实际上tcflag_t类型就是unsigned int,而cc_t类型就是unsigned char。
由于struct termios里面的成员太多,因此使用tcgetattr函数先获取到原来的属性,然后再修改我们关心的属性。
调用tcgetattr函数获取属性,tcgetattr函数原型为:
int tcgetattr(int fd, struct termios *termios_p);
成功返回0,失败返回-1。
一般来说串口需要关心的属性为:波特率、数据位、校验位、停止位、流控。下面逐一说明这些数据在哪里改。
波特率:
与波特率相关的成员为c_cflag,其中键值(域)CBAUD表示波特率,该域中的位不同的组合表示不同的波特率,表示波特率也用的也是宏,支持的宏及对应的值如下如下:
B0 <==> 0x0000
B50 <==> 0x0001
B75 <==> 0x0002
B110 <==> 0x0003
B134 <==> 0x0004
B150 <==> 0x0005
B200 <==> 0x0006
B300 <==> 0x0007
B600 <==> 0x0008
B1200 <==> 0x0009
B1800 <==> 0x000a
B2400 <==> 0x000b
B4800 <==> 0x000c
B9600 <==> 0x000d
B19200 <==> 0x000e
B38400 <==> 0x000f
B57600 <==> 0x1001
B115200 <==> 0x1002
B230400 <==> 0x1003
比如将波特率修改为115200的代码就可以这样写:
1 tcgetattr(fd, &termios_uart);
2 termios_uart.c_cflag &= ~CBAUD;
3 termios_uart.c_cflag |= B115200;
4 tcsetattr(fd, TCSANOW, &termios_uart);
在linux中提供了专门设置波特率的函数,用cfsetispeed和cfsetospeed设置输入输出波特率,还有一个cfsetspeed函数,它们的函数原型为:
int cfsetispeed(struct termios *termios_p, speed_t speed);
int cfsetospeed(struct termios *termios_p, speed_t speed);
int cfsetspeed(struct termios *termios_p, speed_t speed);
其speed_t类型其实就是unsigned int类型,其取值也正是CBAUD域中可以选择的数据。
那么设置波特率的代码就变成了这样:
1 tcgetattr(fd, &termios_uart);
2 cfsetspeed(&termios_uart, B115200);
3 tcsetattr(fd, TCSANOW, &termios_uart);
但是看到这里会有疑问,c_cflag成员中没有将输入波特率和输出波特率分开,这里为什么会有几个不同的设置波特率的函数,这个问题在另一篇博客中仔细探究。
数据位:
与数据位相关的成员为c_cflag,其中键值(域)CSIZE表示数据位,与波特率的设置一样,该域中的位不同的组合表示不同的数据位,也可以用宏来表示,支持的宏如下:
CS5,CS6,CS7,CS8,分别表示数据位为5位、6位、7位、8位。
将数据位设置为8位就可以这样写:
1 tcgetattr(fd, &termios_uart);
2 termios_uart.c_cflag &= ~CSIZE;
3 termios_uart.c_cflag |= CS8;
4 tcsetattr(fd, TCSANOW, &termios_uart);
校验位:
c_cflag中键值PARENB置1表示使用奇偶校验,否则表示不使用校验,在PARENB置1的前提下,键值 PARODD置1表示使用奇校验,否则使用偶校验。
另外在c_iflag中也有与校验相关的位。如下表:
IGNPAR | Ignore framing errors and parity errors. |
PARMRK | If IGNPAR is not set, prefix a character with a parity error or framing error with \377 \0. If neither IGNPAR nor PARMRK is set, read a character with a par‐ ity error or framing error as \0. |
INPCK | Enable input parity checking. |
ISTRIP | Strip off eighth bit. |
停止位:
c_cflag中键值CSTOPB置1表示使用2个停止位,否则表示使用1个停止位。
流控:
c_cflag中键值CRTSCTS置1表示使用硬件流控,否则表示不使用硬件流控。
软件流控则在c_iflag成员中定义,c_iflag成员中和软件流控相关的键值及解释如下:
IXON | Enable software flow control (outgoing) |
IXOFF | Enable software flow control (incoming) |
IXANY | Allow any character to start flow again |
所谓流控指的就是控制数据流的流量,如果当接收端接收不了那么多数据时,发送端还在不断的发数据,这样就会出问题。
硬件流控就是比如A给B发送数据,他们之间除了一根数据线连接以外还有一根流控线,当B将流控线拉高时A才能像B发送数据,当B将流控线拉低时,A就不能发送数据了。
软件流控没有专门的流控线,如果串口是全双工的,但是如果当 A给B发送数据的时候B不会给A发送数据,那么B就可以将这根数据线用来发送流控数据,当B想要接受数据时,B给A发送IXON,当B不想接受数据时,B给A发送IXOFF。
理论上来说硬件流控与软件流控可以同时使用,但一般没人会这么做。
其他设置:
你以为这样就完了吗,这只是按照裸机开发的思维方式的操作步骤,但这是在linux操作系统上的东西,而且struct termios结构中有这么多成员,这些成员肯定都是有用的啊。
如果这些东西不设置,程序也许能正常运行,那是因为通过tcgetattr函数获取到了一个默认值,而且这个值是会保存的,下次获取到的值是上次设置的值(无论文件是否关闭,程序是否退出),因此写一份比较健全的代码就至关重要了。
这里举两个例子说明一下其他参数的重要性,如c_lflag中有一个ECHO标志,该标志置位时串口会将收到的数据回传回去,表现就是你给他发什么东西它就会给你回什么东西。
另外还有一个函数比较重要,那就是tcflush,tcflush函数原型为:int tcflush(int fd, int queue_selector); queue_selector的取值和意义如下:
TCIFLUSH | 刷新数据接收但不读 |
TCOFLUSH | 刷新数据写入但不传输 |
TCIOFLUSH | 刷新数据接收但不读同时刷新数据写入但不传输 |
直接翻译过来非常拗口,理解一下就是无论是即将要发送的数据还是即将要接收的数据,都是放在缓冲区里的,TCIFLUSH意思就是数据已经放在缓冲区里了,但是还没有调用read函数去读,那么直接清空读缓冲区,调用read的时候也接收不到数据。TCOFLUSH的意思就是如果调用了write函数,但是实际上数据还没有开始往外发,那么清空写缓冲区,数据也不发了。
https://www.cnblogs.com/dartagnan/archive/2013/04/25/3042417.html这篇博客中将里面的成员讲解的非常详细。
测试程序代码:
1 /* 2 * filename: uart.c 3 * author: Suzkfly 4 * date: 2021-01-16 5 * platform: Ubuntu 6 * 将USB转串口连接至Ubuntu中,运行程序,能打印串口接收到的数据,也能从终端中获取 7 * 数据发送出去。 8 * 如果不能成功打开设备,使用则ls /dev/ttyUSB0命令看设备是否存在,若存在,则使用 9 * sudo chmod 666 /dev/ttyUSB0 修改文件权限后重新运行程序。 10 */ 11 12 #include <sys/types.h> 13 #include <sys/stat.h> 14 #include <fcntl.h> 15 #include <termios.h> 16 #include <unistd.h> 17 #include <stdio.h> 18 #include <string.h> 19 20 /** 21 * \brief define 22 * @{ 23 */ 24 #define UART_DEV_PATH "/dev/ttyUSB0" /**\brief< 定义打开的串口设备路径 */ 25 26 /** 27 * @} 28 */ 29 30 /** 31 * \brief 打开串口设备 32 * 33 * \param[in] p_path:设备路径 34 * 35 * \retval 成功返回文件描述符,失败返回-1 36 */ 37 int uart_open(const char *p_path) 38 { 39 /* O_NOCTTY:阻止操作系统将打开的文件指定为进程的控制终端,如果没有指定这个标 40 志,那么任何一个输入都将会影响用户的进程。 */ 41 /* O_NONBLOCK:使I/O变成非阻塞模式,调用read如果没有接收到数据则立马返回-1, 42 并且设置errno为EAGAIN。*/ 43 /* O_NDELAY: 与O_NONBLOCK完全相同 */ 44 return open(p_path, O_RDWR | O_NOCTTY); 45 } 46 47 /** 48 * \brief 测试函数,打印struct termios各成员值 49 */ 50 static void __print_termios(struct termios *p_termios) 51 { 52 printf("c_iflag = %#010x\n", p_termios->c_iflag); 53 printf("c_oflag = %#010x\n", p_termios->c_oflag); 54 printf("c_cflag = %#010x\n", p_termios->c_cflag); 55 printf("c_lflag = %#010x\n\n", p_termios->c_lflag); 56 } 57 58 /** 59 * \brief 设置串口属性 60 * 61 * \param[in] fd:打开的串口设备的文件描述符 62 * \param[in] baudrate:波特率 63 * #{0, 50, 75, 110, 134, 150, 200, 300, 600, 1200, 1800, 64 * 2400, 4800, 9600, 19200, 38400, 57600, 115200, 230400} 65 * \param[in] bits:数据位 66 * #{5, 6, 7, 8} 67 * \param[in] parity:校验 68 * #'n'/'N':无校验 69 * #'o'/'O':奇校验 70 * #'e','E':偶校验 71 * \param[in] stop:停止位 72 * #1:1个停止位 73 * #2:2个停止位 74 * \param[in] flow:流控 75 * #'n'/'N':不使用流控 76 * #'h'/'H':使用硬件流控 77 * #'s'/'S':使用软件流控 78 * 79 * \retval 成功返回0,失败返回-1,并打印失败原因 80 * 81 * \note 虽然波特率设置支持这么多值,但并不代表输入表中支持的值波特率就 82 * 一定能设置成功。 83 */ 84 int uart_set(int fd, int baudrate, int bits, char parity, int stop, char flow) 85 { 86 struct termios termios_uart; 87 int ret = 0; 88 speed_t uart_speed = 0; 89 90 /* 获取串口属性 */ 91 memset(&termios_uart, 0, sizeof(termios_uart)); 92 ret = tcgetattr(fd, &termios_uart); 93 if (ret == -1) { 94 printf("tcgetattr failed\n"); 95 return -1; 96 } 97 98 //__print_termios(&termios_uart); 99 100 /* 设置波特率 */ 101 switch (baudrate) { 102 case 0: uart_speed = B0; break; 103 case 50: uart_speed = B50; break; 104 case 75: uart_speed = B75; break; 105 case 110: uart_speed = B110; break; 106 case 134: uart_speed = B134; break; 107 case 150: uart_speed = B150; break; 108 case 200: uart_speed = B200; break; 109 case 300: uart_speed = B300; break; 110 case 600: uart_speed = B600; break; 111 case 1200: uart_speed = B1200; break; 112 case 1800: uart_speed = B1800; break; 113 case 2400: uart_speed = B2400; break; 114 case 4800: uart_speed = B4800; break; 115 case 9600: uart_speed = B9600; break; 116 case 19200: uart_speed = B19200; break; 117 case 38400: uart_speed = B38400; break; 118 case 57600: uart_speed = B57600; break; 119 case 115200: uart_speed = B115200; break; 120 case 230400: uart_speed = B230400; break; 121 default: printf("Baud rate not supported\n"); return -1; 122 } 123 cfsetspeed(&termios_uart, uart_speed); 124 125 /* 设置数据位 */ 126 switch (bits) { 127 case 5: /* 数据位5 */ 128 termios_uart.c_cflag &= ~CSIZE; 129 termios_uart.c_cflag |= CS5; 130 break; 131 132 case 6: /* 数据位6 */ 133 termios_uart.c_cflag &= ~CSIZE; 134 termios_uart.c_cflag |= CS6; 135 break; 136 137 case 7: /* 数据位7 */ 138 termios_uart.c_cflag &= ~CSIZE; 139 termios_uart.c_cflag |= CS7; 140 break; 141 142 case 8: /* 数据位8 */ 143 termios_uart.c_cflag &= ~CSIZE; 144 termios_uart.c_cflag |= CS8; 145 break; 146 147 default: 148 printf("Data bits not supported\n"); 149 return -1; 150 } 151 152 /* 设置校验位 */ 153 switch (parity) { 154 case 'n': /* 无校验 */ 155 case 'N': 156 termios_uart.c_cflag &= ~PARENB; 157 termios_uart.c_iflag &= ~INPCK; /* 禁能输入奇偶校验 */ 158 break; 159 160 case 'o': /* 奇校验 */ 161 case 'O': 162 termios_uart.c_cflag |= PARENB; 163 termios_uart.c_cflag |= PARODD; 164 termios_uart.c_iflag |= INPCK; /* 使能输入奇偶校验 */ 165 //termios_uart.c_iflag |= ISTRIP; /* 除去第八个位(奇偶校验位),如果这么写的话,最高位永远为0 */ 166 termios_uart.c_iflag &= ~ISTRIP; 167 break; 168 169 case 'e': /* 偶校验 */ 170 case 'E': 171 termios_uart.c_cflag |= PARENB; 172 termios_uart.c_cflag &= ~PARODD; 173 termios_uart.c_iflag |= INPCK; /* 使能输入奇偶校验 */ 174 //termios_uart.c_iflag |= ISTRIP; /* 除去第八个位(奇偶校验位),如果这么写的话,最高位永远为0 */ 175 termios_uart.c_iflag &= ~ISTRIP; 176 break; 177 178 default: 179 printf("Parity not supported\n"); 180 return -1; 181 } 182 183 /* 设置停止位 */ 184 switch (stop) { 185 case 1: termios_uart.c_cflag &= ~CSTOPB; break; /* 1个停止位 */ 186 case 2: termios_uart.c_cflag |= CSTOPB; break; /* 2个停止位 */ 187 default: printf("Stop bits not supported\n"); 188 } 189 190 /* 设置流控 */ 191 switch (flow) { 192 case 'n': 193 case 'N': /* 无流控 */ 194 termios_uart.c_cflag &= ~CRTSCTS; 195 termios_uart.c_iflag &= ~(IXON | IXOFF | IXANY); 196 break; 197 198 case 'h': 199 case 'H': /* 硬件流控 */ 200 termios_uart.c_cflag |= CRTSCTS; 201 termios_uart.c_iflag &= ~(IXON | IXOFF | IXANY); 202 break; 203 204 case 's': 205 case 'S': /* 软件流控 */ 206 termios_uart.c_cflag &= ~CRTSCTS; 207 termios_uart.c_iflag |= (IXON | IXOFF | IXANY); 208 break; 209 210 default: 211 printf("Flow control parameter error\n"); 212 return -1; 213 } 214 215 /* 其他设置 */ 216 termios_uart.c_cflag |= CLOCAL; /* 忽略modem(调制解调器)控制线 */ 217 termios_uart.c_cflag |= CREAD; /* 使能接收 */ 218 219 termios_uart.c_iflag &= ~ICRNL; /* 禁止将输入的CR转换为NL,如果不加这一行的话,串口接收到的0D会变成0A */ 220 221 /* 禁能执行定义(implementation-defined)输出处理,意思就是输出的某些特殊数 222 据会作特殊处理,如果禁能的话那么就按原始数据输出 */ 223 termios_uart.c_oflag &= ~OPOST; 224 225 /** 226 * 设置本地模式位原始模式 227 * ICANON:规范输入模式,如果设置了那么退格等特殊字符会产生实际动作 228 * ECHO:则将输入字符回送到终端设备 229 * ECHOE:如果ICANON也设置了,那么收到ERASE字符后会从显示字符中擦除一个字符 230 * 通俗点理解就是收到退格键后显示内容会往回删一个字符 231 * ISIG:使终端产生的信号起作用。(比如按ctrl+c可以使程序退出) 232 */ 233 termios_uart.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG); 234 235 /** 236 * 设置等待时间和最小接收字符 237 * 这两个值只有在阻塞模式下有意义,也就是说open的时候不能传入O_NONBLOCK, 238 * 如果经过了c_cc[VTIME]这么长时间,缓冲区内有数据,但是还没达到c_cc[VMIN]个 239 * 数据,read也会返回。而如果当缓冲区内有了c_cc[VMIN]个数据时,无论等待时间 240 * 是否到了c_cc[VTIME],read都会返回,但返回值可能比c_cc[VMIN]还大。如果将 241 * c_cc[VMIN]的值设置为0,那么当经过c_cc[VTIME]时间后read也会返回,返回值 242 * 为0。如果将c_cc[VTIME]和c_cc[VMIN]都设置为0,那么程序运行的效果与设置 243 * O_NONBLOCK类似,不同的是如果设置了O_NONBLOCK,那么在没有数据时read返回-1, 244 * 而如果没有设置O_NONBLOCK,那么在没有数据时read返回的是0。 245 */ 246 termios_uart.c_cc[VTIME] = 1; /* 设置等待时间,单位1/10秒 */ 247 termios_uart.c_cc[VMIN] = 1; /* 最少读取一个字符 */ 248 249 tcflush(fd, TCIFLUSH); /* 清空读缓冲区 */ 250 251 /* 写入配置 */ 252 ret = tcsetattr(fd, TCSANOW, &termios_uart); 253 if (ret == -1) { 254 printf("tcsetattr failed\n"); 255 } 256 257 return ret; 258 } 259 260 261 /** 262 * \test 263 * @{ 264 */ 265 int main(int argc, const char *argv[]) 266 { 267 int fd = 0; 268 int ret = 0; 269 int pid = 0; 270 char buf[128] = { 0 }; 271 int len = 0; 272 int i = 0; 273 274 /* 打开串口设备 */ 275 fd = uart_open(UART_DEV_PATH); 276 if (fd < 0) { 277 printf("open %s failed\n", UART_DEV_PATH); 278 return 0; 279 } 280 281 /** 282 * 配置串口: 283 * 波特率:9600 284 * 数据位:8 285 * 校验 :无校验 286 * 停止位:1 287 * 流控 :无流控 288 */ 289 ret = uart_set(fd, 9600, 8, 'n', 1, 'n'); 290 if (ret == -1) { 291 return 0; 292 } 293 294 pid = fork(); 295 if (pid > 0) { /* 读数据 */ 296 while (1) { 297 memset(buf, 0, sizeof(buf)); 298 len = read(fd, buf, sizeof(buf)); 299 if (len > 0) { 300 printf("len: %d\n", len); 301 printf("data: "); 302 #if 0 /* 十六进制打印 */ 303 for (i = 0; i < len; i++) { 304 printf("%02x ", buf[i]); 305 } 306 printf("\n\n"); 307 #else /* 字符串打印 */ 308 printf("%s\n\n", buf); 309 #endif 310 } else { 311 printf("len = %d\n", len); 312 } 313 } 314 } else if (pid == 0) { /* 写数据 */ 315 while (1) { 316 scanf("%s", buf); 317 write(fd, buf, strlen(buf)); 318 } 319 } else { 320 printf("fork failed\n"); 321 } 322 323 return 0; 324 } 325 326 /** 327 * @} 328 */