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里要加超时处理
注意:Flash的擦除寿命是有限的。Bootloader里如果频繁擦写同一个扇区做参数存储,用不了多久Flash就废了。我见过一个产品,因为每5秒存一次参数,半年后Flash就挂了。后来改成磨损均衡算法才解决。

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 应用固件
... ... ... ...
经验之谈:我习惯把Bootloader放在最小的扇区里,比如16KB。这样擦除快,而且不容易误伤应用区。另外,参数存储最好单独用一个扇区,别跟代码混在一起。

3.3 擦除、写入、读取——三步走

Flash驱动核心就三个操作:擦除、写入、读取。咱们一个一个说。

3.3.1 擦除操作

擦除就是把整个扇区清成0xFF。注意,是整个扇区,不是部分。你哪怕只想改1个字节,也得把整个扇区擦掉,然后重新写入。

擦除流程大致如下:

  1. 检查Flash是否忙(正在擦写)
  2. 解锁Flash控制寄存器
  3. 设置扇区编号
  4. 触发擦除命令
  5. 等待擦除完成(轮询状态寄存器)
  6. 锁定Flash

我曾经在一个项目里,因为没检查Flash是否忙,直接发了擦除命令,结果跟之前的写入操作冲突了,Flash直接挂了。从那以后,我每次操作前都会加一个“忙检查”。

3.3.2 写入操作

写入是按页或按字写的。大多数MCU支持16位半字或32位字写入。写入前,目标地址必须是擦除过的(也就是0xFF状态)。

写入流程:

  1. 检查Flash是否忙
  2. 解锁Flash
  3. 设置写入模式
  4. 写入数据(按字或半字)
  5. 等待写入完成
  6. 锁定Flash
关键点:写入时地址必须对齐。如果是32位写入,地址必须是4字节对齐。不对齐的话,会触发硬件错误。我刚开始写驱动时,因为地址没对齐,查了整整一天才找到原因。

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;
}
移植要点:不同MCU的Flash控制器寄存器不一样,但逻辑大同小异。你只需要改寄存器地址和位定义,整体框架不用动。我移植这个驱动到GD32、AT32、NXP的芯片上,基本就是改改宏定义。

3.5 Flash操作的知识结构

下面这张图,是我自己总结的Flash驱动知识结构。每次写驱动前,我都会对照着检查一遍。

Flash驱动知识结构 Flash驱动核心 擦除操作 写入操作 读取操作 最小单位:扇区 擦除后全为0xFF 需等待完成 最小单位:页/字 地址必须对齐 写前必须擦除 按字节/字读取 无需解锁 通过指针直接访问 关键:写前必擦、地址对齐、检查忙状态

3.6 避坑指南——我踩过的那些坑

做Flash驱动,有几个坑特别容易踩。我列出来,你写代码时多留个心眼。

  • 擦除时掉电:擦除过程中掉电,扇区数据会损坏。Bootloader里最好加个校验,擦除前备份关键数据。
  • 写入地址不对齐:32位写入地址必须是4的倍数。不对齐的话,轻则写入失败,重则硬件错误。
  • 忘记解锁:Flash控制器默认是锁住的。不解锁直接写,写不进去。
  • 超时处理:擦除和写入都有时间限制。如果Flash坏了或者卡住了,没有超时处理,程序就死在那了。
  • 中断干扰:擦写Flash时,如果中断来了,可能会干扰时序。我习惯在擦写前关中断,完事再开。
曾经有一次:我在一个量产项目里,因为没处理擦除超时,结果一批Flash有坏块的板子全卡死在Bootloader里。后来加了超时重试和错误上报,才把问题解决。从那以后,我每个Flash操作都加超时。

好了,Flash驱动的基本原理和实现就这些。你把这个驱动框架吃透了,后面做Bootloader的移植部署就会轻松很多。记住:写前必擦、地址对齐、检查忙状态,这三句话值一万块。