目录
[(1)特殊控制字符 c_cc[NCCS]](#(1)特殊控制字符 c_cc[NCCS])
在上篇文章中,我们借助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),按这个键就会给程序发终止信号;VMIN和VTIME是嵌入式里最常用的 ------ 在关闭规范模式后,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]
在非规范模式下,VMIN 和 VTIME 是控制 read() 行为的核心,直接决定了程序的响应性和效率。他用于控制read的读取行为是阻塞还是非阻塞的。
VMIN:最少读取字节数。
VMIN = 0:不要求最少字节,read()会立即返回。VMIN > 0:read()会阻塞,直到收到至少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()永远读不到数据。CSIZE与CS8:数据位掩码与 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的晶振不稳定,导致波特率发生器不稳定,最终影响到串口通信。




