【linux】系统调用版串口分析&源码实战


前言

  • 目前不涉及驱动源码

参考

1. 实战分析

1.1 开发步骤

  1. 获取串口设备路径
  2. 打开设备文件
  3. 配置串口
  4. 对该设备文件进行读写,相当于对该串口设备进行读写,即通信
  5. 关闭设备文件

以下代码段默认从 附件-最终串口测试源码 中摘取

1.1.1 获取串口设备路径

  • 使用数组或者宏定义在相关文件前面定义默认串口路径,方便修改,源码段如下:
/* 不同的设备,不同的路径 */
const char def_uart_path[] = "/dev/ttymxc2"  // 默认串口路径(备用)
  • 串口路径优先从传入参数中获取,如果参数中没有传入,便使用 def_uart_path 默认路径
/* 终端设备选择 */
if(argc > 1)
    path = argv[1];
else
    path = (char *)def_uart_path;

1.1.2 打开设备文件

  • 获取设备句柄,如果获取失败,便结束
/* 打开终端 */
fd = open(path, O_RDWR);
if(fd < 0){
    printf("[%s] open err!", path);
    return 0;
}

1.1.3 配置串口

  • 定义一个结构体 termios 用于获取、设置终端设备的参数
    1. 包括波特率、数据位数、校验位等
termios 结构体
  • 成员值作用,推荐先看官方手册,看不懂再看本笔记中文表格
  • termios 结构体定义在编译链接工具的头文件默认路径中的bits文件夹中
    • 如如下源码来自 /usr/arm-linux-gnueabihf/include/bits/termios.h
成员 说明
c_iflag 输入模式标志
c_oflag 输出模式标志
c_cflag 控制模式标志
c_lflag 本地模式标志
c_line 行控制
c_cc[NCCS] 控制字符
c_ispeed 输入波特率
c_ospeed 输出波特率
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
  };
1. c_iflag 输入模式标志
  • 用于控制如何对串口输入的字符进行处理
选项值 说明
IGNBRK 忽略输入中的 BREAK 状态。 (忽略命令行中的中断)
BRKINT (命令行出 现中断时,可产生一插断)如果设置了IGNBRK,中断条件被忽略。如果没有设置IGNBRK而设置了BRKINT,中断条件清空输入输出队列中所有的数据并且向tty的前 台进程组中所有进程发送一个SIGINT信号。如果这两个都没有设置,中断条件会被看作一个0字符。这时,如果设置了PARMRK,当检测到一个帧误差时 将会向应用程序发送三个字节’/377”/0”/0’,而不是只发送一个’/0′
IGNPAR 忽略桢错误和奇偶校验错
PARMRK 如果设定了IGNPAR,则忽略接收到的数据的奇偶检验错误或帧错误(除了前面提到的中断条件)。如果没有设置IGNPAR而设置了PARMRK, 当接收到的字节存在奇偶检验错误或帧错误的时候。将向应用程序发送一个三字节的’/377”/0”/n’错误报告。其中n表示所接收到的字节。如果两 者都没有设置,除了接收到的字节存在奇偶检验错误或帧误差之外的中止条件都会向应用程序发送一个单字节(’/0’)的报告
INPCK 如果设置,则进行奇偶校验。如果不进行奇偶检验,PARMRK和IGNPAR将对存在的奇偶校验错误不产生任何的影响
ISTRIP 如果设置,所接收到的所有字节的高位将会被去除,保证它们是一个7位的字符
INLCR 如果设置,所接收到的换行字符(’/n’)将会被转换成回车符(’/r’)
IGNCR 如果设置,则会忽略所有接收的回车符(’/r’)
ICRNL 如果设置,但IGNCR没有设置,接收到的回车符向应用程序发送时会变换成换行符
IUCLC 如果IUCLC和IEXTEN都设置,接收到的所有大写字母发送给应程序时都被转换成小写字母。POSIX中没有定义该标记
IXON 如果设置,接收到S后会停止向这个tty设备输出,接收到Q后会恢复输出
IXANY 如果设置,则接到任何字符都会重新开始输出,而不仅仅是^Q字符
IXOFF 如果设置,为避免tty设备的输入缓冲区溢出,tty设备可以向终端发送停止符S和开始符Q,要求终端停止或重新开始向计算机发送数据。通过停 止符和开始符来控制数据流的方式叫软件流控制,软件流控制方式较少用,我们主要还是用硬件流控制方式。硬件流控制在c_cflag标志中设置
IMAXBEL 如果设置,当输入缓冲区空间满时,再接收到的任何字符就会发出警报符’/a’。POSIX中没有定义该标记
IUTF8 (不属于 POSIX)输入 IUTF8 ,这是允许 character-erase 在 cooked 模式下被正确执行
2. c_oflag 输出模式标志
  • 用于控制串口的输出模式
