NV存储 用户指南
NV(Non-Volatile,非易失)存储用于在本地存储介质中保存掉电后仍需保留的数据。本指南说明 NV 的工作方式、编译预置配置和运行时 API,并提供一个可通过 AT 命令验证“写入、读取校验、空间查询”的完整 Native Demo。
接口参数、返回值和完整类型定义以 NV API 参考 为准;本文重点说明何时使用、如何配置、如何跑通和如何排查。
NV 存储背景知识
NV 工作原理
NV 中的每个数据项采用类似 Key-Value 的形式:key 是唯一索引,value 是业务数据。业务可通过两种方式让数据进入 NV:
- 编译预置:在编译阶段修改 NV 头文件和配置文件,生成客制化 NV 镜像;烧录时随烧录包统一写入存储介质。预置项在运行阶段仍可通过 API 读取和更新。
- API 写入:应用在运行阶段直接调用 API 新增或更新 NV 项,适用于用户设置、运行状态和业务配置等动态数据。
以 Flash 为例,NV 按 Flash sector(扇区,存储空间的基本逻辑单元)管理。一个 NV 页等于一个 sector,即 4096 Byte;当前 diting-community 默认配置中,NV 区(NV_PAGES)为 4 页,备份区(NV_BACKUP_PAGES)为 2 页。扣除管理结构后,单个非加密 NV 项的有效数据不应超过 4060 Byte。当前芯片配置同时定义了加密项最大长度为 4048 Byte。
单项长度、可用页数和可回收空间均会影响是否可写入。提交数据前应评估数据长度,并使用 uapi_nv_get_store_status 查询空间状态。
操作流程
NV 的使用流程分为预置路径和运行时路径。实际业务可以同时使用两者:将出厂默认值放入预置 NV,再在运行时更新用户可修改的值。
- 为业务分配唯一
key_id,并确定数据类型与属性。 - 需要出厂默认值时,在
common.h定义自定义类型,在app.json增加 NV 描述项;仅运行时数据可直接进入代码实现。 - 编译 A 核目标,构建系统自动生成 NV 镜像并打包到烧录用
fwpkg。 - 设备启动后,NV 初始化完成,再调用读写、属性查询或空间查询接口。
- 异步存储开启时,在下电、关机或重启前调用 uapi_nv_flush。
Key、属性与预置配置说明
新增 NV 项
新增预置 NV 项的流程:
- 非通用类型时,在头文件定义 kvalue 类型。
- 在 JSON 文件新增 NV 描述项。
新增 kvalue 数据类型
- 通用数据类型:
uint8_t、uint16_t、uint32_t、bool。 - 自定义数据类型:支持
enum和struct。 - 自定义类型定义文件:common.h。
使用通用类型或已有类型时不需要修改 common.h;新增枚举或结构体时,必须先在该文件定义类型,再在 JSON 的 structure_type 中引用。
typedef struct {
int8_t param1;
int8_t param2;
int8_t param3;
int8_t param4;
int8_t param5;
uint32_t param6;
uint32_t param7;
int32_t param8;
uint32_t param9;
uint32_t param10;
uint32_t param11;
uint32_t param12;
uint32_t param13;
uint32_t param14;
uint32_t param15;
uint32_t param16;
uint32_t param17;
} sample_type_t;
typedef struct {
uint16_t param1;
uint16_t param2;
uint16_t param3;
uint16_t param4[2];
} sample_two;
typedef enum {
PARAM1,
PARAM2,
PARAM3,
PARAM4
} sample_three;
新增 NV 描述项
NV 描述项文件为 app.json。
表 1 NV 配置选项说明
| NV 配置选项 | 说明 |
|---|---|
key_id |
NV 项的 ID。 |
key_status |
NV 项的状态。 |
structure_type |
NV 项的数据结构类型。 |
attributions |
NV 项的属性值。 |
value |
NV 项的数据。 |
"common": {
"module_id": "0x0",
"unused": {
"key_id": "0x0",
"key_status": "reserve",
"structure_type": "uint8_t",
"attributions": 1,
"value": [0]
},
"sample": {
"key_id": "0x1",
"key_status": "alive",
"structure_type": "sample_type_t",
"attributions": 1,
"value": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17]
}
}
key_id:以十六进制配置,必须唯一。建议把 16 bit Key 划分为“高 8 bit 为模块module_id、低 8 bit 为模块内编号”,避免不同模块发生冲突。key_status:值为alive时,该项会被编入生成的 bin,表示当前固件使用这个 Key;其他值或空值不生效。structure_type:NV 数据类型。基础类型可直接引用;枚举或结构体必须先在common.h定义。attributions:1、2、4三选一且互斥:1:Normal NV,普通 NV。2:Permanent NV,不可修改。4:Un-upgrade NV,版本升级时不随版本更新而修改。
value:当类型不是通用基础类型时,必须用列表书写。可以为全部成员赋值,也可以只赋值前若干成员;未赋值的末尾成员默认值为0。
基础类型和结构体类型的预置项示例如下。
"sample1": {
"key_id": "0x1",
"key_status": "alive",
"structure_type": "uint8_t",
"attributions": 1,
"value": 0
},
"sample2": {
"key_id": "0x2",
"key_status": "alive",
"structure_type": "sample_type_t",
"attributions": 1,
"value": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17]
}
新增结构体并配置结构体预置项后,应先编译任意 A 核版本以生成必要的中间文件,再执行工程编译生成 NV 镜像。
编译生成 NV 镜像
重新执行工程编译会自动生成 NV 镜像,并把它打入烧录用 fwpkg。修改 common.h、app.json 或构建配置后,必须重新编译。
API 接口列表
本文档示例代码中使用的接口如下;使用 NV 接口时包含 nv.h。
| 接口函数 | 说明 |
|---|---|
| uapi_nv_init | 在使用 NV 接口前完成初始化 |
| uapi_nv_write | 写入 Normal 属性的 Key,不注册回调 |
| uapi_nv_write_with_attr | 写入时设置永久、加密、不可升级等属性,并可传入完成回调 |
| uapi_nv_read | 读取 Key 的 value,不获取属性 |
| uapi_nv_read_with_attr | 同时读取 value 和 nv_key_attr_t |
| uapi_nv_get_key_attr | 查询指定 Key 的属性与长度信息 |
| uapi_nv_delete_key | 删除指定 Key |
| uapi_nv_get_store_status | 获取总空间、已用、可回收、异常和单项最大空间 |
| uapi_nv_backup | 按区域配置备份 NV |
| uapi_nv_set_restore_mode_all | 配置所有 Key 区域的恢复模式 |
| uapi_nv_set_restore_mode_partitial | 按区域配置恢复模式;接口名称按 SDK 中的 partitial 拼写使用 |
| uapi_nv_flush | 异步存储启用时,将缓存数据同步到 Flash |
| uapi_nv_register_change_notify_proc | 为指定 Key 区间注册值变更通知回调 |
完整 API 列表
更多 NV 接口请参考:NV API 参考
运行时写入时,nv_key_attr_t 包含 permanent、encrypted、non_upgrade 和保留字段。HiDiTingV100 支持加密 NV;永久属性和加密属性不能修改,永久属性的 value 也不能修改。对 uapi_nv_write_with_attr,无回调需求时可传 NULL。
常用调用方式
以下五种调用覆盖运行时最常用的 NV 工作流;完整参数说明请直接跳转到上表中的 API 锚点。
- 写入默认 Normal NV:调用
uapi_nv_write(key, value, length)。它不附加永久、加密、不可升级等属性。 - 写入带属性 NV:创建
nv_key_attr_t,例如设置encrypted = true,再调用uapi_nv_write_with_attr;写入完成回调在本平台可传NULL。 - 读取 NV:调用
uapi_nv_read,传入接收缓冲区容量,同时检查返回的实际kvalue_length。 - 读取 NV 及属性:调用
uapi_nv_read_with_attr,从attr获取 Key 的永久、加密、不可升级属性。 - 查询空间状态:调用
uapi_nv_get_store_status,读取total_space、used_space、reclaimable_space、corrupted_space和max_key_space。
NV 是否已存储的判断边界
uapi_nv_is_stored 声明在内部头文件 /src/middleware/utils/nv/nv_storage_app/nv_storage.h,不在公共头文件 /src/include/middleware/utils/nv.h 中,也没有对应的公开 API 参考入口。应用代码不应把它作为通用 NV 接口调用。
应用需要判断某个 Key 是否可用时,应调用 uapi_nv_read,结合返回值和实际 kvalue_length 判定:返回成功且长度符合业务预期时,说明已获得可用数据;返回失败或长度不符合预期时,应按错误码和业务默认值策略处理。若还要判断数据内容是否与期望一致,再比较读回缓冲区与期望数据。
快速跑通 NV Demo
功能说明
本 Demo 位于 /samples/native_samples/nv_storage/README.md,并随示例在 key_id.h 中集中登记 NV_ID_NV_STORAGE_SAMPLE(当前值为 0x3301);它使用固定 payload nv-storage-demo,通过三个 AT 命令验证通用 NV API。集成 Demo 时必须同时纳入该 Key 声明。
| AT 命令 | 调用接口 | 功能 |
|---|---|---|
AT+NVWRITE |
uapi_nv_write | 将固定 payload 写入 NV_ID_NV_STORAGE_SAMPLE。 |
AT+NVREAD |
uapi_nv_read | 读回数据,校验长度与内容是否匹配。 |
AT+NVSTATUS |
uapi_nv_get_store_status | 输出总空间、已用空间、可回收空间、损坏空间和最大单项空间。 |
samples/js_samples/CipherSample 的“NV 密钥存储”页面还提供了 @system.cipher 的密钥存储闭环:查询可用 Key ID,依次完成保存、读取校验、覆盖校验和删除。底层入口在 src/ohos/base/security/huks/frameworks/crypto_lite/js/builtin/src/cipher_module.cpp。该 JS 样例面向密钥专用场景,不能替代通用 uapi_nv_* API,但可作为上层加密密钥存储的关联参考。
Demo 的 Key 先在 src/middleware/chips/3322/nv/nv_config/nv_default/include/key_id.h 中集中登记,再由示例引用;nv.h 已包含该头文件。
// key_id.h
#define NV_ID_NV_STORAGE_SAMPLE 0x3301
// nv_storage_demo.c
errcode_t ret = uapi_nv_write(NV_ID_NV_STORAGE_SAMPLE, payload, payload_len);
编译
准备工作
说明:本文示例命令以一站式 CLI 为主。实际开发可从以下三种环境中选择,推荐使用一站式 CLI。
| 开发环境 | 适用场景 | 使用指南 |
|---|---|---|
| 一站式 CLI(推荐) | 快速完成目标选择、构建、烧录和串口监视 | 一站式 CLI 开发环境使用指南 |
| HiSpark Studio for VS Code | 图形化编辑、编译、烧录和调试 | HiSpark Studio for VS Code 开发环境使用指南 |
| WSL 与 Docker | 在 Windows 上使用一致的 Linux 容器构建环境 | WSL 与 Docker 环境使用指南 |
- 按《一站式 CLI 开发环境使用指南》安装并初始化 FBB CLI。
- 将完整 SDK 工作区中的
src子目录配置为环境变量;该src的同级目录必须包含 samples/native_samples,否则构建系统无法发现外层 Native Sample。 - Demo 通过
CONFIG_ENABLE_NV_STORAGE_SAMPLE参与diting-community目标配置;使用pack_diting_community目标组构建。samples/native_samples/CMakeLists.txt 仅增加本 Demo 的条件引入;diting-community目标配置同时包含该功能宏和nv_storage_sample组件。
$env:FBB_SDK_DIR = "<SDK_WORKSPACE>\src"
Set-Location $env:FBB_SDK_DIR
fbb setup
fbb doctor
fbb set-target pack_diting_community
fbb build
构建成功后,从 output/3322/fwpkg/diting-community.fwpkg 获取烧录包。编译过程会同时生成 NV 镜像并打入 fwpkg。
若刚新增了结构体预置项,先完成一次 A 核版本构建以生成中间文件,再重新执行上述构建命令。
使用方式
烧录与串口连接
将 COM3 替换为开发板实际串口。
fbb flash -f "$env:FBB_SDK_DIR\output\3322\fwpkg\diting-community.fwpkg" --port COM3 --chip Hi3322 --manual-reset --timeout 600
fbb monitor --port COM3 --baud 115200
如开发板日志使用 750000 波特率,可将 --baud 115200 替换为 --baud 750000。
执行命令
等待系统完成启动后再执行上述命令。AT 命令表的注册早于标准启动流程中的 NV 初始化;若在启动早期执行,NV 接口可能返回 ERRCODE_NV_NOT_INIT。示例不重复调用 uapi_nv_init,由标准启动流程完成初始化。
调试方法
- 先执行
AT+NVWRITE,确认ret=0x00000000,再执行AT+NVREAD;日志中的verification=PASS表示长度和内容均校验通过。 - 执行
AT+NVSTATUS,观察used、reclaimable和max_key。 - 在业务代码中保留
errcode_t返回值并打印 Key、数据长度和错误码;错误码定义可在include/errcode.h查询。 - 需要验证重启后的持久化时,写入成功后重启设备,再执行
AT+NVREAD。 - 仅在
CONFIG_NV_SUPPORT_ASYNCHRONOUS_STORE开启时,在关机、下电、重启处理流程中调用 uapi_nv_flush。 - 若返回
ERRCODE_NV_NOT_INIT,确认系统是否已完成启动;不要通过在 AT 回调中重复初始化 NV 来绕过启动顺序。
预期结果
实际 AT 框架可能在业务日志前后输出 OK 或 ERROR。
NV 写入与读取校验(AT+NVWRITE / AT+NVREAD)
[NVWRITE] key=0x3301 length=15 ret=0x00000000
[NVREAD] key=0x3301 length=15 value=nv-storage-demo verification=PASS
ret=0x00000000 且 verification=PASS 表示写入成功,读回长度与内容校验通过。读取失败时会输出 Key 和错误码;读回长度或内容不一致时会输出 verification failed 及实际、预期长度。
NV 空间查询(AT+NVSTATUS)
[NVSTATUS] total=<total> used=<used> reclaimable=<reclaimable> corrupted=<corrupted> max_key=<max_key>
total、used、reclaimable、corrupted 和 max_key 分别对应 NV 总空间、已用空间、可回收空间、异常空间和单项最大空间。
文件结构与代码走读
文件结构
NV Demo 的文件位于以下路径:
/samples/native_samples/nv_storage/
├── nv_storage_demo.c # NV 示例主文件,包含 AT 命令处理函数
├── nv_storage_demo.h # NV 示例头文件,包含命令表和函数声明
└── CMakeLists.txt # 编译配置
各文件职责
| 文件 | 职责 | 关键内容 |
|---|---|---|
nv_storage_demo.c |
NV 示例主文件,实现命令表、写入、读取校验和空间查询 | at_nv_storage_write()、at_nv_storage_read()、at_nv_storage_status() |
nv_storage_demo.h |
头文件,声明供 AT 适配层调用的注册函数 | at_diting_nv_storage_example_cmd_register() |
CMakeLists.txt |
编译配置,定义源文件、头文件路径和编译选项 | set(SOURCES ...)、PUBLIC_DEFINES、build_component() |
| src/middleware/chips/3322/nv/nv_config/nv_default/include/key_id.h | 集中声明 NV Key | NV_ID_NV_STORAGE_SAMPLE |
相关接入文件如下:
- samples/native_samples/CMakeLists.txt:在
CONFIG_ENABLE_NV_STORAGE_SAMPLE开启时引入 Demo 组件。 - src/build/config/target_config/3322/config.py:在
diting-community目标配置中增加功能宏和nv_storage_sample组件;pack_diting_community是对应的构建目标组。 - src/middleware/chips/3322/at_adapter/at_adapter.c:在功能宏开启时包含 Demo 头文件并调用命令注册函数。
- src/include/middleware/utils/nv.h:定义 NV 公共接口、属性结构、空间状态结构和 Key 区域。
- src/middleware/chips/3322/nv/nv_config/nv_default/include/key_id.h:集中声明各类 NV Key;示例 Key 在此处登记为
NV_ID_NV_STORAGE_SAMPLE。 - common.h 与 app.json:分别定义编译预置 NV 的自定义类型和默认 Key 数据。
代码走读
CMakeLists.txt 解析
set(COMPONENT_NAME "nv_storage_sample")
set(SOURCES
${CMAKE_CURRENT_SOURCE_DIR}/nv_storage_demo.c
)
set(PUBLIC_HEADER
${CMAKE_CURRENT_SOURCE_DIR}
)
set(PRIVATE_HEADER
${ROOT_DIR}/middleware/utils/at/at
${ROOT_DIR}/middleware/utils/at/at/include
${ROOT_DIR}/middleware/utils/at/at/src
${ROOT_DIR}/include/middleware/utils
${ROOT_DIR}/middleware/chips/3322/nv/nv_config/nv_default/include
)
set(PUBLIC_DEFINES
AT_DITING_EXAMPLE_NV_STORAGE
)
set(WHOLE_LINK true)
set(MAIN_COMPONENT false)
build_component()
install_sdk(${CMAKE_CURRENT_SOURCE_DIR} "*.h")
关键配置说明:
COMPONENT_NAME:组件名称为nv_storage_sample,需在config.py的diting-community配置项下添加该组件名。SOURCES:指定源文件nv_storage_demo.c。PUBLIC_HEADER:公开头文件路径,使 AT 适配层可以引用nv_storage_demo.h。PRIVATE_HEADER:提供 NV 和 AT 组件编译时所需的头文件查找路径。WHOLE_LINK:设置为true,避免命令处理函数在链接阶段被裁剪。PUBLIC_DEFINES:定义AT_DITING_EXAMPLE_NV_STORAGE,用于在at_adapter.c中条件性地添加 AT 命令注册。- 上层 samples/native_samples/CMakeLists.txt 以
CONFIG_ENABLE_NV_STORAGE_SAMPLE为条件引入nv_storage目录。 - 构建配置文件为 config.py;AT 注册文件为 at_adapter.c。
AT 命令表解析
命令表在 nv_storage_demo.c 中保持为文件内静态对象,头文件只公开注册函数:
void at_diting_nv_storage_example_cmd_register(void);
static const at_cmd_entry_t g_at_nv_storage_cmd_table[] = {
{
.name = "NVWRITE",
.cmd_id = 0x226A,
.attribute = 0,
.syntax = NULL,
.cmd = at_nv_storage_write,
.set = NULL,
.read = NULL,
.test = NULL,
},
{
.name = "NVREAD",
.cmd_id = 0x226B,
.attribute = 0,
.syntax = NULL,
.cmd = at_nv_storage_read,
.set = NULL,
.read = NULL,
.test = NULL,
},
{
.name = "NVSTATUS",
.cmd_id = 0x226C,
.attribute = 0,
.syntax = NULL,
.cmd = at_nv_storage_status,
.set = NULL,
.read = NULL,
.test = NULL,
},
};
NVWRITE、NVREAD、NVSTATUS 的命令 ID 分别为 0x226A、0x226B、0x226C。每个表项将执行命令绑定到相应处理函数;注册时直接用 sizeof(g_at_nv_storage_cmd_table) / sizeof(g_at_nv_storage_cmd_table[0]) 计算数量。
命令注册函数
void at_diting_nv_storage_example_cmd_register(void)
{
errcode_t ret = uapi_at_cmd_table_register(g_at_nv_storage_cmd_table,
sizeof(g_at_nv_storage_cmd_table) / sizeof(g_at_nv_storage_cmd_table[0]),
NV_STORAGE_AT_MAX_LEN);
if (ret != ERRCODE_SUCC) {
printf("[NV] command registration failed, ret=0x%08x\n", (unsigned int)ret);
}
}
at_diting_nv_storage_example_cmd_register() 调用 uapi_at_cmd_table_register,将命令表注册到系统。若注册失败,函数打印错误码。
NV 写入函数
at_nv_storage_write() 使用 key_id.h 中已登记的 NV_ID_NV_STORAGE_SAMPLE,再调用 uapi_nv_write 写入静态 payload。
static at_ret_t at_nv_storage_write(void)
{
uint16_t payload_len = (uint16_t)(sizeof(g_nv_storage_payload) - 1U);
errcode_t ret = uapi_nv_write(NV_ID_NV_STORAGE_SAMPLE, g_nv_storage_payload, payload_len);
printf("[NVWRITE] key=0x%04x length=%u ret=0x%08x\n",
NV_ID_NV_STORAGE_SAMPLE, payload_len, (unsigned int)ret);
return (ret == ERRCODE_SUCC) ? AT_RET_OK : AT_RET_RUN_ERROR;
}
默认写入属性是 Normal,不附加永久、加密、不可升级属性,也不注册回调。业务需要属性时应改用 uapi_nv_write_with_attr。
NV 读取校验函数
at_nv_storage_read() 分配与 payload 等长的本地数组,调用 uapi_nv_read,再同时比较实际读取长度和内容。只有两项都匹配时才输出 verification=PASS。
static at_ret_t at_nv_storage_read(void)
{
uint8_t buffer[sizeof(g_nv_storage_payload)] = {0};
uint16_t actual_len = 0;
uint16_t expected_len = (uint16_t)(sizeof(g_nv_storage_payload) - 1U);
errcode_t ret = uapi_nv_read(NV_ID_NV_STORAGE_SAMPLE,
(uint16_t)sizeof(buffer), &actual_len, buffer);
if (ret != ERRCODE_SUCC) {
printf("[NVREAD] key=0x%04x ret=0x%08x\n",
NV_ID_NV_STORAGE_SAMPLE, (unsigned int)ret);
return AT_RET_RUN_ERROR;
}
if ((actual_len != expected_len) ||
(memcmp(buffer, g_nv_storage_payload, expected_len) != 0)) {
printf("[NVREAD] verification failed, actual_length=%u expected_length=%u\n",
actual_len, expected_len);
return AT_RET_RUN_ERROR;
}
printf("[NVREAD] key=0x%04x length=%u value=%.*s verification=PASS\n",
NV_ID_NV_STORAGE_SAMPLE, actual_len, (int)actual_len, (const char *)buffer);
return AT_RET_OK;
}
动态数据可通过 osal_kmalloc 申请缓冲区;完成业务数据处理后,应在每条错误路径和成功路径用 osal_kfree 释放。Demo 采用静态常量数据,避免把内存管理细节混入基础读写流程。
NV 信息查询函数
at_nv_storage_status() 调用 uapi_nv_get_store_status,输出:
static at_ret_t at_nv_storage_status(void)
{
nv_store_status_t status = {0};
errcode_t ret = uapi_nv_get_store_status(&status);
if (ret != ERRCODE_SUCC) {
printf("[NVSTATUS] ret=0x%08x\n", (unsigned int)ret);
return AT_RET_RUN_ERROR;
}
printf("[NVSTATUS] total=%u used=%u reclaimable=%u corrupted=%u max_key=%u\n",
(unsigned int)status.total_space, (unsigned int)status.used_space,
(unsigned int)status.reclaimable_space, (unsigned int)status.corrupted_space,
(unsigned int)status.max_key_space);
return AT_RET_OK;
}
total_space:当前核 NV 总空间。used_space:已用空间。reclaimable_space:擦除后可重新使用的空间。corrupted_space:有效但异常、擦除后可复用的空间。max_key_space:可存储的最大单项空间。
编译预置 NV 配置走读
预置类型和 JSON 项构成“类型定义 → Key 描述 → 编译生成镜像”的闭环。自定义类型放在 common.h,JSON 项放在 app.json。其中 key_status: "alive" 使该项进入镜像,attributions 决定 Normal、Permanent 或 Un-upgrade 行为。
API 调用片段用于说明调用关系。可直接运行的路径、AT 注册、返回值检查和读取校验已在本节 Demo 中给出;产品代码还应在 key_id.h 中完成正式 Key 登记,并补充业务数据校验、错误处理和必要的回调定义。
基于 NV Demo 开发自己的应用
本节使用 app_run 将 NV 逻辑注册为系统启动入口。它与前文的 AT Demo 是两种独立接入方式:本示例不定义 AT_DITING_* 宏、不修改 at_adapter.c、也不注册 AT 命令。系统启动时自动调用入口函数,适合加载用户配置、创建业务默认值等一次性初始化工作。
代码清单
以下以 my_nv_app 为例。目录仍位于外层 Native Sample,接入方式与现有 Sample 一致:
完成下列步骤后,组件会随 diting-community 目标构建,并在系统启动阶段运行:
- 在
key_id.h为业务申请并登记唯一的NV_ID_MY_NV_APP_CONFIG。 - 新建
my_nv_app.c、my_nv_app.h和CMakeLists.txt。 - 在
config.py、samples/native_samples/CMakeLists.txt 中接入组件。 - 在实际使用的 A 核链接脚本中保留
app_run分段。 - 编译、烧录并通过启动日志和重启后的读取结果验证。
不要复用 NV_ID_NV_STORAGE_SAMPLE。nv.h 中的 KEY_ID_REGION3 范围为 0x3000, 0x4000),用于 user normal Key;示例 Key NV_ID_NV_STORAGE_SAMPLE=0x3301 仅供 NV Demo 使用。产品应先检查 key_id.h 中的现有登记项,再由产品侧分配未占用的 user normal Key,并以 NV_ID_MY_NV_APP_CONFIG 这类命名集中登记。业务代码只引用该名称,不写裸 Key 数值。
CMakeLists.txt 修改示例
app_run 入口位于静态组件中,WHOLE_LINK 必须为 true,避免仅被链接段引用的对象在链接时被裁剪。该组件不依赖 AT,因此 PRIVATE_HEADER、PUBLIC_DEFINES 和 COMPONENT_CCFLAGS 保持为空;app_init.h 已由 Native Sample 父组件公开。
# src/build/config/target_config/3322/config.py
# diting-community 的 defines 列表
'CONFIG_ENABLE_MY_NV_APP_RUN_SAMPLE',
# 同一 diting-community 配置的 ram_component 列表;名称必须与 COMPONENT_NAME 一致
'my_nv_app_run_sample',
# samples/native_samples/CMakeLists.txt
if("CONFIG_ENABLE_MY_NV_APP_RUN_SAMPLE" IN_LIST DEFINES)
add_subdirectory_if_exist(my_nv_app)
endif()
# samples/native_samples/my_nv_app/CMakeLists.txt
set(COMPONENT_NAME "my_nv_app_run_sample")
set(SOURCES
${CMAKE_CURRENT_SOURCE_DIR}/my_nv_app.c
)
set(PUBLIC_HEADER
${CMAKE_CURRENT_SOURCE_DIR}
)
set(PRIVATE_HEADER
)
set(PRIVATE_DEFINES
)
set(PUBLIC_DEFINES
)
set(COMPONENT_CCFLAGS
)
set(WHOLE_LINK
true
)
set(MAIN_COMPONENT
false
)
build_component()
install_sdk(${CMAKE_CURRENT_SOURCE_DIR} "*.h")
不要为这个组件增加 AT_DITING_EXAMPLE_MY_NV,也不要在 at_adapter.c 中添加 include 或注册调用;上述功能宏只负责条件引入 Native Sample。当前 diting-community 目标已开启 OHOS 启动支持,app_run 本身不需要额外的 AT 功能宏。
关键代码片段
app_run(func) 将函数指针放入专用链接段;入口函数类型必须为 void (*)(void)。nv.h 已包含 key_id.h,因此在完成产品 Key 登记后可以直接使用 NV_ID_MY_NV_APP_CONFIG。
/* my_nv_app.c */
#include <stdint.h>
#include <stdio.h>
#include "app_init.h"
#include "errcode.h"
#include "nv.h"
#include "my_nv_app.h"
typedef struct {
uint8_t enable;
uint8_t level;
} my_nv_config_t;
void my_nv_app_run(void)
{
my_nv_config_t config = {0};
uint16_t value_length = 0;
errcode_t ret = uapi_nv_read(NV_ID_MY_NV_APP_CONFIG, (uint16_t)sizeof(config),
&value_length, (uint8_t *)&config);
if (ret == ERRCODE_SUCC && value_length == (uint16_t)sizeof(config)) {
printf("my_nv_app: loaded enable=%u level=%u\n", (unsigned int)config.enable,
(unsigned int)config.level);
return;
}
config.enable = 1;
config.level = 3;
ret = uapi_nv_write(NV_ID_MY_NV_APP_CONFIG, (const uint8_t *)&config, (uint16_t)sizeof(config));
printf("my_nv_app: create-default ret=%u\n", (unsigned int)ret);
}
app_run(my_nv_app_run);
该入口先读后写:读取成功且长度匹配时使用已有值;读不到、读取失败或结构体长度不匹配时写入默认值。它使用 uapi_nv_read 和 uapi_nv_write,不在业务入口中重复调用 uapi_nv_init。标准 A 核启动流程会先初始化 NV,再启动 OHOS 初始化流程。
需要加密、永久或不可升级属性时,改用 uapi_nv_write_with_attr 并填写 nv_key_attr_t。永久属性和加密属性不能在后续写入中修改,永久 Key 的 value 也不能修改;确定产品升级策略后再配置这些属性。若要读取属性,使用 uapi_nv_read_with_attr;若只关心属性和长度,使用 uapi_nv_get_key_attr。
测试验证
- 在
key_id.h完成产品 Key 登记,并完成前述config.py、父 CMake、组件 CMake 与链接脚本配置。 - 按快速 Demo 的构建和烧录步骤执行
fbb set-target pack_diting_community、fbb build、烧录和串口监视。 - 首次启动应看到
my_nv_app: create-default ret=0;该日志表示默认结构体已写入。 - 重启设备后应看到
my_nv_app: loaded enable=1 level=3;这表示读取长度与内容结构均符合预期。 - 调用 uapi_nv_get_store_status 记录空间变化,并保留 Key、数据长度和错误码日志以便排查。
- 仅在开启
CONFIG_NV_SUPPORT_ASYNCHRONOUS_STORE时,在产品的关机、下电或重启处理流程调用 uapi_nv_flush;不要把它当作启动入口的固定操作。应分别验证正常下电和未执行 flush 的异常下电场景。
app_run运行配置
app_run 在 /src/middleware/utils/app_init/app_init.h 中定义,会生成 .zinitcall.app_run0.init 链接段。OHOS_SystemInit() 会执行 MODULE_INIT(run),所以必须在当前 diting-community 使用的 A 核链接脚本 /src/drivers/boards/3322_evb/linker/standard/acore/normal/acore.prelds 中,将以下行放在 __zinitcall_run_start 与 __zinitcall_run_end 之间:
__zinitcall_run_start = .;
KEEP (*(.zinitcall.run*.init))
KEEP (*(SORT(.zinitcall.app_run*.init)))
__zinitcall_run_end = .;
链接段必须位于这两个边界之间,否则启动流程不会遍历 app_run 入口。若产品选择了其他 A 核链接脚本变体,应只在该目标实际使用的脚本中加入同一条 KEEP 规则。
app_run 的执行还依赖 OHOS 启动路径。当前 ENABLE_UIKIT 场景下,如果系统检测不到 LCD,ohos_startup() 会直接返回而不调用 OHOS_SystemInit();此时不会执行 app_run 入口。请在 LCD 已连接、对应启动路径可用的板型上验证本示例。无论哪种情况,都不要为了提前运行而在业务线程中重复初始化 NV。
注意事项
写入与属性注意事项:
- uapi_nv_write 默认不为 Key 附加永久、加密等额外属性。
- HiDiTingV100 支持加密 NV;需要加密时使用 uapi_nv_write_with_attr 并正确填写
nv_key_attr_t。 uapi_nv_write_with_attr同时支持属性配置和写入完成回调;本平台无回调需求时可以传NULL。- 永久属性和加密属性不能修改;永久 Key 的 value 不能修改。
容量与寿命注意事项:
nv_key_attr_t和nv_store_status_t的字段说明见 nv.h 及 NV API 参考。- Flash 介质的擦写寿命由具体器件规格决定。避免把高频计数、采样数据或日志逐次写入 NV;应采用缓存、合并写入或限频策略。
- 写入前评估单项长度和可用空间;非加密项不超过
4060 Byte,加密项不超过4048 Byte。
异步存储注意事项:
- uapi_nv_flush 仅在 NV 支持异步存储时有效。
- 异步存储开启后,必须在关机、下电、重启等流程中主动调用该接口,确保缓存数据写入 Flash,避免数据丢失。
功能依赖注意事项:
uapi_nv_delete_key、备份、恢复、变更通知和异步存储依赖对应 NV 特性宏。调用前确认目标配置已开启相应能力。
常见编译错误
| 错误现象 | 原因 | 解决方法 |
|---|---|---|
找不到 nv.h 或 NV 接口未声明 |
组件头文件路径不完整,或没有包含 nv.h。 |
在源文件包含 nv.h,并检查 CMakeLists.txt 中的头文件路径。 |
AT+NVWRITE、AT+NVREAD 不存在 |
Demo 组件未加入目标,或 AT 注册宏未生效。 | 检查 CONFIG_ENABLE_NV_STORAGE_SAMPLE、samples/native_samples/CMakeLists.txt、组件 PUBLIC_DEFINES 和 at_adapter.c 注册调用。 |
NV 接口返回 ERRCODE_NV_NOT_INIT |
系统尚未完成标准 NV 初始化。 | 等待系统启动完成后再发 AT 命令;标准启动流程已完成初始化,不要在 AT 回调中重复调用 uapi_nv_init。 |
| 编译后没有新预置项 | key_status 不是 alive、JSON 格式错误,或修改后未重新编译。 |
检查 app.json 的字段和值,重新执行 fbb build。新增结构体时先完成 A 核构建。 |
| Key 冲突或写入结果异常 | 多个模块使用同一 key_id。 |
在 key_id.h 中集中登记唯一的 NV_ID_ 名称,业务代码只引用该名称,不写裸 Key 数值。 |
| 数据长度超限 | 单项超过容量或加密项限制。 | 非加密项不超过 4060 Byte,加密项不超过 4048 Byte;同时查询 max_key_space。 |
| 永久或加密属性修改失败 | 这些属性已固化,永久 Key 的 value 不可修改。 | 重新规划 Key 和属性;不要尝试用普通写入覆盖不可修改的 Key。 |
| NV 数据读取失败 | 异步存储数据未落盘,或 Key、缓冲区长度、实际长度、返回码异常。 | 若启用了异步存储,先检查下电、关机或重启前是否漏调 uapi_nv_flush,再检查 Key、缓冲区长度、实际长度和返回码。 |
| 读取长度与预期不一致 | JSON 类型、结构体布局或写入长度不一致。 | 使用 kvalue_length 判断实际长度,不要只依赖缓冲区容量;确认类型、布局和写入长度一致。 |
| 空间不足或碎片较多 | 可用空间不足或可回收空间过多。 | 调用 uapi_nv_get_store_status,查看 used_space、reclaimable_space 和 corrupted_space,并减少重复写入。 |
| 调用删除、备份、恢复或通知接口失败 | 目标未支持对应能力,或相关 NV 特性宏、参数未配置。 | 对照 NV API 参考 检查能力、特性宏和参数配置。 |