arduino-esp32 Preferences 库实战指南:基于 NVS 的键值对持久化存储
2026/9/14 17:28:58 网站建设 项目流程

arduino-esp32 Preferences 库实战指南:基于 NVS 的键值对持久化存储

【免费下载链接】arduino-esp32Arduino core for the ESP32 family of SoCs项目地址: https://gitcode.com/GitHub_Trending/ar/arduino-esp32

导读

Preferences 是 arduino-esp32 独有的核心库,官方定位为 Arduino EEPROM 库的替代方案。它以 ESP32 片上非易失性存储(NVS)为后端,通过"命名空间 + 键值对"模型让开发者以极简 API 保存配置与运行状态,数据在重启与掉电后依然保留。读完本文,你将掌握 Preferences 的完整工作流(创建命名空间、初始化检测、写入与读取、删除与清空)、16 种数据类型的 putX/getX 用法、getBytes 大块数据存取、多命名空间管理,以及用默认值参数做错误检测和彻底格式化 NVS 的高级技巧。

为什么选择 Preferences:从 EEPROM 到 NVS

在 arduino-esp32 生态中,Preferences 库被设计为 Arduino EEPROM 库的直接替代品。二者最大的差异在于底层存储介质:

  • EEPROM(传统方案):需要自己管理偏移地址、逐字节读写,并且 ESP32 本身并不像 AVR 单片机那样内置真正的 EEPROM(Arduino 的 EEPROM 库在 ESP32 上实际也是借助 flash 模拟);
  • Preferences(推荐方案):直接封装 ESP-IDF 的nvs组件,数据以"命名空间(namespace)+ 键(key)+ 值(value)"的键值对形式组织,无需关心地址分配。

从源码看,libraries/Preferences/src/Preferences.cpp 顶部直接#include "nvs.h"#include "nvs_flash.h",全部读写操作最终落在 ESP-IDF 的 NVS API 上(如nvs_set_i8nvs_get_blobnvs_erase_allnvs_commit等),因此具有与 ESP-IDF NVS 一致的掉电保持能力与磨损均衡管理。

Preferences 最适合存储大量小值(如一组配置参数),而非少量大值。如果需要保存大块文件类数据(例如日志、图片),请改用文件系统库(如 LittleFS)。Preferences 库对所有 ESP32 系列芯片(ESP32、ESP32-S2/S3、ESP32-C3/C6 等)均可用。

Preferences 数据模型:命名空间与键值对

Preferences 在 NVS 中以"命名空间"为分区单位存储数据,每个命名空间内部是一组键值对。可以类比为:键是变量名,值是该变量的值,而键值对本身具有数据类型

关键规则如下:

  1. 一个 NVS 分区内允许存在多个命名空间,每个命名空间的名字必须唯一;
  2. 键名只在其所属命名空间内唯一——同一个键名可以出现在多个不同命名空间中而互不冲突;
  3. 命名空间名与键名均区分大小写MyKeymyKey是两个不同的键);
  4. 每个键名在一个命名空间内必须唯一
  5. 命名空间名和键名都是字符串,最长 15 个字符(超出会导致操作失败);
  6. 同一时刻只能打开一个命名空间,需要访问其他命名空间时必须先关闭当前命名空间。

这里可以补充一个源码级细节:在 libraries/Preferences/src/Preferences.cpp 的getType()实现中,对键名长度做了显式校验——strlen(key) > 15时直接返回PT_INVALID。也就是说,超过 15 字符的键名不仅存储会失败,连类型查询也会被判定为无效,这印证了长度限制是硬性约束。

支持的数据类型

Preferences 直接支持以下数据类型(原文档 Table 1):

Preferences 类型底层数据类型大小(字节)
Boolbool1
Charint8_t1
UCharuint8_t1
Shortint16_t2
UShortuint16_t2
Intint32_t4
UIntuint32_t4
Longint32_t4
ULonguint32_t4
Floatfloat_t4
Long64int64_t8
ULong64uint64_t8
Doubledouble_t8
Stringconst char*(C 字符串)可变
StringString(Arduino String)可变
Bytesuint8_t(任意字节序列)可变

