杰理 SDK 文档中心
首页
首页
  • 项目概览与快速开始

    • 项目概述与芯片支持
    • 环境搭建与工具链
    • 工程与构建系统
    • 烧录与升级工具
    • 文档与硬件资料
  • 系统架构与芯片平台

    • 芯片平台与启动流程
    • 预编译库与头文件体系
    • 消息、定时器与中断服务
    • 通用外设驱动
  • 存储与文件系统

    • 文件系统实现
    • 存储设备驱动
    • VM 参数存储系统
  • 音频处理

    • 音频解码器
    • 音频编码器
    • MIDI 合成与播放
    • 音效、变速变调与降噪
  • 语音玩具应用

    • 应用框架与状态机
    • 音乐播放与外部音源
    • MIDI 乐器模式
    • 录音应用
    • 待机、电源管理与 USB 从机
  • 小音箱应用

    • 应用框架与模式管理
    • 播放源:音乐、FM、录音与 LineIn
  • 应用层与示例工程

    • 通用 MCU 应用
  • 固件更新与补丁

    • 固件升级机制
    • AD14N 主动降噪补丁

VM 参数存储系统

VM(Volatile/Value Memory)参数存储系统是 AD15N 等杰理 MCU SDK 中用于在 SPI NOR Flash 上持久化小尺寸参数的键值存储子系统。应用通过统一的 vm_read / vm_write 接口按 ID 读写参数,底层由编译期开关选择"新版 VM(new_vm)"或"旧版 VM(old_vm)"实现,从而在掉电保持、擦写均衡、预擦除与中断响应等方面获得不同取舍。

Purpose and Scope

本文档全面介绍 VM 参数存储系统的设计与使用,涵盖:

  • VM_INDEX 索引空间的分配规则(用户区 / 系统保留区 / 应用区 / FM 频道区);
  • 统一 API 适配层 vm_api.c 如何通过 SYS_MEMORY_SELECT 在 new_vm 与 old_vm 之间切换;
  • 新版 VM 的对象模型(双块交替、位图、写缓存)与预擦除机制;
  • 底层 vm_sfc 与 SPI Flash 的交互,以及擦写期间中断释放机制;
  • 配置选项、API 参考、失败模式与并发注意事项。

以下主题属于兄弟页面,不在本文档展开:文件系统(如 FAT)与磁盘挂载逻辑、SPI Flash 底层驱动(SFC 控制器寄存器级操作)、其他外设驱动。

Overview

MCU 产品(如语音玩具、音箱、点读机等)需要保存音量、播放进度(BP)、FM 频率、系统模式等少量运行参数,这些参数要求掉电后不丢失、频繁写入、且写入过程不能阻塞系统响应过久。直接在 Flash 上做任意地址改写并不现实——NOR Flash 必须先擦除(按扇区/块)才能编程,且擦除寿命有限。VM 子系统正是为此设计的轻量键值层。

核心设计思路:

  1. 按 ID 索引而非文件系统:VM_INDEX 枚举为每个参数分配固定 ID,vm_read(id) / vm_write(id) 以 ID 为键,省去目录解析开销。
  2. 编译期选择实现:SYS_MEMORY_SELECT 决定链接 new_vm 或 old_vm 二进制库(new_vm_lib.a / old_vm_lib.a),应用代码无需改动。
  3. 双块交替 + 位图:新版 VM 将存储区划分为两个块,配合 512 位位图记录有效数据位置,写入时在空闲块追加,满了再擦除另一块(nvm_format_another),实现擦写均衡并缩短单次写入等待。
  4. 预擦除:vm_pre_erase() 在系统空闲时提前擦除备用块,把最耗时的擦除操作移出关键路径。
  5. 中断释放:Flash 擦写期间通过 vm_isr_response_list_register 释放指定中断(如音频中断),避免长时间关中断导致音频断流。

Architecture

