一些背景
在大部分讲解 Linux 编程书籍的时候会发现没有单独的串口编程章节,实际上串口编程已经被概括在了“终端”或者“终端IO”章节里面。在系统中会经常出现的几个容易混淆的概念:tty,串口,控制台与驱动程序。后面会在实际使用过程中对几种设备的原理与使用进行详解。
在系统下面通过执行 "ls /dev" 或者 "cat /proc/tty/drivers" 可以看到经常碰到的一些术语以及分类,如下所示:
对开发者而言,比较熟悉的有 console 控制台、tty 终端、ttyS serial串口设备、pty 伪终端等。由于 pty 成对使用,所以又细分为了主从两类。这些设备类对应的系统设备文件名参见第二列,可以输入 "ls /dev" 进行查看。
需要理清这些概念的关系就需要追溯早起计算机的使用历史,最初计算机成本高昂,通常需要连接多套键盘显示器供多人使用,因此就出现了这样一种专门连接计算机的设备,它只有显示器和键盘,外加简单处理电路。用户可以通过这套设备连接到计算机上(通常是通过串口连接),然后登录系统,并对计算机进行操作。这样一台只有输入、显示器件并能连接到计算机的设备就称为终端。tty 设备的名称是从过去的电传打字机(Teletype)缩写而来,也是最早出现的一种终端设备,因此现在在 Linux 系统中,就用 tty 来表示 “终端”。而 console 控制台,pty 伪终端等可以理解为虚拟 tty。总之,在 Unix 系统中 tty 就可以理解为连接到系统的物理或者虚拟终端。
“console”控制台用于用户和系统进行交互的设备,与终端作用类似。该虚拟 tty 与普通终端相比,多了一些功能:如显示系统内核消息,后台服务日志等。从硬件上看,控制台与终端等都是具备输入显示功能的设备,没有区别。实际上他们表达的意思相同。控制台与终端的区别体现在软件上,在启动 Linux 内核前传入的命令行参数 "console=..." 就是用来指定具体的控制台。控制台在 tty 驱动初始化之前就可以使用了,最开始被用来显示内核消息。我们在计算机或者嵌入式系统中经常会看到 "console = ttySAC0"、"console = ttyS1" 等语句,实际就是选取某个虚拟或者物理终端作为控制台与用户交互。
当 tty 驱动初始化结束,用户程序就可以通过 tty 驱动的接口来操作各类终端设备,包括控制台。而后面要介绍的应用程序操作接口也由此而来。之前内容讲了终端,tty,控制台等概念以及区别,因此在串口编程相关章节中提及串口,有时也会用终端,tty等来替代,注意它们实际上所指是相同的。
在 Linux 中可以通过一组函数调用(通用终端接口,简称GTI)来控制终端,这组函数调用与用于读写数据的函数是分离的,这就使得读写数据的接口非常简洁,同时又允许可以对终端或串口的行为进行更精细地控制。但由于需要支持大量不同类型的硬件,GTI 中实现的 IO 接口却不简洁。
详解 termios
termios 是在 POSIX 规范中定义的标准接口,它类似与 System V 中的 termio 接口。通过设置 termios 类型的数据结构中的值和使用一小组函数调用,就可以对终端接口进行控制。termios 的结构体定义以及相关函数调用参见 termios.h 头文件。termios 结构的定义如下:
如定义所示,影响终端的参数按照不同模式分成如下几类:
输入模式
输出模式
控制模式
本地模式
线路规程
特殊控制字符
输入速率
输出速率
输入模式
输入模式控制输入数据(终端驱动程序从串行口或键盘接收到的字符)在被传递给程序之前的处理方式。通过设置 termios 结构中 c_iflag 成员的标志对它们进行控制。所有的标志都被定义为宏,这也是所有终端模式都采用的方法。可用于 c_iflag 成员的宏如下所示:
BRKINT:当在输入行中检测到一个终止状态(连接丢失)时,产生一个中断。
IGNBRK:忽略输入行中的终止状态。
ICRNL:将接收到的回车符转换为新行符。
IGNCR:忽略接收到的回车符。
INLCR:将接收到的新行符转换为回车符。
IGNPAR:忽略奇偶校验错误的字符。
INPCK:对接收到的字符执行奇偶校验。
PARMRK:对奇偶校验错误做出标记。
ISTRIP:将所有接收到的字符裁剪为 7 比特位。
IXOFF:对输入启动软件流控。
IXON:对输出启动软件流控。
如果 BRKINT 和 IGNBRK 标志都未被设置,则输入行中的终止状态就被读取为 NULL 字符。
输出模式
输出模式控制输出字符的处理方式,即由程序发送出去的字符在传递到串行口或屏幕之前是如何处理的。可用于 c_oflag 成员的宏如下所示:
OPOST:打开输出处理功能。
ONLCR:将输出中的换行符转换为回车/换行符。
OCRNL:将输出中的回车符转换为新行符。
ONOCR:在第0列不输出回车符。
ONLRET:不输出回车符。
OFILL:发送填充字符以提供延时。
OFDEL:用DEL而不是NULL字符作为填充字符。
NLDLY:新行符延时选择。
CRDLY:回车符延时选择。
TABDLY:制表符延时选择。
BSDLY:退格符延时选择。
VTDLY:垂直制表符延时选择。
FFDLY:换页符延时选择。
如果没有设置 OPOST,其他标志都被忽略,输出模式使用频率较小。
控制模式
控制模式控制终端的硬件特性。通过设置 termios 结构中 c_cflag 标志对控制模式进行配置。可用于 c_cflag 成员宏如下所示:
CLOCAL:忽略所有调制解调器的状态行。
CREAD:启动字符接收器。
CS5:发收采用5位数据位。
CS6:发收采用6位数据位。
CS7:发收采用7位数据位。
CS8:发收采用8位数据位。
CSTOPB:字符采用两位停止位。
HUPCL:关闭时挂断调制解调器。
PARENB:使能奇偶校验。
PARODD:使用奇校验。
若设置了 HUPCL,当终端驱动程序检测到与终端对应的最后一个文件描述符被关闭时,它将通过设置调制解调器控制线来挂断线路。控制模式主要用于串行线连接的物理模型中,是在串口编程中十分重要的标志。
本地模式
本地模式控制终端的各种特性。通过设置 termios 结构中 c_lflag 标志对本地模式进行配置。可用于 c_lflag 成员宏如下所示:
ECHO:启用输入字符的本地回显功能。
ECHOE:接收到 ERASE 时执行退格、空格、退格的动作组合。
ECHOK:接收到 KILL 字符时执行行删除操作。
ECHONL:回显新行符。
ICANON:启用标准输入处理。
IEXTEN:启用基于特定实现的函数。
ISIG:启用新号。
NOFLSH:禁止清空队列。
TOSTOP:在试图进行写操作之前给后台进程发送一个信号。
这里最重要的标志是 ECHO 和 ICANON。如果设置了 ICANON 标志,就启用标准输入行处理模式,否则,就启动非标准模式。
特殊控制字符
特殊控制字符是一些字符组合,如 Ctrl+C,当用户键入这样的组合键,终端会采取特殊处理方式。termios 中 c_cc 数组将各种特殊字符映射到对应的支持函数。每个字符位置(数组下标)由对应的宏定义的。根据终端是否被设置为标准模式(即上节提到的 ICANON 标志),数组使用也分为标准与非标准两种情形。
标准模式可以使用的数组下标:
VEOF:EOF 字符。
VEOL:EOL 字符。
VERASE:ERASE 字符。
VINTR:INTR 字符。
VKILL:KILL 字符。
VQUIT:QUIT 字符。
VSUSP:SUSP 字符。
VSTART:START 字符。
VSTOP:STOP 字符。
非标准模式可以使用的数组下标:
VINTR:INTR 字符。
VMIN:MIN 值。
VQUIT:QUIT 字符。
VSUSP:SUSP 字符。
VTIME:TIME 值。
VSTART:START 字符。
VSTOP:STOP 字符。
字符的详细解释如下表所示:
INTR:该字符使终端驱动程序向与终端相连的进程发送 SIGINT 信号
QUIT:该字符使终端驱动程序向与终端相连的进程发送 SIGQUIT 信号
ERASE:该字符使终端驱动程序删除输入行中的最后一个字符
KILL:该字符使终端驱动程序删除整个输入行
EOF:该字符使终端驱动程序将输入行中的全部字符传递给正在读取输入的应用程序。若输入行为空,read为0
EOL:作用类似于行结束符,效果和常用的新行符相同
SUSP:该字符使终端驱动程序向与终端相连的进程发送SIGSUSP信号,用于挂起当前应用程序
STOP:字符作用“截流”,即阻止向终端的进一步输出。用于支持 XON/XOFF 流控,通常被设置为 ASCII 的XOFF
START:重新启动被 STOP 暂停的输出,通过被设置为 ASCII 的 XON 字符。
VTIME 和 VMIN
TIME值和MIN值只能用于非标准模式,关于二者的使用详解参见BBS其他帖子,或者发邮件至 tech@wch.cn 进行咨询了解。
SHELL下使用 stty 访问终端模式
在 shell 下可以使用 stty 可以访问终端 termios。如:
1. #打印串口设备 ttyUSB0 设置情况。
2. root@ubuntu:/# stty -F /dev/ttyUSB0 -a
3. #设置 ttyUSB0 为 115200 波特率,8位数据位。
4. root@ubuntu:/# stty -F /dev/ttyUSB0 ispeed 115200 ospeed 115200 cs8
在设置成功之后就可以通过 cat、echo 等 shell 命令对设备进行读写了。
在 termios 结构体以及内部终端控制标志中,并非所有的参数对于实际的物理串口都是有效的,在使用过程中也不需要对于所有标志的作用都有所理解。事实上,快速掌握一项技术的核心点也是一种学习能力。对于使用,熟悉并掌握操作框架十分有用。对于串口编程,核心步骤也十分鲜明,下面首先介绍 termios 相关的 API 函数。
核心配置函数
1. int tcgetattr(int fd, struct termios *termios_p);
函数功能:获取当前终端接口配置并将配置写入参数 termios_p 指向的 termios 结构体。一般操作时将配置保存为 old_termios,可以在需要时通过 tcsetattr 函数对终端接口进行重新配置。
2. int tcsetattr(int fd, int actions, const struct termios *termios_p);
函数功能:使用 termios_p 指向的 termios 结构体对终端接口进行配置,参数 actions 控制修改方式,共有3种修改方式,如下所示:
TCSANOW:立刻对配置进行修改。
TCSADRAIN:等当前输出完成后再对配置进行修改。
TCSAFLUSH:等当前输出完成后再对配置进行修改,但丢弃还未从 read 调用返回的当前可用的任何输入。
Note:如果需要在程序操作结束恢复终端或者串口的初始状态,那么就需要使用 tcgetattr 介绍中的操作步骤进行恢复。
终端速度函数
1.speed_t cfgetispeed(const struct termios *);
函数功能:获取终端读取速度。
2.speed_t cfgetospeed(const struct termios *);
函数功能:获取终端输出速度。
3.int cfsetispeed(const struct termios *, speed_t speed);
函数功能:设置终端读取速度。
4.int cfgetispeed(const struct termios *, speed_t speed);
函数功能:设置终端输出速度。
Note:输入与输出速度是分开控制的;根据函数形参,这些函数只作用于termios结构,而不是直接作用于设备。因此如果要设置速度,就要首先使用tcgetattr获取当前终端配置,然后使用上述函数设置速度,最后使用tcsetattr将termios配置写入设备。此外,还要注意操作系统支持的波特率范围,通过查看 termios.h 可以获取到。
其他控制函数
1.int tcdrain(int fd);
函数功能:让调用程序一直等待,直到所有排队的输出都已发送完毕。
2.int tcflow(int fd, int flowtype);
函数功能:用于暂停或重新开始输出。
3.int tcflush(int fd, int in_out_selector);
函数功能:用于清空输入、输出或者两者同时清空。
串口编程和程序相对来说是很简单的,之所以用较长篇幅展示,主要是想在编程的基础上掌握相关背景,原理以及注意事项。相信在遇到问题的时候,就不会对于技术的概念和 API 的使用浅尝辄止了。下面进入具体应用案例,由于现在很多电脑已经没有引出串口以及波特率范围会受到限制,这里以 CH340 USB 转串口芯片制作的模块为基础讲解串口应用程序开发,芯片的 Linux 驱动链接如下:/download/CH341SER_LINUX_ZIP.html
Notes:如果串口程序发生阻塞,检查程序中是否调用了上述API。在打开终端或者串口设备之前,对应输入或者待输出数据缓存在驱动程序中,因此要根据实际需求选择是否调用tcflush清空相应缓冲区数据。在实际应用开发中必须明确程序中配置的标志位和函数的作用,在不确定作用的情况下最好保持默认设备。
设备的打开与关闭
1. int libtty_open(const char *devname);
函数功能:根据传入的串口设备名打开相应的设备。成功返回设备句柄,失败返回-1。
2. int libtty_close(int fd);
函数功能:关闭打开的设备句柄。成功返回0,失败返回负值。
设备的配置
1. int libtty_setopt(int fd, int speed, char databits, char stopbits, char parity);
函数功能:配置串口设备,传入参数依次为波特率设置、数据位设置、停止位设置、检验设置。
Notes:设备打开前,可以通过 ls /dev 确认自己的硬件设备名,对于 USB 转串口 IC,在系统下名称为 "ttyUSBx",设备序号是根据插入主机的先后顺序自动分配的,这里我的为 "ttyUSB0",读者根据自己的需要修改。
1. /**
2. * libtty_open - open tty device
3. * @devname: the device name to open
4. *
5. * In this demo device is opened blocked, you could modify it at will.
6. */
7. int libtty_open(const char *devname)
8. {
9. int fd = open(devname, O_RDWR | O_NOCTTY | O_NDELAY);
10. int flags = 0;
11.
12. if (fd == -1) {
13. perror("open device failed");
14. return -1;
15. }
16.
17. flags = fcntl(fd, F_GETFL, 0);
18. flags &= ~O_NONBLOCK;
19. if (fcntl(fd, F_SETFL, flags) < 0) {
20. printf("fcntl failed.\n");
21. return -1;
22. }
23.
24. if (isatty(fd) == 0)
25. {
26. printf("not tty device.\n");
27. return -1;
28. }
29. else
30. printf("tty device test ok.\n");
31.
32. return fd;
33. }
Notes:
传入的 devname 参数为设备绝对路径;
O_NOCTTY 标志用于通知系统,这个程序不会成为对应这个设备的控制终端。如果没有指定这个标志,那么任何一个输入(如SIGINT等)都将会影响用户的进程;
O_NDELAY标志与O_NONBLOCK等效,但这里不仅仅是设置为非阻塞,还用于通知系统,这个程序不关系DCD信号线所处的状态(即与设备相连的另一端是否激活或者停止)。如果用户指定了这一标志,则进程将会一直处在休眠状态,直到DCD信号线被激活;
使用 fcntl 函数恢复设备为阻塞状态,在数据收发时就会进行等待;
使用 isatty 函数测试当前打开的设备句柄是否关联到终端设备,如果不是tty设备,那么返回出错;
1. /**
2. * libtty_setopt - config tty device
3. * @fd: device handle
4. * @speed: baud rate to set
5. * @databits: data bits to set
6. * @stopbits: stop bits to set
7. * @parity: parity set
8. *
9. * The function return 0 if success, or -1 if fail.
10. */
11. int libtty_setopt(int fd, int speed, int databits, int stopbits, char parity)
12. {
13. struct termios newtio;
14. struct termios oldtio;
15. int i;
16.
17. bzero(&newtio, sizeof(newtio));
18. bzero(&oldtio, sizeof(oldtio));
19.
20. if (tcgetattr(fd, &oldtio) != 0) {
21. perror("tcgetattr");
22. return -1;
23. }
24. newtio.c_cflag |= CLOCAL | CREAD;
25. newtio.c_cflag &= ~CSIZE;
26.
27. /* set tty speed */
28. for (i = 0; i < sizeof(speed_arr) / sizeof(int); i++) {
29. if (speed == name_arr[i]) {
30. cfsetispeed(&newtio, speed_arr[i]);
31. cfsetospeed(&newtio, speed_arr[i]);
32. }
33. }
34.
35. /* set data bits */
36. switch (databits) {
37. case 5:
38. newtio.c_cflag |= CS5;
39. break;
40. case 6:
41. newtio.c_cflag |= CS6;
42. break;
43. case 7:
44. newtio.c_cflag |= CS7;
45. break;
46. case 8:
47. newtio.c_cflag |= CS8;
48. break;
49. default:
50. fprintf(stderr, "unsupported data size\n");
51. return -1;
52. }
53.
54. /* set parity */
55. switch (parity) {
56. case 'n':
57. case 'N':
58. newtio.c_cflag &= ~PARENB; /* Clear parity enable */
59. newtio.c_iflag &= ~INPCK; /* Disable input parity check */
60. break;
61. case 'o':
62. case 'O':
63. newtio.c_cflag |= (PARODD | PARENB); /* Odd parity instead of even */
64. newtio.c_iflag |= INPCK; /* Enable input parity check */
65. break;
66. case 'e':
67. case 'E':
68. newtio.c_cflag |= PARENB; /* Enable parity */
69. newtio.c_cflag &= ~PARODD; /* Even parity instead of odd */
70. newtio.c_iflag |= INPCK; /* Enable input parity check */
71. break;
72. default:
73. fprintf(stderr, "unsupported parity\n");
74. return -1;
75. }
76.
77. /* set stop bits */
78. switch (stopbits) {
79. case 1:
80. newtio.c_cflag &= ~CSTOPB;
81. break;
82. case 2:
83. newtio.c_cflag |= CSTOPB;
84. break;
85. default:
86. perror("unsupported stop bits\n");
87. return -1;
88. }
89.
90. newtio.c_cc[VTIME] = 0; /* Time-out value (tenths of a second) [!ICANON]. */
91. newtio.c_cc[VMIN] = 0; /* Minimum number of bytes read at once [!ICANON]. */
92.
93. tcflush(fd, TCIOFLUSH);
94.
95. if (tcsetattr(fd, TCSANOW, &newtio) != 0)
96. {
97. perror("tcsetattr");
98. return -1;
99. }
100. return 0;
101. }
Notes:
首先保存了原先串口配置,为了方便演示,这里保存为局部变量,实际使用时是需要把配置保存到全局 termios 结构体中的。使用 tcgetattr 还可以测试配置是否正确、串口是否可用等。返回值参见 man 手册;
使用 CLOCAL 用于忽略所有 MODEM 状态信号线,CREAD 标志用于使能接收。CSIZE 为数据位掩码;
调用 cfsetispeed 与cfsetospeed 参数设置波特率,函数中引用了两个数组,在后面的完整代码中会看到,这样书写可以简化设置代码;
通过控制 c_cflag 与 c_iflag 配置串口数据位、停止位以及校验设置等;
VTIME 与 VMIN 作用已经讲解多次,不再赘述,值得注意的是,TIME 值的单位是十分之一秒;
通过 tcflush 清空输入和输出缓冲区,根据实际需要修改;
最后通过 tcsetattr 函数对将配置实际作用于串口;
数据读写直接使用 read、write 函数接口就可以了,因此没有列举出来。下面给出完整的串口读写测试代码:
1. /* TTY testing utility (using tty driver)
2. * Copyright (c) 2017
3. * This program is free software; you can redistribute it and/or modify
4. * it under the terms of the GNU General Public License as published by
5. * the Free Software Foundation; either version 2 of the License.
6. *
7. * Cross-compile with cross-gcc -I /path/to/cross-kernel/include
8. */
9.
10. #include
11. #include
12. #include
13. #include
14. #include
15. #include
16. #include
17. #include
18. #include
19.
20. int speed_arr[] = {
21. B115200,
22. B57600,
23. B38400,
24. B19200,
25. B9600,
26. B4800,
27. B2400,
28. B1200,
29. B300
30. };
31.
32. int name_arr[] = {
33. 115200,
34. 57600,
35. 38400,
36. 19200,
37. 9600,
38. 4800,
39. 2400,
40. 1200,
41. 300
42. };
43.
44. /**
45. * libtty_setopt - config tty device
46. * @fd: device handle
47. * @speed: baud rate to set
48. * @databits: data bits to set
49. * @stopbits: stop bits to set
50. * @parity: parity set
51. *
52. * The function return 0 if success, or -1 if fail.
53. */
54. int libtty_setopt(int fd, int speed, int databits, int stopbits, char parity)
55. {
56. struct termios newtio;
57. struct termios oldtio;
58. int i;
59.
60. bzero(&newtio, sizeof(newtio));
61. bzero(&oldtio, sizeof(oldtio));
62.
63. if (tcgetattr(fd, &oldtio) != 0) {
64. perror("tcgetattr");
65. return -1;
66. }
67. newtio.c_cflag |= CLOCAL | CREAD;
68. newtio.c_cflag &= ~CSIZE;
69.
70. /* set tty speed */
71. for (i = 0; i < sizeof(speed_arr) / sizeof(int); i++) {
72. if (speed == name_arr[i]) {
73. cfsetispeed(&newtio, speed_arr[i]);
74. cfsetospeed(&newtio, speed_arr[i]);
75. }
76. }
77.
78. /* set data bits */
79. switch (databits) {
80. case 5:
81. newtio.c_cflag |= CS5;
82. break;
83. case 6:
84. newtio.c_cflag |= CS6;
85. break;
86. case 7:
87. newtio.c_cflag |= CS7;
88. break;
89. case 8:
90. newtio.c_cflag |= CS8;
91. break;
92. default:
93. fprintf(stderr, "unsupported data size\n");
94. return -1;
95. }
96.
97. /* set parity */
98. switch (parity) {
99. case 'n':
100. case 'N':
101. newtio.c_cflag &= ~PARENB; /* Clear parity enable */
102. newtio.c_iflag &= ~INPCK; /* Disable input parity check */
103. break;
104. case 'o':
105. case 'O':
106. newtio.c_cflag |= (PARODD | PARENB); /* Odd parity instead of even */
107. newtio.c_iflag |= INPCK; /* Enable input parity check */
108. break;
109. case 'e':
110. case 'E':
111. newtio.c_cflag |= PARENB; /* Enable parity */
112. newtio.c_cflag &= ~PARODD; /* Even parity instead of odd */
113. newtio.c_iflag |= INPCK; /* Enable input parity check */
114. break;
115. default:
116. fprintf(stderr, "unsupported parity\n");
117. return -1;
118. }
119.
120. /* set stop bits */
121. switch (stopbits) {
122. case 1:
123. newtio.c_cflag &= ~CSTOPB;
124. break;
125. case 2:
126. newtio.c_cflag |= CSTOPB;
127. break;
128. default:
129. perror("unsupported stop bits\n");
130. return -1;
131. }
132.
133. newtio.c_cc[VTIME] = 0; /* Time-out value (tenths of a second) [!ICANON]. */
134. newtio.c_cc[VMIN] = 0; /* Minimum number of bytes read at once [!ICANON]. */
135.
136. tcflush(fd, TCIOFLUSH);
137.
138. if (tcsetattr(fd, TCSANOW, &newtio) != 0)
139. {
140. perror("tcsetattr");
141. return -1;
142. }
143. return 0;
144. }
145.
146. /**
147. * libtty_open - open tty device
148. * @devname: the device name to open
149. *
150. * In this demo device is opened blocked, you could modify it at will.
151. */
152. int libtty_open(const char *devname)
153. {
154. int fd = open(devname, O_RDWR | O_NOCTTY | O_NDELAY);
155. int flags = 0;
156.
157. if (fd == -1) {
158. perror("open device failed");
159. return -1;
160. }
161.
162. flags = fcntl(fd, F_GETFL, 0);
163. flags &= ~O_NONBLOCK;
164. if (fcntl(fd, F_SETFL, flags) < 0) {
165. printf("fcntl failed.\n");
166. return -1;
167. }
168.
169. if (isatty(fd) == 0)
170. {
171. printf("not tty device.\n");
172. return -1;
173. }
174. else
175. printf("tty device test ok.\n");
176.
177. return fd;
178. }
179.
180. /**
181. * libtty_close - close tty device
182. * @fd: the device handle
183. *
184. */
185. int libtty_close(int fd)
186. {
187. return close(fd);
188. }
189.
190. void tty_test(int fd)
191. {
192. int nwrite, nread;
193. char buf[100];
194.
195. memset(buf, 0x32, sizeof(buf));
196.
197. while (1) {
198. nwrite = write(fd, buf, sizeof(buf));
199. printf("wrote %d bytes already.\n", nwrite);
200. nread = read(fd, buf, sizeof(buf));
201. printf("read %d bytes already.\n", nread);
202. sleep(2);
203. }
204.
205. }
206.
207. int main(int argc, char *argv[])
208. {
209. int fd;
210. int ret;
211.
212. fd = libtty_open("/dev/ttyUSB0");
213. if (fd < 0) {
214. printf("libtty_open error.\n");
215. exit(0);
216. }
217.
218. ret = libtty_setopt(fd, 9600, 8, 1, 'n');
219. if (!ret) {
220. printf("libtty_setopt error.\n");
221. exit(0);
222. }
223.
224. tty_test(fd);
225.
226. ret = libtty_close(fd);
227. if (!ret) {
228. printf("libtty_close error.\n");
229. exit(0);
230. }
231. }
执行成功的话,会在终端屏幕上看到每隔两秒输出串口成功发送和接收的字节数,测试时可以直接短接串口的发送和接收引脚进行测试。以下为成功测试截图: