【自学嵌入式:51单片机】用I2C通信读写AT24C02芯片,用串口通信获取AT24C02芯片存储的数据


所有自学嵌入式:51单片机系列的操作相关器件的代码都汇总到了:https://gitee.com/qin-ruiqian/Qin51

AT24C02

AT24C02是EEPROM芯片(读做E方P ROM芯片),可读可写,掉电不丢失,共256B
STC89C52RC芯片有ROM也有RAM,ROM掉电不丢失但不可写,RAM可读可写但掉电丢失
image
E0 E1 E2都是其地址线,图中把这3个引脚均接地了,也就是都是低电平0,AT24C02的设备地址高4位是固定的,1010,低3位由E0 E1 E2这3根地址线来决定,如果全部都接地,它的低3位都是000,则这个模块的设备地址是1010 000,也就是十六进制的0x50
WE 写入使能端,接GND默认为0,低电平触发(WE上面画了一个横线,表示低电平开启)
VCC、GND的电压范围在1.8V到5.5V
其内部结构图如下:
image
数据网格左侧连着一个译码器,SERIAL MUX是串行数据选择端,通过Y译码器将数据一位一位地输出出去,DATA RECOVERY数据恢复(擦除用),DATA WORD ADDR/COUNTER(数据字地址/计数器),用于I2C通信的模块里有START STOP LOGIC通信启动停止逻辑,DEVICE ADDRESS COMPARATOR器件地址比较器,SERIAL CONTROL LOGIC串行控制逻辑,它是把I2C通信的数据拿过来进行操作。

AT24C02数据帧

image
image

实验

写程序,单片机上电后,不断读取EEPROM芯片0x02地址的的数据,将该数据通过串口传送给上位机(电脑端),利用电脑端串口调试助手进行显示,然后数据自动加1,之后再回写到EEPROM芯片中,回写的地址还是0x02
代码如下:

#include <REGX52.H>
#include <intrins.h> // 延时空语句
sbit SCL = P2^1;
sbit SDA = P2^0;

//延时用(测试)
void Delay(unsigned int xms)		//@11.0592MHz
{
	unsigned char i, j;

	while(xms--)
	{
		i = 2;
		j = 199;
		do
		{
			while (--j);
		} while (--i);
	}
}


//初始化串口通信
void initScon()
{
	SCON = 0x50; //0101 0000,串口通信方式1,由定时器控制,高4位的最低位REN为1,允许接收控制位,允许接收
	TMOD = 0x20; //设置T1工作方式2,8位自动重载
	TH1 = 0xFA; //波特率4800
	TL1 = 0XFA;
	ES = 1; //串口中断
	EA = 1; //开总中断
	TR1 = 1; //打开波特率发生器(定时器1)
}

//UART串口通信需要发送的字节
void sentUART(unsigned char Byte)
{
	TI = 1; //打开发送中断,准备发送数据
	SBUF = Byte; //待发送数据写入缓存
	while(TI == 0); //TI变成1才算发送完成
	TI = 0; //软件复位
}

//I2C通信专用延时程序
void delayI2C()
{
	_nop_();
	_nop_();
	_nop_();
	_nop_();
}

//初始化I2C通信
void initI2C()
{
	//先都拉高成高电平
	SCL = 1;
	SDA = 1;
	//下降沿
	delayI2C();
	SDA = 0;
	delayI2C();
	SCL = 0;
}

//关闭I2C通信
void closeI2C()
{
	//先都拉低
	SCL = 0;
	SDA = 0;
	delayI2C();
	SCL = 1; //先拉高SCL
	delayI2C();
	SDA = 1; //SCL高电平期间,再拉高SDA,这样就产生了一个上升沿
	delayI2C();
	
}

//I2C写入一个字节,I2C_Byte是待传送的数据
void writeByteI2C(unsigned char I2C_Byte)
{
	unsigned char i = 0;
	for(i = 0; i<8; i++)
	{
		//I2C_Byte & 0x80(1000 0000)
		//获取最高位,看是1还是0
		if(I2C_Byte & 0x80)
		{
			SDA = 1;
		}
		else
		{
			SDA = 0;
		}
		delayI2C();
		SCL = 1; //拉高电平,发送数据
		delayI2C();
		SCL = 0; //拉低,发送完成
		I2C_Byte <<= 1; //左移,循环读取每一位,让每一位都成为首位
	}
}

