MCU固件升级基础:Bootloader与App分区、Flash存储器特性、升级流程
做固件升级,说白了就是让MCU自己把自己给“换掉”。听起来有点玄乎,但拆开来看,核心就三块:Bootloader怎么设计、Flash怎么操作、流程怎么走通。我做了这么多年嵌入式,见过太多升级失败翻车的案例,十有八九都是这三块没吃透。
这一章,咱们就把地基打牢。
1. Bootloader与App分区:MCU的“双系统”架构
MCU的Flash空间,通常被划分为两个区域:Bootloader区和App区。你可以把它想象成电脑的BIOS和操作系统。BIOS负责启动、自检、加载系统;操作系统负责干活。MCU里也一样。
核心原则:Bootloader和App在物理地址上是完全隔离的。Bootloader不能轻易被擦除,它是升级操作的“保险丝”。
1.1 分区布局
我习惯把Flash空间规划成下面这样。具体地址偏移量取决于芯片,但逻辑结构是通用的。
| 区域 | 起始地址(示例) | 大小 | 内容 |
|---|---|---|---|
| 中断向量表(Boot) | 0x08000000 | 4KB | Bootloader的中断向量 |
| Bootloader代码 | 0x08001000 | 48KB | 升级逻辑、Flash驱动、通信协议 |
| App中断向量表 | 0x08010000 | 4KB | App的中断向量 |
| App代码 | 0x08011000 | 400KB | 用户应用程序 |
| 参数/标志区 | 0x0807F000 | 4KB | 升级标志、版本号、校验值 |
注意看,App区也有自己的中断向量表。这是跳转的关键,后面会细说。
1.2 Bootloader的核心职责
Bootloader不是万能的,它只干三件事:
- 检查升级请求:上电后,先看有没有升级标志。比如某个GPIO电平、或者Flash里的特定标志位。
- 执行升级流程:如果有请求,就进入升级模式,接收数据、写入Flash。
- 跳转到App:如果没有升级请求,或者升级完成,就跳转到App区执行。
我的经验:Bootloader代码要尽量精简。我见过有人把RTOS塞进Bootloader里,结果升级时自己把自己搞死机了。Bootloader越简单,越可靠。
2. Flash存储器特性:你得懂它的脾气
Flash不是RAM,不能随便写。它有自己的一套规矩。不懂这些规矩,升级代码写出来就是定时炸弹。
2.1 三大操作:读、写、擦除
| 操作 | 特点 | 注意事项 |
|---|---|---|
| 读 | 最快,直接寻址 | 无特殊限制,但要注意对齐(32位MCU通常4字节对齐) |
| 写(编程) | 只能将1写成0,不能将0写成1 | 写入前目标地址必须是擦除状态(全0xFF) |
| 擦除 | 将整个扇区恢复成全0xFF | 擦除单位是扇区(Sector),不是字节。扇区大小因芯片而异,从1KB到256KB不等。 |
为什么会这样?因为Flash的物理结构决定了它只能“放电”(写0)和“充电”(擦除成1)。你想想看,如果你要修改一个字节,得先把整个扇区读到RAM里,擦除扇区,再在RAM里改好,最后写回去。这就是经典的“读-改-写”流程。
避坑指南:我曾经在一个项目里,没注意扇区大小,直接按字节擦除。结果芯片锁死了,只能重新烧录。记住:擦除的最小单位是扇区,不是字节!
2.2 Flash的寿命与可靠性
Flash有擦写次数限制,一般是1万到10万次。对于Bootloader区,因为很少擦写,问题不大。但App区如果频繁升级,就得考虑磨损均衡。
另外,Flash在写入时如果掉电,数据会损坏。所以升级过程中,校验是必须的。我习惯在每包数据写入后立即读回校验,而不是等全部写完再校验。这样能尽早发现问题。
3. 升级流程:下载、校验、跳转
标准的MCU固件升级流程,可以概括为三步。下面这张图,是我画给团队新人的,你一看就明白。
3.1 下载:怎么把固件塞进去
下载方式有很多种:UART、SPI、I2C、USB、CAN、以太网……不管用哪种,核心逻辑都一样:分包传输。
我一般把固件分成固定大小的包,比如512字节一包。每包带一个序号和CRC校验。接收方收到后,先校验,再写入Flash,然后回复ACK。发送方收到ACK才发下一包。这样即使中间断了,也能断点续传。
我的习惯:在参数区保存一个“已接收包序号”。下次重启后,Bootloader先读这个序号,从断点处继续接收。这个功能在产线升级时特别有用,能省不少时间。
3.2 校验:别让坏固件跑起来
校验分两个层面:
- 包校验:每包数据写入前,用CRC32或SHA256校验。我推荐CRC32,速度快,硬件支持也好。
- 整体校验:全部写完后,对整个App区做一次哈希校验,确保固件完整。
如果整体校验失败,怎么办?绝对不能跳转!我见过有人直接跳转,结果App跑飞,设备变砖。正确的做法是:保留旧固件,重新进入升级模式。这就是“双备份”的思想。
3.3 跳转:最关键的一步
跳转不是简单的goto。你需要做几件事:
- 关闭所有中断:包括全局中断和所有外设中断。
- 禁用SysTick:如果用了RTOS,还要停止调度器。
- 重置中断向量表:把SCB->VTOR寄存器指向App的中断向量表起始地址。
- 设置栈指针:从App的中断向量表首地址读取栈顶指针,赋值给MSP。
- 跳转到复位向量:从App的中断向量表第二个字(偏移4字节)读取复位地址,然后跳转。
代码实现大概是这样的:
/* Bootloader跳转到App的示例代码 */
typedef void (*pFunction)(void);
void JumpToApp(uint32_t app_addr)
{
uint32_t stack_ptr;
pFunction reset_handler;
/* 1. 关闭所有中断 */
__disable_irq();
/* 2. 从App向量表读取栈顶指针 */
stack_ptr = *(volatile uint32_t*)app_addr;
/* 3. 从App向量表读取复位向量 */
reset_handler = (pFunction)(*(volatile uint32_t*)(app_addr + 4));
/* 4. 设置主栈指针 */
__set_MSP(stack_ptr);
/* 5. 重映射中断向量表到App区 */
SCB->VTOR = app_addr;
/* 6. 跳转 */
reset_handler();
/* 理论上不会执行到这里 */
while(1);
}
避坑指南:我曾经在跳转前忘了关一个定时器中断。结果跳转后,App还没初始化完,中断就来了,直接跑飞。排查了两天才找到原因。记住:跳转前,把所有外设都复位到默认状态。
4. 总结一下
这一章的内容,说白了就是三个关键词:分区、特性、流程。
- 分区要清晰,Bootloader和App物理隔离。
- Flash操作要小心,擦除是扇区级的,写入前必须擦除。
- 流程要严谨,下载、校验、跳转,一步都不能省。