☰
STM32H743串口IAP升级实战:Ymodem协议与Bootloader跳转避坑指南
2026/10/28 17:18:43 网站建设 项目流程

简介:这份源码包面向嵌入式开发工程师与STM32学习者,聚焦基于STM32H743单片机的Ymodem文件传输与APP程序在线更新方案。STM32H743采用ARM Cortex-M7内核,主频可达400MHz,接口资源丰富,适合高速处理与实时控制场景;Ymodem协议在XMODEM基础上优化了数据包大小与批处理能力,可提升传输效率与可靠性,二者结合可用于远程固件升级、工业设备参数配置等场合。资源包共833个文件,以297个h头文件、242个c源文件为核心,辅以146个icf、62个s、21个sct、20个ld等链接与启动脚本,以及a、lib库文件、uvprojx工程文件和hex固件,整体约8.28MB,工程结构完整。目前已有69人学习下载。读者可从中获取协议收发、Flash读写、APP跳转等关键模块的实现思路,对照工程配置与目录组织,快速理解嵌入式在线升级的落地方法。

1. 串口里跑 Ymodem:为什么 H743 的 IAP 升级值得单独拆一遍

手里这块 STM32H743 跑着业务固件,产线或现场却只留了一根 UART 线,没有调试器、没有 USB DFU 入口,怎么把新的 APP 程序塞进去?答案就是 IAP(在应用编程)加 Ymodem 文件传输。这套源码包干的事很具体:用 H743 的 Flash 当仓库,通过串口按 Ymodem 协议把 PC 上的 bin 文件分块收进来,写进指定扇区,再跳转执行。它解决的是「设备已经装到现场、只剩串口可用」时的固件更新问题,适合做工业控制、仪表、网关这类带 H743 又需要远程或现场升级的开发者。很多人第一次做 IAP 会卡在跳转后 HardFault,或者 Ymodem 收包丢字节,这份源码把 Bootloader 和 APP 两端的骨架都摆出来了,照着改比从零搭省事得多。

2. Bootloader 与 APP 的分区约定:地址、向量表、栈顶怎么定

2.1 先想清楚 Flash 怎么切,再谈代码

H743 的 Flash 结构和 F1/F4 那套完全不是一回事。它按 Bank 划分,每 Bank 又分多个 Sector,Sector 大小不统一(有 128KB 的,也有 8KB 的),擦除粒度直接决定你 Bootloader 和 APP 的边界能不能对齐。我一般会先翻对应型号的参考手册 Flash 章节,把 Sector 起始地址列出来,再决定 Bootloader 占多少、APP 从哪个 Sector 开始。

常见做法是 Bootloader 只占第一个 128KB Sector(0x08000000 ~ 0x0801FFFF),APP 从 0x08020000 开始。这样 APP 的起始地址天然落在 Sector 边界上,擦除时不会误伤 Bootloader。源码里如果用的是别的地址,你要先确认它和你的芯片型号、Bank 配置对得上,别直接照抄。

分区定完,还要留一块区域存升级标志和固件长度。我习惯在 Bootloader 末尾或单独一个 Sector 放一个结构体,记录「是否需要升级」「APP 有效长度」「校验值」。没有这块,断电重启后 Bootloader 根本不知道上次升级成没成功。

2.2 向量表偏移:跳转失败十有八九栽在这

Cortex-M7 的向量表默认在 0x00000000 映射区,但 H743 上电后实际取的是 0x08000000 的复位向量。APP 被放到 0x08020000 后,它的向量表也必须跟着搬过去,否则中断一来就跳到 Bootloader 的向量表里,程序直接跑飞。

APP 端要改两处。第一处是系统初始化时设置向量表偏移寄存器:

/* APP 端 main 或 SystemInit 里,地址要和实际烧录地址一致 */ #define APP_BASE_ADDR 0x08020000U #define VECT_TAB_OFFSET (APP_BASE_ADDR - 0x08000000U) SCB->VTOR = APP_BASE_ADDR; /* 直接写绝对地址,H7 支持 */ /* 或者用相对偏移:SCB->VTOR = VECT_TAB_OFFSET; */

这里APP_BASE_ADDR必须和 Bootloader 里定义的 APP 起始地址一模一样,差一个字节都会在中断触发时翻车。第二处是链接脚本,把 Flash 起始地址和长度改掉:

/* STM32H743ZI 的 APP 链接脚本片段 */ MEMORY { FLASH (rx) : ORIGIN = 0x08020000, LENGTH = 1792K RAM (xrw) : ORIGIN = 0x24000000, LENGTH = 512K }

