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,那效率得多低啊。

重要提示:H7的Flash写入必须按32字节对齐。也就是说,你写数据时,起始地址必须是32的倍数,数据长度也必须是32的倍数。这个坑我踩过,当时调试了半天,发现写进去的数据总是错位。

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):这是安全升级的关键。万一升级失败,可以从这里恢复。
避坑指南:我曾经在项目里把参数存储区和App放在同一个扇区。结果每次升级都要先读出参数,擦除扇区,再写回去。万一掉电,参数就丢了。后来我专门给参数留了一个独立扇区,再也没出过问题。

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

这样写的好处是,以后想调整分区大小,只需要改宏定义就行。我在做产品迭代时,经常需要调整分区,用宏定义管理起来特别方便。

警告:千万不要把Bootloader和App的地址搞重叠了!我见过有人把App起始地址设成0x08020000,结果跟Bootloader的扇区重叠了。一运行App,Bootloader就被覆盖了,下次上电直接变砖。

3.5 实际项目中的分区策略

最后,分享一个我在实际项目中的经验。如果你的产品需要支持远程升级,我建议采用双备份分区策略:

  1. 运行区:当前正在运行的App
  2. 下载区:新固件下载到这里
  3. 备份区:上一次成功运行的App

升级流程是这样的:

1. 下载新固件到下载区
2. 校验固件完整性(CRC/SHA256)
3. 如果校验通过:
   - 将运行区固件复制到备份区
   - 将下载区固件复制到运行区
   - 跳转到运行区执行
4. 如果校验失败:
   - 从备份区恢复固件
   - 保持系统可用

这个策略虽然占用了更多Flash空间,但安全性大大提高。我做过一个医疗设备项目,客户要求升级失败后必须能自动回滚,用的就是这个方案。

好了,关于存储架构和分区设计,今天就聊到这里。下一章我们会深入Bootloader的启动流程,看看芯片上电后到底发生了什么。到时候我会结合实际的汇编代码来讲解,敬请期待。