嵌入式linux入门3-2-串口

tty体系

tty是teletype的缩写,在上世纪计算机还很昂贵的时候,多人可以通过这种终端来连接并共用一台计算机,发展到今日,tty已经成为了字符类设备的统称,这类设备包括:控制台、UART等物理串行接口以及伪终端。*也就是说,开发板物理串口一般对应linux中/dev目录下的tty设备文件,访问这些文件,就访问硬件串口了。**

tips:以下内容为阅读 Serial Programming Guide for POSIX Operating Systems 的笔记

可以去此处直接阅读原文:Serial Programming Guide for POSIX Operating Systems

串口硬件基础知识

串口是一类以串行方式传输数据的接口(每次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 */

打开串口

以下代码演示如何正确打开一个串口设备文件

/* 
 * 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
    {
        fcntl(fd, F_SETFL, 0);
        return fd;
    }
}

在打开终端设备文件时,我们使用了O_NOCTTY和O_NDELAY标志,以下引用解释了原因:

The O_NOCTTY flag tells UNIX that this program doesn't want to be the "controlling terminal" for that port. If you don't specify this then any input (such as keyboard abort signals and so forth) will affect your process. Programs like getty(1M/8) use this feature when starting the login process, but normally a user program does not want this behavior.

The O_NDELAY flag tells UNIX that this program doesn't care what state the DCD signal line is in - whether the other end of the port is up and running. If you do not specify this flag, your process will be put to sleep until the DCD signal line is the space voltage.

在打开成功后,我们使用了fcntl函数去清空文件的状态标志位,但是示例中的fcntl函数调用方法其实是不严谨的,fcntl用于根据第二个参数指定的cmd修改文件描述符,其中F_SETFL命令的解释如下:

Set the file status flags to the value specified by arg. File access mode (O_RDONLY, O_WRONLY, O_RDWR) and file creation flags (i.e., O_CREAT, O_EXCL, O_NOCTTY, O_TRUNC) in arg are ignored. On Linux, this command can change only the O_APPEND, O_ASYNC, O_DIRECT, O_NOATIME, and O_NONBLOCK flags. It is not possible to change the O_DSYNC and O_SYNC flags; see BUGS, below.

StackOverflow上有对这个问题的进一步讨论,见此链接:Why fcntl(fd, F_SETFL, 0) use in serial port programming

串口数据写入

只需要向普通文件中写入数据一样写入串口对应的设备文件,就可以通过串口向外发送数据了,如下:

/*
 * 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可以通过设置串口的最小接收字节数和超时时间解决,具体见后面的串口配置内容。

串口配置

Linux提供了一组用于配置串口终端的数据结构以及对应的函数接口,称为 “POSIX termios interface”。要使用它们,只需要包含此头文件:

#include <termios.h> /* POSIX terminal control definitions */

对串口进行配置,其实就是对一个配置相关的结构体进行设置,此结构体的定义如下(不同版本可能有差异):

struct termios
  {
    tcflag_t c_iflag;       /* input mode flags */
    tcflag_t c_oflag;       /* output mode flags */
    tcflag_t c_cflag;       /* control mode flags */
    tcflag_t c_lflag;       /* local mode flags */
    cc_t c_line;            /* line discipline */
    cc_t c_cc[NCCS];        /* control characters */
    speed_t c_ispeed;       /* input speed */
    speed_t c_ospeed;       /* output speed */
#define _HAVE_STRUCT_TERMIOS_C_ISPEED 1
#define _HAVE_STRUCT_TERMIOS_C_OSPEED 1
  };

通过系统提供的如下两个接口,我们可以获取当前串口的状态或设置新的状态:

int tcgetattr(int fd, struct termios *termios_p);
int tcsetattr(int fd, int optional_actions, const struct termios *termios_p);

配置串口的正确流程如下:

  1. 使用tcgetattr()获取当前的串口配置;

  2. 通过直接读写返回的termios结构体或借助如下的修改函数进行串口配置的读取与修改:

    /* Return the output baud rate stored in *TERMIOS_P.  */
    extern speed_t cfgetospeed (const struct termios *__termios_p) __THROW;
    
    /* Return the input baud rate stored in *TERMIOS_P.  */
    extern speed_t cfgetispeed (const struct termios *__termios_p) __THROW;
    
    /* Set the output baud rate stored in *TERMIOS_P to SPEED.  */
    extern int cfsetospeed (struct termios *__termios_p, speed_t __speed) __THROW;
    
    /* Set the input baud rate stored in *TERMIOS_P to SPEED.  */
    extern int cfsetispeed (struct termios *__termios_p, speed_t __speed) __THROW;
    
    /* Set both the input and output baud rates in *TERMIOS_OP to SPEED.  */
    extern int cfsetspeed (struct termios *__termios_p, speed_t __speed) __THROW;
    
    /* Set *TERMIOS_P to indicate raw mode.  */
    extern void cfmakeraw (struct termios *__termios_p) __THROW;
  3. 使用tcsetattr()将修改后的配置写入串口;

以下示例为一个串口配置函数,其将串口配置为传入的波特率,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

改进串口数据读取

我们可以在开启串口的RAW模式后,通过配置termios结构体中的成员:

.c_cc[VTIME]; /* 指定超时时间 */
.c_cc[VMIN];  /* 指定最少读取的字节 */

来指定read读取时至少需要读到多少字节才返回(这个需要配合串口的RAW格式),也可以对接收的超时时间进行设置,如果将这两者都设为0,则不管是否读到数据,read函数都会立刻退出,这就解决了前面提到的问题2

改进后的函数如下:

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

常见用法:

read_port(fd, buf, len, 0, 0); /* polling read, 不管是否收到字节都会立刻退出 */
read_port(fd, buf, len, 5, 0); /* read with timeout, 收到至少一个字节或超时5秒时退出 */
read_port(fd, buf, len, 0, 5); /* blocking read, 读到至少5个字节后退出 */

/* 
 * read with interbyte timeout, 这种情况比较复杂
 * 一个字节间隔计时器会在收到第一个字节后启动,每当收到新的字节,
 * 计时器都会复位,以下情况会导致函数退出: 
 *     1. 收到至少5个字节
 *     2. 字节间隔计时器超时5秒
 */
read_port(fd, buf, len, 5, 5); 

关闭串口

像关闭普通文件一样关闭串口即可:

/*
 * close serial port 
 */
int close_port(int fd)
{
    if (close(fd) == -1)
    {
        perror("close_port: Unable to close port - ");
        return -1;
    }
    return 0;
}

完整的示例代码

#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;
}

缺少的内容

原英文文档的部分内容由于我暂时用不到,所以就没有记录了,以下章节是我认为值得阅读的:

  1. Chapter 2, Configuring the Serial Port 章节的后面还讨论了流控制的相关内容,如果需要使用可以阅读;
  2. Chapter 4, Advanced Serial Programming 一些串口编程的高级特性,比如配置串口时实际调用的ioctl系统调用函数以及select系统调用函数的使用;

发表评论

您的电子邮箱地址不会被公开。