ORIGIN改成 APP 起始地址,LENGTH是 APP 可用空间,别超过芯片实际 Flash 尾部。改完链接脚本,编译出来的 bin 文件起始地址才对得上。

2.3 Bootloader 跳转前必须做的三件事

跳转本身就一行函数指针调用,但跳之前有三件事不做,APP 大概率起不来。

第一,关掉所有外设中断。Bootloader 里开过的 USART、DMA、定时器中断,跳转前要逐个关掉,尤其是串口接收中断,否则 APP 刚跑起来就被残留中断打断。

第二,复位 SysTick 和清除挂起中断。SysTick->CTRL = 0;关掉滴答定时器,NVIC->ICPR清一下挂起位,避免跳过去立刻进异常。

第三,取 APP 的栈顶和复位向量。标准写法:

typedef void (*pFunction)(void); void jump_to_app(uint32_t app_addr) { uint32_t stack_top = *(volatile uint32_t *)app_addr; uint32_t reset_vec = *(volatile uint32_t *)(app_addr + 4); pFunction app_entry = (pFunction)reset_vec; /* 检查栈顶是否落在合法 RAM 范围,防止空片跳转 */ if ((stack_top & 0xFFF00000U) != 0x24000000U && (stack_top & 0xFFF00000U) != 0x20000000U) { return; /* 栈顶不合法,放弃跳转 */ } __disable_irq(); __set_MSP(stack_top); /* 先设主栈指针 */ SCB->VTOR = app_addr; /* 再设向量表 */ __enable_irq(); app_entry(); /* 跳过去,不再返回 */ }

stack_top是 APP 向量表第一个字,reset_vec是第二个字。先设 MSP 再设 VTOR 的顺序不能反,否则中间来中断会用错栈。栈顶合法性检查是我踩过坑之后加的:Flash 空片读出来是 0xFFFFFFFF,直接跳过去必进 HardFault,加个判断至少能退回 Bootloader 报错。

3. Ymodem 收包流程:从握手到写 Flash 的完整链路

3.1 Ymodem 和 Xmodem 差在哪,为什么升级都用它

Xmodem 只传数据,文件名和大小得另外约定。Ymodem 在 Xmodem 基础上加了第 0 包,里面带文件名和文件长度,接收端拿到长度就能提前规划 Flash 写入范围,还能校验文件是否完整。升级场景里这个长度信息很关键——没有它,你只能靠结束符判断收完,中途丢一包就可能写进半截固件。

Ymodem 用 1024 字节数据包(STX 开头)为主,128 字节包(SOH 开头)为辅。每包带 CRC16 校验,接收端校验通过回 ACK(0x06),失败回 NAK(0x15),连续失败多次就发 CAN(0x18)中止。这套握手逻辑源码里都有,但实际跑起来最容易出问题的是超时和字节对齐。

3.2 接收状态机:一个字节一个字节地喂

串口中断里不要做协议解析,只把收到的字节塞进环形缓冲区,主循环里再跑状态机。这是血泪经验——在中断里等包、算 CRC,稍微慢一点就丢字节。

状态机大致分这几个状态:等起始字节、收包头、收包号、收数据、收 CRC、校验回 ACK。核心逻辑:

/* Ymodem 接收状态机简化版,主循环调用 */ typedef enum { YM_IDLE, YM_HEADER, YM_PKTNO, YM_DATA, YM_CRC, YM_DONE } ym_state_t; static ym_state_t state = YM_IDLE; static uint8_t pkt_buf[1024]; static uint16_t pkt_len, pkt_idx; static uint8_t pkt_no, pkt_no_inv; void ymodem_poll(void) { uint8_t ch; while (ringbuf_get(&ch)) { /* 从环形缓冲区取字节 */ switch (state) { case YM_IDLE: if (ch == SOH) { pkt_len = 128; state = YM_PKTNO; } else if (ch == STX) { pkt_len = 1024; state = YM_PKTNO; } else if (ch == EOT) { state = YM_DONE; } break; case YM_PKTNO: pkt_no = ch; state = YM_PKTNO_INV; break; case YM_PKTNO_INV: pkt_no_inv = ch; if ((uint8_t)(pkt_no + pkt_no_inv) != 0xFF) { send_nak(); state = YM_IDLE; /* 包号校验失败 */ } else { pkt_idx = 0; state = YM_DATA; } break; case YM_DATA: pkt_buf[pkt_idx++] = ch; if (pkt_idx >= pkt_len) state = YM_CRC; break; case YM_CRC: /* 收两个 CRC 字节,和本地计算的比对 */ crc_bytes[pkt_idx++] = ch; if (pkt_idx >= 2) { if (crc16(pkt_buf, pkt_len) == get_crc16(crc_bytes)) { write_flash(pkt_no, pkt_buf, pkt_len); send_ack(); } else { send_nak(); } state = YM_IDLE; } break; } } }

