第12章 I2C总线与EEPROM(12.3 12.4)

内容分享7小时前发布
0 0 0

12.3 EEPROM的学习

在实际应用中,保存在单片机RAM中的数据,掉电后就丢失了,使用code关键字保存在单片机的FLASH中的数据,又不能随意改变,也就是不能用它来记录变化的数值。但是在某些场合,又的确 需要记录下某些数据,而它们还时常需要改变或更新,掉电之后数据还不能丢失,列如家用电表度数,电视机里边的频道记忆,一般都是使用EEPROM来保存数据,特点就是掉电后不丢失。Kingst51开发板上使用的这个器件是24C02,是一个容量大小是2Kbits,也就是256个字节的EEPROM。一般情况下,EEPROM拥有30万到100万次的寿命,也就是它可以反复写入30-100万次,而读取次数是无限的。

24C02是一个基于I2C通信协议的器件,但要分清楚,I2C是一个通信协议,它拥有严密的通信时序逻辑要求,而EEPROM是一个器件,只是这个器件采用了I2C协议的接口与单片机相连而已,二者并没有必然的联系,EEPROM可以用其它接口,I2C也可以用在其它许多器件上。

12.3.1 EEPROM单字节读写操作时序

1、EEPROM写数据流程

(1)第一是I2C的起始信号,接着跟上首字节,也就是I2C的器件地址,并且在读写方向上选择“写”操作。

(2)发送数据的存储地址。24C02一共256个字节的存储空间,地址从0x00~0xFF,想把数据存储在哪个位置,此刻写的就是哪个地址。

(3)发送要存储的数据第一个字节、第二个字节……注意在写数据的过程中,EEPROM每个字节都会回应一个“应答位0”,来通知用户写EEPROM数据成功,如果没有回应答位,说明写入不成功。

在写数据的过程中,每成功写入一个字节,EEPROM存储空间的地址就会自动加1,当加到0xFF后,再写一个字节,地址会溢出又变成了0x00。

2、EEPROM读数据流程

(1)第一是I2C的起始信号,接着跟上首字节,也就是I2C的器件地址,并且在读写方向上选择“写”操作。明明是读数据为何方向也要选“写”呢?24C02一共有256个地址,选择写操作,是为了把所要读的数据的存储地址先写进去,告知EEPROM要读取哪个地址的数据。这就如同打电话,先拨总机号码(EEPROM器件地址),而后还要继续拨分机号码(数据地址),而拨分机号码这个动作,主机依旧是发送方,方向依然是“写”。

(2)发送要读取的数据的地址,注意是地址而非存在EEPROM中的数据,通知EEPROM要哪个分机的信息。

(3)重新发送I2C起始信号和器件地址,并且在方向位选择“读”操作。

这3步当中,每一个字节实际上都是在“写”,所以每一个字节EEPROM都会回应一个“应答位0”。

(4)读取从器件发回的数据,读一个字节后,如果还想继续读下一个字节,就发送一个“应答位ACK(0)”,如果不想读了,通知EEPROM不想要数据了,那就发送一个“非应答位NAK(1)”。

和写操作规则一样,每读一个字节地址会自动加1。如果想继续往下读,给EEPROM一个ACK(0)低电平后,再继续给SCL完整的时序,EEPROM会继续往外送数据。如果不想读了,直接给一个NAK(1)高电平。

梳理一下几个要点:

1、在本例中单片机是主机,24C02是从机;

2、无论是读是写,SCL始终都是由主机控制的;

3、写的时候应答信号由从机给出,表明从机是否正确接收了数据;

4、读的时候应答信号则由主机给出,表明是否继续读下去。

下面写一个程序,读取EEPROM的0x02这个地址上的一个数据,不管这个数据之前是多少都将读出来的数据加1,再写到EEPROM的0x02这个地址上。此外将I2C的程序建立一个文件,写一个I2C.c程序文件,形成又一个程序模块。

/******************************I2C.c文件程序源代码******************************/

#include <reg52.h>

#include <intrins.h>

#define I2CDelay() {_nop_();_nop_();_nop_();_nop_();}

sbit I2C_SCL = P3^7;

