蓝桥杯单片机学习笔记(七)——AT24C02 存储芯片完全攻略
核心概念:AT24C02 就像是单片机的"移动硬盘"
,即使断电关机,数据依然安全保存。它基于 I²C 总线通信,拥有 2KB(256 字节 × 8 位)的存储空间,是嵌入式系统中常用的非易失性存储器。
一、AT24C02 芯片概述
1.1 核心特性
| 特性 | 参数 | 生动比喻 |
|---|---|---|
| 存储容量 | 2KB (2048 位 = 256 字节) | 相当于一本 256 页的小笔记本 |
| 通信协议 | I²C 总线 | 只需两根线(SDA/SCL)就能"对话" |
| 数据保持 | 掉电不丢失 | 就像写在纸上的字,停电也不会消失 |
| 擦写次数 | 100 万次以上 | 足够你用一辈子 |
| 页大小 | 8 字节/页 | 一次最多写 8 个字符 |
1.2 工作原理图解
单片机 (主设备) AT24C02 (从设备)
│ │
├──── SCL (时钟线) ────────────────┤
│ "指挥棒,控制节奏" │
│ │
├──── SDA (数据线) ────────────────┤
│ "运输车,双向传输数据" │
│ │
└───────────────────────────────────┘
二、设备地址:AT24C02 的"门牌号"
2.1 地址结构解析
AT24C02 的 I²C 地址由 8 位二进制组成:
┌────┬────┬────┬────┬────┬────┬────┬────┐
│ 1 │ 0 │ 1 │ 0 │ A2 │ A1 │ A0 │R/W │
└────┴────┴────┴────┴────┴────┴────┴────┘
固定前缀 (1010) 硬件地址 读写位
蓝桥杯开发板配置(A2/A1/A0 全部接地):
| 操作 | 二进制地址 | 十六进制 | 记忆口诀 |
|---|---|---|---|
| 写操作 | 1010 0000 |
0xA0 | A |
| 读操作 | 1010 0001 |
0xA1 | A |
比喻理解:这就像快递包裹,
1010是省份,000是街道,最后一位告诉快递员你是要"收件(读)“还是"寄件(写)”。
三、写入数据:把信息"刻"进芯片
3.1 两种写入方式对比
| 写入方式 | 特点 | 适用场景 | 形象比喻 |
|---|---|---|---|
| 字节写入 | 一次写 1 个字节 | 零散数据存储 | 一个字一个字地抄书 |
| 页写入 | 一次写 8 个字节 | 批量数据存储 | 一次复制一整段 |
3.2
关键注意事项
为什么地址必须是 8 的倍数?
AT24C02 的页大小是 8 字节,这意味着:
地址范围:0x00 ~ 0xFF (0 ~ 255)
页划分:
┌─────────┬─────────┬─────────┬─────────┐
│ 第 0 页 │ 第 1 页 │ 第 2 页 │ ... 31页 │
│ 0~7 │ 8~15 │ 16~23 │ 248~255 │
└─────────┴─────────┴─────────┴─────────┘
错误示范:
// ❌ 从地址 5 开始写 8 个字节
EEPROM_Write(data, 5, 8);
// 结果:只能写入 3 个字节(5,6,7),剩下的会跨页出错!
正确做法:
// ✅ 从地址 0 或 8 或 16... 开始
EEPROM_Write(data, 0, 8); // 正好写满第 0 页
EEPROM_Write(data, 8, 8); // 正好写满第 1 页
生活比喻:就像停车位,一个车位(页)只能停 8 辆车(字节)。如果你从车位中间开始停,后面的车就没地方了!
3.3 写入函数详解
/**
* @brief 向 EEPROM 写入数据
* @param EEPROM_String: 要写入的数据数组指针
* @param addr: 起始地址(必须是 8 的倍数!)
* @param num: 写入字节数(建议 ≤8,刚好写满一页)
* @retval 无
*/
void EEPROM_Write(unsigned char* EEPROM_String, unsigned char addr, unsigned char num)
{
// 【第1步】发送起始信号 —— "喂,AT24C02,准备接收数据!"
I2CStart();
// 【第2步】发送设备地址(写模式)—— "我要往你这里存东西"
I2CSendByte(0xA0); // 写地址
I2CWaitAck(); // 等待芯片应答
// 【第3步】发送存储地址 —— "存到第 X 个格子里"
I2CSendByte(addr); // 例如 0、8、16...
I2CWaitAck();
// 【第4步】循环写入数据 —— "一个一个往里塞"
while(num--) {
I2CSendByte(*EEPROM_String++); // 发送一个字节
I2CWaitAck();
I2C_Delay(200); // ⚠️ 关键!给芯片时间写入(约 5ms)
}
// 【第5步】发送停止信号 —— "写完了,收工!"
I2CStop();
}
代码细节解析
-
指针递增
\*EEPROM_String++:// 等价于: I2CSendByte(*EEPROM_String); // 先取当前值 EEPROM_String++; // 再指向下一个元素 -
为什么需要延时
I2C_Delay(200)?-
AT24C02 内部写入需要 5ms 的擦写时间
-
如果写太快,数据会丢失
-
就像给存储卡"反应时间"