ringbuf_get从串口环形缓冲区取一个字节,取不到就返回 0,主循环继续跑别的。pkt_no是包序号,从 0 开始递增,第 0 包是文件名和长度,从第 1 包开始才是固件数据。write_flash负责把数据写到对应地址,地址 = APP 起始地址 + (pkt_no - 1) * pkt_len。

3.3 写 Flash 的节奏:H743 擦除和编程的时序坑

H743 的 Flash 编程和 F4 类似,但 Bank 切换、ECC 校验这些细节更多。写之前必须先擦除,擦除按 Sector 来,一次擦一整个 Sector。如果你收一包写一包,跨 Sector 时就要先擦下一个 Sector,不能边写边擦同一个。

常见做法是收到第 0 包拿到文件长度后,先把 APP 区域所有涉及的 Sector 擦干净,再开始逐包写入。擦除期间串口还在收数据,所以擦除要快,或者先暂停接收、擦完再继续。源码里如果是一边收一边擦,你要确认擦除时间不会导致串口溢出。

写 Flash 的函数大致长这样:

/* H743 Flash 写入,地址必须 32 字节对齐 */ HAL_StatusTypeDef flash_write(uint32_t addr, uint8_t *data, uint32_t len) { HAL_FLASH_Unlock(); for (uint32_t i = 0; i < len; i += 32) { /* H743 按 256bit(32字节)编程 */ if (HAL_FLASH_Program(FLASH_TYPEPROGRAM_FLASHWORD, addr + i, (uint32_t)(data + i)) != HAL_OK) { HAL_FLASH_Lock(); return HAL_ERROR; } } HAL_FLASH_Lock(); return HAL_OK; }

FLASH_TYPEPROGRAM_FLASHWORD是 H7 特有的 256 位编程模式,一次写 32 字节。地址必须 32 字节对齐,数据长度也最好是 32 的整数倍,否则最后一包要补齐。补齐的字节写 0xFF,不影响固件执行。

3.4 收完之后的校验和跳转决策

最后一包收完,PC 端会发 EOT,接收端回 ACK,然后 PC 再发一个全 0 的第 0 包表示传输结束。这时候你要做两件事:校验整个 APP 区域的 CRC 或哈希,确认写入无误;置升级成功标志,下次复位由 Bootloader 决定跳不跳。

校验我一般用 CRC32,把 APP 区域从头到尾算一遍,和 PC 端算好的值比对。源码里如果只做了逐包 CRC,没做整体校验,建议自己补一个,否则单包都对但整体错位的情况查不出来。

4. 避坑与排查:跳转失败、收包丢字节、Flash 写不进的现场记录

4.1 跳转后进 HardFault,PC 停在 0xFFFFFFFE

现象:Bootloader 跳转后,调试器显示 PC 停在 0xFFFFFFFE 或某个异常处理里,APP 完全没跑起来。

原因:APP 的向量表没设对,或者栈顶取错了。H743 的 RAM 地址和 F4 不同,DTCM 在 0x20000000,AXI SRAM 在 0x24000000,如果 APP 链接脚本把栈放在一个 Bootloader 没使能的 RAM 区域,跳过去取栈就异常。

解决:先确认 APP 链接脚本里的 RAM ORIGIN 是芯片实际可用的区域,再确认SCB->VTOR设的是 APP 起始地址。用调试器单步跟到跳转那行,看stack_top和reset_vec读出来的值是否合理——正常应该是 0x2400xxxx 和 0x0802xxxx,如果是 0xFFFFFFFF 说明 APP 根本没烧进去。

4.2 Ymodem 传输中途卡死,PC 端一直重发同一包

现象:传输到某一包后停住,PC 端反复重发,接收端不回 ACK 也不回 NAK。

原因:接收状态机卡在某个状态出不来,通常是 CRC 字节没收全,或者环形缓冲区溢出导致丢字节。H743 主频高,如果串口中断优先级低,被其他中断打断就可能丢字节。

解决:把串口中断优先级设到最高,环形缓冲区开大一点(至少 2 倍包长)。在状态机里加超时计数,某个状态停留超过一定时间就复位到 YM_IDLE 并回 NAK,让 PC 端重发。别让状态机无限等。

4.3 Flash 写入返回 HAL_ERROR,地址看着没问题

现象:HAL_FLASH_Program返回错误,但地址对齐、Sector 也擦过了。

