第 7 章:NVS 参数持久化
📂 示例代码:07_nvs
7.1 知识要点
- NVS(Non-Volatile Storage)的分区结构与命名空间
- 基本类型(u8/i32/float)和 blob 的读写 API
- 参数初始化标志位的设计模式
- 4 个命名空间的组织方式(pid_params / mag_calib / cf_params / level_cal)
- USB CDC 串口命令解析与参数热更新
7.2 课程内容
机器人控制系统中,PID 参数、传感器校准数据等需要在断电后保留。ESP32-S3 的 NVS 提供了一个键值存储系统,基于 Flash 的磨损均衡分区,支持字符串、整数、浮点数和任意二进制数据(blob)的持久化存储。本章演示 OSRCORE 使用的全部 4 个命名空间,并通过 USB CDC 串口命令实时修改各参数。
7.3 基础学习
NVS 结构
NVS 使用命名空间(namespace)隔离不同模块的数据,每个命名空间下可以存储多个键值对。OSRCORE 使用 4 个命名空间:
NVS Flash
├── namespace: "pid_params"
│ ├── "init" → u8(是否已初始化)
│ ├── "kp" → blob (float)
│ ├── "ki" → blob (float)
│ └── "kd" → blob (float)
├── namespace: "mag_calib"
│ ├── "init" → u8
│ ├── "hi" → blob (3×float,硬铁偏移)
│ └── "si" → blob (9×float,软铁矩阵)
├── namespace: "cf_params"
│ ├── "init" → u8
│ ├── "alpha_s"→ blob (float,静止互补滤波系数)
│ ├── "alpha_m"→ blob (float,运动互补滤波系数)
│ └── "spd_thr"→ blob (float,速度切换阈值)
└── namespace: "level_cal"
├── "init" → u8
├── "ox" → blob (float,加速度计 X 偏移)
├── "oy" → blob (float,加速度计 Y 偏移)
└── "oz" → blob (float,加速度计 Z 偏移)初始化标志模式
首次上电时 NVS 为空,需要写入默认值。通过一个 init 标志位判断是否已初始化,避免每次重启都覆盖用户修改的参数:
c
uint8_t inited = 0;
nvs_get_u8(h, "init", &inited);
if (!inited) {
// 使用默认值,不从 NVS 读取
}Blob 读写
Blob 可以存储任意结构体,读取时需要传入缓冲区大小:
c
// 写入
nvs_set_blob(h, "params", &g_params, sizeof(pid_params_t));
nvs_commit(h); // 必须 commit 才会真正写入 Flash
// 读取
size_t sz = sizeof(pid_params_t);
nvs_get_blob(h, "params", &g_params, &sz);7.4 程序学习
加载参数(带默认值回退):
c
static void params_load(void)
{
nvs_handle_t h;
if (nvs_open("pid_params", NVS_READONLY, &h) != ESP_OK) goto defaults;
uint8_t inited = 0;
nvs_get_u8(h, "init", &inited);
if (!inited) { nvs_close(h); goto defaults; }
size_t sz = sizeof(pid_params_t);
if (nvs_get_blob(h, "params", &g_params, &sz) == ESP_OK) {
nvs_close(h);
return;
}
defaults:
g_params.kp = 447.0f;
g_params.ki = 4.7f;
g_params.kd = 47.0f;
}保存参数:
c
static void params_save(void)
{
nvs_handle_t h;
ESP_ERROR_CHECK(nvs_open("pid_params", NVS_READWRITE, &h));
nvs_set_blob(h, "params", &g_params, sizeof(pid_params_t));
nvs_set_u8(h, "init", 1);
nvs_commit(h);
nvs_close(h);
}串口命令解析(全部 4 个命名空间):
c
char line[128];
if (fgets(line, sizeof(line), stdin)) {
line[strcspn(line, "\r\n")] = '\0';
float a, b, c;
/* pid_params */
if (sscanf(line, "kp %f", &a) == 1) { g_pid.kp = a; pid_save(); }
else if (sscanf(line, "ki %f", &a) == 1) { g_pid.ki = a; pid_save(); }
else if (sscanf(line, "kd %f", &a) == 1) { g_pid.kd = a; pid_save(); }
/* mag_calib */
else if (sscanf(line, "mag hi %f %f %f", &a, &b, &c) == 3) {
g_mag.hard[0]=a; g_mag.hard[1]=b; g_mag.hard[2]=c; mag_save();
}
/* cf_params */
else if (sscanf(line, "cf %f %f %f", &a, &b, &c) == 3) {
g_cf.alpha_static=a; g_cf.alpha_moving=b; g_cf.speed_thr=c; cf_save();
}
/* level_cal */
else if (sscanf(line, "level %f %f %f", &a, &b, &c) == 3) {
g_level.ox=a; g_level.oy=b; g_level.oz=c; level_save();
}
/* show / reset */
else if (strcmp(line, "show") == 0) { all_show(); }
else if (strcmp(line, "reset") == 0) { all_reset(); }
}7.5 课程总结
本章掌握了 NVS 的命名空间、blob 读写和 commit 机制,实现了 4 个命名空间(pid_params / mag_calib / cf_params / level_cal)的断电保持和串口热更新。每个命名空间独立管理 init 标志,首次上电自动使用默认值,后续修改持久保存。reset 命令可一键恢复全部默认值。