嵌入式串口编程termios 配置与设备数据收发一、引言在嵌入式开发中串口UART是祖师爷级别的通信接口——它古老、简单、可靠。MCU 通过串口和 ESP8266/4G 模块通信传感器通过 RS485 总线组网GPS 模块通过 TTL 串口输出 NMEA 报文……可以说没搞过串口编程就不算真正做过嵌入式。在 Linux 下串口设备以/dev/ttyS0、/dev/ttyUSB0等形式呈现。操作串口本质上还是文件 IO——打开、配置、读写、关闭——但配置环节涉及termios结构体参数奇多稍有不慎就明明接线正确却死活收不到数据。本文带你彻底弄懂。二、串口设备节点节点路径含义/dev/ttyS0板载硬件串口 0SOC 原生 UART/dev/ttyS1板载硬件串口 1/dev/ttyUSB0USB 转串口适配器如 CH340、CP2102/dev/ttyAMA0ARM 平台原生串口树莓派常见查看设备可用的串口ls-l/dev/tty*dmesg|greptty三、termios 结构体串口的控制面板termios是 POSIX 标准定义的终端 IO 配置结构体串口的波特率、数据位、校验位、停止位、流控等全在这里配置。#includetermios.hstructtermios{tcflag_tc_iflag;// 输入模式标志tcflag_tc_oflag;// 输出模式标志tcflag_tc_cflag;// 控制模式标志波特率、数据位等tcflag_tc_lflag;// 本地模式标志cc_tc_cc[NCCS];// 特殊控制字符};对嵌入式串口编程来说最关键的字段是c_cflag它控制着串口的硬件参数。常见配置项速查// 波特率cfsetispeed(options,B115200);// 输入波特率cfsetospeed(options,B115200);// 输出波特率// 数据位options.c_cflag~CSIZE;// 清除数据位设置options.c_cflag|CS8;// 8 位数据位// 校验位options.c_cflag~PARENB;// 无校验// options.c_cflag | PARENB; // 有校验// options.c_cflag | PARODD; // 奇校验默认偶校验// 停止位options.c_cflag~CSTOPB;// 1 位停止位// options.c_cflag | CSTOPB; // 2 位停止位// 硬件流控options.c_cflag~CRTSCTS;// 关闭硬件流控// options.c_cflag | CRTSCTS; // 开启硬件流控波特率常量映射常量波特率B96009600B1920019200B3840038400B5760057600B115200115200B230400230400B460800460800B921600921600四、串口读取的三种模式在c_cc数组中有两个特殊控制字符决定串口读取的行为VMIN最少读取字节数0VTIME超时时间单位 0.1 秒0VMINVTIME行为00非阻塞有数据就读没数据立即返回 000纯超时模式最多等 VTIME×100ms有数据就返回00阻塞模式必须读到 VMIN 个字节才返回00混合模式至少读 VMIN 字节或超过 VTIME嵌入式最常用的模式是 VMIN0, VTIME0options.c_cc[VMIN]1;// 至少读 1 个字节options.c_cc[VTIME]10;// 最多等 1 秒这样不会无限期阻塞也不会 CPU 空转。五、完整串口配置代码#includestdio.h#includestdlib.h#includestring.h#includeunistd.h#includefcntl.h#includeerrno.h#includetermios.h/* 配置串口 */intuart_init(constchar*device,intbaud){intfdopen(device,O_RDWR|O_NOCTTY|O_NDELAY);if(fd0){perror(open);return-1;}// 获取当前配置structtermiosoptions;if(tcgetattr(fd,options)!0){perror(tcgetattr);close(fd);return-1;}// ---------- 设置波特率 ----------speed_tspeedB115200;switch(baud){case9600:speedB9600;break;case19200:speedB19200;break;case38400:speedB38400;break;case57600:speedB57600;break;case115200:speedB115200;break;case921600:speedB921600;break;default:speedB115200;break;}cfsetispeed(options,speed);cfsetospeed(options,speed);// ---------- 控制标志 ----------options.c_cflag|(CLOCAL|CREAD);// 允许接收options.c_cflag~CSIZE;options.c_cflag|CS8;// 8 数据位options.c_cflag~PARENB;// 无校验options.c_cflag~CSTOPB;// 1 停止位options.c_cflag~CRTSCTS;// 关闭硬件流控// ---------- 本地标志 ----------options.c_lflag~(ICANON|ECHO|ECHOE|ISIG);// ICANON0: 关闭规范模式原始模式适合二进制数据// ECHO0: 不回显// ---------- 输入标志 ----------options.c_iflag~(IXON|IXOFF|IXANY);// 关闭软件流控options.c_iflag~(INLCR|ICRNL|IGNCR);// 不转换换行符// ---------- 输出标志 ----------options.c_oflag~OPOST;// 原始输出不做处理// ---------- 超时设置 ----------options.c_cc[VMIN]1;// 至少读 1 字节options.c_cc[VTIME]5;// 0.5 秒超时// 刷新缓冲区并应用配置tcflush(fd,TCIFLUSH);if(tcsetattr(fd,TCSANOW,options)!0){perror(tcsetattr);close(fd);return-1;}printf(串口 %s 配置成功, 波特率%d\n,device,baud);returnfd;}/* 读取一行数据以 \n 或 \r\n 结尾 */intuart_readline(intfd,char*buf,intmax_len){inti0;while(imax_len-1){charch;ssize_tnread(fd,ch,1);if(n0){if(n0)continue;// 超时return-1;// 错误}buf[i]ch;if(ch\n)break;}buf[i]\0;returni;}intmain(void){intfduart_init(/dev/ttyUSB0,115200);if(fd0)return-1;charbuf[256];while(1){memset(buf,0,sizeof(buf));intlenuart_readline(fd,buf,sizeof(buf));if(len0){buf[strcspn(buf,\r\n)]\0;// 去掉换行符printf([串口数据] %s\n,buf);}}close(fd);return0;}六、串口通信调试工具箱开发串口程序常常遇到明明发了数据、对端就是收不到以下是排查武器# 1. 用 stty 查看当前串口配置stty-F/dev/ttyUSB0-a# 2. 用 echo/cat 快速收发测试# 终端1监听cat/dev/ttyUSB0# 终端2发送echohello/dev/ttyUSB0# 3. 用 minicom 交互调试minicom-D/dev/ttyUSB0-b115200# 4. 用逻辑分析仪抓波形终极武器# 确认 TX/RX 线是否接对波特率是否一致七、常见故障排查现象可能原因没有任何数据TX/RX 接反地线没接波特率不一致收到乱码波特率不匹配校验位/停止位设置不一致数据丢字节缓冲区溢出VTIME 设置太小只能收不能发流控设置问题CRTSCTS 开启但硬件没接线read 无限阻塞VMIN0 且 VTIME0termios未正确设置八、总结Linux 串口编程的核心在于理解和配置termios结构体打开设备O_RDWR | O_NOCTTY | O_NDELAY配置参数波特率、数据位 8、无校验、1 停止位、无流控原始模式关闭ICANON、ECHO避免终端处理干扰超时设置VMIN1, VTIME5是嵌入式黄金配置调试三板斧stty看配置、minicom交互测、逻辑分析仪抓波形把这段初始化代码收到你的工具库中以后每次启动一个串口项目拿出来直接套就行。