目录
前言
在嵌入式Linux中,串口是一个字设备,访问具体的串行端口的编程与读/写文件的操作类似,只需打开相应的设备文件即可操作。串口编程特殊在于串口通信时相关参数与属性的设置。嵌入式Linux的串口编程时应注意,若在根文件中没有串口设备文件,应使用mknod命令创建,这这里假设串口设备是/dev/ttyS0,介绍一下串口的编程过程。
#mknod /dev/ttyS0 c 4 64
一、tty体系
tty是teletype的缩写,在上世纪计算机还很昂贵的时候,多人可以通过这种终端来连接并共用一台计算机,发展到今日,tty已经成为了字符类设备的统称,这类设备包括:控制台、UART等物理串行接口以及伪终端。也就是说,开发板物理串口一般对应linux中/dev目录下的tty*设备文件,访问这些文件,就访问硬件串口了。
二、串口硬件基础知识
串口是一类以串行方式传输数据的接口(每次1bit)这类接口常用于网络设备、键盘、鼠标、调制解调器、以及终端设备(teletype,上世纪电脑还是大块头时,大家通过这种设备远程共用一台电脑)等。
常用bps(每秒传输的位数)或称为波特率来表示串口的通讯速度,常见的有9600bps、38400bps、115200bps等。
对于常用的RS232接口,除了常见的TXD、RXD、GND这三个必须的通讯信号线外,还有一些用于控制的信号线:
名称 | 方向 | 用途 |
---|---|---|
DCD(Data Carrier Detect) | 输入 | 如果读到低电平(逻辑1),说明线缆对面的设备已经正确连接。 |
DTR(Data Terminal Ready) | 输出 | 输出低电平(逻辑1)表明我方已经准备好数据传输,一般软件打开串行端口时,此信号会自动拉低。 |
CTS(Clear To Send) | 输入 | 如果读到低电平(逻辑1),说明线缆对面的设备允许我们此时发送数据。 |
RTS(Request To Send) | 输出 | 输出低电平(逻辑1)以请求线缆对面的设备传输数据,大多数时候,此信号线总是输出低电平。 |
一般在连接时,我方的DTR连接对方的DCD,我方的RTS连接对方的CTS,反之亦然。**DCD和DTR信号用于设备检测,CTS和RTS信号用于进行信号流控制(除此之外还有一种软件信号流控制方法,当然我在工作中很少用到这些)。
三、Linux下的串口编程
既然Linux下串口设备是以文件的形式存在,我们当然可以用通用的文件I/O函数访问它们,不过访问设备文件一般需要超级管理员权限,在开发板上我们一般以root身份登录系统,这当然也就不存在问题。除此之外,为了对串口进行设置,Linux系统提供了一个名为 termios.h 的文件,里面包含串口设置相关的数据结构以及函数接口,这些函数接口,归根结底其实也是调用了文件I/O函数中的ioctl函数。
下面的示例代码中,均默认包含以下头文件:
#include <stdio.h>
#include <string.h>
#include <unistd.h> /* UNIX standard function definitions */
#include <fcntl.h> /* File control definitions */
#include <errno.h> /* Error number definitions */
#include <termios.h> /* POSIX terminal control definitions */
3.1 打开串口
以下代码演示如何正确打开一个串口设备文件
打开串口设备文件的操作与普通文件的操作类似,都采用标准的I/O操作函数open()。
fd = open("/dev/ttyS0",O_RDWR|O_NDELAY|O_NOCTTY);
open()函数有两个参数,第一个参数是要打开的文件名(此处为串口设备文件/dev/ttyS0);第二个参数设置打开的方式,O_RDWR表示打开的文件可读/写,O_NDELAY表示以非阻塞方式打开,O_NOCTTY表示若打开的文件为终端设备,则不会将终端作为进程控制终端。
3.2 从串口读写数据,问题1、2的诞生
串口的数据读/写与普通文件的读/写一样,都是使用read()/write()函数实现。
n = write( fd, buf, len ); //将buf中len个字节的数据从串口输出,返回输出的字节数
n = read( fd, buf, len ); //从串口读入len个字节的数据并放入buf, 返回读取的字节数
示例如下:
写数据
/*
* write data to serial port
* return the length of bytes actually written on success, or -1 on error
*/
ssize_t write_port(int fd, void *buf,size_t len)
{
ssize_t actual_write;
actual_write = write(fd, buf, len);
if (actual_write == -1)
{
/* write failure */
perror("write_port: Unable to write port - ");
}
return actual_write;
}
读数据
/*
* read data from serial port
* return the length of bytes actually read on success, or -1 on error
*/
ssize_t read_port(int fd, void *buf, size_t len)
{
ssize_t actual_read;
actual_read = read(fd, buf, len);
if (actual_read == -1)
{
/* read failure */
perror("read_port: Unable to read port - ");
}
return actual_read;
}
函数实现了读取功能,但是存在两个问题:
1.只有在读完一行才会退出,也就是读到’\r’或’\n’,否则进程会一直堵塞到read函数上;
2.如果串口没有收到数据,函数会一直堵塞直到超时或接收到数据,最小接收长度以及超时时间可以通过后面提到的配置函数进行配置;
问题1可以通过设置串口终端工作在RAW模式解决,问题2可以通过设置串口的最小接收字节数和超时时间解决,具体见 3.4 小节的串口配置内容。
3.3 关闭串口
关闭串口的操作很简单,将打开的串口设备文件句柄关闭即可。
close(fd);
3.4 串口配置
Linux提供了一组用于配置串口终端的数据结构以及对应的函数接口,称为 “POSIX termios interface”。要使用它们,只需要包含此头文件:
#include <termios.h> /* POSIX terminal control definitions */
它的参数设置需要使用struct termios结构体,这个结构体在termio.h文件中定义,且应在程序中包含这个头文件。
typedef unsigned char cc_t ;
typedef unsigned int speed_t ;
typedef unsigned int tcflag_t ;
struct termios
{
tcflag_t c_iflag ; /*输入模式标志*/
tcflag_t c_oflag ; /*输出模式标志*/
tcflag_t c_cflag ; /*控制模式标志*/
tcflag_t c_lflag ; /*本地模式标志*/
tcflag_t c_line ; /*行规程类型,一般应用程序不使用*/
cc_t c_cc[NCC]; /*控制字符*/
speed_t c_ispeed ; /*输入数据波特率*/
speed_t c_ospeed ; /*输出数据波特率*/
};
串口的设置主要是设置这个结构体的各成员值,然后利用该结构体将参数传给硬件驱动程序。在Linux中,串口以串行终端的方式进行处理,因而,可以使用tcgetattr()/tcsetattr()函数获取/设置串口的参数。
3.4.1 获取/设置串口的参数
通过系统提供的如下两个接口,我们可以获取当前串口的状态或设置新的状态:
int tcgetattr(int fd, struct termios *termios_p);
int tcsetattr(int fd, int optional_actions, const struct termios *termios_p);
这两个参数都有一个批向termios结构体的指针作为参数,用于返回当前终端的属性或设置该终端的属性。参数fd就是用open()函数打开的终端文件句柄,而串口就是用open()打开的串口设备文件句柄。tcsetattr()函数的optional_action参数用于指定新设定的参数起作用的时间,其设定值可以为:
- TCSANOW 改变立即生效
- TCSADRAIN 在所有的输出都被传输后改变生效,适用于更改影响输出参数的情况。
- TCSAFLUSH 在所有输出都被传输后改变生效,丢弃所有末读入的输入(清空输入缓 存)。
3.4.2 设置波特率
使用cfsetospeed()/cfsetispeed()函数设置波特率,它们分别用于在termios结构体中设置输出和输入的波特率。设置波特率可以使用波特率常数,其定义为字母“B+速率”,如B19200就是波特率为19200bps,B115200就是波特率为115200bps。
int cfsetispeed( struct termios *termios_p, speed_t speed ); //speed为波特率常数
int cfsetospeed( struct termios *termios_p, speed_t speed );
例 :
cfsetispeed( ttys0_opt, B115200 );
cfsetospeed( ttys0_opt, B115200 );
3.4.3 设置控制模式标志
控制模式标志c_cflag主要用于设置串口对DCD信号状态检测、硬件流控制、字符位宽、停止位和奇偶校验等,常用标志位如下:
CLOCAL 忽略DCD信号,若不使用MODEM,或没有串口没有CD脚就设置此标志
CREAD 启用接收装置,可以接收字符
CRTSCTS 启用硬件流控制,对于许多三线制的串不应使用,需设置~CRTCTS
CSIZE 字符位数掩码,常用CS8
CSTOPB 使用两个停止位,若用一位应设置~CSTOPB
PARENB 启用奇偶校验
例如,下面的代码将串口设置为忽略DCD信号,启用接收装置,关闭硬件流控制,传输数据时使用8位数据位和一位停止位(8N1),不使用奇偶校验。
struct temios ttys0
ttyso_opt.c_cflag |= CLOCAL | CREAD ; //将CLOCAL与CREAD位设置为1
ttys0_opt.c_cflag &= ~CRTSCTS ; //将硬件流控制位CRTSCTS清0,其他位不变
ttys0_opt.c_cflag &= ~CSIZE ; //清除数据位掩码
ttys0_opt.c_cflag |= CS8 ; //设置8位数据位标志CS8
ttys0_opt.c_cflag &= ~(PARENB|CSTOPB); //使用1位停止位,停用奇偶校验
3.4.4 设置本地模式标志
本地模式标志c_lflag主要用于设置终端与用户的交互方式,常见的设置标志位有ICAN-ON,ECHO和ECHOE等。其中,ICANON标志位用于实现规范输入,即read()读到行结束符后返回,常用于终端的处理;若串口用于发送/接收数据,则应清除此标志,使用非规范模式(raw mode)。非规范模式中,输入数据不组成行,不处规范模式中的特殊字符。在规范模式中,当设置ECHO标志位时,用户向终端输入的字符将被回传给用户;当设置ECHOE标志位时,用户输入退格键时,则回传“退格-空格-退格”序列给用户,使得退格键覆盖的字符从显示中消失,这样更符合用户的习惯(若未设置此标志,输入退格键时,则光标回退一个字符,但原有的字符未从显示中消失)。
3.4.5 设置输入模式标志
输入模式标志c_iflag主要用于控制串口的输入特性,常用的设置有IXOFF和IXON,分别用于软件流控制。其中,IXOFF用于防止输入缓冲区溢出;IXON则是在输入数据中识别软件流控制标志。由于许多嵌入式系统无法使用硬件流控制,因此,只能使用软件流控制数据传输的速度,但是,它可能降低串口数据传输效率。启用软件流控制的代码如下:
ttys0_opt.c_iflag |= IXOFF|IXON ;
3.4.6 设置输出模式标志
输出模式标志c_oflag主要用于对串口在规范模式时输出的特殊字符处理,而对非规范模式无效。
3.4.7 设置控制字符
在非规范模式中,控制字符数组c_cc[]中的变量c_cc[VMIN]和c_cc[VTIME]用于设置read()返回前读到的最少字节数和读超时时间,其值分为四种情况:
- 1.c_cc[VMIN]>0,c_cc[VTIME]>0
读到一个字节后,启动定时器,其超时时间为c_cc[VTIME],read()返回的条件为至少读到c_cc[VMIN]个字符或定时器超期。 - 2.c_cc[VMIN]>0, c_cc[VTIME] ==0
只要读到数据的字节数大于等于c_cc[VMIN],则read()返回;否则,将无限期阻塞等待。 - 3.c_cc[VMIN] == 0, c_cc[VTIME]>0
只要读到数据,则read()返回;若定时器超期(定时时间c_cc[VTIME])却未读到数据,则read()返回0; - 4.c_cc[VMIN] == 0, c_cc[VTIME] == 0
若有数据,则read()读取指定数量的数据后返回;若没有数据,则read()返回0;
在termios结构体中填写完这些参数后,接下来就可以使用tcsetattr()函数设置串口的属性。
tcsetattr( fd, &old_opt ); //将原有的设置保存到old_opt,以便程序结束后恢复
tcsetattr( fd, TCSANOW, &ttsy0_opt );
3.4.8 清空发送/接收缓冲区
为保证读/写操作不被串口缓冲区中原有的数据干拢,可以在读/写数据前用tcflush()函数清空串口发送/接收缓冲区。tcflush()函数的参数可为:
TCIFLUSH 清空输入队列
TCOFLUSH 清空输出队列
TCIOFLUSH 同时清空输入和输出队列
四、解决问题 1、2
4.1 cfmakeraw() RAW模式
示例为一个串口配置函数,其将串口配置为传入的波特率,8位数据位,无校验位,RAW格式:
/*
* config serial port baud
* and set data format to 8N1(8bits data, no parity bit, 1 stop bit)
* return 0 on success, or -1 on error
*/
int config_port(int fd, speed_t baud)
{
struct termios term;
/* read serial port configure */
if (tcgetattr(fd, &term) != 0)
{
perror("config_port: Unable to read configure - ");
return -1;
}
/* set baudrate */
cfsetspeed(&term, baud);
/* 8N1 mode */
term.c_cflag &= ~PARENB;
term.c_cflag &= ~CSTOPB;
term.c_cflag &= ~CSIZE;
term.c_cflag |= CS8;
/* enbale raw mode */
cfmakeraw(&term);
/* write serial port configure */
if (tcsetattr(fd, TCSADRAIN, &term) != 0)
{
perror("config_port: Unable to write configure - ");
return -1;
}
}
cfmakeraw()
函数使得串口工作于RAW模式,在此模式下,tty子系统会将从串口收到的所有字节数据直接递交给应用层,而不进行额外处理,此时read()函数会在读取完缓冲区的所有数据后立刻退出并返回实际读到的数值,而不是等到\r或\n的到来,这便解决了上面读取函数的问题1。
4.2 改进串口数据读取
可以在开启串口的RAW模式后,通过配置termios结构体中的成员:
.c_cc[VTIME]; /* 指定超时时间 */
.c_cc[VMIN]; /* 指定最少读取的字节 */
来指定read读取时至少需要读到多少字节才返回(这个需要配合串口的RAW格式),也可以对接收的超时时间进行设置,如果将这 两者都设为0
,则不管是否读到数据,read函数都会立刻退出,这就解决了前面提到的问题2。
五、完整的示例代码
仅供参考
#include <errno.h> /* Error number definitions */
#include <fcntl.h> /* File control definitions */
#include <stdio.h>
#include <string.h>
#include <termios.h> /* POSIX terminal control definitions */
#include <unistd.h> /* UNIX standard function definitions */
/*
* open serial port
* return file descriptor on success or -1 on error.
*/
int open_port(char *path)
{
int fd;
fd = open(path, O_RDWR | O_NOCTTY | O_NDELAY);
if (fd == -1)
{
/* open failure, print system error msg. */
perror("open_port: Unable to open port - ");
}
else
{
/* clear file descriptor flags */
fcntl(fd, F_SETFL, 0);
return fd;
}
}
/*
* write data to serial port
* return the length of bytes actually written on success, or -1 on error
*/
ssize_t write_port(int fd, void *buf, size_t len)
{
ssize_t actual_write;
actual_write = write(fd, buf, len);
if (actual_write == -1)
{
/* write failure */
perror("write_port: Unable to write port - ");
}
return actual_write;
}
/* set timeout and minimum number of bytes to read */
int read_port_setup(int fd, cc_t timeout_sec, cc_t min_bytes)
{
struct termios term;
/* read serial port configure */
if (tcgetattr(fd, &term) != 0)
{
perror("config_port: Unable to read configure - ");
return -1;
}
/* set timeout in deciseconds for noncanonical read */
term.c_cc[VTIME] = timeout_sec * 10;
/* set minimum number of bytes to read */
term.c_cc[VMIN] = min_bytes;
/* write serial port configure */
if (tcsetattr(fd, TCSADRAIN, &term) != 0)
{
perror("config_port: Unable to write configure - ");
return -1;
}
}
/*
* read data from serial port
* return the length of bytes actually read on success, or -1 on error
*
* fd - file descriptor
* buf - pointer of buffer to receive bytes
* len - buffer
* sec - timeout second
* min - minimum bytes to read
*/
ssize_t read_port(int fd, void *buf, size_t len, cc_t sec, cc_t min)
{
ssize_t actual_read;
/* setup serial port */
if (read_port_setup(fd, sec, min) == -1)
{
return -1;
}
/* read bytes from serial port */
actual_read = read(fd, buf, len);
if (actual_read == -1)
{
/* read failure */
perror("read_port: Unable to read port - ");
}
return actual_read;
}
/*
* config serial port baud and set data format to 8N1
* (8bits data, no parity bit, 1 stop bit)
* return 0 on success, or -1 on error
*/
int config_port(int fd, speed_t baud)
{
struct termios term;
/* read serial port configure */
if (tcgetattr(fd, &term) != 0)
{
perror("config_port: Unable to read configure - ");
return -1;
}
/* set baudrate */
cfsetspeed(&term, baud);
/* 8N1 mode */
term.c_cflag &= ~PARENB;
term.c_cflag &= ~CSTOPB;
term.c_cflag &= ~CSIZE;
term.c_cflag |= CS8;
/* enbale raw mode */
cfmakeraw(&term);
/* write serial port configure */
if (tcsetattr(fd, TCSADRAIN, &term) != 0)
{
perror("config_port: Unable to write configure - ");
return -1;
}
}
/*
* close serial port
*/
int close_port(int fd)
{
if (close(fd) == -1)
{
perror("close_port: Unable to close port - ");
return -1;
}
return 0;
}
int main(int argc, char *argv[])
{
char *path = "/dev/ttyS0";
int fd;
char send_string[] = "hello world!\n";
char read_string[20] = "";
ssize_t length;
/* open port */
fd = open_port(path);
if (fd > 0)
{
printf("open %s success, fd = %d.\n", path, fd);
}
/* config port */
config_port(fd, B9600);
/* write data to port */
length = write_port(fd, send_string, sizeof(send_string));
if (length >= 0)
{
printf("%ld bytes written.\n", length);
}
/* read data from port */
length = read_port(fd, read_string, 20, 5, 5);
if (length >= 0)
{
printf("%ld bytes read : ", length);
for (ssize_t i = 0; i < length; i++)
{
printf("%02X ", (int)(read_string[i]));
}
printf("\n");
}
/* close port */
close_port(fd);
return 0;
}