5.17 FAL Flash抽象层
5.17 FAL Flash抽象层
📚 本节导读
学习时长: 约 35 分钟
难度级别: ⭐⭐⭐☆☆
前置知识: C 语言基础、Flash 存储器基础、5.1 设备驱动框架
🎯 学习目标
- 理解 FAL(Flash Abstraction Layer)的架构设计
- 掌握 Flash 设备的注册与查找
- 掌握 FAL 分区表的配置与使用
- 掌握分区的读写擦除操作
- 理解 MTD 设备与 Block 设备的创建机制
一、概述
FAL(Flash Abstraction Layer)是 OneOS 的 Flash 抽象层,为上层应用(如文件系统、KV 数据库、OTA 升级等)提供统一的 Flash 访问接口。FAL 屏蔽了不同 Flash 硬件的差异(SPI Flash、片上 Flash、eMMC 等),通过分区表将物理 Flash 划分为逻辑分区,每个分区可独立管理。
FAL 位于 drivers/fal/ 目录下,由以下模块组成:
| 模块 | 文件 | 说明 |
|---|---|---|
| Flash 设备抽象 | fal.h / fal.c | Flash 设备注册、查找,定义 fal_flash_t 和 fal_flash_ops |
| 分区管理 | fal_part.h / fal_part.c | 分区表解析、分区读写擦除操作 |
| MTD 设备 | fal_mtd.c | 将 FAL 分区封装为 MTD 设备 |
| Block 设备 | fal_block.c | 将 FAL 分区封装为 Block 设备 |
| RAM 模拟 | fal_ram.c | 基于 RAM 的 Flash 模拟实现(用于测试) |
FAL 架构图:
┌──────────┬──────────┬──────────┬──────────┐
│ FileSys │ KV DB │ OTA │ Bootloader│ ← 上层应用
└────┬─────┴────┬─────┴────┬─────┴────┬─────┘
│ │ │ │
┌────▼──────────▼──────────▼──────────▼────┐
│ MTD Device / Block Device │ ← 设备抽象
└────────────────────┬────────────────────┘
│
┌────────────────────▼────────────────────┐
│ FAL Partition Table │ ← 分区表
└────────────────────┬────────────────────┘
│
┌────────────────────▼────────────────────┐
│ FAL Flash Device Layer │ ← Flash 抽象层
└────┬──────────┬──────────┬──────────────┘
│ │ │
┌────▼────┐ ┌──▼───┐ ┌───▼──────┐
│SPI Flash│ │OnChip│ │RAM Flash │ ← 物理 Flash
└─────────┘ └──────┘ └──────────┘1.1 使用场景
一句话核心:FAL 是"Flash 抽象层 + 分区管理"——屏蔽不同 Flash 硬件差异(片上/SPI/RAM 模拟),用一张静态分区表把物理 Flash 切成多个逻辑分区,再对外提供统一的读写擦接口,并可封装成 MTD/Block 设备供文件系统等使用。
为什么需要 FAL(对比直接操作 Flash):
| 维度 | 用 FAL | 应用直接操作 Flash |
|---|---|---|
| 硬件差异 | 统一抽象,换 Flash 只换适配层 | 每处应用都绑死硬件 API |
| 分区管理 | 静态分区表,多分区独立 | 需手动维护偏移 |
| 上层复用 | 文件系统/KV/OTA 直接用 | 各应用自己封装 |
| 移植成本 | 低 | 高 |
典型应用场景:
| 场景 | 谁在用 | 实际工作情况 |
|---|---|---|
| 文件系统(LFS/FSL) | fal_mtd_device_create/fal_blk_device_create | 把 filesys 分区封装成 MTD/Block 设备挂给文件系统,文件系统按块读写 |
| KV 数据库 | fal_part_find + 直接读写 | 在 kvdb 分区做键值存储,写前擦除、按页写入 |
| OTA 升级 | download 分区 | Bootloader 和 App 通过分区约定升级镜像存放位置 |
| 掉电参数保存 | 专用分区 | 参数写进 Flash 分区,重启后读回 |
选型边界表:
| 场景 | 该用什么 | 为什么 |
|---|---|---|
| 只有一块小 Flash 且无需分区 | 直接寄存器操作 | FAL 引入分区表和抽象层,简单场景过重 |
| 需要文件系统 | FAL(MTD/Block)+ 文件系统 | 文件系统通常需要块设备接口,FAL 提供 |
| 纯粹内存存储 | 直接用 RAM/数组 | 不需要 Flash 语义 |
结合源码的使用注意点:
- 写前必须擦除:
fal_part_write不会自动擦除,未擦除的区域写进去是 OR 操作,结果不对(NOR 特性); - 分区表在
fal_cfg.h:fal_part_info[]是编译期静态数组,改分区需改配置重新编译; - 擦除须块对齐:
fal_part_erase要求 offset 和 size 是block_size的整数倍; - Flash 必须先注册:
fal_part_init遍历分区表时fal_flash_find找不到 Flash 会返回 -1。
二、核心数据结构
2.1 Flash 设备结构体 fal_flash_t
来源:drivers/fal/fal.h
struct fal_flash
{
char name[FAL_DEV_NAME_MAX];
uint32_t page_size; /* flash memory page (without spare area) size measured in bytes */
uint32_t block_size; /* flash memory block size measured in number of pages */
uint32_t capacity; /* flash capacity measured in bytes */
struct fal_flash_ops ops;
os_list_node_t list;
void *priv;
};字段详解(源码注释为英文原版,下表补充说明,不属于源码):
| 字段 | 说明 |
|---|---|
name | Flash 设备名称,如 "onchip_flash"、"spi_flash"(最长 FAL_DEV_NAME_MAX=16) |
page_size | 页大小(字节),Flash 的最小写入/读取单位 |
block_size | 块大小(以页数计),最小擦除单位 = block_size * page_size 字节 |
capacity | Flash 总容量(字节) |
ops | 操作函数表(read_page/write_page/erase_block,见 2.2) |
list | 设备链表节点,挂到 FAL 全局设备链表 fal_flash_table |
priv | HAL 层私有数据(如 SPI 设备句柄、片上 Flash 信息指针) |
关键理解 —— 单位差异:
page_size单位是字节,而block_size单位是页数。所以"一个块的字节数 =block_size * page_size",擦除以块为单位时必须换算。fal_page_shift/fal_block_shift宏用os_ffs(x) - 1算出 2 的幂次移位量,用于地址/页号换算(见 6.2 的page_addr = offset >> fal_page_shift(flash_dev))。
2.2 Flash 操作函数表 struct fal_flash_ops
来源:drivers/fal/fal.h
struct fal_flash_ops
{
int (*read_page)(fal_flash_t *flash, uint32_t page_addr, uint8_t *buff, uint32_t page_nr);
int (*write_page)(fal_flash_t *flash, uint32_t page_addr, const uint8_t *buff, uint32_t page_nr);
int (*erase_block)(fal_flash_t *flash, uint32_t page_addr, uint32_t page_nr);
};函数指针详解(源码无注释,下表补充说明,不属于源码):
| 函数指针 | 参数 | 说明 |
|---|---|---|
read_page | page_addr 起始页号、buff 目标、page_nr 页数 | 从 page_addr 起始读 page_nr 页到 buff |
write_page | page_addr 起始页号、buff 源、page_nr 页数 | 把 buff 中 page_nr 页写入 page_addr 起始页 |
erase_block | page_addr 起始页号、page_nr 页数 | 从 page_addr 起始擦除 page_nr 页(须以块对齐) |
关键理解:三个回调的
page_addr都是页号(第几页),不是字节地址。厂商适配时由page_addr * page_size换算成真实偏移(或直接当页号用,取决于硬件)。
2.3 片上 Flash 信息 struct onchip_flash_info
来源:drivers/fal/fal.h
struct onchip_flash_info
{
uint32_t start_addr;
uint32_t capacity;
uint32_t block_size;
uint32_t page_size;
};字段详解(源码无注释,下表补充说明,不属于源码):
| 字段 | 说明 |
|---|---|
start_addr | Flash 起始地址(片上 Flash 的基址) |
capacity | 容量(字节) |
block_size | 块大小(字节) |
page_size | 页大小(字节) |
说明:
onchip_flash_info是片上 Flash 专用的私有数据结构,通常作为fal_flash.priv传入 HAL 适配层的read_page/write_page/erase_block,供换算真实寄存器地址(见第八章适配)。SPI Flash 等则用其它私有结构。
2.4 分区信息 fal_part_info_t
来源:drivers/fal/fal_part.h
typedef struct fal_part_info
{
char name[FAL_DEV_NAME_MAX];
char flash_name[FAL_DEV_NAME_MAX];
uint32_t offset;
uint32_t size;
uint32_t flags;
} fal_part_info_t;字段详解(源码无注释,下表补充说明,不属于源码):
| 字段 | 说明 |
|---|---|
name | 分区名称,如 "boot"、"app"、"kvdb"(最长 16) |
flash_name | 所属 Flash 设备名称(须与 fal_flash_register 注册的名字一致) |
offset | 分区在 Flash 中的起始偏移(字节) |
size | 分区大小(字节) |
flags | 标志位(锁定/解锁) |
分区标志(drivers/fal/fal_part.h,与项目逐字一致):
#define FAL_PART_INFO_FLAGS_LOCKED (1 << 0)
#define FAL_PART_INFO_FLAGS_UNLOCKED (0 << 0)| 宏 | 值 | 说明 |
|---|---|---|
FAL_PART_INFO_FLAGS_LOCKED | (1 << 0) | 分区已锁定 |
FAL_PART_INFO_FLAGS_UNLOCKED | (0 << 0) | 分区未锁定 |
详细讲解:分区表如何从"静态配置"变成"运行时结构"
核心流程一句话:fal_cfg.h 里用静态数组 fal_part_info[] 声明每个分区的"规划"(叫 fal_part_info_t,编译期确定);系统启动时 fal_part_init() 把这个静态数组逐条翻译成运行时结构 fal_part_t(动态分配、绑定 Flash 设备指针、拷贝 flags)。配置信息(info)不拷贝、只指向静态数组;可变的运行时状态(flash 指针、flags)单独存放。
① 静态配置:fal_cfg.h 里的 fal_part_info[]
fal_part_info_t 是编译期静态数组,定义在项目 fal_cfg.c(被 fal_part.c 用 #include <fal_cfg.c> 引入):
/* fal_cfg.c 中 */
static const struct fal_part_info fal_part_info[] = {
{ "boot", "onchip_flash", 0, 256 * 1024, FAL_PART_INFO_FLAGS_UNLOCKED },
{ "app", "onchip_flash", 256 * 1024, 256 * 1024, FAL_PART_INFO_FLAGS_UNLOCKED },
{ "kvdb", "onchip_flash", 512 * 1024, 128 * 1024, FAL_PART_INFO_FLAGS_LOCKED },
{ "filesys", "spi_flash", 0, 1024 * 1024, FAL_PART_INFO_FLAGS_UNLOCKED },
};| 数组元素 | name | flash_name | offset | size | flags |
|---|---|---|---|---|---|
| [0] | boot | onchip_flash | 0 | 256KB | 未锁定 |
| [1] | app | onchip_flash | 256KB | 256KB | 未锁定 |
| [2] | kvdb | onchip_flash | 512KB | 128KB | 锁定 |
| [3] | filesys | spi_flash | 0 | 1MB | 未锁定 |
这个数组在编译时就确定,
ARRAY_SIZE(fal_part_info)得到分区数量fal_part_table_size。它描述的是"每个分区想怎么规划",还不涉及任何动态内存。
② 运行时翻译:fal_part_init()
来源:drivers/fal/fal_part.c,与项目逐字一致:
int fal_part_init(void)
{
int i;
fal_flash_t *flash_dev = NULL;
fal_part_table = os_calloc(fal_part_table_size, sizeof(fal_part_t));
OS_ASSERT(fal_part_table);
for (i = 0; i < fal_part_table_size; i++)
{
flash_dev = fal_flash_find(fal_part_info[i].flash_name);
if (flash_dev == NULL)
{
LOG_E(DBG_TAG, "Initialize failed! Didn't find the flash device(%s).\r\n", fal_part_info[i].flash_name);
os_free(fal_part_table);
fal_part_table = OS_NULL;
return -1;
}
if (fal_part_info[i].offset >= (long)flash_dev->capacity)
{
LOG_E(DBG_TAG,
"Initialize failed! Partition(%s) offset address(%ld) out of flash bound(<%d).\r\n",
fal_part_info[i].name,
fal_part_info[i].offset,
flash_dev->capacity);
os_free(fal_part_table);
fal_part_table = OS_NULL;
return -1;
}
fal_part_table[i].info = (fal_part_info_t *)&fal_part_info[i];
fal_part_table[i].flash = flash_dev;
fal_part_table[i].flags = fal_part_info[i].flags;
}
fal_show_part_table();
return 0;
}分步详解:
第 1 步:为运行时分区表分配内存(无条件执行)
fal_part_table = os_calloc(fal_part_table_size, sizeof(fal_part_t));
OS_ASSERT(fal_part_table);详细解释:按分区数 fal_part_table_size 动态分配运行时 fal_part_t 数组,OS_ASSERT 保证分配成功。注意:分配的是 fal_part_t(运行时结构),不是 fal_part_info_t(静态结构)——info 数组在编译期已存在,这里只是给每个分区准备一个"运行时壳"。
第 2 步:逐条处理每个静态分区项(条件 Flash 不存在 / offset 越界)
for (i = 0; i < fal_part_table_size; i++)
{
flash_dev = fal_flash_find(fal_part_info[i].flash_name);
if (flash_dev == NULL)
{
LOG_E(DBG_TAG, "Initialize failed! Didn't find the flash device(%s).\r\n", fal_part_info[i].flash_name);
os_free(fal_part_table);
fal_part_table = OS_NULL;
return -1;
}
if (fal_part_info[i].offset >= (long)flash_dev->capacity)
{
LOG_E(DBG_TAG,
"Initialize failed! Partition(%s) offset address(%ld) out of flash bound(<%d).\r\n",
fal_part_info[i].name,
fal_part_info[i].offset,
flash_dev->capacity);
os_free(fal_part_table);
fal_part_table = OS_NULL;
return -1;
}详细解释:对每个分区项做两道校验,任一失败则释放已分配内存并返回 -1:
- Flash 必须已注册:
fal_flash_find(fal_part_info[i].flash_name)在 FAL 设备链表里按名字找 Flash,找不到说明该 Flash 还没fal_flash_register(初始化顺序错误); - offset 不能越界:分区偏移必须小于所属 Flash 的
capacity(offset >= capacity视为越界)。
第 3 步:填充运行时结构(无条件执行)
fal_part_table[i].info = (fal_part_info_t *)&fal_part_info[i];
fal_part_table[i].flash = flash_dev;
fal_part_table[i].flags = fal_part_info[i].flags;
}
fal_show_part_table();
return 0;
}详细解释:对每个分区填三件事:
info:指针指向静态数组元素&fal_part_info[i]——不拷贝配置数据,运行时直接引用编译期数组,省内存、避免重复;flash:绑定找到的 Flash 设备指针(fal_flash_find的结果),后续读写擦都通过它分发到具体 Flash;flags:拷贝静态flags值——这是分区当前的可变状态,运行时可能被fal_part_lock/unlock修改(见下)。
③ 静态 → 运行时对应关系表
静态 fal_part_info_t | 运行时 fal_part_t | 处理方式 |
|---|---|---|
name | info->name(经 info 指针) | 引用(不拷贝) |
flash_name | fal_part_find 后存 flash | 由名字查找并绑定设备指针 |
offset | info->offset(经 info 指针) | 引用(不拷贝) |
size | info->size(经 info 指针) | 引用(不拷贝) |
flags | flags | 拷贝(运行时可变,可锁/解锁) |
④ 锁定机制:flags 怎么拦下写/擦
flags 从静态拷贝到 fal_part_t.flags 后,写/擦前会检查。以 fal_part_write 为例(drivers/fal/fal_part.c,与项目逐字一致):
if (part->flags & FAL_PART_INFO_FLAGS_LOCKED)
{
LOG_E(DBG_TAG, "part write [%s] locked.\r\n", part->info->name);
return -2;
}详细解释:FAL_PART_INFO_FLAGS_LOCKED = (1 << 0),用按位与 part->flags & (1<<0) 判断 bit0 是否为 1。锁定则打印日志并返回 -2(拒绝写)。fal_part_erase 也有同样的检查。运行时可用 fal_part_lock(part)/fal_part_unlock(part) 切换该位。
锁定检查数值演算:
part->flags | & (1<<0) | 结果 |
|---|---|---|
0(未锁定) | 0 & 1 = 0 | 不成立,允许写/擦 |
1(锁定) | 1 & 1 = 1 | 成立,拒绝写/擦并返回 -2 |
2(假设其他标志) | 2 & 1 = 0 | 不成立,允许写/擦(bit0 为 0) |
一句话总结:
fal_part_info_t是编译期"图纸",fal_part_t是运行时"房屋"——fal_part_init按图纸逐条建屋(分配内存、绑定 Flash、拷贝 flags),校验失败则整批作废;之后应用拿到的都是fal_part_t指针(fal_part_find按名字查),读写擦前用flags判断分区是否被锁定。
配置指南:分区表需要自己配置吗?怎么配置?
是的,必须自己配置。FAL 是通用框架,不知道你的 Flash 想怎么划分——分区方案完全取决于业务需求(bootloader/App/文件系统各占多大、放哪)。分区表定义在板级(不是框架层),由你按 Flash 布局规划。下面结合真实项目的 fal_cfg.c 说明。
① 配置入口(两种等价写法)
写法一:直接在 board/ports/fal_cfg.c 定义静态数组(如 STM32 项目):
static const fal_part_info_t fal_part_info[] = {
/* part, flash, addr, size, lock */
{"bootloader", "onchip_flash", 0x00000000, 0x00018000, FAL_PART_INFO_FLAGS_LOCKED},
{ "cfg", "onchip_flash", 0x00018000, 0x00008000, FAL_PART_INFO_FLAGS_LOCKED},
{ "app", "onchip_flash", 0x00020000, 0x00048000, FAL_PART_INFO_FLAGS_UNLOCKED},
{ "download", "onchip_flash", 0x00068000, 0x00018000, FAL_PART_INFO_FLAGS_UNLOCKED},
};写法二:在 fal_cfg.h 用 FAL_PART_TABLE 宏(部分板子用),最终同样生成 fal_part_info[]:
#define FAL_PART_TABLE \
{ \
{OS_BL_PART_NAME, OS_ONCHIP_FLASH_NAME, (MCU_BOOT_ENTRY - MCU_ROM_BEGIN), MCU_BOOT_SIZE, FAL_PART_INFO_FLAGS_LOCK}, \
{ OS_APP_NAME, OS_ONCHIP_FLASH_NAME, (OS_APP_PART_ADDR - MCU_ROM_BEGIN), OS_APP_PART_SIZE, 0}, \
}② 5 个字段怎么填
| 字段 | 填写内容 | 例子 |
|---|---|---|
name | 分区名(fal_part_find 用它找) | "bootloader"/"app"/"filesystem" |
flash_name | 所属 Flash 设备的注册名 | "onchip_flash"(片上)、"nor_flash"(外挂 SPI) |
offset | 分区在 Flash 里的起始偏移(字节) | bootloader 从 0x00000000 起 |
size | 分区大小(字节) | 0x18000 = 96KB |
flags | 锁定/解锁 | bootloader/cfg 用 LOCKED,app/download 用 UNLOCKED |
③ 5 条硬约束(配错会初始化失败)
| 约束 | 违反后果 |
|---|---|
flash_name 必须等于 fal_flash_register 注册名 | fal_part_init 里 fal_flash_find 找不到 Flash,返回 -1 |
offset < flash->capacity(不越界) | fal_part_init 报 "out of flash bound" 返回 -1 |
offset/size 按 block_size 块对齐 | fal_part_erase 对齐检查失败返回 -1 |
| 分区不重叠 | 由你自己规划,框架不检查 |
name 唯一 | fal_part_find 找到错误分区 |
④ 完整配置流程(配具体例子)
流程 4 步:
1. 注册 Flash 设备 fal_flash_register(&onchip_flash_dev) // 名字 "onchip_flash"
2. 在 fal_cfg.c 配分区表 fal_part_info[] = { {"app","onchip_flash",0x20000,0x48000,UNLOCKED}, ... }
3. 初始化分区表 fal_part_init() // 遍历 fal_part_info[] 建 fal_part_t
4. 用分区 fal_part_t *p = fal_part_find("app");
fal_part_read(p, 0, buf, len); fal_part_write(p, 0, buf, len);具体可运行例子(多设备)——假设板上有两块 Flash:片上 Flash onchip_flash(page_size=4096、block_size=4 页=16KB/块、容量 1MB)和外挂 SPI Flash spi_flash(page_size=256、block_size=64 页=16KB/块、容量 8MB)。分区表在 fal_cfg.c 配好两个设备上的分区:
static const fal_part_info_t fal_part_info[] = {
/* part, flash, addr, size, lock */
{"bootloader", "onchip_flash", 0x00000000, 0x00018000, FAL_PART_INFO_FLAGS_LOCKED},
{ "app", "onchip_flash", 0x00020000, 0x00048000, FAL_PART_INFO_FLAGS_UNLOCKED},
{ "filesystem", "spi_flash" , 0x00000000, 0x00200000, FAL_PART_INFO_FLAGS_UNLOCKED},
};即 app 在片上 Flash(offset 128KB),filesystem 在 SPI Flash(offset 0,大小 2MB)。下面演示注册两块 Flash → 初始化分区 → 分别操作两个设备上的分区:
#include <oneos_config.h>
#include <fal.h>
#include <string.h>
/* 板上两块 Flash 设备(HAL 层填充好 name/geometry/ops) */
extern fal_flash_t onchip_flash_dev;
extern fal_flash_t spi_flash_dev;
void fal_multi_device_example(void)
{
fal_flash_t *flash;
fal_part_t *app_part;
fal_part_t *fs_part;
int ret;
uint8_t wbuf[] = "hello fal";
uint8_t rbuf[64];
int len;
/* 第 1 步:注册两块 Flash 设备(名字必须与分区表 flash_name 一致) */
ret = fal_flash_register(&onchip_flash_dev);
if (ret < 0)
{
os_kprintf("register onchip_flash failed, ret=%d\r\n", ret);
return;
}
ret = fal_flash_register(&spi_flash_dev);
if (ret < 0)
{
os_kprintf("register spi_flash failed, ret=%d\r\n", ret);
return;
}
/* 第 2 步:分区表在 fal_cfg.c 已静态配置,通常无需在此写 */
/* 第 3 步:初始化分区表(一般由启动代码自动调用,这里显式演示) */
ret = fal_part_init();
if (ret < 0)
{
os_kprintf("part init failed, ret=%d\r\n", ret);
return;
}
/* 第 4 步:按名字找到两个分区(分别位于不同 Flash) */
app_part = fal_part_find("app");
fs_part = fal_part_find("filesystem");
if (app_part == OS_NULL || fs_part == OS_NULL)
{
os_kprintf("find part failed\r\n");
return;
}
/* 4a. 擦除并写 app 分区(片上 Flash;offset/size 块对齐:0 和 16KB) */
ret = fal_part_erase(app_part, 0, 16 * 1024);
if (ret < 0)
{
os_kprintf("erase app failed, ret=%d\r\n", ret);
return;
}
ret = fal_part_write(app_part, 0, wbuf, strlen((char *)wbuf));
if (ret != strlen((char *)wbuf))
{
os_kprintf("write app failed, ret=%d\r\n", ret);
return;
}
/* 4b. 擦除并写 filesystem 分区(SPI Flash;0 和 16KB 也是其块对齐) */
ret = fal_part_erase(fs_part, 0, 16 * 1024);
if (ret < 0)
{
os_kprintf("erase filesystem failed, ret=%d\r\n", ret);
return;
}
ret = fal_part_write(fs_part, 0, wbuf, strlen((char *)wbuf));
if (ret != strlen((char *)wbuf))
{
os_kprintf("write filesystem failed, ret=%d\r\n", ret);
return;
}
/* 5. 分别回读验证 */
len = fal_part_read(app_part, 0, rbuf, sizeof(rbuf));
if (len > 0)
{
rbuf[len] = '\0';
os_kprintf("app read back: %s (len=%d)\r\n", rbuf, len);
}
len = fal_part_read(fs_part, 0, rbuf, sizeof(rbuf));
if (len > 0)
{
rbuf[len] = '\0';
os_kprintf("filesystem read back: %s (len=%d)\r\n", rbuf, len);
}
}预期执行效果(两块 Flash 各自承载自己的分区):
| 步骤 | 调用 | 命中 Flash/分区 | 实际效果 |
|---|---|---|---|
| 1 | fal_flash_register(&onchip_flash_dev) | onchip_flash | 片上 Flash 挂进 FAL 设备链表 |
| 1 | fal_flash_register(&spi_flash_dev) | spi_flash | SPI Flash 挂进 FAL 设备链表 |
| 3 | fal_part_init | 遍历 fal_part_info[] | 生成 fal_part_t,app 绑定 onchip_flash、filesystem 绑定 spi_flash |
| 4a | fal_part_find("app") | onchip_flash 上 app | 返回 app 分区(片上) |
| 4a | fal_part_erase/write(app, 0, ...) | app → 片上偏移 0x20000 | 擦/写片上 Flash 的 app 区 |
| 4b | fal_part_find("filesystem") | spi_flash 上 filesystem | 返回 filesystem 分区(SPI) |
| 4b | fal_part_erase/write(fs, 0, ...) | filesystem → SPI 偏移 0x00000 | 擦/写 SPI Flash 的 filesystem 区 |
| 5 | fal_part_read 两次 | 各自分区 | 两段都回读 hello fal |
执行输出:
app read back: hello fal (len=10)
filesystem read back: hello fal (len=10)要点:
fal_part_find("app")和fal_part_find("filesystem")虽然名字不同,但它们分别属于不同 Flash 设备——框架通过每个分区的fal_part_t.flash指针把读写擦分发到正确的 Flash。一块 Flash 可以有多个分区(如片上 Flash 上 boot/app),多块 Flash 也能各自承载分区(如 filesystem 在 SPI),只要flash_name与fal_flash_register注册名一一对应即可。写前必须先fal_part_erase——fal_part_write不会自动擦除,NOR Flash 未擦除区域写进去是"按位与"结果,会得到错误数据。第 3 步fal_part_init通常由框架/启动代码自动调用,你只需把第 1、2 步配置对。第 2 步就是你自己的"Flash 地图规划"——改分区大小/位置要重新编译,并确保 bootloader 与 App 对分区布局的理解一致(尤其是 OTA 升级时)。
2.5 分区运行时结构 fal_part_t
typedef struct fal_part
{
fal_part_info_t *info; /* 指向分区配置信息 */
fal_flash_t *flash; /* 指向所属 Flash 设备 */
uint32_t flags; /* 运行时标志 */
} fal_part_t;辅助宏:
#define fal_part_size(part) (part->info->size)
#define fal_part_block_size(part) (part->flash->block_size)
#define fal_part_page_size(part) (part->flash->page_size)三、关键 API
3.1 Flash 设备管理
/* 注册 Flash 设备 */
int fal_flash_register(fal_flash_t *flash);
/* 查找 Flash 设备(按名称) */
fal_flash_t *fal_flash_find(const char *name);3.2 分区操作
/* 查找分区 */
fal_part_t *fal_part_find(const char *name);
/* 读取分区数据 */
int fal_part_read(fal_part_t *part, uint32_t addr, uint8_t *buf, size_t size);
/* 写入分区数据 */
int fal_part_write(fal_part_t *part, uint32_t addr, const uint8_t *buf, size_t size);
/* 擦除分区区域 */
int fal_part_erase(fal_part_t *part, uint32_t addr, size_t size);
/* 擦除整个分区 */
int fal_part_erase_all(fal_part_t *part);
/* 锁定/解锁分区 */
int fal_part_lock(fal_part_t *part);
int fal_part_unlock(fal_part_t *part);3.3 MTD/Block 设备创建
/* 从分区创建 MTD 设备 */
os_mtd_device_t *fal_mtd_device_create(const char *parition_name);
/* 从分区创建 Block 设备 */
os_blk_device_t *fal_blk_device_create(const char *parition_name);四、分区表配置
FAL 分区表在 fal_cfg.h 中定义,通过 fal_part_info 数组描述:
/* fal_cfg.h 示例 */
#include <fal_part.h>
/* 定义 Flash 设备 */
extern struct fal_flash onchip_flash_dev;
extern struct fal_flash spi_flash_dev;
/* 分区表定义 */
static const fal_part_info_t fal_part_info[] =
{
/* name, flash_name, offset, size, flags */
/* 片上 Flash 分区 */
{ "boot", "onchip_flash", 0x00000000, 64*1024, 0 },
{ "app", "onchip_flash", 0x00010000, 256*1024, 0 },
{ "download", "onchip_flash", 0x00050000, 128*1024, 0 },
{ "kvdb", "onchip_flash", 0x00070000, 64*1024, 0 },
/* SPI Flash 分区 */
{ "filesys", "spi_flash", 0x00000000, 2*1024*1024, 0 },
{ "easyflash","spi_flash", 0x00200000, 512*1024, 0 },
{ "ota", "spi_flash", 0x00280000, 512*1024, 0 },
};五、使用示例
5.1 注册 Flash 设备
#include <fal.h>
/* 片上 Flash 设备 */
static struct fal_flash onchip_flash_dev =
{
.name = "onchip_flash",
.page_size = 256, /* 256 字节/页 */
.block_size = 16, /* 16 页/块 = 4KB */
.capacity = 512 * 1024, /* 512KB */
.ops =
{
.read_page = onchip_flash_read,
.write_page = onchip_flash_write,
.erase_block = onchip_flash_erase,
},
};
void fal_init(void)
{
int ret;
/* 注册 Flash 设备,检查返回值 */
ret = fal_flash_register(&onchip_flash_dev);
if (ret < 0)
{
os_kprintf("register onchip_flash failed, ret=%d\r\n", ret);
return;
}
}5.2 分区读写操作
#include <fal.h>
#include <fal_part.h>
void fal_partition_example(void)
{
fal_part_t *part;
uint8_t buf[256];
int ret;
/* 1. 查找分区 */
part = fal_part_find("kvdb");
if (part == OS_NULL)
{
os_kprintf("partition 'kvdb' not found!\r\n");
return;
}
/* 2. 擦除分区 */
os_kprintf("Erasing partition 'kvdb'...\r\n");
ret = fal_part_erase_all(part);
if (ret < 0)
{
os_kprintf("erase failed!\r\n");
return;
}
/* 3. 写入数据 */
memset(buf, 0xAA, sizeof(buf));
ret = fal_part_write(part, 0, buf, sizeof(buf));
if (ret >= 0)
{
os_kprintf("Write %d bytes to 'kvdb' success\r\n", ret);
}
/* 4. 读取验证 */
memset(buf, 0, sizeof(buf));
ret = fal_part_read(part, 0, buf, sizeof(buf));
if (ret >= 0)
{
os_kprintf("Read %d bytes: buf[0]=0x%02X\r\n", ret, buf[0]);
}
}5.3 创建 MTD 设备
#include <fal.h>
#include <mtd/mtd.h>
void create_mtd_from_fal(void)
{
os_mtd_device_t *mtd;
/* 从 FAL 分区创建 MTD 设备 */
mtd = fal_mtd_device_create("filesys");
if (mtd != OS_NULL)
{
os_kprintf("MTD device 'filesys' created\r\n");
os_kprintf(" block_size: %d, page_size: %d\r\n",
mtd->geometry.block_size, mtd->geometry.page_size);
}
else
{
os_kprintf("MTD device 'filesys' create failed\r\n");
}
}5.4 创建 Block 设备
#include <fal.h>
#include <block/block_device.h>
void create_blk_from_fal(void)
{
os_blk_device_t *blk;
/* 从 FAL 分区创建 Block 设备 */
blk = fal_blk_device_create("filesys");
if (blk != OS_NULL)
{
os_kprintf("Block device 'filesys' created\r\n");
os_kprintf(" block_size: %d, block_count: %d\r\n",
blk->geometry.block_size,
blk->geometry.block_count);
}
}六、内部实现机制
6.1 分区表初始化流程
fal_part_init()
│
├── 遍历 fal_part_info[] 表
│ ├── fal_flash_find(flash_name) 查找 Flash 设备
│ ├── 校验 offset + size 不超过 Flash 容量
│ ├── 填充 fal_part_t 结构
│ └── 打印分区表
│
└── 完成初始化6.2 分区读取内部实现 fal_part_read
说明:以下源码取自
drivers/fal/fal_part.c,与项目源码逐字一致(含原始英文注释)。
fal_part_read 处理"分区内偏移 → Flash 绝对页地址"的转换,并处理跨页读取:
int fal_part_read(fal_part_t *part, uint32_t offset, uint8_t *buf, size_t size)
{
int ret = 0;
uint32_t page_addr;
uint8_t *page_buff;
uint32_t page_size;
uint32_t count, remain;
fal_flash_t *flash_dev = OS_NULL;
OS_ASSERT(part);
OS_ASSERT(buf);
flash_dev = part->flash;
if (offset + size > part->info->size)
{
LOG_E(DBG_TAG,
"part read [%s] address out of bound offset:0x%x, size:0x%x, len:0x%x.\r\n",
part->info->name,
offset,
size,
part->info->size);
return -1;
}
page_size = flash_dev->page_size;
page_buff = os_calloc(1, page_size);
if (page_buff == OS_NULL)
{
LOG_E(DBG_TAG, "fal read, memroy lack %s.\r\n", part->info->name);
return 0;
}
uint32_t page_shift = fal_page_shift(flash_dev);
offset += part->info->offset;
page_addr = offset >> fal_page_shift(flash_dev);
offset &= page_size - 1;
remain = size;
while (remain != 0)
{
if (offset != 0 || remain < page_size)
{
ret = flash_dev->ops.read_page(flash_dev, page_addr, page_buff, 1);
}
else
{
count = remain & ~(page_size - 1);
ret = flash_dev->ops.read_page(flash_dev, page_addr, buf, remain >> page_shift);
}
if (ret != 0)
{
LOG_E(DBG_TAG, "flash device(%s) read error, %d .\r\n", part->info->name, page_addr);
break;
}
if (offset != 0 || remain < page_size)
{
count = min(page_size - offset, remain);
memcpy(buf, page_buff + offset, count);
page_addr++;
}
else
{
page_addr += remain >> page_shift;
}
buf += count;
remain -= count;
offset += count;
offset &= page_size - 1;
}
os_free(page_buff);
return (size - remain);
}分步详解:
第 1 步:越界检查(条件 offset + size > part->info->size)
if (offset + size > part->info->size)
{
LOG_E(DBG_TAG,
"part read [%s] address out of bound offset:0x%x, size:0x%x, len:0x%x.\r\n",
part->info->name,
offset,
size,
part->info->size);
return -1;
}详细解释:校验"分区内偏移 + 读取长度"不超过分区大小,越界打印错误并返回 -1(防止越界访问 Flash)。
第 2 步:转换到 Flash 绝对页地址(无条件执行)
offset += part->info->offset;
page_addr = offset >> fal_page_shift(flash_dev);
offset &= page_size - 1;详细解释:关键点:offset += part->info->offset 把分区内偏移加上分区在 Flash 的起始偏移,变成 Flash 上的绝对字节地址。然后 page_addr = offset >> page_shift 得起始页号,offset &= page_size - 1 得页内偏移。
第 3 步:循环跨页读取(条件 remain != 0)
remain = size;
while (remain != 0)
{
if (offset != 0 || remain < page_size)
{
ret = flash_dev->ops.read_page(flash_dev, page_addr, page_buff, 1);
}
else
{
count = remain & ~(page_size - 1);
ret = flash_dev->ops.read_page(flash_dev, page_addr, buf, remain >> page_shift);
}
if (ret != 0)
{
LOG_E(DBG_TAG, "flash device(%s) read error, %d .\r\n", part->info->name, page_addr);
break;
}
if (offset != 0 || remain < page_size)
{
count = min(page_size - offset, remain);
memcpy(buf, page_buff + offset, count);
page_addr++;
}
else
{
page_addr += remain >> page_shift;
}
buf += count;
remain -= count;
offset += count;
offset &= page_size - 1;
}详细解释:remain 是剩余待读字节,循环直到读完。两条读取路径:
- 页内偏移非零或不足一页(
offset != 0 || remain < page_size)→ 读 1 页到临时page_buff,再从page_buff + offset拷出min(page_size - offset, remain)字节(处理页内对齐); - 页对齐且至少一页 → 按
remain >> page_shift一次读多页直接进用户缓冲。
读失败(ret != 0)则LOG_E并break。最后推进buf/remain/offset(offset &= page_size - 1保持页内偏移)。
第 4 步:拷贝/推进指针(无条件执行)
if (offset != 0 || remain < page_size)
{
count = min(page_size - offset, remain);
memcpy(buf, page_buff + offset, count);
page_addr++;
}
else
{
page_addr += remain >> page_shift;
}
buf += count;
remain -= count;
offset += count;
offset &= page_size - 1;
}
os_free(page_buff);
return (size - remain);
}详细解释:部分页读时从 page_buff + offset 拷 min(page_size - offset, remain) 字节并页号 +1;整页读时页号直接推进。最后释放临时缓冲,返回实际读字节数 size - remain。
数值演算 —— 分区偏移 → Flash 绝对地址 + 跨页读取(设 page_size = 256、page_shift = 8、分区 offset = 0x10000,读 300 字节):
| 步骤 | 计算 | 结果 |
|---|---|---|
分区内偏移 offset | 假设从分区内 0x140 起读 | 0x140 = 320 |
| 加分区偏移 | 320 + 0x10000 | 0x10140 = 65984 |
page_addr = offset >> page_shift | 65984 >> 8 | 257(起始页号) |
offset &= page_size - 1 | 65984 & 0xFF | 0x40 = 64(页内偏移) |
| 第 1 轮:页内偏移非零 | 读 1 页到 page_buff,拷 min(256-64, 300) = 192 字节 | page_addr→258,remain = 108,offset = 0 |
| 第 2 轮:页对齐且 ≥1 页 | 读 108 >> 8 = 0 页(不足一页,走分支读 1 页) | 拷 min(256-0, 108) = 108 字节 |
| 结束 | 共读 192 + 108 = 300 字节 | 返回 300 |
关键理解:读页内不对齐的一段数据时,第一页用"临时
page_buff+memcpy拷出"处理页内偏移;之后页对齐的部分可以整页/多页直读。这样既保证读到完整数据,又尽量减少读页次数(对齐时一次读多页)。
fal_part_read() 条件走向总结:
fal_part_read() 条件走向总结:
├── 越界 → LOG_E + 返回 -1
├── page_buff 分配失败 → LOG_E + 返回 0
├── offset += part->info->offset(分区偏移 → Flash 绝对地址)
├── page_addr = offset >> page_shift;offset &= page_size - 1
├── while (remain != 0):
│ ├── (offset != 0 或 remain < page_size) → 读 1 页到 page_buff
│ │ └── 拷贝 min(page_size-offset, remain) 字节;page_addr++
│ ├── 否则 → 一次读 remain>>page_shift 页到 buf
│ ├── 读失败 → LOG_E + break
│ └── 推进 buf/remain/offset
├── os_free(page_buff)
└── 返回 size - remain6.3 MTD 设备封装 fal_mtd_device_create
说明:以下源码取自
drivers/fal/fal_mtd.c,与项目源码逐字一致(含原始英文注释)。
fal_mtd_device_create() 将 FAL 分区封装为标准的 MTD 设备,底层读写擦全部透传到 FAL 分区操作:
struct fal_mtd_device
{
os_mtd_device_t mtd_dev;
fal_part_t *fal_part;
};
int fal_mtd_read_page(os_mtd_device_t *mtd, uint32_t page_addr, uint8_t *buff, uint32_t page_nr)
{
int count;
struct fal_mtd_device *fal_mtd_dev = (struct fal_mtd_device *)mtd;
fal_part_t *part = fal_mtd_dev->fal_part;
count = fal_part_read(part, page_addr * mtd->geometry.page_size, buff, page_nr * mtd->geometry.page_size);
if (count != page_nr * mtd->geometry.page_size)
return OS_FAILURE;
return OS_SUCCESS;
}
os_mtd_device_t *fal_mtd_device_create(const char *parition_name)
{
struct fal_mtd_device *fal_mtd_dev;
struct fal_part *fal_part = fal_part_find(parition_name);
if (!fal_part)
{
LOG_E(DBG_TAG, "Error: the partition name (%s) is not found.", parition_name);
return NULL;
}
const struct fal_flash *fal_flash = fal_part->flash;
fal_mtd_dev = (struct fal_mtd_device *)os_calloc(1, sizeof(struct fal_mtd_device));
if (fal_mtd_dev == OS_NULL)
{
LOG_E(DBG_TAG, "Error: no memory for create FAL block device");
return OS_NULL;
}
fal_mtd_dev->fal_part = fal_part;
fal_mtd_dev->mtd_dev.geometry.page_size = fal_flash->page_size;
fal_mtd_dev->mtd_dev.geometry.block_size = fal_flash->block_size;
fal_mtd_dev->mtd_dev.geometry.capacity = fal_part->info->size;
fal_mtd_dev->mtd_dev.mtd_ops = &fal_mtd_ops;
mtd_device_register(&fal_mtd_dev->mtd_dev, parition_name);
LOG_I(DBG_TAG, "The FAL mtd device (%s) created successfully", parition_name);
return &fal_mtd_dev->mtd_dev;
}分步详解:
第 1 步:查找分区并分配设备(条件 fal_part 为空 / 分配失败)
struct fal_part *fal_part = fal_part_find(parition_name);
if (!fal_part)
{
LOG_E(DBG_TAG, "Error: the partition name (%s) is not found.", parition_name);
return NULL;
}
const struct fal_flash *fal_flash = fal_part->flash;
fal_mtd_dev = (struct fal_mtd_device *)os_calloc(1, sizeof(struct fal_mtd_device));
if (fal_mtd_dev == OS_NULL)
{
LOG_E(DBG_TAG, "Error: no memory for create FAL block device");
return OS_NULL;
}
fal_mtd_dev->fal_part = fal_part;详细解释:fal_part_find 按名字找分区,找不到打印错误并返回 NULL。从 fal_part->flash 取所属 Flash 设备(稍后取几何信息)。申请 fal_mtd_device(包含 os_mtd_device_t + 分区指针),失败返回 OS_NULL。最后 fal_mtd_dev->fal_part = fal_part 绑定分区。
第 2 步:填充几何信息并注册(无条件执行)
fal_mtd_dev->mtd_dev.geometry.page_size = fal_flash->page_size;
fal_mtd_dev->mtd_dev.geometry.block_size = fal_flash->block_size;
fal_mtd_dev->mtd_dev.geometry.capacity = fal_part->info->size;
fal_mtd_dev->mtd_dev.mtd_ops = &fal_mtd_ops;
mtd_device_register(&fal_mtd_dev->mtd_dev, parition_name);
LOG_I(DBG_TAG, "The FAL mtd device (%s) created successfully", parition_name);
return &fal_mtd_dev->mtd_dev;
}详细解释:把 MTD 的几何信息设为:页大小/块大小来自 Flash 设备,容量来自分区大小(fal_part->info->size)。mtd_ops 指向 FAL 的 MTD 操作表(fal_mtd_ops,含 read_page/write_page/erase_block),再 mtd_device_register 注册到设备框架。成功打印日志并返回 MTD 设备指针。
fal_mtd_device_create() 条件走向总结:
fal_mtd_device_create(parition_name) 条件走向总结:
├── fal_part = fal_part_find(parition_name)
│ ├── fal_part == NULL(分区不存在)
│ │ ├── LOG_E
│ │ └── return NULL
│ └── fal_part != NULL
│ ├── fal_flash = fal_part->flash
│ ├── fal_mtd_dev = os_calloc(1, sizeof(fal_mtd_device))
│ │ ├── fal_mtd_dev == NULL(内存不足)
│ │ │ ├── LOG_E
│ │ │ └── return OS_NULL
│ │ └── 分配成功
│ │ ├── fal_mtd_dev->fal_part = fal_part
│ │ ├── 填充 geometry(page/block/capacity)
│ │ ├── mtd_ops = &fal_mtd_ops
│ │ ├── mtd_device_register() 注册
│ │ ├── LOG_I 成功日志
│ │ └── return &fal_mtd_dev->mtd_dev关键理解 —— 底层透传:fal_mtd_read_page 把 MTD 的"页号×页大小"换算回 FAL 分区的字节偏移,再调 fal_part_read。所以 MTD 设备读写擦最终都落在 FAL 分区上,实现了"FAL 分区 → MTD 设备"的封装。
七、FAL 的适配(接入新 Flash 芯片)
7.1 先认识适配层:FAL 是怎么"长"到 Flash 上的
drivers/fal/ 下是跨平台统一的框架层(fal.c 设备管理、fal_part.c 分区、fal_mtd.c/fal_block.c 封装),不碰具体 Flash 寄存器。真正读写/擦除某款 Flash 的是各厂商/芯片的适配代码,例如 drivers/fal/fal_ram.c(RAM 模拟)或各 BSP 里的 onchip_flash.c/spi_flash.c。换 Flash 芯片,就是新写一份 read_page/write_page/erase_block 并注册。
适配全景图:
┌──────────────────────────────────────────────────────────┐
│ 应用层(文件系统 / KV / OTA) │
└───────────────┬──────────────────────┬──────────────────┘
│ MTD/Block 设备 │ 分区直接操作
▼ ▼
┌──────────────────────────────────────────────────────────┐
│ FAL 框架层(drivers/fal/) │
│ fal.c 设备管理 / fal_part.c 分区 / fal_mtd|block.c 封装 │
└───────────────┬──────────────────────┬──────────────────┘
│ ①实现 fal_flash_ops │ ②fal_flash_register
▼ ▼
┌──────────────────────────────────────────────────────────┐
│ 厂商适配层(onchip_flash.c / spi_flash.c / fal_ram.c) │
│ 实现 read_page / write_page / erase_block ★需实现★ │
└───────────────┬──────────────────────┬──────────────────┘
│ HAL / SPI 库 │
▼ ▼
┌──────────────────────────────────────────────────────────┐
│ 硬件(片上 Flash / SPI Flash 芯片 / RAM 模拟) │
└──────────────────────────────────────────────────────────┘适配的关键洞察:接入一款新 Flash 只需做两件事:① 实现 struct fal_flash_ops 三个回调(read_page/write_page/erase_block);② 填充 fal_flash_t(名称/页大小/块大小/容量)并 fal_flash_register 注册。框架层和分区表都不用改。
7.2 以 RAM 模拟为例,看一整套适配代码
以 drivers/fal/fal_ram.c 为例(与项目逐字一致,它是最简单的适配参考):
① 实现 fal_flash_ops 三个回调:
static int fal_ram_read(fal_flash_t *flash, uint32_t page_addr, uint8_t *buff, uint32_t page_nr)
{
memcpy(buff, fal_ram_base + page_addr, page_nr);
return 0;
}
static int fal_ram_write(fal_flash_t *flash, uint32_t page_addr, const uint8_t *buff, uint32_t page_nr)
{
memcpy(fal_ram_base + page_addr, buff, page_nr);
return 0;
}
static int fal_ram_erase(fal_flash_t *flash, uint32_t page_addr, uint32_t page_nr)
{
memset(fal_ram_base + page_addr, 0xff, page_nr);
return 0;
}要点:三个回调都接收页号
page_addr和页数page_nr。RAM 模拟里直接把页号当字节偏移用(page_size=1),真实 Flash 则需page_addr * page_size换算(或直接传页号给硬件控制器)。erase里memset成0xFF是模拟 NOR 擦除后全 1 的特性。
② 填充 fal_flash_t 并注册:
static int fal_ram_probe(void)
{
fal_flash_t *fal_flash = os_calloc(1, sizeof(fal_flash_t));
if (fal_flash == OS_NULL)
{
os_kprintf("fal flash mem leak %s.\r\n", FLASH_NAME);
return -1;
}
memcpy(fal_flash->name, FLASH_NAME, min(FAL_DEV_NAME_MAX - 1, strlen(FLASH_NAME)));
fal_flash->name[min(FAL_DEV_NAME_MAX - 1, strlen(FLASH_NAME))] = 0;
fal_flash->capacity = OS_FAL_RAM_SIZE;
fal_flash->block_size = 1;
fal_flash->page_size = 1;
fal_flash->ops.read_page = fal_ram_read;
fal_flash->ops.write_page = fal_ram_write;
fal_flash->ops.erase_block = fal_ram_erase;
return fal_flash_register(fal_flash);
}要点:分配
fal_flash_t、填名称(截断到FAL_DEV_NAME_MAX-1)、填容量/块大小/页大小、挂三个回调,最后fal_flash_register注册到 FAL 设备链表。fal_flash_register返回注册结果(0 成功、-1 失败/重名)。
③ 用 OS_INIT_CALL 在设备层初始化:
static int fal_ram_init(void)
{
fal_ram_base = os_calloc(1, OS_FAL_RAM_SIZE);
OS_ASSERT(fal_ram_base != OS_NULL);
memset(fal_ram_base, 0xff, OS_FAL_RAM_SIZE);
fal_ram_probe();
return 0;
}
OS_INIT_CALL(fal_ram_init, OS_INIT_LEVEL_DEVICE, OS_INIT_SUBLEVEL_HIGH);要点:初始化时分配模拟内存并填成
0xFF(模拟擦除态),调fal_ram_probe注册设备,用OS_INIT_LEVEL_DEVICE在设备层完成。
7.3 接口契约:接入 FAL 需要实现什么
| 层 | 接口/回调 | 职责 | 对应 7.2 代码 |
|---|---|---|---|
| 读回调 | fal_flash_ops.read_page | 读 page_nr 页到 buff | fal_ram_read |
| 写回调 | fal_flash_ops.write_page | 写 page_nr 页到 Flash | fal_ram_write |
| 擦回调 | fal_flash_ops.erase_block | 擦 page_nr 页(块对齐) | fal_ram_erase |
| 注册 | fal_flash_register | 把 Flash 设备挂进 FAL 链表 | fal_ram_probe |
| 初始化 | OS_INIT_CALL | 在设备层初始化并注册 | fal_ram_init |
7.4 适配新 Flash 芯片的标准流程
第 1 步:查数据手册确认几何参数——页大小(page_size)、块大小(每块页数 block_size)、总容量(capacity),以及页/块对齐规则、擦除是否需要先解锁等。
第 2 步:新建适配文件——在 BSP 或 drivers/fal/ 下新建 xxx_flash.c,实现 read_page/write_page/erase_block,页号按 page_addr * page_size 换算成真实地址。
第 3 步:填充 fal_flash_t 并注册——仿照 7.2 的 probe:填名称、几何参数、挂回调,fal_flash_register 注册。
第 4 步:配置分区表——在项目 fal_cfg.h 里把新 Flash 设备名加入 fal_part_info[](flash_name 字段须与注册名字一致)。
第 5 步:初始化——用 OS_INIT_CALL 在设备层初始化(OS_INIT_LEVEL_DEVICE),确保分区初始化(fal_part_init)之前 Flash 已注册。
第 6 步:测试——用 fal_part_read/write/erase 读写分区并回读比对,验证地址换算和擦除正确。
7.5 适配检查清单(快速自查)
| 检查项 | 说明 |
|---|---|
| 三个回调都实现? | 漏一个则读写擦调用空函数,数据丢失 |
| 页号换算正确? | page_addr * page_size 或直接页号,必须与硬件一致 |
page_size/block_size/capacity 填对? | 填错则分区越界检查误判、擦除对齐错 |
flash_name 与注册名一致? | 不一致则 fal_part_init 的 fal_flash_find 返回 NULL |
| Flash 先于分区初始化? | 分区 fal_part_init 找不到 Flash 返回 -1 |
| 擦除是否把区域填 0xFF? | 模拟/真实 Flash 擦除语义需符合 NOR 特性 |
八、注意事项
- 对齐要求: Flash 写入通常有页对齐要求,擦除有块对齐要求,需确保操作的地址和大小符合硬件约束
- 写前擦除: NAND/NOR Flash 写入前需要先擦除,FAL 框架不自动擦除,需显式调用
fal_part_erase() - 分区表定义:
fal_part_info表在fal_cfg.h中定义,需在编译前配置好 - RAM 模拟:
fal_ram.c提供基于 RAM 的 Flash 模拟实现,可用于单元测试和功能验证 - MTD vs Block: MTD 设备面向原始 Flash 操作(支持页级读写),Block 设备面向块存储(如文件系统)
九、驱动文件位置
| 文件 | 路径 | 说明 |
|---|---|---|
fal.h / fal.c | drivers/fal/ | Flash 设备抽象层,注册与查找 |
fal_part.h / fal_part.c | drivers/fal/ | 分区表管理与分区操作 |
fal_mtd.c | drivers/fal/ | FAL → MTD 设备封装 |
fal_block.c | drivers/fal/ | FAL → Block 设备封装 |
fal_ram.c | drivers/fal/ | RAM 模拟 Flash 实现(适配参考) |
fal_cfg.h | 用户项目 | 分区表配置文件 |
📝 本节小结
核心要点回顾
本节围绕 FAL Flash 抽象层展开,核心要点如下:
- 统一抽象:FAL 屏蔽 SPI Flash、片上 Flash、RAM 模拟等硬件差异,通过分区表把物理 Flash 划分为逻辑分区。
- 核心结构:
fal_flash_t(Flash 设备)+fal_part_info_t(分区配置)+fal_part_t(分区运行时结构)。 - 分区读写:
fal_part_read/fal_part_write内部把"分区内偏移"加part->info->offset转成 Flash 绝对地址,再按页处理跨页;擦除须块对齐。 - MTD/Block 封装:
fal_mtd_device_create/fal_blk_device_create把分区封装成标准设备,底层透传 FAL 分区操作。 - 锁定机制:分区支持锁定(
FAL_PART_INFO_FLAGS_LOCKED),写/擦前检查锁定标志。 - 硬件适配:接入新 Flash = 实现
fal_flash_ops三回调(read_page/write_page/erase_block)+ 填充fal_flash_t并fal_flash_register注册,框架层不用改。
关键易错点速查
| 易错点 | 正确做法 |
|---|---|
| 写前未擦除 | FAL 不自动擦除,NAND/NOR 写前必须先 fal_part_erase/fal_part_erase_all |
| 擦除地址/大小不对齐 | fal_part_erase 要求 offset 和 size 都是 block_size 的整数倍,否则返回 -1 |
| 忽略锁定标志 | 分区被锁定后写/擦返回 -2,需先 fal_part_unlock |
| 分区越界 | 偏移 + 长度不能超过分区大小,越界返回 -1;地址还会加上 part->info->offset |
| 分区表未配置 | fal_part_info[] 在 fal_cfg.h 中定义,fal_part_init 找不到 Flash 设备会初始化失败 |
混淆 page_size/block_size 单位 | page_size 单位是字节、block_size 单位是页数,块字节数 = block_size * page_size |
| Flash 未先注册就初始化分区 | fal_part_init 里 fal_flash_find 找不到 Flash 返回 -1,Flash 须在分区初始化前注册 |
| 适配回调页号换算错误 | 回调参数 page_addr 是页号,真实地址 = page_addr * page_size(或直接当页号用) |
下一步
掌握了 FAL Flash 抽象层后,接下来学习 SDIO 设备:
- 5.18 SDIO设备:了解 OneOS 的 SDIO 总线、SD 卡初始化和块读写。