> 文章列表 > Linux串口应用编程

Linux串口应用编程

Linux串口应用编程

在Linux系统中,操作设备的统一接口就是:open/ioctl/read/write
对于UART,又在ioctl之上封装了很多函数,主要是用来设置行规程。
所以对于UART,编程的套路就是:

  • 使用open函数打开串口
  • 设置行规程,比如波特率、数据位、停止位、检验位、RAW模式、一有数据就返回
  • read/write数据
    Linux串口应用编程

1. 打开串口

由于串行端口是一个文件,因此使用open(2)函数来访问它。C语言代码示例如下。

1.1 示例

#include <stdio.h>   /* 标准输入/输出定义 */
#include <string.h>  /* 字符串函数定义 */
#include <unistd.h>  /* UNIX标准函数定义 */
#include <fcntl.h>   /* 文件控制定义 */
#include <errno.h>   /* 错误号定义 */
#include <termios.h> /* POSIX终端控制定义 */// 成功时返回文件描述符,错误时返回-1。
int open_port(void){int fd; /* 端口的文件描述符 */fd = open("/dev/ttyf1", O_RDWR | O_NOCTTY | O_NDELAY);if (fd == -1){// 打开端口失败perror("open_port: Unable to open /dev/ttyf1 - ");}elsefcntl(fd, F_SETFL, 0);return (fd);
}

其他系统可能需要相应的设备文件名,但除此之外代码是相同的。

1.2 open函数的标志位

当我们打开设备文件时,我们使用了另外两个标志以及读+写模式:

fd = open("/dev/ttyf1", O_RDWR | O_NOCTTY | O_NDELAY);

其中,

  • O_NOCTTY :表示告诉操作系统,应用程序(进程)打开串口之后,不要把程序当作控制终端。如果指定这一点,那么任何输入(如键盘中止信号等)都将影响进程。
  • O_NDELAY:表示告诉操作系统,应用程序(进程)不关心DCD信号线的状态,即不关心端口的另一端是否启动并运行。如果没有指定这个标志,进程将被置于休眠状态,直到DCD信号线是空间电压。

2. 配置串口

配置串口也就是设置行规程,行规程的参数用结构体struct termios来表示。设置行规程就是设置该结构体中成员的值。

2.1 结构体struct termios

结构体struct termios定义如下:
Linux串口应用编程

struct termios{unsigned short c_iflag;   /* 输入模式标志*/unsigned short c_oflag;   /* 输出模式标志*/unsigned short c_cflag;   /* 控制模式标志*/unsigned short c_lflag;   /* 区域模式标志或本地模式标志或局部模式*/unsigned char c_line;     /* 行控制line discipline */unsigned char c_cc[NCC];  /* 控制字符特性*/
};

2.2 struct termios作用

struct termios被用来提供一个健全的线路设置集合, 如果这个端口在被用户初始化前使用. 驱动初始化这个变量使用一个标准的数值集, 它拷贝自 tty_std_termios 变量. tty_std_termostty 核心被定义为:

struct termios tty_std_termios = {.c_iflag = ICRNL | IXON;.c_oflag = OPOST | ONLCR;.c_cflag = B38400 | CS8 | CREAD | HUPCL;.c_lflag = ISIG | ICANON | ECHO | ECHOE | ECHOK | ECHOCTL | ECHOKE | IEXTEN;.c_cc = INIT_C_CC;
};

这个 struct termios 结构用来持有所有的当前线路设置,给这个 tty 设备的一个特定端口。这些线路设置控制当前波特率。数据大小。数据流控设置。以及许多其他值。

2.3 struct termios成员介绍

2.3.1 c_iflag标志常量:Input mode ( 输入模式)

输入模式成员c_iflag控制对端口上接收到的字符所做的任何输入处理。c_iflag中存储的最终值由下表中选项的按位或。

常量 描述
INPCK 启用奇偶校验
IGNPAR 忽略奇偶校验错误
PARMRK 标记奇偶校验错误
ISTRIP 去掉奇偶校验位
IXON 启用输出的 XON/XOFF 流控制
IXOFF 启用输入的 XON/XOFF 流控制
IXANY (不属于 POSIX.1;XSI) 允许任何字符来重新开始输出
IGNBRK 忽略输入中的 BREAK 状态。 (忽略命令行中的中断)
BRKINT 当检测到中断条件时发送SIGINT
INLCR 将输入中的 NL 翻译为 CR。(将收到的换行符号转换为Return)
IGNCR 忽略输入中的回车
ICRNL 将输入中的回车翻译为新行 (除非设置了 IGNCR)(否则当输入信号有 CR 时不会终止输入)
IUCLC (不属于 POSIX) 将输入中的大写字母映射为小写字母
IMAXBEL (不属于 POSIX) 当输入队列满时响零。Linux 没有实现这一位,总是将它视为已设置

2.3.2 c_oflag 标志常量: Output mode ( 输出模式)

c_oflag成员包含输出过滤选项。与输入模式一样,您可以选择已处理原始数据输出。c_oflag 中存储的最终值由下表中选项的按位或。

常量 描述
OPOST 启用具体实现自行定义的输出处理(未设置=原始输出)
OLCUC (不属于 POSIX) 将输出中的小写字母映射为大写字母
ONLCR (XSI) 将输出中的新行符映射为回车-换行
OCRNL 将输出中的回车映射为新行符
ONOCR 不在第 0 列输出回车
ONLRET 不输出回车
OFILL 发送填充字符作为延时,而不是使用定时来延时
OFDEL (不属于 POSIX) 填充字符是 ASCII DEL (0177)。如果不设置,填充字符则是 ASCII NUL
NLDLY 新行延时掩码。取值为 NL0 和 NL1
CRDLY 回车延时掩码。取值为 CR0, CR1, CR2, 或 CR3
BSDLY 回退延时掩码。取值为 BS0 或 BS1。(从来没有被实现过)
VTDLY 竖直跳格延时掩码。取值为 VT0 或 VT1
IUCLC (不属于 POSIX) 将输入中的大写字母映射为小写字母
FFDLY 进表延时掩码。取值为 FF0 或 FF1

更多选项如下图所示:
Linux串口应用编程
一般有两种输出模式可供选择:
(1)选择已处理输出
通过在c_oflag成员中设置OPOST选项来选择处理后的输出:

options.c_oflag |= OPOST;

在所有不同的选项中,目前只能使用ONLCR选项,它将换行符映射为CR-LF对。其余的输出选项主要是历史上的,可以追溯到行打印机和终端无法跟上串行数据流的时候。
(2)选择原始输出
通过重置c_oflag成员中的OPOST选项来选择原始输出:

options.c_oflag &= ~OPOST;

OPOST选项被禁用时,c_oflag中的所有其他选项位都会被忽略。

2.3.3 c_cflag 标志常量: Control mode ( 控制模式)

c_cflag成员控制波特率、数据位数、奇偶校验、停止位和硬件流控制。所有支持的配置都有常量。

c_cflag中存储的最终值由下表中选项确定。

常量 描述
CBAUD (不属于 POSIX) 波特率掩码 (4+1 位)
OLCUC (不属于 POSIX) 扩展的波特率掩码 (1 位),包含在 CBAUD 中
CSIZE 字符长度掩码(传送或接收字元时用的位数)。取值为 CS5(传送或接收字元时用5bits), CS6, CS7, 或 CS8
CSTOPB 设置两个停止位,而不是一个
CREAD 打开接受者
PARENB 允许输出产生奇偶信息以及输入的奇偶校验(启用同位产生与侦测)
PARODD 输入和输出是奇校验(使用奇同位而非偶同位)
HUPCL 在最后一个进程关闭设备后,降低 modem 控制线 (挂断)
CLOCAL 忽略 modem 控制线
LOBLK (不属于 POSIX) 从非当前 shell 层阻塞输出(用于 shl )
CIBAUD (不属于 POSIX) 输入速度的掩码。CIBAUD 各位的值与 CBAUD 各位相同,左移了 IBSHIFT 位
CRTSCTS (不属于 POSIX) 启用 RTS/CTS (硬件) 流控制

Linux串口应用编程
c_cflag成员包含两个应该始终启用的选项,CLOCALCREAD。这将确保您的程序不会成为端口的“所有者”,受到零星的作业控制和挂起信号的影响,并且串行接口驱动程序将读取传入的数据字节。

不要直接初始化c_cflag(或任何其他标志)成员。应该始终使用按位的ANDORNOT操作符来设置或清除成员中的位。不同的操作系统版本可以以不同的方式使用位,因此使用位操作符将防止破坏新串行驱动程序中所需的位标志。

2.3.4 c_lflag 标志常量: Local mode ( 局部模式)

本地模式成员c_lflag控制串口驱动程序如何管理输入字符。通常,将为规范或原始输入配置c_lflag成员。c_cflag中存储的最终值由下表中选项确定。

常量 描述
ISIG 使能SIGINTR、SIGSUSP、SIGDSUSP和SIGQUIT信号
ICANON 启用规范化输入(否则为raw)
XCASE (不属于 POSIX; Linux 下不被支持) 如果同时设置了 ICANON,终端只有大写。输入被转换为小写,除了有前缀的字符。输出时,大写字符被前缀(某些系统指定的特定字符) ,小写字符被转换成大写。
ECHO 启用输入字符的回显
ECHOE 如果同时设置了 ICANON,字符 ERASE 擦除前一个输入字符,WERASE 擦除前一个词
ECHOK 如果同时设置了 ICANON,字符 KILL 删除当前行
ECHONL 如果同时设置了 ICANON,回显字符 NL,即使没有设置 ECHO
NOFLSH 禁止在产生 SIGINT, SIGQUIT 和 SIGSUSP 信号时刷新输入和输出队列,即关闭queue中的flush
IEXTEN 启用扩展功能
ECHOCTL 如果同时设置了 ECHO,除了 TAB, NL, START, 和 STOP 之外的 ASCII 控制信号被回显为 ^X, 这里 X 是比控制信号大 0x40 的 ASCII 码。例如,字符 0x08 (BS) 被回显为 ^H
ECHOPRT 如果同时设置了 ICANON 和 IECHO,字符在删除的同时被打印
CRTSCTS (不属于 POSIX) 启用 RTS/CTS (硬件) 流控制

一般有两种输入模式可供选择:
(1)选择规范输入
规范输入是面向行的。输入字符被放入缓冲区,用户可以交互地编辑缓冲区,直到收到CR(回车)或LF(换行)字符。
当选择此模式时,通常选择ICANONECHOECHO选项:

options.c_lflag |= (ICANON | ECHO | ECHOE);

(2)选择原始输入
原始输入未经处理。当接收到输入字符时,它们将完全按照接收到的方式传递。通常,当使用原始输入时,您将取消选择ICANON, ECHO, ECHOEISIG选项:

options.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG);

