Linux上位机开发中的串口termios库函数使用

目录

一、termios基础概念与知识

(1)termios库是什么?

(2)termios结构体的简单认识

二、termios的核心函数与操作流程

(1)get与set函数

(2)输入、输出波特率设置函数

(3)串口缓冲区函数

(4)一键进入原始模式函数

三、标志位的理解与配置

[(1)特殊控制字符 c_cc[NCCS]](#(1)特殊控制字符 c_cc[NCCS])

(2)控制模式c_cflag

四、完整的配置例子


在上篇文章中,我们借助AI工具生成代码打通了:单片机--->串口通信--->Linux读取的完整路径,但是核心的termios库函数并没有很多了解,于是本篇文章将补充这部分知识,方便后续学习修改。

一、termios基础概念与知识

(1)termios库是什么?

termios库是Linux中用于控制终端(上位机)和串口设备(底层单片机)通信的标准接口,本质上而言,是一个针对于串口的配置箱,里面针对各种串口设备有一个完善的配置过程。当然由于Linux内核需要尽量适配更多的外设,所以他的配置选项可能十分复杂多样,但90%都不会遇到,我们只涉及最为基础、最核心的几个函数、配置项。

  • 它通过 struct termios 结构体管理串口所有参数(波特率、数据位、校验位等);
  • 配套 12 个核心函数,实现 "读取配置、修改配置、应用配置、缓冲区管理" 等核心功能;
  • 所有 USB 转串口设备(如 /dev/ttyUSB0)都通过 termios 配置,是嵌入式上位机通信的基础。

从上图中可以看出,最核心的一个是fd,另一个就是struct termios。其中fd作为Linux的精髓:一切皆文件思想。必定会把串口封装成一个文件,然后利用各种指针跳转就能访问到串口真正的数据,所以fd这里不用太过关心。真正的重点是结构体 struct termios。

(2)termios结构体的简单认识

**控制模式标志位c_cflag:**管 "硬件底层" 的规则

这是最核心的一组,直接对应物理终端的参数,是和单片机通信必须要对齐的配置。他管的就是波特率、数据位、停止位、奇偶校验位这些关于数据怎么传递的参数。举个例子:你和单片机约定用 115200 波特率、8 位数据位、1 位停止位、无校验,本质就是通过设置 c_cflag 里的 CS8(8 位数据)、清空 CSTOPB(1 位停止位)、清空 PARENB(无校验),再配合波特率函数完成的。

**输入模式标志位c_iflag:**管 "数据进来后" 的预处理

这一组决定的是Linux收到数据后,怎么对数据进行预处理解析。核心是为了解决不同设备的格式差异。。比如有些设备输出的是回车(CR),但 Linux 系统认换行(NL),就可以开 ICRNL 开关,让系统自动把 CR 转成 NL;又比如串口传数据时可能有奇偶校验错误,开 IGNPAR 就能忽略这些错误数据。这里主要是不同终端(上位机)之间的格式转换。

但在嵌入式上位机开发中,主要是单片机与Linux通信,所以通常会关闭所有输入转换,即把c_iflag=0。因为单片机要传递完整的、原始的二进制数据,不存在C语言层面的ASCII码,一旦自动格式转换就变成错误消息了。

**本地模式标志位c_lflag:**管 "人和终端交互" 的行为

这组是为 "人用终端操作" 设计的,控制的是 "终端和用户的交互规则",和外设通信时基本要全部关掉。

比如 ECHO(回显):你输入一个字符,终端立刻显示出来,这是给人看的;但串口和单片机通信时,开回显会导致 "发一个字节,终端又把这个字节发回去",造成数据冗余。

又比如 ICANON(规范模式):你输入字符后,按回车才会把数据传给程序,这是给人输入命令用的;但串口通信需要 "收到一个字节就立刻处理",所以必须关掉规范模式,进入 "原始模式"。

总结:c_lflag 是 "为人服务" 的,和外设通信时要把这些 "人性化开关" 全关,让数据能实时、原封不动地传输。

**输出模式标志位c_oflag:**管 "数据出去前" 的后处理

c_iflag 对应,数据从系统传到硬件输出前,c_oflag 决定要不要做格式转换。

比如开 OPOST + ONLCR,系统输出的换行(NL)会自动转成回车 + 换行(CR+NL),适配那些需要 CR+NL 才换行的终端;

嵌入式场景下,同样会关闭所有输出转换c_oflag 置 0)------ 理由和输入一样:要保证发给单片机的二进制数据是原始的,不被篡改。

