参数存储 VM 与保留区
本文介绍 AD24N SDK 中用于掉电参数存储的 VM(Volatile/参数存储)机制及其在 Flash 中预留的保留区设计,涵盖新版 VM(NEW_VM,支持预擦除)、旧版 VM(OLD_VM)、统一抽象层 vm_api.c、读写缓存、半区(double-block)格式整理与预擦除流程,以及 RTC、音量、音频校准等典型使用场景。
Purpose and Scope
本页聚焦"参数存储 VM 与保留区"这一完整能力,内容包括:
- VM 抽象层(
sdk/app/bsp/common/vm/vm_api.c):SYS_MEMORY_SELECT编译期分发机制,以及vm_read/vm_write/syscfg_vm_init等统一接口; - 新版 VM(
new_vm.h+nvm_api.c):半区式双块存储、位图管理、缓存(NVM_CACHE)、预擦除(nvm_erasure_next_api)与格式整理策略; - 旧版 VM(
vm.h):基于vm_hdl句柄的设备式接口、VM_ERR错误码与碎片整理(defrag); - 保留区:通过
syscfg_vm_init(addr, size)/nvm_init_api(addr, size)在 Flash 中划定的参数存储区域及其生命周期管理。
以下内容属于兄弟页面、不在本页展开:Flash 驱动的 SFC 命令细节、通用文件系统(如 FATFS/私有文件系统)的目录与文件管理、电源管理与低功耗流程。关于 VM 使用的完整说明,可参见 SDK 文档"VM 掉电存储详细说明"章节(nvm_api.c 注释中多次引用)。
Overview
在 MCU 类产品(如玩具、语音播放设备)中,音量、RTC 时间、播放进度、音频校准值等参数必须掉电不丢失。若每次直接写 Flash 的同一地址,会因 Flash 擦写寿命(通常 1 万~10 万次)和"先擦后写"的原子性问题导致器件过早损坏或掉电时数据损坏。VM(参数存储)机制正是为解决这些问题而设计的一层键值型(id → data)掉电存储抽象:
- 磨损均衡:数据不是固定在某个地址,而是在保留区内顺序滚动写入(append-only),配合位图标记有效条目,延长 Flash 寿命;
- 掉电安全:采用**双半区(double-block)**结构,写满后整理到另一半区,避免单次断电破坏全部数据;
- 缓存加速:最近读写过的条目在 RAM 缓存(
NVM_CACHE)中记录 id/偏移,减少 Flash 访问; - 预擦除:新版 VM 支持在系统空闲时预先擦除后续会用到的区域,把耗时的擦除操作从关键路径(如正在播放音乐时)中移走,避免卡音。
SDK 通过 SYS_MEMORY_SELECT 宏在编译期选择使用新版还是旧版 VM,两者对上层暴露完全相同的 vm_read / vm_write 接口,业务代码无需感知底层差异。
Architecture
flowchart TD
subgraph sg_App["应用层(业务/驱动)"]
RTC["rtc.c(RTC 参数)"]
TOY["toy_main.c(音量记忆)"]
TRIM["audio_power_trim.c(VBG 校准)"]
OTHER["其他 VM_INDEX_* 使用者"]
end
subgraph sg_Wrapper["VM 抽象层 vm_api.c"]
VMAPI["vm_read / vm_write<br/>syscfg_vm_init / vm_pre_erase"]
end
subgraph sg_Dispatch["实现选择:SYS_MEMORY_SELECT"]
NEW["新版 VM(new_vm.h + nvm_api.c)<br/>支持预擦除"]
OLD["旧版 VM(old_vm.h 库)"]
end
subgraph sg_Storage["存储设备层"]
SFC["SFC 设备(__SFC_NANE)"]
FLASH[("SPI NOR Flash<br/>参数保留区(addr, size)")]
end
RTC --> VMAPI
TOY --> VMAPI
TRIM --> VMAPI
OTHER --> VMAPI
VMAPI --> NEW
VMAPI --> OLD
NEW --> SFC
OLD --> SFC
SFC --> FLASH
各层职责
- 应用层:业务代码与驱动通过
VM_INDEX_*常量(见 vm.h 中的VM_INDEX_BUFF示例:VM_INDEX_SONG、VM_INDEX_ENG、VM_INDEX_VOL等)以键值方式读写参数,不关心存储地址; - 抽象层(vm_api.c):把底层两套实现统一映射为
vm_read_api/vm_write_api/syscfg_vm_init_api/vm_pre_erase_api四个宏; - 实现层:新版 VM 由
nvm_api.c(应用侧封装)与new_vm静态库(new_vm_lib.a,核心算法)组成;旧版 VM 由old_vm库实现; - 设备层:VM 通过
dev_open(__SFC_NANE, 0)打开 SFC 设备(见 nvm_api.c),最终操作 SPI NOR Flash 中由addr/size划定的保留区。
为何这样分层
分层让业务代码与 Flash 实现解耦:切换存储介质(NOR→NAND/EEPROM)或升级 VM 算法时,只需替换实现层并保持 vm_read / vm_write 语义不变。SYS_MEMORY_SELECT 的编译期选择(而非运行时判断)保证零额外开销,且允许在未启用 VM 时退化为返回 -1 的空实现。
新版 VM(NEW_VM)实现详解
新版 VM 是当前 SDK 的推荐实现(SYS_MEMORY_SELECT == USE_NEW_VM),其核心特点是双半区 + 位图 + RAM 缓存 + 预擦除。应用侧封装在 sdk/app/bsp/common/vm/new_vm/nvm_api.c,核心算法封装在 new_vm_lib.a 静态库中,对外暴露的头文件是 new_vm.h。
核心数据结构
typedef struct __nvm_entry {
u16 id;
u16 rw_cnt;
u32 offset;
} NVM_ENTRY;
typedef struct __nvm_cache {
u16 rw_cnt;
u16 number_entry;
NVM_ENTRY *entries;
} NVM_CACHE;
NVM_ENTRY 是缓存表项:id 为参数索引(即 VM_INDEX_*),rw_cnt 为读写计数,offset 为条目在 Flash 保留区中的偏移。NVM_CACHE 则是整张缓存表的描述符,number_entry 固定为 NVM_CACHE_NUMBER(6),entries 指向静态分配的 g_nvm_entry[6]。
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;
u16 pre_sec_b;
u16 id;
u16 offset;
} NEW_VM_OBJ;
NEW_VM_OBJ 是 VM 运行时对象,字段含义:
| 字段 | 含义 |
|---|---|
device | 底层存储设备句柄(SFC 设备) |
cache | RAM 缓存描述符,指向 NVM_CACHE |
addr | 保留区起始地址 |
bool_block(1 bit) | 当前活跃半区标记(双半区选择) |
block_size(24 bit) | 单个半区的大小 |
area_len | 整个保留区长度 |
w_offset | 当前写入游标(顺序滚动写的位置) |
pre_sec_a / pre_sec_b | 两个半区的预擦除标记 |
id / offset | 最近一次读写的条目索引与偏移(供调试/断点续读) |
该对象作为全局变量 g_nvm_obj 存在于 nvm_api.c,VM 库通过 nvm_buf_for_lib 回调取得 32 位对齐的工作缓冲 new_vm_buff[NVM_BUFF_SIZE / 4](NVM_BUFF_SIZE = NVM_MAX_LEN(64) + BIT_MAP_SIZE(64) = 128 字节),用于位图与单次读写暂存。
位图与条目长度约束
#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 字节
BIT_MAP(512 bit)用于标记每个条目在当前半区中的有效/占用状态,格式整理时依据位图决定哪些数据需要拷贝到新半区;NVM_MAX_LEN为单条目最大长度 64 字节。业务数据超过该长度需拆分或改用连续 id 区间(可参考VM_INDEX_BUFF中 68 字节数组的用法,但注意那是旧版 VM 的常量);- 工作缓冲同时容纳"最长条目 + 位图",保证一次读写不需要额外的动态内存。
初始化流程
u32 nvm_init_api(u32 addr, u32 size)
{
memset(&g_nvm_obj, 0, sizeof(NEW_VM_OBJ));
g_nvm_obj.device = dev_open(__SFC_NANE, 0);
if (NULL == g_nvm_obj.device) {
log_error("nvm init E_NVM_OPEN_DEVICE\n");
return E_NVM_OPEN_DEVICE;
}
#if NVM_CACHE_ENABLE
g_nvm_obj.cache = nvm_cache_init_api();
#endif
return nvm_init(&g_nvm_obj, addr, size);
}
初始化分三步:清零运行时对象 → 打开 SFC 设备(失败返回 E_NVM_OPEN_DEVICE)→ 初始化 RAM 缓存 → 调用库的 nvm_init 扫描保留区、重建位图与写游标。addr/size 即保留区在 Flash 中的位置与大小,通常由板级配置文件(config.h / app_config.h 中的 SYS_MEMORY_SELECT 配套宏)决定;本 SDK 中确切地址常量在板级配置中定义,未在本页源码范围内展开。
RAM 缓存机制
#define NVM_CACHE_ENABLE 1
#define NVM_CACHE_NUMBER 6
NVM_ENTRY g_nvm_entry[NVM_CACHE_NUMBER];
NVM_CACHE g_nvm_cache;
NVM_CACHE *nvm_cache_init_api(void)
{
memset(&g_nvm_entry[0], 0, sizeof(g_nvm_entry));
memset(&g_nvm_cache, 0, sizeof(g_nvm_cache));
g_nvm_cache.entries = &g_nvm_entry[0];
g_nvm_cache.number_entry = NVM_CACHE_NUMBER;
return &g_nvm_cache;
}
缓存采用全静态分配(g_nvm_entry[6]),无动态内存开销,符合 MCU 场景。nvm_write_cache / nvm_read_cache / nvm_clear_cache(见 new_vm.h)由库内部调用:写入时登记条目偏移,读取时先查缓存命中(nvm_read_cache 返回 offset),减少 Flash 扫描。当缓存表满时按 rw_cnt 淘汰最久未用的条目。
预擦除(Pre-Erase)与格式整理(Format/Defrag)
新版 VM 的格式整理采用双半区轮流使用策略:数据在一个半区内顺序滚动写入;当该半区写满时,调用 nvm_format_another 把位图标记的有效数据拷贝到另一半区并重建索引,随后旧半区整体擦除,成为新的空闲半区。这一设计保证任意时刻至少有一个半区保存着完整数据,掉电不丢。
void nvm_erasure_next_api(void)
{
nvm_pre_erasure_next(&g_nvm_obj, 1, 1);
}
nvm_erasure_next_api 封装库接口 nvm_pre_erasure_next(p_nvm, using_next, idle_next):参数 1, 1 表示"接下来将要使用的 1 个区域"和"另一个半区中低 1 个区域"都执行预擦除。其设计意图(源码注释原文):该函数在系统空闲时由主循环调用,把耗时的 Flash 擦除提前完成,已经擦过的区域不会被重复擦除。这样在音乐播放等实时场景中,写入路径只做"编程"而不做"擦除",避免卡音。
格式整理后的擦除时机由配置开关控制:
const bool config_vm_erasure_after_format_en = 1;
源码注释给出了两种典型场景的权衡:
- 不开启(=0):格式整理后立即擦除旧半区。代价是若系统在音乐播放中途触发整理,擦除操作会造成卡音;收益是断电后重启读到的一定是最新数据;
- 开启(=1,默认):整理后旧半区数据延迟到系统空闲时才擦除。代价是若整理后立即断电且从未进入空闲状态,重启会读到旧半区残留的旧数据;收益是擦除不再打断实时任务。
另一个配置开关 config_vm_multiple_read_en = 0 控制是否支持"单个 id 分多次读"(读写长度不一致的容错读取)。默认关闭,即读写长度必须一致。
flowchart TD
W["vm_write 写入条目"] --> F{"当前半区空间耗尽?"}
F -->|"否"| CONT["在当前半区顺序写入<br/>更新位图与缓存"]
F -->|"是"| FORMAT["nvm_format_another<br/>切换到另一半区并整理有效数据"]
FORMAT --> ERASE{"config_vm_erasure_after_format_en?"}
ERASE -->|"1(默认)"| DEFER["旧半区延迟擦除<br/>等系统空闲(避免卡音)"]
ERASE -->|"0"| IMM["立即擦除旧半区<br/>(断电后读到最新数据)"]
DEFER --> IDLE["主循环调用 nvm_erasure_next_api<br/>预擦除后续区域"]
IMM --> IDLE
IDLE --> W
新版 VM 的应用接口
nvm_api.c 提供以下应用层接口(见 new_vm.h):
| 接口 | 作用 |
|---|---|
nvm_init_api(addr, size) | 初始化 VM 并划定保留区 |
nvm_read_api(id, buf, len) | 按 id 读取;成功返回 len,失败返回错误码 |
nvm_write_api(id, buf, len) | 按 id 写入;成功返回 len,失败返回错误码 |
nvm_erasure_next_api() | 空闲时预擦除后续区域 |
nvm_format_anotheri_api() | 强制格式整理(切换到另一半区) |
读写封装对返回值做了归一化:库返回 0 表示成功,封装层转成 len 返回,上层(如 toy_main.c)通过 res == sizeof(vol) 判断读取成功。
旧版 VM(OLD_VM)
当 SYS_MEMORY_SELECT == USE_OLD_VM 时使用旧版 VM,接口定义在 vm.h。它采用句柄式设计:typedef u16 vm_hdl,应用先 vm_init 绑定设备与区域,再用 vm_write_phy(hdl, buf, len) / vm_read_phy(hdl, buf, len) 读写。
struct vm_table {
u16 index;
u16 value_byte;
int value; //cache value which value_byte <= 4
};
typedef enum _vm_err {
VM_ERR_NONE = 0,
VM_INDEX_ERR = -0x100,
VM_INDEX_EXIST,
VM_DATA_LEN_ERR,
VM_READ_NO_INDEX,
VM_READ_DATA_ERR,
VM_WRITE_OVERFLOW,
VM_NOT_INIT,
VM_INIT_ALREADY,
VM_DEFRAG_ERR,
VM_ERR_INIT,
VM_ERR_PROTECT
} VM_ERR;
旧版 VM 的关键设计:
- 小值缓存:
vm_table为长度 ≤ 4 字节的条目提供 RAM 缓存(value字段),减少对 Flash 的读访问; - 错误码语义:
VM_ERR枚举完整描述失败场景——索引不存在(VM_READ_NO_INDEX)、长度错误(VM_DATA_LEN_ERR)、写溢出(VM_WRITE_OVERFLOW)、未初始化/重复初始化、整理失败(VM_DEFRAG_ERR)、保护错误(VM_ERR_PROTECT)等,便于驱动层精确上报; - 碎片整理:
vm_defrag_line_set(u8 defrag_line)按百分比(0~100)设置触发整理的阈值;vm_get_area_using_info返回保留区总长与已用量,供业务监控剩余空间; - 批量接口:
vm_api_write_mult(start_id, end_id, buf, len, delay)/vm_api_read_mult支持连续 id 区间一次读写(用于歌曲索引等大块数据),delay参数用于分散写入耗时; - 物理层:
sfc_erase/sfc_write/sfc_read/sfc_erase_zone直接操作 SPI Flash 控制器,FLASH_ERASER枚举(CHIP/BLOCK/SECTOR/PAGE)决定擦除粒度。
旧版 VM 不支持预擦除(vm_pre_erase_api 为空宏,见 vm_api.c),因此其写路径可能包含整块擦除,实时性弱于新版 VM——这正是 SDK 引入新版 VM 的原因。
核心读写流程
以 toy_main.c 中读取音量为例,走一次完整的新版 VM 读路径:
sequenceDiagram
participant App as 应用(toy_main.c)
participant VmApi as vm_api.c(vm_read)
participant Nvm as new_vm 库(nvm_read)
participant Cache as NVM_CACHE(g_nvm_cache)
participant Sfc as SFC 设备(__SFC_NANE)
participant Flash as Flash 保留区
App->>VmApi: vm_read(VM_INDEX_VOL, &vol, sizeof(vol))
VmApi->>Nvm: nvm_read_api → nvm_read(&g_nvm_obj, id, buf, len)
Nvm->>Cache: nvm_read_cache 查缓存
alt 缓存命中
Cache-->>Nvm: 返回条目 offset
Nvm->>Sfc: 按 offset 读取数据
else 缓存未命中
Nvm->>Sfc: 扫描位图/顺序查找该 id
Sfc->>Flash: 读取条目
Nvm->>Cache: nvm_write_cache 登记 (id, offset)
end
Sfc-->>Nvm: 数据
Nvm-->>VmApi: 0(成功)
VmApi-->>App: len(归一化返回值)
写路径与之对称:vm_write → nvm_write_api → nvm_write,库内部先更新缓存与位图,再通过 SFC 把数据编程到 w_offset 指向的位置并推进写游标;若当前半区写满则触发 nvm_format_another 整理(见上文流程图)。RTC 驱动以宏封装了这套接口:
#define rtc_save_api(p0, p1, p2) vm_write(p0, p1, p2)
#define rtc_read_api(p0, p1, p2) vm_read(p0, p1, p2)
Usage Examples
示例一:读取音量参数(业务层标准读法)
u8 vol = 0;
u32 res = vm_read(VM_INDEX_VOL, &vol, sizeof(vol));
if ((vol <= 31) && (res == sizeof(vol))) {
// 读取成功且音量值合法(0~31),应用该音量
}
来源:sdk/app/src/voice_func/toy_main.c
这段代码展示了 VM 的典型使用契约:返回值 res 等于请求长度 sizeof(vol) 表示读取成功;随后还需对数据本身做业务合法性校验(vol <= 31),因为保留区中可能残留未初始化数据。
示例二:读取音频校准值(驱动内部用法)
u32 rlen = 0;
rlen = vm_read(VM_INDEX_AUDIO_VBG_TRIM, &g_adda_high_voltage_vbg.gear, sizeof(g_adda_high_voltage_vbg.gear));
if (rlen == sizeof(g_adda_high_voltage_vbg.gear)) {
// 校准值有效,直接使用
}
来源:sdk/app/bsp/cpu/sh58/audio_power_trim.c
驱动在启动时读取产线校准参数(VBG 档位),同样以"返回长度 == 请求长度"作为有效判据。可见 vm_read 语义在业务与驱动层保持一致。
示例三:驱动内部专用读写接口
// 驱动内部使用接口
int syscfg_write(u16 item_id, const void *buf, u16 len)
{
return vm_write_api(item_id, (u8 *)buf, len);
}
// 驱动内部使用接口
int syscfg_read(u16 item_id, void *buf, u16 len)
{
return vm_read_api(item_id, buf, len);
}
来源:sdk/app/bsp/common/vm/vm_api.c
syscfg_read / syscfg_write 是仅供驱动内部调用的直通接口,跳过 vm_read / vm_write 的外层包装,直接进入所选实现的 vm_*_api 宏。
示例四:新版 VM 压力自测(源码内置测试)
err = nvm_write_api(1, nvm_demo_buf, sizeof(nvm_demo_buf));
if (0 != err) {
log_info("nvm_write_api err : 0x%x", err);
break;
}
memset(nvm_demo_buf, 0, sizeof(nvm_demo_buf));
nvm_read_api(1, nvm_demo_buf, sizeof(nvm_demo_buf));
err = memcmp(nvm_demo_buf, nvm_test_buf, sizeof(nvm_demo_buf));
if (0 != err) {
log_info("save buf :");
log_info_hexdump(nvm_test_buf, sizeof(nvm_demo_buf));
log_info("read buf :");
log_info_hexdump(nvm_demo_buf, sizeof(nvm_demo_buf));
break;
}
来源:sdk/app/bsp/common/vm/new_vm/nvm_api.c(#if 0 测试代码)
nvm_api.c 内置了一段 #if 0 关闭的验证代码:随机数据写 → 读回 → memcmp 比对,循环 2046 次并喂狗(wdt_clear)。它既是新库的冒烟测试,也示范了如何直接调用 nvm_write_api / nvm_read_api 进行底层验证。另一个测试函数 test_nvm_0 展示了多 id 随机长度写读,并在每次写入前调用 nvm_erasure_next_api() 模拟空闲期预擦除(nvm_api.c#L181-L240)。
Configuration Options
以下配置项均可在源码中直接找到;除 SYS_MEMORY_SELECT 在板级配置(config.h / app_config.h)中定义外,其余位于 nvm_api.c / new_vm.h。
| 配置项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
SYS_MEMORY_SELECT | 宏 | USE_NEW_VM | 选择 VM 实现:USE_NEW_VM(新版,支持预擦除)/ USE_OLD_VM(旧版)/ 其他(空实现,所有接口返回 -1)。见 vm_api.c#L9-L31 |
NVM_CACHE_ENABLE | 宏 | 1 | 是否启用 RAM 缓存(NVM_ENTRY 表) |
NVM_CACHE_NUMBER | 宏 | 6 | 缓存表项数,决定 RAM 缓存可登记的条目上限 |
config_vm_multiple_read_en | const bool | 0 | 是否允许单个 id 分多次读(读写长度不一致容错) |
config_vm_erasure_after_format_en | const bool | 1 | 格式整理后旧半区是否延迟擦除:1 等空闲擦(避免卡音,断电可能读到旧数据);0 立即擦(断电数据最新,但可能卡音) |
NVM_MAX_LEN | 宏 | 64 | 单条目最大字节数 |
BIT_MAP | 宏 | 32*16=512 | 半区位图 bit 数,标记条目有效/占用状态 |
BIT_MAP_SIZE | 宏 | 64 | 位图字节数(512 bit / 8) |
NVM_BUFF_SIZE | 宏 | 128 | VM 工作缓冲字节数(NVM_MAX_LEN + BIT_MAP_SIZE),需 4 字节对齐 |
API Reference
统一抽象层(vm_api.c)
int syscfg_vm_init(u32 mem_addr, u32 mem_size)
初始化 VM 并划定 Flash 保留区(mem_addr 起始地址、mem_size 长度)。
- Returns: 底层实现返回值;新版返回
nvm_init结果(0 成功),未启用时返回-1。
int vm_read(u32 id, u8 *data_buf, u16 len)
按参数 id 读取数据。
- 参数:
id(VM_INDEX_*常量);data_buf输出缓冲;len期望长度。 - Returns: 成功返回实际读取长度;失败返回错误码(未启用时恒为
-1)。
int vm_write(u32 id, u8 *data_buf, u16 len)
按参数 id 写入数据。
- 参数: 同
vm_read。 - Returns: 成功返回写入长度;失败返回错误码。
void vm_pre_erase(void)
空闲时预擦除后续区域(仅新版 VM 有效;旧版为空操作)。
int syscfg_read(u16 item_id, void *buf, u16 len) / int syscfg_write(u16 item_id, const void *buf, u16 len)
驱动内部直通接口,绕过外层包装直接调用底层实现。
新版 VM(new_vm.h)
| 接口 | 签名 | 说明 |
|---|---|---|
nvm_init_api | u32 nvm_init_api(u32 addr, u32 size) | 打开 SFC 设备、初始化缓存并调用库 nvm_init |
nvm_read_api | u32 nvm_read_api(u32 id, u8 *buf, u32 len) | 读取;成功返回 len |
nvm_write_api | u32 nvm_write_api(u32 id, u8 *buf, u32 len) | 写入;成功返回 len |
nvm_erasure_next_api | void nvm_erasure_next_api(void) | 主循环空闲时调用,预擦除下一个区域 |
nvm_format_anotheri_api | u32 nvm_format_anotheri_api(void) | 强制格式整理(切换到另一半区) |
| 库接口 | u32 nvm_init(NEW_VM_OBJ*, u32 addr, u32 size) 等 | new_vm_lib.a 内部核心算法:nvm_read / nvm_write / nvm_format_another / nvm_pre_erasure_next |
| 回调 | void *nvm_buf_for_lib(NEW_VM_OBJ*, u32 *p_len) | 向库提供 128 字节 32 位对齐工作缓冲 |
旧版 VM(vm.h)
| 接口 | 说明 |
|---|---|
VM_ERR vm_init(void *dev_hdl, u32 vm_addr, u32 vm_len, u8 vm_mode) | 绑定设备与保留区,vm_mode 选择存储模式 |
VM_ERR vm_eraser(void) | 整区擦除 |
void vm_check_all(u8 level) | 全量校验(level 默认 0) |
void vm_defrag_line_set(u8 defrag_line) | 设置整理阈值(百分比 0~100) |
int vm_get_area_using_info(u32 *area_len, u32 *used_len) | 查询保留区总长与已用量 |
s32 vm_write_phy(vm_hdl hdl, u8 *data_buf, u16 len) / s32 vm_read_phy(vm_hdl hdl, u8 *data_buf, u16 len) | 句柄式物理读写 |
void vm_api_write_mult(u16 start_id, u16 end_id, void *buf, u16 len, u32 delay) / int vm_api_read_mult(...) | 连续 id 区间批量读写 |
VM_ERR syscfg_vm_init_phy(u32 eeprom_saddr, u32 eeprom_size) | 基于 EEPROM 地址的初始化 |
bool sfc_erase(FLASH_ERASER cmd, u32 addr) / u32 sfc_write(...) / u32 sfc_read(...) / bool sfc_erase_zone(u32 addr, u32 len) | SPI Flash 物理层操作 |
Failure Modes, Edge Cases & Concurrency
- 断电写中断:双半区设计保证任意时刻有一个半区完整有效;整理(
nvm_format_another)期间断电由位图与bool_block标记恢复。但若config_vm_erasure_after_format_en = 1且整理后从未进入空闲期就断电,重启可能读到旧半区残留数据(源码注释明确提示此场景)。 - 条目长度超限:新版 VM 单条目最大
NVM_MAX_LEN(64 字节),超长写入会返回错误;旧版 VM 对超长/溢出返回VM_DATA_LEN_ERR/VM_WRITE_OVERFLOW。config_vm_multiple_read_en = 0时读写长度必须一致。 - 非法/未初始化数据:
vm_read即使返回成功,数据也可能为保留区残留值,业务必须自行校验(如toy_main.c的vol <= 31)。 - 设备打开失败:
nvm_init_api中dev_open(__SFC_NANE, 0)失败返回E_NVM_OPEN_DEVICE,VM 不可用,调用方应检查初始化返回值。 - 并发与临界区:
g_nvm_obj、g_nvm_cache、g_nvm_entry均为全局静态对象,未发现加锁;nvm_erasure_next_api明确设计为主循环空闲时调用,写操作不应与预擦除并发(例如中断/多任务中调用vm_write需自行保证互斥)。 - 写游标与空间耗尽:顺序滚动写意味着需要周期性整理;旧版 VM 提供
vm_defrag_line_set阈值与vm_get_area_using_info监控,新版 VM 则在半区写满时自动整理。
Performance & Operational Notes
- 预擦除把耗时移出关键路径:新版 VM 的核心收益——Flash 擦除(毫秒级)提前到空闲期完成,写入路径仅剩编程(微秒级),避免播放音乐等实时场景卡顿。
- 磨损均衡:顺序滚动写 + 双半区轮流使用,使保留区各扇区磨损均匀,显著延长 Flash 寿命;但单条目频繁写入(如音量调节)仍会加速该 id 区域的滚动,可考虑降低写入频率。
- RAM 开销:工作缓冲 128 字节(4 字节对齐)+ 缓存表 6×8 字节,全部静态分配,无 malloc,适合 MCU 内存受限场景。
- 启动开销:
nvm_init需扫描保留区重建位图与写游标,保留区越大启动耗时越长;vm_check_all(旧版)为可选的全量校验,默认不启用。
Extension Points
- 新增参数条目:在
VM_INDEX_*命名空间中分配新 id(参考VM_INDEX_BUFF中VM_INDEX_SONG/VM_INDEX_ENG/VM_INDEX_POETRY/VM_INDEX_STORY/VM_INDEX_EXT_SONG/VM_INDEX_VOL等已有用法,见 vm.h#L40-L48),业务层直接调vm_read/vm_write。 - 驱动内部参数:使用
syscfg_read/syscfg_write直通接口(RTC、音频校准等驱动即此模式)。 - 切换实现:改
SYS_MEMORY_SELECT即可在USE_NEW_VM/USE_OLD_VM间切换;新版不支持预擦除时业务无需改动,因为vm_pre_erase在旧版下为空宏。 - 空闲挂钩:在系统主循环空闲处调用
vm_pre_erase()(新版),即可获得预擦除收益;测试代码中nvm_erasure_next_api()的调用方式可作为参考。
Related Links
- vm_api.c(统一抽象层)
- new_vm.h(新版 VM 结构与接口)
- nvm_api.c(新版 VM 应用封装)
- vm.h(旧版 VM 接口与错误码)
- rtc.c(VM 在 RTC 驱动中的应用)
- toy_main.c(音量参数读写示例)
- audio_power_trim.c(VBG 校准参数读取)
相关兄弟主题:Flash 底层驱动与 SFC 命令、文件系统页、电源管理页。VM 保留区的具体地址/大小常量定义于板级配置(
config.h/app_config.h),不在本页源码范围内。