跳到主要内容

数据存储

在 Arduino 项目中,经常需要保存配置参数、传感器数据或运行状态。本章将介绍几种常用的数据存储方案。

EEPROM 存储

EEPROM(电可擦可编程只读存储器)是 Arduino 内置的非易失性存储器,断电后数据不会丢失。Arduino Uno 的 ATmega328P 有 1024 字节的 EEPROM。

EEPROM 特点

特性说明
容量Uno: 1KB, Mega: 4KB
读写次数约 10 万次写入
保存时间约 20 年
访问速度约 3.3ms 每字节

基本读写操作

需要包含 EEPROM.h 库:

#include <EEPROM.h>

void setup() {
Serial.begin(9600);

// 写入单个字节
EEPROM.write(0, 123); // 地址 0 写入值 123

// 读取单个字节
byte value = EEPROM.read(0);

Serial.print("读取的值: ");
Serial.println(value);
}

void loop() {}

写入不同类型数据

EEPROM 每个地址只能存储一个字节(0-255)。存储更大的数据需要使用 put()get()

#include <EEPROM.h>

// 存储地址定义
const int ADDR_INT = 0; // int 占 2 字节
const int ADDR_FLOAT = 2; // float 占 4 字节
const int ADDR_STRING = 6; // 字符串

void setup() {
Serial.begin(9600);

// 存储整数
int counter = 12345;
EEPROM.put(ADDR_INT, counter);

// 存储浮点数
float temperature = 23.56;
EEPROM.put(ADDR_FLOAT, temperature);

// 存储字符串
char name[20] = "Arduino";
EEPROM.put(ADDR_STRING, name);

Serial.println("数据已保存");

// 读取验证
int readCounter;
float readTemp;
char readName[20];

EEPROM.get(ADDR_INT, readCounter);
EEPROM.get(ADDR_FLOAT, readTemp);
EEPROM.get(ADDR_STRING, readName);

Serial.print("计数器: "); Serial.println(readCounter);
Serial.print("温度: "); Serial.println(readTemp);
Serial.print("名称: "); Serial.println(readName);
}

void loop() {}

存储自定义结构体

使用结构体可以方便地存储配置信息:

#include <EEPROM.h>

// 配置结构体
struct Config {
int deviceId;
float calibration;
char ssid[32];
char password[64];
bool enabled;
};

const int CONFIG_ADDR = 0;
Config config;

void setup() {
Serial.begin(9600);

// 设置配置
config.deviceId = 1001;
config.calibration = 1.0234;
strcpy(config.ssid, "MyWiFi");
strcpy(config.password, "MyPassword");
config.enabled = true;

// 保存配置
saveConfig();
Serial.println("配置已保存");

// 读取配置
loadConfig();
printConfig();
}

void loop() {}

void saveConfig() {
EEPROM.put(CONFIG_ADDR, config);
// 注意:ESP8266/ESP32 需要 EEPROM.commit() 才能真正写入
// 标准 Arduino(Uno、Mega 等)不需要 commit()
}

void loadConfig() {
EEPROM.get(CONFIG_ADDR, config);
}

void printConfig() {
Serial.println("=== 当前配置 ===");
Serial.print("设备 ID: "); Serial.println(config.deviceId);
Serial.print("校准值: "); Serial.println(config.calibration);
Serial.print("WiFi SSID: "); Serial.println(config.ssid);
Serial.print("启用状态: "); Serial.println(config.enabled ? "是" : "否");
}

update() 减少写入次数

update() 只在值变化时才写入,可以延长 EEPROM 寿命:

// write(): 每次都写入,即使值相同
EEPROM.write(0, 100); // 写入

// update(): 先检查值是否相同,不同才写入
EEPROM.update(0, 100); // 如果已经是 100,则不写入

记录运行次数

使用 EEPROM 记录 Arduino 的启动次数:

#include <EEPROM.h>

const int BOOT_COUNT_ADDR = 0;

