按照对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中也有与校验相关的位。如下表:

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  */