EEPROM库详解

news2025/6/2 19:08:04

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);)。
  • 支持的数据类型:

    • 基本类型:byteintfloatdouble 等。
    • 自定义类型:结构体、数组等(需确保地址空间足够)。
  • 示例:

    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 总大小
    

注意

  1. 擦写寿命限制
    • 虽然 EEPROM 支持约 100,000 次擦写,但频繁写入(如在loop()中循环写入)仍可能导致磨损。建议:
      • 仅在必要时写入数据(如配置变更时)。
      • 对高频数据(如传感器读数)优先使用 RAM 缓存,定期批量写入 EEPROM。
  2. 数据类型与对齐
    • 使用 EEPROM.put()/get() 处理非字节数据(如 intfloat)时,需确保地址对齐(例如,int 占用 2 字节,应从偶数地址开始写入)。
  3. 原子性问题
    • EEPROM 写入操作需要约 3.3ms(ATmega328P),期间若发生中断(如复位),可能导致数据不完整。建议:
      • 写入关键数据前禁用中断(noInterrupts()),完成后恢复(interrupts())。

示例

#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 的写入过程分为两个阶段:

  1. 设置地址阶段:告诉 EEPROM “我接下来要操作哪个内存地址”。
  2. 传输数据阶段:发送真正要写入的数据。

代码中的执行顺序完全符合这个流程:

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

三、代码详解

  1. Wire.write((int)(addr >> 8));
  • addr >> 8:将地址右移 8 位,丢弃低 8 位,只保留高 8 位。
    例如:addr = 0x12340x1234 >> 8 = 0x12
  • (int):强制类型转换为int(实际转换为uint8_t更合适,但 Arduino 的 Wire 库会自动截断为 8 位)。
  • 作用:发送地址的高 8 位字节。
  1. Wire.write((int)(addr & 0xFF));
  • addr & 0xFF:将地址与0xFF(二进制1111 1111)按位与,只保留低 8 位。
    例如:addr = 0x12340x1234 & 0xFF = 0x34
  • (int):同样为强制类型转换(实际发送时会截断为 8 位)。
  • 作用:发送地址的低 8 位字节。

四、为什么要这样设计?

这是由 I2C 协议和 EEPROM 的通信机制决定的:

  1. I2C 协议限制:每次只能发送 1 个字节的数据,因此 16 位地址必须分两次发送。
  2. 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)

  • 写入多字节数据(如 intfloat)时,需注意字节序(大端 / 小端)。上述示例为 小端序(低字节在前)。

在计算机中,多字节数据(如 intfloat 等占 2 字节以上的数据类型)在内存或存储设备中的字节排列顺序称为 字节序(Endianness)。字节序主要分为两种:大端序(Big-Endian)和小端序(Little-Endian)。以下结合示例详细说明:

字节序的核心区别

假设要存储一个 16 位整数 0x1234(二进制 00010010 00110100):