-
四、读取数据:把信息"取"出来
4.1 两种读取方式
| 读取方式 | 特点 | 使用场景 |
|---|---|---|
| 随机读取 | 从指定地址开始读 | 读取特定位置的数据 |
| 顺序读取 | 连续读取多个字节 | 读取连续的数据块 |
4.2 读取函数详解
/**
* @brief 从 EEPROM 读取数据
* @param EEPROM_String: 存放读取数据的数组指针
* @param addr: 起始地址
* @param num: 读取字节数
* @retval 无
*/
void EEPROM_Read(unsigned char* EEPROM_String, unsigned char addr, unsigned char num)
{
// 【第1步】起始信号 + 写地址 —— "我要告诉你读哪里"
I2CStart();
I2CSendByte(0xA0); // 先发写地址
I2CWaitAck();
// 【第2步】发送要读取的地址 —— "读第 X 个格子"
I2CSendByte(addr);
I2CWaitAck();
// 【第3步】重新起始 + 读地址 —— "现在切换到接收模式"
I2CStart();
I2CSendByte(0xA1); // 读地址
I2CWaitAck();
// 【第4步】循环接收数据
while(num--) {
*EEPROM_String++ = I2CReceiveByte(); // 读一个字节
if(num) {
I2CSendAck(0); // 还有数据,发送应答(继续读)
} else {
I2CSendAck(1); // 最后一个数据,发送非应答(停止)
}
}
// 【第5步】停止信号
I2CStop();
}
关键点解析
为什么要"先写后读"?
正常人的思维:我要读数据,直接发读命令不就行了?
实际情况:AT24C02 需要先知道你要读"哪个地址",所以:
1. 先发写地址(0xA0),告诉它位置
2. 再发读地址(0xA1),开始接收数据
应答信号的"潜台词":
I2CSendAck(0); // "继续,我还要读!" 👍
I2CSendAck(1); // "够了,别读了!" 🛑
五、完整底层驱动代码
5.1 iic.h 头文件
#ifndef __IIC_H_
#define __IIC_H_
// ========== I²C 基础协议函数 ==========
void I2CStart(void); // 起始信号
void I2CStop(void); // 停止信号
void I2CSendByte(unsigned char byt); // 发送一个字节
unsigned char I2CReceiveByte(void); // 接收一个字节
unsigned char I2CWaitAck(void); // 等待应答
void I2CSendAck(unsigned char ackbit); // 发送应答
// ========== AT24C02 专用函数 ==========
/**
* @brief 写入数据到 EEPROM
* @param EEPROM_String 数据源数组指针
* @param addr 起始地址(建议 0, 8, 16, 24...)
* @param num 写入字节数(建议 ≤8)
*/
void EEPROM_Write(unsigned char* EEPROM_String, unsigned char addr, unsigned char num);
/**
* @brief 从 EEPROM 读取数据
* @param EEPROM_String 数据接收数组指针
* @param addr 起始地址
* @param num 读取字节数
*/
void EEPROM_Read(unsigned char* EEPROM_String, unsigned char addr, unsigned char num);
#endif
5.2 iic.c 实现文件
#include "iic.h"
#include "reg52.h"
#include "intrins.h"
#define DELAY_TIME 10
sbit sda = P2 ^ 1; // 数据线
sbit scl = P2 ^ 0; // 时钟线
// ========== 微秒级延时 ==========
static void I2C_Delay(unsigned char n)
{
do {
_nop_();_nop_();_nop_();_nop_();_nop_();
_nop_();_nop_();_nop_();_nop_();_nop_();
_nop_();_nop_();_nop_();_nop_();_nop_();
} while(n--);
}
// ========== I²C 起始信号 ==========
void I2CStart(void)
{
sda = 1;
scl = 1;
I2C_Delay(DELAY_TIME);
sda = 0; // SDA 下降沿
I2C_Delay(DELAY_TIME);
scl = 0; // 拉低 SCL,准备传输
}
// ========== I²C 停止信号 ==========
void I2CStop(void)
{
sda = 0;
scl = 1;
I2C_Delay(DELAY_TIME);
sda = 1; // SDA 上升沿
I2C_Delay(DELAY_TIME);
}
// ========== 发送一个字节(高位先发) ==========
void I2CSendByte(unsigned char byt)
{
unsigned char i;
for(i=0; i<8; i++) {
scl = 0;
I2C_Delay(DELAY_TIME);
// 根据最高位设置 SDA
if(byt & 0x80) {
sda = 1;
} else {
sda = 0;
}
I2C_Delay(DELAY_TIME);
scl = 1; // 上升沿,从设备读取
byt <<= 1; // 左移,准备下一位
I2C_Delay(DELAY_TIME);
}
scl = 0;
}
// ========== 接收一个字节 ==========
unsigned char I2CReceiveByte(void)
{
unsigned char da = 0;
unsigned char i;
for(i=0; i<8; i++) {
scl = 1;
I2C_Delay(DELAY_TIME);
da <<= 1; // 左移一位
if(sda) {
da |= 0x01; // 如果 SDA 为高,最低位置 1
}
scl = 0;
I2C_Delay(DELAY_TIME);
}
return da;
}
// ========== 等待应答 ==========
unsigned char I2CWaitAck(void)
{
unsigned char ackbit;
scl = 1;
I2C_Delay(DELAY_TIME);
ackbit = sda; // 读取应答位(0=应答,1=非应答)
scl = 0;
I2C_Delay(DELAY_TIME);
return ackbit;
}
// ========== 发送应答 ==========
void I2CSendAck(unsigned char ackbit)
{
scl = 0;
sda = ackbit; // 0=应答,1=非应答
I2C_Delay(DELAY_TIME);
scl = 1;
I2C_Delay(DELAY_TIME);
scl = 0;
sda = 1; // 释放 SDA
I2C_Delay(DELAY_TIME);
}
// ========== EEPROM 写入函数 ==========
void EEPROM_Write(unsigned char* EEPROM_String, unsigned char addr, unsigned char num)
{
I2CStart();
I2CSendByte(0xA0); // 写地址
I2CWaitAck();
I2CSendByte(addr); // 存储地址
I2CWaitAck();
while(num--) {
I2CSendByte(*EEPROM_String++);
I2CWaitAck();
I2C_Delay(200); // ⚠️ 写入延时(约 5ms)
}
I2CStop();
}
// ========== EEPROM 读取函数 ==========
void EEPROM_Read(unsigned char* EEPROM_String, unsigned char addr, unsigned char num)
{
I2CStart();
I2CSendByte(0xA0); // 写地址(定位)
I2CWaitAck();
I2CSendByte(addr); // 读取地址
I2CWaitAck();
I2CStart(); // 重新起始
I2CSendByte(0xA1); // 读地址
I2CWaitAck();
while(num--) {
*EEPROM_String++ = I2CReceiveByte();
if(num) {
I2CSendAck(0); // 继续读
} else {
I2CSendAck(1); // 停止读
}
}
I2CStop();
}
六、实战案例:数据掉电保存
6.1 需求分析
设计一个温度设定系统:
-
上电时自动读取上次保存的温度值
-
通过按键调整温度
-
按下"保存"键后,数据写入 EEPROM
-
即使断电重启,数据依然存在
6.2 完整代码实现
/* ========== 头文件 ========== */
#include <Init.h>
#include <Key.h>
#include <Seg.h>
#include <Led.h>
#include <STC15F2K60S2.H>
#include "iic.h"
/* ========== 全局变量 ========== */
unsigned char Key_Val, Key_Down, Key_Up, Key_Old;
unsigned char Key_Slow_Down; // 按键扫描减速(10ms)
unsigned int Seg_Slow_Down; // 数码管刷新减速(500ms)
unsigned char Seg_Pos; // 数码管位选
unsigned char Seg_Buf[8] = {10,10,10,10,10,10,10,10}; // 显示缓存
unsigned char Seg_Point[8] = {0,0,0,0,0,0,0,0}; // 小数点
unsigned char ucLed[8] = {0,0,0,0,0,0,0,0}; // LED 状态
// ⭐ 核心数据:存储两个温度设定值
unsigned char dat[2] = {30, 60}; // 默认 30°C 和 60°C
/* ========== 按键处理 ========== */
void Key_Proc()
{
if(Key_Slow_Down) return;
Key_Slow_Down = 1;
Key_Val = Key_Read();
Key_Down = Key_Val & (Key_Val ^ Key_Old);
Key_Up = ~Key_Val & (Key_Val ^ Key_Old);
Key_Old = Key_Val;
switch(Key_Down) {
case 19: // S19:温度1 减少
if(dat[0] > 0) dat[0] -= 10;
break;
case 18: // S18:温度2 减少
if(dat[1] > 0) dat[1] -= 10;
break;
case 17: // S17:保存到 EEPROM
EEPROM_Write(dat, 0, 2); // 从地址 0 开始写 2 个字节
ucLed[0] = 1; // LED 闪烁提示保存成功
break;
}
}
/* ========== 数码管显示 ========== */
void Seg_Proc()
{
if(Seg_Slow_Down) return;
Seg_Slow_Down = 1;
// 显示格式:[温度1] -- [温度2]
Seg_Buf[0] = dat[0] / 10; // 十位
Seg_Buf[1] = dat[0] % 10; // 个位
Seg_Buf[2] = 10; // 熄灭
Seg_Buf[3] = 10; // 熄灭
Seg_Buf[4] = 10; // 熄灭
Seg_Buf[5] = 10; // 熄灭
Seg_Buf[6] = dat[1] / 10; // 十位
Seg_Buf[7] = dat[1] % 10; // 个位
}
/* ========== LED 闪烁 ========== */
void Led_Proc()
{
static unsigned int led_count = 0;
if(ucLed[0]) {
if(++led_count >= 500) { // 500ms 后自动熄灭
ucLed[0] = 0;
led_count = 0;
}
}
}
/* ========== 定时器初始化 ========== */
void Timer0Init(void) // 1ms @ 12MHz
{
AUXR &= 0x7F;
TMOD &= 0xF0;
TL0 = 0x18;
TH0 = 0xFC;
TF0 = 0;
TR0 = 1;
ET0 = 1;
EA = 1;
}
/* ========== 定时器中断 ========== */
void Timer0Server() interrupt 1
{
if(++Key_Slow_Down == 10) Key_Slow_Down = 0;
if(++Seg_Slow_Down == 500) Seg_Slow_Down = 0;
if(++Seg_Pos == 8) Seg_Pos = 0;
Seg_Disp(Seg_Pos, Seg_Buf[Seg_Pos], Seg_Point[Seg_Pos]);
Led_Disp(Seg_Pos, ucLed[Seg_Pos]);
}
/* ========== 主函数 ========== */
void main()
{
// ⭐ 上电第一件事:从 EEPROM 读取保存的数据
EEPROM_Read(dat, 0, 2); // 读取地址 0~1 的 2 个字节
System_Init();
Timer0Init();
while(1) {
Key_Proc(); // 按键扫描
Seg_Proc(); // 数码管显示
Led_Proc(); // LED 闪烁
}
}
6.3 程序运行流程图
┌──────────────────────────────────────────┐
│ 系统上电 │
│ ↓ │
│ 从 EEPROM 读取上次保存的数据 │
│ EEPROM_Read(dat, 0, 2); │
│ ↓ │
│ 显示读取的数据:30°C 和 60°C │
│ ↓ │
│ 用户按下 S19/S18 调整温度 │
│ ↓ │
│ 用户按下 S17 保存 │
│ EEPROM_Write(dat, 0, 2); │
│ ↓ │
│ 断电关机... │
│ ↓ │
│ 重新上电 → 数据依然是修改后的值! │
└──────────────────────────────────────────┘
七、常见问题与解决方案
问题 1:数据写入后读出来全是 0xFF
可能原因:
-
写入延时不够(
I2C_Delay(200)太小) -
硬件连接问题(SDA/SCL 接反或松动)
-
地址超出范围(>255)
解决方案:
// 增加写入延时
I2C_Delay(500); // 从 200 改到 500
// 检查地址合法性
if(addr > 255) return; // 防止地址越界
问题 2:跨页写入数据丢失
错误示范:
unsigned char data[10] = {0,1,2,3,4,5,6,7,8,9};
EEPROM_Write(data, 5, 10); // ❌ 从地址 5 写 10 个字节
原因分析:
第 0 页:[5][6][7] ← 只能写 3 个
第 1 页:[8][9][10][11][12][13][14][15] ← 这里就乱了
正确写法:
// 方案1:分两次写
EEPROM_Write(data, 0, 8); // 第一页写 8 个
EEPROM_Write(data+8, 8, 2); // 第二页写剩下 2 个
// 方案2:改用字节写入
for(i=0; i<10; i++) {
EEPROM_Write(&data[i], i, 1);
}
问题 3:数据读取异常
症状:读出的数据和写入的不一致
排查步骤:
// 1. 写入后立即读取测试
EEPROM_Write(dat, 0, 2);
Delay_ms(10); // 等待写入完成
EEPROM_Read(test, 0, 2);
// 2. 用串口打印调试
printf("写入: %d %d\n", dat[0], dat[1]);
printf("读取: %d %d\n", test[0], test[1]);
// 3. 检查数组是否越界
if(num > 8) return; // 防止一次写太多
八、数据类型与存储空间
8.1 Keil C51 中的数据类型
| 类型 | 字节数 | 取值范围 | 实际应用 |
|---|---|---|---|
unsigned char |
1 字节 | 0 ~ 255 | 温度、湿度、按键值 |
unsigned int |
2 字节 | 0 ~ 65535 | 计时器、计数器 |
unsigned long |
4 字节 | 0 ~ 4294967295 | 长时间计时 |
8.2 存储空间规划示例
AT24C02 内存布局(256 字节):
┌─────────────────────────────────────────┐
│ 0~7 : 系统配置参数(温度上下限等) │
│ 8~15 : 用户设置数据(亮度、音量等) │
│ 16~23 : 历史记录1 │
│ 24~31 : 历史记录2 │
│ ... │
│ 248~255: 校验码或标志位 │
└─────────────────────────────────────────┘
九、知识点总结
| 核心概念 | 关键要点 | 记忆口诀 |
|---|---|---|
| 设备地址 | 写 0xA0,读 0xA1 | “爱写爱读” |
| 页大小 | 8 字节/页 | “八仙过海” |
| 地址对齐 | 起始地址必须是 8 的倍数 | “起点要整齐” |
| 写入延时 | 每字节写入后延时 5ms | “写完要等等” |
| 应答信号 | 0=继续,1=停止 | “零继续,一停止” |
结语
掌握 AT24C02,你就拥有了让单片机"拥有记忆"的能力!记住:
EEPROM 是数据的保险箱
,正确使用它,你的系统将更加智能可靠!
快速参考
/* ========== 常用代码片段 ========== */
// 保存单个变量
unsigned char temp = 25;
EEPROM_Write(&temp, 0, 1);
// 保存数组
unsigned char data[4] = {10, 20, 30, 40};
EEPROM_Write(data, 0, 4);
// 读取数据
unsigned char buffer[4];
EEPROM_Read(buffer, 0, 4);
// 清空 EEPROM(写入 0xFF)
unsigned char clear[8] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
for(i=0; i<32; i++) { // 32 页
EEPROM_Write(clear, i*8, 8);
}