**特殊控制字符数组c_cc[NCCS]:**管 "特殊按键的功能"

它不是 "开关",而是定义 "哪些字符代表特殊操作",配合 c_lflag 生效。

比如 VINTR 定义 "中断字符"(默认 Ctrl+C),按这个键就会给程序发终止信号;VMINVTIME 是嵌入式里最常用的 ------ 在关闭规范模式后,VMIN 定义 "最少读多少字节才返回",VTIME 定义 "等多久没数据就超时",通过这两个值可以控制串口读取数据的 "实时性" 和 "等待规则"。
简而言之就是:一般情况下只有c_cflag、c_cc[NCCS]这两个成员需要配置,剩下的三个全部给0即可。

二、termios的核心函数与操作流程

在嵌入式上位机开发中,我们并不需要记住所有的termios库函数,通常只需要掌握"获取配置--->修改配置--->应用配置--->恢复配置"这4个步骤,即可完成绝大部分的串口初始化工作。

(1)get与set函数

这两个函数在正常运行时的返回值都是0,所以通常会检测一下返回值,一旦不为0则需要报错到日志,方便程序员排查错误。

(2)输入、输出波特率设置函数

这里关于波特率的写法,是大写B加上波特率。比如你想要57.6K的波特率,就写作B57600。

(3)串口缓冲区函数

缓冲区是Linux常见的操作,在串口这里也不例外,其实在Linux中的其他很多地方都有体现,比如read、write函数等等。

一般建议大家初始化串口的时候,把两个缓冲区都清空一下再调用 tcsetattr() 应用其他配置。

(4)一键进入原始模式函数

所以我们之前使用AI生成的代码其实过于复杂了,他选择了手动关闭一大堆转换选项,而我们自己做的时候可以直接使用这个官方提供的一键按钮,更加全面更加标准。

以上就是最基本的几个函数了,当然还有几个函数这里没有讨论,主要是一些进阶辅助函数,大家可以在有需要的时候自己查阅man手册。

三、标志位的理解与配置

前面我们已经明白了常见的termios函数的使用方法。但是你有没有注意到,他们主要都在配置一些结构体中没有明确写出(在Linux内核的定义中肯定是有的,但是在手册中不作为主要成员变量)。于是现在我们开始讨论这些明确写出的成员该如何配置。

而根据前面的理解,我们只需要把c_cflag、c_cc[NCCS]配置一下,然后剩余的三个成员都幅值0就好(相当于做了一次更加彻底的一键原始模式的操作)。

(1)特殊控制字符 c_cc[NCCS]

在非规范模式下,VMINVTIME 是控制 read() 行为的核心,直接决定了程序的响应性和效率。他用于控制read的读取行为是阻塞还是非阻塞的。

  • VMIN:最少读取字节数。

    • VMIN = 0:不要求最少字节,read() 会立即返回。
    • VMIN > 0read() 会阻塞,直到收到至少 VMIN 个字节。
  • VTIME:超时时间(单位:0.1 秒)。

    • VTIME = 0:无限等待,永不超时。
    • VTIME > 0:最多等待 VTIME * 0.1 秒,超时则返回。
      黄金配置组合
  • VMIN = 1, VTIME = 0:阻塞等待,直到收到至少 1 个字节,保证必须读到数据。

  • VMIN = 0, VTIME = 10 :最多等待 1 秒,超时返回,避免程序卡死。