flowchart TD
    subgraph sg_App["应用层 (App)"]
        App["业务代码<br/>syscfg_vm_init / vm_read / vm_write / vm_pre_erase"]
    end

    subgraph sg_Adapter["适配层 (vm_api.c)"]
        Adapter["SYS_MEMORY_SELECT 编译期选择"]
    end

    subgraph sg_NewVM["新版 VM (new_vm)"]
        NVM["nvm_init_api / nvm_read_api / nvm_write_api<br/>nvm_erasure_next_api / nvm_format_anotheri_api"]
        Cache["NVM_CACHE 缓存表<br/>nvm_write_cache / nvm_read_cache"]
        Obj["NEW_VM_OBJ 双块对象<br/>w_offset / bool_block / pre_sec_a / pre_sec_b"]
    end

    subgraph sg_OldVM["旧版 VM (old_vm)"]
        OVM["syscfg_old_vm_init / old_vm_read / old_vm_write"]
    end

    subgraph sg_SFC["介质与中断层 (vm_sfc)"]
        SFC["SPI Flash 驱动 / SFC 控制器"]
        ISR["中断响应注册<br/>vm_isr_response_list_register"]
        Busy["vm_busy 擦写忙标志"]
    end

    App -->|"统一 API"| Adapter
    Adapter -->|"USE_NEW_VM"| NVM
    Adapter -->|"USE_OLD_VM"| OVM
    NVM --> Cache
    NVM --> Obj
    NVM -->|"写 Flash"| SFC
    OVM -->|"写 Flash"| SFC
    SFC --> Busy
    SFC --> ISR
    ISR --> FLASH[("Flash 芯片")]

各层职责:

  • 应用层:只依赖 vm_api.h 中四个函数(初始化、读、写、预擦除)和 VM_INDEX 枚举,不感知底层实现差异。
  • 适配层(vm_api.c):用宏把统一 API 映射到 new_vm 或 old_vm 的真实实现;SYS_MEMORY_SELECT 未匹配时退化为返回 -1 的空桩,便于裁剪。
  • new_vm:以 NEW_VM_OBJ 为核心的双块键值库,带 NVM_CACHE 写缓存;头文件在 new_vm.h,实际算法以预编译库形式提供(如 sdk/include_lib/liba/ch58/voice_toy/new_vm_lib.a)。
  • old_vm:旧版实现(ovm_api.c + old_vm_lib.a),接口相似但不支持预擦除,写入耗时更长。
  • vm_sfc:SPI Flash 抽象层,提供 vm_busy 忙标志、代码区写保护回调、以及擦写期间中断释放的注册接口。

索引空间管理(VM_INDEX)

VM_INDEX 是全局唯一的参数 ID 枚举,定义于 vm_api.h。整个 32 位 ID 空间被划分为几个有严格顺序约束的区域:

区域ID 范围用途与约束
用户区VM_INDEX_DEMO = 0示例/用户自定义,官方注明"用户可以使用"
系统 lib 保留区1 ~ LIB_SYSMEM_END = 32供系统库使用(低功耗计数、RTC/闹钟时间、PMU 电压等),预留 32 个 ID 且不可修改顺序——库内以固定 ID 访问,插入/删除会导致旧固件数据错位
应用业务区33 起VM_INDEX_SONG、VM_INDEX_ENG、VM_INDEX_VOL(音量)等,含 *_BP 播放进度族
Mbox/外设区后续VM_INDEX_SYSMODE、U 盘/SD/外部 Flash 的 BP 与索引、VM_INDEX_ACTIVE_DEV
FM 频道区VM_INDEX_FM_CHANNL ~ VM_INDEX_FM_CHANNL_END = VM_INDEX_FM_CHANNL + 28连续 29 个 ID,注释明确"之间不能插入其它 INDEX",因为 FM 频道按频率段连续寻址
收尾区VM_INDEX_FLASH_UPDATE、VM_INDEX_AUTO_BP升级标志、自动 BP
上限VM_INDEX_MAX = 128索引总数硬上限,超出部分不可用
typedef enum {
    VM_INDEX_DEMO = 0,//用户可以使用

    // 系统lib使用,预留32个id,不可修改顺序
    LIB_VM_INDEX_LP_SUM_CNT     = 1,
    LIB_VM_INDEX_LP_LAST_CNT    = 2,
    LIB_VM_INDEX_RTC_TIME       = 3,
    LIB_VM_INDEX_ALM_TIME       = 4,
    LIB_VM_PMU_VOLTAGE          = 5,
    LIB_SYSMEM_END              = 32,
    // 系统lib使用结束

    VM_INDEX_SONG,
    VM_INDEX_ENG,
    VM_INDEX_POETRY,
    ...
    VM_INDEX_FM_CHANNL,//VM_INDEX_FM_CHANNL和VM_INDEX_FM_CHANNL_END之间不能插入其它INDEX
    VM_INDEX_FM_CHANNL_END = VM_INDEX_FM_CHANNL + 28,
    ...
    VM_INDEX_MAX = 128,
} VM_INDEX;

