杰理 SDK 文档中心
首页
首页
  • 概述与快速入门

    • 芯片平台与 SDK 概述
    • 环境搭建与编译工具链
    • 快速开始:选型、编译与烧录
    • 烧录与量产工具
  • 构建系统与板级工程

    • 顶层 Makefile 与编译目标
    • 板级工程与配置
    • 后处理与配置工具
  • HID 人机交互应用

    • HID 应用架构总览
    • 键盘、翻页器与遥控应用
    • 鼠标应用:单模、双模与低延迟
    • 空闲应用与初始化流程
  • BLE 透传与数传应用

    • 透传应用总览
    • 多连接与无连接传输
    • AT 命令模组应用
    • Dongle 适配器应用
  • BSP 公共模块

    • 蓝牙公共处理
    • 按键、LED 与红外
    • 传感器与编码器
    • 存储、VM 与文件系统
    • 电源管理与低功耗
    • 消息调度与通信外设
  • 协议栈与预编译库

    • 蓝牙协议栈库
    • 设备驱动与文件系统库
    • 音频、升级与其他库
  • 开发资料与补丁发布

    • 文档资料中心
    • 版本补丁与兼容性修复

存储、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 之上:

  1. VM 参数存储:以「ID + 数据」为粒度的小块非易失存储,用于保存配对信息、音量、EQ、配置标志等。VM 的特点是不需要文件系统,写入时按 ID 定位,内部通过双缓冲(A/B 半区)与位图(BIT_MAP)实现磨损均衡与掉电安全。SDK 同时保留旧版 VM 与新版 NVM 两套实现,通过 SYS_MEMORY_SELECT 编译宏选择。
  2. 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_SELECTenumUSE_OLD_VM 或新版选择旧版 VM(vm.h + vm_buff)还是新版 NVM(new_vm.h + new_vm_lib.a),在 vm_api.c 编译期判定
HAS_VFS_ENbool1开启后 fs_* 宏映射到 vfs_* 分发层;关闭则直调 FAT 库函数(fat_read_api 等)
BIT_MAP常量32 * 16 = 512 bitVM 区分配位图总位数,决定最小分配单元数量
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常量16VFS 短文件名缓冲长度(g_file_sname)
FS_IOCTL_SET_LFN_BUF / SET_LDN_BUFioctl 参数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

  1. 新增文件系统后端:实现 struct vfs_operations 并用 REGISTER_VFS_OPERATIONS 注册,VFS 核心零改动;注意 fs_type 字符串需唯一且避免 "norfs" 的默认跳过语义冲突。
  2. VM 区多实例:库接口 nvm_init(NEW_VM_OBJ*, ...) 支持在 RAM 中创建多个 NEW_VM_OBJ,可管理多个独立 VM 分区;nvm_format_another_ignore 支持格式化时保留指定 ID,适合 OTA 升级保留配置等场景。
  3. 文件过滤与扫描:FS_IOCTL_SET_NAME_FILTER、FS_IOCTL_SET_EXT_TYPE 与可中断扫描 fscan_interrupt 允许业务层定制媒体库扫描策略并让出 CPU。
  4. 旧版/新版 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 联动)
Prev
传感器与编码器
Next
电源管理与低功耗