杰理 SDK 文档中心
首页
首页
  • 入门指南

    • SDK 概述与芯片平台
    • 环境搭建与开发工具链
    • 编译、烧录与快速开始
  • 应用层开发

    • 语音玩具应用 voice_toy
    • 扩音器应用 voice_enhanced
    • 语音功能状态机 voice_func
    • 应用公共框架与配置
  • 音频子系统

    • 音频解码器与 MIDI 播放
    • 音频编码与录音
    • 音效算法(ANS、变调、变声、混响)
    • 音频输出、功放与硬件重采样
  • 存储与文件系统

    • 文件系统层(FAT、NOR_FS、SYDF 等)
    • 存储设备与设备管理
    • 参数存储 VM 与保留区
  • 系统机制

    • 消息与事件机制
    • 电源管理与低功耗
    • 固件升级机制
    • 外设驱动(按键、红外、SPI、USB)
    • 实时时钟与定时器
  • 构建系统与工具

    • 构建系统(Makefile 与 Code::Blocks)
    • 编译后处理与语音资源打包
  • 硬件平台与文档

    • 芯片平台与启动流程
    • 硬件文档、规格书与原理图

参数存储 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 / 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 定义

NEW_VM_OBJ 是 VM 运行时对象,字段含义:

字段含义
device底层存储设备句柄(SFC 设备)
cacheRAM 缓存描述符,指向 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_table / 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)

RTC 读写宏

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_enconst bool0是否允许单个 id 分多次读(读写长度不一致容错)
config_vm_erasure_after_format_enconst bool1格式整理后旧半区是否延迟擦除:1 等空闲擦(避免卡音,断电可能读到旧数据);0 立即擦(断电数据最新,但可能卡音)
NVM_MAX_LEN宏64单条目最大字节数
BIT_MAP宏32*16=512半区位图 bit 数,标记条目有效/占用状态
BIT_MAP_SIZE宏64位图字节数(512 bit / 8)
NVM_BUFF_SIZE宏128VM 工作缓冲字节数(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_apiu32 nvm_init_api(u32 addr, u32 size)打开 SFC 设备、初始化缓存并调用库 nvm_init
nvm_read_apiu32 nvm_read_api(u32 id, u8 *buf, u32 len)读取;成功返回 len
nvm_write_apiu32 nvm_write_api(u32 id, u8 *buf, u32 len)写入;成功返回 len
nvm_erasure_next_apivoid nvm_erasure_next_api(void)主循环空闲时调用,预擦除下一个区域
nvm_format_anotheri_apiu32 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),不在本页源码范围内。

Prev
存储设备与设备管理