资讯动态

嵌入式UART串口驱动开发实战:从协议底层到Linux tty与DMA避坑指南

发布时间:2026/10/9 13:48:18 来源:尧图企业网站定制
1. 为什么UART值得单独拿出来讲嵌入式开发里UART串口大概是每个工程师最早接触、也最容易被低估的外设。说它简单两根线就能通数据说它复杂从波特率误差到DMA接收丢首帧从TTL电平到RS485差分传输从裸机轮询到Linux tty子系统每一层都能挖出足够写一篇长文的内容。我做了十多年驱动开发调试过的串口问题少说也有几百个从8位单片机到ARM Cortex-A系列从裸跑到Linux内核UART几乎是贯穿始终的那根线。这篇内容面向的是有一定嵌入式基础、正在做或准备做UART驱动开发的工程师。不管你是用STM32 HAL库做串口收发还是在Linux下写tty驱动或者用FPGA自己实现一个UART收发模块这里面的思路和踩坑经验都能直接参考。我会从协议底层讲到驱动实现从裸机讲到操作系统从硬件电平讲到软件封装尽量把每个关键决策背后的逻辑说清楚。先明确一个定位UART是异步串行通信没有时钟线靠双方约定的波特率来同步。这个“异步”两个字决定了它所有的设计取舍。没有时钟线意味着成本低、接线少但也意味着对时序精度敏感、对起始位检测依赖强。理解了这一点后面很多问题就顺理成章了。2. UART协议核心细节拆解2.1 帧结构与时序逻辑UART的一帧数据由起始位、数据位、校验位、停止位组成。起始位是逻辑0持续一个位时间接收方检测到下降沿后开始按波特率采样。数据位通常是5到9位LSB先行。校验位可选奇校验、偶校验、无校验。停止位是逻辑1可以是1位、1.5位或2位。这里有个容易被忽略的点接收方并不是在起始位下降沿立刻采样而是在检测到下降沿后等待半个位时间再开始采样第一个数据位。这个“半位等待”是为了让采样点落在每个位周期的中间最大化容错空间。你可以这样理解如果采样点偏前或偏后一旦累积误差超过半个位时间就会采错。所以波特率误差的容忍度大约是±2%到±3%具体取决于帧长度和采样策略。实际项目中我一般建议波特率误差控制在1%以内。怎么算假设系统时钟是72MHz你要115200波特率分频系数是72000000/(16115200)39.0625。如果取39实际波特率是72000000/(1639)115384误差0.16%完全够用。但如果取38误差就超过2%了长帧传输可能出错。所以配置寄存器时一定要算清楚别随便填个近似值。2.2 电平标准与物理层差异TTL电平的UART是0V到3.3V或5V直接连MCU引脚。RS232是负逻辑-3V到-15V表示逻辑13V到15V表示逻辑0需要电平转换芯片。RS485是差分信号抗干扰能力强适合长距离和多点通信。热词里有人问“TTL UART通过光耦能传多远”这个问题很实际。光耦的作用是隔离不是放大。传输距离取决于光耦的响应速度和驱动电流。普通PC817光耦波特率超过9600就开始失真因为它的上升沿和下降沿时间在微秒级。要传更远或更快得用高速光耦如6N137配合合适的限流电阻。但即便如此TTL加光耦的距离也就几十米量级再远就得转RS485或光纤。RS485的传输距离和波特率成反比。100kbps以下可以到1200米115200bps大概能到几百米。实际布线时双绞线、终端电阻、共地处理缺一不可。我见过太多现场问题是因为没接终端电阻或者地电位差太大导致的通信不稳定。2.3 流控机制什么时候需要RTS/CTS硬件流控用RTS和CTS两根线。发送方拉低RTS表示“我要发数据”接收方拉低CTS表示“我准备好了”。如果接收方缓冲区快满了就拉高CTS让发送方暂停。软件流控用XON/XOFF字符在数据流里插入特殊字符来控制。什么时候用流控当你的接收方处理速度跟不上发送方且数据不能丢的时候。比如MCU通过串口往PC传大量日志PC端程序如果偶尔卡顿没有流控就会丢数据。但流控也有代价多两根线协议复杂度增加而且如果流控线本身受干扰反而添乱。我的经验是短距离、低波特率、数据量不大时不用流控高速率或大数据量时优先考虑DMA加环形缓冲区流控作为辅助。3. 裸机驱动开发实操3.1 初始化配置的完整流程以STM32F103为例用HAL库初始化UART的步骤大致如下。首先使能GPIO和UART时钟配置TX为复用推挽输出RX为浮空输入或上拉输入。然后设置UART参数波特率、字长、停止位、校验位、模式、硬件流控。最后使能UART。但这里有几个细节值得展开。第一GPIO速度要设对。TX引脚如果速度设太低高波特率下波形上升沿变缓可能导致接收方采样错误。一般设50MHz或最高档。第二RX引脚建议开上拉防止悬空时收到乱码。第三如果用到DMA要在UART初始化之后配置DMA通道并把UART的DR寄存器地址作为外设地址。我习惯在初始化完成后加一段自检代码发送一个已知字节然后检查TXE和TC标志是否正常置位。这能快速判断时钟和引脚配置有没有问题。3.2 中断收发与环形缓冲区设计轮询方式简单但效率低中断方式更实用。发送中断在TXE置位时触发往DR写下一个字节。接收中断在RXNE置位时触发从DR读数据存入缓冲区。但直接在中断里处理数据是大忌。正确做法是中断只负责搬运数据到环形缓冲区主循环或任务去消费。环形缓冲区的大小要根据数据速率和处理周期来定。假设波特率115200每秒最多11520字节主循环每10ms处理一次那缓冲区至少要116字节。实际我会留2到4倍余量。环形缓冲区的实现有个经典陷阱读写指针相等时是空还是满解决办法是牺牲一个字节空间或者额外维护一个计数变量。我倾向于用计数变量逻辑更清晰。下面是一个简化的C语言实现思路typedef struct { uint8_t buffer[256]; volatile uint16_t head; volatile uint16_t tail; volatile uint16_t count; } ring_buffer_t; int ring_buffer_put(ring_buffer_t *rb, uint8_t data) { if (rb-count sizeof(rb-buffer)) return -1; rb-buffer[rb-head] data; rb-head (rb-head 1) % sizeof(rb-buffer); rb-count; return 0; } int ring_buffer_get(ring_buffer_t *rb, uint8_t *data) { if (rb-count 0) return -1; *data rb-buffer[rb-tail]; rb-tail (rb-tail 1) % sizeof(rb-buffer); rb-count--; return 0; }注意head和tail要用volatile修饰因为它们在中断和主循环中都会被访问。count的增减要保证原子性在8位或16位MCU上如果count是16位读写可能是非原子的需要在临界区保护。3.3 DMA接收的配置与首帧丢失问题热词里有人提到“stm32f103使用hal库串口dma接收首帧丢失”这是个非常典型的问题。原因通常是DMA通道使能顺序不对或者没有清除UART的接收标志。正确的初始化顺序是先配置UART再配置DMA使能DMA通道最后使能UART的接收。如果先使能UART再配置DMA第一个字节到达时DMA还没准备好就会丢。另外HAL库的HAL_UART_Receive_DMA函数内部会做一系列操作如果之前有残留的RXNE标志也可能导致问题。我一般会在初始化后手动读一次DR寄存器清标志。还有一个隐蔽的坑DMA传输完成中断里如果直接重新启动DMA接收中间有个窗口期可能丢数据。解决办法是用双缓冲模式或者用DMA的空闲中断IDLE来触发数据处理而不是等传输完成。STM32的UART支持IDLE中断总线空闲一个字节时间后触发非常适合不定长数据的接收。4. Linux下的UART驱动与调试4.1 tty子系统与串口设备节点Linux把串口抽象成tty设备设备节点通常是/dev/ttyS0、/dev/ttyAMA0、/dev/ttyUSB0等。内核里的串口驱动分为两层上层是tty核心下层是具体硬件的serial driver。写驱动时主要实现uart_ops结构体里的函数比如startup、shutdown、start_tx、stop_tx、set_termios等。调试时常用的命令有dmesg | grep tty查看串口注册信息ls /dev/tty*列出设备节点stty -F /dev/ttyS0 -a查看当前配置。要修改波特率可以用stty -F /dev/ttyS0 115200。如果串口被占用lsof /dev/ttyS0或fuser /dev/ttyS0可以查出是哪个进程。热词里有人问“win7下怎么查看串口被哪个程序占用”Windows下可以用Process Explorer或者handle.exe但更简单的是在设备管理器里看端口号然后用mode命令查看状态。不过Windows的串口占用检测确实不如Linux方便这是事实。4.2 从串口接收数据丢失的排查思路Linux下串口丢数据常见原因有几个。第一缓冲区溢出。内核的tty缓冲区默认不大如果应用层读取不及时数据就丢了。可以用cat /proc/tty/driver/serial查看溢出计数。解决办法是增大缓冲区或提高读取优先级。第二中断共享或延迟。如果串口中断和其他高优先级中断共享可能被延迟处理。可以看/proc/interrupts确认中断号然后用chrt或taskset调整应用线程的调度策略。第三DMA配置问题。有些平台的串口DMA需要正确配置突发长度和FIFO阈值否则高速率下会丢。这个得查具体芯片手册。第四流控没开。如果发送方速度快接收方没流控丢数据是必然的。硬件流控要确保RTS/CTS线接对软件流控要双方都支持。我一般按这个顺序排查先看溢出计数再看中断统计然后检查流控配置最后用示波器看波形质量。4.3 串口封装与上层应用接口在应用层我习惯把串口操作封装成统一的接口屏蔽底层差异。比如提供一个serial_open、serial_config、serial_read、serial_write、serial_close的API。配置参数用结构体传递包括设备路径、波特率、数据位、停止位、校验位、流控。读取时要注意超时处理。如果直接用read阻塞程序可能卡死。我一般用select或poll加超时或者设置VMIN和VTIME。VMIN是至少读取的字节数VTIME是等待时间单位0.1秒。比如VMIN0、VTIME10表示最多等1秒有数据就返回。写入时要注意部分写的问题。write返回的字节数可能小于请求的字节数需要循环写直到全部完成。另外写入后最好用tcdrain等待数据真正发送出去再关闭设备。5. 跨平台与特殊场景实战5.1 FPGA实现UART发送ASCII字符串用Verilog实现UART发送核心是波特率发生器和状态机。波特率发生器就是一个计数器计数到分频值时翻转。状态机负责加载数据、移位输出、插入起始位和停止位。发送ASCII字符串时通常用一个FIFO或ROM存储字符串状态机从FIFO读数据逐字节发送。关键点是时序起始位要精确一个位时间数据位按LSB顺序输出停止位保持高电平。如果波特率是115200系统时钟50MHz分频系数是50000000/115200≈434。实际取434误差0.02%没问题。调试时可以用SignalTap或ILA抓波形看起始位、数据位、停止位是否对齐。常见问题是状态机在停止位还没结束时就跳回空闲导致下一帧起始位提前。解决办法是停止位也用一个完整的位时间计数器。5.2 ESP32在PlatformIO下的串口输出配置用VSCode加PlatformIO开发ESP32串口输出默认是UART0引脚GPIO1和GPIO3。在platformio.ini里可以配置monitor_speed来设置串口监视器的波特率。代码里用Serial.begin(115200)初始化Serial.println输出。如果想用其他UART比如UART1或UART2可以指定引脚。ESP32的UART支持引脚矩阵几乎任意GPIO都能映射。但要注意有些引脚在启动时有特殊功能比如GPIO0是启动模式选择GPIO2是启动日志输出用之前要查清楚。Arduino框架下Serial是硬件串口Serial1和Serial2是额外的。如果用USB转串口芯片比如CH340或FT232R需要装驱动。Linux下一般自带CH340驱动Windows下可能要手动装。装完驱动后设备节点是/dev/ttyUSB0或COMx。5.3 虚拟机串口配置与透传在VMware或VirtualBox里配置串口可以把宿主机的物理串口映射给虚拟机也可以创建虚拟串口对。VMware里在虚拟机设置中添加串口选择“使用物理串口”或“使用命名管道”。命名管道的方式可以在两个虚拟机之间或虚拟机和宿主机之间通信。配置时要注意波特率和流控要和宿主机一致。如果宿主机串口被占用虚拟机就映射不了。另外虚拟机的串口中断延迟可能比物理机大高速率下容易丢数据。如果只是调试115200以下问题不大。5.4 RS485与多机通信RS485半双工通信需要控制收发方向。通常用一个GPIO控制收发器的DE/RE引脚。发送前拉高DE发送完拉低。这里的关键是发送完成的判断不能只看TXE要看TC传输完成标志确保最后一个字节的停止位也发完了再切换方向。多机通信时每个从机有地址主机发地址帧从机比对地址后决定是否响应。协议可以自定义也可以用Modbus RTU。Modbus RTU的帧间隔是3.5个字符时间用来区分帧边界。实现时用定时器检测总线空闲时间超过3.5个字符时间就认为一帧结束。RS485的终端电阻在总线两端各接一个120欧姆电阻。中间节点不要接。如果通信距离短、节点少不接也能凑合但长距离或高速率时必须接。共地也很重要如果各地电位差太大收发器可能损坏可以用隔离型收发器。6. 常见问题速查与避坑经验6.1 串口调试常见问题排查表现象可能原因排查方法解决措施收不到任何数据TX/RX接反交换两根线测试确认交叉连接收到乱码波特率不匹配核对双方波特率统一波特率并计算误差偶尔丢数据缓冲区溢出查看溢出计数增大缓冲区或加流控首字节丢失DMA使能顺序检查初始化流程先配DMA再使能UART长距离通信失败电平不匹配测量信号幅度加电平转换或转RS485中断不触发中断未使能检查NVIC配置使能对应中断向量发送完成标志不置位时钟未使能检查RCC配置使能UART时钟串口被占用其他进程打开lsof或fuser关闭占用进程6.2 实操心得与避坑技巧第一个心得永远不要相信“默认配置”。很多开发板的例程里波特率是9600但实际项目可能需要115200甚至更高。改波特率时记得同步改上位机和所有相关设备。第二个心得示波器是串口调试的终极武器。当软件层面查不出问题时用示波器看TX和RX波形测量位宽、上升沿时间、噪声。我遇到过因为电源纹波导致串口误码的情况软件查了半天示波器一测就发现了。第三个心得环形缓冲区的大小要留足余量。我一般按最大数据速率的2到4倍来设计。比如115200波特率每秒11520字节如果主循环10ms处理一次缓冲区至少116字节实际用256或512。第四个心得DMA接收一定要用IDLE中断。等DMA传输完成再处理中间可能丢数据。IDLE中断在总线空闲时触发能及时处理不定长数据。第五个心得Linux下串口权限问题很常见。普通用户默认没有/dev/ttyS0的读写权限要么用sudo要么把用户加入dialout组。后者更安全。第六个心得跨平台开发时串口API差异很大。Windows用CreateFile和ReadFileLinux用open和read。封装一层抽象接口能省很多事。6.3 工具选型参考串口调试助手方面Windows下常用的有SSCOM、XCOM、友善串口调试助手。Linux下可以用minicom、picocom、screen。我個人偏好picocom轻量、命令行、支持脚本。USB转串口芯片CH340便宜但驱动兼容性一般FT232R稳定但贵CP2102居中。如果做产品建议用FT232或CP2102少很多驱动麻烦。逻辑分析仪方面Saleae的软件好用但硬件贵国产的DSLogic性价比高。抓UART波形时设置采样率至少是波特率的10倍以上否则可能采不准。7. 从驱动到应用的完整链路思考UART驱动开发不只是配置寄存器那么简单。从硬件电平到协议帧从中断处理到缓冲区管理从内核驱动到应用接口每一层都有它的设计考量和取舍。我见过很多项目底层驱动写得没问题但应用层读取不及时导致丢数据最后排查了半天才发现是上层的问题。所以做UART开发要有全链路的视角。硬件上确认电平匹配、接线正确、终端电阻合适驱动上确认初始化顺序、中断优先级、DMA配置应用上确认缓冲区大小、读取周期、流控策略。任何一层出问题表现都是“串口不通”或“丢数据”但根因可能完全不同。最后分享一个我常用的调试方法在驱动层加统计计数器记录发送字节数、接收字节数、溢出次数、校验错误次数。应用层定期打印这些统计一旦发现溢出或错误增长就能快速定位问题方向。这个方法在多个项目中帮我省了大量排查时间。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