简介:这份源码包面向嵌入式开发工程师与STM32学习者,聚焦基于STM32H743单片机的Ymodem协议文件传输与APP在线更新方案。STM32H743搭载ARM Cortex-M7内核,主频可达400MHz,接口资源丰富,适合高速处理与实时控制场景;Ymodem协议支持大包与批处理传输,可提升固件升级的效率和可靠性。资源包共833个文件,以297个h头文件、242个c源文件为核心,配合146个icf、62个s、21个sct、20个ld等链接与启动脚本,以及16个a、3个lib库文件,完整覆盖工程构建、外设驱动与协议实现,压缩包约8.28MB。目前已有69人学习下载。读者可从中获取Ymodem收发流程、APP跳转与固件校验的完整实现思路,参考模块化目录组织与工程配置方式,用于远程升级、设备固件更新等场景的二次开发与排错借鉴。
1. STM32H743 上跑 Ymodem 更新 APP:为什么串口升级值得认真做
手里有一块 STM32H743 的板子,Bootloader 已经烧进去,APP 程序却还在用 ST-Link 反复插拔调试,这种场景在量产和现场维护里几乎不可接受。Ymodem 文件传输更新 APP 程序,解决的正是这个问题:让设备通过串口接收一个 bin 文件,自己完成擦写和跳转,不需要拆机、不需要仿真器。STM32H743 是 ST 的高性能 M7 系列,主频能到 480MHz,Flash 和 RAM 都够大,跑 Bootloader + APP 双区方案绰绰有余。这套源码方案适合做工业设备、测试仪器、嵌入式网关的工程师,尤其是那些设备已经装在机柜里、只留了一个串口或者 USB 转串口引出的场景。Ymodem 协议本身不复杂,但真正落地到 H743 上,Flash 扇区划分、中断向量表偏移、看门狗处理这几个点没处理好,升级就会变成玄学。
2. Ymodem 协议在 STM32H743 上的落地拆解
2.1 Ymodem 的帧结构和 H743 串口接收的配合方式
Ymodem 是 Xmodem 的扩展版本,核心区别在于它支持 1024 字节的大数据包,并且第一个包传的是文件名和文件大小。每一帧的结构是:起始字节 SOH(0x01,128 字节包)或 STX(0x02,1024 字节包),然后是包序号、包序号的反码、有效数据区、CRC16 校验。接收方每收到一帧,校验通过后回 ACK(0x06),校验失败回 NAK(0x15),传输结束回 EOT(0x04)。
在 STM32H743 上,串口接收不能一个字节一个字节地阻塞等,否则 1024 字节的包还没收完,上位机就超时了。常见做法是用 DMA + 空闲中断的方式收数据。H743 的 USART 支持 DMA 双缓冲,配合 IDLE 中断可以做到不定长数据的整包接收。我一般会把 DMA 接收缓冲区设成 1024 字节以上,空闲中断触发后把数据搬到一个环形缓冲区里,Ymodem 状态机再从环形缓冲区里取数据。
这里有个细节:Ymodem 的包间隔时间不能太长,上位机一般等 1 到 3 秒没收到 ACK 就会重传。所以状态机处理一帧的时间必须远小于这个超时窗口。H743 主频高,CRC16 计算用查表法或者硬件 CRC 都能轻松跑完。
2.2 Flash 扇区划分与 APP 区的地址规划
STM32H743 的 Flash 是 2MB,分成两个 Bank,每个 Bank 1MB,扇区大小是 128KB。做 Bootloader + APP 方案时,第一步就是确定 Bootloader 占多大、APP 从哪个地址开始。
我一般这样分:Bootloader 放在 0x08000000 开始的第一个扇区,大小 128KB,足够放一个带 Ymodem 协议和 Flash 擦写功能的 Bootloader。APP 从 0x08020000 开始,占后面的扇区。为什么从 0x08020000 而不是 0x08010000?因为 H743 的扇区是 128KB 对齐的,0x08020000 正好是第二个扇区的起始地址,擦除的时候不会误伤 Bootloader。
APP 区的结束地址要看 APP 实际大小,但至少要留一个扇区做参数存储或者备份。如果 APP 超过 1MB,就要跨 Bank 操作,这时候要注意 Bank 切换的时序,擦除和写入不能跨 Bank 同时进行。
在 Bootloader 里需要定义一个宏来表示 APP 的起始地址,APP 工程里也要把链接脚本的 ROM 起始地址改成同样的值。这两个地址必须一致,否则跳转过去就是 HardFault。
2.3 中断向量表偏移和跳转前的现场清理
Cortex-M7 的中断向量表默认在 0x08000000,APP 如果从 0x08020000 开始,中断向量表也必须跟着偏移。在 APP 的 main 函数开头,第一件事就是设置 SCB->VTOR = 0x08020000。这个寄存器在 system_stm32h7xx.c 里通常有 VECT_TAB_OFFSET 宏,但很多人只改宏不改链接脚本,结果编译出来的中断向量表还是在旧地址。
跳转前 Bootloader 要做几件事:关闭所有外设中断、关闭 SysTick、清除所有挂起的中断标志、把 MSP 设置成 APP 向量表的第一个字、把跳转地址设置成向量表的第二个字。H743 还有 Cache 的问题,跳转前最好执行一次 __DSB() 和 __ISB(),确保指令和数据同步。
/* Bootloader 跳转到 APP 的核心代码 */ typedef void (*pFunction)(void); pFunction JumpToApp; uint32_t JumpAddress; /* 检查 APP 区第一个字是否是合法的栈顶地址 */ if (((*(__IO uint32_t*)APP_ADDR) & 0x2FFE0000) == 0x20000000) { /* 关闭全局中断 */ __disable_irq(); /* 关闭 SysTick */ SysTick->CTRL = 0; SysTick->LOAD = 0; SysTick->VAL = 0; /* 清除所有中断挂起标志 */ for (int i = 0; i < 8; i++) { NVIC->ICER[i] = 0xFFFFFFFF; NVIC->ICPR[i] = 0xFFFFFFFF; } /* 设置 MSP 和跳转地址 */ JumpAddress = *(__IO uint32_t*)(APP_ADDR + 4); JumpToApp = (pFunction)JumpAddress; __set_MSP(*(__IO uint32_t*)APP_ADDR); /* 跳转 */ JumpToApp(); }这段代码里 APP_ADDR 就是 0x08020000。检查栈顶地址是否合法是为了防止 APP 区是空的或者被擦除了,跳过去直接死机。__disable_irq() 之后没有开中断,因为跳转后 APP 会重新初始化中断。注意 H743 的 NVIC 寄存器数量和 F1/F4 不一样,ICER 和 ICPR 数组要按 H7 的头文件来。
2.4 用 Ymodem 接收数据并写入 Flash 的完整流程
Ymodem 接收流程分几个阶段:等待起始帧、接收文件信息帧、接收数据帧、接收结束帧。每个阶段的状态机要处理好超时和重传。
/* Ymodem 接收状态机简化版 */ typedef enum { YM_IDLE, YM_WAIT_START, YM_RECV_INFO, YM_RECV_DATA, YM_DONE, YM_ERROR } YmodemState; YmodemState ym_state = YM_IDLE; uint32_t flash_addr = APP_ADDR; uint32_t file_size = 0; void Ymodem_Process(uint8_t *buf, uint16_t len) { switch (ym_state) { case YM_WAIT_START: /* 收到 'C' 后发送起始帧请求 */ if (buf[0] == 'C') { Send_Byte(0x01); /* SOH */ Send_Byte(0x00); /* 包序号 */ Send_Byte(0xFF); /* 包序号反码 */ /* 发送 128 字节的起始帧,包含文件名和大小 */ Send_StartFrame(); ym_state = YM_RECV_INFO; } break; case YM_RECV_INFO: /* 解析文件名和文件大小 */ if (buf[0] == 0x01 || buf[0] == 0x02) { Parse_InfoFrame(buf, &file_size); Send_ACK(); Send_Byte('C'); /* 请求第一个数据包 */ ym_state = YM_RECV_DATA; } break; case YM_RECV_DATA: if (buf[0] == 0x01 || buf[0] == 0x02) { if (Check_CRC(buf)) { /* 擦除并写入 Flash */ Flash_Write(flash_addr, buf + 3, (buf[0] == 0x01) ? 128 : 1024); flash_addr += (buf[0] == 0x01) ? 128 : 1024; Send_ACK(); } else { Send_NAK(); } } else if (buf[0] == 0x04) { /* EOT */ Send_ACK(); Send_Byte('C'); ym_state = YM_DONE; } break; default: break; } }这段代码是简化版,实际用的时候要处理包序号翻转、超时重传、Flash 擦除对齐。Flash_Write 函数里要先擦除再写,H743 的 Flash 擦除是按扇区的,所以写入地址要按 128KB 对齐。如果 APP 大小不是扇区整数倍,最后一个扇区擦除后剩余空间写 0xFF 就行。
参数说明:APP_ADDR 是 APP 起始地址,要和链接脚本一致。file_size 从起始帧里解析出来,用来判断什么时候收完。实际项目中我还会在 Flash 末尾写一个标志位,记录 APP 是否完整,Bootloader 启动时先检查这个标志再决定跳不跳。
3. 避坑与排查:Ymodem 升级翻车现场记录
3.1 跳转后不进中断,程序像死了一样
现象:Bootloader 跳转到 APP 后,APP 的 main 函数能跑,但串口中断、定时器中断全都不响应。
原因:APP 的 SCB->VTOR 没有设置,或者设置的值和实际链接地址不一致。中断发生时,CPU 还是去 0x08000000 取中断向量,取到的是 Bootloader 的向量表,自然进不了 APP 的中断服务函数。
解决:在 APP 的 main 函数第一行就设置 SCB->VTOR = APP_ADDR。同时检查链接脚本里的 ROM 起始地址是不是 APP_ADDR。用 Keil 的话,在 Target 选项里 IROM1 的 Start 要改成 0x08020000,Size 要相应减小。
3.2 Ymodem 传输到一半卡住,上位机一直重传
现象:传输到某个包之后,上位机反复重传同一个包,设备端不回应。
原因:Flash 擦除时间太长,H743 擦除一个 128KB 扇区大概需要 1 到 2 秒,这期间串口数据还在进来,但 CPU 在忙等 Flash 操作,没来得及处理串口数据,导致后续包丢失。
解决:擦除操作要提前做,不要等到收到数据才擦。在 Ymodem 开始接收之前,先把 APP 区所有扇区擦一遍。如果 APP 区很大,擦除时间更长,可以在擦除期间给上位机发 NAK 让它等,或者用双 Bank 交替擦写。另一个办法是把 Flash 操作放到 RAM 里执行,但 H743 的 Flash 编程需要调用 HAL 库,放 RAM 里比较麻烦。
3.3 升级完成后 APP 跑不起来,读 Flash 全是 0xFF
现象:Ymodem 传输显示成功,但跳转后 APP 不运行,用调试器读 APP 区发现全是 0xFF。
原因:Flash 写入地址没有按扇区对齐,或者写入前没有擦除。H743 的 Flash 只能把 1 写成 0,不能把 0 写成 1,如果目标地址没擦除就写入,写进去的数据是乱的。
解决:每次写入前确认目标扇区已经擦除。可以在 Flash_Write 函数里加一个判断,如果目标地址是扇区起始地址,先擦除再写。另外检查 Ymodem 的包序号,如果包序号乱了,写入地址也会乱。
3.4 串口 DMA 接收丢包,CRC 校验频繁失败
现象:Ymodem 传输过程中经常 NAK,重传几次才能过一个包。
原因:DMA 接收缓冲区太小,或者空闲中断处理不及时。H743 的串口波特率如果开到 921600 甚至更高,1024 字节的包在 10ms 左右就传完了,如果空闲中断里处理时间过长,下一个包就覆盖了。
解决:把 DMA 缓冲区设成 2048 字节以上,用双缓冲模式。空闲中断里只做数据搬运,不做 CRC 校验和 Flash 写入,把数据放到环形缓冲区后立刻返回。Ymodem 状态机在主循环里跑,不占用中断时间。
3.5 看门狗在升级过程中复位
现象:升级到一半设备重启,Ymodem 传输中断。
原因:Bootloader 里开了独立看门狗或者窗口看门狗,但升级过程中 Flash 擦除和写入耗时较长,没有及时喂狗。
解决:在 Bootloader 里,升级过程中先关闭看门狗,或者把喂狗操作放到 Flash 操作的循环里。H743 的 IWDG 一旦开启就不能关闭,只能复位,所以更稳妥的做法是在 Bootloader 启动时先判断是否需要升级,如果需要升级就不初始化看门狗。
4. 进阶技巧:让 Ymodem 升级更可靠的两个习惯
4.1 在 APP 里加一个「升级请求」标志
实际现场维护时,不可能每次都让设备断电再上电进 Bootloader。我一般会在 APP 里留一个命令接口,收到特定命令后往 Flash 的某个固定地址写一个魔术字,然后软复位。Bootloader 启动时先读这个地址,如果魔术字存在就进入 Ymodem 接收模式,否则直接跳转 APP。这样设备在运行状态下也能触发升级,不用拆机。
/* APP 里触发升级请求 */ #define UPGRADE_FLAG_ADDR 0x0801F000 /* 最后一个扇区 */ #define UPGRADE_MAGIC 0x5A5A5A5A void Request_Upgrade(void) { HAL_FLASH_Unlock(); /* 擦除标志扇区 */ FLASH_Erase_Sector(FLASH_SECTOR_7, FLASH_BANK_1); /* 写入魔术字 */ HAL_FLASH_Program(FLASH_TYPEPROGRAM_FLASHWORD, UPGRADE_FLAG_ADDR, UPGRADE_MAGIC); HAL_FLASH_Lock(); /* 软复位 */ NVIC_SystemReset(); }Bootloader 里对应的判断逻辑:读 UPGRADE_FLAG_ADDR 的值,如果是 UPGRADE_MAGIC,擦除标志扇区,进入 Ymodem 接收流程。这个习惯让我在现场升级时省了很多事,不用教客户怎么短接跳线。
4.2 用 CRC 校验整个 APP 区再跳转
Ymodem 每包都有 CRC,但传输完成后整个 APP 区的完整性还需要再确认一次。我一般会在 APP 编译完成后,用工具算一个 CRC32 值,放在 APP 的固定位置或者 Bootloader 里硬编码。Bootloader 跳转前,对 APP 区做一次 CRC32 校验,校验通过才跳转,否则停留在 Bootloader 等待重新升级。
这个习惯的代价是每次升级后多花几百毫秒算 CRC,但换来的是不会跳到一个损坏的 APP 里。H743 有硬件 CRC 外设,算 1MB 数据大概几十毫秒,完全可以接受。
/* 用 H743 硬件 CRC 校验 APP 区 */ uint32_t Calc_App_CRC(uint32_t addr, uint32_t size) { CRC->CR = CRC_CR_RESET; for (uint32_t i = 0; i < size; i += 4) { CRC->DR = *(uint32_t*)(addr + i); } return CRC->DR; }调用的时候注意地址要 4 字节对齐,size 要是 4 的倍数。如果 APP 大小不是 4 的倍数,补 0xFF 到 4 的倍数再算。
这两个习惯我用了几年,Ymodem 升级的翻车率从最初的十次里坏三次,降到现在的几十次里偶尔出一次,而且出问题也能快速定位。嵌入式升级这件事,协议本身不难,难的是把边界情况都想到。希望帮到你。
本文还有配套的精品资源,点击获取