3. Flash驱动原理:物理特性、擦除/写入/读取与驱动实现
做Bootloader,绕不开Flash驱动。说白了,Bootloader就住在Flash里,它还得负责把新固件也写到Flash里。你要是连Flash怎么擦、怎么写、怎么读都搞不清楚,那后面移植部署肯定要踩坑。
我刚开始做嵌入式那会儿,总觉得Flash驱动就是调库函数。直到有一次,我在一个STM32F103的项目里,因为没搞清楚扇区对齐,直接把中断向量表给擦没了。板子当场变砖,只能用JTAG救回来。嗯,从那以后,我再也不敢轻视Flash驱动了。
3.1 Flash的物理特性——你得知道它为什么这么“慢”
Flash存储器,本质上是一堆浮栅晶体管。每个存储单元里有个“浮置栅极”,电荷能存进去,断电也不丢。这就是非易失性的来源。
但Flash有个天生的“脾气”:写之前必须先擦除。为什么?因为擦除是把所有位从0变成1,而写入只能把1变成0。你不能直接覆盖写,就像你不能在已经写满字的纸上直接改,得先擦干净。
| 特性 | 说明 | 我踩过的坑 |
|---|---|---|
| 非易失性 | 断电后数据不丢失 | —— |
| 写前必擦 | 只能将1→0,不能0→1 | 直接写会失败,返回错误 |
| 擦除寿命 | 通常1万~10万次 | 频繁擦写同一扇区会报废 |
| 读速度快 | 接近随机访问 | —— |
| 写/擦慢 | 毫秒级,比RAM慢几个数量级 | Bootloader里要加超时处理 |
3.2 扇区与页——Flash的“最小操作单位”
Flash不是按字节擦除的,它有自己的“操作粒度”。这个概念特别重要,你写驱动时得时刻记着。
- 扇区(Sector):擦除的最小单位。不同芯片不一样,有的4KB,有的16KB,有的128KB。你一次擦除,最少得擦一个扇区。
- 页(Page):写入的最小单位。通常是256字节或512字节。你不能只写1个字节,得凑够一页才能写。
- 读取:按字节或字读取,没有限制。
举个例子,我常用的STM32F4系列,它的Flash结构是这样的:
| 扇区编号 | 扇区大小 | 起始地址 | 典型用途 |
|---|---|---|---|
| Sector 0 | 16 KB | 0x0800 0000 | Bootloader |
| Sector 1 | 16 KB | 0x0800 4000 | 参数存储 |
| Sector 2 | 16 KB | 0x0800 8000 | 应用固件起始 |
| Sector 3 | 16 KB | 0x0800 C000 | 应用固件 |
| ... | ... | ... | ... |
3.3 擦除、写入、读取——三步走
Flash驱动核心就三个操作:擦除、写入、读取。咱们一个一个说。
3.3.1 擦除操作
擦除就是把整个扇区清成0xFF。注意,是整个扇区,不是部分。你哪怕只想改1个字节,也得把整个扇区擦掉,然后重新写入。
擦除流程大致如下:
- 检查Flash是否忙(正在擦写)
- 解锁Flash控制寄存器
- 设置扇区编号
- 触发擦除命令
- 等待擦除完成(轮询状态寄存器)
- 锁定Flash
我曾经在一个项目里,因为没检查Flash是否忙,直接发了擦除命令,结果跟之前的写入操作冲突了,Flash直接挂了。从那以后,我每次操作前都会加一个“忙检查”。
3.3.2 写入操作
写入是按页或按字写的。大多数MCU支持16位半字或32位字写入。写入前,目标地址必须是擦除过的(也就是0xFF状态)。
写入流程:
- 检查Flash是否忙
- 解锁Flash
- 设置写入模式
- 写入数据(按字或半字)
- 等待写入完成
- 锁定Flash
3.3.3 读取操作
读取最简单,直接通过指针访问就行。Flash在MCU的地址空间里是统一编址的,所以你可以像读RAM一样读Flash。
// 读取示例
uint32_t read_word(uint32_t addr) {
return *(volatile uint32_t *)addr;
}
uint8_t read_byte(uint32_t addr) {
return *(volatile uint8_t *)addr;
}
读取不需要解锁,也不需要等待。但要注意,如果读取的地址是擦除后的(0xFF),那读回来的就是0xFF。
3.4 驱动代码实现——手写一个Flash驱动
下面我以一个常见的Cortex-M3/M4 MCU为例,写一个精简的Flash驱动。这个驱动我用了好几年,移植到过好几个平台,基本逻辑都一样。
// flash_drv.h
#ifndef FLASH_DRV_H
#define FLASH_DRV_H
#include <stdint.h>
// 扇区大小定义(以STM32F4为例)
#define FLASH_SECTOR_SIZE_16KB (16 * 1024)
#define FLASH_SECTOR_SIZE_64KB (64 * 1024)
#define FLASH_SECTOR_SIZE_128KB (128 * 1024)
// 错误码
typedef enum {
FLASH_OK = 0,
FLASH_ERR_BUSY,
FLASH_ERR_ADDR_ALIGN,
FLASH_ERR_WRITE_PROTECT,
FLASH_ERR_TIMEOUT
} flash_status_t;
// 函数声明
flash_status_t flash_erase_sector(uint32_t sector_addr);
flash_status_t flash_write_word(uint32_t addr, uint32_t data);
flash_status_t flash_write_buffer(uint32_t addr, uint32_t *buf, uint32_t len);
uint32_t flash_read_word(uint32_t addr);
void flash_read_buffer(uint32_t addr, uint32_t *buf, uint32_t len);
#endif
// flash_drv.c
#include "flash_drv.h"
// 等待Flash操作完成
static flash_status_t flash_wait_busy(uint32_t timeout_ms) {
uint32_t tick = 0;
while (/* 检查Flash状态寄存器忙标志 */) {
if (tick++ > timeout_ms) {
return FLASH_ERR_TIMEOUT;
}
// 延时1ms
}
return FLASH_OK;
}
// 擦除整个扇区
flash_status_t flash_erase_sector(uint32_t sector_addr) {
flash_status_t status;
// 1. 检查地址是否在Flash范围内
if (sector_addr < FLASH_BASE || sector_addr > FLASH_END) {
return FLASH_ERR_ADDR_ALIGN;
}
// 2. 等待Flash空闲
status = flash_wait_busy(1000);
if (status != FLASH_OK) return status;
// 3. 解锁Flash
FLASH->KEYR = 0x45670123;
FLASH->KEYR = 0xCDEF89AB;
// 4. 设置扇区编号并触发擦除
FLASH->CR |= (sector_num << 3); // 设置扇区号
FLASH->CR |= FLASH_CR_SER; // 扇区擦除使能
FLASH->CR |= FLASH_CR_STRT; // 开始擦除
// 5. 等待完成
status = flash_wait_busy(5000);
// 6. 锁定Flash
FLASH->CR &= ~FLASH_CR_SER;
FLASH->CR |= FLASH_CR_LOCK;
return status;
}
// 写入一个字(32位)
flash_status_t flash_write_word(uint32_t addr, uint32_t data) {
flash_status_t status;
// 地址必须4字节对齐
if (addr & 0x3) {
return FLASH_ERR_ADDR_ALIGN;
}
status = flash_wait_busy(1000);
if (status != FLASH_OK) return status;
// 解锁
FLASH->KEYR = 0x45670123;
FLASH->KEYR = 0xCDEF89AB;
// 设置编程模式
FLASH->CR |= FLASH_CR_PG;
// 写入数据
*(volatile uint32_t *)addr = data;
// 等待完成
status = flash_wait_busy(1000);
// 锁定
FLASH->CR &= ~FLASH_CR_PG;
FLASH->CR |= FLASH_CR_LOCK;
return status;
}
// 读取一个字
uint32_t flash_read_word(uint32_t addr) {
return *(volatile uint32_t *)addr;
}
3.5 Flash操作的知识结构
下面这张图,是我自己总结的Flash驱动知识结构。每次写驱动前,我都会对照着检查一遍。
3.6 避坑指南——我踩过的那些坑
做Flash驱动,有几个坑特别容易踩。我列出来,你写代码时多留个心眼。
- 擦除时掉电:擦除过程中掉电,扇区数据会损坏。Bootloader里最好加个校验,擦除前备份关键数据。
- 写入地址不对齐:32位写入地址必须是4的倍数。不对齐的话,轻则写入失败,重则硬件错误。
- 忘记解锁:Flash控制器默认是锁住的。不解锁直接写,写不进去。
- 超时处理:擦除和写入都有时间限制。如果Flash坏了或者卡住了,没有超时处理,程序就死在那了。
- 中断干扰:擦写Flash时,如果中断来了,可能会干扰时序。我习惯在擦写前关中断,完事再开。
好了,Flash驱动的基本原理和实现就这些。你把这个驱动框架吃透了,后面做Bootloader的移植部署就会轻松很多。记住:写前必擦、地址对齐、检查忙状态,这三句话值一万块。