1. 为什么需要扩展存储空间?
在嵌入式系统开发中,存储空间不足是个常见痛点。PIC18F96J94这类微控制器虽然功能强大,但内置的Flash和EEPROM容量有限(通常只有几十到几百KB)。当项目需要记录大量配置参数、历史数据或固件升级包时,就需要外扩非易失性存储器。
M24M01E-F这颗1Mb(128KB)的EEPROM芯片正好填补了这个需求。相比Flash存储器,EEPROM有以下优势:
- 单字节擦写:无需整页擦除,适合频繁修改小数据
- 超高耐久度:100万次擦写周期(NOR Flash通常仅10万次)
- 数据保持:200年不丢失
- 宽电压工作:1.6V-5.5V,兼容多数MCU电源
2. 硬件设计要点
2.1 器件选型对比
| 参数 | M24M01E-F | 典型SPI Flash | 内置EEPROM |
|---|---|---|---|
| 接口 | I2C | SPI | 芯片内置 |
| 容量 | 128KB | 通常≥512KB | 通常≤4KB |
| 擦写单位 | 单字节 | 扇区(4KB+) | 单字节 |
| 典型写时间 | 4ms | 10-100ms | 5ms |
| 耐久度 | 1M次 | 10万次 | 10万次 |
选择M24M01E-F的核心原因是其平衡性:既有EEPROM的字节级操作特性,又有接近Flash的容量,还采用最简单的I2C接口。
2.2 电路连接方案
PIC18F96J94与M24M01E-F的典型连接:
PIC18F96J94 M24M01E-F RC3/SCL ------> SCL RC4/SDA <------> SDA VDD ------> VCC (2.5-5V) GND ------> GND关键细节:
- 上拉电阻:SCL/SDA线需加4.7kΩ上拉(I2C标准要求)
- 地址引脚:A0/A1/A2接地时器件地址为0x50
- 写保护:WP引脚接高电平禁用写保护
注意:PIC的I2C引脚需要配置为开漏输出模式,硬件I2C模块时钟建议设为400kHz(标准模式)
3. 底层驱动实现
3.1 I2C初始化代码
void I2C_Init(void) { SSP1CON1 = 0b00101000; // I2C主模式, 时钟=FOSC/(4*(SSP1ADD+1)) SSP1ADD = 39; // 16MHz晶振时产生400kHz时钟 SSP1STAT = 0b10000000; // 标准速度模式 TRISC3 = 1; // SCL引脚输入 TRISC4 = 1; // SDA引脚输入 }3.2 EEPROM读写函数
字节写入函数(带轮询):
void EEPROM_WriteByte(uint16_t addr, uint8_t data) { I2C_Start(); I2C_Write(0xA0); // 器件地址 + 写命令 I2C_Write(addr >> 8); // 高地址字节 I2C_Write(addr & 0xFF);// 低地址字节 I2C_Write(data); I2C_Stop(); // 等待写入完成 do { I2C_Start(); } while(!I2C_Write(0xA0)); // 直到ACK响应 I2C_Stop(); }页读取函数(连续读):
void EEPROM_ReadPage(uint16_t addr, uint8_t *buf, uint8_t len) { I2C_Start(); I2C_Write(0xA0); // 器件地址 + 写命令 I2C_Write(addr >> 8); // 高地址字节 I2C_Write(addr & 0xFF);// 低地址字节 I2C_Restart(); I2C_Write(0xA1); // 器件地址 + 读命令 for(uint8_t i=0; i<len-1; i++) buf[i] = I2C_Read(1); // 带ACK的读 buf[len-1] = I2C_Read(0); // 最后字节无ACK I2C_Stop(); }4. 高级应用技巧
4.1 写均衡算法实现
EEPROM虽然耐久度高,但频繁写同一地址仍会损坏。实现简单的写均衡:
#define EEPROM_SIZE 131072 #define PAGE_SIZE 64 uint16_t wear_leveling_write(uint8_t data) { static uint16_t write_ptr = 0; uint16_t current_addr = write_ptr; EEPROM_WriteByte(current_addr, data); write_ptr = (write_ptr + 1) % EEPROM_SIZE; // 每写完一页进行校验 if((write_ptr % PAGE_SIZE) == 0) { uint8_t buf[PAGE_SIZE]; EEPROM_ReadPage(write_ptr - PAGE_SIZE, buf, PAGE_SIZE); // 可添加CRC校验逻辑 } return current_addr; }4.2 数据加密存储
防止EEPROM数据被篡改的简单方案:
void Secure_Write(uint16_t addr, uint8_t data) { uint8_t encrypted = data ^ 0x55; // 简单异或加密 uint8_t checksum = ~data; // 补码校验 EEPROM_WriteByte(addr, encrypted); EEPROM_WriteByte(addr+1, checksum); } uint8_t Secure_Read(uint16_t addr) { uint8_t encrypted = EEPROM_ReadByte(addr); uint8_t checksum = EEPROM_ReadByte(addr+1); uint8_t data = encrypted ^ 0x55; if((uint8_t)(~data) != checksum) return 0xFF; // 校验失败 return data; }5. 性能优化与调试
5.1 I2C时序优化技巧
实测中发现的问题及解决方案:
时钟拉伸问题:
- 现象:MCU速度远快于EEPROM,导致ACK超时
- 解决:在I2C初始化后添加延时
SSP1CON2bits.ACKDT = 1; // 配置ACK脉冲宽度总线冲突处理:
if(SSP1CON1bits.WCOL) { // 检测写冲突 SSP1CON1bits.WCOL = 0; SSP1BUF = data; // 重新发送 }信号完整性优化:
- 缩短走线长度(<10cm)
- 在SCL/SDA线上串联33Ω电阻
- 用示波器检查上升时间(应<300ns)
5.2 典型问题排查流程
当EEPROM无响应时:
检查硬件:
- 电源电压(1.8-5.5V)
- 上拉电阻(4.7kΩ)
- 地址引脚电平
发送I2C通用呼叫地址(0x00):
I2C_Start(); uint8_t ack = I2C_Write(0x00); // 应返回ACK I2C_Stop();用逻辑分析仪捕获波形:
- 检查START条件(SDA下降沿早于SCL)
- 确认时钟频率≤1MHz
- 验证ACK脉冲位置
6. 实际项目应用案例
6.1 物联网设备配置存储
在智能传感器节点中,我们使用M24M01E-F存储:
- 设备ID和校准参数(占用前256字节)
- 网络配置(SSID/密码等)
- 历史数据缓存(环形缓冲区设计)
typedef struct { uint32_t device_id; float calibration[4]; uint8_t wifi_ssid[32]; uint8_t wifi_pass[64]; } DeviceConfig; void Save_Config(DeviceConfig *cfg) { uint8_t *p = (uint8_t*)cfg; for(uint16_t i=0; i<sizeof(DeviceConfig); i++) EEPROM_WriteByte(i, p[i]); }6.2 固件升级方案
利用EEPROM作为二级Bootloader:
- 接收新固件并暂存到EEPROM
- 校验通过后写入主Flash
- 实现代码:
void Firmware_Update(void) { uint8_t buf[64]; uint16_t flash_addr = 0x1000; for(uint16_t eeprom_addr=0; eeprom_addr<FW_SIZE; eeprom_addr+=64) { EEPROM_ReadPage(eeprom_addr, buf, 64); Flash_WritePage(flash_addr, buf); flash_addr += 64; } }通过这个方案,我们在PIC18F96J94上成功实现了远程固件升级功能,EEPROM的字节操作特性使得断点续传成为可能。