FPGA SoC 的 RISC-V 固件开发全攻略(七):Bootloader 设计详解
FPGA SoC 的 RISC-V 固件开发全攻略(七):Bootloader 设计详解
本文是《FPGA SoC 的 RISC-V 固件开发全攻略》专栏第 7 篇。
上一篇:第 6 篇《Flash 参数持久化:双备份+循环磨损》 | 下一篇:第 8 篇《Ymodem 协议详解》
本文介绍嵌入式系统中 Bootloader 的通用设计方案与关键模块实现思路,涵盖启动状态机、Flash 分区管理、双区容错、环境参数持久化、串口升级通道与 APP 跳转。不涉及任何具体芯片或产品的私有实现细节。
一、Bootloader 的职责与设计目标
Bootloader 是上电后最先运行的固件,负责把系统从"复位"安全地引导到"应用正常运行"。一个健壮的 Bootloader 通常要满足三个设计目标:
- 可恢复:应用固件损坏或升级失败时,系统不能变砖,必须能回到可恢复状态。
- 可升级:支持现场固件升级,且升级过程中断电不会导致系统无法启动。
- 可容错:应用启动失败(如崩溃、看门狗复位)时能自动回退到备用固件。
围绕这三个目标,Bootloader 的设计可以拆解为几个核心模块,下面逐一展开。
二、整体启动流程(状态机)
Bootloader 的主流程通常是一个顺序状态机,每一步职责单一、清晰。典型流程如下:
上电复位 │ ├─ Step 1: 最小系统初始化 │ 关中断 → 初始化调试串口 → 初始化 Flash 驱动 │ ├─ Step 2: 读取环境标志 │ 获取默认启动分区、升级待执行标志、各分区启动失败计数 │ ├─ Step 3: 判断是否进入强制升级模式 │ 入口①: 串口初始化后的窗口期内收到升级命令帧 │ 入口②: 两个应用分区连续启动失败均超阈值 │ ├─ Step 4: 有待执行升级则校验并刷写升级包 │ ├─ Step 5: 启动分区决策(当前分区失败则切换备用分区) │ ├─ Step 6: 强制升级模式(等待并接收升级包) │ └─ Step 7: 跳转 APP(清理上下文 → 跳转应用入口)这种"顺序步骤 + 返回值"的结构,比层层嵌套的if-else更易读、易维护,也便于后续增加或合并步骤。
三、重要模块一:Flash 分区管理
Bootloader 的所有数据都落在同一颗 SPI NOR Flash 上,通过分区来隔离不同用途的数据。一个典型的通用分区布局如下:
| 分区 | 用途 |
|---|---|
| Bootloader 区 | 存放 Bootloader 本体,只读,通常不做自升级 |
| APP1 区 | 应用固件主分区 |
| APP2 区 | 应用固件备份分区(A/B 双区) |
| 升级包暂存区 | 先接收升级包,校验通过后再刷写,避免边收边写损坏目标分区 |
| 参数区 | 掉电不丢失的用户参数(Flash EEPROM 模拟) |
| 环境标志区 | Bootloader 自身的状态标志(启动分区、失败计数等) |
关键设计点:
- 升级包先暂存、后刷写:升级数据先完整写入"暂存区"并校验,确认无误后再擦写目标分区。这样即使传输中途断电,也只是暂存区数据不完整,目标分区(旧固件)不受影响,下次上电仍能正常启动。
- Bootloader 只读、不自升级:Bootloader 越简单越稳定,把它设计成"只读、不做自升级",可显著降低变砖风险。
四、重要模块二:环境参数持久化(双备份 + 循环磨损)
Bootloader 需要把"当前启动哪个分区"“各分区失败几次”"是否有待执行升级"等信息掉电保存。这些数据存放在 Flash 的环境标志区。
4.1 双备份防掉电损坏
Flash 擦写中途断电,会把当前扇区写成垃圾数据。若只存一份环境标志,一旦损坏,Bootloader 就无法判断启动分区,系统变砖。
因此采用A/B 双备份:同一份数据在相邻两个区域各存一份,写的时候两份都写(或交替写),读的时候取有效的那份。
环境标志区 ┌───────────────┐ A 备份 ├───────────────┤ │ ... │ 预留 ├───────────────┤ └───────────────┘ B 备份读取仲裁:分别读 A/B 两份,校验通过且"写入序号(seq)较大"的那份视为最新有效数据;若只有一份有效则用该份;两份都坏则按"首次启动"处理,直接进入强制升级模式。
4.2 循环磨损均衡
Flash 有擦写寿命限制,若每次都擦写同一个扇区,该扇区会提前磨穿。解决方法是把每份备份再拆成多个等大小的 slot,循环轮流写入,让擦写次数均匀分摊到所有 slot 上,成倍延长寿命。
写入序号(seq)的作用:每次写入时 seq 单调递增,读取时通过比较 seq 判断哪份更新。seq 的仲裁 + 双备份 + 多 slot 轮转,共同构成了一个可靠的掉电安全持久化方案。
这是通用的"Flash EEPROM 模拟"思想,广泛应用于各类嵌入式设备的参数存储。
五、重要模块三:双区容错(A/B 分区 + 启动成功确认)
双区容错是 Bootloader 可靠性的核心,解决"应用固件坏了怎么办"的问题。
5.1 A/B 双分区
维护两个平等的应用分区(APP1 / APP2),同一时刻只有一个"活动分区"被启动。当活动分区启动失败时,自动切换到另一个分区重试。
5.2 启动成功确认协议
如何判定"启动失败"?关键是一个启动成功标志(boot_success),配合看门狗:
Bootloader 跳转 APP 前: 写 boot_success = 0 (等待 APP 确认) APP 启动并自检通过后: 写 boot_success = 1 (显式确认启动成功) 清零该分区失败计数 APP 崩溃 / 看门狗复位(未及确认): boot_success 仍为 0 → Bootloader 下次读到 boot_success == 0 → 判定上次启动失败,该分区失败计数 +1核心约定:
- "启动失败"的唯一判据:上次跳转后
boot_success未变为 1(APP 没来得及确认就复位了)。 - 失败计数清零的时机:APP 确认成功时,或升级刷写新固件成功后(新固件不继承旧固件的失败计数)。
- 看门狗保证"卡死的 APP"最终会复位,从而触发失败计数。
5.3 失败计数与自动回退
启动分区决策: ├─ 活动分区失败计数 < 阈值 → 正常启动该分区 ├─ 活动分区失败计数 ≥ 阈值 → 切换到备用分区 │ ├─ 备用分区失败计数 < 阈值 → 启动备用分区 │ └─ 备用分区也超阈值 → 双分区都失败 → 进入强制升级模式这个"失败计数 + 自动切换 + 双失败进强制升级"的机制,是 A/B 分区 OTA 方案的通用设计。
六、重要模块四:强制升级模式
当系统无法正常启动时(双分区都失败),需要进入"强制升级模式"来救砖。
6.1 入口条件(通常两个)
| 入口 | 说明 |
|---|---|
| 硬件/命令触发 | 上电初始化后,在极短的窗口期内收到约定的升级命令帧 |
| 连续启动失败 | 两个应用分区连续启动失败均超过阈值 |
命令码窗口要设计得足够短(如几十毫秒),既能捕捉用户的升级指令,又不拖慢正常启动时间。
6.2 串口升级通道(Ymodem)
强制升级模式最常用的是串口通道,协议采用Ymodem(文件传输协议,自带文件名/大小/CRC16 校验,终端工具普遍支持)。
关于 Ymodem 协议的帧格式与接收实现,可参考单独的文章《Ymodem 协议详解》。这里只强调与 Bootloader 结合的两个要点:
- 先收后写:升级数据先写入"升级包暂存区",校验通过后再刷写到目标分区。
- 擦除粒度:刷写时的擦除操作要分块进行,单次擦除耗时需小于上位机的应答超时,否则会导致发送方超时重传。
七、重要模块五:APP 跳转
Bootloader 完成决策后,把控制权交给 APP。跳转前必须做上下文清理,否则 APP 可能因残留状态而运行异常。典型跳转序列:
voidjump_to_app(uint32_tapp_entry){disable_irq();/* 关闭全局中断 */flush_dcache();/* 刷写数据缓存 */invalidate_icache();/* 失效指令缓存 */fence();/* 内存屏障 */((void(*)(void))app_entry)();/* 跳转应用入口,不再返回 */}关键点:
- 跳转目标是应用的复位入口(reset vector),而不是
main()。应用的启动代码会重新完成整套初始化(设置栈、中断向量表、拷贝代码段到运行区等),因此 Bootloader 无需手动搬运镜像或设置栈指针。 - 跳转前写回环境标志(含
boot_success = 0),为下一轮"启动成功确认"做准备。 - 跳转成功后就"不可达"了,APP 已接管系统。
不同架构的跳转细节略有差异(如 Cortex-M 需手动设置 SP 再跳复位向量,而部分 RISC-V 架构直接跳复位入口即可),但"清理上下文 + 跳复位入口"的思想是通用的。
八、总结
一个健壮的嵌入式 Bootloader,本质上是由几个正交的模块组合而成:
| 模块 | 解决的问题 |
|---|---|
| Flash 分区管理 | 数据隔离,升级包先暂存后刷写 |
| 环境参数持久化 | 双备份 + 循环磨损,掉电安全 |
| 双区容错 | A/B 分区 + 启动成功确认 + 失败回退 |
| 强制升级模式 | 命令触发 + 串口 Ymodem 救砖 |
| APP 跳转 | 上下文清理,安全移交控制权 |
设计 Bootloader 时,始终围绕三个目标展开:可恢复、可升级、可容错。把每个模块的职责理清、边界划清,就能得到一个稳定、可维护、可现场升级的启动系统。
本文仅介绍 Bootloader 的通用设计思想与工程经验,不涉及任何具体项目、芯片或产品的私有实现细节。