sbit I2C_SDA = P3^6;

/* 产生总线起始信号 */

void I2CStart()

{

I2C_SDA = 1; //第一确保SDA、SCL都是高电平

I2C_SCL = 1;

I2CDelay();

I2C_SDA = 0; //先拉低SDA

I2CDelay();

I2C_SCL = 0; //再拉低SCL

}

/* 产生总线停止信号 */

void I2CStop()

{

I2C_SCL = 0; //第一确保SDA、SCL都是低电平

I2C_SDA = 0;

I2CDelay();

I2C_SCL = 1; //先拉高SCL

I2CDelay();

I2C_SDA = 1; //再拉高SDA

I2CDelay();

}

/* I2C总线写操作,dat-待写入字节,返回值-从机应答位的值 */

bit I2CWrite(unsigned char dat)

{

bit ack; //用于暂存应答位的值

unsigned char mask; //用于探测字节内某一位值的掩码变量

for (mask=0x80; mask!=0; mask>>=1) //从高位到低位依次进行

{

if ((mask&dat) == 0) //该位的值输出到SDA上

I2C_SDA = 0;

else

I2C_SDA = 1;

I2CDelay();

I2C_SCL = 1; //拉高SCL

I2CDelay();

I2C_SCL = 0; //再拉低SCL,完成一个位周期

}

I2C_SDA = 1; //8位数据发送完后,主机释放SDA,以检测从机应答

I2CDelay();

I2C_SCL = 1; //拉高SCL

ack = I2C_SDA; //读取此时的SDA值,即为从机的应答值

I2CDelay();

I2C_SCL = 0; //再拉低SCL完成应答位,并保持住总线

return (~ack); //应答值取反以符合一般的逻辑:

//0=不存在或忙或写入失败,1=存在且空闲或写入成功

}

/* I2C总线读操作,并发送非应答信号,返回值-读到的字节 */

unsigned char I2CReadNAK()

{

unsigned char mask;

unsigned char dat;

I2C_SDA = 1; //第一确保主机释放SDA

for (mask=0x80; mask!=0; mask>>=1) //从高位到低位依次进行

{

I2CDelay();

I2C_SCL = 1; //拉高SCL

if(I2C_SDA == 0) //读取SDA的值

dat &= ~mask; //为0时,dat中对应位清零

else

dat |= mask; //为1时,dat中对应位置1

I2CDelay();

I2C_SCL = 0; //再拉低SCL,以使从机发送出下一位

}

I2C_SDA = 1; //8位数据发送完后,拉高SDA,发送非应答信号

I2CDelay();

I2C_SCL = 1; //拉高SCL

I2CDelay();

I2C_SCL = 0; //再拉低SCL完成非应答位,并保持住总线

return dat;

}

/* I2C总线读操作,并发送应答信号,返回值-读到的字节 */

unsigned char I2CReadACK()

{

unsigned char mask;

unsigned char dat;

I2C_SDA = 1; //第一确保主机释放SDA

for (mask=0x80; mask!=0; mask>>=1) //从高位到低位依次进行

{

I2CDelay();

I2C_SCL = 1; //拉高SCL

if(I2C_SDA == 0) //读取SDA的值

dat &= ~mask; //为0时,dat中对应位清零

else

dat |= mask; //为1时,dat中对应位置1

I2CDelay();

I2C_SCL = 0; //再拉低SCL,以使从机发送出下一位

}

I2C_SDA = 0; //8位数据发送完后,拉低SDA,发送应答信号

I2CDelay();

I2C_SCL = 1; //拉高SCL

I2CDelay();

I2C_SCL = 0; //再拉低SCL完成应答位,并保持住总线

return dat;

}

/*****************************main.c文件程序源代码******************************/

#include <reg52.h>

extern void I2CStart();

extern void I2CStop();

extern unsigned char I2CReadNAK();

extern bit I2CWrite(unsigned char dat);

unsigned char E2ReadByte(unsigned char addr);

void E2WriteByte(unsigned char addr, unsigned char dat);

void main()

{

unsigned char dat;

dat = E2ReadByte(0x02); //读取指定地址上的一个字节

dat++; //将其数值+1

E2WriteByte(0x02, dat); //再写回到对应的地址上

while (1);

}