(2)控制模式c_cflag

c_cflag 是和单片机通信时必须严格对齐的部分,任何一个位配置错误,都可能导致通信完全失败。

  • CLOCAL :忽略调制解调器控制线。
    • 嵌入式场景下,我们没有电话线和 Modem,必须开启这个位,否则串口会因为检测不到 DCD 等信号而拒绝工作。
  • CREAD :启用接收功能。
    • 这是串口能 "收" 数据的总开关,必须开启,否则 read() 永远读不到数据。
  • CSIZECS8 :数据位掩码与 8 位数据。
    • &= ~CSIZE 清除所有数据位配置,再 |= CS8 设置为 8 位数据位,这是单片机最常用的配置。
  • PARENB / PARODD :校验位使能与奇 / 偶校验。
    • PARENB 开启校验,PARODD 为 1 是奇校验,为 0 是偶校验。嵌入式中常关闭校验(&= ~PARENB)以提高效率。
  • CSTOPB :停止位。
    • 清除该位(&= ~CSTOPB)表示 1 位停止位,置位则为 2 位。
  • CRTSCTS :硬件流控。
    • 嵌入式串口通信几乎不用硬件流控,必须关闭(&= ~CRTSCTS),否则会因等待 RTS/CTS 信号而卡死。

四、完整的配置例子

cpp 复制代码
#include<fcntl.h>
#include<unistd.h>
#include<stdio.h>
#include<errno.h>
#include<termios.h>
#include<string.h>


int configure_uart(int fd) {
    struct termios uart_cfg;
    struct termios old_cfg;

    // 1. 获取当前串口配置(保存原始配置,用于程序退出时恢复)
    if (tcgetattr(fd, &old_cfg) != 0) {
        perror("tcgetattr get old config failed");
        return -1;
    }

    // 2. 清空新配置结构体,避免脏数据
    memset(&uart_cfg, 0, sizeof(struct termios));

    // 3. 核心配置:c_cflag(硬件层,11位帧格式关键)
    uart_cfg.c_cflag |= CLOCAL;    // 本地模式,忽略Modem状态线(嵌入式必开)
    uart_cfg.c_cflag |= CREAD;     // 启用接收器(必须开,否则收不到数据)
    uart_cfg.c_cflag &= ~CSIZE;    // 清空数据位掩码,准备设置8位数据位
    uart_cfg.c_cflag |= CS8;       // 设置8位数据位
    uart_cfg.c_cflag |= PARENB;    // 启用奇偶校验(关键:开启后才会有校验位)
    uart_cfg.c_cflag &= ~PARODD;   // 偶校验(PARODD=1是奇校验,清0是偶校验)
    uart_cfg.c_cflag &= ~CSTOPB;   // 1位停止位(CSTOPB=1是2位,清0是1位)
    // 注:起始位由硬件自动处理,无需配置;11位帧=1起始+8数据+1校验+1停止

    // 4. 设置波特率57600(输入/输出波特率一致)
    if (cfsetispeed(&uart_cfg, B57600) != 0 || cfsetospeed(&uart_cfg, B57600) != 0) {
        perror("set baud rate 57600 failed");
        return -1;
    }

    // 5. 关闭所有数据转换(嵌入式裸传需求,和你要求的清零逻辑一致)
    uart_cfg.c_iflag = 0;    // 关闭输入转换(奇偶校验由硬件处理,软件不干预)
    uart_cfg.c_oflag = 0;    // 关闭输出转换
    uart_cfg.c_lflag = 0;    // 关闭人机交互(回显、规范模式等)

    // 6. 配置非规范模式读取规则(嵌入式常用)
    uart_cfg.c_cc[VMIN] = 1;  // 最少读取1个字节就返回(实时性优先)
    uart_cfg.c_cc[VTIME] = 50; // 超时时间0.5秒(避免无限等待,可根据需求调整)

    // 7. 应用配置(TCSANOW:立即生效;TCSAFLUSH:清空缓冲区后生效,可选)
    if (tcsetattr(fd, TCSANOW, &uart_cfg) != 0) {
        perror("tcsetattr apply config failed");
        // 恢复原始配置
        tcsetattr(fd, TCSANOW, &old_cfg);
        return -1;
    }

    printf("UART配置成功:57600波特率、8数据位、偶校验、1停止位(11位帧)\n");
    return 0;
}

