3、Flash分区管理:分区表概念、固定分区与动态分区、分区操作API

好,咱们接着聊Flash分区管理。说实话,分区这玩意儿在嵌入式开发里,看着简单,但坑是真不少。我早期做Zephyr移植的时候,就因为分区表没规划好,导致OTA升级时把引导程序给覆盖了——嗯,那次差点被老板请去喝茶。

分区管理说白了,就是给Flash这块“大饼”切块。每块分给谁用、用多大、权限怎么定,都得提前说清楚。Zephyr在这方面做得挺灵活,支持固定分区和动态分区两种模式。咱们一个一个来看。

3.1 分区表概念

分区表,你可以把它想象成一张“房产证”。它记录了每个分区的起始地址、大小、用途和属性。Zephyr里,分区表通常定义在设备树(DTS)中,或者用C语言的结构体数组来描述。

我个人习惯用设备树来定义分区,因为这样硬件和软件能解耦。你想想看,换一块Flash芯片,只需要改DTS文件,代码基本不用动。

一个典型的分区表条目长这样:

// 设备树中的分区定义
&flash0 {
    partitions {
        compatible = "fixed-partitions";
        #address-cells = <1>;
        #size-cells = <1>;

        boot_partition: partition@0 {
            label = "mcuboot";
            reg = <0x00000000 0x00010000>;  // 起始地址0,大小64KB
            read-only;
        };

        slot0_partition: partition@10000 {
            label = "image-0";
            reg = <0x00010000 0x000F0000>;  // 起始地址0x10000,大小960KB
        };

        storage_partition: partition@100000 {
            label = "storage";
            reg = <0x00100000 0x00100000>;  // 起始地址1MB,大小1MB
        };
    };
};

这里要注意几个关键点:

  • label:分区的名字,用于在代码中引用
  • reg:起始地址和大小,单位是字节
  • read-only:标记只读分区,防止误写
⚠️ 重要提醒:分区地址必须对齐到Flash的擦除块大小。比如某款Flash的擦除块是4KB,那分区起始地址必须是0x1000的整数倍。我见过有人把分区起始设在0x1100,结果擦除时把隔壁分区也给抹了——这锅得自己背。

3.2 固定分区与动态分区

Zephyr支持两种分区模式,各有各的适用场景。

3.2.1 固定分区

固定分区,顾名思义,分区的大小和位置在编译时就定死了。运行时不能改。这种模式适合:

  • Bootloader分区(必须固定,否则系统起不来)
  • 固件升级的slot分区(A/B分区大小必须一致)
  • 关键配置存储区(比如MAC地址、序列号)

固定分区的优点是简单、可靠。缺点是不够灵活——万一某个分区空间不够了,你得重新编译整个固件。

我在项目中遇到过这么个事:客户要求增加日志存储功能,但固定分区里没预留空间。最后只能压缩固件分区,把OTA升级的slot从1MB砍到768KB。嗯,那段时间测试同事天天追着我问“为什么升级包不能超过768KB”。

3.2.2 动态分区

动态分区就灵活多了。它允许在运行时创建、删除、调整分区。Zephyr的flash_map API支持动态分区管理,底层基于Flash的擦除块进行分配。

动态分区适合:

  • 文件系统存储区(比如LittleFS,需要动态分配空间)
  • 日志缓冲区(按需分配,用完回收)
  • 用户数据存储(比如相机拍照,每张照片大小不同)

但动态分区也有代价:

  • 需要额外的元数据存储(记录分区表)
  • 存在碎片问题(频繁创建删除会导致空间浪费)
  • 可靠性不如固定分区(元数据损坏可能导致分区丢失)
💡 我的建议:关键系统分区用固定分区,用户数据区用动态分区。别把所有鸡蛋放一个篮子里。

3.3 分区操作API

Zephyr提供了一套完整的API来操作分区。核心头文件是include/storage/flash_map.h。咱们挑几个常用的说说。

3.3.1 获取分区信息