/* 读取EEPROM中的一个字节,addr-字节地址 */

unsigned char E2ReadByte(unsigned char addr)

{

unsigned char dat;

I2CStart();

I2CWrite(0x50<<1); //寻址器件,后续为写操作

I2CWrite(addr); //写入存储地址

I2CStart(); //发送重复启动信号

I2CWrite((0x50<<1)|0x01); //寻址器件,后续为读操作

dat = I2CReadNAK(); //读取一个字节数据

I2CStop();

return dat;

}

/* 向EEPROM中写入一个字节,addr-字节地址 */

void E2WriteByte(unsigned char addr, unsigned char dat)

{

I2CStart();

I2CWrite(0x50<<1); //寻址器件,后续为写操作

I2CWrite(addr); //写入存储地址

I2CWrite(dat); //写入一个字节数据

I2CStop();

}

I2C.c文件提供了I2C总线底层函数,包括起始、停止、字节写、字节读+应答、字节读+非应答。将这个程序复编译会发现Keil软件提示一个警告:*** WARNING L16: UNCALLED SEGMENT, IGNORED FOR OVERLAY PROCESS,这个警告的意思是在代码中存在没有被调用过的变量或者函数,I2C.c文件中的I2CReadACK()这个函数在本例中没有用到。

读取EEPROM的时候,由于只读了一个字节就要告知EEPROM不需要再读数据了,读完后直接发送一个“NAK”,因此只调用了I2CReadNAK()这个函数,而并没有调用I2CReadACK()这个函数。今后很可能读数据的时候要连续读几个字节,因此这个函数写在了I2C.c文件中,作为I2C功能模块的一部分是必要的,方便这个文件后来移植到其他程序中使用,因此这个警告在这里就不必管它了。

将这个程序中,I2C的读写EEPROM操作用逻辑分析仪抓出来,并且用I2C-EEPROM协议解析出来,如图12-7所示。

第12章 I2C总线与EEPROM(12.3 12.4)

图12-7 I2C-EEPROM解析结果图

从图12-7能看出,第一个字节是器件地址0x50+ACK,第二个字节是数据地址0x02+ACK,第三个字节是器件地址0x50+ACK,第四个是读取到了0x04+NAK数据,第五个字节是器件地址0x50+ACK,第6个字节是数据地址0x02+ACK,第七个字节是写入数据0x05+ACK。

12.3.2 EEPROM多字节读写操作时序

读取EEPROM的时候很简单,EEPROM根据主机的时序,直接就把数据送出来了,但是写EEPROM却没有这么简单了。给EEPROM发送数据后,先保存在了EEPROM的缓存,EEPROM必须要把缓存中的数据搬移到“非易失”的区域,才能达到掉电不丢失的效果。而往非易失区域写需要必定的时间,每种器件不完全一样,ATMEL公司的24C02的这个写入时间最高不超过5ms。在往非易失区域写的过程,EEPROM是不会再响应访问的,不仅接收不到数据,即使用I2C标准的寻址模式去寻址,EEPROM都不会应答,就如同这个总线上没有这个器件一样。数据写入非易失区域完毕后,EEPROM再次恢复正常。

12.2节程序中写数据的代码,程序上有读取应答ACK,但是读取完毕后没有做任何处理。这是由于一次只写一个字节的数据进去,等到下次重新再写的时候,时间肯定远远超过了5ms,但是如果是连续写入几个字节的时候,就必须得思考到应答位的问题了。写入一个字节后,再写入下一个字节之前,必须要等待EEPROM再次响应才可以。

先从EEPROM的0x90这个地址连续读出4个字节,然后把这4个数据分别加1,加2,加3, 加4后重新写入到这四个地址中去。I2C.c文件和之前是完全一样的,因此只把main.c文件给发出来。

/*****************************I2C.c文件程序源代码*******************************/

(此处省略,可参考之前章节的代码)

/*****************************main.c文件程序源代码******************************/

#include <reg52.h>

extern void I2CStart();

extern void I2CStop();

extern unsigned char I2CReadACK();

