初学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.rhkb.cn/news/439245.html

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

相关文章

【C++】入门基础介绍(上)C++的发展历史与命名空间

文章目录 1. 前言2. C发展历史2. 1 C版本更新特性一览2. 2 关于C23的一个小故事: 3. C的重要性3. 1 编程语言排行榜3. 2 C在工作领域中的应用 4. C学习建议和书籍推荐4. 1 C学习难度4. 2 学习书籍推荐 5. C的第一个程序6. 命名空间6. 1 namespace的价值6. 2 namespace的定义6. …

首届中美可持续发展峰会在加州圆满举行,引领国际绿色发展新方向

现场嘉宾与(部分)与会人员大合影 2024年8月18日,由美国领创商业联盟(Youth Entrepreneur Business Alliance, YEBA)主办的首届中美可持续发展峰会(Sino-American Symposium on Sustainable Development)在加州森林湖市(Lake Forest)盛大举行。此次峰会吸引了数百名来自中美两国…

HTML+CSS之表格(15个案例+代码+效果图+素材)

目录 1.table标签的border属性 案例:制作一个带边框的表格 1.代码 2.效果 2.table标签的cellspacing属性 案例:制作一个带边距的表格 1.代码 2.效果 3.table标签的cellpadding属性 1.代码 2.效果 4.table标签的width和height属性 案例:指定宽高的表格 1.代码 2.效果 5.table标签…

全新芒果YOLOv10改进135:最新注意力机制EMA:即插即用,具有跨空间学习的高效多尺度注意力模块,ICCASSP 2023

💡本篇内容:芒果YOLOv10改进135:最新注意力机制EMA:即插即用,具有跨空间学习的高效多尺度注意力模块,ICCASSP 2023 **具有跨空间学习的高效多尺度注意力模块EMA | 即插即用 该模块通常包括多个并行的注意力子模块,每个子模块关注于输入数据的不同尺度或分辨率。这些子模块…

HTML+CSS表单控件(11个案例+代码+效果图)

目录 单行文本框 (text) 案例:制作一个单行文本框 1.代码 2.效果 密码输入框 (password) 案例:制作密码输入框 1.代码 2.效果 单选按钮 (radio) 案例:制作单选按钮 1.代码 2.效果 复选框 (checkbox) 案例:制作一个复选框 1.代码 2.效果 普通按钮 (button) 案例:制作一个普通按钮…

Java毕业设计实战项目之基于SSM框架的民宿预定系统

项目技术架构&#xff1a; 该SSMVue的民宿预定系统&#xff0c;后端采用SSM架构&#xff0c;前端采用VueElementUI实现页面的快速开发&#xff0c;并使用关系型数据库MySQL存储系统运行数据。本系统分为三种角色&#xff0c;分别是系统管理员&#xff0c;用户&#xff0c;房主…

RD-Agent Windows安装教程

RD-Agent Windows安装教程 QuantML QuantML 2024年09月23日 18:30 Content RD-Agent 是微软亚洲研究院推出的一款自动化研究与开发工具&#xff0c;能够通过LLMs自动构建因子和策略&#xff0c;相关介绍见我们之前的文章&#xff1a;RD-Agent &#xff1a;自动化Quant工厂 然…

10.5二分专练,二分边界情况,+1不加1的判断,方向判断,各种DEBUG

5 https://leetcode.cn/problems/minimum-speed-to-arrive-on-time/submissions/570242512/ 就是说总时间是 前n-1量汽车的运行时间&#xff0c;向上取整&#xff0c;然后再加上最后一辆列车的运行时间 最快的话是需要n-1个小时 搜索空间就是时速&#xff0c;左边界是1&#x…

windows中下载、安装、配置JDK/JDK环境配置/Java配置环境变量/Linux中安装配置JDK环境

JDK下载(官网)、安装、配置(包括系统、idea、eclipse)一篇就够了 1、问题概述? Java开发者必须掌握的JDK下载、安装、配置过程。 包括在Eclipse及IDEA中的配置使用 2、下载JDK 【注册Oracle官网账号】 下载的前天是注册orcle官网账号,作为开发者,这个必须有,随时关注…

VBA信息获取与处理第三个专题第三节:工作薄在空闲后自动关闭

《VBA信息获取与处理》教程(版权10178984)是我推出第六套教程&#xff0c;目前已经是第一版修订了。这套教程定位于最高级&#xff0c;是学完初级&#xff0c;中级后的教程。这部教程给大家讲解的内容有&#xff1a;跨应用程序信息获得、随机信息的利用、电子邮件的发送、VBA互…

