在 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,把上位机写没了”
敬请期待。
全文完 🚀