一、问题背景与现象
项目中基于 STM32F407 实现 Ymodem 协议固件升级功能,在处理结束帧应答时遇到一个诡异问题:
协议流程:STM32 收到 Ymodem 结束帧(SOH+0x00+0xFF)后,发送 0x6(ACK 应答),写入升级标志位到 Flash,最后执行系统复位,启动新固件。
核心代码(简化版):
/* 收到结束帧 /
else if(pData[0] == YMODEM_SOH && pData[1] == 0x00 && pData[2] == 0xff)
{
/ 发送正确应答(0x6),非阻塞发送 */IAP_Send((uint8_t *)&YMODEM_ACK, 1);
/* 写入升级成功标志位到Flash */
uint8_t data_type = IAP_UPDATE_ON;
IAP_FLASH_WriteByte(IAP_UPDATE_FLAG_ADDR, &data_type, 1);
/* 复位设备 */
SYSTEM_RESET;
}
异常现象:上位机始终收不到 0x6,反而收到乱码 0xFE;但在IAP_Send和SYSTEM_RESET之间加入printf(如打印 "即将重启"),上位机就能正常收到 0x6 的 ACK 应答。
二、问题根源分析
这个问题的核心是混淆了 "非阻塞发送的指令发起" 和 "硬件实际发送完成",结合 STM32 的外设工作机制,拆解原因如下:
-
非阻塞发送(IAP_Send)的本质
项目中IAP_Send是基于串口 DMA / 中断的非阻塞发送,执行该函数时,STM32 仅做 3 件事:
把 0x6(ACK)写入发送缓冲区;
配置 DMA 通道 / 开启串口发送中断,让硬件后台执行 "从缓冲区→串口寄存器→TX 引脚" 的传输;
函数立即返回,CPU 继续执行后续的 "写 Flash" 和 "复位" 操作。
整个IAP_Send的 CPU 耗时仅微秒级,但串口发送 1 个字节的硬件耗时是固定的(由波特率决定):
以 115200 波特率为例,单个字节(8 数据位 + 1 停止位 + 无校验)的发送耗时 = 10bit / 115200bps = 86.8μs,这是硬件层面无法缩短的时间。
-
系统复位(SYSTEM_RESET)的 "终止特性"
SYSTEM_RESET是 STM32 的全局强制复位,执行后会立即:
禁用所有外设(包括串口、DMA 控制器),清空串口发送寄存器、DMA 传输计数器;
中断 AHB/APB 总线操作,导致硬件后台的发送流程直接中断;
TX 引脚电平因复位出现乱跳,上位机收到的 0xFE 就是复位导致的乱码,而非有效 ACK。
-
printf "修复" 问题的真相
printf本身是阻塞式串口发送(即使底层是 DMA,printf的格式解析 + 字符发送也会耗时),以 115200 波特率打印 10 个字符为例,耗时约 868μs,这个耗时恰好覆盖了 0x6 的硬件发送时间(86.8μs),让 0x6 在复位前通过 TX 引脚完整发送,属于 "治标不治本" 的临时解决方案。
三、解决思路:等待硬件发送完成后再复位
核心原则:非阻塞发送后,必须等待 "硬件实际发送完成" 的标志,再执行复位,不能依赖 printf 等耗时操作。
STM32 串口的 "发送完成" 有一个关键硬件标志 ------TC 位(Transmission Complete),其含义是:
串口发送寄存器(TDR)中的数据已全部通过移位寄存器发送到TX引脚,且移位寄存器为空,这是判断数据是否真正发出去的最终标志。
无论IAP_Send是 DMA 发送还是中断发送,只要等待 TC 标志置 1,就能 100% 确保 0x6 已发送完成,再执行复位就不会丢失数据。
四、最终改进代码(适配 HAL 库 / 标准库)
以下提供两种常用库的完整改进代码,核心是在IAP_Send和SYSTEM_RESET之间添加 "等待 TC 标志" 的逻辑,并加入超时保护防止死等。
方案 1:HAL 库(STM32CubeMX 生成代码)
假设串口句柄为huart1(根据实际项目修改):
/* 收到结束帧 /
else if(pData[0] == YMODEM_SOH && pData[1] == 0x00 && pData[2] == 0xff)
{
/ 1. 非阻塞发送ACK(0x6) */IAP_Send((uint8_t *)&YMODEM_ACK, 1);
/* 2. 核心改进:等待串口硬件发送完成(TC标志),带超时保护 */
uint32_t tx_timeout = HAL_GetTick() + 10; // 10ms超时(足够覆盖1字节发送)
while(__HAL_UART_GET_FLAG(&huart1, UART_FLAG_TC) == RESET)
{
if(HAL_GetTick() > tx_timeout) break; // 超时退出,避免死等
}
__HAL_UART_CLEAR_FLAG(&huart1, UART_FLAG_TC); // 清除TC标志,防止后续失效
/* 3. 写入升级成功标志位到Flash */
uint8_t data_type = IAP_UPDATE_ON;
if(IAP_FLASH_WriteByte(IAP_UPDATE_FLAG_ADDR, &data_type, 1) == 0)
{
// 可选:记录写Flash失败日志
}
/* 4. IAP状态复位(可选) */
iap_state.status = IAP_STATUS_IDLE;
/* 5. 安全复位:此时ACK已100%发送完成 */
SYSTEM_RESET;
}
方案 2:标准库(STM32F4xx_StdPeriph_Driver)
假设使用 USART1(根据实际项目修改):
/* 收到结束帧 /
else if(pData[0] == YMODEM_SOH && pData[1] == 0x00 && pData[2] == 0xff)
{
/ 1. 非阻塞发送ACK(0x6) */IAP_Send((uint8_t *)&YMODEM_ACK, 1);
/* 2. 核心改进:等待串口TC标志,带超时保护 */
uint32_t timeout = 0xFFFF; // 超时计数值(足够覆盖1字节发送)
while((USART_GetFlagStatus(USART1, USART_FLAG_TC) == RESET) && (timeout-- > 0));
USART_ClearFlag(USART1, USART_FLAG_TC); // 清除TC标志
/* 3. 写入升级成功标志位到Flash */
uint8_t data_type = IAP_UPDATE_ON;
if(IAP_FLASH_WriteByte(IAP_UPDATE_FLAG_ADDR, &data_type, 1) == 0)
{
// 可选:记录写Flash失败日志
}
/* 4. IAP状态复位(可选) */
iap_state.status = IAP_STATUS_IDLE;
/* 5. 安全复位 */
SYSTEM_RESET;
}
五、关键细节避坑
必须清除 TC 标志:TC 标志不会自动清零,需手动通过__HAL_UART_CLEAR_FLAG或USART_ClearFlag清除,否则下次等待时会误判为 "已完成";
超时保护不可少:避免因串口硬件异常(如引脚短路)导致程序卡在 while 循环,无法复位;
无需额外等待 DMA 完成:若IAP_Send是 DMA 发送,DMA 完成标志(TCIF)仅表示 "数据写入串口寄存器",而 TC 标志表示 "数据发送到 TX 引脚",直接等 TC 标志更简洁;
写 Flash 操作不影响发送:写 1 字节 Flash 耗时约几十微秒,在等待 TC 标志之后执行,完全不影响 ACK 发送。
六、总结
这个问题是 STM32 非阻塞发送 + 复位的典型坑,核心教训是:
非阻塞发送的 "函数返回"≠"数据发送完成",硬件后台传输需要时间,复位、断电等强制终止操作前,必须通过硬件标志确认传输完成,不能依赖 printf 等临时耗时操作。
改进后的代码通过等待串口 TC 标志,实现了 "精准等待 + 通用适配"(适配所有波特率),比 printf 更可靠、更高效,已在实际项目中验证通过,上位机可稳定接收 0x6 的 ACK 应答。