Source: vm_api.h

设计意图:枚举即契约。系统库使用固定 ID 意味着 OTA 升级后旧参数仍可被新固件读到;"预留 32 个、不可改序"与"FM 频道连续区不可插入"两条硬约束共同保证 ID 语义的向后兼容。新增参数时应追加在 VM_INDEX_MAX 之前、避免改动既有项。

适配层与编译期实现切换

vm_api.c 是唯一同时接触应用与底层实现的文件。它用 SYS_MEMORY_SELECT 宏做三路编译分支:

#if (SYS_MEMORY_SELECT == USE_NEW_VM)
//新版vm,支持预擦除
#include "new_vm.h"
#define syscfg_vm_init_api(addr, size)        	nvm_init_api(addr, size)
#define vm_read_api(id, buf, len)      		nvm_read_api(id, buf, len)
#define vm_write_api(id, buf, len)     		nvm_write_api(id, buf, len)
#define vm_pre_erase_api()             		nvm_erasure_next_api()

#elif (SYS_MEMORY_SELECT == USE_OLD_VM)
//旧版vm
#include "old_vm.h"
#define syscfg_vm_init_api(addr, size)			syscfg_old_vm_init(addr, size)
#define vm_read_api(id, buf, len)				old_vm_read(id, buf, len)
#define vm_write_api(id, buf, len)				old_vm_write(id, buf, len)
#define vm_pre_erase_api()

#else
#define syscfg_vm_init_api(addr, size)			-1
#define vm_read_api(id, buf, len)				-1
#define vm_write_api(id, buf, len)				-1
#define vm_pre_erase_api()
#endif

Source: vm_api.c

四个公开函数 syscfg_vm_init / vm_read / vm_write / vm_pre_erase 只是逐行转发到 *_api 宏:

int syscfg_vm_init(u32 mem_addr, u32 mem_size)
{
    return syscfg_vm_init_api(mem_addr, mem_size);
}

int vm_read(u32 id, u8 *data_buf, u16 len)
{
    return vm_read_api(id, data_buf, len);
}

int vm_write(u32 id, u8 *data_buf, u16 len)
{
    return vm_write_api(id, data_buf, len);
}

void vm_pre_erase(void)
{
    vm_pre_erase_api();
}

Source: vm_api.c

设计意图:宏转发比函数指针表更省 RAM/Flash,且让编译器对每个调用点做常量折叠;#else 空桩保证在无 VM 需求的裁剪配置下链接仍能通过。新旧实现差异被完全封装在宏内——应用代码只需要一次编写、两种后端通用。

新版 VM 内部结构(new_vm)

