我在没有硬件的情况下,把 STM32 的 IAP 固件升级流程完整跑通了

86 阅读4分钟

在 MCU 项目里,IAP(In-Application Programming) 永远是让工程师头疼的模块——擦 Flash 怕写坏,跳转 App 一不小心就黑屏,打断点更是靠运气。真正把一个 Bootloader 调通,往往不是“会不会写代码”的问题,而是能不能复现每一个细节的执行现场

我之前也遇到过这些问题,但后来我发现:

IAP 并不一定要靠硬件调试。

这一次,我把 STM32F412 的 Bootloader + IAP 升级链路,完全跑在了 Renode 模拟器内,无需接线、无需烧录、无需硬件。

运行一次升级,就像跑一次单元测试。

🌟 为什么要做这件事?

做 IAP 最大的痛点是:

痛点原因
每次升级要烧录硬件物理操作慢、浪费时间
跳转 App 很容易死机MSP/PC/VTOR 弄错一次就凉
Flash 只能擦写有限次数开发期不断写入就是在消耗寿命
串口协议调试麻烦上位机、重传、校验链路不好验证

于是我开始思考——

能不能在没有硬件的情况下,把整个 IAP 升级流程验证完?

Renode 给了我答案。

🚀 一句话总结这个项目

一个极简、零 HAL 依赖的 STM32F412 Bootloader,通过 UART3 + XMODEM 在线刷写 App,并在 Renode 中可全流程调试,无需实体设备。

🗂️ 项目目录结构(核心)

apps/
  boot/              # Bootloader:UART3 + XMODEM + Flash + 跳转
    main.c
    update.c
    xmodem.c/h
    uart3.c/h
    flash.c/h
    jump.c/h
    ld/boot.ld       # Bootloader 16KB
  app/               # 示例应用:UART3 心跳输出
    main.c
    STM32F412ZGJX_FLASH.ld
cmake/
renode/platform.resc # Renode 平台脚本
docs/DEBUG.md
.vscode/tasks.json    # 一键编译+运行 Renode

仓库不到几十个源文件,阅读成本极低,非常适合作为 Bootloader 学习参考。

🧭 Bootloader 到底要做什么?

Bootloader 的职责很简单:

上电 → 初始化外设 → 判断是否要更新 → 擦除/写入 Flash → 校验镜像 → 跳转 App

我的实现采用最朴素、也最经典的方式判断 App 是否有效

static int app_is_valid(void) {
    uint32_t app_sp = *(uint32_t *)APP_START_ADDRESS;
    uint32_t app_reset = *(uint32_t *)(APP_START_ADDRESS + 4U);
    int sp_in_range = (app_sp >= RAM_START) && (app_sp <= RAM_END);
    int pc_thumb = (app_reset & 0x1U) != 0U;
    int pc_in_range = (app_reset >= APP_START_ADDRESS) && (app_reset < APP_END);
    return sp_in_range && pc_thumb && pc_in_range;
}

判断逻辑包括:

检查项目的
SP 是否落在 RAM 区栈合法性
Reset Handler 是否为 Thumb 地址ARM 指令合法性
PC 是否在 App 区域防止跳野

只有合法,才允许跳转。

🌀 跳转 App 怎么才能不死机?

这是所有 Bootloader 的带宗问。

正确做法必须包含:

  • 禁止中断
  • 清除 SysTick
  • 重定向 VTOR
  • 重设 MSP
  • 执行 App Reset Handler

完整逻辑如下:

void jump_to_app(void) {
    uint32_t app_sp = *(uint32_t *)(APP_START_ADDRESS);
    uint32_t app_reset = *(uint32_t *)(APP_START_ADDRESS + 4U);

    __disable_irq();
    SysTick->CTRL = SysTick->LOAD = SysTick->VAL = 0;
    SCB->VTOR = APP_START_ADDRESS;
    __set_MSP(app_sp); __DSB(); __ISB();

    void (*app_entry)(void) = (void (*)(void))(app_reset);
    app_entry();
}

Renode 的加持让这个行为完全可见、可复现,不用赌运气。

📡 为什么我选择 XMODEM?

因为 Bootloader 不追求高级,而追求绝对可靠

XMODEM 的协议帧只有 133 Bytes:

SOH | 序号 | ~序号 | 128 字节数据 | CRC16_H | CRC16_L

实现代码也极致干净:

if (frame_len == 1 && frame[0] == EOT) {
    uart3_write_byte(ACK);
    *received_bytes = write_addr - app_start_addr;
    return 0;
}

没有冗余设计,没有复杂状态机,但稳得不能再稳。

🔁 完整升级链路(核心)

上电复位
→ clock_init / uart3_init
→ Boot 周期性发送 'C' 发起 XMODEM 握手
→ 接收 SOH 包 → 校验序号/CRC16
→ 擦除 App 扇区
→ flash_write_word 写入数据块
→ ACK/NAK/EOT 控制数据流
→ firmware 接收完成
→ app_is_valid()
→ jump_to_app()
→ App 心跳输出开始运行

在 Renode 上,这条链可以无限次测试,没有任何硬件损耗。

🎮 运行效果(你可以在本地复现)

Bootloader 在 Renode 输出如下:

Bootloader start...
Waiting for XMODEM...
Receiving block 1/84...
ACK
Receiving block 2/84...
ACK
EOT detected
APP valid, jumping...
[APP] tick=123
[APP] tick=124
...

第一次看到这段输出的时候,我真的非常爽:

没有硬件,却完成了真正意义上的 MCU 固件升级。

🔮 这个项目的未来

这个 Bootloader 可以继续扩展:

方向价值
A/B 双分区断电不中断升级
镜像签名/哈希构建可信启动链
支持 TCP远程升级 MCU
YAML )协议驱动下一篇文章要讲的重点 🚀

是的,我已经在做一个“用 YAML 控制协议逻辑的上位机框架”。 让升级逻辑不需要写代码,只需要一份 YAML。

下一篇我会系统讲这个方向👇 (欢迎关注防走丢)

📌 总结

Bootloader 并不是“玄学黑盒”, 它是完全可以被模拟、验证、理解和复制的软件逻辑。

Renode 让嵌入式开发第一次拥有了:

⚡ 可回放的升级流程 ⚡ 可预测的调试体验 ⚡ 无损硬件的测试环境

这件事本身,就值得被看见。

📎 项目开源地址

👉 仓库链接请放在这里

评论区可见

如果文章对你有帮助,欢迎一键三连支持我继续写下一篇 YAML 协议引擎的文章。

下一章,将是你从没见过的:

“用一份 YAML,把上位机写没了”

敬请期待。

全文完 🚀