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) 需要做磨损均衡,避免频繁擦写同一扇区
编程时间较长 擦除一个扇区通常需要几十到几百毫秒 不能在中断服务函数里做擦除操作
⚠️ 我曾经踩过的坑: 在中断里调用了Flash擦除函数。擦除操作会阻塞CPU几十毫秒,导致中断响应超时,看门狗复位。记住: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;
}
💡 我的习惯: 读Flash时,我一般会加一个CRC校验。因为Flash虽然比RAM稳定,但在强电磁干扰环境下,偶尔也会出现位翻转。尤其是汽车电子和工业控制场景,这个校验不能省。

4.3 Flash写操作(编程)

写Flash比读复杂多了。你得按照芯片手册的流程来,一步都不能错。不同厂商的MCU,写Flash的流程大同小异,但寄存器名字不一样。我以STM32为例,给你拆解一下。

4.3.1 写操作的基本流程

  1. 解锁Flash控制器:向KEYR寄存器写入两个密钥序列
  2. 设置编程模式:配置CR寄存器的PG位
  3. 写入数据:向目标地址写入半字(16位)或字(32位)
  4. 等待操作完成:轮询SR寄存器的BSY位,直到为0
  5. 检查错误:读取SR寄存器,检查是否有编程错误
  6. 锁定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;
}
⚠️ 注意: 写入前一定要确认目标地址已经被擦除。如果地址里不是0xFFFFFFFF,写入操作会把数据搞乱。我见过有人没做这个检查,结果写进去的数据和原有数据“与”在一起,怎么读都不对。

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 扇区擦除流程

  1. 解锁Flash控制器:和写操作一样,先解锁
  2. 设置扇区擦除模式:配置CR寄存器的SER位,并选择扇区号
  3. 启动擦除:设置CR寄存器的STRT位
  4. 等待操作完成:轮询SR寄存器的BSY位
  5. 检查错误:检查是否有擦除错误
  6. 锁定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;
}
💡 避坑指南: 擦除期间,如果发生中断,中断服务函数里的代码如果也在Flash里,就会导致CPU取指失败。我建议你在擦除前关掉所有中断,或者把中断向量表和中断服务函数都放在RAM里。这个在Bootloader里尤其重要,因为Bootloader本身就在Flash里运行。

4.5 Flash驱动时序与寄存器配置

不同MCU的Flash控制器,寄存器布局不一样,但核心逻辑是相通的。我总结了一个通用的寄存器模型,你对照着手册看,很快就能上手。

4.5.1 核心寄存器

寄存器 功能 关键位
KEYR 解锁密钥寄存器 写入两个固定密钥序列解锁
CR 控制寄存器 PG(编程使能)、SER(扇区擦除使能)、STRT(启动操作)、LOCK(锁定)
SR 状态寄存器 BSY(忙标志)、PGERR(编程错误)、WRPRT(写保护错误)
AR 地址寄存器 某些芯片用于指定擦除或编程地址

4.5.2 时序图

下面这张图,是我画的一个Flash编程操作的时序流程。你把它理解透了,写驱动就不会出大问题。

Flash编程操作时序流程 Flash编程操作时序流程 1. 解锁Flash 2. 设置PG位 3. 写入目标地址 4. 轮询BSY位直到为0 5. 检查PGERR标志 6. 锁定Flash 关键说明 每一步都必须按顺序执行 跳过任何一步都会导致操作失败 BSY轮询期间不能访问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的固件下载功能,就是在这个基础上加一个通信协议的事。记住:驱动是地基,地基不稳,上面盖什么都白搭。