字符串值既可以用 ArduinoString类型存储/读取,也可以用以\0结尾的char数组(C 字符串)存储/读取;Bytes 类型则用于在命名空间中存储和读取任意数量的字节。

对照 libraries/Preferences/src/Preferences.h 可以看到,putChar/putUChar/putShort/putUShort/putInt/putUInt/putLong/putULong/putLong64/putULong64/putFloat/putDouble/putBool/putString/putBytes一一对应地声明了这些类型,且putLongputIntputULongputUInt在实现上是完全相同的(见 Preferences.cpp),这是因为它们的底层 C 类型相同(int32_t/uint32_t),只是命名上沿用了传统 Arduino 的习惯。

另外值得注意:putFloat/putDouble/putBool在实现上并未调用专门的 NVS 类型 API,而是分别复用putBytesputUChar(见 Preferences.cpp),这也解释了为什么在getType()的类型探测中 Float/Double/Bytes 共享同一个底层 blob 类型PT_BLOB

核心工作流:写入、读取与初始化

存储与读取的标准流程

一旦完成初始化,Preferences 的使用非常简单:

存储一个值:

  1. 读写(RW)模式打开命名空间;
  2. 使用putX方法把值写入键;
  3. 关闭命名空间。

读取一个值:

  1. 只读(RO)模式打开命名空间;
  2. 使用getX方法按键取出值;
  3. 关闭命名空间。

技术上,命名空间以只读或读写模式打开都可以读取值;但最佳实践是:如果只做读取,就以只读模式打开,避免误写。

存储数据时使用"putX"方法并指定键名;读取数据时使用"getX"方法并指定键名。只要确保所有getput的数据类型一致,即可正常工作。文后涉及泛指时,"putX"/"getX"中的 "X" 即上表所列的 Preferences 类型(Bool、UInt、Char 等)。

初始化是唯一的难点

真正的关键点在于启动时的初始化:在使用 Preferences 存取任何数据之前,命名空间和其中的键都必须已存在。因此标准初始化流程是:

  1. 创建或打开命名空间;
  2. 测试某个"该命名空间已初始化则必然存在"的键是否存在;
  3. 若该键不存在,则创建所需的键(并写入初始值);
  4. 继续执行 sketch 其余部分,此后即可正常存取数据。
第一步:创建或打开命名空间

在 sketch 中先声明一个Preferences对象:

Preferences mySketchPrefs; // "mySketchPrefs" 是 Preferences 对象名,可自定义

然后用.begin方法使命名空间可用:

mySketchPrefs.begin("myPrefs", false)
  • 若命名空间myPrefs尚不存在,begin创建并打开它;
  • 若命名空间已存在,begin打开它;
  • 第二个参数为false时以读写(RW)模式打开——既可存储也可读取值;为true时以只读(RO)模式打开——只能读取,不能存储。

begin的完整签名在 libraries/Preferences/src/Preferences.h 中是bool begin(const char *name, bool readOnly = false, const char *partition_label = NULL),即还有一个可选的partition_label参数用于指定 NVS 分区;省略时默认使用 "nvs" 分区(见 docs/en/api/preferences.rst)。对应实现中,指定分区时会调用nvs_flash_init_partitionnvs_open_from_partition,否则调用nvs_open(见 Preferences.cpp)。

第二步:测试键的初始存在性

ESP32 上电启动时,程序本身无从得知这是首次上电还是已运行过的再次启动。利用 Preferences 的持久化能力,可以存储一个跨重启保留的标志信息,据此判断是否为首次运行并采取相应动作。

方法是用isKey测试命名空间中某个键是否存在:

isKey("myTestKey")

"myTestKey"存在于命名空间则返回true,否则返回false。从实现上看,isKey就是getType(key) != PT_INVALID的简写(见 Preferences.cpp),只有键存在且类型有效时才返回真。

示例判断逻辑:

