3、存储架构与分区设计:STM32H7内部Flash结构、扇区与块的区别、Bootloader与App分区规划
大家好,我是你们的嵌入式系统讲师。今天我们来聊聊STM32H7的存储架构。说实话,这部分内容看起来有点枯燥,但它是整个Bootloader和安全升级的基石。你想想看,如果连Flash怎么分区都没搞明白,后面写代码肯定会踩坑。
3.1 STM32H7内部Flash结构
STM32H7系列的Flash,跟F系列相比,变化挺大的。我个人习惯把它的内部结构想象成一个大仓库。这个仓库有2MB的空间(以H743为例),但它的组织方式跟F4/F1完全不同。
先看一张简化的结构图:
Flash基地址: 0x0800 0000
总大小: 2MB (0x0800 0000 - 0x081F FFFF)
+------------------+ 0x0800 0000
| Sector 0 | 128KB
+------------------+ 0x0802 0000
| Sector 1 | 128KB
+------------------+ 0x0804 0000
| Sector 2 | 128KB
+------------------+ 0x0806 0000
| Sector 3 | 128KB
+------------------+ 0x0808 0000
| Sector 4 | 128KB
+------------------+ 0x080A 0000
| Sector 5 | 128KB
+------------------+ 0x080C 0000
| Sector 6 | 128KB
+------------------+ 0x080E 0000
| Sector 7 | 128KB
+------------------+ 0x0810 0000
| ... 更多Sector |
+------------------+ 0x081F FFFF
嗯,这里要注意:H7的Flash扇区大小是统一的128KB。跟F4那种大小不一的扇区比起来,规划起来省心多了。我在项目中遇到过用F429的,它的前4个扇区才16KB,后面又变成64KB,分区时特别别扭。
3.2 扇区与块的区别
很多初学者会把扇区(Sector)和块(Block)搞混。我刚开始学的时候也犯过这个错。说白了,扇区是擦除的最小单位,而块是读写的最小单位。
具体区别看这个表格:
| 特性 | 扇区 (Sector) | 块 (Block) |
|---|---|---|
| 最小擦除单位 | 128KB | 不支持擦除 |
| 最小写入单位 | 32字节(双字) | 32字节(双字) |
| 最小读取单位 | 1字节 | 1字节 |
| 用途 | 分区管理、擦除操作 | 数据缓存、DMA传输 |
| 数量 | 16个(2MB总容量) | 由用户定义 |
为什么会这样设计?因为Flash的物理特性决定了擦除必须是大块操作,而读写可以按字节或字来。你想想看,如果每次写一个字节都要擦除128KB,那效率得多低啊。
3.3 Bootloader与App分区规划
好了,到了最核心的部分。怎么给Bootloader和App分配Flash空间?我建议遵循一个原则:Bootloader要小,但要稳;App要大,但要灵活。
我个人常用的分区方案是这样的:
Flash布局(2MB总容量):
+------------------+ 0x0800 0000
| Bootloader | 128KB (Sector 0)
+------------------+ 0x0802 0000
| 参数存储区 | 128KB (Sector 1)
+------------------+ 0x0804 0000
| App 固件区 | 1.5MB (Sector 2-13)
+------------------+ 0x081C 0000
| 备份固件区 | 256KB (Sector 14-15)
+------------------+ 0x081F FFFF
为什么这么分?我来解释一下:
- Bootloader(128KB):足够了。Bootloader代码量一般不会超过64KB,留128KB是为了以后扩展。我曾经见过有人只留了32KB,结果后面想加个加密功能都塞不进去。
- 参数存储区(128KB):存放设备ID、升级标志、校验值等。单独一个扇区,擦写方便,不会影响到其他区域。
- App固件区(1.5MB):主程序区。H7的App通常比较大,特别是用了GUI或文件系统的话。
- 备份固件区(256KB):这是安全升级的关键。万一升级失败,可以从这里恢复。
3.4 分区对齐与地址计算
分区时要注意对齐。H7的每个扇区是128KB,所以分区起始地址必须是128KB的整数倍。比如:
Bootloader起始地址: 0x0800 0000
App起始地址: 0x0804 0000 (偏移量 = 4 * 128KB = 512KB)
备份区起始地址: 0x081C 0000 (偏移量 = 14 * 128KB = 1.75MB)
在代码里,我习惯用宏定义来管理这些地址:
#define FLASH_BASE_ADDR 0x08000000
#define SECTOR_SIZE 0x20000 // 128KB
#define BOOTLOADER_START_ADDR 0x08000000
#define BOOTLOADER_SIZE 0x20000 // 1个扇区
#define PARAM_START_ADDR 0x08020000
#define PARAM_SIZE 0x20000 // 1个扇区
#define APP_START_ADDR 0x08040000
#define APP_SIZE 0x180000 // 12个扇区,1.5MB
#define BACKUP_START_ADDR 0x081C0000
#define BACKUP_SIZE 0x40000 // 2个扇区,256KB
这样写的好处是,以后想调整分区大小,只需要改宏定义就行。我在做产品迭代时,经常需要调整分区,用宏定义管理起来特别方便。
3.5 实际项目中的分区策略
最后,分享一个我在实际项目中的经验。如果你的产品需要支持远程升级,我建议采用双备份分区策略:
- 运行区:当前正在运行的App
- 下载区:新固件下载到这里
- 备份区:上一次成功运行的App
升级流程是这样的:
1. 下载新固件到下载区
2. 校验固件完整性(CRC/SHA256)
3. 如果校验通过:
- 将运行区固件复制到备份区
- 将下载区固件复制到运行区
- 跳转到运行区执行
4. 如果校验失败:
- 从备份区恢复固件
- 保持系统可用
这个策略虽然占用了更多Flash空间,但安全性大大提高。我做过一个医疗设备项目,客户要求升级失败后必须能自动回滚,用的就是这个方案。
好了,关于存储架构和分区设计,今天就聊到这里。下一章我们会深入Bootloader的启动流程,看看芯片上电后到底发生了什么。到时候我会结合实际的汇编代码来讲解,敬请期待。