int main()
{
    int fd=open("/dev/ttyUSB0",O_RDWR | O_NOCTTY);
    if(fd==-1)
    {
        perror("打开串口失败");
        // 【修改3】打开失败后直接退出,避免后续操作无效fd
        return -1;
    }

    configure_uart(fd);

    // 读取串口信息:原代码仅读1次,无阻塞/循环,大概率读不到数据
    char buffer[100];
    // 补充:打印提示,告知用户程序在等待数据
    printf("等待51单片机发送数据...\n");
    int len=read(fd,buffer,16);  // 读16字节(匹配buffer大小,无问题)
    
    if(len>0)
    {
        buffer[len]='\0';
        // 优化:打印原始十六进制,便于核对51的SBUF数据(字符串可能有不可见字符)
        printf("读取到 %d 字节数据:\n", len);
        printf("字符串形式:%s\n", buffer);
        printf("十六进制形式:");
        for(int i=0; i<len; i++) {
            printf("0x%02X ", (unsigned char)buffer[i]);
        }
        printf("\n");
    }
    else if(len==0)
    {
        printf("未读取到数据(串口无数据发送)\n");
    }
    else
    {
        // 优化:打印具体错误原因,便于排查
        perror("读取串口失败");
    }
    
    close(fd);
    return 0;
}

最后的结果也与我们预料的一致,成功与单片机串口通信。关于这篇文章并不是说要求大家以后自己手动编写这些配置,而是有个大概了解,基本上在使用的时候可以让AI帮你写。

然后我自己在测试的时候发现有时候57600波特率数据正常,有时候却是错误的,后续为了首先保证数据稳定性,我改成了19200波特率。关于这个问题,我觉得主要是因为51的波特率发生器是复用了定时器T1,可能不太稳定,而STM32中有独立的硬件波特率发生器,更加稳定。也有可能是我第二次测试时用的是eide的sdcc编译器做的,而这里默认是12MHZ,与单片机的11.0592MHZ不匹配,产生了细小偏移量。

不过经过测试发现,EIDE写的12MHZ只是给人看的,并不会对编译结果造成任何影响。因为编译器无论是多少频率的晶振,编译出的机器码都完全一样,真正影响精确度和频率的是硬件晶振的稳定性,所以极大可能就是因为51的晶振不稳定,导致波特率发生器不稳定,最终影响到串口通信。

相关推荐
隐退山林2 小时前
JavaEE初阶:网络原理之TCP/IP协议(一)
服务器·网络·tcp/ip
XerCis2 小时前
Linux内网环境无法访问外网的情况下安装程序
linux·运维·服务器
dajun1811234562 小时前
音乐制作从创作到发行完整流程图表怎么画
大数据·运维·人工智能·信息可视化·架构·流程图·能源
炸炸鱼.2 小时前
linux系统安全及应用_扫描版
linux·运维·系统安全
流水迢迢lst2 小时前
靶场练习day12--SSRF
服务器·网络·安全
艾文-你好2 小时前
深信服SSL aTrust设备密码重置及管理密码重置
linux·服务器·ssl
WHD3062 小时前
苏州华为/联想/浪潮 国产服务器 硬件维修
运维·服务器·git
百结2142 小时前
Linux系统安全
linux·运维·服务器
程序员敲代码吗2 小时前
DVR设备FTP更新故障及修复指南
服务器·开发语言·php