Web安全 - 路径穿越(Path Traversal)

文章目录 OWASP 2023 TOP 10导图定义路径穿越的原理常见攻击目标防御措施输入验证和清理避免直接拼接用户输入最小化权限日志监控 ExampleCode漏洞代码&#xff1a;路径穿越攻击案例漏洞说明修复后的安全代码代码分析 其他不同文件系统下的路径穿越特性Windows系统类Unix系统&a…

记录|Modbus-TCP产品使用记录【摩通传动】

目录 前言一、摩通传动实验图1.1 配置软件 IO_Studio1.2 测试软件Modbus Poll1.2.1 读写设置测试1.2.2 AI信号的读取 1.3 对应的C#连接Modbus的测试代码如下【自制&#xff0c;仅供参考】1.4 最终实验图 更新时间 前言 参考文章&#xff1a; 自己需要了解和对比某些产品的Modbu…

【MySQL】服务器管理与配置

MySQL服务器 服务器默认配置 查看服务器默认选项和系统变量 mysqld --verbose --help 查看运行时的系统变量&#xff0c;可以通过like去指定自己要查询的内容 状态变量的查看 系统变量和状态变量的作用域 全局作用域&#xff1a; 对于每个会话都会生效当前会话&#xff1a;只…

初识算法 · 滑动窗口(1)

目录 前言&#xff1a; 长度最小的子数组 题目解析 算法原理 算法编写 无重复长度的最小字符串 题目解析 算法原理 算法编写 前言&#xff1a; 本文开始&#xff0c;介绍的是滑动窗口算法类型的题目&#xff0c;滑动窗口本质上其实也是双指针&#xff0c;但是呢&#…

异常处理【C++提升】(基本思想,重要概念,异常处理的函数机制、异常机制,栈解旋......你想要的全都有)

更多精彩内容..... &#x1f389;❤️播主の主页✨&#x1f618; Stark、-CSDN博客 本文所在专栏&#xff1a; C系列语法知识_Stark、的博客-CSDN博客 座右铭&#xff1a;梦想是一盏明灯&#xff0c;照亮我们前行的路&#xff0c;无论风雨多大&#xff0c;我们都要坚持不懈。 异…

828华为云征文|华为云Flexus云服务器X实例搭建部署H5美妆护肤分销商城、前端uniapp

准备国庆之际&#xff0c;客户要搭个 H5 商城系统&#xff0c;这系统好不容易开发好啦&#xff0c;就差选个合适的服务器上线。那可真是挑花了眼&#xff0c;不知道哪款性价比高呀&#xff01;就像在琳琅满目的选择前。最终慧眼识珠&#xff0c;选择了华为云 Flexus X。至于为什…

redis高级篇 抢红包案例的设计以及分布式锁

一 抢红包案例 1.1 抢红包 二倍均值算法&#xff1a; M为剩余金额&#xff1b;N为剩余人数&#xff0c;公式如下&#xff1a; 每次抢到金额随机区间&#xff08;0&#xff0c;&#xff08;M/N&#xff09;*2&#xff09; 这个公式&#xff0c;保证了每次获取的金额平均值…

TX-LCN框架 分布式事务

一、三种事务模式 1&#xff09;LCN 基于XA协议&#xff0c;事务提交或回滚的操作由事务管理服务器统一告诉它管理的多个项目&#xff0c;也就是说在A事务&#xff0c;B事务的事务提交操作或回滚操作都是在同一时刻发生&#xff0c;并且要么都提交&#xff0c;要么都回滚。 LCN…

低代码可视化-UniApp二维码可视化-代码生成器

市面上提供了各种各样的二维码组件&#xff0c;做了一简单的uniapp二维码组件&#xff0c;二维码实现依赖davidshimjs/qrcodejs。 组件特点 跨浏览器支持&#xff1a;利用Canvas元素实现二维码的跨浏览器兼容性&#xff0c;兼容微信小程序、h5、app。 无依赖性&#xff1a;QR…

数据库(MySQL):使用命令从零开始在Navicat创建一个数据库及其数据表(一).创建基础表

一. 使用工具和命令 1.1 使用的工具 Navicat Premium 17 &#xff1a;“Navicat”是一套可创建多个连接的数据库管理工具。 MySQL版本8.0.39 。 1.2 使用的命令 Navicat中使用的命令 命令命令解释SHOW DATABASES&#xff1b;展示所有的数据库CREATE DATABASE 数据库名称; 创…