选项值 说明
OPOST 开启该标记,后面的输出标记才会生效。否则,不会对输出数据进行处理
OLCUC 如果设置,大写字母被转换成小写字母输出
ONLCR 如果设置,在发送换行符(’/n’)前先发送回车符(’/r’)
OCRNL 如果设置,回车符会被转换成换行符。另外,如果设置了ONLRET,则current column会被设为0
ONOCR 如果设置,当current column为0时,回车符不会被发送也不会被处理
ONLRET 如果设置,当一个换行符或回车符被发送的时候,current column会被设置为0
OFILL 发送填充字符作为延时,而不是使用定时来延时
OFDEL (不属于 POSIX) 填充字符是 ASCII DEL (0177)。如果不设置,填充字符则是 ASCII NUL
VTDLY 竖直跳格延时掩码。取值为 VT0 或 VT1
3. c_cflag 控制模式标志
  • 用于控制串口的基本参数,如数据位、停止位等,常用配置见下表,特别地,c_cflag结构体成员还包含了波特率的参数
选项值 说明
CLOCAL 如果设置,modem的控制线将会被忽略。如果没有设置,则open()函数会阻塞直到载波检测线宣告modem处于摘机状态为止
CREAD 只有设置了才能接收字符,该标记是一定要设置的
CSIZE 设置传输字符的位数。CS5表示每个字符5位,CS6表示每个字符6位,CS7表示每个字符7位,CS8表示每个字符8位
CSTOPB 设置停止位的位数,如果设置,则会在每帧后产生两个停止位,如果没有设置,则产生一个停止位。一般都是使用一位停止位。需要两位停止位的设备已过时 了
HUPCL 如果设置,当设备最后打开的文件描述符关闭时,串口上的DTR和RTS线会减弱信号,通知Modem挂断。也就是说,当一个用户通过Modem拔号 登录系统,然后注销,这时Modem会自动挂断
PARENB 允许输出产生奇偶信息以及输入的奇偶校验(启用同位产生与侦测)
PARODD 输入和输出是奇校验(使用奇同位而非偶同位)
CRTSCTS 使用硬件流控制。在高速(19200bps或更高)传输时,使用软件流控制会使效率降低,这个时候必须使用硬件流控制
4. c_lflag 本地模式标志
  • 主要用于控制驱动程序与用户的交互,在串口通信中,实际上用不到该成员变量
选项值 说明
ISIG 当接受到字符 INTR, QUIT, SUSP, 或 DSUSP 时,产生相应的信号
ICANON 启用标准模式 (canonical mode)。允许使用特殊字符EOF, EOL, EOL2, ERASE, KILL, LNEXT, REPRINT, STATUS, 和WERASE,以及按行的缓冲
ECHO 它可以让你阻止键入字元的回应
ECHOE 如果同时设置了 ICANON,字符 ERASE 擦除前一个输入字符,WERASE 擦除前一个词
ECHOK 如果同时设置了 ICANON,字符 KILL 删除当前行
ECHONL 如果同时设置了 ICANON,回显字符 NL,即使没有设置 ECHO
NOFLSH 禁止在产生 SIGINT, SIGQUIT 和 SIGSUSP 信号时刷新输入和输出队列,即关闭queue中的flush
TOSTOP 向试图写控制终端的后台进程组发送 SIGTTOU 信号(传送欲写入的信息到后台 处理)
IEXTEN 启用实现自定义的输入处理。这个标志必须与 ICANON同时使用,才能解释特殊字符 EOL2,LNEXT,REPRINT 和WERASE,IUCLC 标志才有效
5. c_cc[NCCS] 控制字符
  • 该数组包含了终端的所有特殊字符,可以修改特殊字符对应的键值(Ctrl+C产生的^C,ASCII码为0x03)
  • 仅列出常用的
选项值 说明
VINTR 中断字符。发出 SIGINT 信号。当设置了c_lflag的ISIG标志位时,该字母不再作为输入传递
VERASE 删除字符。删除上一个还没有删掉的字符,但不删除上一个EOF 或行首。当设置 ICANON 时可被识别,不再作为输入传递
VIM 设置非标准模式读取的最小字节数
VTIM 设置非标准模式读取时的延时值,单位为十分之一秒
6. c_ispeed和c_ospeed 波特率
  • 注意以 0 开头的数字在是 C语言 的 8进制 数字形式
#define  B0		0000000		/* hang up */
#define  B50		0000001
#define  B75		0000002
#define  B110		0000003
#define  B134		0000004
#define  B150		0000005
#define  B200		0000006
#define  B300		0000007
#define  B600		0000010
#define  B1200		0000011
#define  B1800		0000012
#define  B2400		0000013
#define  B4800		0000014
#define  B9600		0000015
#define  B19200	0000016
#define  B38400	000001

