初学51单片机之I2C总线与E2PROM

首先先推荐B站的I2C相关的视频I2C入门第一节-I2C的基本工作原理_哔哩哔哩_bilibili

     看完视频估计就大概知道怎么操作I2C了,他的LCD1602讲的也很不错,把数据建立tsp和数据保持thd,比喻成拍照时候的摆pose和按快门两个过程,感觉还是很形象的。

        数据建立tsp和数据保持thd,这两个参数在描述上就很反直觉。“建立”是数据传输的开头代表摆pose,“保持”是数据传输的结尾代表按快门,而且LCD1602和I2C在thd上不太一样,后续笔者会描述一下原因(是笔者的个人见解)。

在描述I2C之前向分享一下,笔者在写程序的时候遇到的一些错误,其实是抄程序()。

后续会贴出函数。一个是函数声明的时候忘记了分号。如

结果报了一堆错keilkeil软件没有直接指向,漏了分号那句,核对了好久才找到问题。

第二个错误:keil软件没报错,下述函数节选有一个是错的。各位可以找找看,我也是灯下黑看花了,都找不到问题在哪,最后是找源程序,一部分一部分替代后才发现问题在哪里,最后才找到。有些人可能一眼就看出来了,我就奇怪为什么Keil没报错。

void MemToStr(unsigned char *str, unsigned char *src, unsigned char len)
{unsigned char tmp;while(len--){tmp = *src >> 4;if(tmp <= 9)*str++ = tmp + '0';else*str++ = tmp - 10 + 'A';tmp = *src & 0x0F;if(tmp <= 9)*str++ = tmp + '0';else*str++ = tmp - 10 + 'A';*str++ = ' ';src++;		}
}
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++;}}

下面的程序是由有误的,while()函数后面加了个分号,也说明大括号成对出现的话,keil软件不会报错,它的存在不需要依赖函数,单个大括号还是会报错的。

转回正题:前面的博文笔者介绍了UART异步串口通信,这篇介绍另外一种通信协议I2C。

UART通信如图:

I2C通信如图:

(注:这个示意图是笔者百度随便找的,好像是站内哪个老兄的图,笔者好像在哪篇看到过,特意声明下)

     UART属于异步通信,比如计算机发送给单片机,计算机只负责把数据通过TXD发送出来即可,接收数据是单片机自己的事情。而I2C属于同步通信,SCL时钟线负责收发双方的时钟节拍,SDA数据线负责传输数据,I2C的发送方和接收方都以SCL这个时钟节拍为基数进行数据的发送和接收。

    在硬件上,I2C总线是由时钟总线SCL和数据总线SDA两条线构成。连接到总线上的所有器件的SCL都连到一起,所有SDA都连到一起。I2C总线是开漏引脚并联的结构,因此外部要添加上拉电阻

       对于开漏电路外部加上拉电阻,就组成了线“与”的关系。总线上线“与”的关系就是说,所有接入的器件保持高电平,那这条线才是高电平,而任何一个器件输出一个低电平,那这条线就会保持低电平,因此可以做到任何一个器件都可以拉低电平,也就是说任何一个器件都可以作为主机。

      虽然说任何一个设备都可以作为主机,但绝大多数情况下都是单片机来做主机,而总线上挂的多个器件,每一个都像电话机一样有自己唯一的地址,在信息传输的过程中,通过唯一的地址就可以正常识别到属于自己的信息,笔者使用的是金沙滩工作室宋老师的板子,他的开发板上就接了两个使用I2C通信的设备,一个是24C02,另一个是PCF8591.

    UART串行通信的时候,知道通信的流程为起始位、数据位、停止位(基础方式)这三部分,同理在I2C中也有起始信号,数据传输和停止信号如下图:

从图上来看,I2C和UART时序流程有相似性,也有一定的区别。

1:UART每个字节中都有1个起始位,8个数据位,1个结束位。UART是先传输数据的低位,I2C刚好相反是先传输字节的高位。

2:I2C分为起始信号、数据传输和停止信号,其中数据传输部分可以一次通信过程传输多个字节,字节数是不受限制的,而每个字节的数据最后也跟了一位,这一位叫做应答位(低电平信号),通常用ACK表示,有点类似UART的停止位,它通常是表达1个字节的数据传递结束的信号,这个数据可以是设备地址信息,设备内存地址信息,设备内存中将要或者已经存储的数据信息等。

