2. Flash存储器详解:NOR Flash与NAND Flash的区别,STM32内部Flash的组织结构

说到单片机里的存储器,Flash绝对是绕不开的主角。我刚开始做嵌入式那会儿,总觉得Flash就是个存代码的地方,能读能写就行。直到有一次项目量产时发现程序跑飞,查了三天才发现是Flash擦写时序没处理好。嗯,从那以后我再也不敢小看它了。

这一节,咱们就把Flash掰开揉碎了讲。重点看三个东西:NOR和NAND到底差在哪、STM32内部Flash长什么样、以及怎么用代码去操作它。

2.1 NOR Flash vs NAND Flash:核心差异

很多人一上来就问:NOR和NAND,选哪个?我的回答是:先搞清楚你要干什么。

对比项 NOR Flash NAND Flash
读取方式 随机读取,类似RAM,可寻址到字节 页读取,必须按块/页操作
写入方式 按字节/字写入,但擦除必须按扇区 按页写入,擦除按块
擦除粒度 扇区(通常64KB~256KB) 块(通常128KB~512KB)
读取速度 快(几十ns级) 较慢(μs级,需先加载到页缓存)
写入速度 慢(μs级/字节) 快(几百μs/页)
擦除速度 慢(几百ms/扇区) 快(几ms/块)
坏块管理 无坏块问题 出厂就有坏块,需要ECC和坏块表
典型用途 代码就地执行(XIP)、小容量存储 大容量数据存储、文件系统

核心结论:NOR适合存代码,因为CPU可以直接在上面跑程序(XIP)。NAND适合存数据,容量大、成本低,但操作复杂。STM32内部集成的就是NOR Flash。

我个人习惯:如果只是存固件,用NOR;如果要存日志、图片、音频,用NAND。你想想看,NAND的坏块管理要是没做好,存到坏块上的数据就全丢了——我在一个数据记录仪项目里就吃过这个亏。

2.2 STM32内部Flash的组织结构

STM32的Flash属于NOR类型,但它的内部结构比单纯的NOR要复杂一些。以STM32F4系列为例,它的Flash分为两大块:主存储块和信息块。

STM32内部Flash组织结构(以STM32F4为例) 主存储块(Main Memory) 扇区0 ~ 扇区3(16KB × 4) 扇区4(64KB) 扇区5 ~ 扇区7(128KB × 3) 扇区8 ~ 扇区11(128KB × 4) 扇区12 ~ 扇区23(128KB × 12) 信息块 系统存储器 (Bootloader) OTP区域 (一次性编程) 选项字节 (配置保护) 注意:不同系列(F1/F4/H7)的扇区大小和数量不同,操作前务必查手册

2.3 主存储块与信息块详解

主存储块就是咱们存代码和常量数据的地方。它被划分成多个扇区(Sector),每个扇区大小可能不一样。以STM32F407为例,前4个扇区是16KB,第4个是64KB,后面全是128KB。为什么要这么设计?说白了,就是为了灵活存放Bootloader和应用程序。

信息块包含三个部分:

  • 系统存储器:出厂固化的Bootloader,用于串口下载等。这个区域用户不能擦写。
  • OTP区域:一次性编程区,只能写一次。我曾在产品里用它存设备序列号,挺方便。
  • 选项字节:配置读保护、写保护、看门狗等。改这个要小心,搞不好芯片就锁死了。

我曾经犯过的错:有一次想修改选项字节来关闭读保护,结果写错了配置值,芯片直接锁死。最后只能用串口ISP擦除全片才救回来。所以操作选项字节前,一定要先读出来、备份、再修改。

2.4 Flash的读/写/擦除时序与驱动代码实战

操作STM32内部Flash,说白了就三步:解锁、操作、加锁。但每一步都有坑。

2.4.1 读取操作

读取最简单,直接指针访问就行。因为NOR Flash支持随机读取,CPU可以直接寻址。

// 从Flash地址0x08010000读取32位数据
uint32_t data = *(volatile uint32_t*)0x08010000;

但要注意:如果读到了擦除后的区域(全0xFF),那返回的就是0xFFFFFFFF。这在做固件升级校验时经常用到。

2.4.2 写入操作(编程)

写入前必须确保目标扇区已被擦除。STM32的Flash写入是按字(32位)或半字(16位)进行的。写入时序如下:

  1. 检查BSY位,确保Flash空闲
  2. 解锁Flash控制寄存器(写KEY1和KEY2)
  3. 设置编程模式(PG位=1)
  4. 写入目标地址和数据
  5. 等待BSY位清零
  6. 检查编程错误
  7. 加锁Flash
void Flash_WriteWord(uint32_t addr, uint32_t data) {
    // 1. 解锁Flash
    while(FLASH->SR & FLASH_SR_BSY);  // 等待空闲
    FLASH->KEYR = 0x45670123;         // KEY1
    FLASH->KEYR = 0xCDEF89AB;         // KEY2

    // 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) {
        // 处理编程错误
    }

    // 6. 加锁
    FLASH->CR |= FLASH_CR_LOCK;
}

我的经验:写入时如果地址没对齐到4字节,会触发硬件错误。所以写入前最好检查一下地址是否4字节对齐。另外,写入过程中千万不要断电,否则数据就废了。

2.4.3 擦除操作

擦除是Flash操作里最耗时的。STM32支持扇区擦除和批量擦除。扇区擦除时序:

  1. 检查BSY位
  2. 解锁Flash
  3. 设置扇区擦除模式(SER位=1),选择扇区号
  4. 启动擦除(STRT位=1)
  5. 等待BSY位清零
  6. 检查擦除错误
  7. 加锁
void Flash_EraseSector(uint8_t sectorNum) {
    while(FLASH->SR & FLASH_SR_BSY);

    FLASH->KEYR = 0x45670123;
    FLASH->KEYR = 0xCDEF89AB;

    // 设置扇区擦除
    FLASH->CR |= FLASH_CR_SER;
    FLASH->CR &= ~FLASH_CR_SNB_Msk;          // 清扇区号
    FLASH->CR |= (sectorNum << FLASH_CR_SNB_Pos);

    FLASH->CR |= FLASH_CR_STRT;               // 启动擦除

    while(FLASH->SR & FLASH_SR_BSY);

    if(FLASH->SR & FLASH_SR_EOP) {
        FLASH->SR = FLASH_SR_EOP;             // 清除完成标志
    }

    FLASH->CR |= FLASH_CR_LOCK;
}

注意:擦除一个128KB的扇区,大约需要几百毫秒。这段时间内如果发生中断,要确保中断服务程序不访问Flash,否则会卡死。我一般会在擦除前关全局中断,擦完再开。

2.5 实战中的避坑指南

  • 不要在Flash操作中执行中断:中断向量表也在Flash里,操作期间发生中断会导致死机。要么关中断,要么把中断向量表搬到RAM。
  • 写之前一定要擦:Flash只能从1写成0,不能从0写成1。所以写之前必须擦除(全变成1)。
  • 注意扇区大小:不同型号的STM32,扇区大小不一样。F1系列是2KB一页,F4系列是16KB/128KB不等。写代码时最好用宏定义,别写死。
  • 备份关键数据:如果擦除的扇区里存了重要参数,先读到RAM里,擦完再写回去。