初学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值加1E2WriteByte(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;   //先拉低SDAI2CDelay();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;elseI2C_SDA = 1;      //通过上述语句把dat的8位电平信息从最高位开始依次发出I2CDelay();I2C_SCL = 1;I2CDelay();I2C_SCL = 0;  //再拉低SCL,完成一个位周期}I2C_SDA = 1; //8位数据发送完后,主机释放SDA,以检测从机应答I2CDelay();I2C_SCL = 1; //拉高SCLack = 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;   //首先确保主机释放SDAfor(mask = 0x80; mask != 0; mask >>= 1) //从高位到低位依次进行{I2CDelay();I2C_SCL = 1;     //拉高SCLif(I2C_SDA == 0) //读取SDA的值dat &= ~mask; //为0时,dat中对应位清零elsedat |= mask; //为1时,dat中对应位置1I2CDelay();I2C_SCL = 0;	//再拉低SCL,以使从机发送下一位}I2C_SDA = 1;  //8位数据发送完后,拉高SDA,发送非应答信号I2CDelay();I2C_SCL = 1;  //拉高SCLI2CDelay();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;elsedat |= mask;I2CDelay();I2C_SCL = 0;//再拉低SCL,以使从机发送出下一位}I2C_SDA = 0; //8位数据发送完后,拉低SDA。发送应答信号I2CDelay();I2C_SCL = 1; //拉高SCLI2CDelay();I2C_SCL = 0; //再拉低SCL完成应答,并保持住总线return dat;}

1602LCD.C

#include<reg52.h>#define LCD1602_DB P0sbit 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 postionLCD1602_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 levelLCD1602_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 levelLCD1602_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;elseaddr = 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 cursorwhile(*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 文字不动,地址自动加1LcdWriteCmd(0x0C);//显示器开 ,光标关闭}

看下程序结果:

     开关一次电源开关,地址0x02储存的数值显示在液晶上然后把数值加1储存到0x02地址上。即该次显示的数值是上次开关动作的时候储存在0x02地址里面的信息。液晶分别显示了177、178、179程序工作正常。这个程序在编译的会有个警告,是因为没有调用I2CReadACK()这个函数。

看一下24C02单字节的读写操作时序图:

     上图是24C02写字节时序图,因此这个E2PROM写数据的流程是:

  1. 首先是I2C的起始信号,接着跟上首字节,也就是I2C的器件地址(本案是24C02的器件地址)并且读写方向上选择“写”操作。
  2. 发送数据的存储地址。24C02一共256个字节的存储空间。地址从0x00~0xFF,想把数据存储在哪个位置,此刻写的就是哪个地址。

  3. 发送要存储的数据第一个字节 、第二个字节......注意在写数据的过程中,E2PROM每个字节都会回应一个“应答位0”,来告诉我们写E2PROM数据成功,如果没有应答位,说明写入不成功。

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

E2PROM读数据流程

  1. 首先I2C的起始信号,接着跟上首字节,也就是I2c的器件地址,并且在读写功能选择“写”操作。这地方可能会有异议,明明是读数据为何方向也要选“写”呢?24C02一个256个地址,选择写操作,是为了把所要读的,数据的储存地址先写进去,告诉E2PROM要读取哪个地址的数据。
  2. 发送要读取的数据的地址,注意是地址而非存在E2PROM中的数据,通知E2PROM要哪个分机的信息。
  3. 重新发送I2C起始信号和器件地址,并且在方向上选择“读”操作,在这三步当中,每一个字节实际上都是在“写”,所以每一个字节E2PROM都会回应一个“应答位0”。即ACK信号来自从机
  4. 读取从器件发回的数据,读一个字节,如果还想继续读下一个字节,就发送一个“应答位ACK(0)”,如果不想读了,告诉E2PROM不想要数据了,别再发数据了,那就发送1个“非应答位NAK(1)”.
  5. 和写操作规则一样,每读取一个字节,地址会自动加1,如果想继续往下读,给E2PROM一个ACK(0),那再继续给SCL完整的时序,E2PROM会继续往外送数据。如果不想读了,要告诉E2PROM不要数据了,直接给1个MAK(1)高电平即可。
  6. 梳理几个要点: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值加1E2WriteByte(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();		}
  1. 液晶初始化
  2. 起始信号
  3. 寻址器件地址(0x50)方向为“写”(0),对函数I2CWrite(0x50 << 1)返回值进行判断,如果是1执行break;跳出循环。因为对返回值进行了取反,即ACK是“0”的时候跳出循环。类似1602液晶的“忙”判断。
  4. 写入内存地址
  5. 重复起始信号
  6. 写入器件地址,读写方向选择“读”
  7. 把24C02器件从0x90地址开始往后共4个存储空间包括它自己的数据按顺序赋值给buf[]数组前4个数组元素,(ACK来自主机)
  8. 0x90地址开始往后第5个存储空间(0x94)的数据赋值给buf[4],即buf[]数组的最后一个元素。并发送NAK
  9. 发送结束信号

接着是:

	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为低电平的时间)可能是个不错的选择。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/54080.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