3:ACK在“写”与“读”功能中,它的发起方是不同,因此在程序实现上也是不一样的。“写”功能的ACK是由从机发出的,所以这个程序表现是接收这个ACK信号,它意思是告诉主机1个字节数据我已经接收完了因此发了1个ACK。作为主机的单片机在发送完1个字节数据后就要检测这个从机有没有发出ACK,未接收到这个ACK前都不能发送新的数据,否则新发送的信号,从机就无法正确接收产生错误,这和LCD1602的“忙”判断非常的类似。而且在器件地址寻址和器件内存寻址时候都是使用“”这个模式的这和LCD1602一样。因此情景下的ACK是告诉主机,你发的地址信息和我匹配,我来响应你的请求。这两个ACK的区别就是写数据的时候ACK的响应上会花费更长一点的时间,毕竟需要把RAM中数据搬运到"非易失"区。这个时间由手册可知是小于5ms。

“读”功能的ACK是由主机发出的(即我们程序编写的由主机发出从机接收),它在发送完1个字节的数据后,要发1个ACK给从机,当所有的数据都发送完毕时,就不再发送ACK了而是发送NAK(高电平信号NO ACK)。

     这个NAK是主机向从机发送1个高电平信号,即字节传输时钟线第九个高脉冲的高电平过程中,从机在数据线上检测到高电平,从机就关闭允许被“读”这个功能,当然“写”功能其实也是有NAK,未响应其实就是NAK或者来不及响应就检测SDA就可能检测到高电平,区别的是这个信号来自从机本篇主机是51单片机。由于线“与”逻辑,所以ACK的信号必然合适是低电平(因为都是高电平的时候是总线释放状态,换句话说就是你没什么事,就别拉低,拉低了就代表有情况发生)。相较于UART串口通信的停止位是“1”,而ACK的停止位是“0”,作为应答位在使用习惯上ACK是反逻辑的。因此本篇在程序上会再取反,以适应使用习惯。

如图是24C02(串行E2PROM)页写入模式时序图。

4:UART通信虽然用了TXD和RXD两根线,但是实际一次通信中,1条线就可以完成,2条线是把发送和接收分开而已,而I2C每次通信,不管发送还是接收,必须两条线都参与工作才能完成。

然后看一下I2C总线中文文档里面提供的时序图解释:

这是I2C的起始条件和截止条件的时序图,

  • 起始信号:UART通信是从一直持续的高电平出现一个低电平标志起始位,当然我们现在UAR模块化了,因此这个部分在程序上都不再体现,只在传输完1个字节数据后发送1个中断标志位,通过检测标志位来确定下一个步骤。
  • 而I2C通信的起始信号的定义是SCL为高电平期间,SDA由高电平向低电平变化产生一个下降压沿,表示起始信号,如图所示的Start部分。也就是说在程序中要表现出这个下降沿。(插1句笔者在大学期间虽然也大概理解上升沿和下降沿,但是只能理解高低电平作为信号电平,因为没真正看到过或者理解上升沿或者下降压作为触发条件的电路实体,因此对跳变电平总是持有一种忽视的情绪,刚好笔者上篇博文关于51单片机IO输出高电平的强推挽模式只发生在0向1跳变这个逻辑里,展现了跳变电平对电路的控制能力)
  • 停止信号:UART通信的停止位是1位固定的高电平信号,而I2C通信停止信号的定义是SCL为高电平期间,SDA由低电平向高电平变化产生一个上升沿,表示结束信号。如图中Stop部分展示。

I2C数据传输图:

  • 数据传输:首先UART是低位在前,高位在后;而I2C通信是高位在前,低位在后。其次。UART通信数据位是固定长度,波特率分之一,一位一位固定时间发送完毕即可以。而I2C没有固定波特率,但是有时序要求,要求SCL为低电平时,SDA允许变化。也就是说,发送方必须先保持SCL是低电平,才可以改变数据线SDA,输出要发送的当前数据的一位;而当SCL在高电平的时候,SDA绝对不可以变化。因为这个时候,接收方要来读取当前SDA的电平信号是0还是1,因此要保证SDA的稳定。如上图中的每一位数据的变化,都是在SCL的低电平位置。8位数据后边跟着的是一位应答位。

  • 图6红字标的意思
  • 1:应答位信号来自从机
  • 2:字节传输完成,可以发生从机内部中断服务(如果有)
  • 3:如果中断发生,时钟线要保持在地电平(这代表着实体电路中如果有中断发生的可能,那么程序就必须要考虑到这一点让这个中断函数在这个时间段里执行完毕。对中断标志位进行判断,确定没有中断标志位才能把SCL拉高,进行下一个字节的传输)
  • 4:应答信号来自接收者
  • 5:图中的MSB中文意思是,最高有效位。

