ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

STM32串口丢最后一个字节?从TXE到TC彻底解决

STM32串口丢最后一个字节?从TXE到TC彻底解决 在STM32上调试串口时我遇到过最让人摸不着头脑的问题就是发送一串数据上位机收到的总是少最后一个字节。比如我明明发的是“ABCDEF”调试助手里只显示“ABCDE”。刚开始我以为是波特率不对又怀疑是USB转串口工具坏了折腾半天才发现问题出在代码的“最后一哆嗦”上。这种问题在UART通信里非常典型尤其是用轮询方式发送字符串、用DMA批量发送数据、或者发送完立刻进入低功耗模式的时候几乎一踩一个准。这篇文章我打算把这个问题彻底讲透先带你定位现象再拆解UART发送的底层机制然后给出几种立竿见影的修复方案顺便把你可能用到的FT232R、CP2102N这类USB转串口工具的排查方法也一起覆盖了。不管你是刚接触STM32的在校生还是已经在做嵌入式项目开发的工程师只要遇到过“串口丢尾巴”的诡异现象这篇文章都值得你花几分钟看完。1. 丢失最后一个字节的典型场景与问题定位1.1 三个最常见的“丢尾巴”现场第一个场景是轮询发送。很多人习惯直接调用HAL_UART_Transmit或者用标准库写一个while循环往USART_DR里塞数据。发送缓冲区里的最后一个字节有时候就是出不来。这种问题不是偶发而是固定丢只要发送长度超过两三个字节就必现。第二个场景是DMA发送。从DMA的角度看传输完成中断触发时数据已经全部从内存搬到UART的数据寄存器里了。可你在中断回调里加一句“关闭DMA通道”或“关闭UART时钟”或者立刻把CPU拖入睡眠最后一个字节就可能在移位寄存器里断掉。第三个场景是低功耗设备。比如NB-IoT模组、电池供电的传感器节点发送完一帧数据后为了省电马上进入STOP模式。这种场景下丢最后一个字节几乎成了标配因为代码根本没有等待“发送完成”这个时刻。我之前调过一款步进电机驱动芯片TMC2226SA它通过UART发送寄存器配置命令。配置帧发出去了但芯片总是没有反应用示波器一看帧尾少了半个字节。这类对时序敏感的外设最怕的就是这种“丢尾巴”因为寄存器写入是整帧生效最后一个字节丢了整条指令就无效了。1.2 先搞清楚是MCU没发出去还是工具没显示出来遇到这类问题我强烈建议先不要改代码哪怕只是多写一行延时也要先用示波器或者逻辑分析仪看一眼TX引脚的实际波形。这是判断问题归属的唯一权威依据。把示波器探头接到MCU的TX引脚和GND上配置好触发然后运行发送函数。重点看最后一帧数据之后第10个bit停止位是否完整。如果停止位在高电平的持续时间跟前面几帧一致说明MCU波形完整问题很可能出在USB转串口工具或者上位机软件上如果停止位被提前截断、电平被拉低或者整个第10位不见了那就要回MCU代码里找原因。这里有个很容易踩的坑不少USB转串口芯片比如FT232R、CP2102N内部都有FIFO缓冲数据从UART接口进来后会先缓存再由USB批量传输上报给主机。如果上位机软件不刷新或者缓冲区设置太小就会出现“MCU发出了电脑上却看不到”的假象。所以做定位时示波器永远比软件可靠。2. 根因拆解为什么偏偏丢最后一个字节2.1 UART发送链路上的两个寄存器要搞懂这个问题必须理解UART发送端内部的两个关键寄存器。很多初学者把它们当成一个这是丢字节的根本原因之一。第一个是数据寄存器在STM32上叫TDR在标准库的寄存器版本里叫DR。CPU往这个寄存器写一个字节发送硬件就认为“有活儿干了”。第二个是移位寄存器它才是真正负责把数据一位一位搬到TX引脚上的部件。CPU写入TDR后TDR里的数据会被硬件自动搬运到移位寄存器然后移位寄存器在波特率时钟驱动下逐位移出先发起始位再发8个数据位最后发停止位。我用一个“仓库发货”的类比解释一下TDR相当于仓库门口那箱已经打包好的货TXE标志是“门口空了可以放下一箱”的提示移位寄存器则是正在装车的那箱货它什么时候卸完才是真正的“发送完成”。如果你只看TXE那么你只能知道“货已经放到门口了”但不知道“车上的货卸完没有”。2.2 TXE和TC这两个状态位差得很远UART状态寄存器里有两位跟发送相关名字很像但含义完全不同。TXE全称是Transmit Data Register Empty表示数据寄存器为空、可以写入新数据。CPU往TDR写入一个字节后TXE会被硬件清零当TDR里的数据被搬运到移位寄存器后TXE又会被置1表示可以写下一个字节了。TC全称是Transmission Complete表示整个发送过程彻底结束包括最后一个停止位都已经从TX引脚上移出去。问题就出在这里。大多数发送循环的逻辑是查询TXE为1写下一个字节继续查询TXE再写下一个字节。当你写完最后一个字节时TXE置1了程序以为发送完成了但实际上移位寄存器还在工作。如果此时你关闭UART时钟、复位外设、切换TX引脚功能或者让整个MCU睡过去移位寄存器里的最后那点数据就没机会全部移出了。2.3 HAL库发送函数到底做了什么为什么还会丢有的读者会问我用的明明是HAL_UART_Transmit它在HAL库里是有等待TC标志的为什么我还会丢最后一个字节HAL库在轮询模式下的发送流程大致是先等TXE然后写TDR循环往复等到最后一个字节写完再等待TC位置位。逻辑上它确实不丢字节。但实际操作中还有几个漏洞。第一个漏洞是调用方的问题。函数返回确确实实表示TC已经置位了你如果在这个函数返回之后再做一个“禁掉UART时钟”的操作那正常是不会丢的。可很多人是在发送完成后立刻调用HAL_UART_DeInit、或者直接把PB10这种TX引脚重配置为GPIO结果把已经输出到引脚上的电平强行改了。第二个漏洞是部分旧版本的HAL库或者第三方移植版本里HAL_UART_Transmit在发送最后一个字节时存在只等待TXE就返回的情况。这种库代码版本差异在ST官网升级后就不太常见了但如果你用了旧工程或者网上现成的初始化代码就得去翻一下底层实现确认最后有没有等待TC。第三个更隐蔽是重定向printf时自己写死了寄存器操作。比如在fputc里调用USART_SendData然后用while循环等待TXE置位这时printf打印一串字符串最后一个字符写进TDR后TXE置位函数直接返回。从printf的角度看“输出已完成”可硬件还在慢慢挪最后一位。如果主程序紧接着执行了USART_Cmd(USART1, DISABLE)那最后一位就永远醒不过来了。3. 快速修复三种方案与可复制代码模板3.1 方案一发送函数返回前等待TC标志最稳最直接的修复思路是确保在所有数据写入TDR之后、彻底处理完发送流程之前等待硬件把最后一位搬完。标准库的写法是全部数据写完以后加上一行等待TC的循环。void UART_SendString(uint8_t *buf, uint16_t len) { for (uint16_t i 0; i len; i) { USART_SendData(USART1, buf[i]); while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) RESET); } // 关键等待最后一个字节完全发送完毕包括停止位 while (USART_GetFlagStatus(USART1, USART_FLAG_TC) RESET); }如果你用的是HAL库HAL_UART_Transmit本身已经包含了等待TC的逻辑但为了保险我习惯在调用之后再加一个TC等待void UART_SendString(const uint8_t *buf, uint16_t len) { HAL_UART_Transmit(huart1, (uint8_t *)buf, len, 1000); while (__HAL_UART_GET_FLAG(huart1, UART_FLAG_TC) RESET); }注意如果HAL_UART_Transmit超时返回了HAL_TIMEOUT再在这里死等TC可能会卡死。稳妥一点可以检查返回值只有HAL_OK才继续等TC。但大多数情况下这个函数不会出这种问题。我个人的习惯是只要代码里出现“串口”两个字所有发送函数都必须以“等待TC”作为收尾动作。哪怕HAL_UART_Transmit内部已经做了等待我也不会省掉这一条多一个标志位判断对性能的影响几乎为零但能换来绝对的心安。3.2 方案二发送完成后加一个“残影时间”延时如果项目里不方便等待TC标志比如你用的是非常老的寄存器库或者中断优先级分配导致标志位读取很麻烦也可以退而求其次用延时来兜底。计算一下一个字节的发送时间等于起始位、数据位和停止位的总bit数除以波特率。以115200波特率、8数据位、无校验、1停止位为例一帧是10个bit每个bit约8.68微秒一帧就是86.8微秒。为了留足余量发送完最后一个字节后延时200微秒就足够安全。// 用DWT或SysTick实现阻塞延时 UART_SendData(USART1, data); while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) RESET); delay_us(200);这种方式适合快速验证一个猜想但我不建议在正式产品里长期依赖延时。因为波特率变化后延时时间没有跟着变要么浪费CPU要么因为余量不足又出现偶发丢字节。真正可靠的还是等标志位。3.3 方案三DMA发送时先关闭DMA再等待TCDMA发送场景是丢最后一个字节的重灾区。DMA的传输完成中断触发时只是表示数据已经从内存搬运到了UART外设并不代表UART已经把所有数据都发到了TX引脚上。如果此时直接关闭DMA通道UART依然有数据正在发送问题倒不太大但如果你在DMA的回调里顺手把UART时钟关了或者把外设Disable了那就真的把最后一个字节扼杀在摇篮里了。标准库处理方式void DMA1_Channel4_IRQHandler(void) { if (DMA_GetITStatus(DMA1_IT_TC4) ! RESET) { DMA_ClearITPendingBit(DMA1_IT_TC4); // 停止DMA通道防止重复传输 DMA_Cmd(DMA1_Channel4, DISABLE); // 关键等UART移位寄存器发完最后一位 while (USART_GetFlagStatus(USART1, USART_FLAG_TC) RESET); // 此时才能安全关闭UART外设时钟 USART_Cmd(USART1, DISABLE); } }HAL库里则是在HAL_UART_TxCpltCallback回调里做同样的操作。DMA中使用的UART发送中断回调里等TC的操作几乎可以保证一个字节都不丢。这是我调试DMX灯光控制器和步进电机配置文件时反复验证过的写法。4. USB转串口工具与上位机侧丢字节的排查4.1 这类问题经常被误判为MCU故障实际在论坛里看到的问题很大一部分根本不是MCU丢字节而是USB转串口工具或串口调试助手丢显示。FT232R、CP2102N这类USB转串口芯片内部都有FIFO缓冲UART接收的数据先进入芯片FIFO再通过USB批量传输给主机。如果上位机软件的接收线程没有及时读取或者一次性读取的字节数太大最后一个字符就可能滞留在工具芯片的缓冲区里表现成“串口没收到”的样子。这也是为什么我说判断这个问题千万不要只盯着串口调试助手的显示框除非你已经用示波器确认过波形完全没问题。如果MCU输出正常那就要把排查重点转向工具和软件了。4.2 驱动与工具设置的排查步骤先确认USB转串口工具是否被系统正确识别。以FT232R为例安装完FTDI VCP驱动后设备管理器里应该能看到一个COM口设备名带“USB Serial Port”。CP2102N则需要安装Silicon Labs的CP210x驱动安装后同样会出现对应的COM口编号。如果设备管理器里显示的是未知设备或者COM口上有黄色感叹号驱动的状态就不对数据通路断断续续是正常的。工具设置方面检查串口调试助手的接收缓冲区大小。某些调试助手默认缓冲区很小时数据量大一点就容易被覆盖。再看看“暂停显示”、“接收超时”这些参数有时候不需要改缓冲区只需要换一个调试助手再试就能确认问题。我常用XCOM和sscom做交叉验证同一PC上如果两个工具状态不同那基本就是软件显示层的锅了。还有一个很容易被忽略的选项是硬件流控。如果串口助手开启了RTS/CTS流控而MCU侧没有接对应的流控引脚主机会在缓冲区快满时拉低RTSUSB转串口芯片就可能暂停向主机上报数据。这会直接导致最后几个字节被“压”在工具里面等MCU数据发完了主机才慢慢把数据上报看起来就是丢尾巴。排查时先把RTS/CTS全部关掉。4.3 用“加换行结尾”的方法做快速判断这里有个非常实用的小技巧当你怀疑上位机显示不及时时发送字符串时故意在末尾加上\r\n。如果上位机没收到最后一个可见字符但收到了换行符那说明MCU发出的数据整体上是完整的USB转串口芯片也接收到了只是调试助手的显示逻辑或缓冲刷新有问题。反之如果加了换行符仍然丢那大概率是MCU侧真的被截断了。这个方法不需要示波器也能缩小范围但因为它只判断“末尾有没有数据”不能百分百证明中间数据完整所以只能当快速筛选。我之前用这个方法排查过一个FT231X工具丢字节的问题加了\r\n后最后一个字符又冒出来了确认是上位机缓冲区的小毛病一场虚惊。5. 实操避坑清单与排查速查表5.1 五个最容易忽略的细节第一个是RS485收发器的方向控制。在RS485总线上DE引脚控制发送方向。很多工程师发送完数据后立刻把DE拉低让总线回归接收态。问题在于UART的最后一个停止位还在电平转换器和收发器之间传输DE一拉低总线直接被切断最后一个停止位就被“腰斩”了。正确的做法是先等UART的TC标志再拉低DE。第二个是电平转换芯片的电源管理。MAX3232、SP3485这类器件如果发送完马上就关闭电源或关闭使能跟RS485的DE问题一样波形在中间被截断。低功耗设计里尤其常见加一个TC后置动作就能解决。第三个是发送之前没有检查上一次发送是否完成。如果上一次发送还没结束代码就往TDR里写了新数据会把正在发送的帧破坏掉。发送前等一次TC等于给整个外设做一次“清场”。第四个是printf重定向时只处理TXE。前面已经说过fputc内部如果只关心TXEprintf打印结束后最后一位很可能还悬在半空。这种问题用示波器看特别明显波形最后一个字节的停止位短了一截。第五个是仿制的USB转串口芯片。市面上很多便宜的USB转TTL模块用的是盗版芯片驱动兼容性和FIFO行为都跟正品有差异丢最后一个字节是家常便饭。排查到最后如果所有逻辑都对换个官方原装芯片的工具问题也许就消失了。5.2 排查速查表现象可能原因排查方法修复建议发送后立刻关UART时钟最后一个字符固定丢失移位寄存器数据未完成示波器观察停止位是否被截断在TC置位后再关时钟使用DMA发送后立刻关DMA/关外设丢尾字节DMA完成不代表UART发完查看DMA TC中断后波形DMA中断里先等待UART_TC串口助手显示不全但示波器波形完整USB转串口FIFO或上位机刷新问题换调试助手或加\r\n测试增大缓冲区或关流控RSAV发送后DE立即拉低总线帧错误停止位被收发器截断看总线波形等TC后再切换DE用printf打印字符串最后字符丢失fputc只等待TXE在重定向里加TC等待修改fputc实现5.3 不同平台也适用这个思路STM32是这样其他单片机也大同小异。ESP32使用IDF时UART驱动内部有自己的ringbuffer和发送队列调用uart_write_bytes后数据还在驱动层排队如果立刻调用uart_driver_delete会丢失尚未发送的字节。处理方式是先调用uart_wait_tx_done再删除驱动。51单片机使用SBUF发送时查询TI标志是最常见的写法。TI由硬件置位但如果你在TI置位后马上下一个动作关闭串口中断或直接复位外设最后一位同样会烂尾。AVR的UART则用TXC位表示传输完成。不同平台名字不同本质都一样你要等的是“移位寄存器完全腾空”而不是“数据寄存器空出来”。6. 写在最后的实际操作心得经过这几年越来越多的串口调试我自己的习惯已经固定下来不管什么平台发数据前先看上一帧有没有发完发完数据后永远等待发送完成标志位然后再做时钟关闭、引脚切换、方向翻转这些操作。这个习惯帮我省下来的时间比看完十篇技术文档还值。对于手头还没有逻辑分析仪的开发者我真心建议花几十块钱买一个简易的逻辑分析仪。别小看这个工具UART、SPI、I2C这类协议基本全靠它来定案。串口调试助手只能告诉你“电脑收到了什么”而逻辑分析仪能告诉你“MCU到底发出了什么”。这两者之间差的往往就是排查“丢最后一个字节”时的那几个小时。如果你也遇到过类似问题不妨按照这篇文章的步骤先测波形、再看工具、最后改代码大概率能找到你那个“消失的最后一个字节”。
返回列表