void setup() {
Serial.begin(9600);

// 读取当前计数
int bootCount = 0;
EEPROM.get(BOOT_COUNT_ADDR, bootCount);

// 增加计数
bootCount++;

// 保存新计数
EEPROM.put(BOOT_COUNT_ADDR, bootCount);

Serial.print("这是第 ");
Serial.print(bootCount);
Serial.println(" 次启动");
}

void loop() {}

EEPROM 容量查询

#include <EEPROM.h>

void setup() {
Serial.begin(9600);

Serial.print("EEPROM 容量: ");
Serial.print(EEPROM.length());
Serial.println(" 字节");
}

void loop() {}

清空 EEPROM

#include <EEPROM.h>

void setup() {
Serial.begin(9600);
Serial.println("正在清空 EEPROM...");

for (int i = 0; i < EEPROM.length(); i++) {
EEPROM.write(i, 0);
}

Serial.println("EEPROM 已清空");
}

void loop() {}

SD 卡存储

当需要存储大量数据(如数据日志、配置文件)时,SD 卡是理想选择。

硬件连接(SPI 接口)

使用 SD 卡模块:

SD 模块    Arduino Uno
────────────────────
CS → D10
SCK → D13
MOSI → D11
MISO → D12
VCC → 5V 或 3.3V(取决于模块)
GND → GND

基本 SD 卡操作

需要包含 SD.hSPI.h 库:

#include <SD.h>
#include <SPI.h>

const int CHIP_SELECT = 10;

void setup() {
Serial.begin(9600);

// 初始化 SD 卡
if (!SD.begin(CHIP_SELECT)) {
Serial.println("SD 卡初始化失败!");
return;
}

Serial.println("SD 卡初始化成功");

// 获取卡类型
Serial.print("卡类型: ");
switch (SD.cardType()) {
case CARD_NONE:
Serial.println("无卡");
break;
case CARD_MMC:
Serial.println("MMC");
break;
case CARD_SD:
Serial.println("SD");
break;
case CARD_SDHC:
Serial.println("SDHC");
break;
}

// 获取容量
Serial.print("容量: ");
Serial.print((double)SD.cardSize() / 1024 / 1024);
Serial.println(" MB");
}

void loop() {}

写入文件

#include <SD.h>
#include <SPI.h>

const int CHIP_SELECT = 10;

void setup() {
Serial.begin(9600);

if (!SD.begin(CHIP_SELECT)) {
Serial.println("SD 卡初始化失败");
return;
}

// 打开文件(如果不存在会创建)
File dataFile = SD.open("datalog.txt", FILE_WRITE);

if (dataFile) {
dataFile.println("时间,温度,湿度");
dataFile.close();
Serial.println("文件写入成功");
} else {
Serial.println("无法打开文件");
}
}

void loop() {}

追加数据

#include <SD.h>
#include <SPI.h>

const int CHIP_SELECT = 10;

void setup() {
Serial.begin(9600);

if (!SD.begin(CHIP_SELECT)) {
Serial.println("SD 卡初始化失败");
return;
}
}

void loop() {
// 每秒记录一次数据
logData();
delay(1000);
}

void logData() {
// 打开文件以追加模式
File dataFile = SD.open("datalog.txt", FILE_WRITE);

if (dataFile) {
// 记录时间戳和数据
dataFile.print(millis());
dataFile.print(",");
dataFile.print(analogRead(A0));
dataFile.print(",");
dataFile.println(random(20, 30));
dataFile.close();

Serial.println("数据已记录");
} else {
Serial.println("写入失败");
}
}

读取文件

#include <SD.h>
#include <SPI.h>

const int CHIP_SELECT = 10;

void setup() {
Serial.begin(9600);

if (!SD.begin(CHIP_SELECT)) {
Serial.println("SD 卡初始化失败");
return;
}

// 打开文件读取
File dataFile = SD.open("datalog.txt");

if (dataFile) {
Serial.println("=== 文件内容 ===");

while (dataFile.available()) {
Serial.write(dataFile.read());
}

dataFile.close();
} else {
Serial.println("无法打开文件");
}
}