#include <storage/flash_map.h>

// 通过分区标签获取分区信息
const struct flash_area *fa;
int ret = flash_area_open(FLASH_AREA_ID(storage), &fa);
if (ret != 0) {
    printk("打开分区失败: %d\n", ret);
    return;
}

// 获取分区信息
off_t offset = fa->fa_off;   // 分区在Flash中的偏移地址
size_t size  = fa->fa_size;  // 分区大小
uint8_t id   = fa->fa_id;    // 分区ID

printk("分区偏移: 0x%x, 大小: %d字节\n", offset, size);

// 用完记得关闭
flash_area_close(fa);

这里有个细节:FLASH_AREA_ID()宏会根据分区标签自动生成ID。你不需要手动管理ID,Zephyr帮你做了。

3.3.2 读写操作

// 读取分区数据
uint8_t buf[256];
ret = flash_area_read(fa, 0, buf, sizeof(buf));
if (ret != 0) {
    printk("读取失败: %d\n", ret);
}

// 写入数据(注意:需要先擦除)
ret = flash_area_erase(fa, 0, sizeof(buf));
if (ret != 0) {
    printk("擦除失败: %d\n", ret);
}

ret = flash_area_write(fa, 0, buf, sizeof(buf));
if (ret != 0) {
    printk("写入失败: %d\n", ret);
}
⚠️ 曾经踩过的坑:Flash写入前必须擦除,而且擦除是按块来的。如果你只写16字节,也得擦除整个4KB块。我曾经没注意这个,写之前没擦除,结果数据全乱套了。后来养成了习惯:写之前先擦,擦完再写。

3.3.3 动态分区管理

如果你用动态分区,需要先初始化分区表:

// 动态分区表定义
static const struct flash_area dyn_partitions[] = {
    {
        .fa_id = 10,
        .fa_off = 0x200000,   // 从2MB地址开始
        .fa_size = 0x100000,  // 1MB空间
        .fa_dev_id = 0,       // Flash设备ID
    },
};

// 初始化动态分区
int ret = flash_area_layout_init(dyn_partitions, 
                                  ARRAY_SIZE(dyn_partitions));
if (ret != 0) {
    printk("动态分区初始化失败\n");
}

动态分区创建后,用法和固定分区一样。区别在于你可以通过flash_area_create()flash_area_delete()来动态管理。

3.3.4 常用API速查表

API函数 功能说明 注意事项
flash_area_open() 打开分区,获取操作句柄 必须配对调用close()
flash_area_close() 关闭分区 释放资源
flash_area_read() 从分区读取数据 支持任意偏移和长度
flash_area_write() 写入数据到分区 必须先擦除
flash_area_erase() 擦除分区指定区域 按块对齐
flash_area_get_sector() 获取分区内扇区信息 用于计算擦除块大小
flash_area_layout_init() 初始化动态分区表 仅动态分区需要

3.4 实战经验总结

最后,分享几个我在项目中积累的经验:

  1. 分区大小预留余量:别算得太死。比如你预估日志需要100KB,那就给128KB。多出来的28KB,关键时刻能救命。
  2. 分区顺序有讲究:Bootloader放最前面,然后是固件分区,最后是数据分区。这样即使数据分区写坏了,系统还能启动。
  3. 善用只读属性:Bootloader和关键配置分区标记为read-only,防止意外写入。我见过有人调试时写错了地址,把Bootloader覆盖了——嗯,那滋味不好受。
  4. 分区表备份:对于动态分区,建议在Flash末尾备份一份分区表。万一主分区表损坏,还能从备份恢复。

核心要点:

  • 分区表是Flash管理的基石,务必规划好
  • 固定分区用于系统关键区域,动态分区用于用户数据
  • 操作API遵循“打开-操作-关闭”模式
  • 写前必擦除,擦除必对齐

好了,分区管理这块就聊到这儿。下一节咱们讲文件系统挂载,到时候会用到今天学的分区API。你先把这些基础打牢,后面才能玩得转。