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分为两大块:主存储块和信息块。
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位)进行的。写入时序如下:
- 检查BSY位,确保Flash空闲
- 解锁Flash控制寄存器(写KEY1和KEY2)
- 设置编程模式(PG位=1)
- 写入目标地址和数据
- 等待BSY位清零
- 检查编程错误
- 加锁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支持扇区擦除和批量擦除。扇区擦除时序:
- 检查BSY位
- 解锁Flash
- 设置扇区擦除模式(SER位=1),选择扇区号
- 启动擦除(STRT位=1)
- 等待BSY位清零
- 检查擦除错误
- 加锁
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里,擦完再写回去。