图7:表达“写”功能时,作为数据发送方和数据接收方的电平逻辑,可以看到在接收数据的时候,数据接收方的SDA要一直保持高电平。它只在接收完一个字节数据(这个可能是地址信息也可能是数据)后发送一个ACK或者NAK,而这个时候,数据发送方的电平信号就要保持在高电平。在时序上字节数据发送完的下一个时钟线高电平信号来临前,双方的SDA的线的电平信号都要提前确立。在这个时钟线的高电平期间,主机会检测SDA的信号,来确定从机是否正确接收字节数据即有没有发送ACK。一般来说数据的发送方就是主机本身。

I2C寻址模式

在发送起始信号后,传输第一个字节数据。这第一个字节数据包括从机的地址和读写功能选择(7为寻址模式)。看一下手册上是怎么说的

本篇作为从机的是Serial E2PROM存储器24C02,然后看一下24C02设备寻址的示意图

以及本案24C02采用的接线模式:

24C02后的02代表的是存储量,02代表的是2K bit即256字节

可以看到24C02的地址的前4位是固定的1010,然后是可编程的地址位A2,A1,A0.本案是直接把它们接地了,那么这个7位地址就是1010000,然后加上最后一位读写位,寻址的时候是使用“写”模式的因此24C02的寻址字节是 1010 0000 == 0x50<<1。

24C02接线过程中还有一个引脚WP,写保护引脚。它接地的时候允许“读写”功能,它接高电平就处在只读状态。这可能是为什么有些机器破解要硬破,它存储的参数处于只读状态,无法软件修改。

再看一下SCL和SDA的上拉电阻

一般来说这个总线上拉电阻RP以及电容都是有电气要求的。如果你能够看懂的话可以去啃手册,如果不懂就使用典型值,如图示的R63,R64 4.7K电阻。

实践一下:我们用程序来寻址24C02,如果是24C02的地址,24C02会发送1个应答位ACK,再发送1个不是24C02的地址,那么我们检测总线就不会找到应答信号即NAK。然后把这个应答结果用液晶显示出来。

看程序:

main.c

