4、Flash驱动与分区设计:Flash物理特性、扇区/页擦除写入、分区表设计

做MCU Bootloader,绕不开Flash。说句实话,我见过太多项目栽在Flash操作上——不是擦写时序搞错,就是分区表设计不合理,导致升级失败后板子变砖。这一章,咱们就把Flash的脾气摸透,把分区表设计得明明白白。

4.1 Flash物理特性——你得知道它有多“倔”

Flash不是RAM,你不能像写内存那样随便往里头写数据。它有三大特性,你必须刻在脑子里:

  • 写前必擦:Flash只能将1写成0,但不能将0写成1。想改数据?得先把整个扇区擦成0xFF,再写新值。
  • 擦除有寿命:每个扇区擦写次数有限,一般是1万到10万次。Boot区擦得少,App区频繁升级,你得心里有数。
  • 写入要按页:大多数MCU支持按页写入,一页通常是256字节或512字节。你不能写半个字节,得凑够一页。

核心结论:Flash操作的最小单位是“扇区擦除”和“页写入”。扇区大小因芯片而异,从1KB到128KB都有。设计驱动前,先翻芯片手册,把扇区大小、页大小、擦写时间记下来。

4.2 扇区与页——擦除和写入的“砖块”

我习惯把Flash想象成一栋楼:扇区是楼层,页是房间。你要装修(写数据),得先清空整个楼层(擦除扇区),再逐个房间布置(页写入)。

概念 类比 典型大小 操作
扇区 (Sector) 楼层 4KB / 8KB / 64KB 擦除(整层清空)
页 (Page) 房间 256B / 512B / 1KB 写入(逐个房间放家具)

举个例子,STM32F4系列,扇区大小不统一:前4个扇区各16KB,后面是64KB和128KB。我刚开始做时没注意这个,结果擦除时算错地址,把Boot代码给抹了……嗯,从那以后我写驱动前一定会画一张Flash地址映射图。

4.3 Flash驱动实现——擦、写、读三板斧

驱动函数不需要多,三个就够了:擦除扇区、写入页、读取数据。我给你看一个精简的驱动框架:

/* 擦除一个扇区 */
int flash_sector_erase(uint32_t sector_addr) {
    // 1. 检查地址是否在Flash范围内
    // 2. 解锁Flash控制器
    // 3. 等待上次操作完成
    // 4. 设置扇区地址,发起擦除
    // 5. 等待擦除完成(轮询或中断)
    // 6. 加锁Flash控制器
    // 7. 返回状态
}

/* 写入一页数据 */
int flash_page_write(uint32_t page_addr, uint8_t *data, uint32_t len) {
    // 1. 检查地址对齐(页边界)
    // 2. 检查长度不超过页大小
    // 3. 解锁Flash控制器
    // 4. 按字或半字写入
    // 5. 等待写入完成
    // 6. 加锁Flash控制器
    // 7. 回读校验(可选,我强烈建议加上)
}

/* 读取数据(其实就是内存访问) */
void flash_read(uint32_t addr, uint8_t *buf, uint32_t len) {
    memcpy(buf, (uint8_t*)addr, len);
}

我的习惯:每次写入后都做一次回读校验。别信硬件说“写入成功”,我踩过坑——电源不稳时,Flash写入会静默失败。回读能第一时间发现问题。

4.4 分区表设计——Bootloader的“城市规划”

分区表是Bootloader的骨架。设计不好,升级失败连回退的机会都没有。我个人习惯把Flash至少分成四个区:

  1. Boot区:存放Bootloader代码,一般32KB~128KB。这个区几乎不擦写,寿命最长。
  2. App区:存放用户应用程序。升级时擦写这个区。
  3. 备份区:存放上一个稳定版本的App。升级失败时回滚用。
  4. 配置区:存放设备参数、升级标志、版本号等。频繁读写,选小扇区。

下面这张图是我常用的分区结构,你一看就明白:

MCU Flash 分区结构图 Boot区 (32KB ~ 128KB) 存放Bootloader代码,几乎不擦写 App区 (128KB ~ 1MB) 用户应用程序,升级时擦写 备份区 (128KB ~ 1MB) 上一个稳定版本,升级失败时回滚 配置区 (4KB ~ 16KB)

4.5 分区设计要点——避坑指南

分区不是随便划几块就完事。我总结了几条实战经验:

  • Boot区放最前面:MCU上电从0x08000000开始执行,Boot区必须从起始地址开始。
  • App区对齐扇区边界:App起始地址必须是扇区大小的整数倍。否则擦除时会误伤Boot区。
  • 备份区和App区大小一致:这样升级时可以直接整区复制,逻辑简单。
  • 配置区用最小的扇区:配置数据经常改,用大扇区擦一次要等很久,还浪费寿命。

我曾经犯过的错:有一次我把配置区和App区放在同一个大扇区里。每次升级擦除App区,配置数据也跟着没了。后来我专门给配置区划了一个独立的小扇区,再也没出过问题。

4.6 分区表示例——以STM32F407为例

给你看一个实际项目的分区表。STM32F407有1MB Flash,扇区大小不统一,设计时要特别小心:

分区 起始地址 大小 扇区编号 用途
Boot区 0x08000000 64KB Sector 0~3 (各16KB) Bootloader代码
App区 0x08010000 384KB Sector 4~6 (128KB+128KB+128KB) 用户应用程序
备份区 0x08070000 384KB Sector 7~9 (128KB+128KB+128KB) 回滚备份
配置区 0x080D0000 16KB Sector 10 (16KB) 升级标志、版本号
剩余 0x080D4000 176KB Sector 11 (16KB) + 剩余 预留或日志

小技巧:配置区最后两个字节放一个“校验魔数”,比如0x5A5A。Boot启动时检查这个魔数,如果不对,说明配置区被破坏了,直接进入恢复模式。这个做法救过我很多次。

4.7 分区设计检查清单

每次设计新板子的分区,我都会过一遍这个清单:

  • ☐ Boot区起始地址 = Flash起始地址?
  • ☐ 所有分区起始地址对齐扇区边界?
  • ☐ 备份区大小 ≥ App区大小?
  • ☐ 配置区使用最小扇区?
  • ☐ 配置区有校验机制(魔数或CRC)?
  • ☐ 升级过程中断电,能保证回滚?

嗯,Flash驱动和分区设计就这些。说白了,就是摸清Flash的脾气,然后给它画好格子。格子画好了,后面的升级、回滚、配置管理都顺风顺水。要是格子画歪了……你想想看,升级到一半发现擦错了区,那滋味可不好受。