EEPROM
EEPROM 地址空间:
- 每个字节有唯一地址(从 0 开始),例如 ATmega328P 的地址范围是 0~1023(共 1KB)。
- 不同型号的 Arduino 板 EEPROM 大小不同(如 Mega2560 为 4KB,地址 0~4095)。
关键函数说明
EEPROM.read(address)
-
功能:读取指定地址的字节数据。
-
参数:
address
:EEPROM 地址(int
类型,范围 0~EEPROM 大小 - 1)。
-
返回值:
- 成功:该地址存储的字节值(
byte
类型)。 - 失败:若地址超出范围,返回值未定义(可能为 0xFF)。
- 成功:该地址存储的字节值(
-
示例:
byte value = EEPROM.read(0); // 读取地址 0 的值
EEPROM.write(address, value)
-
功能:向指定地址写入字节数据。
-
参数:
address
:目标地址(int
)。value
:要写入的字节值(byte
)。 1 字节的取值范围是 0~255(无符号整数)或 -128~127(有符号整数)
-
注意事项:
- 写入耗时:约 3.3ms(ATmega328P),期间 Arduino 暂停执行。
- 擦写次数限制:约 100,000 次,频繁写入会缩短寿命。
- 仅当值不同时写入:若新值与当前值相同,不会执行写入操作,可减少磨损。
-
示例:
EEPROM.write(0, 65); // 在地址 0 写入值 65(即 ASCII 'A')
EEPROM.update(address, value)
-
功能:与
write()
类似,但仅在新值与当前值不同时执行写入,可减少不必要的擦写。 -
参数:同
write()
。 -
示例:
EEPROM.update(0, 65); // 仅当地址 0 的值不等于 65 时写入
EEPROM.get(address, variable)
-
功能:读取指定地址的数据到变量,支持任意数据类型(自动处理多字节)。
-
参数:
address
:起始地址(int
)。variable
:目标变量的引用(如int num; EEPROM.get(0, num);
)。
-
支持的数据类型:
- 基本类型:
byte
、int
、float
、double
等。 - 自定义类型:结构体、数组等(需确保地址空间足够)。
- 基本类型:
-
示例:
int number; EEPROM.get(0, number); // 从地址 0 开始读取一个 int(占用 2 字节) struct Config { float tempOffset; // 4 字节 bool enabled; // 1 字节 }; Config settings; EEPROM.get(2, settings); // 从地址 2 开始读取结构体(共 5 字节)
EEPROM.put(address, variable)
-
功能:将变量的值写入指定地址,支持任意数据类型。
-
参数:同
get()
。 -
示例:
int number = 1234; EEPROM.put(0, number); // 在地址 0 写入 int(占用 2 字节) Config settings = {25.5, true}; EEPROM.put(2, settings); // 在地址 2 写入结构体
EEPROM.length()
-
功能:返回 EEPROM 的总大小(字节数)。
-
返回值:
- ATmega328P:1024
- ATmega2560:4096
-
示例:
int size = EEPROM.length(); // 获取 EEPROM 总大小
注意
- 擦写寿命限制
- 虽然 EEPROM 支持约 100,000 次擦写,但频繁写入(如在
loop()
中循环写入)仍可能导致磨损。建议:- 仅在必要时写入数据(如配置变更时)。
- 对高频数据(如传感器读数)优先使用 RAM 缓存,定期批量写入 EEPROM。
- 虽然 EEPROM 支持约 100,000 次擦写,但频繁写入(如在
- 数据类型与对齐
- 使用
EEPROM.put()
/get()
处理非字节数据(如int
、float
)时,需确保地址对齐(例如,int
占用 2 字节,应从偶数地址开始写入)。
- 使用
- 原子性问题
- EEPROM 写入操作需要约 3.3ms(ATmega328P),期间若发生中断(如复位),可能导致数据不完整。建议:
- 写入关键数据前禁用中断(
noInterrupts()
),完成后恢复(interrupts()
)。
- 写入关键数据前禁用中断(
- EEPROM 写入操作需要约 3.3ms(ATmega328P),期间若发生中断(如复位),可能导致数据不完整。建议:
示例
#include <EEPROM.h>
void setup() {
// 写入数据到 EEPROM 地址 0
EEPROM.write(0, 'A'); // 写入单个字节
EEPROM.put(1, 12345); // 写入整数(自动处理多字节)
EEPROM.put(5, "Hello"); // 写入字符串
// 读取数据
char c = EEPROM.read(0); // 读取单个字节
int num;
EEPROM.get(1, num); // 读取整数
char str[6];
EEPROM.get(5, str); // 读取字符串
Serial.begin(9600);
Serial.print("Char: "); Serial.println(c);
Serial.print("Number: "); Serial.println(num);
Serial.print("String: "); Serial.println(str);
}
void loop() {
// 主循环
}
扩展EEPROM
1.I²C 接口 EEPROM 芯片
连接外部 EEPROM 芯片(如 AT24C32、AT24C256)
24C256为256Kbit,32KByte,具体容量看手册
使用 Wire.h 库通信
单个函数
byte读写
void writeEEPROM(int addr, byte data) {
Wire.beginTransmission(EEPROM_ADDR);
Wire.write((int)(addr >> 8)); // 发送高地址字节
Wire.write((int)(addr & 0xFF)); // 发送低地址字节
Wire.write(data); // 发送数据
Wire.endTransmission();
delay(5); // 等待写入完成
}
byte readEEPROM(int addr) {
byte data = 0xFF;
Wire.beginTransmission(EEPROM_ADDR);
Wire.write((int)(addr >> 8)); // 发送高地址字节
Wire.write((int)(addr & 0xFF)); // 发送低地址字节
Wire.endTransmission();
Wire.requestFrom(EEPROM_ADDR, 1);
if (Wire.available()) data = Wire.read();
return data;
}
在 I2C 通信中,确实需要先指定地址,再写入数据。这正是代码中 Wire.write((int)(addr >> 8));
和 Wire.write((int)(addr & 0xFF));
的作用 —— 它们是在设置目标地址,而非直接写入数据。
一、I2C EEPROM 的通信流程
I2C EEPROM 的写入过程分为两个阶段:
- 设置地址阶段:告诉 EEPROM “我接下来要操作哪个内存地址”。
- 传输数据阶段:发送真正要写入的数据。
代码中的执行顺序完全符合这个流程:
Wire.beginTransmission(EEPROM_ADDR); // 1. 开始与EEPROM通信
Wire.write((int)(addr >> 8)); // 2. 发送地址的高8位
Wire.write((int)(addr & 0xFF)); // 3. 发送地址的低8位
Wire.write(data); // 4. 发送要写入的数据
Wire.endTransmission(); // 5. 结束通信
二、为什么需要拆分地址?
EEPROM 的地址空间通常很大(例如:24LC256 芯片有 32,768 字节 = 2^15),需要 16 位(2 字节)才能完整表示。但 I2C 协议每次只能传输 8 位数据,因此必须将 16 位地址拆分成两个 8 位字节分别发送。
- 高地址字节:表示地址的高位部分(范围:0-255)。
- 低地址字节:表示地址的低位部分(范围:0-255)。
例如,地址0x1234
需要拆分为:
- 高地址字节:
0x12
(二进制0001 0010
) - 低地址字节:
0x34
(二进制0011 0100
)
三、代码详解
Wire.write((int)(addr >> 8));
addr >> 8
:将地址右移 8 位,丢弃低 8 位,只保留高 8 位。
例如:addr = 0x1234
→0x1234 >> 8 = 0x12
。(int)
:强制类型转换为int
(实际转换为uint8_t
更合适,但 Arduino 的 Wire 库会自动截断为 8 位)。- 作用:发送地址的高 8 位字节。
Wire.write((int)(addr & 0xFF));
addr & 0xFF
:将地址与0xFF
(二进制1111 1111
)按位与,只保留低 8 位。
例如:addr = 0x1234
→0x1234 & 0xFF = 0x34
。(int)
:同样为强制类型转换(实际发送时会截断为 8 位)。- 作用:发送地址的低 8 位字节。
四、为什么要这样设计?
这是由 I2C 协议和 EEPROM 的通信机制决定的:
- I2C 协议限制:每次只能发送 1 个字节的数据,因此 16 位地址必须分两次发送。
- EEPROM 寻址方式:大多数 I2C EEPROM 要求先发送高地址字节,再发送低地址字节,最后发送数据。
五、常见问题
1. 为什么不直接发送addr
?
addr
是 16 位整数(2 字节),而Wire.write()
只能发送 8 位数据。直接发送会导致数据截断,丢失高 8 位信息。
2. 能否交换高低字节的发送顺序?
- 不能。EEPROM 严格要求先接收高地址字节,再接收低地址字节。顺序错误会导致寻址错误。
3. 为什么需要(int)
强制类型转换?
- 严格来说,这里应转换为
(uint8_t)
以明确指定 8 位类型。但 Arduino 的 Wire 库会自动处理类型转换,因此(int)
也能正常工作(但可能引发编译器警告)。
六、优化建议
为提高代码可读性和安全性,建议修改为:
Wire.write((uint8_t)(addr >> 8)); // 高地址字节(明确转换为8位)
Wire.write((uint8_t)(addr & 0xFF)); // 低地址字节(明确转换为8位)
string读写
void writeString(int addr, const char* str) {
int i = 0;
while (str[i] != '\0') { // 循环直到字符串结束符
writeEEPROM(addr + i, str[i]); // 逐个字节写入
i++;
}
writeEEPROM(addr + i, '\0'); // 写入字符串结束符
}
void readString(int addr, char* buffer, int maxLength) {
int i = 0;
char c;
while (i < maxLength - 1) { // 防止缓冲区溢出
c = readEEPROM(addr + i);
if (c == '\0') break; // 遇到结束符则停止
buffer[i] = c;
i++;
}
buffer[i] = '\0'; // 确保字符串以 '\0' 结尾
}
-----------------
// 写入字符串
writeString(10, "Hello"); // 从地址 10 开始写入 "Hello"
// 读取字符串
char buffer[20];
readString(10, buffer, 20); // 从地址 10 读取字符串到 buffer
Serial.println(buffer); // 输出: Hello
int读写
void writeInt(int addr, int value) {
writeEEPROM(addr, (value >> 0) & 0xFF); // 低字节
writeEEPROM(addr + 1, (value >> 8) & 0xFF); // 高字节
}
int readInt(int addr) {
int low = readEEPROM(addr);
int high = readEEPROM(addr + 1);
return (high << 8) | low;
}
float读写
void writeFloat(int addr, float value) {
byte* ptr = (byte*)&value;
for (int i = 0; i < 4; i++) {
writeEEPROM(addr + i, ptr[i]); // 逐个字节写入
}
}
float readFloat(int addr) {
float value;
byte* ptr = (byte*)&value;
for (int i = 0; i < 4; i++) {
ptr[i] = readEEPROM(addr + i); // 逐个字节读取
}
return value;
}
注意事项
地址空间规划
-
写入多字节数据时,需确保地址空间足够且不重叠。例如:
writeString(0, "Hello"); // 占用地址 0~5(包含 '\0') writeInt(10, 1234); // 从地址 10 开始写入,避免冲突
字符串长度限制
-
读取字符串时需提供缓冲区大小,防止溢出:
char buffer[10]; readString(0, buffer, 10); // 最多读取 9 个字符 + '\0'
字节序(Endianness)
- 写入多字节数据(如
int
、float
)时,需注意字节序(大端 / 小端)。上述示例为 小端序(低字节在前)。
在计算机中,多字节数据(如 int
、float
等占 2 字节以上的数据类型)在内存或存储设备中的字节排列顺序称为 字节序(Endianness)。字节序主要分为两种:大端序(Big-Endian)和小端序(Little-Endian)。以下结合示例详细说明:
字节序的核心区别
假设要存储一个 16 位整数 0x1234
(二进制 00010010 00110100
):
字节序类型 | 内存 / 存储中的字节顺序(低地址 → 高地址) | 直观表示 |
---|---|---|
大端序 | 先存高位字节(0x12 ),再存低位字节(0x34 ) | [12][34] |
小端序 | 先存低位字节(0x34 ),再存高位字节(0x12 ) | [34][12] |
示例代码中的小端序问题
在你提供的示例代码中,当写入 int
、float
等多字节数据时(例如通过 (uint8_t*)&data
强制类型转换),默认使用的是小端序,原因如下:
1. 小端序的本质:低字节在前
以 32 位整数 0x12345678
为例:
- 内存中的二进制表示(小端序):
低地址 → 高地址:0x78
(最低位字节)→0x56
→0x34
→0x12
(最高位字节)
存储顺序:[78][56][34][12]
- 代码中的体现:
当通过writePage
写入int
类型变量时,会将其强制转换为uint8_t
数组,按字节顺序逐个写入 EEPROM。由于 Arduino 开发板(如 AVR、ESP32 等)通常使用 小端序架构,转换后的字节数组会按 低字节在前 的顺序存储。
2. 大端序与小端序的兼容性问题
- 如果接收方(如其他单片机、上位机软件)使用 大端序 解析数据,直接读取小端序存储的字节会导致错误。
例:存储int a = 0x1234
(小端序存储为[34][12]
),若按大端序解析会被误读为0x3412
。
如何处理字节序问题?
为确保多字节数据在不同设备间正确解析,需显式指定字节序。以下是两种常见方案:
1. 统一使用大端序(网络序)
适用场景:跨设备通信(如通过网络传输数据)。
实现方法:手动将数据转换为大端序后再写入。
// 将 16 位整数转换为大端序字节数组
void int16ToBigEndian(uint16_t value, uint8_t* buffer) {
buffer[0] = (value >> 8) & 0xFF; // 高位字节(大端序优先)
buffer[1] = value & 0xFF; // 低位字节
}
// 示例:写入大端序的 int16 数据
uint16_t number = 0x1234;
uint8_t buffer[2];
int16ToBigEndian(number, buffer);
eeprom.writePage(0, buffer, 2); // 存储为 [12][34](大端序)
2. 读写时保持一致的字节序
适用场景:同一设备(或同架构设备)的读写操作。
原则:写入时按小端序存储,读取时也按小端序解析。
// 写入小端序的 int32 数据
uint32_t value = 0x12345678;
eeprom.writePage(0, (uint8_t*)&value, 4); // 存储为 [78][56][34][12]
// 读取时按小端序解析
uint32_t readValue = 0;
for (int i = 0; i < 4; i++) {
((uint8_t*)&readValue)[i] = eeprom.readByte(i); // 按顺序填充低到高字节
}
// 解析结果:readValue = 0x12345678(正确)
为什么 Arduino 默认使用小端序?
- 硬件架构:Arduino 常用的 AVR 芯片(如 ATmega328P)和 ESP32 均为 小端序架构,内存中多字节数据按小端序存储。
- 强制类型转换的本质:通过
(uint8_t*)&data
直接读取变量的字节时,会按硬件架构的字节序获取数据。
总结:关键操作建议
- 明确需求:
- 若数据仅在同一设备读写,可直接使用小端序(与 Arduino 架构一致)。
- 若涉及跨设备通信或大端序系统(如某些嵌入式处理器、网络协议),需手动转换字节序。
- 避免隐式转换:
多字节数据写入 EEPROM 时,尽量通过自定义函数显式处理字节序,避免因架构差异导致的兼容性问题。 - 测试验证:
写入后通过串口打印或其他设备读取字节,确认存储顺序是否符合预期。
通过显式处理字节序,可确保多字节数据在存储和传输过程中保持一致性。
EEPROM_I2C.H库封装
通过封装函数,可实现字符串和其他数据类型的读写,但本质仍是通过多次调用单字节读写函数完成。使用时需注意地址规划、字符串结束符和缓冲区大小。
#ifndef EEPROM_I2C_H
#define EEPROM_I2C_H
#include <Wire.h>
class EEPROM_I2C {
private:
uint8_t deviceAddress; // EEPROM 设备地址
uint32_t pageSize; // 页大小(用于页写入优化)
uint32_t maxAddress; // 最大地址
public:
// 构造函数
EEPROM_I2C(uint8_t addr, uint32_t pageSize = 64, uint32_t maxAddr = 0xFFFF)
: deviceAddress(addr), pageSize(pageSize), maxAddress(maxAddr) {}
// 初始化 I2C 总线
void begin() {
Wire.begin();
}
// 写入单个字节
void writeByte(uint16_t address, uint8_t value) {
if (address > maxAddress) return;
Wire.beginTransmission(deviceAddress);
Wire.write((uint8_t)(address >> 8)); // 高地址字节
Wire.write((uint8_t)(address & 0xFF)); // 低地址字节
Wire.write(value);
Wire.endTransmission();
delay(5); // EEPROM 写入需要时间
}
// 读取单个字节
uint8_t readByte(uint16_t address) {
if (address > maxAddress) return 0xFF;
Wire.beginTransmission(deviceAddress);
Wire.write((uint8_t)(address >> 8)); // 高地址字节
Wire.write((uint8_t)(address & 0xFF)); // 低地址字节
Wire.endTransmission(false); // 保持连接
Wire.requestFrom(deviceAddress, (uint8_t)1);
if (Wire.available()) {
return Wire.read();
}
return 0xFF;
}
// 写入字符串
void writeString(uint16_t address, const char* str) {
uint16_t i = 0;
while (str[i] != '\0' && (address + i) <= maxAddress) {
writeByte(address + i, str[i]);
i++;
}
if ((address + i) <= maxAddress) {
writeByte(address + i, '\0'); // 写入字符串结束符
}
}
// 读取字符串
uint16_t readString(uint16_t address, char* buffer, uint16_t maxLength) {
uint16_t i = 0;
char c;
while (i < maxLength - 1 && (address + i) <= maxAddress) {
c = readByte(address + i);
if (c == '\0') break; // 遇到字符串结束符
buffer[i] = c;
i++;
}
buffer[i] = '\0'; // 确保字符串以 '\0' 结尾
return i; // 返回实际读取的字符数
}
// 写入整数
void writeInt(uint16_t address, int value) {
if (address + 1 > maxAddress) return;
writeByte(address, (uint8_t)(value & 0xFF)); // 低字节
writeByte(address + 1, (uint8_t)((value >> 8) & 0xFF)); // 高字节
}
// 读取整数
int readInt(uint16_t address) {
if (address + 1 > maxAddress) return 0;
uint8_t low = readByte(address);
uint8_t high = readByte(address + 1);
return (high << 8) | low;
}
// 写入浮点数
void writeFloat(uint16_t address, float value) {
if (address + 3 > maxAddress) return;
uint8_t* ptr = (uint8_t*)&value;
for (int i = 0; i < 4; i++) {
writeByte(address + i, ptr[i]);
}
}
// 读取浮点数
float readFloat(uint16_t address) {
if (address + 3 > maxAddress) return 0.0f;
float value;
uint8_t* ptr = (uint8_t*)&value;
for (int i = 0; i < 4; i++) {
ptr[i] = readByte(address + i);
}
return value;
}
// 页写入(优化批量写入)
void writePage(uint16_t address, const uint8_t* data, uint16_t length) {
if (address + length > maxAddress) return;
uint16_t bytesWritten = 0;
while (bytesWritten < length) {
// 计算当前页可写入的字节数
uint16_t pageOffset = address % pageSize;
uint16_t bytesThisPage = min(length - bytesWritten, pageSize - pageOffset);
Wire.beginTransmission(deviceAddress);
Wire.write((uint8_t)(address >> 8)); // 高地址字节
Wire.write((uint8_t)(address & 0xFF)); // 低地址字节
for (uint16_t i = 0; i < bytesThisPage; i++) {
Wire.write(data[bytesWritten + i]);
}
Wire.endTransmission();
delay(5); // 等待页写入完成
address += bytesThisPage;
bytesWritten += bytesThisPage;
}
}
};
#endif // EEPROM_I2C_H
调用
#include <Wire.h>
#include "EEPROM_I2C.h"
// 创建 EEPROM 对象 (地址 0x50, 页大小 64 字节, 最大地址 0x7FFF = 32767)
EEPROM_I2C eeprom(0x50, 64, 0x7FFF);
void setup() {
Serial.begin(115200);
eeprom.begin();
// 写入数据
eeprom.writeByte(0, 0xAA); // 写入单个字节
eeprom.writeString(10, "Hello, EEPROM!"); // 写入字符串
eeprom.writeInt(50, 12345); // 写入整数
eeprom.writeFloat(100, 3.14159f); // 写入浮点数
// 读取数据
uint8_t byteValue = eeprom.readByte(0);
char stringBuffer[20];
eeprom.readString(10, stringBuffer, 20);
int intValue = eeprom.readInt(50);
float floatValue = eeprom.readFloat(100);
// 输出结果
Serial.print("Byte value: 0x");
Serial.println(byteValue, HEX);
Serial.print("String value: ");
Serial.println(stringBuffer);
Serial.print("Int value: ");
Serial.println(intValue);
Serial.print("Float value: ");
Serial.println(floatValue, 5);
}
void loop() {
// 主循环可以留空
}
writePage()
#include <Wire.h>
#include "EEPROM_I2C.h"
// 创建 EEPROM 对象 (地址 0x50, 页大小 64 字节, 最大地址 32767)
EEPROM_I2C eeprom(0x50, 64, 0x7FFF);
void setup() {
Serial.begin(115200);
eeprom.begin();
// 示例1:写入一个小数组
uint8_t data1[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
eeprom.writePage(0, data1, 10); // 从地址0开始写入10字节
// 示例2:写入一个大数据块(跨页写入)
uint8_t data2[100];
for (int i = 0; i < 100; i++) {
data2[i] = i; // 填充0~99的数据
}
eeprom.writePage(100, data2, 100); // 从地址100开始写入100字节(跨多个页)
// 验证读取
Serial.println("Reading back data...");
// 读取第一个数组
Serial.print("Data1: ");
for (int i = 0; i < 10; i++) {
Serial.print(eeprom.readByte(i));
Serial.print(" ");
}
Serial.println();
// 读取第二个数组
Serial.print("Data2: ");
for (int i = 0; i < 100; i++) {
if (i % 20 == 0 && i > 0) Serial.println(); // 每20个字节换行
Serial.print(eeprom.readByte(100 + i));
Serial.print(" ");
}
Serial.println();
}
void loop() {
// 无需循环操作
}
结构体数据读写
struct SensorData {
float temperature;
int humidity;
uint8_t status;
};
void setup() {
// ...
// 写入结构体
SensorData data = {25.5f, 60, 0x01};
eeprom.writePage(200, (uint8_t*)&data, sizeof(SensorData));
// 读取结构体
SensorData readData;
for (int i = 0; i < sizeof(SensorData); i++) {
((uint8_t*)&readData)[i] = eeprom.readByte(200 + i);
}
Serial.print("Temperature: ");
Serial.println(readData.temperature);
// ...
}
关键特性
- 页写入优化
- 自动处理跨页写入,避免数据被截断
- 例如:对于 64 字节页大小的 EEPROM,写入 100 字节数据时会自动分 2 次传输
- 性能提升
- 相比逐字节写入,页写入可减少 I2C 传输次数
- 例如:写入 64 字节数据时,页写入只需 1 次传输(而非 64 次)
- 安全机制
- 自动检查地址范围,防止越界写入
- 每次页写入后等待 5ms,确保 EEPROM 完成内部写入操作
使用注意事项
- 页大小限制
- 不同 EEPROM 型号的页大小不同(常见:16/32/64/128 字节)
- 初始化时需正确设置页大小(如
eeprom(0x50, 64, ...)
)
- 写入延时
- 每次页写入后必须等待 5ms(EEPROM 写入周期)
- 频繁写入会影响系统响应速度,建议批量操作
- 数据类型
writePage
仅支持uint8_t
数组- 如需写入其他类型(如字符串、结构体),需先转换为字节数组
- 地址对齐
- 尽量从页边界开始写入(如地址 0、64、128…)
- 非对齐地址会导致页内剩余空间浪费
I2C_EEPROM库
https://github.com/RobTillaart/I2C_EEPROM
1. I2C_eeprom
类
构造函数
I2C_eeprom(uint8_t deviceAddress, TwoWire *wire = &Wire)
- 功能:创建一个
I2C_eeprom
对象,指定 EEPROM 设备地址和 I2C 总线。 - 参数:
deviceAddress
:EEPROM 设备的 I2C 地址。wire
:可选参数,指定 I2C 总线对象,默认为Wire
。
- 示例:
#include "I2C_eeprom.h"
I2C_eeprom eeprom(0x50); // 创建一个地址为 0x50 的 EEPROM 对象
I2C_eeprom(uint8_t deviceAddress, uint32_t deviceSize, TwoWire *wire = &Wire)
- 功能:创建一个
I2C_eeprom
对象,指定 EEPROM 设备地址、设备大小和 I2C 总线。 - 参数:
deviceAddress
:EEPROM 设备的 I2C 地址。deviceSize
:EEPROM 设备的大小。wire
:可选参数,指定 I2C 总线对象,默认为Wire
。
- 示例:
#include "I2C_eeprom.h"
I2C_eeprom eeprom(0x50, 0x8000); // 创建一个地址为 0x50,大小为 0x8000 的 EEPROM 对象
初始化函数
bool begin(uint8_t writeProtectPin = -1)
- 功能:初始化 I2C 总线,检查设备地址是否可用。
- 参数:
writeProtectPin
:可选参数,指定写保护引脚,默认为 -1 表示不使用写保护。
- 返回值:如果初始化成功返回
true
,否则返回false
。 - 示例:
#include "I2C_eeprom.h"
I2C_eeprom eeprom(0x50);
if (eeprom.begin()) {
Serial.println("EEPROM initialized successfully");
} else {
Serial.println("Failed to initialize EEPROM");
}
bool isConnected()
- 功能:测试设备地址是否在总线上可用。
- 返回值:如果设备地址可用返回
true
,否则返回false
。 - 示例:
#include "I2C_eeprom.h"
I2C_eeprom eeprom(0x50);
eeprom.begin();
if (eeprom.isConnected()) {
Serial.println("EEPROM is connected");
} else {
Serial.println("EEPROM is not connected");
}
读写函数
int writeByte(uint16_t memoryAddress, uint8_t value)
- 功能:向指定内存地址写入单个字节。
- 参数:
memoryAddress
:要写入的内存地址。value
:要写入的字节值。
- 返回值:如果写入成功返回 0,否则返回错误码。
- 示例:
#include "I2C_eeprom.h"
I2C_eeprom eeprom(0x50);
eeprom.begin();
eeprom.writeByte(0x10, 0xAA); // 在地址 0x10 写入 0xAA
bool read(T &buffer)
和 bool read(T *buffer)
- 功能:从 EEPROM 读取数据到缓冲区。
- 参数:
buffer
:要读取数据的缓冲区。
- 返回值:如果读取成功返回
true
,否则返回false
。 - 示例:
#include "I2C_eeprom.h"
I2C_eeprom eeprom(0x50);
eeprom.begin();
uint8_t data;
eeprom.read(data); // 读取数据到 data 变量
bool write(T &buffer)
和 bool write(T *buffer)
- 功能:将缓冲区数据写入 EEPROM。
- 参数:
buffer
:要写入的数据缓冲区。
- 返回值:如果写入成功返回
true
,否则返回false
。 - 示例:
#include "I2C_eeprom.h"
I2C_eeprom eeprom(0x50);
eeprom.begin();
uint8_t data = 0xBB;
eeprom.write(data); // 将 data 变量的值写入 EEPROM
更新函数
int updateByte(uint16_t memoryAddress, uint8_t value)
- 功能:写入单个字节,但仅当值发生变化时才写入。
- 参数:
memoryAddress
:要写入的内存地址。value
:要写入的字节值。
- 返回值:如果值相同或写入成功返回 0。
- 示例:
#include "I2C_eeprom.h"
I2C_eeprom eeprom(0x50);
eeprom.begin();
eeprom.updateByte(0x20, 0xCC); // 在地址 0x20 更新字节值
uint16_t updateBlock(uint16_t memoryAddress, uint8_t * buffer, uint16_t length)
- 功能:从指定内存地址开始写入缓冲区,但仅当数据发生变化时才写入。
- 参数:
memoryAddress
:要写入的起始内存地址。buffer
:要写入的数据缓冲区。length
:要写入的数据长度。
- 返回值:实际写入的字节数,小于等于
length
。 - 示例:
#include "I2C_eeprom.h"
I2C_eeprom eeprom(0x50);
eeprom.begin();
uint8_t buffer[] = {0x11, 0x22, 0x33};
eeprom.updateBlock(0x30, buffer, sizeof(buffer)); // 在地址 0x30 开始更新数据块
2. I2C_eeprom_cyclic_store
类
- 延长 EEPROM 寿命:EEPROM 有写入次数的限制,频繁在同一位置写入数据会导致该位置的寿命提前耗尽。
I2C_eeprom_cyclic_store
类通过将写入操作分散到 EEPROM 的不同页面,减少了对单个页面的写入次数,从而延长了整个 EEPROM 的使用寿命。 - 数据版本管理:该类会为每次写入的数据添加一个版本号,在初始化时会扫描 EEPROM 中所有存储槽,找到版本号最高的槽位,将其作为当前数据进行读取。这确保了每次读取的数据都是最新写入的版本。
- 循环写入机制:当数据写入到 EEPROM 分配区域的末尾时,会自动回到起始位置继续写入,实现循环写入。这种机制使得 EEPROM 可以持续使用,而不需要手动管理写入位置。
初始化函数
bool begin(I2C_eeprom &eeprom, uint8_t pageSize, uint16_t totalPages)
- 功能:初始化循环存储实例,搜索 EEPROM 中最新写入的版本并设置当前槽位。
- 参数:
eeprom
:要使用的I2C_eeprom
实例。pageSize
:每个写入页的字节数。totalPages
:要使用的总页数。
- 返回值:如果初始化成功返回
true
,否则返回false
。 - 示例:
#include "I2C_eeprom.h"
#include "I2C_eeprom_cyclic_store.h"
I2C_eeprom eeprom(0x50);
eeprom.begin();
I2C_eeprom_cyclic_store<uint8_t> cyclicStore;
if (cyclicStore.begin(eeprom, 32, 4)) {
Serial.println("Cyclic store initialized successfully");
} else {
Serial.println("Failed to initialize cyclic store");
}
格式化函数
bool format()
- 功能:格式化 EEPROM,清除数据。
- 返回值:如果成功返回
true
,如果无法写入 EEPROM 则返回false
。 - 示例:
#include "I2C_eeprom.h"
#include "I2C_eeprom_cyclic_store.h"
I2C_eeprom eeprom(0x50);
eeprom.begin();
I2C_eeprom_cyclic_store<uint8_t> cyclicStore;
cyclicStore.begin(eeprom, 32, 4);
if (cyclicStore.format()) {
Serial.println("EEPROM formatted successfully");
} else {
Serial.println("Failed to format EEPROM");
}
读写函数
bool read(T &buffer)
和 bool read(T *buffer)
- 功能:从 EEPROM 的当前槽位读取数据到缓冲区。
- 参数:
buffer
:要读取数据的缓冲区。
- 返回值:如果数据读取成功返回
true
,否则返回false
。 - 示例:
#include "I2C_eeprom.h"
#include "I2C_eeprom_cyclic_store.h"
I2C_eeprom eeprom(0x50);
eeprom.begin();
I2C_eeprom_cyclic_store<uint8_t> cyclicStore;
cyclicStore.begin(eeprom, 32, 4);
uint8_t data;
if (cyclicStore.read(data)) {
Serial.println("Data read successfully");
} else {
Serial.println("Failed to read data");
}
bool write(T &buffer)
和 bool write(T *buffer)
- 功能:将缓冲区数据写入 EEPROM 的下一个槽位。
- 参数:
buffer
:要写入的数据缓冲区。
- 返回值:如果数据写入成功返回
true
,否则返回false
。 - 示例
#include "I2C_eeprom.h"
#include "I2C_eeprom_cyclic_store.h"
I2C_eeprom eeprom(0x50);
eeprom.begin();
I2C_eeprom_cyclic_store<uint8_t> cyclicStore;
cyclicStore.begin(eeprom, 32, 4);
uint8_t data = 0xDD;
if (cyclicStore.write(data)) {
Serial.println("Data written successfully");
} else {
Serial.println("Failed to write data");
}
指标函数
bool getMetrics(uint16_t &slots, uint32_t &writeCounter)
- 功能:返回 EEPROM 使用指标。
- 参数:
slots
:用于存储写入数据缓冲区的槽位数。writeCounter
:用于存储自上次格式化(或首次使用)以来的总写入次数。
- 返回值:如果实例已初始化返回
true
,否则返回false
。 - 示例:
#include "I2C_eeprom.h"
#include "I2C_eeprom_cyclic_store.h"
I2C_eeprom eeprom(0x50);
eeprom.begin();
I2C_eeprom_cyclic_store<uint8_t> cyclicStore;
cyclicStore.begin(eeprom, 32, 4);
uint16_t slots;
uint32_t writeCounter;
if (cyclicStore.getMetrics(slots, writeCounter)) {
Serial.print("Slots: ");
Serial.println(slots);
Serial.print("Write counter: ");
Serial.println(writeCounter);
} else {
Serial.println("Failed to get metrics");
}
示例
#include "I2C_eeprom.h"
#include "I2C_eeprom_cyclic_store.h"
#define I2C_EEPROM_ADDR 0x50
#define I2C_EEPROM_SIZE 0x1000
// 定义要存储的数据结构
struct DummyTestData {
uint8_t padding;
};
void setup() {
Serial.begin(9600);
// 创建 I2C_eeprom 实例
I2C_eeprom eeprom(I2C_EEPROM_ADDR, I2C_EEPROM_SIZE);
eeprom.begin();
// 创建 I2C_eeprom_cyclic_store 实例
I2C_eeprom_cyclic_store<DummyTestData> cyclicStore;
// 初始化循环存储实例
if (cyclicStore.begin(eeprom, 32, 4)) {
Serial.println("Cyclic store initialized successfully");
// 准备要写入的数据
DummyTestData data;
data.padding = 0xAA;
// 写入数据
if (cyclicStore.write(data)) {
Serial.println("Data written successfully");
} else {
Serial.println("Failed to write data");
}
// 读取数据
DummyTestData readData;
if (cyclicStore.read(readData)) {
Serial.print("Read data: ");
Serial.println(readData.padding, HEX);
} else {
Serial.println("Failed to read data");
}
} else {
Serial.println("Failed to initialize cyclic store");
}
}
void loop() {
// 主循环中可以添加其他操作
}
2.SPI 接口 EEPROM 芯片
- 适用于高速读写场景,使用
SPI.h
库通信(如 Microchip 25LC512(64KB))。