Linux 串口编程和程序相对来说是很简单的,之所以用博客连载来展示,主要是想在学会使用的基础上掌握相关背景,原理以及注意事项。相信在遇到问题的时候,我们就不会对于技术的概念和 API 的使用浅尝辄止了。下面进入具体应用案例,由于现在很多电脑已经没有引出串口以及波特率范围会受到限制,这里我以 CH340 USB 转串口芯片制作的模块为基础讲解串口应用程序开发,关于该芯片在 Linux 系统的使用以及驱动加载可以参考:CH340 Linux驱动使用教程。
设备的打开与关闭
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",读者根据自己的需要修改。
/*** libtty_open - open tty device* @devname: the device name to open** In this demo device is opened blocked, you could modify it at will.*/
int libtty_open(const char *devname)
{int fd = open(devname, O_RDWR | O_NOCTTY | O_NDELAY); int flags = 0;if (fd == -1) { perror("open device failed");return -1; }flags = fcntl(fd, F_GETFL, 0);flags &= ~O_NONBLOCK;if (fcntl(fd, F_SETFL, flags) < 0) {printf("fcntl failed.\n");return -1;}if (isatty(fd) == 0){printf("not tty device.\n");return -1;}elseprintf("tty device test ok.\n");return fd;
}
Note:
- 传入的 devname 参数为设备绝对路径;
- O_NOCTTY标志用于通知系统,这个程序不会成为对应这个设备的控制终端。如果没有指定这个标志,那么任何一个输入(如SIGINT等)都将会影响用户的进程;
- O_NDELAY标志与O_NONBLOCK 等效,但这里不仅仅是设置为非阻塞,还用于通知系统,这个程序不关心 DCD 信号线所处的状态(即与设备相连的另一端是否激活或者停止)。如果用户指定了这一标志,则进程将会一直处在休眠状态,直到 DCD 信号线被激活;
- 使用 fcntl 函数恢复设备状态为阻塞状态,在数据收发时就会进行等待;
- 使用 isatty函数测试当前打开的设备句柄是否关联到终端设备,如果不是 tty 设备,那么也返回出错;
/*** libtty_setopt - config tty device* @fd: device handle* @speed: baud rate to set* @databits: data bits to set* @stopbits: stop bits to set* @parity: parity set** The function return 0 if success, or -1 if fail.*/
int libtty_setopt(int fd, int speed, int databits, int stopbits, char parity)
{struct termios newtio;struct termios oldtio;int i;bzero(&newtio, sizeof(newtio));bzero(&oldtio, sizeof(oldtio));if (tcgetattr(fd, &oldtio) != 0) {perror("tcgetattr"); return -1; }newtio.c_cflag |= CLOCAL | CREAD;newtio.c_cflag &= ~CSIZE;/* set tty speed */for (i = 0; i < sizeof(speed_arr) / sizeof(int); i++) {if (speed == name_arr[i]) { cfsetispeed(&newtio, speed_arr[i]); cfsetospeed(&newtio, speed_arr[i]); } }/* set data bits */switch (databits) {case 5: newtio.c_cflag |= CS5;break;case 6: newtio.c_cflag |= CS6;break;case 7: newtio.c_cflag |= CS7;break;case 8: newtio.c_cflag |= CS8;break; default: fprintf(stderr, "unsupported data size\n");return -1; }/* set parity */switch (parity) { case 'n':case 'N':newtio.c_cflag &= ~PARENB; /* Clear parity enable */newtio.c_iflag &= ~INPCK; /* Disable input parity check */break; case 'o': case 'O': newtio.c_cflag |= (PARODD | PARENB); /* Odd parity instead of even */newtio.c_iflag |= INPCK; /* Enable input parity check */break; case 'e': case 'E': newtio.c_cflag |= PARENB; /* Enable parity */ newtio.c_cflag &= ~PARODD; /* Even parity instead of odd */ newtio.c_iflag |= INPCK; /* Enable input parity check */break;default: fprintf(stderr, "unsupported parity\n");return -1; } /* set stop bits */ switch (stopbits) { case 1: newtio.c_cflag &= ~CSTOPB; break;case 2: newtio.c_cflag |= CSTOPB; break;default: perror("unsupported stop bits\n"); return -1;}newtio.c_cc[VTIME] = 0; /* Time-out value (tenths of a second) [!ICANON]. */newtio.c_cc[VMIN] = 0; /* Minimum number of bytes read at once [!ICANON]. */tcflush(fd, TCIOFLUSH); if (tcsetattr(fd, TCSANOW, &newtio) != 0) {perror("tcsetattr");return -1;}return 0;
}
Note:
- 首先保存了原先串口配置,为了方便演示,这里保存为局部变量,实际使用时是需要把配置保存到全局 termios 结构体中的。使用tcgetattr还可以测试配置是否正确、串口是否可用等。返回值参见 man 手册;
- 使用 CLOCAL 用于忽略所有 MODEM 状态信号线,CREAD 标志用于使能接收。CSIZE 为数据位掩码;
- 调用 cfsetispeed与cfsetospeed参数设置波特率,函数中引用了两个数组,在后面的完整代码中会看到,这样书写可以简化设置代码;
- 通过控制 c_cflag 与 c_iflag 配置串口数据位、停止位以及校验设置等;
- VTIME与VMIN作用已经讲解多次,不再赘述,值得注意的是,TIME 值的单位是十分之一秒;
- 通过 tcflush清空输入和输出缓冲区,根据实际需要修改;
- 最后通过 tcsetattr 函数对将配置实际作用于串口;
/* TTY testing utility (using tty driver)* Copyright (c) 2017* This program is free software; you can redistribute it and/or modify* it under the terms of the GNU General Public License as published by* the Free Software Foundation; either version 2 of the License.** Cross-compile with cross-gcc -I /path/to/cross-kernel/include*/#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <termios.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h> int speed_arr[] = {B115200,B57600,B38400,B19200,B9600,B4800,B2400,B1200,B300
};int name_arr[] = {115200,57600,38400,19200,9600,4800,2400,1200,300
};/*** libtty_setopt - config tty device* @fd: device handle* @speed: baud rate to set* @databits: data bits to set* @stopbits: stop bits to set* @parity: parity set** The function return 0 if success, or -1 if fail.*/
int libtty_setopt(int fd, int speed, int databits, int stopbits, char parity)
{struct termios newtio;struct termios oldtio;int i;bzero(&newtio, sizeof(newtio));bzero(&oldtio, sizeof(oldtio));if (tcgetattr(fd, &oldtio) != 0) {perror("tcgetattr"); return -1; }newtio.c_cflag |= CLOCAL | CREAD;newtio.c_cflag &= ~CSIZE;/* set tty speed */for (i = 0; i < sizeof(speed_arr) / sizeof(int); i++) {if (speed == name_arr[i]) { cfsetispeed(&newtio, speed_arr[i]); cfsetospeed(&newtio, speed_arr[i]); } }/* set data bits */switch (databits) {case 5: newtio.c_cflag |= CS5;break;case 6: newtio.c_cflag |= CS6;break;case 7: newtio.c_cflag |= CS7;break;case 8: newtio.c_cflag |= CS8;break; default: fprintf(stderr, "unsupported data size\n");return -1; }/* set parity */switch (parity) { case 'n':case 'N':newtio.c_cflag &= ~PARENB; /* Clear parity enable */newtio.c_iflag &= ~INPCK; /* Disable input parity check */break; case 'o': case 'O': newtio.c_cflag |= (PARODD | PARENB); /* Odd parity instead of even */newtio.c_iflag |= INPCK; /* Enable input parity check */break; case 'e': case 'E': newtio.c_cflag |= PARENB; /* Enable parity */ newtio.c_cflag &= ~PARODD; /* Even parity instead of odd */ newtio.c_iflag |= INPCK; /* Enable input parity check */break;default: fprintf(stderr, "unsupported parity\n");return -1; } /* set stop bits */ switch (stopbits) { case 1: newtio.c_cflag &= ~CSTOPB; break;case 2: newtio.c_cflag |= CSTOPB; break;default: perror("unsupported stop bits\n"); return -1;}newtio.c_cc[VTIME] = 0; /* Time-out value (tenths of a second) [!ICANON]. */newtio.c_cc[VMIN] = 0; /* Minimum number of bytes read at once [!ICANON]. */tcflush(fd, TCIOFLUSH); if (tcsetattr(fd, TCSANOW, &newtio) != 0) {perror("tcsetattr");return -1;}return 0;
}/*** libtty_open - open tty device* @devname: the device name to open** In this demo device is opened blocked, you could modify it at will.*/
int libtty_open(const char *devname)
{int fd = open(devname, O_RDWR | O_NOCTTY | O_NDELAY); int flags = 0;if (fd == -1) { perror("open device failed");return -1; }flags = fcntl(fd, F_GETFL, 0);flags &= ~O_NONBLOCK;if (fcntl(fd, F_SETFL, flags) < 0) {printf("fcntl failed.\n");return -1;}if (isatty(fd) == 0){printf("not tty device.\n");return -1;}elseprintf("tty device test ok.\n");return fd;
}/*** libtty_close - close tty device* @fd: the device handle**/
int libtty_close(int fd)
{return close(fd);
}void tty_test(int fd)
{int nwrite, nread;char buf[100];memset(buf, 0x32, sizeof(buf));while (1) {nwrite = write(fd, buf, sizeof(buf));printf("wrote %d bytes already.\n", nwrite);nread = read(fd, buf, sizeof(buf));printf("read %d bytes already.\n", nread);sleep(2);}}int main(int argc, char *argv[])
{int fd;int ret;fd = libtty_open("/dev/ttyUSB0");if (fd < 0) {printf("libtty_open error.\n");exit(0);}ret = libtty_setopt(fd, 9600, 8, 1, 'n');if (ret != 0) {printf("libtty_setopt error.\n");exit(0);}tty_test(fd);ret = libtty_close(fd);if (ret != 0) {printf("libtty_close error.\n");exit(0);}
}
关于 Linux 串口编程的其他文章,可以移步至以下链接:
- 《Linux 串口编程<一> 一些背景》
- 《Linux 串口编程<二> 深入了解 termios》
- 《Linux 串口编程<三> 使用termios与API 进行串口程序开发》
- 《Linux 串口编程<四> 串口设备程序开发》