void loop() {}

检查文件是否存在

if (SD.exists("config.txt")) {
Serial.println("配置文件存在");
} else {
Serial.println("配置文件不存在");
}

删除文件

if (SD.exists("oldfile.txt")) {
SD.remove("oldfile.txt");
Serial.println("文件已删除");
}

创建和列出目录

#include <SD.h>
#include <SPI.h>

const int CHIP_SELECT = 10;

void setup() {
Serial.begin(9600);
SD.begin(CHIP_SELECT);

// 创建目录
SD.mkdir("logs");
Serial.println("目录已创建");

// 列出根目录文件
Serial.println("=== 根目录文件 ===");
File root = SD.open("/");
printDirectory(root, 0);
}

void printDirectory(File dir, int numTabs) {
while (true) {
File entry = dir.openNextFile();

if (!entry) {
break; // 没有更多文件
}

for (uint8_t i = 0; i < numTabs; i++) {
Serial.print('\t');
}

Serial.print(entry.name());

if (entry.isDirectory()) {
Serial.println("/");
printDirectory(entry, numTabs + 1);
} else {
Serial.print("\t\t");
Serial.println(entry.size());
}

entry.close();
}
}

void loop() {}

综合示例:数据记录器

结合传感器、EEPROM 和 SD 卡实现完整的数据记录系统:

#include <SD.h>
#include <SPI.h>
#include <DHT.h>

#define DHT_PIN 2
#define DHT_TYPE DHT22
#define CHIP_SELECT 10

DHT dht(DHT_PIN, DHT_TYPE);

// 配置存储地址
const int CONFIG_ADDR = 0;
struct Config {
int logInterval; // 记录间隔(秒)
float tempOffset; // 温度校准值
bool enableLogging; // 是否启用记录
};

Config config;
unsigned long lastLogTime = 0;

void setup() {
Serial.begin(9600);

// 初始化传感器
dht.begin();

// 初始化 SD 卡
if (!SD.begin(CHIP_SELECT)) {
Serial.println("SD 卡初始化失败,使用 EEPROM 存储");
} else {
Serial.println("SD 卡初始化成功");
initLogFile();
}

// 加载配置
loadConfig();

Serial.println("=== 数据记录器已启动 ===");
Serial.print("记录间隔: ");
Serial.print(config.logInterval);
Serial.println(" 秒");
}

void loop() {
// 按间隔记录数据
if (millis() - lastLogTime >= config.logInterval * 1000) {
recordData();
lastLogTime = millis();
}

// 处理串口命令
handleCommands();
}

void initLogFile() {
// 创建日志文件并写入表头
if (!SD.exists("sensor.csv")) {
File file = SD.open("sensor.csv", FILE_WRITE);
if (file) {
file.println("timestamp,temperature,humidity,light");
file.close();
}
}
}

void recordData() {
// 读取传感器
float temp = dht.readTemperature() + config.tempOffset;
float humidity = dht.readHumidity();
int light = analogRead(A0);
unsigned long timestamp = millis();

// 串口输出
Serial.print(timestamp);
Serial.print(" | T: ");
Serial.print(temp);
Serial.print("°C | H: ");
Serial.print(humidity);
Serial.print("% | L: ");
Serial.println(light);

// 写入 SD 卡
if (config.enableLogging) {
File file = SD.open("sensor.csv", FILE_WRITE);
if (file) {
file.print(timestamp);
file.print(",");
file.print(temp);
file.print(",");
file.print(humidity);
file.print(",");
file.println(light);
file.close();
}
}
}

void loadConfig() {
// 从 EEPROM 加载配置
EEPROM.get(CONFIG_ADDR, config);

// 检查配置是否有效
if (config.logInterval < 1 || config.logInterval > 3600) {
// 使用默认值
config.logInterval = 10;
config.tempOffset = 0;
config.enableLogging = true;
saveConfig();
}
}

void saveConfig() {
EEPROM.put(CONFIG_ADDR, config);
}

