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 子系统正是为此设计的轻量键值层。
核心设计思路:
- 按 ID 索引而非文件系统:
VM_INDEX枚举为每个参数分配固定 ID,vm_read(id)/vm_write(id)以 ID 为键,省去目录解析开销。 - 编译期选择实现:
SYS_MEMORY_SELECT决定链接 new_vm 或 old_vm 二进制库(new_vm_lib.a/old_vm_lib.a),应用代码无需改动。 - 双块交替 + 位图:新版 VM 将存储区划分为两个块,配合 512 位位图记录有效数据位置,写入时在空闲块追加,满了再擦除另一块(
nvm_format_another),实现擦写均衡并缩短单次写入等待。 - 预擦除:
vm_pre_erase()在系统空闲时提前擦除备用块,把最耗时的擦除操作移出关键路径。 - 中断释放: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_INFO | ioctl | _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_MAXdata_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); // 清空缓存
说明: nvm_init/nvm_read/nvm_write 等库接口供库内部或深度定制场景使用,需自行管理 NEW_VM_OBJ;普通应用请使用 *_api 系列或统一 API。
vm_sfc 介质/中断接口(vm_sfc.h)
| 函数 | 签名 | 说明 |
|---|---|---|
flash_code_protect_callback | u32 (*)(u32 offset, u32 len) | 代码区写保护回调;返回非 0 表示该区间禁止 VM 写入 |
spi_cache_way_switch | void (u8 way_num) | 切换 SPI 缓存方式,规避擦写与 cache 读冲突 |
vm_isr_response_list_register | void (u32 bit_list) | 按 32 位位图注册擦写期间放开的多个中断 |
vm_isr_response_index_register | void (u8 index) | 按索引注册单个放开中断(兼容旧程序写法) |
vm_isr_response_index_unregister | void (u8 index) | 取消单个中断的放开注册 |
get_vm_isr_response_index_h/l | u32 (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 不变,参数可无缝迁移。
扩展点
- 新增参数:在
VM_INDEX枚举末尾、VM_INDEX_MAX之前追加 ID,保持既有项不动;配合vm_read默认值回退实现向前兼容。 - 更换后端:修改
SYS_MEMORY_SELECT为USE_NEW_VM/USE_OLD_VM即可整体切换,无需改动业务代码;如需全新后端,可在vm_api.c增加#elif分支并实现四个*_api宏。 - 中断放开策略:通过
vm_isr_response_list_register/index_register自定义擦写期间可响应的中断集合,适配不同音频/通信场景。 - 存储区定制:
nvm_init(NEW_VM_OBJ*, addr, size)与nvm_buf_for_lib回调允许深度定制对象布局与内部缓冲;普通应用不建议直接使用库接口。 - 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),头文件与封装文件是接口与行为约束的权威来源;本文档中涉及库内部细节的描述均以头文件定义为准。