2.3.5 c_cc 数组:特殊控制字元

UNIX串行接口驱动程序提供了指定字符和数据包超时的能力。c_cc数组中的两个元素用于超时:VMINVTIME。在规范输入模式下或通过openfcntl在文件上设置NDELAY选项时,会忽略超时。

VMIN指定要读取的最小字符数。如果设置为0,则VTIME值指定等待读取每个字符的时间。请注意,这并不意味着对N个字节的读取调用将等待N个字符进入。相反,超时将应用于第一个字符,read调用将返回立即可用的字符数(最多可达您请求的字符数)。

如果VMIN不为零,则VTIME指定等待读取第一个字符的时间。如果在给定的时间内读取一个字符,则任何读取将阻塞(等待),直到读取所有VMIN字符。也就是说,一旦读取了第一个字符,串行接口驱动程序期望接收整个字符包(VMIN字节总数)。如果在允许的时间内没有读取任何字符,则调用read返回0。此方法允许您告诉串行驱动程序您需要恰好N个字节,并且任何读调用将返回0或N个字节。然而,超时只适用于第一个字符读取,所以如果由于某种原因驱动程序错过了N字节包中的一个字符,那么read调用可能会永远阻塞,等待额外的输入字符。

VTIME指定等待传入字符的时间,以十分之一秒为单位。如果VTIME设置为0(默认值),读取将无限期阻塞(等待),除非在端口上设置NDELAY选项openfcntl

