STM32F103VET6实战:用W25Q64外部Flash实现Bootloader+Application双备份(含完整代码解析)
最近在做一个工业数据采集终端的项目,客户要求设备在野外部署后能够稳定运行数年,同时还要支持远程固件升级。最头疼的问题就是升级过程中万一断电怎么办?总不能让人跑到荒郊野外去重新烧录程序吧。这种场景下,传统的单备份升级方案风险太高,一旦升级失败设备就“变砖”了。
我调研了几种方案,最终决定采用Bootloader+双备份的架构,利用STM32F103VET6内部Flash运行程序,再通过SPI接口连接W25Q64外部Flash存储两个完整的应用程序副本。这样即使升级过程中出现任何意外,设备也能自动回滚到之前的稳定版本,确保系统始终可用。
这篇文章就是我在实际项目中摸索出来的完整实现方案,从硬件选型到代码细节都会详细拆解。如果你也在为STM32的远程升级安全性发愁,或者想了解如何利用外部Flash做双备份,相信这篇实战指南能给你不少启发。
1. 硬件架构设计与选型考量
选择STM32F103VET6搭配W25Q64这个组合,是我经过多次测试对比后的决定。STM32F103VET6属于Cortex-M3内核,512KB的内部Flash和64KB的RAM,性能足够应对大多数嵌入式应用。更重要的是它的性价比——在需要大量部署的工业场景中,成本控制是个现实问题。
W25Q64是Winbond的8MB SPI Flash,它的几个特性特别适合做程序备份:
- SPI接口简单:只需要4根线(CS、CLK、MISO、MOSI)就能连接,不占用太多IO资源
- 扇区结构规整:整个芯片分为128个扇区,每个扇区64KB,还有256个4KB的块,擦写管理很方便
- 低功耗:深度睡眠模式下电流只有1μA,对于电池供电的设备很友好
- 耐用性:每个扇区可擦写10万次,数据保存期限20年
注意:W25Q64的写操作需要先擦除,擦除的最小单位是4KB扇区。这意味着如果你的程序大小不是4KB的整数倍,会浪费一些存储空间。
硬件连接上,我推荐下面这种接法:
STM32F103VET6 W25Q64
PA4 (SPI1_NSS) --- CS
PA5 (SPI1_SCK) --- CLK
PA6 (SPI1_MISO) --- DO
PA7 (SPI1_MOSI) --- DI
3.3V --- VCC
GND --- GND
SPI的时钟频率我设置在18MHz左右,这个速度既能保证数据传输效率,又不会因为信号完整性问题导致读写错误。实际测试中,通过W25Q64备份一个200KB的应用程序大约需要2.3秒,这个时间对于大多数升级场景都是可以接受的。
2. Flash空间规划与分区策略
空间规划是整个方案的基础,分得好不好直接影响到后续的维护复杂度。W25Q64有8MB空间,看起来很大,但如果不合理规划,后期扩展会很麻烦。
我的分区方案是这样的:
| 分区名称 | 起始地址 | 大小 | 用途说明 |
|---|---|---|---|
| Bootloader区 | 0x08000000 | 16KB | 存放Bootloader程序 |
| Application区 | 0x08004000 | 496KB | 运行中的应用程序 |
| 外部Flash分区1 | 0x000000 | 1MB | 存储待升级的新程序 |
| 外部Flash分区2 | 0x100000 | 1MB | 存储当前程序的备份 |
| 标志位区 | 0x200000 | 4KB | 存储升级状态标志和校验信息 |
为什么Application区从0x08004000开始?因为STM32的中断向量表默认在0x08000000,Bootloader需要占用前面的空间。16KB对于Bootloader来说绰绰有余,实际上我的Bootloader代码编译后只有8KB左右,留一些余量给未来功能扩展。
外部Flash的1MB分区是怎么来的?STM32F103VET6内部Flash最大512KB,去掉Bootloader的16KB,剩下496KB。取整到1MB是为了对齐和管理方便,虽然浪费了一些空间,但换来了清晰的地址映射。
标志位区只需要很小的空间,我分配了4KB(一个扇区)。这里存放的关键信息包括:
struct ProgramStatusFlag {
uint8_t backup_flag; // 0xA5表示已有备份
uint32_t old_prog_add_checksum; // 备份程序累加校验和
uint8_t old_prog_xor_checksum; // 备份程序异或校验和
uint32_t new_prog_add_checksum; // 新程序累加校验和
uint8_t new_prog_xor_checksum; // 新程序异或校验和
uint8_t update_flag; // 0x55表示需要更新
};
这种分区方案有几个好处:
- 地址对齐:每个分区起始地址都是1MB边界,计算和跳转都很方便
- 预留空间:即使未来应用程序增大,1MB的空间也足够容纳
- 隔离性好:Bootloader、Application、备份数据物理隔离,互不干扰
3. Bootloader设计与实现细节
Bootloader是整套系统的“大脑”,它需要在每次上电时做出关键决策:是运行现有程序,还是执行升级流程?我的Bootloader设计遵循“简单可靠”的原则,功能尽量精简,代码经过充分测试。
3.1 启动流程与状态判断
Bootloader的main函数逻辑是这样的:
int main(void) {
// 初始化硬件
hardware_init();
// 读取外部Flash中的状态标志
read_status_flags();
// 判断是否需要备份当前程序
if (status.backup_flag != 0xA5) {
// 首次运行或Flash被擦除,执行备份
backup_current_program();
}
// 判断是否需要更新程序
if (status.update_flag == 0x55) {
// 有新的程序需要更新
update_new_program();
}
// 跳转到应用程序
jump_to_application();
// 正常情况下不会执行到这里
while(1);
}
这里有个细节需要注意:backup_flag的判断。W25Q64擦除后的值是0xFF,所以如果读出来不是0xA5,就说明要么是第一次运行,要么是标志位区被意外擦除了。这种情况下Bootloader会主动备份当前程序,确保系统至少有一个可用的备份。
3.2 程序备份机制
备份过程的核心是数据校验。我

&spm=1001.2101.3001.5002&articleId=152850455&d=1&t=3&u=47d0d6175b924b1faa997ccc53cee7e4)
3万+

被折叠的 条评论
为什么被折叠?