// I2C读一个字节
unsigned char readByteI2C()
{
	unsigned char i, Byte = 0x00;
	for(i = 0; i<8; i++)
	{
		SDA = 1; //拉高SDA,释放I2C总线
		//读总线之前一定要将SDA拉高
		SCL = 1; //然后将SCL拉高,准备接收SDA状态
		delayI2C();
		if(SDA)
		{
			Byte |= (0x80>>i); //依次循环右移,得到一个字节(0x00分别或每一位得到,如果是低电平,不需要操作,该位就是0)
		}
		SCL = 0;
	}
	return Byte;
}

//读取I2C总线应答信号
bit readAckI2C()
{
	bit Ack_bit; //应答信号
	SDA = 1; //拉高SDA,释放I2C
	delayI2C();
	SCL = 1; // 把SCL拉高,第9个时钟信号
	delayI2C();
	Ack_bit = SDA; //读取应答信号
	delayI2C();
	SCL = 0; //拉低SCL,结束接收应答
	return Ack_bit;
}

//I2C发送应答位
void writeAckI2C(bit Ack_bit)
{
	//根据应答状态变量来确定SDA是1还是0
	if(Ack_bit)
	{
		SDA = 1;
	}
	else
	{
		SDA = 0;
	}
	//第9个时钟周期
	//拉高SCL,再拉低SCL,完成一次发送应答信号
	SCL = 1; 
	SCL = 0;
}

//向EEPROM中写入一个字节,addr是地址
void writeByteE2PROM(unsigned char addr, unsigned char dat)
{
	//启动信号
	initI2C();
	//写入设备地址和写数据位
	writeByteI2C(0x50<<1); //寻址器件,后续为写操作,左移1位,右侧低位多出来的0是读写位的写入0
	//接收应答位
	readAckI2C();
	//写入寄存器地址
	writeByteI2C(addr);
	//接收应答位
	readAckI2C();
	//写入数据
	writeByteI2C(dat);
	//接收应答位
	readAckI2C();
	//停止信号
	closeI2C();
}

//读取EEPROM中的一个字节,addr是地址
unsigned char readByteE2PROM(unsigned char addr)
{
	unsigned char dat = 0;
	//启动信号
	initI2C();
	//写入设备地址和写数据位
	writeByteI2C(0x50<<1);
	//接收应答位
	readAckI2C();
	//写入寄存器地址
	writeByteI2C(addr);
	//接收应答位
	readAckI2C();
	//这里不要有停止信号,看上面的示意图
	//closeI2C();
	//启动信号
	initI2C();
	//写入设备地址和写数据位(最低位为1,也就是读写位为1,读数据)
	writeByteI2C((0x50<<1)|0x01);
	//接收应答位
	readAckI2C();
	dat = readByteI2C(); //读取一个字节数据
	//发送应答位
	writeAckI2C(1);//这里应答位要发送1
	//停止信号
	closeI2C();
	///////
	return dat;
}

void main()
{
	unsigned char dat = 0;
	initScon();
	dat = readByteE2PROM(0x02); //读取E2PROM 0x02地址中的一个字节
		dat++;
		if(dat>=10)
		{
			dat = 0;
		}
		sentUART(dat); //送串口
		writeByteE2PROM(0x02, dat); //回写到对应地址
	while(1)
	{
	}
}

其效果是每次EEPROM自动记录上次的值,每次单片机开机后都加1,多次反复复位单片机,最后在串口助手中能看到以下效果(其实用LCD1602也行,我懒得写)
image

补充

24C02芯片手册列给了当前地址读的功能
内部地址计数器(指针)保存着上次访问最后一个地址加1的值。只要芯片有电,该地址就一直保存。当读到最后页的最后字节,地址会回转到0;当写到某页尾的最后一个字节,地址会回转到该页的首字节。
接收器件地址(读/写选择位为1)、EEPROM应答ACK后,当前地址的数据就随时钟送出。主器件无需应答0,但需发送停止条件,见下图。
image
也就是不用指定地址了,它自动按递增顺序存进去

【注】可以把 EEPROM 里的 “页” 理解成 “固定大小的存储小分区” :
想象 EEPROM 是个 “小仓库”,为了方便 批量读写数据 ,把仓库分成很多 “小格子组” ,每个 “小格子组” 就是 1 页 。
比如:假设 1 页有 16 个字节(具体大小看芯片型号),就像 1 页纸上画了 16 个格子,写数据时,填满这页最后 1 个格子后,再写就会 绕回这页第 1 个格子重新写 ;读数据时,也会按 “页” 的边界管理地址,避免乱套~
简单说:“页” 是 EEPROM 里为了规范读写、防止地址乱跑,预先划好的「固定长度小存储块」 ,类似笔记本里 “一页纸有固定行数” 的逻辑 。

posted @ 2025-08-02 11:45  秦瑞迁  阅读(284)  评论(0)    收藏  举报