51单片机I2C实战:手把手教你用C语言驱动24C02 EEPROM(附完整代码)

张开发
2026/4/20 4:14:27 15 分钟阅读

分享文章

51单片机I2C实战:手把手教你用C语言驱动24C02 EEPROM(附完整代码)
51单片机I2C实战从零实现24C02 EEPROM驱动第一次接触I2C总线的开发者常会遇到这样的困境明明看懂了协议文档动手写代码时却无从下手。本文将用最直白的代码演示如何用51单片机模拟I2C时序实现对24C02芯片的完整读写控制。不同于理论讲解这里每个函数都经过实际硬件验证包含你可能遇到的坑和解决方案。1. 硬件准备与环境搭建在开始编码前需要准备以下硬件组件STC89C52单片机开发板或其他51内核芯片24C02 EEPROM芯片10kΩ上拉电阻×2面包板与杜邦线接线示意图51单片机 24C02 P2.0 —— SDA P2.1 —— SCL VCC —— VCC GND —— GND注意SDA和SCL线必须接上拉电阻到VCC这是I2C总线正常工作的关键开发环境建议使用Keil μVision新建工程时选择正确的单片机型号。新建main.c文件前先配置好晶振频率通常11.0592MHz这会直接影响后续延时函数的准确性。2. I2C底层时序实现2.1 基础信号生成I2C通信的核心是精确控制SDA和SCL线的时序。以下是必须实现的四个基本函数// 定义IO口 sbit SDA P2^0; sbit SCL P2^1; // 起始信号SCL高电平时SDA产生下降沿 void I2C_Start() { SDA 1; SCL 1; Delay5us(); // 保持时间4.7μs SDA 0; Delay5us(); SCL 0; // 钳住总线 } // 停止信号SCL高电平时SDA产生上升沿 void I2C_Stop() { SDA 0; SCL 1; Delay5us(); SDA 1; Delay5us(); } // 发送应答(ACK)SCL低电平时SDA置低 void I2C_Ack() { SDA 0; SCL 1; Delay5us(); SCL 0; } // 发送非应答(NACK)SCL低电平时SDA置高 void I2C_NAck() { SDA 1; SCL 1; Delay5us(); SCL 0; }2.2 字节传输函数数据传送需要处理位序和应答信号这是最容易出错的部分// 发送单字节数据 bit I2C_SendByte(unsigned char dat) { unsigned char i; for(i0; i8; i) { SDA dat 0x80; // 先传高位 SCL 1; Delay5us(); SCL 0; dat 1; } // 等待从机应答 SDA 1; // 释放总线 SCL 1; Delay5us(); if(SDA) { // 检测ACK SCL 0; return 0; // 应答失败 } SCL 0; return 1; // 应答成功 } // 接收单字节数据 unsigned char I2C_RecvByte() { unsigned char i, dat 0; SDA 1; // 确保主机释放SDA for(i0; i8; i) { SCL 1; Delay5us(); dat 1; dat | SDA; // 读取当前位 SCL 0; Delay5us(); } return dat; }3. 24C02驱动实现3.1 器件地址与写操作24C02的器件地址由硬件引脚决定默认A0-A2接地时地址为0xA0#define EEPROM_ADDR 0xA0 // 单字节写入 void EEPROM_WriteByte(unsigned char addr, unsigned char dat) { I2C_Start(); I2C_SendByte(EEPROM_ADDR); // 器件地址写标志 I2C_SendByte(addr); // 存储地址 I2C_SendByte(dat); // 写入数据 I2C_Stop(); Delay10ms(); // 等待写入完成 } // 页写入最多16字节 void EEPROM_PageWrite(unsigned char addr, unsigned char *buf, unsigned char len) { unsigned char i; if(len 16) len 16; // 页写入限制 I2C_Start(); I2C_SendByte(EEPROM_ADDR); I2C_SendByte(addr); for(i0; ilen; i) { I2C_SendByte(buf[i]); } I2C_Stop(); Delay10ms(); }3.2 读操作实现24C02支持三种读模式最常用的是随机地址读取// 随机地址读取 unsigned char EEPROM_RandomRead(unsigned char addr) { unsigned char dat; // 伪写操作设置地址指针 I2C_Start(); I2C_SendByte(EEPROM_ADDR); I2C_SendByte(addr); // 重新启动读操作 I2C_Start(); I2C_SendByte(EEPROM_ADDR | 0x01); // 读标志 dat I2C_RecvByte(); I2C_NAck(); // 结束读取 I2C_Stop(); return dat; } // 连续读取 void EEPROM_SequentialRead(unsigned char addr, unsigned char *buf, unsigned char len) { unsigned char i; // 设置起始地址 I2C_Start(); I2C_SendByte(EEPROM_ADDR); I2C_SendByte(addr); // 连续读取 I2C_Start(); I2C_SendByte(EEPROM_ADDR | 0x01); for(i0; ilen; i) { buf[i] I2C_RecvByte(); if(i len-1) I2C_NAck(); else I2C_Ack(); } I2C_Stop(); }4. 调试技巧与常见问题4.1 时序问题排查当通信失败时建议按以下步骤检查用示波器观察SCL和SDA波形确认起始/停止信号符合规范检查时钟频率是否在100kHz左右检查上拉电阻值推荐4.7kΩ-10kΩ验证延时函数准确性// 精确延时函数示例11.0592MHz晶振 void Delay5us() { unsigned char i; _nop_(); _nop_(); // 2个机器周期 i 11; while(--i); // 11×333个机器周期 }4.2 典型错误案例案例1写入后立即读取失败现象写入数据后马上读取得到错误值原因24C02写入需要5ms左右时间解决写入后添加足够延时5ms案例2页写入时数据覆盖现象写入超过16字节时数据出现循环覆盖原因24C02页缓冲区只有16字节解决分页写入每16字节插入延时案例3从机无应答现象I2C_SendByte()总是返回0排查步骤检查器件地址是否正确含R/W位确认硬件连接无短路/断路测量VCC电压是否达标≥2.5V5. 完整应用示例下面是一个将运行日志保存到EEPROM的典型应用// 日志结构体 typedef struct { unsigned char hour; unsigned char minute; unsigned char second; unsigned char event; } LogEntry; // 保存日志到EEPROM void SaveLog(unsigned char index, LogEntry log) { unsigned char addr index * sizeof(LogEntry); EEPROM_PageWrite(addr, (unsigned char*)log, sizeof(LogEntry)); } // 从EEPROM读取日志 LogEntry ReadLog(unsigned char index) { LogEntry log; unsigned char addr index * sizeof(LogEntry); EEPROM_SequentialRead(addr, (unsigned char*)log, sizeof(LogEntry)); return log; } void main() { LogEntry currentLog {12, 30, 45, 0x01}; // 示例操作 SaveLog(0, currentLog); // 保存到第0个位置 Delay10ms(); LogEntry readBack ReadLog(0); printf(Time: %02d:%02d:%02d Event: %d, readBack.hour, readBack.minute, readBack.second, readBack.event); }在实际项目中建议添加写保护机制避免频繁写入同一地址导致器件寿命缩短。24C02每个单元可擦写约100万次合理设计存储结构可以大幅延长使用寿命。

更多文章