当51单片机应用系统需要存放一些在掉电后需要保存的数据时,可以使用E2^{2}2PROM。AT24C04是很常用的E2^{2}2PROM芯片。
AT24C04A是Atmel公司出品的I2^{2}2C总线接口E2^{2}2PROM,有8KB的内部存储空间,采取8字节/页、256页、2个块的分页方式。
AT24C04A的电路简图如上所示,主要有A1、A2、WP、SDA、SCL五个引脚。
SCK:I2^{2}2C总线的时钟引脚;
SDA:I2^{2}2C总线的数据引脚;
A1、A2:地址引脚,用于决定AT24C04A芯片的I2^{2}2C地址;
WP:写保护引脚。当该地址连接到GND时,芯片可以进行正常的读/写操作;当该引脚连接到VCC时,不同的芯片有不同的应用方式。
AT24C04A有自己独立的I2^{2}2C总线地址,其地址结构为“1010+A2、A1+内部页选择位+读写选择位”。当A2、A1均为0时,对AT24C02A的内部页面1进行读操作的地址是0xA1,写操作地址是0xA0。
AT24C04A的操作分为写操作和读操作,写操作包括字节写和页面写两种工作方式;而读操作则分为指定位置读、连续读和当前地址读三种工作方式。
I2^{2}2C的基本结构与主要特点
在单片机系统中,带有I2^{2}2C总线接口的电路现在呗使用得越来越多,主要因为采用I2^{2}2C总线接口的器件连接线和引脚数目少,成本低。且与单片机连接简单,结构紧凑,在总线上增加器件不影响系统正常工作,系统修改和可扩展性好,即使工作时钟不同的器件,也可以直接连接到总线上。
I2^{2}2C总线是由Philips公司开发的一种简单、双向二线制同步串行总线。它只需要两根线,即可在连接与总线上的器件之间传递信息。
I2^{2}2C总线的特点:
I2^{2}2C总线是由数据线SDA和时钟线SCL构成的串行总线,可发送和接收数据。各种采用I2^{2}2C总线标准的器件均并联在总线上,每个器件内部都有I2^{2}2C接口电路,用于实现I2^{2}2C总线的连接。
每个器件都有唯一的地址,器件两两之间都可以进行信息传送。当某个器件向总线上发送信息时,它就是发送器(也叫主器件),而当其从总线上接收信息时,它又成为接收器(也叫从器件)。在信息传输过程中,主器件发送的信号分为器件地址码、器件单元地址和数据3部分,其中器件地址码用来选择从器件,确定操作的类型(类型为发送信息或者接受信息);器件单元地址用于选择器件内部的单元;数据是在各器件间传递的信息。处理过程就像打电话一样,只有拨通号码才能进行信息交流。各控制电路虽然挂在同一条总线上,却彼此独立,互不相关。
I2^{2}2C信息传送与读写过程
当 I2^{2}2C总线没有进行信息传送时,数据线(SDA)和时钟线(SCL)都为高电平。当主器件向某个器件传送信息时,首先应向总线传递开始信号,然后才能传送信息,当信息传送结束时,应传送结束信号,开始信号和结束信号规定如下:
开始信号和结束信号之间传递的是信息,信息的字节数没有限制,但每个字节必须为8位,高位在前,低位在后。
数据线SDA上每一位信息状态的改变只能发生在时钟线SCL为低电平期间,因为SCL为高电平期间SDA状态的改变已经被用来表示开始信号和结束信号。
主器件每次传送的信息的第一个字节必须是器件地址码,第二个字节为器件单元地址,用于实现选择所操作的器件的内部单元,从第三个字节开始,为传送的数据。
4bit器件类型码 | 片选 | R/W‾\overline{W}W |
---|---|---|
D7-D4 | D3-D1 | D0 |
高4位为器件类型识别码(不同的芯片类型有不同的定义,EEPROM一般应为1010);接着三位为片选,同种类型器件最多可接8个,高7位用于选择对应的器件;最后一位为读写位,当为1时进行读操作,表示主器件从高总线上读取信息,为0时进行写操作,表示主器件将传送信息到总线上。
I2^{2}2C总线数据传送时,每传送一个字节数据后,都必须有应答信号。与应答信号相对应的时钟由主器件产生,发送器必须在这一时钟位上释放数据线,使其处于高电平状态,以便接收器在这一位上送出应答信号。从器件在接收到起始信号后,每接收一个地址编码或数据后都会会送一个低电平应答信号,用以表示已收到。主器件收到应答信号后,可根据实际情况做出是否继续传递信号的判断。若未收到低电平应答信号,则判断为从器件出现故障。
当主器件从从器件读取数据时,从器件发送数据,主器件接收数据,主器件接收到数据后,也发送一个低电平应答信号,从器件接收后,继续发送下一个数据,如果主器件没有发送低电平应答信号,而是高电平非应答信号,则从器件结束数据传送,且等待主器件的结束信号,然后结束读操作。
I2^{2}2C的读写操作
从当前地址读从所选器件的当前地址读,读的字节数不指定,格式如下:
开始信号 | 控制码(R/W‾\overline{W}W=1) | 应答信号 | 数据1 | 应答信号 | 数据2 | 应答信号 | 结束信号 |
---|
从所选器件的指定地址读,读的字节数不指定,格式如下:
开始信号 | 控制码(R/W‾\overline{W}W=1) | 应答信号 | 器件地址 | 应答信号 | 开始信号 | 控制码(R/W‾\overline{W}W=1) | 应答信号 | 数据1 | 应答信号 | 数据2 | 应答信号 | 结束信号 |
---|
从所选器件的指定地址写,写的字节数不指定,格式如下:
开始新号 | 控制码(R/W‾\overline{W}W=1) | 应答信号 | 器件单元地址 | 应答信号 | 数据1 | 应答信号 | 数据2 | 应答信号 | 结束信号 |
---|
在连续读工作方式的操作过程中,当地址计数器的值超过了器件的最大地址之后会自动溢出,从而覆盖最低地址数据。
实例应用使用串口控制向AT24C04A中写入并且读出数据。单片机通过串口从PC接收5字节数据,然后将所接收数据写入AT24C04A的指定单元,再将对应单元的数据读出后通过串口发送出去。
#include
#include
#define R24C04ADD 0xA1
#define W24C04ADD 0xA0
sbit SDA = P1 ^ 7; //数据线
sbit SCL = P1 ^ 6; //时钟线
sbit LED = P2 ^ 0; //指示灯
bit bAck; //应答标志 当bbAck=1是为正确的应答
unsigned char wBuff[5]; //待写入字节缓冲区
unsigned char rBuff[5]; //读出的字节缓冲区
unsigned char rxCounter = 0; //接收计数器
unsigned char wrCounter = 0; //读写计数器
bit rxFlg = 0; //接收缓冲标志位
void StartI2C(); //启动函数
void StopI2C(); //结束函数
void AckI2C(); //应答函数
void SendByte(unsigned char c); //字节发送函数
unsigned char RevByte(); //接收一个字节数据函数
unsigned char WIICByte(unsigned char WChipAdd,unsigned char InterAdd,unsigned char WIICData);
//WChipAdd:写器件地址;InterAdd:内部地址;WIICData:待写数据;如写正确则返回0xff,
//否则返回对应错误步骤序号
unsigned char RIICByte(unsigned char WChipAdd,unsigned char RChipAdd,unsigned char InterDataAdd);
//WChipAdd:写器件地址;RChipAdd:读器件地址;InterAdd:内部地址;如写正确则返回数据,
//否则返回对应错误步骤序号
//向指定器件的内部指定地址发送一个指定字节
unsigned char WIICByte(unsigned char WChipAdd,unsigned char InterAdd,unsigned char WIICData)
{
StartI2C(); //启动总线
SendByte(WChipAdd); //发送器件地址以及命令
if (bAck==1) //收到应答
{
SendByte(InterAdd); //发送内部子地址
if (bAck ==1)
{
SendByte(WIICData); //发送数据
if(bAck == 1)
{
StopI2C(); //停止总线
return(0xff);
}
else
{
return(0x03);
}
}
else
{
return(0x02);
}
}
return(0x01);
}
//读取指定器件的内部指定地址一个字节数据
unsigned char RIICByte(unsigned char WChipAdd,unsigned char RChipAdd,unsigned char InterDataAdd)
{
unsigned char TempData;
TempData = 0;
StartI2C(); //启动
SendByte(WChipAdd); //发送器件地址以及读命令
if (bAck==1) //收到应答
{
SendByte(InterDataAdd); //发送内部子地址
if (bAck ==1)
{
StartI2C();
SendByte(RChipAdd);
if(bAck == 1)
{
TempData = RevByte(); //接收数据
StopI2C(); //停止I2C总线
return(TempData); //返回数据
}
else
{
return(0x03);
}
}
else
{
return(0x02);
}
}
else
{
return(0x01);
}
}
//启动I2C总线,即发送起始条件
void StartI2C()
{
SDA = 1; //发送起始条件数据信号
_nop_();
SCL = 1;
_nop_(); //起始建立时间大于4.7us
_nop_();
_nop_();
_nop_();
_nop_();
SDA = 0; //发送起始信号
_nop_();
_nop_();
_nop_();
_nop_();
_nop_();
SCL = 0; //时钟操作
_nop_();
_nop_();
}
//结束I2C总线,即发送I2C结束条件
void StopI2C()
{
SDA = 0; //发送结束条件的数据信号
_nop_(); //发送结束条件的时钟信号
SCL = 1; //结束条件建立时间大于4us
_nop_();
_nop_();
_nop_();
_nop_();
_nop_();
SDA = 1; //发送I2C总线结束命令
_nop_();
_nop_();
_nop_();
_nop_();
_nop_();
}
//发送一个字节的数据
void SendByte(unsigned char c)
{
unsigned char BitCnt;
for(BitCnt = 0;BitCnt < 8;BitCnt++) //一个字节
{
if((c << BitCnt)& 0x80) SDA = 1; //判断发送位
else SDA = 0;
_nop_();
SCL = 1; //时钟线为高,通知从机开始接收数据
_nop_();
_nop_();
_nop_();
_nop_();
_nop_();
SCL = 0;
}
_nop_();
_nop_();
SDA = 1; //释放数据线,准备接受应答位
_nop_();
_nop_();
SCL = 1;
_nop_();
_nop_();
_nop_();
if(SDA == 1) bAck =0;
else bAck = 1; //判断是否收到应答信号
SCL = 0;
_nop_();
_nop_();
}
//接收一个字节的数据
unsigned char RevByte()
{
unsigned char retc;
unsigned char BitCnt;
retc = 0;
SDA = 1;
for(BitCnt=0;BitCnt<8;BitCnt++)
{
_nop_();
SCL = 0; //置时钟线为低,准备接收
_nop_();
_nop_();
_nop_();
_nop_();
_nop_();
SCL = 1; //置时钟线为高使得数据有效
_nop_();
_nop_();
retc = retc <4) //到了第16个字节
{
rxCounter = 0; //清除
rxFlg = 1; //接收缓冲标志位置位
}
}
}
main()
{
unsigned char temp;
InitUART(); //初始化串口
while(1)
{
while(rxFlg == 0); //等待接收标志位被置位
rxFlg = 0; //清除接收标志位
for(wrCounter=0;wrCounter<5;wrCounter++)
{
rBuff[wrCounter] = RIICByte(W24C04ADD,R24C04ADD,(0x01+wrCounter));
//连续读取一字节数据,共16字节
Send(rBuff[wrCounter]); //将数据通过串口发送
}
for(wrCounter=0;wrCounter<5;wrCounter++)
{
temp = WIICByte(W24C04ADD,(0x01+wrCounter),wBuff[wrCounter]);
// Send(temp);
//连续写入16字节数据
}
}
}