#define  B57600  	 0010001
#define  B115200 	 0010002
#define  B230400 	 0010003
#define  B460800  	0010004
#define  B500000  	0010005
#define  B576000  	0010006
#define  B921600  	0010007
#define  B1000000 	0010010
#define  B1152000 	0010011
#define  B1500000 	0010012
#define  B2000000 	0010013
#define  B2500000 	0010014
#define  B3000000 	0010015
#define  B3500000 	0010016
#define  B4000000 	0010017
分析
  • 以上只是介绍了 termios 结构体,在编写代码时,我们使用相关 api 去配置该结构体从而配置串口
  • api 接口推荐先看本文推荐链接,不懂再看本文
// api 如下
#include <termios.h>
#include <unistd.h>

int tcgetattr(int fd, struct termios *termios_p);

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

int tcsendbreak(int fd, int duration);

int tcdrain(int fd);

int tcflush(int fd, int queue_selector);

int tcflow(int fd, int action);

void cfmakeraw(struct termios *termios_p);

speed_t cfgetispeed(const struct termios *termios_p);

speed_t cfgetospeed(const struct termios *termios_p);

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

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

int cfsetspeed(struct termios *termios_p, speed_t speed);
  • 清空接收缓冲区,获取串口参数,配置,更新配置
/* 定义串口结构体 */
struct termios opt;
/* 清空串口接收缓冲区 */
tcflush(fd, TCIOFLUSH);
/* 获取串口参数 */
tcgetattr(fd, &opt);
/* 设置输入、输入波特率 */
cfsetospeed(&opt, B9600);
cfsetispeed(&opt, B9600);
/* 设置数据位数 */
opt.c_cflag &= ~CSIZE;
opt.c_cflag |= CS8;
/* 校验位 */
opt.c_cflag &= ~PARENB;
opt.c_iflag &= ~INPCK;
/* 设置停止位 */
opt.c_cflag &= ~CSTOPB;
/* 更新配置 */
tcsetattr(fd, TCSANOW, &opt);

1.1.4 串口收发测试

  • 串口收发测试就是对该串口进行读写
 /* 发送测试 */
write(fd, bufW, strlen(bufW));
/* 接收测试 */
res = read(fd, bufR, 512);
if(res > 0){
    printf("receive data is %s", bufR);
}

1.1.5 关闭设备文件

  • 程序正常结束,不要忘记关闭设备文件
/* 关闭文件 */
close(fd);

附件

最终串口测试源码

/** @file main.c
* @brief 串口测试文件-系统调用版
* @details 详细说明
* @author lzm
* @date 2020-11-23 19:18:20
* @version v1.0
* @copyright Copyright By lizhuming, All Rights Reserved
*
**********************************************************
* @LOG 修改日志:
**********************************************************
*/
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <termios.h>
#include <string.h>
#include <sys/ioctl.h>
/* 不同的设备,不同的路径 */
const char def_uart_path[] = "/dev/ttymxc2"; // 默认串口路径(备用)
/**
* @brief 主函数
* @param 无
* @retval 无
*/
int main(int argc, char *argv[])
{
    int fd;
    int res;
    char *path;
    char bufW[512] = "This is sys uart test!\n";
    char bufR[512];
    /* 终端设备选择 */
    if(argc > 1)
        path = argv[1];
    else
        path = (char *)def_uart_path;
    /* 打开终端 */
    fd = open(path, O_RDWR);
    if(fd < 0){
        printf("[%s] open err!", path);
        return 0;
    }
    /* 定义串口结构体 */
    struct termios opt;
    /* 清空串口接收缓冲区 */
    tcflush(fd, TCIOFLUSH);
    /* 获取串口参数 */
    tcgetattr(fd, &opt);
    /* 设置输入、输入波特率 */
    cfsetospeed(&opt, B9600);
    cfsetispeed(&opt, B9600);
    /* 设置数据位数 */
    opt.c_cflag &= ~CSIZE;
    opt.c_cflag |= CS8;
    /* 校验位 */
    opt.c_cflag &= ~PARENB;
    opt.c_iflag &= ~INPCK;
    /* 设置停止位 */
    opt.c_cflag &= ~CSTOPB;
    /* 更新配置 */
    tcsetattr(fd, TCSANOW, &opt);
    do{
        /* 发送测试 */
        write(fd, bufW, strlen(bufW));
        /* 接收测试 */
        res = read(fd, bufR, 512);
        if(res > 0){
            printf("receive data is %s", bufR);
        }
    }while(res >= 0);
    /* 读取错误 */
    printf("read error, res = %d", res);
    /* 关闭文件 */
    close(fd);
    return 0;
}