深入解析 Java 虚拟机:内存区域、类加载与垃圾回收机制

我的主页&#xff1a;2的n次方_ 1. JVM 内存区域划分 程序计数器&#xff08;空间比较小&#xff09;。保存了下一条要执行的指令的地址&#xff08;指向元数据区指令的地址&#xff09;堆。JVM 最大的空间&#xff0c;new 出来的对象都在堆上栈。函数中的局部变量&#x…

linux服务器部署filebeat

# 下载filebeat curl -L -O https://artifacts.elastic.co/downloads/beats/filebeat/filebeat-7.17.23-linux-x86_64.tar.gz # 解压 tar xzvf filebeat-7.17.23-linux-x86_64.tar.gz# 所在位置&#xff08;自定义&#xff09; /opt/filebeat-7.17.23-linux-x86_64/filebeat.ym…

一篇文章快速学会docker容器技术

目录 一、Docker简介及部署方法 1.1Docker简介 1.1.1什么是docker 1.1.2 docker在企业中的应用场景 1.1.3 docker与虚拟化的对比 1.1.4 docker的优势 二 、部署docker 2.1 容器工作方法 2.2 部署第一个容器 2.2.1 配置软件仓库 2.2.2 安装docker-ce并启动服务 2.2.…

C嘎嘎入门篇:类和对象(2)

前言&#xff1a; 上一篇小编讲了类和对象&#xff08;1&#xff09;&#xff0c;当然&#xff0c;在看这篇文章之前&#xff0c;读者朋友们一定要掌握好前面的基础内容&#xff0c;因为这篇和前面息息相关&#xff0c;废话不多说&#xff0c;下面小编就加快步伐&#xff0c;开…

css的背景background属性

CSS的background属性是一个简写属性&#xff0c;它允许你同时设置元素的多个背景相关的子属性。使用这个属性可以简化代码&#xff0c;使其更加清晰和易于维护。background属性可以设置不同的子属性。 background子属性 定义背景颜色 使用background-color属性 格式&#x…

ARM Process state -- CPSR

Holds PE status and control information. 保存PE状态和控制信息。 N, bit [31] Negative condition flag. Set to bit[31] of the result of the last flag-setting instruction. If the result is regarded as a twos complement signed integer, then N is set to 1 if…

Docker学习和部署ry项目

文章目录 停止Docker重启设置开机自启执行docker ps命令&#xff0c;如果不报错&#xff0c;说明安装启动成功2.然后查看数据卷结果3.查看数据卷详情结果4.查看/var/lib/docker/volumes/html/_data目录可以看到与nginx的html目录内容一样&#xff0c;结果如下&#xff1a;5.进入…

理解:两横三纵 简述

理解&#xff1a;两横三纵 简述 下图:两横三纵-yellow色线分布图 时间&#xff1a;2013年12月提出的城市化战略格局 所谓“两横三纵”&#xff0c; 即以陆桥通道、沿长江通道为两条横轴&#xff0c; 以沿海、京哈京广、包昆通道为三条纵轴&#xff0c; 以主要的城市群地区为…

C#和数据库高级:虚方法

文章目录 一、抽象方法和抽象类中的思考1.1、回顾抽象方法的特点1.2、针对抽象方法问题的引出 二、虚方法的使用步骤2.1、虚方法重写方法的调用2.2、系统自带的虚方法2.3、重写Equals方法2.4、虚方法和抽象方法的比较 三、虚方法和抽象方法的联系3.1、ToString()方法的应用 一、…

SpringBoot技术栈:打造下一代网上租赁系统

第2章 关键技术简介 2.1 Java技术 Java是一种非常常用的编程语言&#xff0c;在全球编程语言排行版上总是前三。在方兴未艾的计算机技术发展历程中&#xff0c;Java的身影无处不在&#xff0c;并且拥有旺盛的生命力。Java的跨平台能力十分强大&#xff0c;只需一次编译&#xf…