# include<reg52.h>
# include<intrins.h>
# define I2CDelay()  {_nop_();_nop_();_nop_();_nop_();}sbit I2C_SCL = P3^7;
sbit I2C_SDA = P3^6;bit I2CAddressing(unsigned char addr);//I2C寻址函数,返回值为器件应答值
extern void InitLcd1602();
extern void LcdShowStr(unsigned char x,unsigned char y, unsigned char *str);void main()
{bit ack;unsigned char str[10];InitLcd1602();ack = I2CAddressing(0x50); //应答位赋值给ACK 0x50 =0101 0000,24C02器件地址str[0] = '5';             //7位地址最高位是0,这个值左移1为就是写功能寻址字节str[1] = '0';str[2] = ':';str[3] = (unsigned char )ack + '0';//应答位强制装换为char型并转换为相应字符的ASCII码str[4] = '\0';LcdShowStr(0,0,str); //显示位置0列0行ack = I2CAddressing(0x62);str[0] = '6';str[1] = '2';str[2] = ':';str[3] = (unsigned char)ack+'0';str[4] = '\0';LcdShowStr(8,0,str); //显示位置8列0行,其实是第9列while(1);
}
/*产生总线起始信号    */
void I2CStart()
{I2C_SDA = 1; //首先确保SDA SCL都是高电平I2C_SCL = 1;I2CDelay();  //维持时间I2C_SDA = 0; //先拉低SDA I2CDelay();  //维持时间I2C_SCL = 0; //再拉低SCL,此后SDA可以发送数据}
/*产生总线停止信号   */
void I2CStop()
{I2C_SCL = 0;  //首先确保SDA,SCL都是低电平维持一段时间大于等于5usI2C_SDA = 0;I2CDelay();   //延迟4个机器周期//先拉高SCL并维持5us.  11.0592M晶振1个机器周期的时间大概是1us左右,赋值运算是1个机器周期I2C_SCL = 1;I2CDelay();//在拉高SDA并维持5usI2C_SDA = 1; I2CDelay();}
/* I2C总线写操作,dat为待写入字节,返回值为从机应答的值    */
bit I2CWrite(unsigned char dat)
{bit ack;unsigned char mask;for(mask = 0x80; mask != 0; mask >>= 1)//0x80 = 1000 0000{if((mask&dat) == 0) I2C_SDA = 0;  //该处赋值是单片机输出电平信号输出电平信号需要SCL为低电平,该动作在I2CStart已操作elseI2C_SDA = 1;I2CDelay();//以下这两句是读数据的过程	I2C_SCL = 1; I2CDelay();  I2C_SCL = 0; //再拉低SCL完成一个周期,拉低SCL是为了下个SDA输出数据,SDA只有在SCL是低电平的时候才能改变电平}I2C_SDA = 1;//8位数据发送完后,主机释放SDA,以检测从机应答I2CDelay();I2C_SCL = 1;//拉高SCLack = I2C_SDA;//读取此时SDA的值,即为从机的应答值I2CDelay();  //维持4个机器周期I2C_SCL = 0;//再拉低SCL完成应答位,并保持住总线return ack; //返回从机应答值
}/*I2C寻址函数,即检查地址为addr的器件是否存在,返回值为从器件应答值   */
bit I2CAddressing(unsigned char addr)
{bit ack;I2CStart();           //产生起始位,即启动一次总线操作 ack = I2CWrite(addr << 1); //器件地址需左移一位,因寻址命令的最低位,//为“写”功能,I2CStop();                 //不需要进行后序读写,而直接停止本次总线操作return ack;                //这里如果不习惯可以直接写首地址字节,addr只代表地址不包含读写}

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);//显示器开 ,光标关闭}

该程序,主机发出了两个地址一个是24C02的地址0x50,一个是杜撰地址0x62。然后检测应答信号。看下结果

可以看到地址0x50它接收到了应答位ACK(0),地址0x62没有接收到应答位NAK(1)。

如下图:用逻辑分析仪解析可以看到写入的地址字节0xA0(1010 0000)有ACK ,写入的0xC4字节(1100 0100)检测到的是NAK。特意提醒一下0x50是7位的设备地址,加上最低位读写位0,就是0xA0。

可以看到程序正确运行,结果也符合要求。

就着程序以及时序图和24C02时序图的要求一一对照看是不是都满足。

如下图:

如上图:

fscl:SCL时钟频率,可以看到手册给出的是100KHZ和400KHZ,笔者的24C02是可以工作在这两种频率下,根据注释1如果你的丝印上右下脚印有“D”这个字母,就可以工作在400Kb的模式下。这个值代表着通信速度。一般来说你可以通过这个参数知道高低电平的大概持续时间。也就是说实际程序产生的时序必须小于等于100K的时序参数,(为什么是小于呢?因为超过100K,有可能器件还在读取数据的时候,你时钟线就到了下一个时序,那肯定是不允许的)。也就是说传输1位的周期要大于10us,即平均一下高低电平持续时间内都不短于5us。不过这个一般看具体I2C器件的时序参数就可以。100KHZ是属于低速模式,400KHZ是属于快速模式。由手册可知它们对电源的要求也是不一样的。

按照前文所说的I2C开始通信会产生一个开始信号,看下程序以及怎么描述的

/*产生总线起始信号    */
void I2CStart()
{I2C_SDA = 1; //首先确保SDA SCL都是高电平I2C_SCL = 1;I2CDelay();  //维持时间I2C_SDA = 0; //先拉低SDA I2CDelay();  //维持时间I2C_SCL = 0; //再拉低SCL,此后SDA可以发送数据}
  • 首先把SDA和SCL都拉高,维持一段时间再拉低SDA。这个过程就是时序图的tsu.sta

tsu.sta:这个时序范围是SCL的上升沿到SDA的下降沿之间的时间即SDA,SCL都是高电平的持续时间,称之为重复起始条件建立时间(注:重复起始条件和起始条件的时序要求是一样的),它的时间要求是最小值是4.7us,我看了下逻辑分析仪,其实它持续的时间很长。就以第二次寻址为例它持续了385us,

当然我们这个程序控制的延时时间是:

    I2C_SCL = 1;I2CDelay();  //维持时间

这两句的时间大概是5us。

  • SDA变为下降沿后,维持一段时间才允许SCL电平由高变低,这个时间就是tHD.STA

tHD.STA 重复起始条件的保持时间在这 个周期后产生第一个时钟脉冲,它最小值是4us。我们看一下程序它延时时间来自

    I2C_SDA = 0; //先拉低SDA I2CDelay();  //维持时间

分析下这个语句,赋值运算是1个机器周期。I2CDelay用来4个_NOP_();是4个机器周期。

一个机器周期在11.0592M的晶振下大概是1us多一点,即总计大概5us。看一下时序图的时间

这个时间是5.46us。没有问题。

这样产生总线其实信号这个过程就结束了。

  • 在SCL被拉低这段时间,SDA是被允许改变的,被拉低后再拉高,这段低电平的时间是tlow

tlow:SCL的低电平周期最小值是4.7us看下程序是怎么实现的。

I2C_SCL = 0;//这句本案是放在起始信号最后一句,但它是属于SCL低电平周期的语句
bit I2CWrite(unsigned char dat)
{bit ack;unsigned char mask;for(mask = 0x80; mask != 0; mask >>= 1)//0x80 = 1000 0000{if((mask&dat) == 0) I2C_SDA = 0;  //该处赋值是单片机输出电平信号输出电平信号需要SCL为低电平,该动作在I2CStart已操作elseI2C_SDA = 1;I2CDelay();//以下这两句是读数据的过程	I2C_SCL = 1; //之前语句消耗的时间就是SCL低电平周期的时间

可以看到在实现输出第一个要传输的电平信息后,SCL拉高了。这些语句消耗的时间都是SCL低电平周期时间,从程序结构上来看,SCL拉高前还进行了4个机器周期的延时语言。这编程过程相当的保守了。我们看一下实际中这个时间是多少21.7us。

如果你仔细看的话,它其实有两个tlow,第二个tlow其实是tHD.DAT和tsu.DAT的时间和,而第一个tlow的按照功能来说只是tsu.DAT.因此保守编程的4个机器周期是为了tsu.DAT服务的。

  • tsu.DAT:数据建立时间,它的意思数据建立后要持续一段时间才能拉高时钟线。由手册可知它只有最小时间200ns,本案编程速率是100KB。因此它编程的相当保守。我们再看一下I2C手册上该时间是多少,它的要求也只是最小值250ns,而tHD.DAT它的时间要求是0.
  • 在SCL被重新拉高后,SCL时钟线要持续一段时间,然后再拉低。这段时间就是thigh

thign:SCL高电平周期持续时间是最小是4us看下程序实现

        I2C_SCL = 1; I2CDelay();  

可以看到它的延时时间也应该是5us左右,

可以看到逻辑分析仪采样出来的是5.38us,好的没有问题。

然后我们再看之前提到的一个时序参数,tHD.DAT,

tHD.DAT数据保持时间,它的意思是SCL下降沿后,SDA还要保持一段时间,才允许变化。

然后我们发现手册给出时间竟然是0,不可思议!amazing!!回想一下LCD16202也是有tHD.DAT,而它的时间是10ns(写模式下)。是什么原因造成他们的不同?

答:因为他们的通信协议不一样,如果是这种答案的话未免太笼统了。因为他们的工作逻辑是不一样的,对于I2C器件(24C02)它在高电平的时间就完成了对电平信号的读取,因此下降沿后SDA数据线不需要在保持一段时间。而LCD1602它对电平信号的读取是发生在下降沿后10ns内完成。注意这个过程都发生在“写”模式下。如果以笔者推荐的时序视频描述的话,对于I2C,它摆Pose的时间发生在SCL低电平周期里,按快门的时间发生在SCL高电平周期。而对于LCD1602它摆Pose的时间不仅是在SCL的低电平周期里,它高电平周期里也是处于摆pose的状态,它按快门的时间是发生在下降沿这里,然后这个低电平的持续时间是10ns。看一下LCD1602的时序图,这是笔者觉得这个数值0的来由。(这个结论在LCD1602的博文中笔者有简单的探索,但是对于这个结论是不是百分百正确不保证,只是个人论证,可以认为是学习过程中的阶段探索.而且笔者认为这些“读”“写”功能的流程一般都是由三部分组成,开始-维持/使能-使能结束/结束,因此这个控制时序应是高脉冲或者低脉冲实现,就好比对于 LCD1602读功能,它写功能的使能条件都是高电平,但它对THD2(读)依然是有时序要求的。因此笔者认为THD2(读)是读功能完全关闭的时间。

  • 然后开始数据传输和ACK以及NAK

可以看到SDA线按照规则输出了1010 0000,最后第9九个高电平期间检测到ACK,前文提到这个ACK来自于从机(24C02),我们看一下程序是怎么实现的。

 I2C_SDA = 1;//8位数据发送完后,主机释放SDA,以检测从机应答I2CDelay();I2C_SCL = 1;//拉高SCLack = I2C_SDA;//读取此时SDA的值,即为从机的应答值I2CDelay();  //维持4个机器周期I2C_SCL = 0;//再拉低SCL完成应答位,并保持住总线return ack; //返回从机应答值
}

看下程序逻辑:

1:先拉高SDA,然后维持5发个机器周期

2:再拉高SCL时钟线,立刻读取SDA的电平信息,再维持4个机器周期。

3:最后又保持总线拉低SCL。

4:可以看到在ACK信号是在字节传输的第9个高电平期间读取的。因为ACK是由从机发送的,但是这个ACK信号会维持一段时间还是维持到SCL变成低电平为止?看下图笔者拉长了ACK高电平时间

由二副图可知这个ACK信号持续了整个高电平时间,一旦SCL拉低,总线就立刻处于释放状态,即SDA总线处于高电平。 

5:前文笔者提到对于ACK信号应该要先判断有没有收到在进行下一步操作,而这个程序并没有体现。因为写地址这个过程的ACK反馈是即刻的,目前笔者知道“多字节连续写入”的时候,ACK是需要进行类似LCD1602“忙”判断程序的。这在下篇博文笔者会体现。

6:再看下NAK的时序图,地址0x62是虚假地址因此没有ACK信号,我们看下时序图:

由图可知在第9个高电平期间SDA是维持在高电平的。注:绿点事起始信号,红点是结束信号

  • 然后看下I2C结束信号的编程
void I2CStop()
{I2C_SCL = 0;  //首先确保SDA,SCL都是低电平维持一段时间大于等于5usI2C_SDA = 0;I2CDelay();   //延迟4个机器周期//先拉高SCL并维持5us.  11.0592M晶振1个机器周期的时间大概是1us左右,赋值运算是1个机器周期I2C_SCL = 1;I2CDelay();//在拉高SDA并维持5usI2C_SDA = 1; I2CDelay();}

看下编程过程:

1:先把SCL和SDA都拉低并延长一段时间再把SCL拉高,然后SDA保持一段时间这段时间就是Tsu.STO

  • Tsu.STO停止条件的建立时间,由手册可知它只有最小值4.7us
  • 看下程序实现语句
        I2C_SCL = 1;I2CDelay();

2:SDA电平跳变后,高电平持续时间是tBUF

  • tBUF:停止和启动条件之间的总线空闲时间看一下程序实现
  •    I2C_SDA = 1; I2CDelay();

这个程序的前三句的确保这个SDA = 0是在SCL是低电平发生的

    I2C_SCL = 0;  //首先确保SDA,SCL都是低电平维持一段时间大于等于5usI2C_SDA = 0;I2CDelay();   //延迟4个机器周期

到此对于I2C的主要时序功能都做了基本介绍,

     但是还有一个问题要确认即:在字节传输过程中,如果在SCL为高电平的时候,电平信号受到到干扰,有0变1或者由1变0,它会影响最终的结果吗,由前文得知它数据是在高电平只有最小值4.0us,因此读取必然是发生在这4us中。如果我把这个高脉冲再延迟4个机器周期,而在这个4个机器周期中把它的电平信号改动一下,那么最终I2C期间会接受到的数据会是什么?

结果是出错,,而且由于跳变电平发生在高电平期间,因此被I2C器件认为是重复开始或者结束的信号。

看图

修改的程序:

再测验一次另外一种电平跳动。

可以看到它从高电平变成了重复起始条件信号。

由于笔者的24c02是可以工作在400KB下的,事实上所有的延迟语句I2CDelay()删除 其实都不影响工作。

结语:本文描述了I2C 的器件寻址,以及笔者的一些个人的不成熟结论

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

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

相关文章

C语言实现归并排序(Merge Sort)

目录 一、递归实现归并排序 1. 归并排序的基本步骤 2.动图演示 3.基本思路 4.代码 二、非递归实现 1.部分代码 2.代码分析 修正后代码&#xff1a; 归并过程打印 性能分析 复杂度分析 归并排序是一种高效的排序算法&#xff0c;采用分治法&#xff08;Divide and Con…

javase复习day35反射

反射 获取class对象的方法 public class Demo1 {public static void main(String[] args) throws ClassNotFoundException {//获取反射的三种方式//第一种 Class.forName(全类名)//用法&#xff1a;最为常用Class<?> clazz1 Class.forName("Reflection.Student&q…

程序员如何以最快的方式提升自己?分享4个有效方法!

作家周国平说&#xff1a;人与人之间最重要的区别&#xff0c;不在物质的贫富和社会方面的境遇&#xff0c;是内在的素质和层次&#xff0c;把人分出了伟大与渺小、优秀与平庸。有的人醉心于三五成群的消遣&#xff0c;有的人专注于一步一脚印的努力&#xff0c;人和人之间的差…

Shiro-550—漏洞分析(CVE-2016-4437)

文章目录 漏洞原理源码分析加密过程解密过程 漏洞复现 漏洞原理 Shiro-550(CVE-2016-4437)反序列化漏洞 在调试cookie加密过程的时候发现开发者将AES用来加密的密钥硬编码了&#xff0c;并且所以导致我们拿到密钥后可以精心构造恶意payload替换cookie&#xff0c;然后让后台最…

利用Puppeteer-Har记录与分析网页抓取中的性能数据

引言 在现代网页抓取中&#xff0c;性能数据的记录与分析是优化抓取效率和质量的重要环节。本文将介绍如何利用Puppeteer-Har工具记录与分析网页抓取中的性能数据&#xff0c;并通过实例展示如何实现这一过程。 Puppeteer-Har简介 Puppeteer是一个Node.js库&#xff0c;提供…

VUE.js笔记

1.介绍vue Vue 是一款用于构建用户界面的 JavaScript 框架。它基于标准 HTML、CSS 和 JavaScript 构建&#xff0c;并提供了一套声明式的、组件化的编程模型&#xff0c;帮助你高效地开发用户界面。无论是简单还是复杂的界面&#xff0c;Vue 都可以胜任。 Vue 应用程序的基本…

初识C语言(三)

感兴趣的朋友们可以留个关注&#xff0c;我们共同交流&#xff0c;相互促进学习。 文章目录 前言 八、函数 九、数组 &#xff08;1&#xff09;数组的定义 &#xff08;2&#xff09;数组的下标和使用 十、操作符 &#xff08;1&#xff09;算数操作符 &#xff08;2&#xff…

统计本周的订单数,统计最近7天的订单数

3个函数 DATE_SUB和SUBDATE在MySQL中的作用是一样的&#xff0c;它们都是用于执行日期的减法运算。具体来说&#xff0c;这两个函数都允许你从给定的日期或日期时间值中减去一个指定的时间间隔&#xff0c;然后返回一个新的日期或日期时间值。 DATE函数 DATE(time) 用于获取…

多机部署,负载均衡-LoadBalance

文章目录 多机部署,负载均衡-LoadBalance1. 开启多个服务2. 什么是负载均衡负载均衡的实现客户端负载均衡 3. Spring Cloud LoadBalance快速上手使用Spring Cloud LoadBalance实现负载均衡修改IP,端口号为服务名称启动多个服务 负载均衡策略自定义负载均衡策略 LoadBalance原理…

图像处理04

图像处理 问题&#xff1a;把不规则的图片按照参考图摆放 步骤&#xff1a; 1. 用ORB找关键点 2. 关键点匹配 3. 根据上一步匹配的关键点得出单应性矩阵 4. 根据单应性矩阵对不规则进行透视变换 import cv2 import numpy as np import matplotlib.pyplot as pltimgl cv2.imrea…

liunxcentos7下 跟目录空间不足docker load镜像报错空间不足

前两天在公司&#xff0c;做jenkins流水线项目&#xff0c;然后把项目放到docker容器里面运行&#xff0c;就在我把镜像打好包的时候正准备往服务器里面导入镜像的时候报错&#xff1a;如图所示 这时发现自己的根目录空间不足。 解决办法&#xff1a;重新加一块磁盘将磁盘挂载…

Java线程池和原子性

文章目录 前言1 线程池1.1 线程池概述1.1.1 线程池存在的意义1.1.2 Executors默认线程池 1.2 线程状态介绍1.2.1 线程状态源码1.2.2 线程状态含义1.2.3 线程状态转换图 2 原子性2.1 volatile关键字2.2 synchronized解决2.3 原子性2.4 AtomicInteger类2.5 悲观锁和乐观锁 前言 …

Vue 响应式监听 Watch 最佳实践

一. 前言 上一篇文章我们学习了 watch 的基础知识&#xff0c;了解了它的基本使用方法及注意事项&#xff0c;本篇文章我们继续了解在Vue 中 响应式监听 watch 的妙用。了解 watch 的基础使用请参考上一篇文章&#xff1a; 详解 Vue 中 Watch 的使用方法及注意事项https://bl…

【Docker】02-数据卷

1. 数据卷 数据卷(volume) 是一个虚拟目录&#xff0c;是容器内目录与宿主机目录之间映射的桥梁。 2. 常见命令 docker volume createdocker volume lsdocker volume rmdocker volume inspect 查看某个数据卷的详情docker volume prune 清除数据卷 **数据卷挂载&#xff1a…

bench.sh:一行命令测试Linux服务器基准测试

简介 bench.sh 是一个 Linux 系统性能基准测试工具。它的测试结果如下图&#xff1a;给出服务器的整体配置信息&#xff0c;IO 性能&#xff0c;网络性能。很多人使用它测试 vps 性能。 ​​ 一键运行 服务器在国外可以使用以下命令运行测试 wget -qO- bench.sh | bash复制…

微信小程序配置prettier+eslint

虽然微信开发者工具是基于vscode魔改的.但是由于版本过低,导致很多插件也用不上新版本.所以在微信开发者工具限制的版本下使用的prettier,eslint也是有版本要求. 本文主要就是记录一下需要的版本号 1.微信开发者工具安装插件 2.package.json中添加以下依赖及安装依赖 "de…

JVM(HotSpot):方法区(Method Area)

文章目录 一、内存结构图二、方法区定义三、内存溢出问题四、常量池与运行时常量池 一、内存结构图 1.6 方法区详细结构图 1.8方法区详细结构图 1.8后&#xff0c;方法区是JVM内存的一个逻辑结构&#xff0c;真实内存用的本地物理内存。 且字符串常量池从常量池中移入堆中。 …

云服务器连接不上是什么原因引起的?

云服务器连接不上是什么原因引起的&#xff1f;云服务器连接不上是一个常见的问题&#xff0c;常见的原因有网络连接、账户权限、安全组设置、服务器状态、端口占用、远程登录未开启、云服务器已关闭或到期、防护软件限制、DNS劫持、资源负载过高。以下是一些主要原因及解决方法…

微信小程序 - 最新详细安装使用 Vant weapp UI 框架环境搭建详细教程

前言 自从 2024 年开始,小程序做了很多改变和升级, 导致网上很多搭建教程文章的教程失效了,本文来做最新的教程。 第一步 为了更贴合新手,我这里创建了一个纯净无任何业务代码的小程序项目。

SpringBoot-全局处理异常,时间格式,跨域,拦截器,监听器

1.全局异常处理 使用ControllerAdvice与ExceptionHandler注解 /*** 全局异常处理程序** author * date */ ControllerAdvice ResponseBody public class GlobalExceptionHandler {ExceptionHandler(Exception.class)public JsonResult handleException(Exception e) {e.print…