字节序类型内存 / 存储中的字节顺序(低地址 → 高地址)直观表示
大端序先存高位字节(0x12),再存低位字节(0x34[12][34]
小端序先存低位字节(0x34),再存高位字节(0x12[34][12]

示例代码中的小端序问题

在你提供的示例代码中,当写入 intfloat 等多字节数据时(例如通过 (uint8_t*)&data 强制类型转换),默认使用的是小端序,原因如下:

1. 小端序的本质:低字节在前

以 32 位整数 0x12345678 为例:

  • 内存中的二进制表示(小端序):
    低地址 → 高地址:0x78(最低位字节)→ 0x560x340x12(最高位字节)
    存储顺序[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 直接读取变量的字节时,会按硬件架构的字节序获取数据。

总结:关键操作建议

  1. 明确需求
    • 若数据仅在同一设备读写,可直接使用小端序(与 Arduino 架构一致)。
    • 若涉及跨设备通信或大端序系统(如某些嵌入式处理器、网络协议),需手动转换字节序。
  2. 避免隐式转换
    多字节数据写入 EEPROM 时,尽量通过自定义函数显式处理字节序,避免因架构差异导致的兼容性问题。
  3. 测试验证
    写入后通过串口打印或其他设备读取字节,确认存储顺序是否符合预期。

通过显式处理字节序,可确保多字节数据在存储和传输过程中保持一致性。

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);
  // ...
}

关键特性

  1. 页写入优化
    • 自动处理跨页写入,避免数据被截断
    • 例如:对于 64 字节页大小的 EEPROM,写入 100 字节数据时会自动分 2 次传输
  2. 性能提升
    • 相比逐字节写入,页写入可减少 I2C 传输次数
    • 例如:写入 64 字节数据时,页写入只需 1 次传输(而非 64 次)
  3. 安全机制
    • 自动检查地址范围,防止越界写入
    • 每次页写入后等待 5ms,确保 EEPROM 完成内部写入操作

使用注意事项

  1. 页大小限制
    • 不同 EEPROM 型号的页大小不同(常见:16/32/64/128 字节)
    • 初始化时需正确设置页大小(如 eeprom(0x50, 64, ...)
  2. 写入延时
    • 每次页写入后必须等待 5ms(EEPROM 写入周期)
    • 频繁写入会影响系统响应速度,建议批量操作
  3. 数据类型
    • writePage 仅支持 uint8_t 数组
    • 如需写入其他类型(如字符串、结构体),需先转换为字节数组
  4. 地址对齐
    • 尽量从页边界开始写入(如地址 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
  1. 延长 EEPROM 寿命:EEPROM 有写入次数的限制,频繁在同一位置写入数据会导致该位置的寿命提前耗尽。I2C_eeprom_cyclic_store 类通过将写入操作分散到 EEPROM 的不同页面,减少了对单个页面的写入次数,从而延长了整个 EEPROM 的使用寿命。
  2. 数据版本管理:该类会为每次写入的数据添加一个版本号,在初始化时会扫描 EEPROM 中所有存储槽,找到版本号最高的槽位,将其作为当前数据进行读取。这确保了每次读取的数据都是最新写入的版本。
  3. 循环写入机制:当数据写入到 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))。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2394410.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

在win10/11下Node.js安装配置教程

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

飞致云开源社区月度动态报告(2025年5月)

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

压缩包方式在Linux和Windows下安装mongodb

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

智慧场馆:科技赋能的艺术盛宴

智慧场馆作为城市公共服务设施数字化转型的典型代表&#xff0c;通过深度融合新一代信息技术&#xff0c;构建起全方位、智能化的运营管理体系。其功能架构不仅提升了场馆本身的运营效能&#xff0c;更重塑了公共服务体验模式&#xff0c;展现出显著的社会价值和商业潜力。 一…

《ChatGPT o3抗命:AI失控警钟还是成长阵痛?》

ChatGPT o3 “抗命” 事件起底 在人工智能的飞速发展进程中&#xff0c;OpenAI 于 2025 年推出的 ChatGPT o3 推理模型&#xff0c;犹如一颗重磅炸弹投入了技术的海洋&#xff0c;激起千层浪。它被视为 “推理模型” 系列的巅峰之作&#xff0c;承载着赋予 ChatGPT 更强大问题解…

【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:?]官…

多台电脑共用一个ip地址可以吗?会怎么样

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

线程(上)【Linux操作系统】

文章目录 线程概念及其相关知识线程的概念及一些重要认识重要认识Linux中线程的实现Linux中的被调度的执行流是被task_struct描述的 线程是如何瓜分进程的代码和数据的&#xff1f;对于数据&#xff1a;对于代码&#xff1a; 线程的优点线程的缺点线程调度细节调度&#xff1a;…

进程同步:生产者-消费者 题目

正确答案&#xff1a; 问题类型&#xff1a; 经典生产者 - 消费者问题 同时涉及同步和互斥。 同步&#xff1a;生产者与消费者通过信号量协调生产 / 消费节奏&#xff08;如缓冲区满时生产者等待&#xff0c;空时消费者等待&#xff09;。互斥&#xff1a;对共享缓冲区的访问需…

