存储、VM 与文件系统
本文档介绍 AW31N BLE SDK 中 Flash 非易失性存储的完整体系:参数存储(VM/Value Memory,含旧版 VM 与新版 NVM)、虚拟文件系统层(VFS)及其 FAT/norfs 底层实现,覆盖数据结构、接口、注册机制、控制流与配置项。
Purpose and Scope
本页面向需要读写掉电保存参数、操作文件系统(U 盘/Flash 资源)或移植存储子系统的开发者,完整说明以下内容:
- VM(Value Memory):用于保存用户参数/系统配置的掉电存储机制,包括旧版 VM(
vm.h/vm_sfc.h)与新版 NVM(new_vm.h/nvm_api.c)两套实现及选择方式。 - VFS(Virtual File System):SDK 提供的统一文件系统抽象层(
vfs.h/vfs.c),通过vfs_operations函数指针表统一 FAT、norfs 等具体文件系统,并提供fs_*系列宏别名给应用层调用。 - 底层存储:Flash 控制器(SFC)与设备抽象,仅作为依赖关系提及,不展开芯片寄存器细节。
以下主题属于其他目录页,不在本页展开:蓝牙协议栈与连接管理、电源/低功耗管理、USB 设备协议(apps/app/bsp/common/usb/device/ 下的 CDC 等)。VFS 的 FAT 细节(vfs_fat.c)与资源文件系统(vfs_resource.c)属于本页 VFS 抽象之下的具体实现,会在接口层面说明。
Overview
AW31N 是杰理(Jieli Tech)面向 BLE 应用的 SoC,片上通过 SPI Flash(SFC 控制器)提供存储能力。SDK 将存储划分为两个逻辑层次,二者都直接建立在 Flash 之上:
- VM 参数存储:以「ID + 数据」为粒度的小块非易失存储,用于保存配对信息、音量、EQ、配置标志等。VM 的特点是不需要文件系统,写入时按 ID 定位,内部通过双缓冲(A/B 半区)与位图(BIT_MAP)实现磨损均衡与掉电安全。SDK 同时保留旧版 VM 与新版 NVM 两套实现,通过
SYS_MEMORY_SELECT编译宏选择。 - VFS 文件系统:面向大块数据(音乐文件、录音、资源包)的类 POSIX 文件抽象,支持按路径/索引/簇打开文件、读写、seek、ioctl、目录扫描等。VFS 本身不实现任何存储格式,而是通过链接段注册的
vfs_operations表分发到底层具体文件系统(如 FAT、norfs)。
关键设计意图:
- 统一入口、多重后端:
REGISTER_VFS_OPERATIONS宏把各文件系统的操作表放入独立链接段.vfs_operations,vfs_mount运行时遍历段内所有表并按fs_type匹配,新增文件系统无需改动 VFS 核心代码。 - 掉电安全优先:VM 采用 A/B 半区交替写入 + 预擦除(
nvm_pre_erasure_next/nvm_erasure_next_api)+ 写繁忙标志(nvm_op_is_busy_api),把「写一半掉电」的影响降到最低。 - 资源隔离:VFS 相关代码段、BSS 段通过
#pragma显式放置到.vfs.*链接段,便于芯片厂商裁剪与覆盖。
Architecture
flowchart TD
subgraph sg_App["应用层 Application"]
APP["业务代码 / 参数读写"]
FSAPI["fs_* 宏别名 / vfs_* API"]
end
subgraph sg_VM["VM 参数存储子系统"]
VMAPI["vm_api.c / nvm_api.c 应用接口"]
NVM_OBJ["NEW_VM_OBJ 对象 + NVM_CACHE"]
NVMLIB["new_vm_lib.a / fs_lib.a 预编译库"]
end
subgraph sg_VFS["VFS 文件系统层"]
VFSCORE["vfs.c 核心分发"]
OPSTABLE["vfs_operations 链接段表"]
FAT["vfs_fat.c (FAT12/16/32)"]
NORFS["vfs_resource.c (norfs 资源)"]
end
subgraph sg_Flash["Flash 存储底层"]
SFC["SFC Flash 控制器 / 设备抽象"]
FLASH[("SPI Flash 芯片")]
end
APP --> VMAPI
APP --> FSAPI
VMAPI --> NVM_OBJ
NVM_OBJ --> NVMLIB
FSAPI --> VFSCORE
VFSCORE --> OPSTABLE
OPSTABLE --> FAT
OPSTABLE --> NORFS
NVMLIB --> SFC
FAT --> SFC
NORFS --> SFC
SFC --> FLASH
架构说明:
- 上层入口:业务代码可以直接调用
nvm_read_api/nvm_write_api(VM 路径),或通过fs_*/vfs_*API(文件路径)。vfs.h在HAS_VFS_EN开启时把fs_*全部宏映射到vfs_*函数。 - VM 路径:
vm_api.c根据SYS_MEMORY_SELECT == USE_OLD_VM决定是否启用旧版 VM;新版 NVM 的算法(A/B 区管理、位图、合并)以预编译库new_vm_lib.a形式提供,SDK 侧只保留头文件与薄封装(nvm_api.c)。 - VFS 路径:
vfs.c是纯分发层,不感知具体格式;vfs_operations表由各文件系统通过REGISTER_VFS_OPERATIONS宏注册进.vfs_operations链接段,vfs_init/vfs_mount遍历该段完成初始化与挂载匹配。 - 统一底座:无论 VM 还是 VFS,最终都经由 SFC 控制器访问同一片 SPI Flash,因此 Flash 分区规划(VM 区地址/大小、文件系统区)在系统配置中统一定义。
VM 参数存储子系统
新旧两套 VM 的选择
vm_api.c 是整个 VM 子系统的编译入口,它通过 SYS_MEMORY_SELECT 宏在编译期决定启用哪套实现:
#if (SYS_MEMORY_SELECT == USE_OLD_VM)
#define LOG_TAG_CONST NORM
#define LOG_TAG "[normal]"
#include "log.h"
#define LABEL_INDEX_LEN_CRC_SIZE (4)
static u8 vm_buff[sizeof(VM_INDEX_BUFF) + LABEL_INDEX_LEN_CRC_SIZE];
u16 vm_buff_alloc(u8 **buf)
{
if (buf == NULL) {
return 0;
}
*buf = vm_buff;
return sizeof(vm_buff);
}
#endif
Source: vm_api.c
设计意图:旧版 VM 需要一块「索引缓冲 + CRC」的静态 RAM(vm_buff,大小为 VM_INDEX_BUFF 加上 4 字节 LABEL_INDEX_LEN_CRC_SIZE),由 vm_buff_alloc 提供给底层库;新版 NVM 不再需要这块大缓冲,而是改用 RAM cache 记录「ID → 偏移」映射。USE_OLD_VM 分支之外即新版路径(new_vm.h + nvm_api.c + new_vm_lib.a)。
新版 NVM 的数据结构(new_vm.h)
新版 VM 的核心数据结构定义在 new_vm.h:
typedef struct __nvm_entry {
u16 id;//vm id
u16 rw_cnt;//访问次数
u32 offset;//位置偏移
} NVM_ENTRY;
typedef struct __nvm_cache {
u16 rw_cnt;//记录使用cache记录最新的读写值,用于刷新记录项的rw_cnt值
u16 number_entry;//记录个数
NVM_ENTRY *entries;//记录ram入口
} NVM_CACHE;
Source: new_vm.h
NVM_ENTRY:一条「ID → Flash 偏移」的 RAM 缓存记录,rw_cnt用于 LRU 式刷新与合并决策。NVM_CACHE:缓存整体状态,number_entry表示当前有效条目数。cache 的意义是避免每次读写都扫描整片 VM 区,读写过的 ID 直接命中偏移。
VM 区对象与缓冲区常量:
#define BIT_MAP (32 * 16) //512
#define BIT_MAP_SIZE (BIT_MAP / 8) //64
#define NVM_MAX_LEN 128 //merge buffer
#define NVM_BUFF_SIZE (NVM_MAX_LEN + BIT_MAP_SIZE)//128+64
typedef struct __new_vm_obj {
void *device;//open handle
NVM_CACHE *cache;//缓存 id 记录机制
u32 addr;//vm 起始地址
u32 reserve : 7;
u32 bool_block : 1; //当前是否使用下半区B区去存储
u32 block_size : 24;//总vm空间大小
u32 area_len;//实际存储的空间大小
u32 w_offset;//记录区域已写入数据长度
u16 pre_sec_a;//已可写入的多个sec
u16 pre_sec_b;//已可写入的多个sec
u16 id;//缓存支持多次读的id
u16 offset;//记录读的位置
} NEW_VM_OBJ;
Source: new_vm.h
要点解析:
BIT_MAP(512 bit = 64 字节)是 VM 区的分配位图,标记每个最小分配单元是否已被占用;NVM_MAX_LEN(128 字节)是单次合并(merge)缓冲长度;NVM_BUFF_SIZE(192 字节)是库要求的缓冲区总大小。NEW_VM_OBJ用位域把状态压缩进 4 字节:bool_block指示当前写入 A 区(0)还是 B 区(1);block_size(24 bit)记录总 VM 空间;w_offset是已写数据长度;pre_sec_a/pre_sec_b记录 A/B 两区各自已预擦除的扇区数,配合nvm_pre_erasure_next做后台预擦除以降低写延迟。- 双区(A/B)设计:写入交替落在两个半区,某半区写满后
nvm_format_another会格式化另一个半区,实现简单的磨损均衡与掉电恢复(总有至少一个半区是完整可用的)。
NVM 接口分层
库接口(供 SDK 层调用):
| 函数 | 作用 |
|---|---|
nvm_init(NEW_VM_OBJ*, u32 addr, u32 size) | 初始化 VM 区对象,指定 Flash 地址与大小 |
nvm_read(obj, id, buf, len) / nvm_write(obj, id, buf, len) | 按 ID 读写数据 |
nvm_format_another(obj) | 格式化另一个半区(A/B 切换) |
nvm_format_another_ignore(obj, ignore_map, ignore_bits) | 格式化另一区但保留指定 ID |
nvm_format_reset(obj, addr, size) | 全区格式化复位 |
nvm_pre_erasure_next(obj, using_next, idle_next) | 预擦除后续扇区 |
nvm_get_half_addr(obj) / nvm_get_half_len(obj) | 获取当前半区地址/长度 |
nvm_get_cur_date_len(obj) | 获取当前已用长度 |
nvm_buf_for_lib(obj, *p_len) | 提供给库的工作缓冲回调 |
Source: new_vm.h
应用接口(业务代码直接使用):
u32 nvm_init_api(u32 addr, u32 size);
u32 nvm_format_anotheri_api(void);
u32 nvm_read_api(u32 id, u8 *buf, u32 len);
u32 nvm_write_api(u32 id, u8 *buf, u32 len);
void nvm_erasure_next_api(void);
bool nvm_op_is_busy_api(void);
Source: new_vm.h
nvm_op_is_busy_api 返回当前是否正在执行写/格式化等不可打断操作,业务层在低功耗(进入 sleep 前)或切换上下文中必须查询该标志,避免打断 Flash 写序列导致数据损坏。
缓存辅助函数(nvm_api.c 中封装,头文件声明):
u32 nvm_cache_cnt(NVM_ENTRY *entries, u32 len);
u32 nvm_write_cache(NVM_CACHE *cache, u16 id, u32 offset);
u32 nvm_read_cache(NVM_CACHE *cache, u16 id);
u32 nvm_clear_cache(NVM_CACHE *cache);
Source: new_vm.h
nvm_read_cache 以 ID 查缓存返回偏移,未命中返回无效值触发全区扫描重建;nvm_write_cache 写入新偏移并更新 rw_cnt,用于合并时挑选最冷条目。
VFS 文件系统层
核心抽象:vfs_operations 操作表
VFS 的核心是 struct vfs_operations——一张包含全部文件系统操作的函数指针表。具体文件系统(FAT、norfs)各自实现这张表并注册,VFS 核心只负责遍历与分发:
struct vfs_operations {
const char *fs_type;
void (*init)(void);
u32(*mount)(void **ppfs, void *p_device);
u32(*openbypath)(void *pfs, void **ppfile, const char *path);
u32(*openbyindex)(void *pfs, void **ppfile, u32 index, void *parm);
u32(*openbyfile)(void *pcvfile, void **ppfile, void *ext_name);
u32(*openbyclust)(void *pfs, void **ppfile, u32 clust, void *parm);
u32(*createfile)(void *pfs, void **ppfile, u32 *pindex);
u32(*read)(void *pfile, void *buff, u32 len);
u32(*write)(void *pfile, void *buff, u32 len);
u32(*seek)(void *pfile, u32 offset, u32 mode);
u32(*close_fs)(void **ppfs);
u32(*close_file)(void **ppfile);
u32(*fdelete)(void *);
int (*fget_attr)(void *, void *attr);
int (*flen)(void *, u32 *parm);
int (*ftell)(void *, u32 *parm);
u32(*name)(void *, void *name, u32 len);
int (*ioctl)(void *, int cmd, int arg);
int (*fscan_interrupt)(void *, void *, const char *path, u8 max_deepth, int (*callback)(void));
void (*fscan_release)(void *);
int (*fsel)(void *, void *, int sel_mode, void **, int);
int (*file_crc)(void *pfile);
};
Source: vfs.h
设计要点:
fs_type是字符串标识(如"fat"、"norfs"),vfs_mount用它做挂载匹配。- 打开方式有四种:按路径(
openbypath)、按索引(openbyindex,配合FS_IOCTL_*扫描结果)、按文件句柄(openbyfile,用于扩展名关联,如歌词)、按簇(openbyclust,用于快速定位)。 - 目录扫描采用「可中断」设计:
fscan_interrupt接受callback,在扫描过程中周期性回调,让上层(如 UI)可以中断扫描,避免长时间阻塞;fsel是带选择模式的选择器。 - 头部注释中保留了大量被注释掉的旧版接口(
fopen/fread/fwrite等),说明该结构是渐进演化的结果:新 SDK 只保留路径/索引式打开,不再暴露 FILE* 式 API。
注册机制:链接段宏
#define REGISTER_VFS_OPERATIONS(ops) \
const struct vfs_operations ops SEC(.vfs_operations)
Source: vfs.h
SEC(.vfs_operations) 把每个操作表实例放进名为 .vfs_operations 的链接段。vfs.c 通过段首尾符号遍历该段:
extern struct vfs_operations vfs_ops_begin[];
extern struct vfs_operations vfs_ops_end[];
#define list_for_each_vfs_operation(ops) \
for (ops=vfs_ops_begin; ops<vfs_ops_end; ops++)
Source: vfs.c
这是典型的「编译期注册、运行时发现」模式:新增文件系统只需 REGISTER_VFS_OPERATIONS(fat_ops) 定义一张表,链接器自动把它纳入遍历范围,VFS 核心零改动。
挂载与打开的控制流
vfs_mount 是核心分发逻辑,遍历所有已注册操作表,按 fs_type 匹配设备并调用底层 mount:
u32 vfs_mount(void **ppvfs, void *device, void *type)
{
if ((void *)NULL == *ppvfs) {
*ppvfs = vfs_hdl_malloc();
if ((void *)NULL == *ppvfs) {
return E_NO_VFS;
}
}
struct imount *pvfs = *ppvfs;
struct vfs_operations *ops;
list_for_each_vfs_operation(ops) {
if (NULL != ops->mount) {
if (NULL != type) {
if (0 != strcmp(ops->fs_type, type)) {
continue;
}
} else {
if (0 == strcmp(ops->fs_type, "norfs")) {
continue;
}
}
if (0 == ops->mount(&(pvfs->pfs), device)) {
pvfs->ops = ops;
return 0;
} else {
if (NULL != ops->close_fs) {
ops->close_fs(&pvfs->pfs);
}
}
}
}
*ppvfs = vfs_fhdl_free(*ppvfs);
return E_NO_FS;
}
Source: vfs.c
关键行为与设计意图:
- 句柄分配:
vfs_hdl_malloc分配struct imount(内含ops指针 +pfs/pfile联合体),失败返回E_NO_VFS。 - 类型匹配策略:显式传入
type时严格strcmp匹配;不传时跳过"norfs"——因为 norfs 是片上资源文件系统,不应作为默认挂载候选,默认应优先尝试 FAT 等可移动介质。 - 失败回滚:底层
mount失败时调用close_fs清理半初始化状态;全部失败则释放句柄并返回E_NO_FS,保证不会残留悬挂句柄。
vfs_init 在系统启动时调用所有已注册操作表的 init,完成各文件系统的全局初始化:
void vfs_init(void)
{
struct vfs_operations *ops;
list_for_each_vfs_operation(ops) {
if (NULL != ops->init) {
log_info("%s ops_init!!!\n", ops->fs_type);
ops->init();
}
}
}
Source: vfs.c
vfs_openbypath 则把 p_vfs->ops 拷贝到文件句柄 p_vfile->ops,再委托底层 openbypath,实现「挂载时选定文件系统、打开时复用其操作表」:
u32 vfs_openbypath(void *pvfs, void **ppvfile, const char *path)
{
if ((void *)NULL == *ppvfile) {
*ppvfile = vfs_hdl_malloc();
if ((void *)NULL == *ppvfile) {
return E_NO_VFS;
}
}
u32 err;
struct vfs_operations *ops;
struct imount *p_vfs = pvfs;
struct imount *p_vfile = *ppvfile;
if ((void *)NULL == p_vfs) {
err = E_VFS_HDL;
goto __vfs_openbypath;
}
p_vfile->ops = p_vfs->ops;
ops = p_vfs->ops;
if (NULL != ops->openbypath) {
err = ops->openbypath(p_vfs->pfs, &p_vfile->pfile, path);
} else {
/* ... */
Source: vfs.c
应用层别名:fs_* 宏
当 HAS_VFS_EN 开启时,vfs.h 将传统 fs_* 命名全部映射到 vfs_* 实现,保证应用层代码风格统一:
#define fs_resource_init()
#define fs_init vfs_init
#define fs_type_name vfs_type_name
#define fs_mount vfs_mount
#define fs_openbypath vfs_openbypath
#define fs_openbyindex vfs_openbyindex
#define fs_openbyfile vfs_openbyfile
#define fs_openbyclust vfs_openbyclust
#define fs_createfile vfs_createfile
#define fs_read vfs_read
#define fs_write vfs_write
#define fs_file_close vfs_file_close
#define fs_fs_close vfs_fs_close
#define fs_file_name vfs_file_name
#define fs_get_attrs vfs_get_attrs
#define fs_get_fsize vfs_get_fsize
#define fs_ftell vfs_ftell
#define fs_file_delete vfs_file_delete
#define fs_fscan_new vfs_fscan_new
#define fs_fscan vfs_fscan
#define fs_fscan_release vfs_fscan_release
#define fs_select vfs_select
#define fs_ioctl vfs_ioctl
#define fs_mk_dir vfs_mk_dir
#define fs_get_encfolder_info vfs_get_encfolder_info
#define fs_seek vfs_seek
Source: vfs.h
关闭 HAS_VFS_EN 时(#else 分支),vfs.h 直接 extern 声明 fat_monut_api、fat_read_api、fat_seek_api 等 FAT 直调函数,即跳过 VFS 抽象、直接使用 FAT 库——这是为极小 RAM/Flash 场景保留的瘦身路径。
IOCTL 命令集与属性
文件属性与 seek 模式定义:
#define SEEK_SET 0 /* Seek from beginning of file. */
#define SEEK_CUR 1 /* Seek from current position. */
#define SEEK_END 2 /* Seek from end of file. */
#define F_ATTR_RO 0x01
#define F_ATTR_ARC 0x02
#define F_ATTR_DIR 0x04
#define F_ATTR_VOL 0x08
struct vfs_attr {
u8 attr; //属性
u32 fsize; //文件大小
u32 sclust; //地址
};
Source: vfs.h
FS_IOCTL_* 枚举(vfs.h 第 27-57 行)定义了文件系统控制命令全集,覆盖:文件计数(FS_IOCTL_GET_FILE_NUM)、文件校验(FS_IOCTL_FILE_CHECK)、缓存释放(FS_IOCTL_FREE_CACHE)、文件名过滤(FS_IOCTL_SET_NAME_FILTER)、长文件名缓冲(FS_IOCTL_SET_LFN_BUF / FS_IOCTL_SET_LDN_BUF,各 512 字节)、后缀类型过滤(FS_IOCTL_SET_EXT_TYPE)、目录导航(FS_IOCTL_OPEN_DIR / ENTER_DIR / EXIT_DIR / GET_DIR_INFO)、按名取关联文件(FS_IOCTL_GETFILE_BYNAME_INDIR,如按歌曲名取歌词)、长文件名显示信息(FS_IOCTL_GET_DISP_INFO)、建目录(FS_IOCTL_MK_DIR)、加密目录/录音信息(FS_IOCTL_GET_ENCFOLDER_INFO)、统计类(FS_IOCTL_DIR_FILE_TOTAL / FILE_TOTAL / FS_TOTAL)、文件/文件系统索引(FS_IOCTL_FILE_INDEX / FS_IOCTL_FS_INDEX)、文件同步(FS_IOCTL_FILE_SYNC)与扫描复位(FS_IOCTL_RESET_VFSCAN)。业务层通过这些命令驱动音乐播放列表扫描、U 盘枚举等上层逻辑。
具体后端实现文件
- vfs_fat.c / vfs_fat.h:FAT 文件系统后端,实现
vfs_operations表(fat_ops),内部复用预编译库fs_lib.a中的 FAT 逻辑(fat/ff_opr.h、fat/fat_resource.h),支持 FAT12/16/32,是 U 盘/大容量存储的主要后端。 - vfs_resource.c:norfs(片上 Flash 资源文件系统)后端,用于存放固定资源(提示音、UI 资源等),挂载匹配时被默认跳过(见
vfs_mount中strcmp(ops->fs_type, "norfs")分支)。 - vfs_resource_init:
vfs.h中声明,负责初始化资源文件系统。
Core Flow
VFS 挂载与文件读写时序
sequenceDiagram
participant APP as 应用层 (fs_* API)
participant VFS as vfs.c 分发层
participant OPS as vfs_operations 表
participant FAT as FAT 后端 (vfs_fat.c)
participant SFC as SFC/Flash
APP->>VFS: fs_init()
VFS->>OPS: 遍历 .vfs_operations 段调用 init
APP->>VFS: fs_mount(&pvfs, device, "fat")
VFS->>OPS: 匹配 fs_type == "fat"
OPS->>FAT: fat_ops->mount(&pfs, device)
FAT->>SFC: 读取 FAT 引导/目录扇区
FAT-->>VFS: mount 成功, pvfs->ops = fat_ops
APP->>VFS: fs_openbypath(pvfs, &pfile, "/1.mp3")
VFS->>OPS: fat_ops->openbypath(pfs, &pfile, path)
FAT->>SFC: 定位目录项与起始簇
APP->>VFS: fs_read(pfile, buf, len)
VFS->>OPS: fat_ops->read(pfile, buf, len)
FAT->>SFC: 按 FAT 链读扇区
SFC-->>FAT: 数据
FAT-->>APP: 数据返回
APP->>VFS: fs_file_close(&pfile)
流程要点:VFS 层本身不做任何格式解析,所有 I/O 都经 pvfs->ops 委托给挂载时选定的操作表;文件句柄(struct imount)复用同一 ops 指针,因此一个挂载点上的所有文件操作天然走同一后端。
VM 写入与 A/B 区切换流程
flowchart TD
Start([nvm_write_api id, buf, len]) --> Cache{"缓存命中?"}
Cache -->|"是"| Locate["按缓存偏移定位"]
Cache -->|"否"| Scan["扫描当前半区位图找空闲单元"]
Locate --> Check{"当前半区空间足够?"}
Scan --> Check
Check -->|"是"| Write["写入数据 + 更新位图与 w_offset"]
Check -->|"否"| Switch["切换 bool_block 到另一半区<br/>nvm_format_another 格式化"]
Switch --> Migrate["迁移仍有效的 ID 数据"]
Migrate --> Write
Write --> PreErase["nvm_pre_erasure_next 预擦除后续扇区"]
PreErase --> UpdateCache["nvm_write_cache 更新 RAM 缓存"]
UpdateCache --> Busy{"nvm_op_is_busy_api 检查"}
Busy -->|"忙"| Wait["业务层等待/推迟低功耗"]
Busy -->|"空闲"| Done([完成])
设计意图:写操作尽量「只追加、不搬移」,同一 ID 的新值写到新区位置,旧值留在原地成为垃圾;当半区写满时一次性切换到另一半区并做合并迁移(merge),这与 NVM_MAX_LEN(128 字节合并缓冲)配合,把最坏写延迟控制在可预测范围内;预擦除(nvm_pre_erasure_next / nvm_erasure_next_api)把擦除耗时从写路径移到空闲时间,降低峰值延迟。
Usage Examples
1. 业务层读写 VM 参数(应用接口)
u32 nvm_init_api(u32 addr, u32 size);
u32 nvm_format_anotheri_api(void);
u32 nvm_read_api(u32 id, u8 *buf, u32 len);
u32 nvm_write_api(u32 id, u8 *buf, u32 len);
void nvm_erasure_next_api(void);
bool nvm_op_is_busy_api(void);
Source: new_vm.h
典型用法:系统启动时 nvm_init_api(vm_addr, vm_size) 初始化 VM 区;读写参数时以固定 ID 调用 nvm_write_api(id, buf, len) / nvm_read_api(id, buf, len);进入低功耗前调用 nvm_op_is_busy_api() 确认无写操作在进行;空闲时调用 nvm_erasure_next_api() 触发后台预擦除。
2. 直接使用库对象接口(需要精细控制时)
u32 nvm_init(NEW_VM_OBJ *p_nvm, u32 addr, u32 size);
u32 nvm_format_another(NEW_VM_OBJ *p_nvm);
u32 nvm_read(NEW_VM_OBJ *p_nvm, u32 id, u8 *buf, u32 len);
u32 nvm_write(NEW_VM_OBJ *p_nvm, u32 id, u8 *buf, u32 len);
void nvm_pre_erasure_next(NEW_VM_OBJ *p_nvm, u16 using_next, u16 idle_next);
u32 nvm_format_reset(NEW_VM_OBJ *p_nvm, u32 addr, u32 size);
u32 nvm_get_half_addr(NEW_VM_OBJ *p_nvm);
u32 nvm_get_half_len(NEW_VM_OBJ *p_nvm);
Source: new_vm.h
这些接口暴露 NEW_VM_OBJ 与缓存结构,适合需要同时管理多个 VM 区、或需要在格式化时保留特定 ID(nvm_format_another_ignore)的定制场景。
3. 注册一个新的 VFS 后端
#define REGISTER_VFS_OPERATIONS(ops) \
const struct vfs_operations ops SEC(.vfs_operations)
Source: vfs.h
新后端实现 struct vfs_operations my_ops = { .fs_type = "myfs", .init = ..., .mount = ..., ... }; 后用 REGISTER_VFS_OPERATIONS(my_ops) 注册,链接器自动将其放入 .vfs_operations 段,vfs_init/vfs_mount 无需任何修改即可发现它。
4. 使用 fs_* 别名打开并读取文件
#define fs_init vfs_init
#define fs_mount vfs_mount
#define fs_openbypath vfs_openbypath
#define fs_read vfs_read
#define fs_file_close vfs_file_close
Source: vfs.h
应用代码统一使用 fs_mount(&pvfs, device, "fat") → fs_openbypath(pvfs, &pfile, path) → fs_read(pfile, buf, len) → fs_file_close(&pfile) 完成文件访问;切换 HAS_VFS_EN 开关即可在「走 VFS」与「直调 FAT」两种模式间切换,应用层代码保持不变。
Configuration Options
| 配置项 | 类型 | 默认/典型值 | 说明 |
|---|---|---|---|
SYS_MEMORY_SELECT | enum | USE_OLD_VM 或新版 | 选择旧版 VM(vm.h + vm_buff)还是新版 NVM(new_vm.h + new_vm_lib.a),在 vm_api.c 编译期判定 |
HAS_VFS_EN | bool | 1 | 开启后 fs_* 宏映射到 vfs_* 分发层;关闭则直调 FAT 库函数(fat_read_api 等) |
BIT_MAP | 常量 | 32 * 16 = 512 bit | VM 区分配位图总位数,决定最小分配单元数量 |
BIT_MAP_SIZE | 常量 | BIT_MAP / 8 = 64 字节 | 位图占用字节数 |
NVM_MAX_LEN | 常量 | 128 字节 | 单次合并(merge)缓冲长度 |
NVM_BUFF_SIZE | 常量 | NVM_MAX_LEN + BIT_MAP_SIZE = 192 字节 | 库工作缓冲总大小 |
LABEL_INDEX_LEN_CRC_SIZE | 常量 | 4 字节 | 旧版 VM 索引标签的 CRC 附加长度(vm_api.c) |
VFS_FILE_NAME_LEN | 常量 | 16 | VFS 短文件名缓冲长度(g_file_sname) |
FS_IOCTL_SET_LFN_BUF / SET_LDN_BUF | ioctl 参数 | 512 | 长文件名/长目录名缓冲大小,通过 fs_ioctl 设置 |
| VM 区地址/大小 | 分区配置 | 芯片规划决定 | 传给 nvm_init_api(addr, size),需与 Flash 分区表一致 |
Failure Modes、边界情况与并发
错误码与句柄生命周期
vfs.c 中定义了完整的失败返回路径:句柄分配失败返回 E_NO_VFS;挂载遍历全部失败返回 E_NO_FS;文件句柄无有效挂载点返回 E_VFS_HDL。所有失败路径都保证释放已分配句柄(vfs_fhdl_free),避免泄漏。vfs_openbypath 在底层打开失败时保留文件句柄以便上层重试(注释掉的释放逻辑说明历史上曾直接释放,当前改为保留)。
VM 掉电安全与并发约束
- 写中断:VM 采用 A/B 双半区 + 位图 + 预擦除设计。若在写入过程中掉电,重启后扫描位图与半区标志即可恢复到上一个一致状态,最多损失本次未完成的写入;
nvm_format_another只格式化非活动半区,保证至少一个半区始终有效。 - 写繁忙互斥:Flash 写/擦除操作不可打断,业务层在低功耗流程中必须先查询
nvm_op_is_busy_api();若返回忙则推迟休眠或等待完成,否则可能造成 Flash 内部状态机错乱。 - 缓存一致性:
NVM_CACHE中的rw_cnt用于合并时选择最冷条目;nvm_read_cache未命中时需全区扫描重建缓存,因此首次读取延迟高于后续命中读取——对频繁访问的 ID 建议在初始化后预热。 - 多任务访问:SDK 中 VM/VFS 调用通常运行在同一任务上下文或受全局临界区保护;若在中断中调用读写 API,需自行保证与主循环的互斥(源码未提供内部锁,属调用方责任)。
边界情况
vfs_mount未指定type时显式跳过"norfs",防止片上资源文件系统被当作可移动介质默认挂载——这是设计上的防御性分支。vfs_type_name/vfs_openbypath对空指针(NULL 句柄、NULL ops)均有显式保护,返回 NULL 或错误码而不是解引用崩溃。vm_buff_alloc对buf == NULL直接返回 0,避免空指针写入。
性能与运维注意事项
- 写放大与磨损均衡:VM 的「只追加」策略会积累垃圾条目,靠半区切换时的合并回收;
NVM_MAX_LEN = 128限制单次合并的搬移量,避免单次写触发过大搬移。频繁修改同一 ID 会加速垃圾累积,建议对高频参数合理选择 ID 并批量写入。 - 预擦除降低峰值延迟:调用
nvm_erasure_next_api()/nvm_pre_erasure_next()把擦除操作分散到空闲时段,显著改善写响应抖动。 - 链接段布局:VFS 代码通过
#pragma bss_seg(".vfs.data.bss")、code_seg(".vfs.text")等独立放置,可配合链接脚本整体裁剪;关闭HAS_VFS_EN可进一步省去 VFS 分发层 RAM/Flash 开销(换取 FAT 直调耦合)。 - 缓存释放:
FS_IOCTL_FREE_CACHE允许上层在内存紧张时主动释放 FAT 层缓存;FS_IOCTL_FILE_SYNC用于在拔出 U 盘/掉电前把脏数据刷回 Flash。
Extension Points
- 新增文件系统后端:实现
struct vfs_operations并用REGISTER_VFS_OPERATIONS注册,VFS 核心零改动;注意fs_type字符串需唯一且避免"norfs"的默认跳过语义冲突。 - VM 区多实例:库接口
nvm_init(NEW_VM_OBJ*, ...)支持在 RAM 中创建多个NEW_VM_OBJ,可管理多个独立 VM 分区;nvm_format_another_ignore支持格式化时保留指定 ID,适合 OTA 升级保留配置等场景。 - 文件过滤与扫描:
FS_IOCTL_SET_NAME_FILTER、FS_IOCTL_SET_EXT_TYPE与可中断扫描fscan_interrupt允许业务层定制媒体库扫描策略并让出 CPU。 - 旧版/新版 VM 切换:通过
SYS_MEMORY_SELECT宏在编译期切换,SDK 同时维护vm.h/vm_sfc.h(旧)与new_vm.h(新)两套头文件,迁移时只需改宏并重新编译。
测试情况说明
源码仓库中未发现针对 VM/VFS 的独立单元测试目录;存储子系统的正确性主要依赖预编译库(new_vm_lib.a、fs_lib.a)的出厂验证与整机功能测试(录音回放、参数掉电保存、U 盘枚举)。仓库 patch_release/AW31N_开机&低功耗&VM兼容性修复说明_20250102/ 目录包含官方补丁说明(PDF)与修复后的 nvm_api.c、new_vm.h 及更新库,涉及 VM 兼容性修复,建议集成时参考该补丁以保证新老库版本一致。
Related Links
- new_vm.h — 新版 VM 接口定义
- nvm_api.c — NVM 应用层封装
- vm_api.c — 旧版 VM 选择与缓冲分配
- vm.h / vm_sfc.h — 旧版 VM 头文件
- vfs.h — VFS 接口与 fs_* 别名
- vfs.c — VFS 分发层实现
- vfs_fat.c — FAT 后端实现
- vfs_resource.c — norfs 资源后端实现
- 相关目录页:蓝牙协议栈与连接管理、电源/低功耗管理(VM 写繁忙与休眠配合)、USB 设备协议(大容量存储设备与 VFS 联动)