extern unsigned char I2CReadNAK();

extern bit I2CWrite(unsigned char dat);

void E2Read(unsigned char *buf, unsigned char addr, unsigned char len);

void E2Write(unsigned char *buf, unsigned char addr, unsigned char len);

void main()

{

unsigned char i;

unsigned char buf[5];

E2Read(buf, 0x90, sizeof(buf)); //从E2中读取一段数据

for (i=0; i<sizeof(buf); i++) //数据依次+1,+2,+3…

{

buf[i] = buf[i] + 1 + i;

}

E2Write(buf, 0x90, sizeof(buf)); //再写回到E2中

while(1);

}

/* E2读取函数,buf-数据接收指针,addr-E2中的起始地址,len-读取长度 */

void E2Read(unsigned char *buf, unsigned char addr, unsigned char len)

{

do { //用寻址操作查询当前是否可进行读写操作

I2CStart();

if (I2CWrite(0x50<<1)) //应答则跳出循环,非应答则进行下一次查询

{

break;

}

I2CStop();

} while(1);

I2CWrite(addr); //写入起始地址

I2CStart(); //发送重复启动信号

I2CWrite((0x50<<1)|0x01); //寻址器件,后续为读操作

while (len > 1) //连续读取len-1个字节

{

*buf++ = I2CReadACK(); //最后字节之前为读取操作+应答

len–;

}

*buf = I2CReadNAK(); //最后一个字节为读取操作+非应答

I2CStop();

}

/* E2写入函数,buf-源数据指针,addr-E2中的起始地址,len-写入长度 */

void E2Write(unsigned char *buf, unsigned char addr, unsigned char len)

{

while (len–)

{

do { //用寻址操作查询当前是否可进行读写操作

I2CStart();

if (I2CWrite(0x50<<1)) //应答则跳出循环,非应答则进行下一次查询

{

break;

}

I2CStop();

} while(1);

I2CWrite(addr++); //写入起始地址

I2CWrite(*buf++); //写入一个字节数据

I2CStop(); //结束写操作,以等待写入完成

}

}

函数E2Read:读数据前,要查询当前是否允许读写操作,EEPROM正常响应才表明允许。读最后一个字节之前的,全部给ACK,而读完最后一个字节,要给出一个NAK。

函数E2Write:写操作前,要查询当前EEPROM是否响应,正常响应后才可以写数据。

将I2C多字节读写EEPROM的时序部分用逻辑分析仪抓取,由于此次的读写数据量特别大,因此用逻辑分析仪抓取后,直接将解析后的数据导出到excel表格中,如图12-8所示。

第12章 I2C总线与EEPROM(12.3 12.4)

图12-8 连续读写解析后数据示意图

从图12-8表格看出,第一行为读到的4个字节的数据,下面只有红框内为写入EEPROM的数据,而红框外的为检测0x50是否响应。由于EEPROM正在将前次写入的数据搬移到非易失区,因此一直检测一直等待到EEPROM响应才能再次往里边写数据。

12.3.3 EEPROM的页写入

在向EEPROM连续写入多个字节的数据时,如果每写一个字节都要等待几ms的话,整体上的写入效率就太低了。因此EEPROM的厂商就想了一个办法,把EEPROM分页管理。24C01、24C02这两个型号是8个字节一个页,而24C04、24C08、24C16是16个字节一页。Kingst51开发板上用的型号是24C02,一共是256个字节,8个字节一页,一共有32页。

分配好页之后,同一个页内连续写入几个字节后再发送停止位,EEPROM检测到停止位后,就会一次性把这一页的数据写到非易失区域,不需要写一个字节检测一次了,并且页写入的时间也不会超过5ms。如果写入的数据跨页了,写完了一页之后,要发送一个停止位,然后等待并且检测EEPROM的空闲模式,一直等到把上一页数据完全写到非易失区域后,再进行下一页的写入,这样就可以在很大程度上提高数据的写入效率,程序如下。

/*****************************I2C.c文件程序源代码*******************************/

(此处省略,可参考之前章节的代码)

/****************************eeprom.c文件程序源代码*****************************/

#include <reg52.h>