new_vm.h 定义了对象、缓存与常量,真实算法位于二进制库 new_vm_lib.a(sdk/include_lib/liba/*/.../new_vm_lib.a)。头文件是理解行为边界的权威来源。

核心常量

#define BIT_MAP  (32 * 16)      // 512 bit
#define BIT_MAP_SIZE  (BIT_MAP / 8)   // 64 字节
#define NVM_MAX_LEN     64     // 单条参数最大长度(字节)
#define NVM_BUFF_SIZE   (NVM_MAX_LEN + BIT_MAP_SIZE)  // 128 字节缓冲

Source: new_vm.h

位图(bit map)是 new_vm 的数据组织核心:一个块内每条记录的占用/有效状态用 1 bit 表示,512 bit 位图恰好对应块内可容纳的记录数上限;读写缓冲为"最大记录 + 位图"共 128 字节,保证一次加载即可操作整块元数据。

对象模型 NEW_VM_OBJ

typedef struct __new_vm_obj {
    void *device;
    NVM_CACHE *cache;
    u32 addr;
    u32 reserve : 7;
    u32 bool_block : 1;     // 当前激活块标志
    u32 block_size : 24;    // 块大小
    u32 area_len;           // 存储区总长
    u32 w_offset;           // 当前写偏移
    u16 pre_sec_a;          // 预擦除扇区 A
    u16 pre_sec_b;          // 预擦除扇区 B
    u16 id;
    u16 offset;
} NEW_VM_OBJ;

Source: new_vm.h

位域布局(reserve:7 + bool_block:1 + block_size:24)说明 bool_block 标记当前正在使用的块(双块交替),block_size 为 24 位块大小(最大 16MB),pre_sec_a/pre_sec_b 记录预擦除进度。id/offset 字段用于单次读写的快速定位(NVM_MULTIPLE_READ 宏被注释掉,默认单次读路径)。

缓存层 NVM_CACHE

typedef struct __nvm_entry {
    u16 id;
    u16 rw_cnt;      // 读写计数,用于缓存淘汰/失效
    u32 offset;      // 该 id 数据在块内的偏移
} NVM_ENTRY;

typedef struct __nvm_cache {
    u16 rw_cnt;
    u16 number_entry;
    NVM_ENTRY *entries;
} NVM_CACHE;

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

设计意图:每次读都扫描整块位图太慢,缓存把 id → offset 映射保存在 RAM,rw_cnt 记录写入频率,供库决定何时重建/清空缓存(nvm_clear_cache)。nvm_write_cache 在写入新值时同步更新映射,nvm_read_cache 命中时直接按 offset 读取,避免位图扫描。

双块交替与格式化

库接口暴露两个关键操作:

u32 nvm_format_another(NEW_VM_OBJ *p_nvm);       // 格式化另一块
void nvm_pre_erasure_next(NEW_VM_OBJ *p_nvm, u16 using_next, u16 idle_next);

Source: new_vm.h

写入流程为:在当前块的 w_offset 处追加记录 → 更新位图 → 若当前块写满则 nvm_format_another 擦除并切换 bool_block,继续在备用块写入。这样任何时刻都有一块可写,擦除动作被推迟到"满了才做",单次写入只需一次页编程。

预擦除机制

新版 VM 独有 nvm_pre_erasure_next / nvm_erasure_next_api:在系统空闲(如低功耗前、UI 空闲回调)时提前把备用块擦干净,并把进度记录在 pre_sec_a/pre_sec_b。应用只需在合适时机调用:

void vm_pre_erase(void)
{
    vm_pre_erase_api();   // USE_NEW_VM 下展开为 nvm_erasure_next_api()
}

Source: vm_api.c

设计意图:NOR 擦除(毫秒级)远慢于页编程(微秒级)。若等写满才擦除,用户触发保存时会产生明显卡顿;预擦除把"昂贵的擦除"提前到后台空闲时段完成,vm_write 关键路径上只剩编程开销。旧版 VM 的 vm_pre_erase_api() 为空宏,因此写入时延较高——这是选用 new_vm 的主要理由。

介质层与中断协调(vm_sfc)

vm_sfc.h 定义 Flash 抽象与中断协调接口:

#define IOCTL_SET_VM_INFO               _IOW('V', 1, 1)
#define IOCTL_GET_VM_INFO               _IOW('V', 2, 1)

typedef u32(*flash_code_protect_cb_t)(u32 offset, u32 len);
u32 flash_code_protect_callback(u32 offset, u32 len);
extern volatile u8 vm_busy;
void spi_cache_way_switch(u8 way_num);
void vm_isr_response_list_register(u32 bit_list);
void vm_isr_response_index_register(u8 index);
void vm_isr_response_index_unregister(u8 index);
u32 get_vm_isr_response_index_h(void);
u32 get_vm_isr_response_index_l(void);

Source: vm_sfc.h

  • vm_busy(volatile)是擦写忙标志,中断/轮询代码据此避开 Flash 访问窗口;
  • flash_code_protect_callback 用于保护代码区不被 VM 误写;
  • spi_cache_way_switch 切换 SPI 缓存方式,解决 cache 与擦写访问冲突;
  • vm_isr_response_list_register(32 位位图,AD14/15/17 仅低 32 位可用)与 vm_isr_response_index_register 声明"擦写期间允许哪些中断响应"——典型用法是放开音频中断,防止长时间静音/断流;get_vm_isr_response_index_h/l 供系统查询当前放开的集合。

核心流程

写入流程(vm_write)

sequenceDiagram
    participant App as 业务代码
    participant Api as vm_api.c (vm_write)
    participant NVM as nvm_write_api (库)
    participant Cache as NVM_CACHE
    participant SFC as SFC / Flash 驱动
    participant ISR as 中断协调

    App->>Api: vm_write(id, data_buf, len)
    Api->>NVM: nvm_write_api(id, buf, len)
    NVM->>NVM: 校验 len <= NVM_MAX_LEN(64)
    NVM->>Cache: nvm_write_cache 更新 id→offset 映射
    NVM->>NVM: 检查当前块剩余空间 (w_offset / 位图)
    alt 当前块空间充足
        NVM->>SFC: 页编程写入数据 + 置位图
    else 当前块已满
        NVM->>NVM: nvm_format_another 切换 bool_block
        NVM->>SFC: 擦除备用块并重写
    end
    SFC->>ISR: 擦写期间释放已注册中断
    NVM-->>Api: 返回结果 (0 / 错误码)
    Api-->>App: 返回结果

初始化流程(syscfg_vm_init)

系统启动早期(Flash 驱动就绪后)调用 syscfg_vm_init(mem_addr, mem_size),指定 VM 专用存储区在 Flash 中的起始地址与大小;new_vm 后端 nvm_init_api 会解析双块布局、恢复 w_offset 与位图、初始化缓存。旧版后端 syscfg_old_vm_init 行为类似但不维护双块状态。

预擦除流程(vm_pre_erase)

空闲时机(如退出菜单、低功耗前)调用 vm_pre_erase() → nvm_erasure_next_api() → nvm_pre_erasure_next 擦除未激活块,进度记录在 pre_sec_a/pre_sec_b,可分段执行以避免长时间占用总线。

使用示例

示例 1:系统初始化与参数读写

以下代码展示典型的系统级用法——初始化 VM 区后按 ID 读写参数(完整 API 声明见头文件):

int syscfg_vm_init(u32 mem_addr, u32 mem_size);
int vm_read(u32 id, u8 *data_buf, u16 len);
int vm_write(u32 id, u8 *data_buf, u16 len);
void vm_pre_erase(void);

Source: vm_api.h

调用模式:

// 1. 启动时初始化(传入 Flash 分区中的 VM 地址与大小)
syscfg_vm_init(VM_ADDR, VM_SIZE);

// 2. 读取音量参数(未写入过则按默认值处理)
u8 vol = 10;
if (vm_read(VM_INDEX_VOL, &vol, sizeof(vol)) != 0) {
    vol = 10;  // 首次上电,使用默认值
}

// 3. 修改后写回
vol = 12;
vm_write(VM_INDEX_VOL, &vol, sizeof(vol));

// 4. 空闲时预擦除备用块,保证后续写入低延迟
vm_pre_erase();

示例 2:编译期选择实现后端

工程配置通过 SYS_MEMORY_SELECT 选择后端;宏映射是适配层唯一的分叉点:

#if (SYS_MEMORY_SELECT == USE_NEW_VM)
#define syscfg_vm_init_api(addr, size)        	nvm_init_api(addr, size)
#define vm_read_api(id, buf, len)      		nvm_read_api(id, buf, len)
#define vm_write_api(id, buf, len)     		nvm_write_api(id, buf, len)
#define vm_pre_erase_api()             		nvm_erasure_next_api()
#elif (SYS_MEMORY_SELECT == USE_OLD_VM)
#define syscfg_vm_init_api(addr, size)			syscfg_old_vm_init(addr, size)
...
#endif

Source: vm_api.c

示例 3:直接使用 new_vm 应用接口

当需要绕过适配层、直接操作底层(如 bootloader 或特殊分区)时,使用 new_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);

Source: new_vm.h

示例 4:擦写期间放开中断

播放/录音等对时序敏感的场景,在 VM 擦写前注册需放开的音频中断,避免音频断流:

// 注册位图:放开音频等中断(低 32 位,AD14/15/17 只有低 32 位)
vm_isr_response_list_register(BIT(IRQ_AUDIO_IDX) | ...);
// 或按索引注册/注销
vm_isr_response_index_register(IRQ_AUDIO_IDX);
vm_isr_response_index_unregister(IRQ_AUDIO_IDX);
// 查询当前放开集合
u32 hi = get_vm_isr_response_index_h();  // index 32-63
u32 lo = get_vm_isr_response_index_l();  // index 0-31

Source: vm_sfc.h

配置选项

选项类型默认/可选值说明
SYS_MEMORY_SELECT宏USE_NEW_VM / USE_OLD_VM选择 VM 实现后端;new_vm 支持预擦除,old_vm 无
VM_INDEX_MAX枚举常量128参数 ID 总数硬上限
LIB_SYSMEM_END枚举常量32系统 lib 保留 ID 区终点,不可改序
NVM_MAX_LEN宏64单条参数最大字节数(new_vm)
BIT_MAP宏512 bit(32*16)块内记录有效位图位数(new_vm)
NVM_BUFF_SIZE宏128(64+64)读写缓冲大小(最大记录 + 位图)
NVM_MULTIPLE_READ宏默认注释关闭开启多读路径;关闭时走单次读
IOCTL_SET/GET_VM_INFOioctl_IOW('V',1,1) / _IOW('V',2,1)向 SFC 驱动设置/查询 VM 分区信息

API 参考

统一 API(vm_api.h / vm_api.c)

int syscfg_vm_init(u32 mem_addr, u32 mem_size)

初始化 VM 存储区。必须在首次读写前调用一次。

参数:

  • mem_addr (u32):VM 区在 Flash 中的起始地址(由分区表决定)
  • mem_size (u32):VM 区大小,需容纳双块布局(new_vm)

返回: int,0 表示成功;-1 表示未配置后端(SYS_MEMORY_SELECT 未匹配时适配层直接返回 -1)。

int vm_read(u32 id, u8 *data_buf, u16 len)

按 ID 读取参数。

参数:

  • id (u32):VM_INDEX 枚举值,须 < VM_INDEX_MAX
  • data_buf (u8*):输出缓冲区
  • len (u16):期望读取长度;new_vm 下不得超过 NVM_MAX_LEN(64)

返回: int,0 成功;非 0 表示该 ID 不存在或读取失败(首次上电常见)。

int vm_write(u32 id, u8 *data_buf, u16 len)

按 ID 写入参数(追加式写入 + 位图更新)。

参数:

  • id (u32):VM_INDEX 枚举值
  • data_buf (u8*):待写入数据
  • len (u16):数据长度,new_vm 下不得超过 NVM_MAX_LEN(64)

返回: int,0 成功;非 0 失败(参数超长、块满且格式化失败、Flash 编程错误等)。

void vm_pre_erase(void)

预擦除备用块。new_vm 下展开为 nvm_erasure_next_api();old_vm 下为空操作。建议在系统空闲时调用,以把擦除耗时移出写入关键路径。

new_vm 应用接口(new_vm.h)

u32 nvm_init_api(u32 addr, u32 size)

底层初始化,等价于 syscfg_vm_init 的 new_vm 后端。

u32 nvm_read_api(u32 id, u8 *buf, u32 len) / u32 nvm_write_api(u32 id, u8 *buf, u32 len)

底层读写,等价于 vm_read / vm_write 的 new_vm 后端。

u32 nvm_format_anotheri_api(void)

立即格式化另一块(切换 bool_block)。一般在当前块写满时由库内部调用,也可由上层主动触发以强制轮换。

void nvm_erasure_next_api(void)

执行一次"预擦除下一步",进度记录于 NEW_VM_OBJ.pre_sec_a/pre_sec_b,可多次调用分段完成。

new_vm 库接口与缓存接口(new_vm.h)

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);
void *nvm_buf_for_lib(NEW_VM_OBJ *p_nvm, u32 *p_len);  // 供库申请内部缓冲的回调

u32 nvm_cache_cnt(NVM_ENTRY *entries, u32 len);        // 统计缓存条目数
u32 nvm_write_cache(NVM_CACHE *cache, u16 id, u32 offset); // 更新 id→offset
u32 nvm_read_cache(NVM_CACHE *cache, u16 id);          // 按 id 查偏移
u32 nvm_clear_cache(NVM_CACHE *cache);                 // 清空缓存

Sources: new_vm.h、new_vm.h

说明: nvm_init/nvm_read/nvm_write 等库接口供库内部或深度定制场景使用,需自行管理 NEW_VM_OBJ;普通应用请使用 *_api 系列或统一 API。

vm_sfc 介质/中断接口(vm_sfc.h)

函数签名说明
flash_code_protect_callbacku32 (*)(u32 offset, u32 len)代码区写保护回调;返回非 0 表示该区间禁止 VM 写入
spi_cache_way_switchvoid (u8 way_num)切换 SPI 缓存方式,规避擦写与 cache 读冲突
vm_isr_response_list_registervoid (u32 bit_list)按 32 位位图注册擦写期间放开的多个中断
vm_isr_response_index_registervoid (u8 index)按索引注册单个放开中断(兼容旧程序写法)
vm_isr_response_index_unregistervoid (u8 index)取消单个中断的放开注册
get_vm_isr_response_index_h/lu32 (void)查询当前放开集合高/低 32 位

全局变量: volatile u8 vm_busy — 擦写忙标志,非零时禁止访问 Flash 上的 VM 区。

IOCTL: IOCTL_SET_VM_INFO(_IOW('V',1,1))向 SFC 驱动下发 VM 分区信息;IOCTL_GET_VM_INFO(_IOW('V',2,1))查询之。

Source: vm_sfc.h

失败模式、边界与并发

首次上电 / 参数未写入

vm_read 对不存在的 ID 返回非 0。应用必须对读取失败有默认值回退逻辑(典型模式见"使用示例"),否则会读到未初始化数据。

参数超长

new_vm 单条记录上限 NVM_MAX_LEN = 64 字节,超出即写入失败。设计时需保证每个 VM_INDEX 的写入长度恒定且 ≤ 64;若需保存大块数据(如歌词、EQ 曲线),应压缩或改存文件系统,VM 只保存指针/标志。

索引空间耗尽 / 顺序约束

VM_INDEX_MAX = 128 为硬上限;系统保留区(1~32)与 FM 频道连续区(VM_INDEX_FM_CHANNL 起 29 个)不可插入或改动,否则旧固件数据错位、FM 频道丢失。新增参数只能追加在既有项之后且不超过 128。

块写满与掉电

写入为"追加 + 位图"模式:当前块写满后由 nvm_format_another 擦除另一块再切换。若在擦除/切换中途掉电,库通过位图与 bool_block 恢复一致性;因此不要在 vm_busy 非零时断电或复位。预擦除(vm_pre_erase)把擦除提前到空闲时段,显著缩小掉电风险窗口。

擦写期间的中断/并发

  • vm_busy(volatile u8)是擦写忙标志,中断服务程序或 DMA 回调中应先检查再访问 VM 区;
  • Flash 擦写需要长时间占用总线,SDK 通过 vm_isr_response_list_register 放开音频等关键中断,避免关中断导致音频断流;放开集合可经 get_vm_isr_response_index_h/l 查询(AD14/15/17 仅低 32 位有效);
  • spi_cache_way_switch 解决 SPI cache 与擦写访问的冲突——擦写期间切换 cache 方式,防止读到陈旧数据。

缓存一致性

NVM_CACHE 的 rw_cnt 跟踪读写频率;当缓存条目失效(如块切换后 offset 变化)需调用 nvm_clear_cache 重建。nvm_write_cache 与位图更新必须成对进行,否则读缓存会命中过期 offset。

代码区保护

flash_code_protect_callback 用于拒绝 VM 写入落在代码区的地址区间。定制分区表时若 VM 区与代码区重叠,回调是最后一道防线,返回非 0 会中止写入。

性能与运维注意

  • new_vm 优于 old_vm 的核心:预擦除使 vm_write 关键路径只含页编程(μs 级),而 old_vm 写入常伴随擦除(ms 级)。对写入频繁的产品(音量、播放进度实时保存)应选 USE_NEW_VM。
  • 预擦除时机:在 UI 空闲、退出菜单、进入低功耗前调用 vm_pre_erase();不要在音频播放繁忙回调中调用,以免擦除占用总线造成干扰(配合中断放开机制使用)。
  • 分区规划:syscfg_vm_init(mem_addr, mem_size) 的地址/大小须与 Flash 分区表一致;双块布局要求 mem_size 至少为两块之和,且块大小与 Flash 擦除块(扇区)对齐。
  • 写放大与寿命:追加式写入每次 vm_write 只编程一条记录,位图每块只重写一次;相比原地改写,有效降低擦除次数、延长 Flash 寿命。
  • 固件升级兼容:VM_INDEX_FLASH_UPDATE 作为升级标志由升级流程读写;升级后系统保留区 ID 不变,参数可无缝迁移。

扩展点

  1. 新增参数:在 VM_INDEX 枚举末尾、VM_INDEX_MAX 之前追加 ID,保持既有项不动;配合 vm_read 默认值回退实现向前兼容。
  2. 更换后端:修改 SYS_MEMORY_SELECT 为 USE_NEW_VM/USE_OLD_VM 即可整体切换,无需改动业务代码;如需全新后端,可在 vm_api.c 增加 #elif 分支并实现四个 *_api 宏。
  3. 中断放开策略:通过 vm_isr_response_list_register/index_register 自定义擦写期间可响应的中断集合,适配不同音频/通信场景。
  4. 存储区定制:nvm_init(NEW_VM_OBJ*, addr, size) 与 nvm_buf_for_lib 回调允许深度定制对象布局与内部缓冲;普通应用不建议直接使用库接口。
  5. Flash 分区保护:实现/替换 flash_code_protect_callback 可对任意地址区间启用写保护。

相关链接

  • vm_api.h(索引与统一 API)
  • vm_api.c(适配层与后端切换)
  • new_vm.h(新版 VM 对象/缓存/接口定义)
  • old_vm.h(旧版 VM 接口定义)
  • vm_sfc.h(Flash 介质层与中断协调)
  • nvm_api.c(新版 VM 封装)
  • ovm_api.c(旧版 VM 封装)

说明:new_vm / old_vm 的算法实现以预编译库形式提供(如 sdk/include_lib/liba/ch58/voice_toy/new_vm_lib.a),头文件与封装文件是接口与行为约束的权威来源;本文档中涉及库内部细节的描述均以头文件定义为准。

Prev
存储设备驱动