VMINVTIME的组合方式如下:
(1)VMIN = 0 , VTIME =0
read立即回传,否则传回 0 ,不读取任何字元
(2)VMIN = 0 , VTIME >0
read传回读到的字元,或在十分之一秒后传回VTIME
(3)VMIN > 0 , VTIME =0
read会等待,直到VMIN字元可读
(4)VMIN > 0 , VTIME > 0
每一格字元之间计时器即会被启动
read会在读到VMIN字元,传回值或VTIME的字元计时(1/10秒)超过时将值传回

2.4 与结构体struct termios相关的函数

函数命名解释:

  • tc:terminal contorl
  • cf:control flag

2.4.1 tcgetattr()与tcsetattr()

(1)tcgetattr()函数:get terminal attributes,获得终端的属性
原型:

#include <termios.h>
#include <unistd.h>int tcgetattr(int fd, struct termios *termios_p);

作用:取得终端介质(fd)初始值,并把其值 赋给temios_p; 函数可以从后台进程中调用;但是,终端属性可能被后来的前台进程所改变。

(2)tcsetattr() 函数:set terminal attributes,修改终端参数
原型:

#include <termios.h>
#include <unistd.h>int tcsetattr(int fd, int optional_actions, const struct termios *termios_p);

作用:设置与终端相关的参数 ,使用 termios_p 引用的 termios 结构。optional_actionstcsetattr函数的第二个参数)指定了什么时候改变会起作用,可以使用的值如下:

  • TCSANOW:改变立即发生
  • TCSADRAIN:改变在所有写入 fd 的输出都被传输后生效。这个函数应当用于修改影响输出的参数时使用。(当前输出完成时将值改变)
  • TCSAFLUSH :改变在所有写入 fd 引用的对象的输出都被传输后生效,所有已接受但未读入的输入都在改变发生前丢弃(同TCSADRAIN,但会舍弃当前所有值)。

