第三章 Flash驱动深度解析:读/写/擦除操作、等待周期与时序、分区规划策略
做MCU Boot,说白了就是跟Flash打交道。你写的代码最终要烧进去,上电后CPU第一条指令要从Flash取出来。如果Flash驱动没写好,轻则启动慢,重则直接变砖。这一章,我把这些年跟Flash死磕的经验掰开揉碎讲给你听。
核心认知:Flash是Boot的基石。读不对,程序跑飞;写不对,升级失败;擦不对,整片报废。分区规划不好,后面所有逻辑都得推倒重来。
3.1 Flash读操作:看似简单,坑却不少
读Flash,大部分MCU直接寻址就能读。比如STM32,你定义一个const数组指向0x08000000,直接取值就行。但这里有个细节——对齐访问。
我记得有一次,一个同事用uint8_t*去读一个32位数据,结果地址没对齐,硬生生多花了3个总线周期。你想想看,Boot里要校验几十KB甚至几MB的数据,每个地址多3个周期,累积下来就是几百毫秒的延迟。
我的习惯:读Flash时,尽量用32位或64位对齐的指针。如果数据量不大,可以用memcpy,编译器会自动优化成最佳指令序列。
另外,有些MCU在读Flash时,如果总线频率太高,需要插入等待周期。这个后面会细说。
3.2 Flash写操作:不是你想写就能写
写Flash跟写RAM完全是两码事。RAM是直接写地址,Flash得先解锁、再编程、再锁定。而且,Flash只能把1写成0,不能把0写成1。想恢复成1?只能擦除。
我见过不少新手,写Flash前忘了检查是否已擦除,结果数据写进去全是乱的。嗯,这里要注意:写之前一定要确认目标地址是0xFF(擦除后的状态)。
下面是一个典型的Flash写流程,以STM32为例:
// Flash写操作标准流程
void Flash_Write(uint32_t addr, uint32_t data) {
// 1. 检查地址是否对齐
if (addr & 0x3) return; // 32位对齐检查
// 2. 解锁Flash
FLASH->KEYR = 0x45670123;
FLASH->KEYR = 0xCDEF89AB;
// 3. 等待上次操作完成
while (FLASH->SR & FLASH_SR_BSY);
// 4. 检查是否已擦除
if (*(volatile uint32_t*)addr != 0xFFFFFFFF) {
// 未擦除,需要先擦除
Flash_Erase_Sector(addr);
}
// 5. 写入数据
FLASH->CR |= FLASH_CR_PG; // 使能编程
*(volatile uint32_t*)addr = data;
while (FLASH->SR & FLASH_SR_BSY);
FLASH->CR &= ~FLASH_CR_PG; // 关闭编程
// 6. 锁定Flash
FLASH->CR |= FLASH_CR_LOCK;
}
我曾经踩过的坑:写Flash时忘了关中断。结果写了一半,中断来了,Flash控制器被中断服务程序又操作了一次,直接导致数据错乱。所以,写Flash前一定要关全局中断,写完了再开。
3.3 Flash擦除操作:最慢、最危险、最容易被忽略
擦除是Flash操作里最慢的。一个扇区(通常4KB或64KB)擦除一次,少则几十毫秒,多则几百毫秒。而且,擦除期间CPU是挂起的——因为Flash控制器忙着擦除,没法响应指令。
为什么会这么慢?因为擦除本质上是给整个扇区加高压,把所有的浮栅晶体管里的电子都赶出去。这个过程物理上就快不了。
擦除的粒度也很重要。有的MCU支持按扇区擦除,有的只能按块擦除。我建议你仔细看芯片手册,搞清楚最小擦除单位。比如:
| MCU系列 | 最小擦除单位 | 擦除时间(典型) | 注意事项 |
|---|---|---|---|
| STM32F1 | 1KB(页) | ~20ms | 页擦除,粒度小 |
| STM32F4 | 16KB/64KB/128KB(扇区) | ~100ms | 不同扇区大小不同 |
| NXP LPC | 256B/4KB(扇区) | ~25ms | 支持部分擦除 |
| GD32 | 4KB(页) | ~30ms | 兼容STM32指令 |
擦除策略上,我个人的经验是:能不擦就不擦,能少擦就少擦。比如OTA升级时,只擦需要更新的扇区,别整片擦。
3.4 Flash等待周期与时序:频率越高,等待越多
Flash的读取速度是有上限的。MCU主频跑得比Flash快,就得插入等待周期(Wait State)。说白了,就是CPU读Flash时,多等几个时钟周期,让Flash把数据准备好。
这个参数在芯片手册里通常有个表格:
| 系统时钟频率 | 等待周期数 | Flash访问时间 |
|---|---|---|
| 0 ~ 24 MHz | 0 | 零等待 |
| 24 ~ 48 MHz | 1 | 1个时钟周期 |
| 48 ~ 72 MHz | 2 | 2个时钟周期 |
| 72 ~ 120 MHz | 3 | 3个时钟周期 |
等待周期越多,CPU执行指令就越慢。因为每条指令都要从Flash取,取指令多等几个周期,流水线就堵住了。
我的优化技巧:如果Boot对启动速度要求高,可以先把关键代码(比如Flash驱动、中断向量表)拷贝到RAM里执行。这样就不用受Flash等待周期影响了。很多MCU都支持这个功能,叫“程序在RAM中运行”。
另外,有些MCU支持预取缓冲(Prefetch Buffer)和指令缓存(I-Cache)。打开这些功能,能显著减少等待周期的影响。我建议你在初始化时钟后,第一时间配置好这些。
3.5 Flash分区规划策略:别等写代码了才想
分区规划是Boot设计里最需要提前想清楚的事。一旦芯片量产了,分区就焊死在Flash里了,想改?只能换芯片。
我一般把Flash分成这么几个区:
- Boot区:存放Bootloader代码,通常16KB~64KB。这个区写保护,防止被意外擦除。
- 参数区:存放配置参数、校准数据、设备序列号等。通常4KB~16KB,需要频繁读写。
- App区:存放用户应用程序。大小取决于你的应用,通常占Flash的60%~80%。
- 备份区:OTA升级时,存放新固件。大小跟App区一样,用于安全升级。
- 日志区:存放运行日志、错误记录。可选,但强烈建议保留。
下面是一个典型的分区布局:
// Flash分区规划示例(假设总容量1MB)
#define BOOT_ADDR 0x08000000 // Boot区:64KB
#define BOOT_SIZE 0x00010000
#define PARAM_ADDR 0x08010000 // 参数区:16KB
#define PARAM_SIZE 0x00004000
#define APP_ADDR 0x08014000 // App区:512KB
#define APP_SIZE 0x00080000
#define BACKUP_ADDR 0x08094000 // 备份区:512KB
#define BACKUP_SIZE 0x00080000
#define LOG_ADDR 0x08114000 // 日志区:48KB
#define LOG_SIZE 0x0000C000
我曾经犯过的错:分区时没考虑擦除对齐。App区结束地址跟备份区起始地址没对齐到扇区边界,结果擦除备份区时,把App区最后几个字节也擦掉了。所以,分区地址一定要对齐到最小擦除单位。
分区规划还有个原则:Boot区要写保护。很多MCU都有Flash写保护寄存器,把Boot区对应的位设成1,Boot区就只读不写了。这样即使App跑飞了,也不会把Boot区冲掉。
下面这张图展示了Flash分区的整体逻辑:
3.6 实战中的几个关键决策点
最后,我总结几个实战中必须想清楚的问题:
- 等待周期设多少? 根据你的主频查手册,别设少了(Flash读错),也别设多了(性能浪费)。
- 要不要开缓存? 如果Boot里代码量大,开I-Cache能明显提速。但要注意,写Flash时要清缓存,否则读到脏数据。
- 分区要不要留冗余? 我建议留10%~20%的空闲空间。万一后期要加功能,不用重新规划分区。
- 擦除策略怎么定? 如果OTA升级频繁,考虑用“双备份”策略:先擦备份区,写入新固件,校验通过后再擦App区,拷贝过去。这样即使升级中断,还能回滚。
我的个人习惯:每次做新项目,我都会先画一张Flash分区图,标清楚每个区的起始地址、大小、用途、写保护状态。然后贴在工位上。这样写代码时不会搞混地址。