初学51单片机之I2C总线与E2PROM二
总结下上篇博文的结论:
1:ACK信号在SCL为高电平期间会一直保持。
2:在字节数据传输过程中如果发送电平跳变,那么电平信号就会变成重复起始或者结束的信号。(上篇博文的测试方法还是不能够明确证明这个结论,因为笔者的开发版晶振已经固定是11.0592M,而改变电平信号是用赋值语句比如SDA = 0;它是需要1个机器周期的即1us。而24C02是可以工作在400KB的速率下的。因此笔者由程序引起电平跳动时序过程是必然满足起始或者结束条件的时序时间的)因此这个结论只能是可能。
在字节数据传输过程中如果发送电平跳变,那么电平信号就会可能变成重复起始或者结束的信号。所以如果电平信号的变化持续时间很短,I2C器件最终会读到什么信号,依然不是很明确。笔者手上的板子应是没有办法做实验测试了,笔者在6T的模式下即1个机器周期为0.5us的模式下测试,跳变电平依然被识别成起始或者结束的信号(400KB模式下高电平最小值是0.6us)。
当然这只是软件工具对电平信号的识别,与实体24C02对电平信号的判读,笔者觉得应是有区别的。怎么理解呢每个I2C通信器件都有自己的时序要求,因此某个跳变信号被器件A认为是起始信号,但对于器件B它可能没达到起始信号的时序要求(器件内部某项功能的开启,都是使能内部功能电路工作,再快速它都是需要时间的,如果时间没达到文档给出的时序要求,那该功能可能开启可也可能没开启),就不被认为是起始信号。但它确实发生了一个跳变电平信号,那么对于器件B它读到的会是什么信号呢?依然是一个跳变信号,只是这个跳变信号无法使能(起始信号),这个信号在软件工具里被捕捉为起始信号标识),因此这个信号如果功能使能了,就会变成功能信号,如果这个信号没被使能并且没干扰其他信号,那该信号就是一个无用的“废”信号,它“来”了又好像没“来”过。
E2PROM的学习
在实际的应用中,保存在单片机RAM中的数据掉电后就丢失了,保存在单片机的FLASH中的数据又不能随意改变,也就是不能用它来记录变化的数值。但是在某些场合又确实需要记录下某些数据,而且它们还时常需要改变或跟新,掉电之后数据还不能丢失,比如家用电表度数,电视机里边的频道记忆,一般都是使用E2PROM来保存数据,特点是掉电后不丢失。开发板上使用的这个器件是24C02,一个容量大小是2KB,也就是256个字节的E2PROM。一般情况下,E2PROM拥有30~100万次的寿命。
24C02是一个基于I2C通信协议的E2PROM器件。
E2PROM单字节读写操作时序
上篇博文对E2PROM器件进行寻址并且检测了ACK,本篇将读取E2PROM的0x02这个地址上的一个数据,不管这个数据之前是多少都将读出来的数据加1,再写到E2PROM的0x02这个地址上。并用LCD1602显示出来。
看程序:
main.c
# include<reg52.h>
extern void InitLcd1602();
extern void LcdShowStr(unsigned char x,unsigned char y, unsigned char *str);
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;
unsigned char str[10];
InitLcd1602(); //初始化液晶
dat = E2ReadByte(0x02);//读取指定地址上的一个字节
str[0] = (dat/100) + '0'; //转换为十进制字符串格式
str[1] = (dat/10%10) + '0';
str[2] = (dat %10) + '0';
str[3] = '\0';
LcdShowStr(0,0,str); //显示在液晶上
dat++; //dat值加1
E2WriteByte(0x02,dat); //再将其写回到24C02对应的地址上
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(); //读取1个字节数据
I2CStop();
return dat;
}
/*向EEPROM中写入一个字节,addr为字节地址 */
void E2WriteByte(unsigned char addr,unsigned char dat)
{
I2CStart();
I2CWrite(0x50<<1); //寻址器件,后续为写操作
I2CWrite(addr); //写入存储地址
I2CWrite(dat); //写入一个字节数据
I2CStop();
}
I2C.C
# include<reg52.h>
# include<intrins.h>
# define I2CDelay() {_nop_();_nop_();_nop_();_nop_();}
// # define I2CDelay() {_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)
I2C_SDA = 0;
else
I2C_SDA = 1; //通过上述语句把dat的8位电平信息从最高位开始依次发出
I2CDelay();
I2C_SCL = 1;
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;
for(mask = 0x80; mask != 0; mask >>= 1)
{
I2CDelay();
I2C_SCL = 1;
if(I2C_SDA == 0)
dat &= ~mask;
else
dat |= mask;
I2CDelay();
I2C_SCL = 0;//再拉低SCL,以使从机发送出下一位
}
I2C_SDA = 0; //8位数据发送完后,拉低SDA。发送应答信号
I2CDelay();
I2C_SCL = 1; //拉高SCL
I2CDelay();
I2C_SCL = 0; //再拉低SCL完成应答,并保持住总线
return dat;
}
1602LCD.C
#include<reg52.h>
#define LCD1602_DB P0
sbit LCD1602_RS = P1^0;
sbit LCD1602_RW = P1^1;
sbit LCD1602_E = P1^5;
/*等待液晶准备好,“忙”判断 */
void LcdWaitReady()
{
unsigned char sta;
LCD1602_DB = 0xFF;
LCD1602_RS = 0;
LCD1602_RW = 1;
do{
LCD1602_E = 1;
sta = LCD1602_DB; //read the status of bit 7 postion
LCD1602_E = 0;
} while(sta & 0x80);// bit 7 equal 1,indicating that LCD is busy.Repeat the detection until it equal 0.
}
/*向LCD1602液晶写入一字节命令,cmd为待写入命令值 */
void LcdWriteCmd(unsigned char cmd)
{
LcdWaitReady();
LCD1602_RS = 0;
LCD1602_RW = 0;
LCD1602_DB = cmd;
//High Pulse operation ,Default state is low level
LCD1602_E = 1;
LCD1602_E = 0;
}
/*向LCD1602液晶写入一字节数据,dat为待写入数据值 */
void LcdWriteDat(unsigned char dat)
{
LcdWaitReady();
LCD1602_RS = 1;
LCD1602_RW = 0;
LCD1602_DB = dat;
//High Pulse operation ,Default state is low level
LCD1602_E = 1;
LCD1602_E = 0;
}
/*设置显示RAM的起始地址,亦即光标位置,(x,y) 为对于屏幕上的字符坐标 */
void LcdSetCursor(unsigned char x, unsigned char y)
{
unsigned char addr;
if(y == 0)
addr = 0x00 + x; //The first line adress starts from 0x00;
else
addr = 0x40 + x; //The second line adress starts from 0x40;
LcdWriteCmd(addr|0x80);//this operation is actually adding 0x80 to the addr.
}
/*在液晶上显示字符串,(x,y)为对应屏幕上的起始坐标,str为字符指针,len为需要显示的字符长度 */
void LcdShowStr(unsigned char x,unsigned char y, unsigned char *str)
{
LcdSetCursor(x,y); //Set the starting position of the cursor
while(*str != '\0')
{
LcdWriteDat(*str++);// Continuously write len character data
}
}
/*初始化1602液晶 */
void InitLcd1602()
{
LcdWriteCmd(0x38);//0x38 = 0011 1000 16*2显示,5*7点阵,8位数据接口
LcdWriteCmd(0x08);//显示关闭
LcdWriteCmd(0x01);//清屏
LcdWriteCmd(0x06);//0x04 = 0000 0100 文字不动,地址自动加1
LcdWriteCmd(0x0C);//显示器开 ,光标关闭
}
看下程序结果:
开关一次电源开关,地址0x02储存的数值显示在液晶上然后把数值加1储存到0x02地址上。即该次显示的数值是上次开关动作的时候储存在0x02地址里面的信息。液晶分别显示了177、178、179程序工作正常。这个程序在编译的会有个警告,是因为没有调用I2CReadACK()这个函数。
看一下24C02单字节的读写操作时序图:
上图是24C02写字节时序图,因此这个E2PROM写数据的流程是:
- 首先是I2C的起始信号,接着跟上首字节,也就是I2C的器件地址(本案是24C02的器件地址)并且读写方向上选择“写”操作。
-
发送数据的存储地址。24C02一共256个字节的存储空间。地址从0x00~0xFF,想把数据存储在哪个位置,此刻写的就是哪个地址。
-
发送要存储的数据第一个字节 、第二个字节......注意在写数据的过程中,E2PROM每个字节都会回应一个“应答位0”,来告诉我们写E2PROM数据成功,如果没有应答位,说明写入不成功。
-
在写数据的过程中,每成功写入一个字节,E2PROM存储空间的地址就会自动加1,当加到0xFF后,在写入一个字节,地址就会溢出又变成0x00。
E2PROM读数据流程
- 首先I2C的起始信号,接着跟上首字节,也就是I2c的器件地址,并且在读写功能选择“写”操作。这地方可能会有异议,明明是读数据为何方向也要选“写”呢?24C02一个256个地址,选择写操作,是为了把所要读的,数据的储存地址先写进去,告诉E2PROM要读取哪个地址的数据。
- 发送要读取的数据的地址,注意是地址而非存在E2PROM中的数据,通知E2PROM要哪个分机的信息。
- 重新发送I2C起始信号和器件地址,并且在方向上选择“读”操作,在这三步当中,每一个字节实际上都是在“写”,所以每一个字节E2PROM都会回应一个“应答位0”。即ACK信号来自从机
- 读取从器件发回的数据,读一个字节,如果还想继续读下一个字节,就发送一个“应答位ACK(0)”,如果不想读了,告诉E2PROM不想要数据了,别再发数据了,那就发送1个“非应答位NAK(1)”.
- 和写操作规则一样,每读取一个字节,地址会自动加1,如果想继续往下读,给E2PROM一个ACK(0),那再继续给SCL完整的时序,E2PROM会继续往外送数据。如果不想读了,要告诉E2PROM不要数据了,直接给1个MAK(1)高电平即可。
- 梳理几个要点:1):在本例中主机是单片机,24C02是从机。2):无论读写,SCL始终都是由主机控制的。3):写的时候应答信号是由从机发出,表示从机是否正确接收了数据 .4):读的时候应答信号由主机给出,表示是否继续读下去(除了读地址的时候)
看一下主程序程序是如何编写的:
InitLcd1602(); //初始化液晶
dat = E2ReadByte(0x02);//读取指定地址上的一个字节
初始化液晶后,E2ReadByte(0x02)函数返回值赋值给变量dat,看一下“读”这个函数怎么 编写的。
/* 读取EEPROM中的一个字节,addr为字节地址 */
unsigned char E2ReadByte(unsigned char addr)
{
unsigned char dat;
I2CStart();
I2CWrite(0x50<<1); //寻址器件,后续为写操作
I2CWrite(addr); //写入储存地址
I2CStart(); //发送重复启动信号
I2CWrite(0x50<<1 | 0x01); //寻址器件,后续为读操作
dat = I2CReadNAK(); //读取1个字节数据
I2CStop();
return dat;
}
- 编写起始信号
- 写入首字节(器件地址与方向即;1010 0000 = 0x50<<1)读写方向为“写”ACK为从机
- 写入内存地址(ACK为从机)
- 重复起始信号
- 写入器件地址,方向为“读”。(ACK为从机),即读地址是写模式使能。
- 读取确认的地址的字节数据,赋值给变量dat,并发送NAK给从机。
- 发送停止信号
- 变量dat的内容作为函数返回值
str[0] = (dat/100) + '0'; //转换为十进制字符串格式
str[1] = (dat/10%10) + '0';
str[2] = (dat %10) + '0';
str[3] = '\0';
LcdShowStr(0,0,str); //显示在液晶上
dat的值是8位的,因此最大的值就是255.即3位就可以表达完全部可能的数据。通过函数变换,把百位,十位,个位上的值转换为字节格式最终显示在液晶1602上。
dat++; //dat值加1
E2WriteByte(0x02,dat); //再将其写回到24C02对应的地址上
while(1);
- dat的值加1
/*向EEPROM中写入一个字节,addr为字节地址 */
void E2WriteByte(unsigned char addr,unsigned char dat)
{
I2CStart();
I2CWrite(0x50<<1); //寻址器件,后续为写操作
I2CWrite(addr); //写入存储地址
I2CWrite(dat); //写入一个字节数据
I2CStop();
}
- 开始信号
- 写入首字节(器件地址,读写方向是“写”(0),ACK来自从机)
- 写入字节存储地址(0x02)
- 写入数据(dat)
- 写入结束信号
本函数至此结束,上述流程是符合时序图给出的流程。前文提到在写数据的时候,ACK不是即刻响应的,这里也没有作判断。主要是这边只写入1个字节,后续就不再写入了。而且开发板使用的是机械开关,而写入的最大时间是5ms,因此无论怎么操作开关,24C02都有足够的时间把数据搬到“非易失区”。
E2PROM多字节读写操作时序
读取E2PROM的时候很简单,E2PROM根据所送的时序,直接就把数据送出来了,但是写E2PROM却没有这么简单。给E2PROM发送数据后,先保存在E2PROM的缓存中,E2PROM必须要把缓存中的数据搬移到“非易失”的区域,才能达到掉电不丢失的效果。而往非易失区写需要一定的时间,每种器件不完全一样,ATMEL公司的24C02的这个写入时间最高不超过5ms。在往非易失区域写的过程,E2PROM都不会应答,就如同这个总线上没有这个器件一样。数据写入非易失区域完毕后,E2PROM再次恢复正常,可以正常读写。
多字节写入并在LCD1602液晶上显示,看程序
main.c
#include <reg52.h>
extern void InitLcd1602();
extern void LcdShowStr(unsigned char x,unsigned char y, unsigned char *str);
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 MemToStr(unsigned char* str,unsigned char* src,unsigned char len);
void main()
{
unsigned char i;
unsigned char buf[5];
unsigned char str[20];
InitLcd1602(); //初始化液晶
E2Read(buf,0x90,sizeof(buf)); //从E2中读取一段数据
MemToStr(str,buf,sizeof(buf)); //转换为16进制字符串
LcdShowStr(0,0,str); //显示到液晶上
for(i = 0; i < sizeof(buf);i++) //数据依次+1,+2,+3
{
buf[i] = buf[i]+1+i; //即buf[0]加1,buf[1]加2是我们要求,该式子即是满足这个要求
}
E2Write(buf,0x90,sizeof(buf));//再写回到E2中
while(1);
}
/* 将一段内存数据转换为十六进制格式的字符串,str为字符串指针,src为源数据buf数组地址,len为buf数组数据长度 */
void MemToStr(unsigned char *str,unsigned char *src,unsigned char len)
{
unsigned char tmp;
while(len--)
{
tmp = *src >> 4; //先取高4位
if(tmp <= 9 ) //转换为0-9或A-F
*str++ = tmp + '0';
else
*str++ = tmp - 10 +'A';
tmp = *src & 0x0F; //再取低4位
if(tmp <= 9) //转换为0-9或A-F
*str++ = tmp+'0';
else
*str++ = tmp - 10 + 'A';
*str++ = ' '; //转换完1个字节添加一个空格
src++;
}
}
/*E2读取函数,buf为数据接收指针,addr位E2中的起始地址,len位读取长度 */
void E2Read(unsigned char *buf,unsigned char addr, unsigned char len)
{
do{
I2CStart(); //用寻址操作查询当前是否可进行读写操作
if(I2CWrite(0x50 << 1)) //应答则跳出循环,非应答则进行下一次查询 0x50<<1 =1010 0000
{
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))//应答则跳出循环,非应答则进行下一次查询 0x50<<1 = 1010 0000
{
break;
}
I2CStop();
}while(1);
I2CWrite(addr++); //写入起始地址
I2CWrite(*buf++); //写入一个字节数据
I2CStop(); //结束写操作,以等待写入完成
}
}
I2C.c
LCD1602.c
这两个文件见前文 给出的文档。
分析下主函数:
InitLcd1602(); //初始化液晶
E2Read(buf,0x90,sizeof(buf)); //从E2中读取一段数据
/*E2读取函数,buf为数据接收指针,addr位E2中的起始地址,len位读取长度 */
void E2Read(unsigned char *buf,unsigned char addr, unsigned char len)
{
do{
I2CStart(); //用寻址操作查询当前是否可进行读写操作
if(I2CWrite(0x50 << 1)) //应答则跳出循环,非应答则进行下一次查询 0x50<<1 =1010 0000
{
break;
}
I2CStop();
}while(1);
I2CWrite(addr); //写入起始地址
I2CStart(); //发送重复启动信号
I2CWrite((0x50 << 1)|0x01); //寻址器件,后续为读操作
while(len > 1) //连续读取(len-1)个字节
{
*buf++ = I2CReadACK(); //最后字节之前为读取操作+应答
len--;
}
*buf = I2CReadNAK(); //最后一个字节为读取操作+非应答
I2CStop();
}
- 液晶初始化
- 起始信号
- 寻址器件地址(0x50)方向为“写”(0),对函数I2CWrite(0x50 << 1)返回值进行判断,如果是1执行break;跳出循环。因为对返回值进行了取反,即ACK是“0”的时候跳出循环。类似1602液晶的“忙”判断。
- 写入内存地址
- 重复起始信号
- 写入器件地址,读写方向选择“读”
- 把24C02器件从0x90地址开始往后共4个存储空间包括它自己的数据按顺序赋值给buf[]数组前4个数组元素,(ACK来自主机)
- 0x90地址开始往后第5个存储空间(0x94)的数据赋值给buf[4],即buf[]数组的最后一个元素。并发送NAK
- 发送结束信号
接着是:
MemToStr(str,buf,sizeof(buf)); //转换为16进制字符串
/* 将一段内存数据转换为十六进制格式的字符串,str为字符串指针,src为源数据buf数组地址,len为buf数组数据长度 */
void MemToStr(unsigned char *str,unsigned char *src,unsigned char len)
{
unsigned char tmp;
while(len--)
{
tmp = *src >> 4; //先取高4位
if(tmp <= 9 ) //转换为0-9或A-F
*str++ = tmp + '0';
else
*str++ = tmp - 10 +'A';
tmp = *src & 0x0F; //再取低4位
if(tmp <= 9) //转换为0-9或A-F
*str++ = tmp+'0';
else
*str++ = tmp - 10 + 'A';
*str++ = ' '; //转换完1个字节添加一个空格
src++;
}
}
以buf[0]=0x2D为例当执行完第1遍这个函数的时候,STR数组里的数组元素是
5次循环结束后跳出while函数,buf[]数组的5个元素分别转化为字节格式存储在str数组里。共计15个元素。
接着:
LcdShowStr(0,0,str); //显示到液晶上
for(i = 0; i < sizeof(buf);i++) //数据依次+1,+2,+3
{
buf[i] = buf[i]+1+i; //即buf[0]加1,buf[1]加2是我们要求,该式子即是满足这个要求
}
E2Write(buf,0x90,sizeof(buf));//再写回到E2中
while(1);
buf[]相对应的元素加上相应的值最后存到以0x90开头往后共5个地址中。
/* E2写入函数,buf为数据指针,addr为E2中的起始地址,len为写入长度*/
void E2Write(unsigned char *buf,unsigned char addr,unsigned char len)
{
while(len--)
{
do{
I2CStart(); //寻址操作查询当前是否可进行读写操作
if(I2CWrite(0x50 << 1))//应答则跳出循环,非应答则进行下一次查询 0x50<<1 = 1010 0000
{
break;
}
I2CStop();
}while(1);
I2CWrite(addr++); //写入起始地址
I2CWrite(*buf++); //写入一个字节数据
I2CStop(); //结束写操作,以等待写入完成
}
}
1:写入开始信号,对器件寻址,检测其ACK。检测到ACK后跳出循环
2 :写入器件地址
3:写入buf数据
4:写入结束信号,进行下一个循环
看下程序结果:
总结1下:
1):函数E2Read,在读之前,要查询一下当前是否可以进行读写操作,E2PROM正常相应才可以进行。进行后,读最后一个字节前,全部给出ACK,而读完最后1个字节,要给出NAK。(其实对本程序来说是不需要进行类似“忙”判断的写法,直接写地址就可以了,但是如果作为模块化,比如先进行“写”数据的操作,马上接“读”的操作,或者工作比较复杂的时候,那么就需要进行ACK判断了)
2):函数E2Write:每次操作之前都要进行查询判断当前E2PROM是否响应,正常响应后才可以写数据。
E2PROM的页写入
在向E2PROM连续写入多个字节的数据时,如果每写一个字节都要等待几ms的话,整体上的写入效率就太低了。因此E2PROM的厂商就想了一个办法,把E2PROM分页管理。24C02、24C01这两个信号是8个字节一页。
24C02一共是256个字节,8个字节1页,那么一共就32页。
分配好页之后,如果在同一个页连续写入几个字节后,最后再发送停止位的时序。E2PROM检测到这个停止位后,就会一次性把这一页的数据写入非易失区域,就不需要像上节那样写一个字节检测一次,并且页写入的时间也不会超过5ms。
如果写入的数据跨页了,那么写完一页之后,要发送一个停止位,然后等待并检测E2PROM的空闲模式,一直等到把上一页数据完全写到非易失区域后,再进行下一页的写入,这样就可以很大程度提高数据的写入效率。
如图24C02页写入
在看一下单字节写入
- 可以看到似乎没什么太大的区别,页写入的时候字节是连续写入的,但ACK好像是即刻反馈似乎和前文说的不一样(笔者之前以为ACK是数据搬运到非易失区后再发送的ACK,事实上是存储在缓冲区后就发送了),这说明前文的描述可能有问题!前文说道写入过程是缓冲区的数据搬运到“非易失区“的过程,那么ACK是什么时候发出的?搬运到非易失区是什么时候发生的?通过逻辑分析仪抓到的时序图,联系前后文可知这么一个情况:
- 在连续写入数据的时候依然会即刻检测到从机发出的ACK,但这个ACK只代表数据转移到了缓冲区(这个缓冲区我觉得可以理解为存到了RAM中)。再写入一个数据,从机依然会即刻发送一个ACK,这个时候缓冲区里存了两个数据,这两个数据都没有转移到非易失区。那什么时后转移到非易失区?当主机发出STOP时序信号的时候,这个时候缓冲区中的数据会开始搬到非易失区?这个时候如果你马上去检测器件地址,是检测不到ACK的因为此时24C02它还处在“搬运”状态。因此在完成一次页写入进行第二次页写入的时候就需要重新对器件地址寻址,以判断E2PROM器件是否响应(即数据搬运结束或者处于空闲状态)
- 由上述描述可知其实单字节写入就是页写入的一个变种而已,页写入但只写一个字节就发送stop信号,不就是单字节写入了!那就出现一个问题,缓冲区一次可以存储多少个字节?24C02应该是8个即1页,如果写9个进去会发生什么?暂时不知,后续如果有精力笔者可以写个程序测试一下。
贴一下页写入的程序:
main.c
# include<reg52.h>
extern void InitLcd1602();
extern void LcdShowStr(unsigned char x,unsigned char y, unsigned char *str);
extern void E2Read(unsigned char* buf,unsigned char addr,unsigned char len );
extern void E2Write(unsigned char* buf,unsigned char addr, unsigned char len);
extern void MemToStr(unsigned char* str,unsigned char* src,unsigned char len);
void main()
{
unsigned char i;
unsigned char buf[5];
unsigned char str[20];
InitLcd1602(); //初始化液晶
E2Read(buf,0x8E,sizeof(buf)); //从E2中读取一段数据
MemToStr(str,buf,sizeof(buf)); //转换为字符串格式
LcdShowStr(0,0,str); //显示到液晶上
for(i=0;i<sizeof(buf);i++) //数据依次+1,+2,+3
{
buf[i] = buf[i]+1+i;
}
E2Write(buf,0x8E,sizeof(buf)); //再写回到E2中
while(1);
}
/*将一段内存数据转换为十六进制格式的字符串 ,str为字符串指针,src为源数据地址,len为数据长度 */
void MemToStr(unsigned char *str,unsigned char *src,unsigned char len)
{
unsigned char tmp;
while(len--)
{
tmp = *src >> 4; //先取高4位
if(tmp <= 9 ) //转换为0-9或A-F
*str++ = tmp + '0';
else
*str++ = tmp - 10 +'A';
tmp = *src & 0x0F; //再取低4位
if(tmp <= 9) //转换为0-9或A-F
*str++ = tmp+'0';
else
*str++ = tmp - 10 + 'A';
*str++ = ' '; //转换完1个字节添加一个空格
src++;
}
}
E2PROM.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位是否为0即可
break; //到达页边界时,跳出循环,结束本次写操作
}
}
I2CStop();
}
}
LCD1602.c和I2C.c见前文给出的。
看下结果:程序功能是和连续写入一样的只是写入方式现在是页写入,看下结果
采用页写入的原因是因为页写入消耗的时间更短,通过逻辑分析仪抓到的时序图:
单字节连续写入时序图写入过程耗时8.4ms
页写入时序图写入过程耗时3.5ms,可以看到整整少了5ms。
再分析一下页写入程序是怎么工作的
/* 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位是否为0即可
break; //到达页边界时,跳出循环,结束本次写操作
}
}
I2CStop();
}
}
1:24c02器件寻址,判断是否响应
2:I2CWrite(addr); 写入内存地址由前后文可知是0x8E,那么要重新写入的5个地址分别是
0x8E,0x8F,0x90,0x91,0x92这5个地址,由于是分页写入那么(0x8E,0x8F,0x90)这三个地址是一页,(0x91,0x92)这两个地址是一页。
3:while(len>0)函数,如果写入地址到达边界跳出循环,主机发出Stop信号。
由程序可以这个循环中,它只写入了0x8E与0x8F,当前页的最后一个地址它没写就跳出循环了,接着进行“搬运”工作了?是不是程序写错了?继续读程序
4:又回到上一个while(len>0)循环,开始24C02的器件寻址,这时的ACK响应,这次的ACK响应应该会发生延时
5ACK响应后,I2CWrite(addr);这时的addr是0x90,然后进入while循环,0x91,0x92地址都写入数据
6:主机发送Stop,开始向三个内存空间搬运数据,即搬运到非易失区。
由上可知的时序是如图:
看下逻辑分析仪的时序图:
可以看到确实是和笔者之前的示意图一样的流程。
由时序图可知进行了跨页写入即在一次写入中同时对两页进行写入。这是被允许的。就看你程序怎么写了。本质上就是24C02一次最多只能写入8个数据,写满后就要进行一次缓冲区到非易失区的搬运工作。
总结一下:
- 对E2PROM器件24C02的读写模式进行了基础探索
- 从使用结果来说ACK基本上都是下个时序即刻响应的,只在缓冲区的数据搬运到非易失区的时候,需要从新对器件进行ACK检测,来判断器件是否空闲(或者叫有效)
- 把缓充区的数据搬运到非易失区的信号是Stop信号。
- 这个程序本质上对ACK都是直接读写的不是把ACK作为一个判断信号进行后续的语句,除了第二条搬运的情况。这可能存在风险,即如果器件受干扰或者什么原因,ACK没有在字节数据的下个时序发出。但程序还是依然向器件写入数据而不是等待,这就可能造成错误。当然一般情况下符合时序要求就没问题,因此无论什么时序下拉长ACK响应的SCL电平时间(SCL为低电平的时间)可能是个不错的选择。