2.4.2 tcflush()

原型:

int tcflush(int fd, int queue_selector);

作用:丢弃要写入 引用的对象,但是尚未传输的数据,或者收到但是尚未读取的数据,取决于 queue_selector 的值。 queue_selector的取值有:

  • TCIFLUSH :刷新收到的数据但是不读
  • TCOFLUSH :刷新写入的数据但是不传送
  • TCIOFLUSH :同时刷新收到的数据但是不读,并且刷新写入的数据但是不传送

2.4.3 tcflow()

原型:

int tcflow(int fd, int action);

作用:挂起 fd 引用的对象上的数据传输或接收,取决于 action 的值。 action 取值有:

  • TCOOFF :挂起输出
  • TCOON :重新开始被挂起的输出
  • TCIOFF :发送一个 STOP 字符,停止终端设备向系统传送数据
  • TCION :发送一个 START 字符,使终端设备向系统传输数据

打开一个终端设备时的默认设置是输入和输出都没有挂起。

2.4.4 波特率函数

波特率函数被用来获取和设置 termios结构体中输入和输出波特率的值。新值不会马上生效,直到成功调用了 tcsetattr() 函数。
(1)cfgetospeed()函数
原型:

speed_t cfgetispeed(const struct termios *termios_p);

作用:返回 termios_p 指向的 termios 结构中存储的输出波特率。返回存储在终端结构中的输入波特率。
(2)cfsetispeed()函数:sets the input baud rate,设置输入波特率
原型:

int cfsetispeed(struct termios *termios_p, speed_t speed);

作用:设置 termios 结构中存储的输入波特率为 speed。如果输入波特率被设为0,实际输入波特率将等于输出波特率。
(3)cfsetospeed()函数:sets the output baud rate,设置输出波特率
原型:

int cfsetospeed(struct termios *termios_p, speed_t speed);

作用:设置 termios 结构中存储的输出波特率为 speed

(4)cfsetspeed()函数:同时设置输入、输出波特率
原型:

