跳转至

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,再在运行时更新用户可修改的值。

NV 的使用流程分为预置路径和运行时路径。实际业务可以同时使用两者:将出厂默认值放入预置 NV,再在运行时更新用户可修改的值

  1. 为业务分配唯一 key_id,并确定数据类型与属性。
  2. 需要出厂默认值时,在 common.h 定义自定义类型,在 app.json 增加 NV 描述项;仅运行时数据可直接进入代码实现。
  3. 编译 A 核目标,构建系统自动生成 NV 镜像并打包到烧录用 fwpkg
  4. 设备启动后,NV 初始化完成,再调用读写、属性查询或空间查询接口。
  5. 异步存储开启时,在下电、关机或重启前调用 uapi_nv_flush

Key、属性与预置配置说明

新增 NV 项

新增预置 NV 项的流程:

  1. 非通用类型时,在头文件定义 kvalue 类型。
  2. 在 JSON 文件新增 NV 描述项。

新增 kvalue 数据类型

  • 通用数据类型:uint8_tuint16_tuint32_tbool
  • 自定义数据类型:支持 enumstruct
  • 自定义类型定义文件: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 定义。
  • attributions124 三选一且互斥:
    • 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.happ.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 包含 permanentencryptednon_upgrade 和保留字段。HiDiTingV100 支持加密 NV;永久属性和加密属性不能修改,永久属性的 value 也不能修改。对 uapi_nv_write_with_attr,无回调需求时可传 NULL

常用调用方式

以下五种调用覆盖运行时最常用的 NV 工作流;完整参数说明请直接跳转到上表中的 API 锚点。

  1. 写入默认 Normal NV:调用 uapi_nv_write(key, value, length)。它不附加永久、加密、不可升级等属性。
  2. 写入带属性 NV:创建 nv_key_attr_t,例如设置 encrypted = true,再调用 uapi_nv_write_with_attr;写入完成回调在本平台可传 NULL
  3. 读取 NV:调用 uapi_nv_read,传入接收缓冲区容量,同时检查返回的实际 kvalue_length
  4. 读取 NV 及属性:调用 uapi_nv_read_with_attr,从 attr 获取 Key 的永久、加密、不可升级属性。
  5. 查询空间状态:调用 uapi_nv_get_store_status,读取 total_spaceused_spacereclaimable_spacecorrupted_spacemax_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 声明。

NV 写入、读回校验和空间状态查询流程

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 环境使用指南
  1. 《一站式 CLI 开发环境使用指南》安装并初始化 FBB CLI。
  2. 将完整 SDK 工作区中的 src 子目录配置为环境变量;该 src 的同级目录必须包含 samples/native_samples,否则构建系统无法发现外层 Native Sample。
  3. 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+NVWRITE
AT+NVREAD
AT+NVSTATUS

等待系统完成启动后再执行上述命令。AT 命令表的注册早于标准启动流程中的 NV 初始化;若在启动早期执行,NV 接口可能返回 ERRCODE_NV_NOT_INIT。示例不重复调用 uapi_nv_init,由标准启动流程完成初始化。

调试方法

  1. 先执行 AT+NVWRITE,确认 ret=0x00000000,再执行 AT+NVREAD;日志中的 verification=PASS 表示长度和内容均校验通过。
  2. 执行 AT+NVSTATUS,观察 usedreclaimablemax_key
  3. 在业务代码中保留 errcode_t 返回值并打印 Key、数据长度和错误码;错误码定义可在 include/errcode.h 查询。
  4. 需要验证重启后的持久化时,写入成功后重启设备,再执行 AT+NVREAD
  5. 仅在 CONFIG_NV_SUPPORT_ASYNCHRONOUS_STORE 开启时,在关机、下电、重启处理流程中调用 uapi_nv_flush
  6. 若返回 ERRCODE_NV_NOT_INIT,确认系统是否已完成启动;不要通过在 AT 回调中重复初始化 NV 来绕过启动顺序。

预期结果

实际 AT 框架可能在业务日志前后输出 OKERROR

NV 写入与读取校验(AT+NVWRITE / AT+NVREAD)

[NVWRITE] key=0x3301 length=15 ret=0x00000000
[NVREAD] key=0x3301 length=15 value=nv-storage-demo verification=PASS

ret=0x00000000verification=PASS 表示写入成功,读回长度与内容校验通过。读取失败时会输出 Key 和错误码;读回长度或内容不一致时会输出 verification failed 及实际、预期长度。

NV 空间查询(AT+NVSTATUS)

[NVSTATUS] total=<total> used=<used> reclaimable=<reclaimable> corrupted=<corrupted> max_key=<max_key>

totalusedreclaimablecorruptedmax_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_DEFINESbuild_component()
src/middleware/chips/3322/nv/nv_config/nv_default/include/key_id.h 集中声明 NV Key NV_ID_NV_STORAGE_SAMPLE

相关接入文件如下:

代码走读

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.pyditing-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.txtCONFIG_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,
    },
};

NVWRITENVREADNVSTATUS 的命令 ID 分别为 0x226A0x226B0x226C。每个表项将执行命令绑定到相应处理函数;注册时直接用 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 一致:

/samples/native_samples/my_nv_app/
├── CMakeLists.txt
├── my_nv_app.c
└── my_nv_app.h

完成下列步骤后,组件会随 diting-community 目标构建,并在系统启动阶段运行:

  • key_id.h 为业务申请并登记唯一的 NV_ID_MY_NV_APP_CONFIG
  • 新建 my_nv_app.cmy_nv_app.hCMakeLists.txt
  • config.pysamples/native_samples/CMakeLists.txt 中接入组件。
  • 在实际使用的 A 核链接脚本中保留 app_run 分段。
  • 编译、烧录并通过启动日志和重启后的读取结果验证。

不要复用 NV_ID_NV_STORAGE_SAMPLEnv.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_HEADERPUBLIC_DEFINESCOMPONENT_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.h */
#ifndef MY_NV_APP_H
#define MY_NV_APP_H

void my_nv_app_run(void);

#endif
/* 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_readuapi_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

测试验证

  1. key_id.h 完成产品 Key 登记,并完成前述 config.py、父 CMake、组件 CMake 与链接脚本配置。
  2. 按快速 Demo 的构建和烧录步骤执行 fbb set-target pack_diting_communityfbb build、烧录和串口监视。
  3. 首次启动应看到 my_nv_app: create-default ret=0;该日志表示默认结构体已写入。
  4. 重启设备后应看到 my_nv_app: loaded enable=1 level=3;这表示读取长度与内容结构均符合预期。
  5. 调用 uapi_nv_get_store_status 记录空间变化,并保留 Key、数据长度和错误码日志以便排查。
  6. 仅在开启 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_tnv_store_status_t 的字段说明见 nv.hNV 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+NVWRITEAT+NVREAD 不存在 Demo 组件未加入目标,或 AT 注册宏未生效。 检查 CONFIG_ENABLE_NV_STORAGE_SAMPLEsamples/native_samples/CMakeLists.txt、组件 PUBLIC_DEFINESat_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_spacereclaimable_spacecorrupted_space,并减少重复写入。
调用删除、备份、恢复或通知接口失败 目标未支持对应能力,或相关 NV 特性宏、参数未配置。 对照 NV API 参考 检查能力、特性宏和参数配置。