void handleCommands() {
if (Serial.available()) {
String cmd = Serial.readStringUntil('\n');
cmd.trim();

if (cmd.startsWith("interval ")) {
config.logInterval = cmd.substring(9).toInt();
saveConfig();
Serial.print("记录间隔已更新: ");
Serial.print(config.logInterval);
Serial.println(" 秒");
}
else if (cmd.startsWith("offset ")) {
config.tempOffset = cmd.substring(7).toFloat();
saveConfig();
Serial.print("温度偏移已更新: ");
Serial.println(config.tempOffset);
}
else if (cmd == "toggle") {
config.enableLogging = !config.enableLogging;
saveConfig();
Serial.println(config.enableLogging ? "记录已启用" : "记录已禁用");
}
else if (cmd == "status") {
Serial.println("=== 系统状态 ===");
Serial.print("记录状态: ");
Serial.println(config.enableLogging ? "启用" : "禁用");
Serial.print("记录间隔: ");
Serial.print(config.logInterval);
Serial.println(" 秒");
Serial.print("温度偏移: ");
Serial.println(config.tempOffset);
}
else {
Serial.println("命令: interval <秒>, offset <值>, toggle, status");
}
}
}

综合示例:配置管理系统

一个完整的配置存储和管理系统:

#include <EEPROM.h>

// 配置结构体
struct AppConfig {
char magic[4]; // 魔数,用于验证配置有效性
int version; // 配置版本
char deviceName[32]; // 设备名称
int baudRate; // 波特率
float calibration; // 校准系数
bool features[8]; // 功能开关
int thresholds[4]; // 阈值设置
};

const int CONFIG_ADDR = 0;
const char MAGIC[] = "ARDU";
const int CONFIG_VERSION = 1;

AppConfig config;

void setup() {
Serial.begin(9600);

// 加载或初始化配置
if (!loadConfig()) {
Serial.println("使用默认配置");
initDefaultConfig();
saveConfig();
}

printConfig();
}

void loop() {
// 配置管理菜单
if (Serial.available()) {
handleMenu();
}
}

bool loadConfig() {
EEPROM.get(CONFIG_ADDR, config);

// 验证魔数
if (strcmp(config.magic, MAGIC) != 0) {
return false; // 配置无效
}

// 验证版本
if (config.version != CONFIG_VERSION) {
migrateConfig(); // 版本迁移
}

return true;
}

void initDefaultConfig() {
strcpy(config.magic, MAGIC);
config.version = CONFIG_VERSION;
strcpy(config.deviceName, "MyArduino");
config.baudRate = 9600;
config.calibration = 1.0;

for (int i = 0; i < 8; i++) {
config.features[i] = false;
}
config.features[0] = true; // 默认启用第一个功能

for (int i = 0; i < 4; i++) {
config.thresholds[i] = 512;
}
}

void saveConfig() {
EEPROM.put(CONFIG_ADDR, config);
Serial.println("配置已保存");
}

void printConfig() {
Serial.println("\n=== 当前配置 ===");
Serial.print("设备名称: ");
Serial.println(config.deviceName);
Serial.print("波特率: ");
Serial.println(config.baudRate);
Serial.print("校准系数: ");
Serial.println(config.calibration);

Serial.println("功能开关:");
for (int i = 0; i < 8; i++) {
Serial.print(" 功能 ");
Serial.print(i);
Serial.print(": ");
Serial.println(config.features[i] ? "启用" : "禁用");
}

Serial.println("阈值设置:");
for (int i = 0; i < 4; i++) {
Serial.print(" 阈值 ");
Serial.print(i);
Serial.print(": ");
Serial.println(config.thresholds[i]);
}
}

void migrateConfig() {
// 处理配置版本迁移
Serial.println("配置版本迁移...");
config.version = CONFIG_VERSION;
// 根据旧版本进行必要的迁移
}