展会聚焦丨漫途科技亮相2025西北水务博览会!

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

【数据结构初阶】顺序表的应用

文章目录 顺序表的应用基于动态顺序表实现通讯录前言1.定义联系人数据2.给顺序表改名3.通讯录的初始化4.通讯录的销毁5.通讯录添加数据6.通讯录删除数据7.通讯录修改数据8.通讯录查找数据9.展示通讯录数据10.通讯录的最终实现 顺序表的应用 基于动态顺序表实现通讯录 前言 功…

C#数字图像处理(一)

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

麻省理工新突破:家庭场景下机器人实现精准控制,real-to-sim-to-real学习助力

麻省理工学院电气工程与计算机科学系Pulkit Agrawal教授&#xff0c;介绍了一种新方法&#xff0c;可以让机器人在扫描的家庭环境模拟中接受训练&#xff0c;为任何人都可以实现定制的家庭自动化铺平了道路。 本文将探讨通过Franka机器人在虚拟环境中训练的特点&#xff0c;研…

从零实现本地语音识别(FunASR)

FunASR 是达摩院开源的综合性语音处理工具包&#xff0c;提供语音识别&#xff08;ASR&#xff09;、语音活动检测&#xff08;VAD&#xff09;、标点恢复&#xff08;PUNC&#xff09;等全流程功能&#xff0c;支持多种主流模型&#xff08;如 Paraformer、Whisper、SenseVoic…

已解决:.NetCore控制台程序(WebAPI)假死,程序挂起接口不通

本问题已得到解决&#xff0c;请看以下小结&#xff1a; 关于《.NetCore控制台程序(WebAPI)假死,程序暂停接口不通》的解决方案 记录备注报错时间2025年报错版本VS2022 WINDOWS10报错复现鼠标点一下控制台&#xff0c;会卡死报错描述——报错截图——报错原因 控制台启用了“快…

Excel如何分开查看工作表方便数据撰写

首先我这里有2class和3class两个工作表 接下来我们点击视图 按照顺序分别点击新建窗口和全部重排 ### 然后就是这样 接下来就OK了

微软技术赋能:解锁开发、交互与数据潜力,共探未来创新路

在微软 Build 2025 大会以及创想未来峰会上&#xff0c;微软展示的一系列前沿技术与创新应用&#xff0c;不仅展现了其在科技领域的深厚底蕴与前瞻视野&#xff0c;更为开发者和企业带来了前所未有的机遇与变革动力。 领驭科技作为微软中国南区核心合作伙伴及 HKCSP 1T 首批授…

VR看房系统,新生代看房新体验

VR看房系统的概念 虚拟现实&#xff08;VirtualReality,VR&#xff09;看房系统&#xff0c;是近年来随着科技进步在房地产行业中兴起的一种创新看房方式。看房系统利用先进的计算机技术模拟出一个三维环境&#xff0c;使用户能够身临其境地浏览和体验房源&#xff0c;无需亲自…

【Linux笔记】Shell-脚本(下)|(常用命令详细版)

在&#xff08;上&#xff09;篇&#xff0c;我们详细的讲解了Shell脚本的基础知识和些许命令与实验&#xff0c;这次的的&#xff08;下&#xff09;篇&#xff0c;我们会详细讲解Shell脚本的常用命令 关于脚本的基础知识请各位移步到&#xff08;上&#xff09;篇啦~ Shell…

钉钉热点实时推送助理-思路篇

以下是针对热点实时推送助理的功能描述&#xff0c;结合机器学习技术栈与用户场景的通俗化解释&#xff1a; 快速体验的话直接用钉钉扫描下方二维码体验 1. 核心功能 &#xff08;1&#xff09;热点抓取引擎 类比&#xff1a;像蜘蛛爬取全网信息&#xff08;网络爬虫信息抽取…