2. Flash硬件基础:NOR Flash与NAND Flash区别、SPI Flash接口、Flash分区管理
好,咱们开始聊Flash。说实话,搞嵌入式存储,Flash是绕不开的坎儿。我最早接触Zephyr的时候,第一件事就是把Flash的脾气摸清楚。你想想看,文件系统是跑在Flash上的,如果连硬件特性都不懂,后面调试起来会非常痛苦。
2.1 NOR Flash与NAND Flash:两个性格迥异的兄弟
很多人问我,NOR和NAND到底选哪个?我的回答是:看场景。它们俩虽然都叫Flash,但底层逻辑完全不同。
2.1.1 NOR Flash:随机访问的“老兵”
NOR Flash最大的特点是什么?支持XIP(就地执行)。也就是说,代码可以直接在Flash上跑,不需要拷贝到RAM里。我在做一款工业控制器时,就靠这个特性省掉了外置RAM的成本。
它的存储单元是并联的,每个单元都能独立寻址。所以读速度很快,但写和擦除就慢得多了。擦除一个扇区,动辄几百毫秒甚至几秒。
核心特性:
- 读速度快,支持随机访问
- 写速度慢,擦除更慢
- 容量通常较小(几MB到几十MB)
- 寿命中等,约10万次擦写
2.1.2 NAND Flash:大容量的“搬运工”
NAND就不一样了。它的存储单元是串联的,像一串珠子。这种结构决定了它不能随机访问,只能按页读写,按块擦除。
我遇到过最坑的一件事:有次用NAND做日志存储,没注意它的坏块管理,结果写到一半数据全丢了。嗯,从那以后我再也不敢轻视NAND的坏块问题了。
重要提醒:NAND出厂时就可能有坏块,而且使用过程中还会产生新的坏块。必须用ECC和坏块管理机制。
| 特性 | NOR Flash | NAND Flash |
|---|---|---|
| 访问方式 | 随机访问(类似RAM) | 页访问(类似磁盘) |
| 读速度 | 快(~100ns) | 较快(~25μs/页) |
| 写速度 | 慢(~10μs/字节) | 快(~200μs/页) |
| 擦除速度 | 慢(~1s/扇区) | 快(~2ms/块) |
| 容量 | 1MB - 128MB | 128MB - 1TB+ |
| 寿命 | 10万次 | 1万 - 10万次 |
| 坏块 | 无 | 有(需管理) |
| 典型用途 | 代码存储、小数据 | 大容量数据、文件系统 |
2.2 SPI Flash接口:嵌入式世界的“通用语”
说完了Flash本身,咱们聊聊怎么跟它通信。SPI Flash,说白了就是通过SPI总线连接的Flash芯片。为什么它这么流行?因为引脚少、速度快、实现简单。
2.2.1 SPI Flash的物理层
标准的SPI Flash有4根线:
- CS(片选):谁选中谁干活
- SCLK(时钟):节奏由它定
- MOSI(主出从入):主机发数据
- MISO(主入从出):从机回数据
我个人习惯用四线SPI,但现在的Flash很多支持Quad SPI(四线模式),速度能翻四倍。我在一个需要高速日志记录的项目里,就用QSPI把吞吐量从10MB/s提到了40MB/s。
2.2.2 常见的SPI Flash命令
每个SPI Flash都有一套标准命令集。我列几个最常用的:
// 读操作
0x03 - 标准读(慢,但兼容性好)
0x0B - 快速读(带dummy周期)
0xEB - Quad输出读(四线模式)
// 写操作
0x02 - 页编程(一次最多256字节)
0x06 - 写使能(写之前必须发)
// 擦除操作
0x20 - 扇区擦除(4KB)
0xD8 - 块擦除(64KB)
0xC7 - 整片擦除(慎用!)
// 状态寄存器
0x05 - 读状态寄存器1
0x35 - 读状态寄存器2
小技巧:写操作前一定要检查状态寄存器的WIP位(忙标志)。我曾经因为没检查这个位,连续发写命令导致数据错乱。后来养成了习惯:每次写之前先读状态,等Flash不忙了再操作。
2.2.3 Zephyr中的SPI Flash驱动
在Zephyr里,操作SPI Flash很简单。它封装了底层的SPI通信,你只需要调用API就行:
#include <zephyr/drivers/flash.h>
const struct device *flash_dev = DEVICE_DT_GET(DT_ALIAS(spi_flash0));
// 读数据
flash_read(flash_dev, offset, data, len);
// 写数据(注意:必须先擦除)
flash_write(flash_dev, offset, data, len);
// 擦除扇区
flash_erase(flash_dev, offset, SECTOR_SIZE);
嗯,这里要注意:flash_write只能把1写成0,不能把0写成1。所以写之前必须先擦除,把整个扇区恢复成0xFF。
2.3 Flash分区管理:把地盘分清楚
好了,现在Flash硬件和接口都清楚了。但怎么用呢?总不能整个Flash当一个大文件系统吧?
我的做法是:分区。把Flash分成几个区域,每个区域干不同的事。
2.3.1 典型的分区方案
以一个常见的IoT设备为例:
| 分区名称 | 起始地址 | 大小 | 用途 |
|---|---|---|---|
| Bootloader | 0x000000 | 64KB | 启动代码,不可变 |
| Application | 0x010000 | 512KB | 主程序,OTA更新 |
| Settings | 0x090000 | 64KB | 配置参数,NVS存储 |
| File System | 0x0A0000 | 1MB | 日志、数据文件 |
| OTA Backup | 0x1A0000 | 512KB | 升级失败时的回滚 |
这个方案是我在一个智能网关项目里用的。你可能会问:为什么Settings分区只有64KB?因为NVS(非易失性存储)是按键值对存的,64KB足够存几百个参数了。
2.3.2 分区对齐的坑
分区时有个关键点:对齐。每个分区的起始地址和大小,必须是擦除块大小的整数倍。
我曾经犯过一个错误:把一个分区设在非对齐地址上。结果擦除时把隔壁分区的数据也擦掉了。那次调试花了我整整两天。
血的教训:分区前先查Flash的数据手册,搞清楚扇区大小和块大小。通常NOR Flash的扇区是4KB或64KB,NAND Flash的块是128KB或256KB。
2.3.3 Zephyr中的分区定义
在Zephyr里,分区通常在设备树(DTS)中定义:
/ {
flash_partitions: partitions {
compatible = "fixed-partitions";
#address-cells = <1>;
#size-cells = <1>;
boot_partition: partition@0 {
label = "bootloader";
reg = <0x000000 0x10000>; // 64KB
read-only;
};
app_partition: partition@10000 {
label = "application";
reg = <0x010000 0x80000>; // 512KB
};
settings_partition: partition@90000 {
label = "settings";
reg = <0x090000 0x10000>; // 64KB
};
fs_partition: partition@A0000 {
label = "file_system";
reg = <0x0A0000 0x100000>; // 1MB
};
};
};
定义好之后,在代码里就能直接引用:
#define FLASH_DEVICE DT_LABEL(DT_ALIAS(spi_flash0))
#define SETTINGS_OFFSET DT_REG_ADDR(DT_NODELABEL(settings_partition))
#define SETTINGS_SIZE DT_REG_SIZE(DT_NODELABEL(settings_partition))
2.3.4 分区管理的几个原则
最后,分享几个我总结的分区原则:
- 隔离原则:系统数据和用户数据分开,避免用户误操作导致系统崩溃
- 冗余原则:关键分区(如Bootloader)要有备份
- 扩展原则:预留一些空间,方便后续功能扩展
- 磨损均衡:频繁写入的分区(如日志)要放在寿命长的区域
好了,这一章就到这里。Flash硬件基础是后面所有内容的地基,一定要打好。下一章咱们聊聊Zephyr的Flash驱动架构,看看它怎么把这些硬件特性封装成统一的API。