尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

UART发送丢最后一个字节?从原理到排查全解析

UART发送丢最后一个字节?从原理到排查全解析 1. 丢字节的现场先从一颗“感觉坏了”的板子说起做嵌入式开发的几乎没人能绕开 UART。不管是调试日志输出、蓝牙模块通信、GPS 数据接收还是和 PC 端上位机对接串口都是最基础也最常用的通信手段。但恰恰是这种最基础的外设一旦出了问题排查起来反而最容易让人抓狂。你大概率遇到过这种场景代码写得挺顺逻辑看着也没毛病串口工具打开数据也收到了——但总是缺了点什么。仔细一看每次发送一串数据最后那个字节就是死活不出现。要么是“丢最后一个字节”要么是“多发一次就好了”要么是“把发送函数后面加个延时就好了”。我当时第一次遇到这问题第一反应是怀疑硬件坏了、芯片有问题、线没接好甚至怀疑是串口调试助手有 bug。折腾了半天最后发现根因根本不在硬件而是出在对 UART 发送机制的误解上。这篇文章我想把“UART 发送数据丢失最后一个字节”这个看似小众、实际非常普遍的问题掰开揉碎讲清楚。我尽量按照从现象到原理、从软件到硬件、从排查到预防的顺序来写。无论你是刚入门的新手还是已经干了几年但没仔细抠过 UART 时序的老手这篇文章应该都能帮你省下不少排查时间。2. 为什么偏偏是最后一个字节先理解 UART 发送的“两段式”动作要搞懂丢最后一个字节的根因必须先理解 UART 外设发送数据时的内部机制。很多人对串口发送的理解是“把数据丢给串口串口自动发完”这个说法在大方向上没错但忽略了最重要的细节——数据从写入数据寄存器到真正从 TX 引脚发出去是两个阶段。2.1 数据寄存器DR与发送移位寄存器TSR的分工STM32、NXP、Microchip 等主流 MCU 的 UART 外设内部发送路径上通常有两个关键部件一个是程序员直接写入的数据寄存器DR/TDR另一个是真正负责把数据一位一位移出去的发送移位寄存器TSR。当你调用发送函数往 DR 里写一个字节时这个字节并没有立刻跑到 TX 引脚上。它先被暂存在 DR 里然后由硬件自动搬运到 TSR再由 TSR 在波特率时钟驱动下把起始位、数据位、校验位、停止位按顺序从 TX 引脚送出去。这个过程对 CPU 来说是“异步”的——你写完了 DRCPU 就可以去干别的了但数据还在硬件里慢慢往外挪。这里就引出了两个容易混淆的状态标志TXE发送数据寄存器空表示 DR 里的数据已经被硬件搬到了 TSRDR 可以接收新数据了。注意此时数据可能还在 TSR 里没发完。TC发送完成表示 TSR 里的数据也全部移位输出完毕TX 引脚上已经完整地发出了整个帧含停止位。大多数人的误区在于只等了 TXE就以为发送完了。实际上 TXE 置位只代表“数据进了 TSR”而 TSR 还在干活。如果在这时候操作了串口外设的复位、关闭、或者进入了低功耗模式TSR 里还没移完的数据就会被直接丢弃——这就是“丢最后一个字节”最常见的软件层面的根因。2.2 一个生活化的类比你可以把 UART 发送想成在食堂打饭DR 是餐盘TSR 是打饭阿姨的勺子。你把餐盘递给阿姨写入 DR阿姨把饭舀到勺子里DR 到 TSR再倒进你的餐盘TSR 移位输出。如果你看到“阿姨已经把饭舀起来了”TXE 置位就转身走了阿姨手一抖或者直接把勺子放下了这顿饭就没了。只有看到“饭已经倒进你盘子里”TC 置位才算真的拿到手。这个类比虽然糙但很贴切。想清楚 TXE 和 TC 的差别再去排查丢字节问题思路会清晰很多。3. 软件层面的“隐形杀手”中断、轮询与 DMA 的末字节陷阱理解了 UART 的发送路径下面进入正题软件代码里到底有哪些写法会导致最后一个字节丢失我总结了实际开发中最高频的几种场景按“作案频率”排序。3.1 发送完立刻关串口或进低功耗不等 TC这是我在各种项目里见过最多的一种写法。某些开发者为了省电会在发完一组数据后立刻调用串口关闭函数或者直接进入 STOP/STANDBY 模式。比如UART_SendData(huart1, buffer, len); // 没有等待 TC 标志 HAL_UART_DeInit(huart1); // 串口关了 // 或者 __WFI(); // 进入低功耗问题在于UART_SendData这类函数无论标准库还是 HAL 库通常只负责把数据写入 DR并等待 TXE 标志。数据进入 TSR 后函数就返回了。这时候立刻关串口或休眠TSR 里的最后一个字节还没移完直接就被断电或时钟关闭给掐断了。解决办法在关闭串口或进入低功耗之前必须等待 TC 标志置位确保所有数据已经从 TX 引脚完整发出。UART_SendData(huart1, buffer, len); while (__HAL_UART_GET_FLAG(huart1, UART_FLAG_TC) RESET) { // 等待真正发送完成 } HAL_UART_DeInit(huart1);3.2 中断发送模式下发送完成回调里做了不该做的事用中断方式发送 UART 数据是嵌入式开发里的常规操作但很多人对“中断发送完成”的理解有偏差。HAL 库的HAL_UART_Transmit_IT函数注册完中断后就返回了真正发送完成是在中断里触发的HAL_UART_TxCpltCallback回调函数。问题往往出在这个回调函数里。有人会在回调里释放 DMA 缓冲区、关闭串口、或者启动下一次发送。如果没有正确理解回调触发的时机——它是在最后一个字节写入 DR 后触发的还是 TSR 完全移位结束后触发的——就很容易在数据还未完全发出时提前释放资源。以 STM32 HAL 库为例HAL_UART_TxCpltCallback的触发时机是TC 标志置位后也就是说中断方式下 HAL 库已经帮你处理了 TC 等待这个问题。但如果你用的是寄存器操作或者自己写的中断服务函数只判断了 TXE 就触发“发送完成”回调那么同样的坑你还会再踩一次。3.3 轮询发送一大包数据最后一个字节在缓冲区里“排队”另一种常见场景是发送的数据量比较大一次塞不进 DR。这时候需要用循环逐字节发送for (uint16_t i 0; i len; i) { while (!(UART-SR UART_SR_TXE)); // 等待 DR 空 UART-DR buffer[i]; // 写入下一个字节 }这段代码看起来没毛病循环结束后呢循环结束只代表最后一个字节被写入了 DR并不代表它已经从 TX 引脚发出去。如果循环后面紧跟着其他外设操作、关中断、或者时钟切换最后一个字节依然可能被截胡。我见过一些优化编译器在开 O2 优化后循环结束后的那条指令和最后写入 DR 的指令之间的时序被压缩得极短导致外部逻辑比如 RS485 方向控制引脚翻转太快把最后一个字节的停止位截断了。RS485 通信里这种“最后一个字节被切掉一半”的现象尤其常见。3.4 DMA 发送完毕但缓冲被提前释放用 DMA UART 发送是高性能场景的标配但 DMA 的“发送完成”和 UART 的“物理发送完成”又是两个概念。DMA 的传输完成中断TCIF表示 DMA 已经把内存里的数据全部搬到了 UART 的 DR但 UART 的 TSR 里可能还憋着最后一个字节没移完。很多人在这里犯的错误是在 DMA 传输完成中断里直接释放 DMA 缓冲区、置标志允许下次发送结果 UART 还在慢慢往外挤最后一个字节而缓冲区已经被复用甚至改写了。虽然 DR 里的数据是硬件锁存的一般情况下缓冲区复用不会影响已经写入 DR 的数据但如果此时你重置了 DMA 通道或者修改了 UART 的 DMA 请求映射那就真的会丢。更稳妥的做法是DMA 传输完成中断里只做“数据已交给串口”的记录真正的“发送完成”应该等 UART 的 TC 中断。做到双重确认既等 DMA 的 TCIF也等 UART 的 TC 标志。这样虽然多了一点延迟但能彻底避免资源释放过早的问题。4. 硬件层面的“背锅侠”电平、接线与外部器件的时序约束软件排查完了如果还是丢最后一个字节就要考虑硬件侧的参与了。硬件问题有时候表现得更诡异——比如丢的字节是随机的、丢的字节是半个、或者只在高波特率下丢。4.1 波特率误差停止位被“吃掉”的真相UART 是异步通信收发双方各自用自己的时钟采样。只要双方的波特率误差在一定范围内通常 ±2% 以内通信就没问题。但误差一旦过大最后一个字节的停止位就可能采不到接收方会报帧错误Frame Error或者直接丢弃该字节。这个误差来源比较多系统主时钟本身不准外部晶振 vs 内部 RC 振荡器UART 波特率分频器的舍入误差使用了不合适的时钟源比如有些 MCU 的 UART 外设只能用特定的 PLL 输出。举个例子某 MCU 主频 72MHz想得到 115200 波特率分频系数是 72,000,000 / 115,200 625。这是整数没问题。但如果主频是 80MHz分频系数 80,000,000 / 115,200 694.44只能取整到 694实际波特率就成了 80,000,000 / 694 115,273误差 0.06%问题不大。可如果分频器只能取 16 位整数而另一个主频算出来的误差到了约 2.5%停止位采样就会飘到边界上。数据短的时候不明显数据一长最后一个字节就悬了。排查时不要只看“代码逻辑”一定要用示波器或逻辑分析仪抓一下 TX 引脚的实际波形数一数停止位的宽度是否符合预期。这一步能省下大量猜测时间。4.2 线缆过长与终端电阻反射把最后一个字节“淹没”了串口线一长寄生电容和线缆阻抗就会显现出来。尤其是波特率拉高到 921600 甚至 1.5Mbps 时方波的上升沿和下降沿会被拉得很缓。如果接收端的输入阈值刚好在下落沿附近边沿抖动就会导致采样错误。我之前在一个项目中遇到过UART 线长 1.5 米波特率 460800发送 20 个字节的指令接收端偶尔丢最后一个字节。一开始我以为是软件问题加了延时、加了 TC 等待都没用。后来用差分探头看波形发现 TX 引脚在停止位末尾有一处明显的振铃幅度刚好超过了接收端的逻辑阈值导致接收端认为停止位之后还有一个额外的“毛刺”。接收端的 UART 外设虽然不会把这个毛刺当成数据但状态机可能因此被搞乱最后一个字节就丢了。解决方向降低波特率使用屏蔽双绞线在接收端串联一个 33Ω~100Ω 的电阻抑制振铃必要时加终端电阻匹配阻抗。4.3 RS485 方向切换发送完成标志没等到就切换了方向RS485 是半双工总线需要通过 DE/RE 引脚控制收发方向。方向切换的时机极为关键必须在最后一个数据位停止位完全发送出去之后才能把方向引脚拉低切到接收模式。如果过早拉低总线上的最后一个字节会被直接截断。很多 RS485 收发器芯片如 MAX485、SP3485的规格书里会写DE 引脚的下降沿到总线输出无效之间有个延迟时间但这只是芯片本身的参数前提是你给的 DE 控制信号本身不能太早。MCU 侧的 UART 必须等 TC 标志置位后再延迟一小段或者直接等一个字节的发送时间再切换方向。实际工程里我通常的做法是// 发送数据阻塞式或DMA等待TC UART_SendData(...); while (__HAL_UART_GET_FLAG(huart, UART_FLAG_TC) RESET); // 先做个短暂延时确保总线电平稳定 delay_us(10); // 具体值根据波特率调整 // 再切换RS485方向 RS485_DE_LOW();这个 10μs 的延时不是拍脑袋定的它大约等于在 115200 波特率下一个位的时长8.68μs再留一点余量。如果波特率更高延时可以相应缩短如果更低要适当加长。4.4 USB 转串口芯片的“隐藏 buffering”行为关键词里提到了 FT232R、CP2102N、FT231X 这些 USB 转 UART 桥接芯片。这类芯片在 PC 端和 MCU 端之间做协议转换时内部有 FIFO 缓冲。桥接芯片的收发缓存策略会影响数据的实时性尤其是发送方向。MCU 通过 USB 转串口芯片向 PC 发送数据时数据先进入桥接芯片的 TX FIFO再通过 USB 打包上传给 PC。如果 MCU 发送完毕但 USB 包还没凑满比如设置了 64 字节的包实际只发了 10 个字节桥接芯片可能不会立刻把数据推给 PC而是等下一个数据或者等超时。这就可能导致 PC 端看到的现象是最后一段数据迟迟不到或者软件关闭串口时缓冲区的数据被丢弃。很多串口调试助手会有“清空接收缓冲区”的按钮。你点了之后发现“哦最后的字节其实到了只是没来得及显示”这种现象跟 MCU 侧丢字节完全是两码事不要混淆。排查方法MCU 发送完毕后不要立刻关串口或者断电等 200ms 再观察 PC 端是否收到完整数据。如果等一会儿就收到了说明 MCU 侧没有丢字节是 USB 桥接芯片的缓冲策略导致的假象。5. 为什么有时候“加个延时”就好了——标志位误判的深层原因你可能听说过这样一句话“发送完数据之后加个延时最后一个字节就不会丢了。”这句话在网上流传很广但它掩盖了真正的原理反而误导了不少人。5.1 延时的本质用时间换 TC 标志置位加延时能够解决问题本质是因为延时让 MCU 在关闭串口或切换模式之前多给了 UART 外设几百微秒到几毫秒的时间来完成 TSR 的移位输出。也就是说延时是在“软件没有等待 TC 标志”的情况下用“碰运气”的方式等到了 TC 置位。它确实管用但非常不优雅。延时的选取依赖波特率、数据长度、主频等因素。同样的延时在 115200 波特率下够用在 9600 波特率下就不一定够——9600 波特率下一个字节就要大约 1.04ms如果你只延时了 100μs等待根本不够。反过来在 921600 波特率下又可能延时过长白白浪费 CPU 时间。5.2 正确做法永远等标志而不是等时间正确、高效、通用的做法永远是等待硬件标志// 发送完所有数据后 while (!(UART-ISR UART_ISR_TC)) { // 等待真正发送完成 }有些 MCU 的 UART 还支持 TC 中断可以在发送完成后通过中断处理后续操作比如关闭串口、释放 DMA 缓冲区、切换 RS485 方向。这种方式既不阻塞 CPU又能保证时序准确是高负载场景下的首选。从工程规范的角度我强烈建议大家把“发送完成必须等待 TC 标志”作为一条铁律写进团队的代码规范里。宁可多写一行 while也不要依赖延时魔法。因为你会忘记延时下一任接手的人也会忘记延时但硬件标志永远不会骗你。6. STM32 平台的典型表现从 HAL 库到寄存器操作的踩坑记录STM32 是嵌入式开发中使用率极高的平台关键词里也提到了 STM32CubeIDE。基于 STM32 平台处理 UART 丢最后一个字节的问题有非常强的代表性因为 HAL 库和 LL 库、寄存器操作的代码风格差异极大踩坑点也完全不同。6.1 HAL 库的阻塞发送与“看似发送完成”STM32 的 HAL 库提供HAL_UART_Transmit函数做阻塞式发送不传超时时间的话会一直等到发送完成。但它内部等待的是 TXE 标志而不是 TC 标志。也就是说HAL_UART_Transmit返回时最后一个字节可能还在 TSR 里没有移完。所以很多人直接写HAL_UART_Transmit(huart1, data, len, 1000); HAL_UART_DeInit(huart1);在HAL_UART_DeInit执行的时候TSR 里的数据还没发完串口就被关了最后一个字节丢了。这也是 STM32 论坛里“为什么 HAL_UART_Transmit 之后马上 DeInit 丢字节”这个问题反复出现的原因。对策在 DeInit 之前加上一段等待 TC 的循环HAL_UART_Transmit(huart1, data, len, 1000); while (__HAL_UART_GET_FLAG(huart1, UART_FLAG_TC) RESET); HAL_UART_DeInit(huart1);6.2 HAL 库中断发送的两种“完成”理解HAL 库中断方式发送HAL_UART_Transmit_IT注册后会立即返回。发送完成后会在中断服务函数里调用HAL_UART_TxCpltCallback。我在 3.2 节说过HAL 库的这个回调是在 TC 之后触发的所以相对安全。但这里有个雷HAL 库中断发送只支持单次发送如果你在回调里立刻再次调用HAL_UART_Transmit_IT要小心 HAL 库内部状态机的gState状态判断。如果在HAL_UART_TxCpltCallback里直接启动下一次发送是可以的但如果你在别的地方比如 DMA 完成回调里启动就必须先确保上一次发送彻底结束否则 HAL 会返回HAL_BUSY什么都不做看起来就像是数据发不出去。6.3 直接操作寄存器时最容易犯的错不使用 HAL 库、直接操作寄存器时控制权完全在你手里。但这也意味着你需要自己管理 TXE 和 TC 的等待顺序。很多人在中断里只判断 TXE 就认为发送完成或者清标志时误清了 TC 标志导致后续的 TC 判断永远不成立。注意事项读取 SR/ISR 寄存器、然后写 DR 寄存器会自动清除 TC 标志。如果顺序搞反了TC 标志可能在你还没来得及判断的时候就被清了。不要用UART-SR 0这种方式去“清标志”这个操作会同时清掉一堆标志而且在不同系列芯片上行为不一致。正确做法是“读相应寄存器然后写 DR”来清除。开启 TC 中断和 TXE 中断时注意两者的优先级。不要让 TXE 中断不停地触发把 TC 中断饿死导致发送完成永远无法执行。6.4 使用 LL 库时更直观但也更“裸”的体验LL 库是 STM32 的另一套库API 更底层、更接近寄存器性能好但使用门槛高。LL_UART_TransmitData8和LL_UART_IsActiveFlag_TXE这些函数非常直观但也意味着 LL 库不会帮你做任何“人性化”处理。在 LL 库下你必须自己等待 TXE、自己等待 TC、自己处理 RS485 方向切换。很多从标准库转 LL 库的开发者第一周都会遇到莫名其妙的丢字节问题原因就是原来库帮你做了 TC 等待现在不帮了。经验建议如果项目对实时性要求高、希望精确控制 UART 行为LL 库是个不错的选择但一定要把 TXE/TC 的等待逻辑封装成统一函数防止每个调用点各写各的、写出错误逻辑。7. 排查“丢最后字节”的工程化套路从现象到根因的标准流程很多人遇到这种问题就慌一会儿怀疑软件一会儿怀疑硬件一会儿又怀疑编译器。这里我根据多年的调试经验整理出一套标准排查流程。按这个流程走大部分丢字节问题都能在半小时内定位到根因。7.1 第一步复现现象确认丢的是什么先明确“丢最后一个字节”的具体表现是每次必丢还是偶发丢的是整个字节还是半个字节用示波器看波形数据长度固定还是变化波特率多少线长多少用的是什么平台、什么发送方式轮询/中断/DMA接收端是什么MCU、PC 串口助手、USB 转串口把这些问题记录清楚能显著缩小排查范围。比如“每次必丢”大概率是软件时序问题“偶发”更可能是硬件干扰或波特率误差“只有接 PC 时丢”要怀疑 USB 转串口芯片的缓冲策略。7.2 第二步先排除最蠢的软件问题按顺序检查这几个点发送函数是否返回成功循环里是否正确等待了 TXE发送完成后是否等待了 TC发送完成后是否有关闭串口、进低功耗、切换时钟、操作 RS485 方向等动作数据缓冲区是否正确有没有越界、有没有被优化掉这一步做完大部分软件层面的问题就已经暴露了。7.3 第三步用逻辑分析仪/示波器验证物理波形软件看完了就要看物理层。把逻辑分析仪的探针夹到 TX 引脚和 GND抓取一次完整发送过程的波形。重点观察最后一个字节的停止位是否完整停止位之后是否有多余的毛刺或电平跳变帧与帧之间的间隔是否正常波特率实际测量值和配置值是否一致这个检查能直接验证波特率误差、振铃、方向切换过早等问题。7.4 第四步隔离测试逐个替换变量如果波形正常但数据仍然丢就要做隔离测试用另一块开发板直接对接排除 PC 端和 USB 转串口的影响换一根短线、换一个 USB 口、换一个串口助手降低波特率到 9600 测试看是否还丢单独发送一个字节看是否也丢。每次只改变一个变量记录结果就能快速定位是 MCU 侧的问题、线缆的问题、还是接收端的问题。7.5 第五步检查编译器优化与代码生成这是容易被忽略的一步。某些编译器在开启高等级优化后会对 volatile 变量的访问顺序做调整。如果你的发送代码没有把 UART 寄存器指针声明为 volatile或者没有使用__IO/volatile修饰优化后的代码可能会改变等待循环的执行顺序导致 TC 标志还没等到就跳出了循环。另外用 STM32CubeMX 配置 UART 时注意检查它生成的初始化代码是否正确配置了 NVIC 优先级、DMA 通道、以及底层时钟。很多时候问题不是出在你的业务代码而是生成代码的配置项没配对。7.6 一个真实的排查案例去年我帮朋友排查过一个很典型的案例他的设备用 STM32F103 和 PC 通信PC 上位机发送指令设备回复应答。问题是设备回复的 20 个字节里最后一个字节总是丢但偶尔又能收到。排查过程复现确认是每次必丢不是偶然。检查代码发现用的是HAL_UART_Transmit函数返回后立刻进入低功耗模式没有等 TC。这是高嫌疑点但修改后问题仍然偶发说明还有第二个问题。上逻辑分析仪抓波形发现 Data 线的最后一个字节的停止位被拉低了。这不像是 UART 外设的行为更像是外部电路在作怪。检查硬件原理图发现 RS485 收发器的 DE 引脚由一颗 GPIO 控制GPIO 在发送完后立刻拉低。GPIO 的操作时机比 UART 的 TC 慢了不到 1μs导致停止位被切断了。修改代码让 DE 引脚在 TC 置位后再延时 20μs 拉低。问题彻底解决。这个案例很有代表性表面上“丢最后一个字节”实际是 RS485 方向切换时序问题。它同时涉及软件和硬件如果不系统地排查很难定位。8. 从“修好”到“预防”架构层面的发送完成管理经验修好一个 bug 只是一个开始。真正体现工程水平的是如何从架构上避免这类问题复发。UART 发送完成管理看起来是小事但在大型项目里如果每个工程师都按自己的习惯写发送代码那代码库很快就会变成一片雷区。8.1 统一封装“阻塞发送并等待 TC”的工具函数不管是用寄存器操作、HAL 库还是 LL 库我都建议在项目里封装一个统一的发送函数内部强制等待 TC 标志/** * brief 发送数据并等待发送完成 * param huart UART 句柄 * param data 数据指针 * param len 数据长度 * retval 0成功-1失败 */ int UART_SendBlocking(UART_HandleTypeDef *huart, const uint8_t *data, uint16_t len) { if (HAL_UART_Transmit(huart, (uint8_t *)data, len, 1000) ! HAL_OK) { return -1; } // 关键等待最后一个字节从移位寄存器中完全输出 while (__HAL_UART_GET_FLAG(huart, UART_FLAG_TC) RESET) { // 避免死等可加超时 } return 0; }所有业务代码都调用这个函数不允许自己写 HAL_UART_Transmit 裸调用。这样即使有人忘了 TC 等待编译器也会在 review 阶段提醒他。8.2 DMA 场景下用“二次确认”机制DMA 发送时建议做两个标志DMA 传输完成标志表示数据已全部写入 UART DRUART TC 标志表示数据已全部从引脚发出只有两个标志都置位才允许释放缓冲区、切换 RS485 方向、进入低功耗。在实现上可以在 DMA 完成中断里置位dma_tc_flag在 UART 的 TC 中断里置位uart_tc_flag然后在应用层定期检查这两个标志。或者用事件标志组、消息队列等 RTOS 机制来实现同步。这样做的代价是代码复杂度增加但带来的可靠性提升是值得的尤其是在工业控制和车载项目中。8.3 低功耗项目的特殊考量对于电池供电、需要频繁进低功耗的项目UART 发送后的低功耗进入时机尤其敏感。很多低功耗 MCU如 STM32L 系列在 STOP 模式下UART 时钟会被关闭如果 TSR 里还有数据没移完数据就会丢失。正确的做法是发送完数据等待 TC 置位关闭 UART 的接收/发送使能等待 UART 完全停止部分芯片需要额外延迟一个 APB 时钟周期再进入 STOP 模式。可以借助 UART 的 TX 引脚空闲状态来判断TX 引脚在空闲时应该保持高电平停止位电平。用示波器观察确保进入低功耗前 TX 引脚已经恢复为高电平并且持续了一段稳定时间再进低功耗。8.4 让硬件设计也来“帮个忙”有些丢字节问题其实可以通过硬件设计来规避在 TX 和 RX 引脚上串联 1kΩ 左右的电阻可以抑制振铃也能在调试时用万用表测量是否短路。如果电路板空间允许在 UART 供电引脚上加一个 100nF 去耦电容防止电源毛刺影响电平阈值。RS485 电路里DE 引脚和 MCU 的 GPIO 之间加一个 RC 滤波防止 GPIO 毛刺导致方向瞬间翻转。长距离传输时选用带屏蔽的线缆屏蔽层单端接地能显著降低外部干扰导致的偶发丢字节。这些设计改动非常小但能在源头减少很多调试烦恼。9. 边角场景那些“不按常理出牌”的丢字节原因文章最后一部分我想分享几个我在实战中遇到的“边角场景”。它们不像前面几个原因那么典型但遇到时非常容易让人懵圈。9.1 发送缓冲区被编译器优化掉了在 C 语言里如果你定义了一个局部数组调用发送函数后马上退出函数而编译器认为这个数组后续不再被使用它可能会把数组内容优化掉。这在发送函数是内联函数或者开启 O3 优化时尤其危险。解决方法是给缓冲区加volatile修饰或者用__attribute__((used))告诉编译器保留。当然更根本的办法是确保发送函数内部确实在循环里逐字节读取了缓冲区而不是只传了首地址。9.2 DMA 的 burst 配置导致的奇偶字节丢失某些 DMA 控制器支持 burst 传输一次可以从内存中搬多个字节。如果 DMA 配置的 burst 大小和 UART 的数据寄存器宽度不匹配可能导致最后一个字节没有被正确搬运。这属于配置层面的坑一般不会在低负载时出现但在高负载、高优先级配置下可能出现。排查方法关掉 burst 模式改用 single 传输看问题是否消失。9.3 “最后一个是结构体末尾”——结构体内存对齐的陷阱如果你发送的是结构体指针而结构体末尾有 padding 字节比如一个 uint8_t 后跟着 uint32_t你实际发送的长度可能比 sizeof(struct) 小也可能因为对齐问题多发了几个字节。这种问题表现更像是“多发了几个奇怪的字节”但偶尔也会因为缓冲区越界导致最后一个有效字节被覆盖。建议发送结构体时不要直接发送整个结构体而是逐字段打包或者使用#pragma pack(push, 1)取消对齐。但注意pack 也会带来性能问题和跨平台可移植性问题需谨慎使用。9.4 外置收发器芯片的“使能斜坡”有些 UART 转 RS232 电平的芯片如 MAX232在初次上电时需要几百微秒的稳定时间。如果你的 MCU 在复位后立刻开始发送数据而外置收发器还没准备好前几个字节可能发了一半就丢了。这类问题表现是“复位后第一次发送丢字节之后一直正常”。解决方法是延迟启动发送或者检测外置芯片的“Power OK”引脚如果有的话。这个原因不太容易想到但遇到复位后首次通信失败的问题时值得往这个方向考虑。写在最后UART 发送完成判断是基本功也是分水岭“UART 发送数据丢失最后一个字节”这个问题表面上看是个简单的 bug实际上牵扯到发送路径原理、库函数行为、硬件时序、RS485 方向控制、低功耗管理甚至编译器优化等多个层面的知识。把它彻底搞明白你对 UART 外设的理解会上一个台阶排查其他串口问题的效率也会明显提高。我在实际开发中反复体会到大多数串口奇奇怪怪的问题最后都能归结到“没等 TC”和“没看波形”这两个原因上。软件上老老实实等硬件标志硬件上老老实实看示波器/逻辑分析仪至少能解决八成的问题。如果你正在被“丢最后一个字节”折磨按这篇文章的思路一步步排查大概率能找到根因。如果排查完发现是更冷门的场景也欢迎分享出来大家一起积累经验。毕竟串口通信这个小到不能再小的外设能坑人的地方还真不少。
返回列表