int cfsetspeed(struct termios *termios_p, speed_t speed);

作用:cfsetspeed()是一个4.4BSD扩展。它接受与cfsetispeed()相同的参数,并设置输入和输出速度。
(5)波特率大小设置选择
如图:
Linux串口应用编程

3. Linux串口应用编程实例

下面给出了串口配置的完整的函数。通常,为了函数的通用性,通常将常用的选项都在函数中列出,这样可以大大方便以后用户的调试使用。该设置函数如下所示:

3.1 串口配置的函数

// fd:设备文件描述符;nSpeed:需要设置的波特率;nBits:需要设置的数据位数;nEvent:奇偶校验位;nStop:停止位
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;}//将 newtio 清零bzero( &newtio, sizeof( newtio ) );/*步骤一,设置字符大小*/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 )newtio.c_cflag &=  ~CSTOPB;else if ( nStop == 2 )newtio.c_cflag |=  CSTOPB;/*设置等待时间和最小接收字符*/newtio.c_cc[VTIME]  = 0;newtio.c_cc[VMIN] = 0;/*处理未接收字符*/tcflush(fd,TCIFLUSH);/*激活新配置*/if((tcsetattr(fd,TCSANOW,&newtio))!=0){perror("com set error");return -1;}printf("set done!\\n");return 0;
}

3.2 打开串口的函数

下面给出了一个完整的打开串口的函数,同样写考虑到了各种不同的情况。程序如下所示:

/*打开串口函数*/
int open_port(int fd,int comport){char *dev[]={"/dev/ttyS0","/dev/ttyS1","/dev/ttyS2"};//串口 1if (comport==1){fd = open( "/dev/ttyS0", O_RDWR|O_NOCTTY|O_NDELAY);if (-1 == fd){perror("Can't Open Serial Port");return(-1);}}else if(comport==2){//串口 2fd = open( "/dev/ttyS1", O_RDWR|O_NOCTTY|O_NDELAY);if (-1 == fd){perror("Can't Open Serial Port");return(-1);}}else if (comport==3){//串口 3fd = open( "/dev/ttyS2", O_RDWR|O_NOCTTY|O_NDELAY);if (-1 == fd){perror("Can't Open Serial Port");return(-1);}}/*恢复串口为阻塞状态*/if(fcntl(fd, F_SETFL, 0)<0)printf("fcntl failed!\\n");elseprintf("fcntl=%d\\n",fcntl(fd, F_SETFL,0));/*测试是否为终端设备*/if(isatty(STDIN_FILENO)==0)printf("standard input is not a terminal device\\n");elseprintf("isatty success!\\n");printf("fd-open=%d\\n",fd);return fd;
}

3.3 从串口中读取数据

//
int read_datas(int fd, char *rcv_buf,int rcv_wait){int retval;fd_set rfds;struct timeval tv;int ret,pos;tv.tv_sec = rcv_wait;      // wait 2.5stv.tv_usec = 0;pos = 0; // point to rceeive bufwhile (1){FD_ZERO(&rfds);FD_SET(fd, &rfds);retval = select(fd+1 , &rfds, NULL, NULL, &tv);if (retval == -1){perror("select()");break;}else if (retval){// pan duan shi fou hai you shu juret = read(fd, rcv_buf+pos, 2048);pos += ret;if (rcv_buf[pos-2] == '\\r' && rcv_buf[pos-1] == '\\n'){FD_ZERO(&rfds);FD_SET(fd, &rfds);retval = select(fd+1 , &rfds, NULL, NULL, &tv);if (!retval) break;// no datas, break}}else{printf("No data\\n");break;}}return 1;
}

3.4 向串口传数据

int send_data(int fd, char *send_buf){ssize_t ret;ret = write(fd,send_buf,strlen(send_buf));if (ret == -1){printf ("write device %s error\\n", DEVICE_TTYS);return -1;}return 1;
}

参考

[1] https://digilander.libero.it/robang/rubrica/serial.htm
[2] https://blog.csdn.net/yemingzhu163/article/details/5897156
[3] https://www.cnblogs.com/feisky/archive/2010/05/21/1740893.html