3、Flash映射与驱动:Flash Map API、Flash驱动注册、Flash读写擦除操作。
好,咱们接着聊Flash。上一章我们把存储系统的整体架构理了一遍,这一章就深入到最底层——Flash映射与驱动。说白了,就是Zephyr怎么认识你的Flash芯片,怎么跟它打交道。
我个人习惯把这一层叫做「地基」。地基没打好,上面盖什么楼都悬。我在项目中遇到过好几次,应用层代码写得漂漂亮亮,结果读写数据时莫名其妙丢字节,查到最后都是Flash驱动配置出了问题。嗯,咱们今天就把它彻底搞明白。
3.1 Flash Map API:给Flash画个地图
你想想看,一个嵌入式设备里可能有好几块Flash:内部Flash、外部SPI Flash、甚至QSPI Flash。每块的大小、起始地址、用途都不一样。Zephyr怎么管理它们?靠的就是Flash Map。
Flash Map说白了就是一个「地址-设备」映射表。它告诉系统:0x0000到0x1000是Bootloader,0x1000到0x2000是参数区,0x2000到0x8000是应用程序……等等。
在Zephyr里,Flash Map通过设备树来定义。举个例子:
// 设备树中的Flash分区定义
&flash0 {
partitions {
compatible = "fixed-partitions";
#address-cells = <1>;
#size-cells = <1>;
boot_partition: partition@0 {
label = "bootloader";
reg = <0x00000000 0x00010000>;
};
storage_partition: partition@10000 {
label = "storage";
reg = <0x00010000 0x00040000>;
};
app_partition: partition@50000 {
label = "application";
reg = <0x00050000 0x000B0000>;
};
};
};
这段代码定义了三块分区。每个分区都有label(名字)和reg(起始地址+大小)。Zephyr的Flash Map API就是基于这套描述来工作的。
常用的API有这几个:
| API函数 | 功能说明 |
|---|---|
flash_area_open() |
根据分区ID打开一个Flash区域 |
flash_area_close() |
关闭打开的Flash区域 |
flash_area_get_size() |
获取分区大小 |
flash_area_get_offs() |
获取分区起始偏移地址 |
flash_area_get_device() |
获取分区对应的Flash设备指针 |
使用起来也很直接:
#include <flash_map.h>
const struct flash_area *my_area;
int ret;
// 打开"storage"分区
ret = flash_area_open(FLASH_AREA_ID(storage), &my_area);
if (ret != 0) {
printk("打开分区失败: %d\n", ret);
return;
}
// 获取分区信息
off_t offset = flash_area_get_offs(my_area);
size_t size = flash_area_get_size(my_area);
const struct device *flash_dev = flash_area_get_device(my_area);
printk("分区偏移: 0x%lx, 大小: %zu\n", offset, size);
// 用完记得关闭
flash_area_close(my_area);
FLASH_AREA_ID() 宏来获取分区ID,而不是硬编码数字。这样设备树一变,代码自动适配,不容易出错。
3.2 Flash驱动注册:让系统认识你的Flash
Flash Map只是定义了「分区」,但真正干活的是Flash驱动。Zephyr的Flash驱动采用设备驱动模型(DDM),每个Flash芯片都需要注册成一个设备。
驱动注册的核心是填充一个 struct 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 (*get_page_info)(const struct device *dev, const struct flash_pages_info **info);
int (*get_erase_value)(const struct device *dev);
// ... 还有其他可选回调
};
每个Flash芯片的驱动都要实现这些回调。比如一个简单的SPI Flash驱动注册流程:
// 驱动实例
static struct flash_driver_api my_spi_flash_api = {
.read = my_spi_flash_read,
.write = my_spi_flash_write,
.erase = my_spi_flash_erase,
.get_page_info = my_spi_flash_get_page_info,
.get_erase_value = my_spi_flash_get_erase_value,
};
// 设备初始化函数
int my_spi_flash_init(const struct device *dev)
{
// 初始化SPI总线、配置GPIO等
// 读取芯片ID确认硬件正常
return 0;
}
// 注册到Zephyr系统
DEVICE_DEFINE(my_spi_flash, "spi_flash", my_spi_flash_init,
NULL, NULL, NULL, POST_KERNEL,
CONFIG_KERNEL_INIT_PRIORITY_DEVICE,
&my_spi_flash_api);
CONFIG_KERNEL_INIT_PRIORITY_DEVICE,确保它在文件系统之前就绪。
Zephyr内置了很多常见Flash芯片的驱动,比如Winbond W25Q系列、Macronix MX25系列等。你只需要在设备树里使能对应的节点,系统会自动完成注册。
3.3 Flash读写擦除操作:真刀真枪干活
好了,地图画好了,驱动注册好了,接下来就是真刀真枪地读写擦除了。这部分我多说几句,因为坑最多。
3.3.1 读操作
读操作相对简单,Flash读和RAM读差不多,没有太多限制:
const struct device *flash_dev = DEVICE_DT_GET(DT_NODELABEL(spi_flash));
uint8_t buffer[256];
// 从偏移0x1000处读取256字节
int ret = flash_read(flash_dev, 0x1000, buffer, sizeof(buffer));
if (ret != 0) {
printk("读取失败: %d\n", ret);
}
读操作一般不会失败,除非设备没初始化好或者地址越界。嗯,这里要注意:读的地址和长度不能超出Flash的地址范围。
3.3.2 写操作
写操作就有讲究了。Flash写入有几个硬性约束:
- 只能把1写成0,不能把0写成1——想改回1必须先擦除
- 写入必须按页对齐——不同芯片页大小不同,常见256字节或512字节
- 写入长度不能超过一页——跨页写入需要分多次
const struct device *flash_dev = DEVICE_DT_GET(DT_NODELABEL(spi_flash));
uint8_t data[] = {0x01, 0x02, 0x03, 0x04};
// 写入前确保目标区域已被擦除
// 写入偏移0x2000,长度4字节
int ret = flash_write(flash_dev, 0x2000, data, sizeof(data));
if (ret != 0) {
printk("写入失败: %d\n", ret);
}
3.3.3 擦除操作
擦除是把Flash的1变成0(或者反过来,取决于芯片)。擦除的最小单位是扇区(Sector),常见大小4KB、64KB不等。有些芯片还支持整片擦除。
const struct device *flash_dev = DEVICE_DT_GET(DT_NODELABEL(spi_flash));
// 擦除从0x2000开始的4KB区域
int ret = flash_erase(flash_dev, 0x2000, 4096);
if (ret != 0) {
printk("擦除失败: %d\n", ret);
}
擦除有几个注意事项:
- 擦除地址必须对齐到扇区边界——比如扇区4KB,地址必须是0x1000、0x2000这样
- 擦除大小必须是扇区大小的整数倍——不能只擦一半
- 擦除很慢——一个4KB扇区可能耗时几十到几百毫秒
3.4 综合示例:一个完整的读写擦除流程
最后,我给大家一个完整的示例,把今天讲的东西串起来:
#include <zephyr.h>
#include <device.h>
#include <drivers/flash.h>
#include <flash_map.h>
#include <string.h>
void flash_demo(void)
{
const struct flash_area *area;
int ret;
// 1. 打开分区
ret = flash_area_open(FLASH_AREA_ID(storage), &area);
if (ret != 0) {
printk("打开分区失败\n");
return;
}
const struct device *dev = flash_area_get_device(area);
off_t offset = flash_area_get_offs(area);
size_t sector_size = 4096; // 假设扇区4KB
// 2. 擦除第一个扇区
printk("擦除扇区...\n");
ret = flash_erase(dev, offset, sector_size);
if (ret != 0) {
printk("擦除失败: %d\n", ret);
goto out;
}
// 3. 写入数据
uint8_t write_buf[] = "Hello from Zephyr Flash!";
printk("写入数据...\n");
ret = flash_write(dev, offset, write_buf, sizeof(write_buf));
if (ret != 0) {
printk("写入失败: %d\n", ret);
goto out;
}
// 4. 读取验证
uint8_t read_buf[64] = {0};
printk("读取数据...\n");
ret = flash_read(dev, offset, read_buf, sizeof(write_buf));
if (ret != 0) {
printk("读取失败: %d\n", ret);
goto out;
}
printk("读取内容: %s\n", read_buf);
// 5. 比较
if (memcmp(write_buf, read_buf, sizeof(write_buf)) == 0) {
printk("读写验证通过!\n");
} else {
printk("读写验证失败!\n");
}
out:
flash_area_close(area);
}
这个例子展示了完整的流程:打开分区 → 擦除 → 写入 → 读取 → 验证。我在实际项目中,每次写完数据都会做一次回读验证,确保数据真的写进去了。Flash有时候会「假写成功」,回读才能发现。
好了,这一章的内容就到这里。Flash映射和驱动是存储系统的基石,理解透了,后面文件系统、NVS、日志系统用起来就顺手多了。下一章我们聊聊Flash分区管理的高级话题——磨损均衡和坏块管理。
公众号:蓝海资料掘金营,微信deep3321