4. Flash驱动开发:从存储器特性到寄存器配置
Flash驱动,说白了就是Bootloader的根基。没有它,你连固件都写不进去。我刚开始做Bootloader时,总觉得Flash驱动就是调几个库函数的事。直到有一次,我在一个ST的芯片上直接调用标准库的擦除函数,结果在中断里触发了擦除操作——整个系统直接挂掉。嗯,从那以后,我再也不敢轻视Flash驱动了。
这一章,咱们就从头捋一遍Flash的那些事。从存储器的物理特性,到读、写、擦除的时序,再到寄存器怎么配。你把这些搞明白了,后面写Bootloader就是水到渠成的事。
4.1 Flash存储器的核心特性
Flash不是RAM,它有几个非常“倔强”的脾气。你得顺着它来,不然它就罢工。
| 特性 | 说明 | 对驱动的影响 |
|---|---|---|
| 非易失性 | 掉电不丢数据 | 写入前必须确保电源稳定,否则数据可能损坏 |
| 按扇区擦除 | 不能单独擦一个字节,最小擦除单位是扇区(通常1KB~128KB) | 擦除前必须备份扇区内其他有效数据 |
| 写前必须擦 | Flash只能从1写为0,不能从0写为1 | 写入前必须确保目标地址已被擦除(全0xFF) |
| 擦写次数有限 | 通常10万次(NOR Flash)或1万次(NAND Flash) | 需要做磨损均衡,避免频繁擦写同一扇区 |
| 编程时间较长 | 擦除一个扇区通常需要几十到几百毫秒 | 不能在中断服务函数里做擦除操作 |
4.2 Flash读操作
读Flash其实最简单。在大多数MCU里,Flash和RAM是统一编址的。你直接定义一个指针,指向Flash地址,然后读就行了。
举个例子,假设Flash基地址是0x08000000:
// 从Flash地址读取一个32位数据
uint32_t *flash_ptr = (uint32_t *)0x08000000;
uint32_t data = *flash_ptr; // 直接读,就这么简单
但这里有个细节——对齐访问。有些MCU要求Flash读取必须按32位对齐,否则会触发HardFault。我建议你写一个通用的读取函数,把对齐问题封装起来:
/**
* @brief 从Flash读取指定长度的数据
* @param addr 起始地址(必须按32位对齐)
* @param buf 输出缓冲区
* @param len 要读取的字节数
* @return 0成功,-1失败
*/
int flash_read(uint32_t addr, uint8_t *buf, uint32_t len) {
uint32_t i;
// 检查地址对齐
if (addr & 0x03) {
return -1; // 地址未对齐
}
// 按32位读取
uint32_t *src = (uint32_t *)addr;
uint32_t *dst = (uint32_t *)buf;
uint32_t word_len = len >> 2;
for (i = 0; i < word_len; i++) {
dst[i] = src[i];
}
// 处理剩余字节(如果有)
uint32_t remain = len & 0x03;
if (remain) {
uint32_t last_word = src[word_len];
uint8_t *last_byte = (uint8_t *)&last_word;
for (i = 0; i < remain; i++) {
buf[word_len * 4 + i] = last_byte[i];
}
}
return 0;
}
4.3 Flash写操作(编程)
写Flash比读复杂多了。你得按照芯片手册的流程来,一步都不能错。不同厂商的MCU,写Flash的流程大同小异,但寄存器名字不一样。我以STM32为例,给你拆解一下。
4.3.1 写操作的基本流程
- 解锁Flash控制器:向KEYR寄存器写入两个密钥序列
- 设置编程模式:配置CR寄存器的PG位
- 写入数据:向目标地址写入半字(16位)或字(32位)
- 等待操作完成:轮询SR寄存器的BSY位,直到为0
- 检查错误:读取SR寄存器,检查是否有编程错误
- 锁定Flash控制器:设置CR寄存器的LOCK位
看代码更直观:
/**
* @brief 向Flash写入一个字(32位)
* @param addr 目标地址(必须按32位对齐)
* @param data 要写入的数据
* @return 0成功,-1失败
*/
int flash_write_word(uint32_t addr, uint32_t data) {
// 1. 解锁Flash
FLASH->KEYR = 0x45670123; // 第一个密钥
FLASH->KEYR = 0xCDEF89AB; // 第二个密钥
// 2. 设置编程模式
FLASH->CR |= FLASH_CR_PG;
// 3. 写入数据
*(volatile uint32_t *)addr = data;
// 4. 等待操作完成
while (FLASH->SR & FLASH_SR_BSY);
// 5. 检查错误
if (FLASH->SR & FLASH_SR_PGERR) {
FLASH->SR |= FLASH_SR_PGERR; // 清除错误标志
FLASH->CR &= ~FLASH_CR_PG; // 退出编程模式
return -1;
}
// 6. 锁定Flash
FLASH->CR |= FLASH_CR_LOCK;
return 0;
}
4.3.2 写操作的时序要求
Flash写入不是瞬间完成的。它需要一定的时间,这个时间由芯片内部的电荷泵和编程电压决定。不同MCU的时序参数不一样,但一般都在这个范围内:
| 参数 | 典型值 | 说明 |
|---|---|---|
| 编程时间(半字) | 20~50 μs | 写入一个16位数据所需时间 |
| 编程时间(字) | 40~100 μs | 写入一个32位数据所需时间 |
| 编程电压 | 1.8V~3.6V | 电压过低会导致写入失败 |
| 最大编程次数 | 10万次 | 超过后该扇区可能失效 |
你可能会问:为什么写入一个32位数据需要几十微秒?因为Flash内部需要先把数据加载到页缓冲区,然后启动编程流程,电荷泵把电压升到足够高,才能把电子注入浮栅。这个过程是物理层面的,快不了。
4.4 Flash擦除操作
擦除是Flash操作里最耗时的。一个扇区的擦除,通常需要几十到几百毫秒。而且擦除期间,CPU不能访问Flash(包括读代码),所以你的程序必须从RAM里运行。
4.4.1 扇区擦除流程
- 解锁Flash控制器:和写操作一样,先解锁
- 设置扇区擦除模式:配置CR寄存器的SER位,并选择扇区号
- 启动擦除:设置CR寄存器的STRT位
- 等待操作完成:轮询SR寄存器的BSY位
- 检查错误:检查是否有擦除错误
- 锁定Flash控制器
/**
* @brief 擦除Flash的一个扇区
* @param sector 扇区编号(0~11,取决于芯片型号)
* @return 0成功,-1失败
*/
int flash_erase_sector(uint32_t sector) {
// 1. 解锁Flash
FLASH->KEYR = 0x45670123;
FLASH->KEYR = 0xCDEF89AB;
// 2. 设置扇区擦除模式
FLASH->CR |= FLASH_CR_SER; // 扇区擦除使能
FLASH->CR &= ~FLASH_CR_SNB_MASK; // 清除扇区号
FLASH->CR |= (sector << FLASH_CR_SNB_POS); // 设置扇区号
// 3. 启动擦除
FLASH->CR |= FLASH_CR_STRT;
// 4. 等待操作完成
while (FLASH->SR & FLASH_SR_BSY);
// 5. 检查错误
if (FLASH->SR & FLASH_SR_ERR_MASK) {
FLASH->SR |= FLASH_SR_ERR_MASK; // 清除错误标志
FLASH->CR &= ~FLASH_CR_SER; // 退出擦除模式
return -1;
}
// 6. 锁定Flash
FLASH->CR |= FLASH_CR_LOCK;
return 0;
}
4.5 Flash驱动时序与寄存器配置
不同MCU的Flash控制器,寄存器布局不一样,但核心逻辑是相通的。我总结了一个通用的寄存器模型,你对照着手册看,很快就能上手。
4.5.1 核心寄存器
| 寄存器 | 功能 | 关键位 |
|---|---|---|
| KEYR | 解锁密钥寄存器 | 写入两个固定密钥序列解锁 |
| CR | 控制寄存器 | PG(编程使能)、SER(扇区擦除使能)、STRT(启动操作)、LOCK(锁定) |
| SR | 状态寄存器 | BSY(忙标志)、PGERR(编程错误)、WRPRT(写保护错误) |
| AR | 地址寄存器 | 某些芯片用于指定擦除或编程地址 |
4.5.2 时序图
下面这张图,是我画的一个Flash编程操作的时序流程。你把它理解透了,写驱动就不会出大问题。
4.6 驱动封装与分层设计
写驱动不能只写一个函数就完事。你得考虑分层,方便以后移植到不同芯片。我一般把Flash驱动分成三层:
| 层级 | 文件 | 职责 |
|---|---|---|
| 硬件抽象层 | flash_hal.c/h | 封装寄存器操作,提供读、写、擦除的底层接口 |
| 驱动层 | flash_drv.c/h | 实现扇区管理、磨损均衡、错误重试等逻辑 |
| 应用层 | flash_app.c/h | 提供面向Bootloader的接口,如固件写入、参数存储 |
举个例子,硬件抽象层的接口可以这样设计:
// flash_hal.h
#ifndef FLASH_HAL_H
#define FLASH_HAL_H
#include <stdint.h>
// Flash硬件参数
#define FLASH_SECTOR_SIZE (16 * 1024) // 16KB per sector
#define FLASH_SECTOR_COUNT 12 // 12 sectors total
#define FLASH_BASE_ADDR 0x08000000
// 底层接口
int flash_hal_init(void);
int flash_hal_read(uint32_t addr, uint8_t *buf, uint32_t len);
int flash_hal_write(uint32_t addr, const uint8_t *data, uint32_t len);
int flash_hal_erase(uint32_t sector);
void flash_hal_lock(void);
void flash_hal_unlock(void);
#endif
驱动层则在这个基础上,加上扇区状态管理:
// flash_drv.c
// 扇区状态表,记录每个扇区的使用情况
typedef struct {
uint32_t sector_id;
uint32_t erase_count; // 擦除次数
uint8_t is_valid; // 是否有效
} sector_info_t;
static sector_info_t sector_table[FLASH_SECTOR_COUNT];
int flash_drv_write_sector(uint32_t sector, const uint8_t *data) {
// 1. 检查扇区是否有效
if (!sector_table[sector].is_valid) {
return -1;
}
// 2. 擦除扇区
if (flash_hal_erase(sector) != 0) {
return -1;
}
// 3. 写入数据
uint32_t addr = FLASH_BASE_ADDR + sector * FLASH_SECTOR_SIZE;
if (flash_hal_write(addr, data, FLASH_SECTOR_SIZE) != 0) {
return -1;
}
// 4. 更新擦除次数
sector_table[sector].erase_count++;
return 0;
}
🔑 核心要点:
- Flash读操作最简单,直接指针访问即可,注意地址对齐
- 写操作必须按“解锁→设置模式→写入→等待→检查→锁定”的流程来
- 擦除操作最耗时,必须在主循环中执行,不能在中断里做
- 驱动要分层设计,方便移植到不同MCU平台
- 每次写入前,确认目标地址已被擦除(值为0xFFFFFFFF)
好了,Flash驱动这块就讲这么多。你把这些代码和流程吃透了,后面写Bootloader的固件下载功能,就是在这个基础上加一个通信协议的事。记住:驱动是地基,地基不稳,上面盖什么都白搭。