Preferences mySketchPrefs; bool doesExist; mySketchPrefs.begin("myPrefs", false); // 以 RW 模式打开(不存在则创建)命名空间 "myPrefs" doesExist = mySketchPrefs.isKey("myTestKey"); if (doesExist == false) { /* 若 doesExist 为 false,说明键尚未创建, 需要执行 "首次运行" 代码:创建命名空间键并写入值。 */ // 在此处插入你的 "首次运行" 代码:创建键并赋值 } else { /* 若 doesExist 为 true,说明所需键此前已创建, 可以直接在启动阶段读取它们的值。 */ // 在此处插入你的 "已经来过" 启动代码 }
第三步:创建键并存储值(putX)

创建键使用某个.putX方法,把 "X" 替换为要存储数据的 Preferences 类型:

myPreferences.putX("myKeyName", value)
  • "myKeyName"在命名空间中不存在,会先创建键,再把值存入该键
  • 命名空间必须以 RW 模式打开才能执行;
  • value是必选参数,每次.putX都必须提供——因此命名空间中的每个键永远持有有效值

示例:

myPreferences.putFloat("pi", 3.14159265359); // 将 float_t 类型数据存入键 "pi"
第四步:读取值(getX)

键存在且命名空间已打开后,用对应的getX方法按类型读取:

myPreferences.getX("myKeyName")

示例:

float_t myFloat = myPreferences.getFloat("pi");

这会把命名空间键"pi"中存储的 float_t 值取出,赋给 float_t 类型变量myFloat

基础使用要点总结

  1. 在命名空间创建并打开、且键已存在之前,无法对键值对进行存储或读取
  2. 若键已存在,说明它在 sketch 首次运行时就被创建了;
  3. 无论命名空间以何种模式打开都可以读取键值,但只有以读写模式打开时才能存储值
  4. getput的数据类型必须匹配;
  5. 牢记命名空间名和键名的 15 字符长度上限

完整实战示例:出厂默认配置与"记忆上次运行状态"

下面是一个使用 Preferences 的setup()片段(来自原文档)。它的目的是:系统首次运行时写入一套出厂默认配置非首次运行时恢复上次运行的配置。由于系统启动时无法得知当前属于哪种情况,它先打开命名空间,再检查一个预先约定的、只有运行过 sketch 才会存在的键,据此决定走哪条分支。

#include <Preferences.h> #define RW_MODE false #define RO_MODE true Preferences stcPrefs; void setup() { // 这不是完整的 setup(),但 setup() 中应包含以下内容... stcPrefs.begin("STCPrefs", RO_MODE); // 以 RO 模式打开(不存在则创建)命名空间 "STCPrefs" bool tpInit = stcPrefs.isKey("nvsInit"); // 测试 "已初始化" 标志键是否存在 if (tpInit == false) { // tpInit 为 false 说明 "nvsInit" 键尚不存在,因此这是首次运行, // 需要建立命名空间的键。所以: stcPrefs.end(); // 以 RO 模式关闭命名空间,然后... stcPrefs.begin("STCPrefs", RW_MODE); // 以 RW 模式重新打开。 // .begin() 已创建 "STCPrefs" 命名空间;由于是首次运行, // 下面创建各键并存储 "出厂默认值"。 stcPrefs.putUChar("curBright", 10); stcPrefs.putString("talChan", "one"); stcPrefs.putLong("talMax", -220226); stcPrefs.putBool("ctMde", true); stcPrefs.putBool("nvsInit", true); // 创建 "已初始化" 标志键并存入值 // 出厂默认值已建立并存储,因此... stcPrefs.end(); // 以 RW 模式关闭命名空间,然后... stcPrefs.begin("STCPrefs", RO_MODE); // 以 RO 模式重新打开,使首次运行 // if 块之外的 setup 代码也能从 // "STCPrefs" 命名空间读取运行期值 } // 从命名空间读取运行参数并存入运行期变量 currentBrightness = stcPrefs.getUChar("curBright"); // tChannel = stcPrefs.getString("talChan"); // 等号左侧的变量已在 tChanMax = stcPrefs.getLong("talMax"); // sketch 中提前定义 ctMode = stcPrefs.getBool("ctMde"); // // 完成。上次运行的配置(或出厂默认值)现已恢复。 stcPrefs.end(); // 关闭命名空间 // 继续执行 setup() 的其余代码... // sketch 运行时,运行参数的任何变更都会被更新到命名空间对应的键值对中。 }