Rust语言桌面应用开发GTK3 Gtk3-rs Glade

文章目录 GTK-RSGithub官网Rust 教程Rust 环境安装 GTK安装 Gladedemo.glade 文件完整示例 main.rs创建 Rust 项目Cargo.toml 文件main.rs 文件 编译运行GTK主题 GTK-RS gtk-rs 是一个用于在 Rust 编程语言中使用 GTK 图形用户界面工具包的库。GTK 是一个流行的跨平台 GUI 工具…

SqlAlchemy使用教程(七) 异步访问数据库

SqlAlchemy使用教程(一) 原理与环境搭建SqlAlchemy使用教程(二) 入门示例及编程步骤SqlAlchemy使用教程(三) CoreAPI访问与操作数据库详解SqlAlchemy使用教程(四) MetaData 与 SQL Express Language 的使用SqlAlchemy使用教程(五) ORM API 编程入门SqlAlchemy使用教程(六) – O…

CSS文本格式化

通过 CSS 中的文本属性您可以像操作 Word 文档那样定义网页中文本的字符间距、对齐方式、缩进等等&#xff0c;CSS 中常用的文本属性如下所示&#xff1a; text-align&#xff1a;设置文本的水平对齐方式&#xff1b;text-decoration&#xff1a;设置文本的装饰&#xff1b;te…

【拥抱AIGC】通义灵码扩展管理

通义灵码提供了扩展管理&#xff0c;支持自定义指令&#xff0c;满足企业编码场景的扩展诉求。 适用版本 企业标准版、企业专属版 通义灵码管理员、组织内全局管理员&#xff08;专属版&#xff09;在通义灵码控制台-扩展管理中&#xff0c;进行自定义指令的管理、查看自定义…

leetcode|刷算法 线段树原理以及模板

线段树出现的题目特征 线段树使用的题目。每次操作都要得到返回结果的。 比如 699. 掉落的方块 - 力扣&#xff08;LeetCode&#xff09; 2286. 以组为单位订音乐会的门票 - 力扣&#xff08;LeetCode&#xff09; 1845. 座位预约管理系统 - 力扣&#xff08;LeetCode&#…

【PHP陪玩系统源码】游戏陪玩系统app,陪玩小程序优势

陪玩系统开发运营级别陪玩成品搭建 支持二开源码交付&#xff0c;游戏开黑陪玩系统: 多客陪玩系统&#xff0c;游戏开黑陪玩&#xff0c;线下搭子&#xff0c;开黑陪玩系统 前端uniapp后端php&#xff0c;数据库MySQL 1、长时间的陪玩APP源码开发经验&#xff0c;始终坚持从客户…

Docker镜像命令和容器命令

目录 镜像命令 镜像命名规范 镜像操作命令 DockerHub拉取镜像 利用docker save将nginx镜像导出磁盘&#xff0c;然后再通过load加载回来 总结 容器命令介绍和案例 容器相关命令 案例&#xff1a;创建运行一个Nginx容器 总结 镜像命令 镜像命名规范 镜像名称一般分两…

uniapp框架中实现文件选择上传组件,可以选择图片、视频等任意文件并上传到当前绑定的服务空间

前言 uni-file-picker是uniapp中的一个文件选择器组件,用于选择本地文件并返回选择的文件路径或文件信息。该组件支持选择单个文件或多个文件,可以设置文件的类型、大小限制,并且可以进行文件预览。 提示:以下是本篇文章正文内容,下面案例可供参考 uni-file-picker组件具…

了解华为计算产品线,昇腾的业务都有哪些?

&#x1f349; CSDN 叶庭云&#xff1a;https://yetingyun.blog.csdn.net/ 随着 ChatGPT 的现象级爆红&#xff0c;它引领了 AI 大模型时代的深刻变革&#xff0c;进而造成 AI 算力资源日益紧缺。与此同时&#xff0c;中美贸易战的持续也使得 AI 算力国产化适配成为必然趋势。 …

Temporal Dynamic Quantization for Diffusion Models阅读

文章目录 AbstractIntroductionBackgrounds and Related Works2.1 扩散模型2.2 量化2.3 量化感知训练和训练后量化 TemporalDynamic Quantization3.1 量化方法3.2 扩散模型量化的挑战3.3 TDQ模块的实现3.4 工程细节时间步的频率编码TDQ模块的初始化 Experimental SetupResults5…