extern void I2CStart();

extern void I2CStop();

extern unsigned char I2CReadACK();

extern unsigned char I2CReadNAK();

extern bit I2CWrite(unsigned char dat);

/* E2读取函数,buf-数据接收指针,addr-E2中的起始地址,len-读取长度 */

void E2Read(unsigned char *buf, unsigned char addr, unsigned char len)

{

do { //用寻址操作查询当前是否可进行读写操作

I2CStart();

if (I2CWrite(0x50<<1)) //应答则跳出循环,非应答则进行下一次查询

{

break;

}

I2CStop();

} while(1);

I2CWrite(addr); //写入起始地址

I2CStart(); //发送重复启动信号

I2CWrite((0x50<<1)|0x01); //寻址器件,后续为读操作

while (len > 1) //连续读取len-1个字节

{

*buf++ = I2CReadACK(); //最后字节之前为读取操作+应答

len–;

}

*buf = I2CReadNAK(); //最后一个字节为读取操作+非应答

I2CStop();

}

/* E2写入函数,buf-源数据指针,addr-E2中的起始地址,len-写入长度 */

void E2Write(unsigned char *buf, unsigned char addr, unsigned char len)

{

while (len > 0)

{

//等待上次写入操作完成

do { //用寻址操作查询当前是否可进行读写操作

I2CStart();

if (I2CWrite(0x50<<1)) //应答则跳出循环,非应答则进行下一次查询

{

break;

}

I2CStop();

} while(1);

//按页写模式连续写入字节

I2CWrite(addr); //写入起始地址

while (len > 0)

{

I2CWrite(*buf++); //写入一个字节数据

len–; //待写入长度计数递减

addr++; //E2地址递增

if ((addr&0x07) == 0) //检查地址是否到达页边界,24C02每页8字节,

{ //所以检测低3位是否为零即可

break; //到达页边界时,跳出循环,结束本次写操作

}

}

I2CStop();

}

}

遵循模块化的原则,把EEPROM的读写函数单独写成一个eeprom.c文件。其中E2Read函数和上一节是一样的,由于读操作与分页无关。重点是E2Write函数,在写入数据的时候,要计算下一个要写的数据的地址是否是一个页的起始地址,如果是的话,则必须跳出循环,等待EEPROM把当前这一页写入到非易失区域后,再进行后续页的写入。

/*****************************main.c文件程序源代码******************************/

#include <reg52.h>

extern void E2Read(unsigned char *buf, unsigned char addr, unsigned char len);

extern void E2Write(unsigned char *buf, unsigned char addr, unsigned char len);

void main()

{

unsigned char i;

unsigned char buf[5];

E2Read(buf, 0x8E, sizeof(buf)); //从E2中读取一段数据

for (i=0; i<sizeof(buf); i++) //数据依次+1,+2,+3…

{

buf[i] = buf[i] + 1 + i;

}

E2Write(buf, 0x8E, sizeof(buf)); //再写回到E2中

while(1);

}

同样数量的多字节写入时间和页写入的时间到底差别多大呢?目前把两次写入时间用逻辑分析仪给抓了出来,并且用时间标签A1和A2标注了开始位置和结束位置,如图12-9和图12-10所示,右侧显示的|A1-A2|就是最终写入5个字节所耗费的时间。多字节一个一个写入,每次写入后都需要再次通信检测EEPROM是否在“忙”,因此耗费了大量的时间,同样的写入5个字节的数据,一个一个写入用了8.4ms左右的时间,而使用页写入,并且还跨页操作,只用了3.5ms左右的时间。

第12章 I2C总线与EEPROM(12.3 12.4)

图12-9 多字节写入时间

第12章 I2C总线与EEPROM(12.3 12.4)

图12-10 跨页写入时间

12.4 练习题

1、彻底理解I2C的通信时序。

2、能够独立完成EEPROM任意地址的单字节读写、多字节的跨页连续写入读出。

3、将前边学的交通灯进行改善,使用EEPROM保存红灯和绿灯倒计时的时间,并且可以通过UART改变红灯和绿灯倒计时时间。

© 版权声明

相关文章

暂无评论

none
暂无评论...