这段代码展示了三个值得反复使用的模式:

  1. RO 探测 → RW 初始化 → RO 恢复:先用只读模式做无害探测,只有确认首次运行才短暂切换到读写模式写入默认值,写完立即切回只读,最大限度避免误写;
  2. 标志键nvsInit:作为"命名空间已初始化"的哨兵,是isKey探测的目标;
  3. 键值对与运行期变量一一对应:恢复逻辑简单清晰。

仓库自带的 StartCounter 示例 是另一个经典应用——用 Preferences 统计 ESP32 的启动次数preferences.getUInt("counter", 0)读取计数(键不存在时返回默认值 0),累加后用preferences.putUInt("counter", counter)写回,配合ESP.restart()循环重启即可验证数据跨重启保持。代码中同样强调了命名空间名与键名均为 15 字符限制,并演示了clear()/remove("counter")两个"重置计数"选项。

工具函数:删除、统计与类型查询

删除键值对

删除当前打开的命名空间中所有键值对:

preferences.clear();
  • 删除后命名空间本身仍然存在
  • 命名空间必须以读写模式打开才能执行。

删除当前打开的命名空间中某个指定键

preferences.remove("keyname");
  • 删除"keyname"键及其关联的值;
  • 命名空间必须以读写模式打开才能执行;
  • 技巧:用remove删除"测试键"(如前面示例中的nvsInit),即可在**下一次重启时强制触发"恢复出厂设置"**分支。

无论使用哪种删除方法,被删除的键值对在再次使用前都必须重新创建。从实现看,clear()remove()分别对应 NVS 的nvs_erase_allnvs_erase_key,且两者都会在删除后调用nvs_commit确保改动真正落盘(见 Preferences.cpp)。

查询可用键表条目数:freeEntries()

Preferences 为每个命名空间维护一张键表,创建新键前表中必须有空余条目。freeEntries()返回当前键表中可用条目的数量

Preferences mySketchPrefs; mySketchPrefs.begin("myPrefs", true); size_t whatsLeft = freeEntries(); // 该方法与命名空间打开模式无关 Serial.printf("There are: %lu entries available in the namespace table.\n", (unsigned long)whatsLeft); mySketchPrefs.end();

关于键表条目数需要知道(详见 docs/en/api/preferences.rst):

  • Bool、Char、UChar、Short、UShort、Int、UInt、Long、ULong、Long64、ULong64 类型的键各占用 1 个条目
  • Float、Double 类型的键各占用 3 个条目
  • String 类型至少占用 2 个条目,且随字符串长度增加而增加
  • Bytes 类型至少占用 3 个条目,且随字节数增加而增加

因此可用条目数会随命名空间中键的数量以及部分类型的动态大小而变化。另外请注意:键表有空余条目并不代表打开的 NVS 命名空间一定有足够空间存放全部数据,NVS 容量与损耗均衡的完整细节以 ESP-IDF 的非易失性存储(NVS)文档为准。

查询键值对类型:getType()

记录每个键值对的数据类型属于开发者自己的簿记工作。若想查询某个键存储的 Preferences 类型,使用:

getType("myKey")

用法示例:

PreferenceType whatType = getType("myKey");

返回值是PreferenceType枚举,定义在 libraries/Preferences/src/Preferences.h,其取值与 Preferences 类型的对应关系如下:

