NOR Flash 与虚拟机存储
本页介绍 AD23N 平台 SDK 中 NOR Flash 底层驱动与虚拟机(VM,Virtual Machine)键值存储的完整体系:从应用层通过 vm_read/vm_write 读写参数,到 VM 索引空间设计,再到 NOR Flash 驱动、SPI 命令集、互斥锁与平台配置的端到端工作机制。
Purpose and Scope
本页覆盖以下内容:
- 虚拟机存储层(VM):
sdk/app/bsp/common/vm/vm_api.h/vm_api.c定义的参数持久化接口(syscfg_vm_init、vm_read、vm_write、vm_pre_erase、syscfg_read、syscfg_write)以及VM_INDEX逻辑索引空间的设计规则;新版虚拟机接口sdk/app/bsp/common/vm/new_vm/nvm_api.c的存在与定位。 - NOR Flash 驱动层:
sdk/app/bsp/common/norflash/norflash.h/norflash.c定义的 Winbond WX25X 命令集、擦除粒度枚举、平台数据(SPI 通道/CS 引脚/分区配置)、设备操作表与无操作系统模式下的flash_mutex互斥机制。 - 初始化参数区:
ini_area_norflash.c所承担的启动参数区功能(按文件定位说明,非本页重点)。
以下内容属于其它页面,本页不展开:
- 文件系统式访问:
norfs_dev_ops(NOR 文件系统)与上层文件系统/多媒体播放器的对接细节。 - 具体业务参数的语义(如 FM 频道表、断点续播数据结构)属于各自业务模块的页面。
资料说明:本文全部接口契约、索引定义、命令字与配置结构均直接取自已核实的头文件源码;
vm_api.c、nvm_api.c、norflash.c的内部算法(如磨损均衡、掉电保护)在可验证范围内说明,未逐行展开的部分会明确标注。
Overview
为什么需要"虚拟机"存储
NOR Flash 物理特性决定了它只能把 1 写成 0,写入前必须先擦除(把 0 恢复为 1),且擦除以扇区(Sector)或块(Block)为最小单位;同时 Flash 存在擦写寿命限制。如果应用每次改一个音量值就直接擦整个扇区,会迅速耗尽寿命。
VM 层正是为解决这个问题而设计:它把逻辑 ID(如 VM_INDEX_VOL 音量)映射到物理存储区,由 VM 管理器统一承担"查表定位 → 擦除 → 编程"的流程,并向应用层暴露极简的 vm_read(id, buf, len) / vm_write(id, buf, len) 接口。应用只需关心"我要持久化哪个 ID 的数据",无需关心它在 Flash 中的物理位置。
系统级与用户级 ID 的划分
VM_INDEX 枚举(vm_api.h)是整个 VM 存储的"地址空间"。它被明确划分为两段:
- 系统保留段(ID 0~32):由 SDK 内部使用,顺序不可修改,包括 RTC 累计/上次计数、闹钟时间、PMU 电压检测、Audio VBG 校准、PLL LDO 配置等。因为系统库可能按固定偏移访问这些 ID,任何顺序调整都会破坏兼容性。
- 用户段(ID > 32):应用可自由添加/调整顺序,例如歌曲索引、音量、断点(BP)、mbox 各设备(U 盘/SD/内外部 Flash)的断点与索引、FM 频率与频道表、当前活跃设备等。
其中 VM_INDEX_FM_CHANNL 到 VM_INDEX_FM_CHANNL_END 之间是连续预留的 28 个频道槽位,头文件注释明确要求"其间不能插入其它 INDEX"——这是为 FM 频道表按固定偏移连续存储而保留的连续地址空间。
Architecture
flowchart TD
subgraph sg_App["应用层 (App)"]
App["应用程序<br/>(播放/设置/设备管理/RTC)"]
end
subgraph sg_VM["虚拟机存储层 (VM)"]
VM_API["vm_api.c<br/>vm_read / vm_write"]
NVM_API["new_vm/nvm_api.c<br/>新版 VM 接口"]
VM_INDEX["VM_INDEX 索引表<br/>(vm_api.h)"]
SYS_INIT["syscfg_vm_init<br/>(内存区注册)"]
end
subgraph sg_Dev["设备管理框架 (dev_mg)"]
DEV_MGR["device.h 设备管理"]
end
subgraph sg_NorFlash["NOR Flash 驱动层 (norflash)"]
NF_DRV["norflash.c<br/>norflash_dev_ops"]
NF_FS["norfs_dev_ops<br/>(文件系统式访问)"]
PLATFORM["norflash_dev_platform_data<br/>(SPI/CS/分区配置)"]
MUTEX["flash_mutex 互斥锁"]
end
subgraph sg_HW["硬件层"]
SPI["SPI1 / SPI2 控制器"]
FLASH[("NOR Flash 芯片<br/>(Winbond WX25X 系列)")]
end
App -->|"vm_read/vm_write"| VM_API
App --> NVM_API
VM_API --> VM_INDEX
VM_API -->|"设备写/读接口"| DEV_MGR
NVM_API --> DEV_MGR
SYS_INIT --> VM_API
DEV_MGR -->|"打开 norflash 设备"| NF_DRV
NF_DRV -->|"互斥保护"| MUTEX
NF_DRV -->|"平台参数"| PLATFORM
NF_DRV -->|"SPI 传输"| SPI
NF_FS --> SPI
SPI --> FLASH
架构说明
- VM 层是"逻辑地址"入口:
vm_api.h中VM_INDEX定义逻辑 ID 空间;vm_api.c把id映射到物理存储。vm_api.h引入了dev_mg/device.h,说明 VM 层通过设备管理框架访问底层设备,而不是直接操作寄存器——这是 SDK 统一设备抽象(device_operations)的体现。 - 驱动层是"物理地址"出口:
norflash.c导出norflash_dev_ops(块设备式操作)与norfs_dev_ops(文件系统式操作)两张操作表(norflash.h)。norflash_dev_platform_data决定驱动挂在哪个 SPI、用哪个 CS、读多宽、分区从哪里开始、多大。 - 并发保护在驱动内:
NORFLASH_NO_SYS为 1 时(不使用操作系统),驱动用自旋式flash_mutex加锁,并在等待期间喂狗(wdt_clear)防止看门狗复位——这是裸机环境下保护 Flash 关键时序的典型手法。 - 新版 VM(new_vm):
nvm_api.c与旧版vm_api.c并存于vm/new_vm/目录,供新项目迁移使用,两者共用同一套 NOR Flash 驱动。
核心设计:VM 索引空间
VM 存储的核心抽象是把"持久化项"抽象为 (id, data, len) 三元组。id 即 VM_INDEX 枚举值,data 为数据缓冲,len 为长度(u16,单次最大 65535 字节)。索引空间的划分规则如下:
| 区间 | ID 范围 | 归属 | 约束 |
|---|---|---|---|
| 系统保留 | 0 ~ 32 | SDK 内部库(RTC/PMU/AUDIO/PLL) | 顺序不可修改、不可删除 |
| 用户自由区 | 33 起 | 应用(歌曲/音量/断点/mbox/FM) | 可自由增删调整 |
| FM 频道槽 | VM_INDEX_FM_CHANNL ~ +28 | FM 应用 | 连续预留,其间不得插入其它 ID |
typedef enum {
VM_INDEX_DEMO = 0,
//================系统内部 ,预留32个id,不可修改顺序===========================//
/*RTC*/
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,
/*PMU VOLTAGE*/
LIB_VM_PMU_VOLTAGE = 5, //电压检测
/*AUDIO_VBG*/
VM_INDEX_AUDIO_VBG_TRIM = 6, //AUDIO_VBG
/*PLL_LDO*/
LIB_VM_PLL_LDO = 7,
LIB_SYSMEM_END = 32,
// 系统lib使用结束
//================================================================================//
//================================================================================//
// 以下用户可以任意修改顺序或添加
VM_INDEX_SONG,
VM_INDEX_ENG,
VM_INDEX_POETRY,
VM_INDEX_STORY,
VM_INDEX_F1X,
VM_INDEX_EXT_SONG,
VM_INDEX_VOL,
...
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_AUTO_BP,
} VM_INDEX;
Source: vm_api.h
设计意图:把系统库依赖的 ID 与用户 ID 物理隔离,使 SDK 升级不会破坏用户已有数据布局;同时保留 0~32 的固定偏移语义,让系统库可以安全地按序号访问。用户段不固定编号(自动递增),因此用户新增/删除枚举项不会引起系统段移位。
VM 接口契约
vm_api.h 暴露的公共接口非常精简——这正是 VM 层设计的核心价值:对上层隐藏一切 Flash 细节。应用只需初始化一次、之后按 ID 读写即可。
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);
// 驱动内部使用接口
int syscfg_write(u16 item_id, const void *buf, u16 len);
int syscfg_read(u16 item_id, void *buf, u16 len);
Source: vm_api.h
各接口的语义与设计要点:
| 接口 | 签名 | 职责 | 说明 |
|---|---|---|---|
syscfg_vm_init | int (u32 mem_addr, u32 mem_size) | 初始化 VM 系统 | 在指定内存区(mem_addr/mem_size)建立 VM 运行时上下文,通常在系统启动早期调用一次 |
vm_read | int (u32 id, u8 *data_buf, u16 len) | 读取持久化项 | 按 id 查表定位并拷贝 len 字节到 data_buf;返回值为读取结果 |
vm_write | int (u32 id, u8 *data_buf, u16 len) | 写入持久化项 | 按 id 定位存储区,必要时触发擦除后编程;返回值为写入结果 |
vm_pre_erase | void (void) | 预擦除 | 在已知即将大量写入的场景(如升级、批量写参数)提前做擦除,把擦除耗时从关键路径移出 |
syscfg_read / syscfg_write | int (u16 item_id, const void *buf, u16 len) | 驱动内部读写 | 头文件明确标注"驱动内部使用接口",供 VM 底层实现调用,应用不应直接使用 |
注意 vm_read/vm_write 的 id 是 u32,而 syscfg_read/syscfg_write 的 item_id 是 u16——两者服务于不同层级:前者是应用可见的逻辑 ID(VM_INDEX),后者是驱动内部更细粒度的条目 ID,两者之间的映射关系由 vm_api.c 内部完成(该映射的具体实现位于 sdk/app/bsp/common/vm/vm_api.c,本页仅核验到接口层)。
NOR Flash 驱动层
Winbond WX25X 命令集
驱动针对 Winbond WX25X 系列定义了完整的 SPI 命令字(norflash.h):
#define WINBOND_WRITE_ENABLE 0x06
#define WINBOND_READ_SR1 0x05
#define WINBOND_READ_SR2 0x35
#define WINBOND_WRITE_SR1 0x01
#define WINBOND_WRITE_SR2 0x31
#define WINBOND_READ_DATA 0x03
#define WINBOND_FAST_READ_DATA 0x0b
#define WINBOND_FAST_READ_DUAL_OUTPUT 0x3b
#define WINBOND_PAGE_PROGRAM 0x02
#define WINBOND_PAGE_ERASE 0x81
#define WINBOND_SECTOR_ERASE 0x20
#define WINBOND_BLOCK_ERASE 0xD8
#define WINBOND_CHIP_ERASE 0xC7
#define WINBOND_JEDEC_ID 0x9F
#define WINBOND_POWER_DOWN 0xB9
#define WINBOND_RELEASE_POWER_DOWN 0xAB
Source: norflash.h
按用途可归纳为四组:
- 状态/控制:
WRITE_ENABLE(0x06)(每次编程/擦除前必须写使能)、READ_SR1/SR2(0x05/0x35)(轮询忙标志 WIP)、WRITE_SR1/SR2(配置状态寄存器)。 - 读取:
READ_DATA(0x03)、FAST_READ(0x0B)、FAST_READ_DUAL_OUTPUT(0x3B)——普通读、快速读、双线输出读,配合平台配置中的spi_read_width选择线宽。 - 编程:
PAGE_PROGRAM(0x02)——页编程,是唯一的写入命令。 - 擦除:
PAGE_ERASE(0x81)、SECTOR_ERASE(0x20)、BLOCK_ERASE(0xD8)、CHIP_ERASE(0xC7)——四级擦除粒度。 - 其它:
JEDEC_ID(0x9F)用于上电识别芯片厂商/型号;POWER_DOWN/B9与RELEASE_POWER_DOWN/AB用于低功耗管理(对应_norflash_power_down()/_norflash_release_power_down())。
擦除粒度枚举
enum {
FLASH_PAGE_ERASER,
FLASH_SECTOR_ERASER,
FLASH_BLOCK_ERASER,
FLASH_CHIP_ERASER,
};
Source: norflash.h
该枚举把擦除能力抽象为四种粒度,驱动内部根据操作范围选择最合适的擦除命令。设计意图:页擦除(0x81)粒度最小、耗时最短但覆盖范围有限;块擦除(0xD8)覆盖大范围、单次命令开销低。VM 层在擦除策略上(例如只擦脏扇区、延迟合并等)会依赖驱动提供的这一粒度选择能力,以摊薄擦写寿命损耗。
平台数据:驱动与硬件的绑定点
struct norflash_dev_platform_data {
s8 spi_hw_num; //只支持SPI1或SPI2
u8 spi_cs_port; //cs的引脚
u8 spi_read_width; //flash读数据的线宽
const struct spi_platform_data *spi_pdata;
u32 start_addr; //分区起始地址
u32 size; //分区大小,若只有1个分区,则这个参数可以忽略
};
Source: norflash.h
这是驱动实例化时的关键配置结构,头文件还提供了构造宏 NORFLASH_DEV_PLATFORM_DATA_BEGIN(data) / NORFLASH_DEV_PLATFORM_DATA_END(),用于在板级配置文件中以结构化方式声明设备。字段含义:
spi_hw_num:仅支持 SPI1 或 SPI2(s8),决定挂接的硬件 SPI 控制器。spi_cs_port:片选引脚,驱动通过 GPIO 控制 CS。spi_read_width:读数据线宽(1/2/4 线),对应命令集里的FAST_READ_DUAL_OUTPUT等扩展读模式。spi_pdata:SPI 控制器自身的平台配置(时钟、模式等)。start_addr/size:分区起始地址与大小——这是 NOR Flash 分区的物理基础,VM 存储区、文件系统区、OTA 区都通过该结构划分地址边界。
核心流程:一次参数写入的完整路径
以 vm_write(VM_INDEX_VOL, buf, len) 为例,展示从应用到 Flash 芯片的完整调用链。其中底层擦除/编程命令序列是 NOR Flash 的硬件要求(先写使能、再擦除、后编程),依据 norflash.h 命令集还原:
sequenceDiagram
participant App as 应用代码
participant VM as vm_api.c (VM 管理器)
participant DM as 设备管理框架 (dev_mg)
participant NF as norflash.c (设备驱动)
participant SPI as SPI 控制器
participant F as NOR Flash 芯片
App->>VM: vm_write(VM_INDEX_VOL, buf, len)
activate VM
VM->>VM: 按 VM_INDEX 查表定位 item 存储区
VM->>DM: 打开 norflash 设备 / 写接口
activate DM
DM->>NF: 调用 norflash_dev_ops 写操作
activate NF
NF->>NF: flash_mutex_pend 加锁 (死等/超时)
NF->>SPI: WRITE_ENABLE (0x06)
SPI->>F: 写使能命令
NF->>SPI: SECTOR_ERASE (0x20) 擦除目标扇区
SPI->>F: 擦除命令
NF->>SPI: 轮询 READ_SR1 (0x05) 等 WIP 清除
SPI->>F: 状态寄存器读取
NF->>SPI: PAGE_PROGRAM (0x02) 写入数据
SPI->>F: 页编程命令 + 数据
NF->>NF: flash_mutex_post 解锁
NF-->>DM: 返回写入结果
deactivate NF
DM-->>VM: 返回结果
deactivate DM
VM-->>App: 返回 len / 错误码
deactivate VM
关键时序点说明:
- 先查表后写:VM 层把逻辑
id转换为物理地址/条目位置——这是"虚拟机"名称的由来:逻辑地址与物理地址解耦,允许 VM 内部做重定位与磨损管理。 - 写使能是前置条件:NOR Flash 规定任何擦除/编程操作前必须先发
WRITE_ENABLE(0x06),否则命令被忽略;驱动每次操作都重新使能,避免遗留状态。 - 擦除与编程分离:编程(
PAGE_PROGRAM)只能把 1 变 0,因此覆盖已有数据必须先擦除(SECTOR_ERASE)。vm_pre_erase()的存在意味着 VM 允许把"擦除"从写路径中提前——例如升级时预擦除目标区,避免运行时擦除造成长时间阻塞。 - 忙检测:擦除/编程期间芯片置 WIP 位,驱动通过
READ_SR1(0x05)轮询直至完成,期间在裸机模式下由wdt_clear()喂狗,防止长时间轮询触发看门狗复位。
互斥与并发:裸机下的临界区保护
norflash.h 在 NORFLASH_NO_SYS == 1(不使用操作系统)时,用软件信号量方式实现互斥(norflash.h):
typedef volatile u8 flash_mutex;
static inline void flash_mutex_create(flash_mutex *sem, u8 count)
{
*sem = count;
}
static inline void flash_mutex_post(flash_mutex *sem)
{
(*sem) = 1;
}
static inline s8 flash_mutex_pend(flash_mutex *sem, u32 timeout)
{
u32 _timeout = timeout + jiffies;
extern void wdt_clear();
while (1) {
if (*sem) {
(*sem) = 0;
break;
}
if ((timeout != 0) && (_timeout < jiffies)) {
return -1;
}
wdt_clear();
}
return 0;
}
Source: norflash.h
设计要点:
flash_mutex_pend(sem, timeout):timeout == 0时死等;非 0 时基于jiffies计算超时,超时返回-1,避免因持锁方异常导致永久卡死。- 等待期间喂狗:循环内调用
wdt_clear(),因为擦除/编程可能持续数十毫秒,裸机中断上下文无法靠任务调度让步,只能主动喂狗防复位。 query_flash_mutex_pend:提供"查一次即返回"的非阻塞变体,适合中断上下文或无需等待的场合。volatile u8信号量:在单核裸机场景下,关中断/原子访问语义由volatile保证读改写;NORFLASH_NO_SYS == 0时则走操作系统互斥路径(#else分支)。
使用示例
示例 1:系统启动时初始化 VM 并读取参数
典型流程是:启动早期调用 syscfg_vm_init 指定 VM 工作内存,之后即可用 vm_read 读取各业务参数:
// 系统启动初始化
syscfg_vm_init((u32)vm_buf, sizeof(vm_buf));
// 读取音量(假设先前已写入)
u8 vol = 0;
if (vm_read(VM_INDEX_VOL, &vol, sizeof(vol)) == 0) {
// 读取成功,vol 为持久化的音量
}
(接口签名见 vm_api.h;具体调用方式与缓冲区分配以 vm_api.c 中实现为准。)
示例 2:写入参数并配合预擦除
// 批量更新参数前预擦除,把擦除耗时移出写路径
vm_pre_erase();
u8 new_vol = 10;
vm_write(VM_INDEX_VOL, &new_vol, sizeof(new_vol));
// 记录当前活跃设备(mbox)
u8 dev = VM_INDEX_ACTIVE_DEV; // 示例值,实际为设备枚举
vm_write(VM_INDEX_ACTIVE_DEV, &dev, sizeof(dev));
(VM_INDEX_ACTIVE_DEV、VM_INDEX_VOL 等 ID 定义见 vm_api.h。)
示例 3:板级配置中声明 NOR Flash 平台数据
NORFLASH_DEV_PLATFORM_DATA_BEGIN(norflash0_data)
.spi_hw_num = 1, // 使用 SPI1
.spi_cs_port = FLASH_CS_GPIO, // CS 引脚
.spi_read_width = 1, // 单线读
.spi_pdata = &spi1_data,
.start_addr = 0x0, // 分区起始
.size = 0x200000, // 分区大小(示例)
NORFLASH_DEV_PLATFORM_DATA_END()
Source(结构定义与宏): norflash.h
配置选项
VM 层配置
| 配置项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
syscfg_vm_init(mem_addr, mem_size) | 函数参数 | 无(由调用方指定) | VM 运行时内存区地址与大小,启动早期必须调用一次 |
VM_INDEX 枚举项 | 枚举 | 见 vm_api.h | 逻辑 ID 空间;系统段 0~32 不可改序,用户段可自由扩展 |
NOR Flash 驱动层配置
| 配置项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
EXT_FLASH_EN | 宏(来自 app_config.h) | 未定义 | 为 1 时使能 TCFG_NORFLASH_DEV_ENABLE,即启用外挂 NOR Flash |
TCFG_NORFLASH_DEV_ENABLE | 宏 | 未定义 | "启动 nor flash"开关(norflash.h) |
NORFLASH_NO_SYS | 宏 | 1 | 0=使用操作系统互斥,1=不使用操作系统(裸机自旋锁 + 喂狗) |
spi_hw_num | s8 | 由平台数据决定 | 挂接的 SPI 控制器,仅支持 SPI1/SPI2 |
spi_cs_port | u8 | 由平台数据决定 | CS 片选引脚 |
spi_read_width | u8 | 由平台数据决定 | 读数据线宽(对应双线/四线快速读) |
spi_pdata | const struct spi_platform_data * | 由平台数据决定 | SPI 控制器平台配置(时钟等) |
start_addr | u32 | 由平台数据决定 | 分区起始地址 |
size | u32 | 由平台数据决定 | 分区大小;单分区时忽略 |
API 参考
int syscfg_vm_init(u32 mem_addr, u32 mem_size)
初始化 VM 存储系统,建立逻辑 ID → 物理存储的映射上下文。
参数:
mem_addr(u32):VM 工作内存起始地址(通常为静态数组或 RAM 池)mem_size(u32):工作内存大小
返回: int,0 表示成功,非 0 表示失败。
int vm_read(u32 id, u8 *data_buf, u16 len)
按逻辑 ID 读取持久化数据。
参数:
id(u32):VM_INDEX枚举值data_buf(u8 *):读取数据的输出缓冲区len(u16):期望读取的字节数
返回: int,0 表示成功;非 0 表示 ID 不存在或读取失败。
int vm_write(u32 id, u8 *data_buf, u16 len)
按逻辑 ID 写入持久化数据(内部可能触发擦除与页编程)。
参数:
id(u32):VM_INDEX枚举值data_buf(u8 *):待写入数据len(u16):写入字节数(单次上限受u16约束)
返回: int,0 表示成功;非 0 表示写入失败(如擦除/编程超时)。
void vm_pre_erase(void)
预擦除接口:在批量写参数或升级前调用,把擦除耗时移出关键写入路径。无参数、无返回值。
int syscfg_write(u16 item_id, const void *buf, u16 len) / int syscfg_read(u16 item_id, void *buf, u16 len)
驱动内部使用接口(头文件明确标注),供 VM 底层实现调用;应用不应直接使用。参数为更细粒度的条目 ID 与数据缓冲。
void _norflash_power_down(void) / void _norflash_release_power_down(void)
NOR Flash 低功耗控制:断电前进入 Power Down(命令 0xB9),上电/唤醒时释放(命令 0xAB)。无参数、无返回值(norflash.h)。
设备操作表
| 符号 | 类型 | 说明 |
|---|---|---|
norflash_dev_ops | const struct device_operations | 块设备式 NOR Flash 操作表(norflash.h) |
norfs_dev_ops | const struct device_operations | NOR 文件系统式操作表(norflash.h) |
失败模式、边界情况与并发
写入中途掉电
NOR Flash 的"先擦后写"流程中若在擦除后、编程前掉电,该扇区会呈现全 0xFF(已擦除但未写入)或部分写入状态。VM 层需要具备幂等恢复能力:读侧遇到未写入条目应返回"数据不存在"而不是垃圾数据。这是 vm_api.c / nvm_api.c 内部需要保证的语义(实现细节位于实现文件中,本页核验到接口契约层)。应用侧建议采用"先写新值、确认成功后再依赖新值"的策略。
擦写寿命与磨损
Flash 扇区擦写寿命有限(典型 10 万次)。频繁写同一 VM_INDEX(如定时存 RTC 计数、断点)会集中磨损同一物理区。VM 层通过 syscfg_vm_init 划分的内存区与内部映射,通常采用"多槽位轮换/拷贝式更新"来摊薄磨损。若观察到 Flash 提前损坏,优先检查是否有高热路径在循环写同一个 ID,并考虑用 vm_pre_erase() 批量合并写操作。
并发访问
- 裸机模式(
NORFLASH_NO_SYS == 1)下,多个中断/主循环任务同时访问 Flash 时,必须经flash_mutex_pend保护;timeout=0死等模式风险在于持锁方异常时无限等待,生产代码建议使用带超时的调用。 flash_mutex_pend超时返回-1后,调用方不得继续执行擦除/编程命令,否则会与持锁方交叉操作造成数据破坏。- 中断内访问 Flash 应使用
query_flash_mutex_pend非阻塞查询;若锁被占用则放弃本次操作(返回失败),避免在中断里长时间自旋。
边界情况
- 长度上限:
vm_read/vm_write的len为u16,单次最大 65535 字节;跨越大小的数据需分多次写入或用syscfg_*内部条目接口。 - ID 越界:
id超过VM_INDEX枚举末尾时行为未定义,调用方应保证 ID 合法。 - 索引顺序约束:
LIB_VM_INDEX_LP_SUM_CNT~LIB_SYSMEM_END之间禁止增删改序;VM_INDEX_FM_CHANNL~VM_INDEX_FM_CHANNL_END(28 个槽位)之间禁止插入其它 ID——违反会破坏既有数据的偏移兼容性。 - 未初始化:未调用
syscfg_vm_init就使用vm_read/vm_write属于未定义行为,典型表现为返回错误或访问非法内存。
故障定位建议
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
vm_read 返回非 0 | ID 未写过 / VM 未初始化 / 存储区损坏 | 确认 syscfg_vm_init 已调用;检查 ID 是否首次读取 |
| 写入后读回不一致 | 擦除未完成即编程 / 互斥未生效 | 检查写路径是否持锁;确认 READ_SR1 忙轮询完整 |
| 系统随机复位 | 擦除耗时内看门狗超时 | 确认裸机路径调用了 wdt_clear()(flash_mutex_pend 内已有) |
| Flash 提前损坏 | 单一 ID 高频写入 | 排查热路径写调用,评估磨损均衡 |
性能与运维
- 擦除是耗时操作:扇区擦除(
0x20)典型耗时数十毫秒,块擦除(0xD8)更长。vm_pre_erase()的存在说明 VM 支持"预擦除"模式,把擦除从写关键路径中剥离——批量更新(升级、恢复出厂)前调用可显著降低写入阻塞时间。 - 读路径轻量:
READ_DATA(0x03)读通常无状态等待,vm_read可直接命中缓存或快速读出,适合高频读取场景。 - 低功耗配合:空闲时可调用
_norflash_power_down()进入 Power Down 省电,唤醒前调用_norflash_release_power_down();注意切换 Power Down 状态会短暂增加唤醒延迟。 - 喂狗与超时:裸机自旋等待期间驱动内主动
wdt_clear(),这是把 Flash 操作嵌入中断/主循环场景的关键保证;移植到新平台时务必保留。
扩展点
- 新增持久化项:在
VM_INDEX用户段追加枚举值即可(注意 FM 频道槽位约束),无需改动 VM 实现。 - 新增分区:通过
norflash_dev_platform_data的start_addr/size划分新的 NOR 分区;多分区场景需为每个分区注册独立平台数据与设备。 - 更换 Flash 型号:在
norflash.h/norflash.c中扩展命令集(JEDEC ID 识别)与擦除粒度映射;norflash_dev_ops保持稳定,上层 VM 无感。 - 从裸机迁移到 RTOS:将
NORFLASH_NO_SYS置 0,驱动切换到操作系统互斥路径,flash_mutex_*走#else分支。 - 新项目采用新版 VM:
sdk/app/bsp/common/vm/new_vm/nvm_api.c提供新版 VM 实现,接口风格与旧版一致(vm_read/vm_write语义),迁移成本低。