ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

UART串口通信全解析:从STM32到FPGA的实战指南

UART串口通信全解析:从STM32到FPGA的实战指南 1. 为什么UART是嵌入式工程师的第一课也是最后一课搞嵌入式这行十来年如果让我选一个最能体现基本功的外设我会毫不犹豫地选UART。它简单到两根线就能跑通又复杂到能让工作五年的老手在凌晨三点对着示波器骂娘。你去看任何一个芯片的参考手册UART那一章永远是最早被翻烂的但也是最容易被轻视的——很多人觉得不就是收发几个字节嘛结果项目里第一个卡住量产的问题往往就出在串口上。这一期咱们就把UART串口从里到外扒一遍。不管你是刚拿到STM32开发板的新手还是正在调试GD32F470VET6这种高端货的老鸟或者是用FPGA实现UART发送ASCII字符串的硬核玩家这篇内容都能让你找到能直接抄作业的东西。我会从协议本质讲起把STM32、GD32、Linux、FPGA几个平台的UART实现串起来重点聊那些手册上不会写、但实际调试中一定会遇到的坑——比如串口DMA为什么丢数据、CH340驱动装不上怎么办、Win7下怎么查串口被哪个程序占用、TTL UART通过光耦到底能传多远这些具体问题。先给个结论UART这东西入门只要十分钟精通需要十年。它的核心价值不在于能通信而在于它是整个嵌入式系统的调试命脉。你想想板子跑起来第一件事是不是printf固件升级是不是靠串口下载设备现场出问题是不是先接串口看log所以我说UART是嵌入式工程师的第一课也是最后一课——你永远在跟它打交道永远能发现新的坑。2. UART协议的本质异步、全双工、起止式传输2.1 两根线背后的时序逻辑UART的全称是Universal Asynchronous Receiver/Transmitter通用异步收发器。注意异步这两个字这是理解一切UART问题的钥匙。所谓异步就是收发双方没有共享时钟线全靠事先约定好的波特率来对时。这就好比你跟朋友约好每天早上七点打电话没有闹钟提醒全靠各自的生物钟——只要有一个人的表快了或慢了通话就会错乱。具体到电气层面UART空闲时线路上是高电平起始位是一个波特率周期的低电平接着是5到9个数据位通常8位可选的校验位最后是1到2个停止位的高电平。接收方检测到下降沿就开始采样采样点通常在位周期的中间位置这样能最大程度避开边沿抖动。我见过太多人调试时波形看着差不多但就是收不到正确数据十有八九是波特率误差累积导致的采样点偏移。这里有个经验公式波特率误差要控制在2%以内最好在1%以内。比如你用8MHz晶振配9600波特率分频系数算出来是52.08取整52的话实际波特率是9615误差0.16%完全没问题。但如果用内部RC振荡器温漂可能就有3%这时候通信就会时好时坏。所以我的建议是只要条件允许UART通信一定要用外部晶振别省那两个电容的钱。2.2 全双工与半双工的接线差异标准UART是全双工的TX接对方的RXRX接对方的TX再加上GND三根线就能双向通信。但实际项目中经常遇到单线半双工的场景比如某些传感器或者RS485总线。这时候怎么和全双工设备连接答案是加一个方向控制电路或者用带方向自动切换的收发器。我遇到过最坑的情况是一个客户把两个全双工设备的TX接在一起、RX接在一起然后问我为什么通信不了。这相当于两个人同时对着对方喊话谁都听不清。正确的做法是交叉连接或者用交叉线序。如果你不确定拿万用表量一下TX对GND的电压在空闲时应该是高电平3.3V或5VRX端同理。2.3 校验位与停止位的实际选择校验位这东西理论上能检测单比特错误但实际项目中我基本不用。为什么因为现代通信环境里干扰往往是突发性的多比特错误奇偶校验根本无能为力。而且加了校验位数据位就得从8位降到7位对于传输二进制数据来说非常不方便。所以我的默认配置永远是8数据位、无校验、1停止位也就是常说的8N1。停止位选1还是2绝大多数情况选1就够了。选2的唯一场景是接收方处理速度特别慢需要额外时间准备下一个字节。但这种情况现在很少见了因为现代MCU的UART都有FIFO或者DMA处理速度不是瓶颈。3. STM32/GD32平台UART驱动的完整实现路径3.1 从寄存器到HAL库三种开发方式的取舍在STM32或GD32上搞UART你有三条路可以走直接操作寄存器、用标准外设库、用HAL库。我这些年三种都用过说说各自的适用场景。直接操作寄存器适合对时序要求极其苛刻的场景比如你要在某个精确的时刻发送数据或者需要极致优化中断响应时间。但缺点是移植性差换个芯片就得重写。标准外设库是ST早期的方案现在新项目基本不用了但很多老代码还在维护。HAL库是ST现在主推的配合STM32CubeMX可以图形化配置开发效率最高但代码体积大、执行效率略低。我的建议是新项目直接用HAL库别纠结那点效率损失。现在MCU的Flash和RAM都够大开发效率比运行效率重要得多。但你要理解HAL库底层做了什么否则出了问题根本无从下手。以STM32F103串口打印为例用CubeMX配置USART1波特率1152008N1开启中断。生成的代码里HAL_UART_Transmit()是阻塞发送HAL_UART_Transmit_IT()是中断发送HAL_UART_Transmit_DMA()是DMA发送。新手最容易犯的错误是在中断里调用阻塞发送函数结果整个系统卡死。记住一个原则中断里只做标记和搬运不做等待。3.2 串口DMA的正确打开方式串口DMA是提升系统效率的利器但也是丢数据的重灾区。我见过太多人配置了DMA发送结果发现数据发不全或者接收时丢包。根本原因通常有三个DMA缓冲区被覆盖、DMA传输完成标志没清除、中断优先级配置不当。先说发送。用DMA发送时CPU把数据丢给DMA就返回了DMA在后台慢慢发。但如果你在DMA还没发完的时候又调用了一次发送函数就会覆盖缓冲区。正确的做法是维护一个发送队列或者用HAL_UART_GetState()检查状态。更稳妥的方案是用HAL_UART_Transmit_DMA()配合传输完成回调在回调里发下一个包。再说接收。串口接收DMA最坑的地方是不知道对方发了多少数据。DMA是按固定长度搬运的但串口数据是流式的。解决方案有两种一是用空闲中断IDLE Interrupt当总线空闲一个字节时间后触发中断这时候读取DMA剩余计数就能知道收到了多少数据二是用DMA的循环模式配合环形缓冲区。我个人推荐空闲中断方案代码简洁实时性好。// STM32串口DMA接收空闲中断的核心代码 void USART1_IRQHandler(void) { if(__HAL_UART_GET_FLAG(huart1, UART_FLAG_IDLE)) { __HAL_UART_CLEAR_IDLEFLAG(huart1); HAL_UART_DMAStop(huart1); uint16_t recv_len BUFFER_SIZE - __HAL_DMA_GET_COUNTER(huart1.hdmarx); // 处理recv_len个字节的数据 process_data(rx_buffer, recv_len); HAL_UART_Receive_DMA(huart1, rx_buffer, BUFFER_SIZE); } }这段代码我用了好几年在各种STM32和GD32芯片上都跑得很稳。注意__HAL_DMA_GET_COUNTER返回的是剩余未传输的字节数用总长度减去它就是已接收的长度。3.3 GD32F470VET6的串口配置差异GD32F470VET6是兆易创新的高性能MCU主频能到240MHz串口资源也很丰富。但它的库函数和STM32的HAL库有差异不能直接照搬。比如GD32的固件库叫GD32F4xx_Firmware_Library函数命名风格是usart_baudrate_set()这种而不是HAL的HAL_UART_Init()。我在GD32F470上踩过的一个坑是它的串口时钟使能和STM32不一样。STM32的USART1挂在APB2上GD32的USART0对应STM32的USART1也是APB2但有些串口挂在APB1上配置时容易搞混。还有中断向量表的名字也不同STM32叫USART1_IRQHandlerGD32叫USART0_IRQHandler。移植代码的时候这些细节必须一个一个核对。另外GD32F470支持更高的波特率理论上能到十几兆。但实际用的时候超过1M波特率就要考虑信号完整性问题了线太长或者没有阻抗匹配误码率会飙升。4. Linux与Android平台的串口编程实战4.1 Linux串口设备节点与权限管理在Linux下搞串口第一步是找到设备节点。常见的命名有/dev/ttyS0原生串口、/dev/ttyUSB0USB转串口、/dev/ttyACM0CDC类设备。用ls /dev/tty*能看到所有串口设备用dmesg | grep tty能看到内核识别串口时的日志。Ubuntu下查看串口设备的命令我常用这几个# 查看所有串口设备 ls -l /dev/ttyS* /dev/ttyUSB* /dev/ttyACM* # 查看串口驱动信息 dmesg | grep -i tty # 查看串口参数 stty -F /dev/ttyUSB0 -a # 设置串口参数 stty -F /dev/ttyUSB0 115200 cs8 -cstopb -parenb权限问题是最常见的坑。普通用户默认没有串口设备的读写权限要么用sudo要么把用户加到dialout组里sudo usermod -aG dialout $USER然后重新登录生效。我见过有人用chmod 777解决这在开发阶段可以但生产环境绝对不行安全风险太大。4.2 Linux串口接收数据丢失的排查思路Linux从串口接收数据丢失这个问题在热词里出现了说明很多人遇到过。我总结的原因通常有这几类第一类是缓冲区溢出。Linux的串口驱动有内核缓冲区默认大小可能只有4KB。如果应用层读取不及时数据就会被覆盖。解决办法是用termios结构体设置VMIN和VTIME或者用select/poll/epoll做多路复用确保有数据就立刻读。第二类是波特率不匹配。Linux的波特率设置有个坑非标准波特率需要用termios2结构体和TCGETS2/TCSETS2ioctl标准的cfsetispeed只支持有限几种波特率。如果你要用921600这种就得用特殊方法。第三类是USB转串口芯片的延迟。FT232R、CH340这些芯片内部有缓冲区默认可能有十几毫秒的延迟。可以通过修改驱动参数或者用setserial命令调整。FTDI芯片可以用ftdi_sio模块的latency_timer参数设成1ms能显著降低延迟。4.3 Android板子做串口通讯为什么这么麻烦Android本质上也是Linux但它的权限管理和标准Linux差别很大。普通Android应用没有权限直接访问/dev/ttyS*除非设备已经root或者应用有系统签名。这就是为什么很多人觉得Android板子做串口通讯特别麻烦。常见的解决方案有三种一是用USB转串口通过Android的USB Host API访问但需要应用申请USB权限二是让设备厂商在系统层做好串口服务应用通过Socket或Binder调用三是root设备后直接操作设备节点。第一种最规范但兼容性受芯片影响第二种最稳定但依赖厂商配合第三种最灵活但只适合内部项目。我在全志V3S这类嵌入式Linux板子上做串口通信时通常会在设备树里把串口配置好然后在应用层用标准的open/read/write操作。全志的串口驱动比较成熟但要注意它的UART时钟源配置配错了波特率会偏差很大。5. USB转串口芯片的驱动与调试5.1 CH340、FT232R、FT231X的驱动安装差异USB转串口是PC端调试嵌入式的标配但驱动安装经常让人抓狂。CH340是国内用得最多的便宜好用但Win7下需要手动装驱动Win10之后系统自带。FT232R和FT231X是FTDI的芯片稳定性好但价格贵驱动需要从官网下载。Win7下装CH340驱动有个经典问题装完之后设备管理器里显示黄色感叹号提示该设备的驱动程序未被安装。这通常是因为驱动签名问题。解决办法是开机时按F8选择禁用驱动程序签名强制然后重新安装。或者用驱动精灵这类工具自动匹配但我不推荐因为可能装到带广告的版本。FTDI芯片的驱动安装相对省心但要注意FT231X和FT232R的驱动是同一个装最新的VCP驱动就行。如果装完发现串口号一直在变可以在设备管理器里手动指定COM端口号避免每次插拔都换号。5.2 Win7下查看串口被哪个程序占用这个问题在热词里出现了说明是很多人的痛点。Windows不像Linux有lsof命令查看串口占用比较麻烦。我常用的方法有这几个方法一用Process Explorer。这是微软官方Sysinternals工具集里的打开后按CtrlF搜索COM能找到哪个进程打开了串口句柄。方法二用PowerShell命令。Get-Process | Where-Object {$_.Modules.FileName -like *serial*}能列出加载了串口相关模块的进程但不一定准确。方法三用串口调试助手的强制打开功能。有些助手比如SSCOM支持强制占用串口如果它能打开而别的程序打不开说明串口被占用了。最彻底的办法是重启电脑但这不是解决问题的态度。我建议在开发阶段就养成好习惯用完串口及时关闭别让调试助手在后台挂着。5.3 串口烧录失败的常见原因串口烧写失败和使用STM32CubeProgrammer通过串口给单片机下载程序这两个热词放在一起看问题就很明确了。用串口给STM32下载程序需要芯片进入Bootloader模式通常是把BOOT0拉高、BOOT1拉低然后复位。烧录失败的常见原因一是BOOT引脚状态不对用万用表量一下BOOT0必须是高电平二是串口线序不对TX和RX要交叉三是波特率太高STM32的系统Bootloader对波特率有要求通常用115200比较稳四是芯片的读保护没解除需要用STM32CubeProgrammer先解除保护。CH340X这个芯片支持一键下载电路能自动控制BOOT0和复位省去了手动跳线的麻烦。但电路设计要注意CH340X的DTR和RTS引脚要接到MCU的BOOT0和NRST上而且逻辑电平要匹配。6. FPGA实现UART从Verilog代码到实际波形6.1 UART发送模块的Verilog实现要点用FPGA实现UART发送ASCII字符串核心是一个状态机加一个波特率计数器。状态机负责把并行数据转成串行位流计数器负责控制每个位的持续时间。// UART发送模块的核心状态机 module uart_tx( input clk, input rst_n, input tx_start, input [7:0] tx_data, output reg tx, output reg tx_done ); parameter BAUD_CNT 868; // 50MHz / 115200 ≈ 434, 这里用2倍频采样 reg [1:0] state; reg [15:0] cnt; reg [3:0] bit_idx; reg [9:0] shift_reg; always (posedge clk or negedge rst_n) begin if(!rst_n) begin state 0; tx 1; tx_done 0; end else begin case(state) 0: if(tx_start) begin shift_reg {1b1, tx_data, 1b0}; // 停止位数据起始位 state 1; cnt 0; bit_idx 0; end 1: begin if(cnt BAUD_CNT-1) begin cnt 0; tx shift_reg[0]; shift_reg {1b1, shift_reg[9:1]}; if(bit_idx 9) begin state 2; end else begin bit_idx bit_idx 1; end end else begin cnt cnt 1; end end 2: begin tx_done 1; state 0; end endcase end end endmodule这段代码的关键点是起始位是低电平停止位是高电平数据位从LSB开始发。波特率计数器的值要根据系统时钟和波特率算出来比如50MHz时钟、115200波特率分频系数是50_000_000/115200≈434。但为了采样准确通常用2倍或4倍过采样。6.2 FPGA串口的时序约束与实测波形FPGA做UART时序约束很重要。如果时钟频率高、走线长不加约束可能综合出来的电路时序不满足。我通常会在XDC或SDC文件里加这几条# 创建时钟约束 create_clock -period 20.000 -name sys_clk [get_ports clk] # 输入输出延迟约束 set_input_delay -clock sys_clk 2.000 [get_ports rx] set_output_delay -clock sys_clk 2.000 [get_ports tx]实测波形的时候重点看起始位的下降沿是否干净、每个位的宽度是否一致、停止位是否回到高电平。如果发现波形有振铃或者过冲可能是驱动能力太强或者线太长可以在输出端串一个22欧姆到100欧姆的电阻。7. 那些手册上不会写的UART实战经验7.1 TTL UART通过光耦能传多远这个问题很具体我直接给结论取决于光耦的速度和传输波特率。普通PC817光耦的上升下降时间在微秒级传9600波特率没问题传115200就勉强了。高速光耦比如6N137能支持到10M波特率传输距离可以到几十米。但光耦传输有个致命问题它是有方向性的而且需要限流电阻。设计电路时发光二极管侧的电流一般取5到10mA光敏三极管侧需要上拉电阻。传输距离还受线缆电容影响普通杜邦线每米大概50pF加上光耦的输入电容信号边沿会变缓。我的经验是9600波特率下PC817能传5米左右115200波特率下最好别超过1米否则误码率会明显上升。7.2 串口调试助手的选择与使用技巧SSCOM是我用得最多的串口调试助手功能全、稳定、免费。但有几个设置技巧很多人不知道一是时间戳功能勾选后每条接收数据前面会加上时间方便分析时序二是自动换行和HEX显示的配合调试二进制协议时特别有用三是多条发送功能可以预设常用命令一键发送。另一个常用的是SecureCRT适合Linux开发支持SSH和串口。它的优势是会话管理方便可以保存多套配置。但它是收费软件免费替代品可以用PuTTY或者MobaXterm。7.3 串口通信协议的设计建议如果你要设计一个基于串口的通信协议我的建议是帧头长度命令字数据校验帧尾。帧头用两个字节的固定值比如0xAA 0x55减少误判。长度字段要明确是数据长度还是总长度避免歧义。校验用CRC16比累加和可靠得多。还有一个经验协议里一定要有超时重传机制。串口通信受干扰的概率不低没有重传的话丢一个包就可能导致整个系统卡死。重传次数建议3次超时时间根据波特率和数据长度算一般100ms到500ms。8. 串口问题排查的完整链路8.1 从物理层到应用层的逐级排查串口出问题最忌讳的就是瞎猜。我总结了一套排查链路按顺序来基本能定位90%的问题。第一步查物理连接。TX和RX有没有交叉GND有没有接电压电平匹配吗3.3V的MCU接5V的USB转串口不加电平转换可能烧芯片。用万用表量TX对GND的电压空闲时应该是高电平。第二步查波特率。双方波特率必须一致误差控制在2%以内。用示波器量一个位的宽度比如115200波特率下一个位是8.68微秒。如果量出来偏差很大检查时钟源配置。第三步查数据格式。数据位、停止位、校验位必须完全一致。8N1对8N1不能一个8N1一个8E1。第四步查流控。如果双方都开了硬件流控RTS/CTS但线没接数据就发不出去。软件流控XON/XOFF也一样一方开了另一方没开就会死锁。第五步查软件配置。中断优先级、DMA配置、缓冲区大小这些都会影响通信。特别是中断优先级如果串口中断被高优先级中断长时间阻塞数据就会丢。8.2 几个经典故障的复现与修复故障一串口能发不能收。最常见的原因是RX引脚配置错了比如配成了输出模式或者复用功能没使能。用示波器量RX引脚如果有波形但MCU收不到就是配置问题。故障二数据偶尔错位。这通常是波特率误差累积导致的。把双方波特率误差算出来如果超过2%就得换晶振或者调整分频系数。故障三DMA接收丢包。检查DMA缓冲区的对齐方式有些芯片要求4字节对齐。还要检查DMA中断优先级如果太低可能被其他中断打断导致数据覆盖。故障四USB转串口识别不到。先换USB线再换USB口然后查驱动。CH340在Win7下要手动装驱动FT232R要装VCP驱动。如果设备管理器里能看到但打不开可能是被其他程序占用了。9. 跨平台串口编程的封装思路9.1 一套代码适配Windows和Linux如果你写的串口程序要同时跑在Windows和Linux上建议做一层抽象。核心接口就四个打开、关闭、读、写。Windows用CreateFile/ReadFile/WriteFileLinux用open/read/write。用条件编译区分#ifdef _WIN32 HANDLE fd CreateFile(port, GENERIC_READ|GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL); #else int fd open(port, O_RDWR | O_NOCTTY | O_NDELAY); #endif波特率设置差异更大。Windows用DCB结构体Linux用termios结构体。建议把配置参数封装成一个结构体两边各自实现。9.2 串口封装C库的设计要点我写过一个跨平台的串口库核心设计是用环形缓冲区做接收缓冲用互斥锁保护读写操作用条件变量实现阻塞读。这样上层应用不用关心底层是Windows还是Linux也不用担心多线程竞争。环形缓冲区的关键是读写指针的原子操作。在单生产者单消费者场景下可以不用锁用内存屏障保证顺序就行。但多生产者或多消费者就必须加锁。超时处理也很重要。串口读操作不能无限阻塞否则程序没法正常退出。我通常设置100ms超时超时后返回0上层根据返回值决定继续等还是退出。10. 我个人在UART调试中的几个习惯干了这么多年我养成了几个习惯分享出来可能对你有用。第一个习惯每个项目的第一行代码永远是串口打印。不管什么芯片先把串口调通能printf了再干别的。这就像盖房子先通水电后面所有调试都靠它。第二个习惯串口日志分级。用宏定义区分DEBUG、INFO、WARN、ERROR发布版本关掉DEBUG只留ERROR。这样既不影响性能又能保留关键信息。第三个习惯关键数据用HEX打印。ASCII打印方便看字符串但二进制数据必须用HEX否则乱码根本没法分析。SSCOM的HEX显示功能我几乎每个项目都用。第四个习惯保留一个紧急串口。产品定型后我会留一个隐藏的串口命令用于现场恢复出厂设置或者强制升级。这个命令不写在文档里只有内部人员知道。第五个习惯示波器常备。串口问题最终都要落到波形上一个几百块的逻辑分析仪或者入门示波器能省下大量猜测的时间。看波形的时候重点看起始位、停止位和位宽这三个对了基本就没大问题。UART这东西说简单是真简单两根线就能通说复杂也是真复杂每个平台都有各自的坑。但正是因为它简单才成了嵌入式系统的基石。你把UART吃透了再看I2C、SPI、CAN这些协议会发现很多思路是相通的。所以别嫌它基础基础的东西往往最考验功力。
返回列表