返回值Preferences 类型底层数据类型枚举名
0Charint8_tPT_I8
1UCharuint8_tPT_U8
1BoolboolPT_U8
2Shortint16_tPT_I16
3UShortuint16_tPT_U16
4Intint32_tPT_I32
4Longint32_tPT_I32
5UIntuint32_tPT_U32
5ULonguint32_tPT_U32
6Long64int64_tPT_I64
7ULong64uint64_tPT_U64
8StringString / *charPT_STR
9Doubledouble_tPT_BLOB
9Floatfloat_tPT_BLOB
9Bytesuint8_tPT_BLOB
10-(无效)-PT_INVALID

注意:一个返回值可能映射到多个 Preferences 类型(例如返回值 1 同时对应 UChar 和 Bool,返回值 9 同时对应 Double、Float 和 Bytes)。getType的探测顺序在 Preferences.cpp 中依次尝试nvs_get_i8/u8/i16/u16/i32/u32/i64/u64/str/blob,先命中者胜出。调用失败(命名空间未打开、键不存在、键名超过 15 字符)时返回PT_INVALID(值 10)。

处理大数据:getBytes 与 putBytes

前面强调过,Preferences 最适合存大量小值而非少量大值。但若确实需要存储超过基础类型容量的任意数据,库提供了三个方法:

putBytes("myBytesKey", value, valueLen) getBytes("myBytesKey", buffer, valueLen) getBytesLength("myBytesKey")
  • putBytesgetBytes负责存储与读取数据;
  • getBytesLength用于查询键中存储的数据大小(读取 Bytes 数据前必须知道它)。

从方法名可以看出,它们操作的是可变长度的字节序列(常称 "blob"),而不是某种数据类型的单个元素。也就是说:即使你存入的是一个int16_t数组,该键的值也只是一串不带数据类型信息的字节——所有 blob 数据本质上都被视为一串uint8_t字节。

因此用getBytes读取时,返回给缓冲区的是uint8_t字节序列,数据的类型与数组大小需要你自己管理。好在有getBytesLengthsizeof运算符帮忙,事情并不复杂。

在实现层面,Preferences.cpp 中getBytes会先调用getBytesLength取得已存字节数,若缓冲区maxLen小于所需长度会打印not enough space in buffer并返回 0——所以务必先用getBytesLength确定所需缓冲区大小

完整示例:存取字节数组并保持数据类型

/* * 使用 Preferences "Bytes" 方法在命名空间中 * 存储和读取任意数量字节的示例 sketch。 */ #include <Preferences.h> #define RO_MODE true #define RW_MODE false void setup() { Preferences mySketchPrefs; Serial.begin(115200); delay(250); mySketchPrefs.begin("myPrefs", RW_MODE); // 以 RW 模式打开(或创建)命名空间 "myPrefs" mySketchPrefs.clear(); // 删除该命名空间中任何历史键 // 创建一组测试值。全程使用十六进制数,便于观察字节的移动。 int16_t myArray[] = { 0x1112, 0x2122, 0x3132, 0x4142, 0x5152, 0x6162, 0x7172 }; Serial.println("Printing myArray..."); for (int i = 0; i < sizeof(myArray) / sizeof(int16_t); i++) { Serial.print(myArray[i], HEX); Serial.print(", "); } Serial.println("\r\n"); // 下一句中的 sizeof() 需与 myArray 元素的数据类型匹配 Serial.print("The number of elements in myArray is: "); Serial.println( sizeof(myArray) / sizeof(int16_t) ); Serial.print("But the size of myArray in bytes is: "); Serial.println( sizeof(myArray) ); Serial.println(""); Serial.println( "Storing myArray into the Preferences namespace \"myPrefs\" against the key \"myPrefsBytes\"."); // 注意:下面这句要存储整个数组,必须使用数组的字节数,而不是元素个数。 mySketchPrefs.putBytes( "myPrefsBytes", myArray, sizeof(myArray) ); Serial.print("The size of \"myPrefsBytes\" is (in bytes): "); Serial.println( mySketchPrefs.getBytesLength("myPrefsBytes") ); Serial.println(""); int16_t myIntBuffer[20] = {}; // 20 并无特殊含义,只是确保缓冲区足够大。 Serial.println("Retrieving the value of myPrefsBytes into myIntBuffer."); Serial.println(" - Note the data type of myIntBuffer matches that of myArray"); mySketchPrefs.getBytes("myPrefsBytes", myIntBuffer, mySketchPrefs.getBytesLength("myPrefsBytes")); Serial.println("Printing myIntBuffer..."); // 下一句中的 sizeof() 需与 myArray 元素的数据类型匹配 for (int i = 0; i < mySketchPrefs.getBytesLength("myPrefsBytes") / sizeof(int16_t); i++) { Serial.print(myIntBuffer[i], HEX); Serial.print(", "); } Serial.println("\r\n"); Serial.println( "We can see how the data from myArray is actually stored in the namespace as follows."); uint8_t myByteBuffer[40] = {}; // 40 并无特殊含义,只是确保缓冲区足够大。 mySketchPrefs.getBytes("myPrefsBytes", myByteBuffer, mySketchPrefs.getBytesLength("myPrefsBytes")); Serial.println("Printing myByteBuffer..."); for (int i = 0; i < mySketchPrefs.getBytesLength("myPrefsBytes"); i++) { Serial.print(myByteBuffer[i], HEX); Serial.print(", "); } Serial.println(""); } void loop() { ; }