原因:H743 写 Flash 前必须确保目标区域已擦除,且没有写保护。有些板子出厂时开了 Flash 写保护,或者 Option Bytes 里配了安全属性,导致编程失败。另外 H743 的 Flash 编程需要正确的电压和时钟配置,电压不够也会失败。

解决:先读一下 FLASH->CR 和 FLASH->SR 寄存器,看错误标志位是哪一种。如果是写保护,用 STM32CubeProgrammer 连上后解除保护再试。如果是电压问题,确认 VDD 和 VDDIO 在规格范围内。

4.4 APP 能跑但中断不响应,串口收不到数据

现象:跳转后 APP 主循环能跑,但串口中断、定时器中断全都不触发。

原因:Bootloader 跳转前没关干净中断,或者 APP 的 NVIC 配置被 Bootloader 残留状态影响。更隐蔽的一种是 APP 的向量表偏移设了,但中断优先级分组没重新配,导致中断被屏蔽。

解决:Bootloader 跳转前逐个NVIC_DisableIRQ关掉用过的中断,清NVIC->ICPR挂起位。APP 启动后重新调HAL_NVIC_SetPriorityGrouping配分组,再初始化各外设中断。别指望 Bootloader 的状态能干净传给 APP。

4.5 升级后重启又跑回 Bootloader,APP 没被跳转

现象:升级流程走完,复位后还是进 Bootloader,APP 不执行。

原因:升级标志没置对,或者 Bootloader 判断 APP 有效的条件太严。常见的是校验值算错,或者 APP 长度记录为 0。

解决:在 Bootloader 里加串口打印,把读到的标志、长度、校验值都打出来,和 PC 端算的比对。确认写入地址和读取地址一致,别一个写 0x08020000 一个读 0x08020100。

5. 进阶:把 Ymodem 升级做成可回滚的双区方案

单区升级最大的问题是:新固件写坏了,设备就变砖,只能拆机接调试器。H743 的 Flash 够大,完全可以划出 A/B 两个 APP 区,升级时写备用区,校验通过再切换启动区,失败还能回滚到旧版本。这套源码给的是单区骨架,但改成双区并不复杂,核心是把「APP 起始地址」从一个常量变成由标志位决定的变量。

具体做法:Flash 划成 Bootloader + APP_A + APP_B + 标志区。标志区存一个结构体,记录当前启动区、备用区、升级状态。Bootloader 启动时读标志,决定跳 A 还是 B。升级时往非启动区写,写完校验,校验通过才改标志切换。

/* 双区启动决策,Bootloader 里调用 */ typedef struct { uint32_t magic; /* 固定值,标识结构体有效 */ uint32_t active_slot; /* 0 = A 区,1 = B 区 */ uint32_t slot_a_len; /* A 区固件长度 */ uint32_t slot_b_len; /* B 区固件长度 */ uint32_t slot_a_crc; /* A 区 CRC32 */ uint32_t slot_b_crc; /* B 区 CRC32 */ } boot_info_t; void boot_decision(void) { boot_info_t *info = (boot_info_t *)BOOT_INFO_ADDR; if (info->magic != BOOT_MAGIC) { /* 标志区无效,默认跳 A 区 */ jump_to_app(APP_A_ADDR); return; } uint32_t addr = (info->active_slot == 0) ? APP_A_ADDR : APP_B_ADDR; uint32_t len = (info->active_slot == 0) ? info->slot_a_len : info->slot_b_len; uint32_t crc = (info->active_slot == 0) ? info->slot_a_crc : info->slot_b_crc; if (crc32_check(addr, len) == crc) { jump_to_app(addr); /* 校验通过,跳当前区 */ } else { /* 当前区坏了,尝试跳另一个区 */ uint32_t backup = (info->active_slot == 0) ? APP_B_ADDR : APP_A_ADDR; jump_to_app(backup); } }

BOOT_MAGIC是一个你自己定的 32 位常数,比如 0x544F4F42("BOOT" 的 ASCII)。active_slot决定跳哪个区,crc32_check在跳转前验一遍固件完整性。如果当前区 CRC 对不上,自动切到备用区,这就是回滚。

Ymodem 传输部分不用大改,只是写入地址从固定的 APP 起始地址变成「当前非活动区的起始地址」。收完第 0 包拿到长度后,先判断备用区空间够不够,不够就拒绝升级并回 CAN。

验证方法:故意在升级过程中断电,重新上电后 Bootloader 应该发现活动区 CRC 不对,自动跳到备用区,设备还能正常工作。这个测试我每次改完 Bootloader 都会跑一遍,比任何单元测试都管用。

从那以后我每次做 IAP,都强制先跑一遍「断电回滚」测试,确认备用区真的能兜底,才敢把固件发到现场。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询