第2章:Flash硬件抽象层(HAL)——驱动模型、API接口与设备树绑定
好,我们进入存储子系统的核心层——Flash硬件抽象层。说白了,这一层就是Zephyr给上层文件系统和存储管理提供的「统一遥控器」。不管底下是NOR Flash、NAND Flash还是SPI Flash,上层调用的API接口都是一样的。我当年刚接触Zephyr时,最佩服的就是这套抽象设计——它让应用代码彻底跟硬件解耦了。
2.1 Flash设备驱动模型
Zephyr的Flash驱动模型,本质上是一个基于设备驱动模型(Device Driver Model)的标准化框架。每个Flash设备都对应一个struct device实例,并通过flash_driver_api结构体挂载操作函数指针。
核心数据结构:
struct flash_driver_api {
int (*read)(const struct device *dev, off_t offset, void *data, size_t len);
int (*write)(const struct device *dev, off_t offset, const void *data, size_t len);
int (*erase)(const struct device *dev, off_t offset, size_t size);
int (*lock)(const struct device *dev, off_t offset, size_t size);
int (*unlock)(const struct device *dev, off_t offset, size_t size);
int (*read_lock_status)(const struct device *dev, off_t offset);
const struct flash_parameters *(*get_parameters)(const struct device *dev);
int (*page_layout)(const struct device *dev, const struct flash_pages_layout **layout, size_t *layout_size);
};
嗯,这里要注意:lock和unlock不是互斥锁,而是Flash的硬件保护机制。我在项目中遇到过有人把这两个接口当成线程锁来用,结果调试了一整天——千万别搞混。
驱动模型的注册流程是这样的:
- 定义设备实例:
DEVICE_DT_DEFINE()宏 - 实现
flash_driver_api中的所有回调函数 - 在初始化函数中配置硬件寄存器、设置时钟、初始化SPI或QSPI控制器
- 通过
DEVICE_DT_INST_DEFINE()绑定设备树节点
我的习惯:写Flash驱动时,我会先实现get_parameters和page_layout这两个接口。为什么?因为上层文件系统(比如LittleFS)需要先知道页大小、擦除块大小这些参数,才能做后续的布局计算。顺序搞反了,后面全是坑。
2.2 API接口详解
Zephyr的Flash API分为两类:基础操作接口和辅助查询接口。我们逐个来看。
2.2.1 基础读写擦接口
| 函数 | 功能 | 注意事项 |
|---|---|---|
flash_read() |
从指定偏移读取数据 | 对齐要求取决于硬件,一般无限制 |
flash_write() |
写入数据到Flash | 必须先擦除,且写入不能跨页 |
flash_erase() |
擦除指定区域 | 擦除大小必须是扇区/块的整数倍 |
flash_flush() |
刷新缓存(可选) | 部分控制器需要显式flush |
你想想看,flash_write()为什么要求先擦除?因为Flash存储单元的特性决定了它只能从1变成0,不能从0变成1。擦除操作就是把整个块重置成全1。我记得有一次调试,发现数据写进去读出来全是乱的,查了半天——原来是忘了先擦除。从那以后,我写任何Flash操作代码,第一行一定是检查擦除状态。
2.2.2 参数查询接口
这部分接口我个人觉得是Zephyr设计最贴心的部分:
const struct flash_parameters {
size_t write_block_size; // 写入块大小,通常等于页大小
size_t erase_value; // 擦除后的默认值,通常是0xFF
size_t capacity; // 总容量(字节)
};
还有flash_get_page_count()和flash_get_page_info_by_offs()这两个辅助函数。它们能帮你快速定位某个偏移量属于哪个页、哪个扇区。我在做OTA升级分区管理时,就靠这两个函数做地址合法性校验——省了不少代码量。
避坑指南:我曾经在STM32平台上踩过一个坑——flash_write()的len参数必须是write_block_size的整数倍。如果传入一个奇数长度,驱动会静默失败或者只写入部分数据。所以,建议每次写入前都做对齐检查:
if (len % params->write_block_size != 0) {
// 做填充或报错处理
}
2.3 设备树绑定
设备树绑定是Zephyr的「硬件描述语言」。对于Flash设备,我们需要在设备树中描述它的基地址、大小、页大小、SPI/QSPI配置等信息。
2.3.1 标准绑定属性
一个典型的NOR Flash设备树节点长这样:
&flash0 {
compatible = "jedec,spi-nor";
reg = <0x80000000 0x1000000>; // 基地址 128MB
spi-max-frequency = <104000000>;
size = <0x1000000>; // 16MB
page-size = <256>;
erase-block-size = <4096>;
jedec-id = [c2 20 19]; // 厂商ID
};
嗯,这里compatible属性决定了Zephyr用哪个驱动来匹配这个设备。比如"jedec,spi-nor"对应的是通用的SPI NOR Flash驱动。如果你用的是国产的GD25Q系列,兼容性一般没问题,但最好还是确认一下JEDEC ID。
2.3.2 分区定义
在实际项目中,我们通常会把Flash划分成多个分区:
&flash0 {
partitions {
compatible = "fixed-partitions";
#address-cells = <1>;
#size-cells = <1>;
boot_partition: partition@0 {
label = "bootloader";
reg = <0x00000000 0x00040000>; // 256KB
read-only;
};
storage_partition: partition@40000 {
label = "storage";
reg = <0x00040000 0x00100000>; // 1MB
};
scratch_partition: partition@140000 {
label = "scratch";
reg = <0x00140000 0x000C0000>; // 768KB
};
};
};
我的建议:分区大小最好对齐到擦除块大小的整数倍。否则,当你用flash_erase()擦除某个分区时,可能会误擦到相邻分区的数据。我曾经因为分区边界没对齐,导致bootloader分区被OTA升级分区擦掉了一半——板子直接变砖。从那以后,我每个分区定义都会手动算一遍对齐。
2.3.3 在代码中访问设备树信息
Zephyr提供了一组宏来从设备树中提取信息:
// 获取Flash设备实例
#define FLASH_DEVICE DT_LABEL(DT_ALIAS(flash_ctrl))
// 获取分区信息
#define STORAGE_PARTITION DT_NODELABEL(storage_partition)
#define STORAGE_OFFSET DT_REG_ADDR(STORAGE_PARTITION)
#define STORAGE_SIZE DT_REG_SIZE(STORAGE_PARTITION)
// 在代码中使用
const struct device *flash_dev = device_get_binding(FLASH_DEVICE);
flash_erase(flash_dev, STORAGE_OFFSET, STORAGE_SIZE);
你想想看,这样做的好处是什么?硬件布局变了,只需要改设备树文件,C代码一行都不用动。我在做产品迭代时,经常遇到Flash型号更换或者分区大小调整的情况——改个dts文件,重新编译,完事。这才是真正的「硬件抽象」。
2.4 实战要点总结
- 驱动开发顺序:先实现
get_parameters,再实现读写擦,最后做性能优化 - 设备树命名规范:分区label用英文小写+下划线,不要用特殊字符
- 错误处理:所有Flash API都返回负值错误码,一定要检查返回值
- 性能考量:SPI Flash的读写速度受限于SPI时钟频率,QSPI可以提升4倍
一句话总结:Flash HAL层就是Zephyr给上层存储系统准备的「万能插座」——不管底下是什么Flash,插上去就能用。但前提是,你得把设备树绑对、驱动写对、分区对齐。这三步走稳了,后面的文件系统和存储管理就顺风顺水。
下一章,我们会深入文件系统层,看看Zephyr如何把LittleFS和FCB跑在这些Flash之上。到时候你会更深刻地理解,为什么这一层的抽象如此重要。