运行输出如下:

Printing myArray... 1112, 2122, 3132, 4142, 5152, 6162, 7172, The number of elements in myArray is: 7 But the size of myArray in bytes is: 14 Storing myArray into the Preferences namespace "myPrefs" against the key "myPrefsBytes". The size of "myPrefsBytes" is (in bytes): 14 Retrieving the value of myPrefsBytes into myIntBuffer. - Note the data type of myIntBuffer matches that of myArray Printing myIntBuffer... 1112, 2122, 3132, 4142, 5152, 6162, 7172, We can see how the data from myArray is actually stored in the namespace as follows. Printing myByteBuffer... 12, 11, 22, 21, 32, 31, 42, 41, 52, 51, 62, 61, 72, 71,

输出清晰地揭示了两个关键点:

  1. 以原始类型缓冲区(int16_t myIntBuffer)读取时,数据按元素完整还原(1112, 2122, ...),前提是缓冲区类型与原数组一致;
  2. 以字节缓冲区(uint8_t myByteBuffer)读取时,可以看到数据实际以小端字节序存储(12, 11对应0x1112的两个字节,22, 21对应0x2122),印证了"所有 blob 数据本质上是uint8_t字节序列"这一事实。

你可以复制该 sketch,修改myArray的数据类型与值来跟随代码和输出理解 Bytes 方法的运作;修改时记得同步调整myIntBuffer的类型及代码注释中标出的sizeof()。核心要点是:你操作的是字节,存储时按类型字节数存满数据,读取时自行管理缓冲区大小与数据类型

仓库的 Prefs2Struct 示例 展示了 Bytes 方法的进阶用法——把整个结构体数组作为 blob 存入 NVS:定义schedule_t结构体后,putBytes("schedule", content, sizeof(content))存入,读取后用(schedule_t *)buffer强转回结构体指针访问字段。该示例注释同时提醒:单次putBytes的最大尺寸受 NVS 分区大小限制(约为其 97%),且 NVS 有较大开销,不适合频繁变动的数据

多命名空间管理

如前所述,Preferences NVS 分区中可以存在多个命名空间,但同一时刻只能打开一个

需要访问另一个命名空间时,先关闭当前命名空间再打开目标命名空间。例如:

Preferences currentNamespace; currentNamespace.begin("myNamespace", false); // 做一些操作... currentNamespace.end(); // 关闭 'myNamespace' currentNamespace.begin("myOtherNamespace", false); // 打开另一个 Preferences 命名空间 // 做另一些操作... currentNamespace.end(); // 关闭 'myOtherNamespace'

这里复用了同一个currentNamespace对象;也可以声明多个Preferences对象分别使用。只需牢记:所有putXgetX等操作只作用于当前打开的这唯一一个命名空间,务必理清当前上下文。

深入 getX:返回值与错误检测

