EEPROM库详解
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 地址 0EEPROM.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))。
相关文章:
EEPROM库详解
EEPROM EEPROM 地址空间: 每个字节有唯一地址(从 0 开始),例如 ATmega328P 的地址范围是 0~1023(共 1KB)。不同型号的 Arduino 板 EEPROM 大小不同(如 Mega2560 为 4KB,地址 0~409…...
JDK21深度解密 Day 10:微服务架构适配JDK21
【JDK21深度解密 Day 10】微服务架构适配JDK21 引言:百万并发时代的微服务进化 作为"JDK21深度解密"系列的第10天,今天我们聚焦微服务架构在JDK21时代的技术跃迁。Java语言历史上最大的一次并发模型革新——虚拟线程(Virtual Threads),正在重塑微服务架构的底…...
Java并发编程实战 Day 2:线程安全与synchronized关键字
【Java并发编程实战 Day 2】线程安全与synchronized关键字 开篇 欢迎来到《Java并发编程实战》系列的第二天!在第一天中,我们学习了Java并发编程的基础知识以及线程模型的核心概念。今天我们将继续深入探讨并发编程中的关键问题——线程安全࿰…...

在win10/11下Node.js安装配置教程
下载安装 官网链接https://nodejs.org/zh-cn 下载好以后双击打开,点击下一步 勾选,然后下一步 选择路径、下一步 下一步 配置环境 找到我们安装的文件夹,创建两个文件夹 node_global node_cache 在CMD中配置路径 npm config set p…...

飞致云开源社区月度动态报告(2025年5月)
自2023年6月起,中国领先的开源软件公司飞致云以月度为单位发布《飞致云开源社区月度动态报告》,旨在向广大社区用户同步飞致云旗下系列开源软件的发展情况,以及当月主要的产品新版本发布、社区运营成果等相关信息。 飞致云开源运营数据概览&…...

压缩包方式在Linux和Windows下安装mongodb
目录 安装流程安装实例1. Linux安装2. Windows安装 总结 安装流程 zip方式安装 优点:自定义性较高,可以自己控制数据、日志等文件的位置 1、下载安装包 2、解压安装包 3、创建各类文件路径 4、配置conf文件 5、使用自定义配置文件启动 安装实例 1. Li…...

智慧场馆:科技赋能的艺术盛宴
智慧场馆作为城市公共服务设施数字化转型的典型代表,通过深度融合新一代信息技术,构建起全方位、智能化的运营管理体系。其功能架构不仅提升了场馆本身的运营效能,更重塑了公共服务体验模式,展现出显著的社会价值和商业潜力。 一…...
flutter常用动画
Flutter 动画基础概念 术语解释Animation表示动画的值,通常是一个 double (0.0 ~ 1.0) 或其他数值。AnimationController管理动画的时间进度和状态。需要 Ticker (vsync) 来驱动。Tween定义动画的取值范围,如从 0.0 到 1.0,从红色到蓝色。Cu…...
Windows10下使用QEMU安装Ubuntu20.04虚拟机,并启用硬件加速
Windows10下使用QEMU安装Ubuntu20.04虚拟机,并启用硬件加速 作者将狼才鲸创建日期2025-05-30 CSDN阅读地址:Windows10下使用QEMU安装Ubuntu20.04虚拟机,并启用硬件加速 本文档源码地址:Windows10下使用QEMU安装Ubuntu20.04虚拟机…...

《ChatGPT o3抗命:AI失控警钟还是成长阵痛?》
ChatGPT o3 “抗命” 事件起底 在人工智能的飞速发展进程中,OpenAI 于 2025 年推出的 ChatGPT o3 推理模型,犹如一颗重磅炸弹投入了技术的海洋,激起千层浪。它被视为 “推理模型” 系列的巅峰之作,承载着赋予 ChatGPT 更强大问题解…...
题目 3293: 蓝桥杯2024年第十五届决赛真题-数位翻转
题目 3293: 蓝桥杯2024年第十五届决赛真题-数位翻转 时间限制: 2s 内存限制: 192MB 提交: 1046 解决: 318 题目描述 小明创造了一个函数 f(x) 用来翻转 x 的二进制的数位(无前导 0)。比如f(11) 13,因为 11 (1011)2,将其左右翻转…...
Reactor 和 Preactor
Reactor 和 Preactor 是两个在工业控制、生产调度和事件驱动系统中非常重要的设计模式或框架,不少人会用这两个名词来描述不同的编程思想或技术架构。 一、Reactor 模式(反应器模式) 1. 概述 Reactor 模式其实是一种I/O事件通知的设计思想…...

【sa-token】 sa-token非 web 上下文无法获取 HttpServletRequest。
Springboot cloud gateway集成sa-token中报错 cn.dev33.satoken.exception.NotWebContextException: 非 web 上下文无法获取 HttpServletRequestat cn.dev33.satoken.spring.SpringMVCUtil.getRequest(SpringMVCUtil.java:45) ~[sa-token-spring-boot-starter-1.38.0.jar:?]官…...
论爱情《态度》
我犹记得,当吴军的《态度》到手之后,从中间翻开的第一页,便是此。 “合适的人,会让你看到,和得到全世界” -- 第22封 其实在我初中、高中的时候,我便产生一个问题,为什么学校要禁止谈恋爱。 …...

多台电脑共用一个ip地址可以吗?会怎么样
在互联网使用日益普及的今天,许多人都面临着多台设备共享网络的需求。一个常见的问题随之而来:多台电脑共用一个IP地址可以吗?这样做会带来哪些影响?本文将深入探讨这一话题。 一、多台电脑共用一个IP地址可以吗? 多…...

线程(上)【Linux操作系统】
文章目录 线程概念及其相关知识线程的概念及一些重要认识重要认识Linux中线程的实现Linux中的被调度的执行流是被task_struct描述的 线程是如何瓜分进程的代码和数据的?对于数据:对于代码: 线程的优点线程的缺点线程调度细节调度:…...
FPGA中的“BPI“指什么
在FPGA(现场可编程门阵列)中,BPI 的全称是 “Byte Peripheral Interface” 或 “Bank Parallel Interface”,具体指一种 并行NOR闪存配置接口,主要用于FPGA的配置(Configuration)过程。以下是BP…...
Splunk Validated Architecture (SVA):构建企业级可观测性与安全的基石
Splunk Validated Architecture (SVA) 是 Splunk 官方提供的一套经过严格测试、性能验证和最佳实践指导的参考架构蓝图。它并非单一固定方案,而是根据企业数据规模、性能需求、高可用性目标和合规要求,提供一系列可落地的部署模型。SVA 的核心价值在于为…...
Python爬虫(40)基于Selenium与ScrapyRT构建高并发动态网页爬虫架构:原理、实现与性能优化
目录 一、引言二、技术背景1. 动态页面处理痛点2. 架构设计目标 三、核心组件详解1. Selenium Grid集群部署2. ScrapyRT服务化改造3. 智能等待策略 四、系统架构图五、性能优化实践1. 资源隔离策略2. 并发控制算法3. 监控体系 六、总结与展望🌈Python爬虫相关文章&a…...
深入解析 Python 字典:从基础到高级应用
文章大纲 引言:什么是字典? 在 Python 编程中,字典(Dictionary)是一种极其重要的数据结构,它以键值对(key-value pair)的形式存储数据,能够高效地进行数据的查找和操作。相比于列表(List)这种依赖整数索引的序列类型,字典通过自定义的键来访问对应的值,提供了更…...

进程同步:生产者-消费者 题目
正确答案: 问题类型: 经典生产者 - 消费者问题 同时涉及同步和互斥。 同步:生产者与消费者通过信号量协调生产 / 消费节奏(如缓冲区满时生产者等待,空时消费者等待)。互斥:对共享缓冲区的访问需…...
Linux轻量级文件传输——tftp命令
摘要 TFTP是基于UDP/69端口的轻量文件传输协议。本文整理tftp命令参数/交互命令,提供示例,涵盖文件上传下载、模式设置等核心操作,帮助快速掌握基础文件传输。 一、TFTP核心特性 tftp(Trivial File Transfer Protocol࿰…...
JavaSwing之--为组件添加背景
JavaSwing之–为组件添加背景 从实践角度,可以把Java Swing中的组件分为容器组件和普通组件,容器组件是为了更好的按照某种布局摆放各种组件,形成功能强大且友好的界面。 Swing中组件的背景可以分为两种类型,一种是背景色&#…...
MySQL项目实战演练:搭建用户管理系统的完整数据库结构【MySQL系列】
本项目适用于后台管理系统、电商用户中心、SaaS 用户模块等场景,特别适合开发者进行实战演练与面试准备。 一、项目背景与需求概述 我们将构建一个基础版的用户管理系统,具备以下业务功能: 用户注册与登录用户角色与权限分配日志记录与用户…...

展会聚焦丨漫途科技亮相2025西北水务博览会!
2025第三届西北水务数字化发展论坛暨供排水节水灌溉新技术设备博览会在兰州甘肃国际会展中心圆满落幕。本届展会以“科技赋能水资源,数智引领新动能”为主题,活动汇集水务集团、科研院所、技术供应商等全产业链参与者,旨在通过前沿技术展示与…...

【数据结构初阶】顺序表的应用
文章目录 顺序表的应用基于动态顺序表实现通讯录前言1.定义联系人数据2.给顺序表改名3.通讯录的初始化4.通讯录的销毁5.通讯录添加数据6.通讯录删除数据7.通讯录修改数据8.通讯录查找数据9.展示通讯录数据10.通讯录的最终实现 顺序表的应用 基于动态顺序表实现通讯录 前言 功…...
抽象工厂模式与策略模式结合使用小案例
目录 1.前言 1.前言 上一篇章就通过简单的案例来了解抽象工厂模式和策略模式的使用,现在就用个支付场景的小案例来演示两者设计模式的联合使用;...

C#数字图像处理(一)
文章目录 1.C#图像处理基础1.1 Bitmap类1.2 Bitmapdata类1.3 Graphics类1.4 Image类 2.彩色图像灰度化1.提取像素法2.内存法3.指针法三种方法的比较4.灰度图像二值化: 3.相关链接 Bitmap类、 Bitmapdata类和 Graphics类是C#图像处理中最重要的3个类,如果要用C# 进行…...

麻省理工新突破:家庭场景下机器人实现精准控制,real-to-sim-to-real学习助力
麻省理工学院电气工程与计算机科学系Pulkit Agrawal教授,介绍了一种新方法,可以让机器人在扫描的家庭环境模拟中接受训练,为任何人都可以实现定制的家庭自动化铺平了道路。 本文将探讨通过Franka机器人在虚拟环境中训练的特点,研…...

从零实现本地语音识别(FunASR)
FunASR 是达摩院开源的综合性语音处理工具包,提供语音识别(ASR)、语音活动检测(VAD)、标点恢复(PUNC)等全流程功能,支持多种主流模型(如 Paraformer、Whisper、SenseVoic…...