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:标记只读分区,防止误写
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);
}
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 实战经验总结
最后,分享几个我在项目中积累的经验:
- 分区大小预留余量:别算得太死。比如你预估日志需要100KB,那就给128KB。多出来的28KB,关键时刻能救命。
- 分区顺序有讲究:Bootloader放最前面,然后是固件分区,最后是数据分区。这样即使数据分区写坏了,系统还能启动。
- 善用只读属性:Bootloader和关键配置分区标记为read-only,防止意外写入。我见过有人调试时写错了地址,把Bootloader覆盖了——嗯,那滋味不好受。
- 分区表备份:对于动态分区,建议在Flash末尾备份一份分区表。万一主分区表损坏,还能从备份恢复。
核心要点:
- 分区表是Flash管理的基石,务必规划好
- 固定分区用于系统关键区域,动态分区用于用户数据
- 操作API遵循“打开-操作-关闭”模式
- 写前必擦除,擦除必对齐
好了,分区管理这块就聊到这儿。下一节咱们讲文件系统挂载,到时候会用到今天学的分区API。你先把这些基础打牢,后面才能玩得转。