Preferences 的各个方法会返回状态值,可用于判断方法是否成功。假设存在一个名为favorites的键,其值为String类型。执行:

dessert = mySketchPrefs.getString("favorites");

变量dessert将包含键"favorites"中存储的字符串。但如果getString因故读取失败,我们该如何察觉错误?

答案在于:getX方法在出错时会返回一个默认值(见原文档 Table 2):

Preferences 类型出错时的默认返回值
Char, UChar, Short, UShort, Int, UInt, Long, ULong, Long64, ULong640
Boolfalse
Float, DoubleNAN
String(String 形式)""(空字符串)
String(char* 缓冲形式)\0

因此可以把返回值与上述默认值比较,相等则视为出错并采取相应动作。但问题来了:如果默认返回值恰好也是合法值呢?比如键里真的存了0false,就无法区分"读取成功"与"读取失败"。

答案在于getX方法的完整形式:

preferences.getX("myKey", myDefault)

在这种形式下,方法返回键"myKey"对应的值;若出错则返回myDefault,且myDefault必须与getX的数据类型一致。回到上面的例子:

dessert = mySketchPrefs.getString("favorites", "gravel");
  • 出错时dessert被赋值为字符串gravel
  • 成功时dessert为键favorites中存储的值。

只要预先选定一个不可能出现在合法值集合中的默认值,就拥有了可靠的错误检测手段。

源码也印证了这一设计:libraries/Preferences/src/Preferences.h 中所有getX方法都有默认参数(数值型默认0getFloat/getDouble默认NANgetBool默认falsegetString默认空String())。实现上,Preferences.cpp 先把value初始化为defaultValue,再调用nvs_get_*;若 NVS 调用失败,value保持默认值不变。

总结:需要确认读取无错误时,用完整形式getX("myKey", myDefault)传入一个"仅在出错时才可能出现"的预定默认值,再与返回结果比较;否则可以省略默认值,方法会自动返回该类型对应的默认值。

高级技巧:彻底擦除并重建 NVS 分区

在 arduino-esp32 的 Preferences 实现中,没有提供直接删除整个命名空间的方法。经过多个项目反复使用后,ESP32 的 NVS Preferences 分区可能积累垃圾或写满。要彻底擦除并重建 Preferences 所用的 NVS 内存,可以创建并运行一个包含以下代码的 sketch:

#include <nvs_flash.h> void setup() { nvs_flash_erase(); // 擦除 NVS 分区,然后... nvs_flash_init(); // 初始化 NVS 分区 while (true); } void loop() { ; }

警告:运行上述代码后,应立即向开发板下载新的 sketch,否则每次上电或重启都会重新格式化 NVS 分区!

这正是 ESP-IDF 层级的操作(nvs_flash.h来自 ESP-IDF,而非 arduino-esp32 自带源码)。它在 arduino-esp32 中的常规用法可见于 cores/esp32/esp32-hal-misc.c——系统启动流程中同样调用nvs_flash_init()初始化 NVS,说明该 API 是平台级基础设施,Preferences 只是其上的友好封装。

推荐阅读路径

  • Preferences API 参考(方法签名、参数、返回值、getType枚举对应表的完整说明):docs/en/api/preferences.rst
  • 官方入门示例(启动计数器):libraries/Preferences/examples/StartCounter/StartCounter.ino
  • 官方进阶示例(结构体 blob 存取):libraries/Preferences/examples/Prefs2Struct/Prefs2Struct.ino
  • 库头文件(全部 API 声明与PreferenceType枚举):libraries/Preferences/src/Preferences.h
  • 库实现源码(NVS 底层调用、类型探测顺序、默认值机制):libraries/Preferences/src/Preferences.cpp
  • NVS 底层细节(键表条目规则、容量与损耗均衡):参考 ESP-IDF 非易失性存储库(Non-volatile storage library)官方文档

【免费下载链接】arduino-esp32Arduino core for the ESP32 family of SoCs项目地址: https://gitcode.com/GitHub_Trending/ar/arduino-esp32

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询