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);
注意: 我曾经在注册驱动时忘了设置初始化优先级,结果Flash驱动在文件系统之前还没初始化好,系统一启动就崩溃。后来我养成了习惯:Flash驱动优先级一般设为 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);
}
核心要点: 写之前一定要擦!我见过太多人直接写,结果数据全是乱的。Flash不像RAM,不能原地修改。

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扇区可能耗时几十到几百毫秒
避坑指南: 我曾经在一个产品里频繁擦写同一个扇区,结果那个扇区提前报废了。Flash有擦写寿命,一般10万次左右。后来我改用磨损均衡算法,把擦写分散到多个扇区,问题就解决了。

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