1. 引言:为什么需要 Preferences?
在 ESP32 开发中,我们经常需要存储一些配置参数、设备状态或用户数据,这些数据需要在设备断电重启后依然能够保留。虽然可以使用文件系统(如 SPIFFS、LittleFS)或 EEPROM 模拟库,但 ESP32 Arduino 核心库内置的Preferences类提供了一种更简单、高效且可靠的非易失性存储(NVS)解决方案。
Preferences 库封装了 ESP32 的非易失性存储(NVS)功能,具有以下优势:
- 键值对存储:使用简单的键(字符串)来存取数据,无需管理复杂的文件路径。
- 数据类型丰富:支持整型、浮点型、字符串、二进制数据等多种类型。
- 命名空间隔离:不同功能模块的数据可以存放在不同的命名空间下,避免键名冲突。
- 原子操作与磨损均衡:底层 NVS 机制保证了数据写入的原子性,并具有磨损均衡特性,延长 Flash 寿命。
- 使用简便:无需手动初始化文件系统,API 直观易用。
2. 基本使用流程与头文件
使用 Preferences 前,需要包含相应的头文件并创建对象。
#include <Preferences.h> Preferences preferences;基本操作遵循“打开 -> 读写 -> 关闭”的流程,关闭操作会将数据真正提交到 Flash。
3. 核心函数详解
3.1 初始化与命名空间管理
begin(const char* name, bool readOnly=false, const char* partition_label=NULL)- 功能:打开一个命名空间。如果命名空间不存在,则创建它。
- 参数:
name:命名空间名称,用于数据隔离。readOnly:是否为只读模式打开。默认为false(读写模式)。partition_label:指定使用的 NVS 分区标签,通常为NULL使用默认的 “nvs” 分区。
- 返回值:成功打开返回
true,失败返回false。
end()- 功能:关闭当前命名空间,确保所有更改写入 Flash。这是一个重要的步骤,不应省略。
3.2 数据写入函数(Put)
用于存储数据。函数名通常以put开头。
putChar(const char* key, int8_t value):存储 8 位有符号整数。putUChar(const char* key, uint8_t value):存储 8 位无符号整数。putShort(const char* key, int16_t value):存储 16 位有符号整数。putUShort(const char* key, uint16_t value):存储 16 位无符号整数。putInt(const char* key, int32_t value):存储 32 位有符号整数。putUInt(const char* key, uint32_t value):存储 32 位无符号整数。putLong(const char* key, int32_t value):存储 32 位长整型(与putInt相同)。putULong(const char* key, uint32_t value):存储 32 位无符号长整型(与putUInt相同)。putLong64(const char* key, int64_t value):存储 64 位有符号整数。putULong64(const char* key, uint64_t value):存储 64 位无符号整数。putFloat(const char* key, float_t value):存储单精度浮点数。putDouble(const char* key, double_t value):存储双精度浮点数。putBool(const char* key, bool value):存储布尔值。putString(const char* key, const String& value):存储字符串(String 对象)。putString(const char* key, const char* value):存储字符串(C 风格字符串)。putBytes(const char* key, const void* value, size_t len):存储任意二进制数据。
3.3 数据读取函数(Get)
用于读取数据。如果键不存在,则返回指定的默认值。
getChar(const char* key, int8_t defaultValue=0)getUChar(const char* key, uint8_t defaultValue=0)getShort(const char* key, int16_t defaultValue=0)getUShort(const char* key, uint16_t defaultValue=0)getInt(const char* key, int32_t defaultValue=0)getUInt(const char* key, uint32_t defaultValue=0)getLong(const char* key, int32_t defaultValue=0)getULong(const char* key, uint32_t defaultValue=0)getLong64(const char* key, int64_t defaultValue=0)getULong64(const char* key, uint64_t defaultValue=0)getFloat(const char* key, float_t defaultValue=NAN)getDouble(const char* key, double_t defaultValue=NAN)getBool(const char* key, bool defaultValue=false)String getString(const char* key, const String& defaultValue=String(""))size_t getBytes(const char* key, void* buf, size_t maxLen):读取二进制数据到缓冲区,返回实际读取的字节数。
3.4 其他实用函数
remove(const char* key):删除指定键及其值。clear():清除当前命名空间下的所有键值对。freeEntries():获取当前命名空间下剩余的可用条目数(键值对数量)。isKey(const char* key):检查指定键是否存在。
4. 综合使用示例
下面是一个完整的示例,演示如何存储 WiFi 配置、设备启动次数和一段自定义二进制数据。
#include <Preferences.h> Preferences prefs; void setup() { Serial.begin(115200); delay(1000); // 1. 打开(或创建)名为 "my_app" 的命名空间 if (!prefs.begin("my_app")) { Serial.println("Failed to open preferences namespace"); return; } // 2. 读写数据 // 读取启动次数,如果不存在则默认为0,然后加1并写回 uint32_t bootCount = prefs.getUInt("boot_count", 0); bootCount++; prefs.putUInt("boot_count", bootCount); Serial.printf("Device boot count: %u\n", bootCount); // 存储 WiFi SSID 和密码 prefs.putString("wifi_ssid", "MyHomeWiFi"); prefs.putString("wifi_pass", "SecurePassword123"); // 读取 WiFi 配置(如果之前存储过) String ssid = prefs.getString("wifi_ssid", ""); String pass = prefs.getString("wifi_pass", ""); if (ssid.length() > 0) { Serial.printf("Stored WiFi SSID: %s\n", ssid.c_str()); // 注意:实际项目中不应在日志中打印密码 } // 存储和读取浮点数(例如传感器校准值) float calibrationFactor = 1.025; prefs.putFloat("cal_factor", calibrationFactor); float readCal = prefs.getFloat("cal_factor", 1.0); Serial.printf("Calibration factor: %.3f\n", readCal); // 存储和读取二进制数据(例如一个简单的结构体) struct MyData { uint8_t id; uint16_t value; } dataToStore = {0xAB, 1234}; prefs.putBytes("my_struct", &dataToStore, sizeof(dataToStore)); MyData dataRead; size_t len = prefs.getBytes("my_struct", &dataRead, sizeof(dataRead)); if (len == sizeof(MyData)) { Serial.printf("Read binary data: ID=0x%02X, Value=%u\n", dataRead.id, dataRead.value); } // 3. 关闭命名空间,提交更改 prefs.end(); Serial.println("Preferences saved successfully."); } void loop() { // 主循环无需操作 Preferences delay(10000); }5. 高级技巧与注意事项
5.1 命名空间规划
为不同的功能模块使用不同的命名空间,例如"wifi_config"、"device_settings"、"user_data"。这可以提高代码的可维护性,并允许单独清除某个模块的数据。
5.2 错误处理
始终检查begin()的返回值。写入失败可能由于 NVS 分区已满或 Flash 损坏。
5.3 数据更新策略
频繁更新同一个键可能会加速 Flash 磨损。对于频繁变化的数据(如传感器实时值),应考虑在 RAM 中缓存,定期或仅在必要时写入 Preferences。
5.4 字符串长度限制
单个键值对的总大小(键名长度 + 数据长度)存在限制(通常约为 1984 字节)。过长的字符串应分段存储或考虑使用文件系统。
5.5 与文件系统的选择
使用 Preferences 当:存储键值对形式的配置、状态标志、计数器等小型结构化数据。
使用 SPIFFS/LittleFS 当:需要存储大文件、日志、网页资源或非结构化的长文本。
6. 常见问题排查(FAQ)
- Q:数据写入后,重启读取不到?
A:确保每次修改后都调用了end()或close()(Preferences类中end()即关闭)。 - Q:
begin()失败返回 false?
A:检查 NVS 分区是否在分区表中被正确配置,或 Flash 存储空间是否已满。 - Q:可以存储数组吗?
A:可以,使用putBytes和getBytes来存储和读取整个数组。 - Q:如何清空所有数据?
A:在 Arduino IDE 的“工具”菜单中,选择“擦除 Flash”选项。在代码中,可以对特定命名空间使用clear()。
7. 总结
Preferences 库是 ESP32 Arduino 开发中管理非易失性数据的利器。它通过简单的键值对 API 和内置的磨损均衡机制,让数据持久化变得安全便捷。掌握其核心函数和最佳实践,可以有效地存储设备配置、运行状态和用户设置,提升项目的可靠性。
建议在实际项目中,结合具体需求规划命名空间和键名,并养成良好的“打开-关闭”习惯,确保数据完整性。