void handleMenu() {
Serial.println("\n=== 配置菜单 ===");
Serial.println("1. 查看配置");
Serial.println("2. 修改设备名称");
Serial.println("3. 修改波特率");
Serial.println("4. 修改校准系数");
Serial.println("5. 切换功能开关");
Serial.println("6. 修改阈值");
Serial.println("7. 重置为默认");
Serial.println("8. 保存配置");
Serial.println("请输入选项:");

while (!Serial.available());

int choice = Serial.parseInt();

switch (choice) {
case 1:
printConfig();
break;
case 2:
Serial.println("输入新名称:");
while (!Serial.available());
{
String name = Serial.readStringUntil('\n');
name.trim();
name.toCharArray(config.deviceName, 32);
}
break;
case 3:
Serial.println("输入波特率 (9600, 115200 等):");
while (!Serial.available());
config.baudRate = Serial.parseInt();
break;
case 4:
Serial.println("输入校准系数:");
while (!Serial.available());
config.calibration = Serial.parseFloat();
break;
case 5:
Serial.println("输入功能编号 (0-7):");
while (!Serial.available());
{
int idx = Serial.parseInt();
if (idx >= 0 && idx < 8) {
config.features[idx] = !config.features[idx];
Serial.print("功能 ");
Serial.print(idx);
Serial.println(config.features[idx] ? " 已启用" : " 已禁用");
}
}
break;
case 6:
Serial.println("输入阈值编号 (0-3) 和新值:");
while (!Serial.available());
{
int idx = Serial.parseInt();
while (!Serial.available());
int val = Serial.parseInt();
if (idx >= 0 && idx < 4) {
config.thresholds[idx] = val;
}
}
break;
case 7:
initDefaultConfig();
Serial.println("已重置为默认配置");
break;
case 8:
saveConfig();
break;
default:
Serial.println("无效选项");
}
}

存储方案对比

存储方式容量读写速度掉电保存适用场景
EEPROM1-4KB配置参数、校准值
SD 卡中等数据日志、大文件
Flash取决于芯片程序数据、大配置
SRAM2-8KB最快临时变量、缓冲

最佳实践

1. EEPROM 写入保护

EEPROM 写入次数有限,应尽量减少写入:

void updateConfig() {
// 检查是否真的需要更新
if (configChanged) {
EEPROM.put(CONFIG_ADDR, config);
configChanged = false;
}
}

2. 数据校验

使用校验和或 CRC 验证数据完整性:

uint16_t calculateCRC(void* data, size_t length) {
uint16_t crc = 0xFFFF;
uint8_t* bytes = (uint8_t*)data;

for (size_t i = 0; i < length; i++) {
crc ^= bytes[i];
for (int j = 0; j < 8; j++) {
if (crc & 1) {
crc = (crc >> 1) ^ 0xA001;
} else {
crc >>= 1;
}
}
}

return crc;
}

3. 数据备份

重要数据应考虑备份策略:

// 双区存储,交替写入
const int CONFIG_ADDR_1 = 0;
const int CONFIG_ADDR_2 = sizeof(Config) + 10;

void saveConfigWithBackup() {
static bool useBackup = false;

if (useBackup) {
EEPROM.put(CONFIG_ADDR_2, config);
} else {
EEPROM.put(CONFIG_ADDR_1, config);
}

useBackup = !useBackup;
}

常见问题

1. SD 卡初始化失败

可能原因

  • 卡未正确插入
  • 卡格式不正确(应为 FAT32)
  • 电源不稳定
  • CS 引脚连接错误

解决方法

  • 检查卡是否插好
  • 重新格式化为 FAT32
  • 使用独立电源供电

2. EEPROM 数据丢失

可能原因

  • 写入过程中断电
  • 地址冲突

解决方法

  • 使用校验和验证数据
  • 合理规划地址空间

3. 写入速度慢

原因:EEPROM 每次写入约 3.3ms

解决方法

  • 使用 update() 代替 write()
  • 批量写入时使用缓冲区

下一步

掌握了数据存储后,你已经具备了开发实用 Arduino 项目的能力。接下来我们将通过实战项目来综合运用所学知识。