初学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 的器件寻址,以及笔者的一些个人的不成熟结论
相关文章:

初学51单片机之I2C总线与E2PROM
首先先推荐B站的I2C相关的视频I2C入门第一节-I2C的基本工作原理_哔哩哔哩_bilibili 看完视频估计就大概知道怎么操作I2C了,他的LCD1602讲的也很不错,把数据建立tsp和数据保持thd,比喻成拍照时候的摆pose和按快门两个过程,感觉还是…...

C语言数组探秘:数据操控的艺术【下】
承接上篇,我们继续讲数组的内容。 八.二维数组的使用 当我们掌握了二维数组的创建和初始化,那我们怎么使用二维数组呢?其实二维数组访问也是使用下标的形式的,二维数组是有行和列的,只要锁定了行和列就能唯一锁定数组中…...

Jmeter关联,断言,参数化
目录 一、关联 边界提取器 JSON提取器 正则表达式提取器 跨线程关联 二、断言 响应断言 JSON断言 断言持续时间 三、参数化 用户参数 csv data setconfig csvread函数 一、关联 常用的关联有三种 1.边界提取器 2.JSON提取器 3.正则表达式提取器 接下来就详细讲述…...

嵌入式单片机底层原理详解
前言 此笔记面向有C语言基础、学习过数字电路、对单片机有一定了解且尚在学习阶段的群体编写,笔记中会介绍单片机的结构、工作原理,以及一些C语言编程技巧,对于还停留在复制模板、copy代码阶段的读者会有比较大的帮助,待学习完成后可以独立完成几乎所有单片机的驱动开发。 …...

重修设计模式-行为型-责任链模式
重修设计模式-行为型-责任链模式 将请求的发送和接收解耦,让多个接收对象都有机会处理这个请求。将这些接收对象串成一条链,并沿着这条链传递这个请求,直到链上的某个接收对象能够处理它为止。 责任链模式(Chain of Responsibilit…...

Vercel部署/前端部署
Vercel 部署 今天要讲的是如何对别人向自己的开源仓库提的PR进行自动代码审核 1. 注册并登录Vercel 访问 Vercel官网点击右上角的"Sign Up"选择使用GitHub、GitLab、Bitbucket或邮箱注册完成注册流程并登录 2. 连接代码仓库 在Vercel仪表板,点击"New Proje…...

常见的css预处理器
CSS预处理器是一种扩展了CSS功能的脚本语言,它允许开发者以编程的方式编写更加干净、结构化的CSS代码。通过引入变量、嵌套规则、混合(Mixins)、函数等高级特性,CSS预处理器使得CSS代码的编写更加灵活、高效,同时也提高…...

mysql—半同步模式
mysql的并行复制 在172.25.254.20(slave)主机上 默认情况下slave中使用的是sql单线程回放 在master中时多用户读写,如果使用sql单线程回放那么会造成组从延迟严重 开启MySQL的多线程回放可以解决上述问题 mysql> show processlist; 在配置文件中进行编辑 [root…...

You are not allowed to push code to this project
原因1 用户权限不够。 具体查看用户权限路径: 原因2 vscode之前都能提交代码,但是突然就提交不上了。 表现为:前端代码能拉取,但是不能提交。使用idea进行前端代码的提交,完全没问题。 解决方案:修改TortoiseG…...

Java刷题:最小k个数
目录 题目描述: 思路: 具体实现 整体建立一个大小为N的小根堆 通过大根堆实现 完整代码 力扣链接:面试题 17.14. 最小K个数 - 力扣(LeetCode) 题目描述: 设计一个算法,找出数组中最小的…...

Redis实战--Redis应用过程中出现的热门问题及其解决方案
Redis作为一种高性能的key-value数据库,广泛应用于缓存、消息队列、排行榜等场景。然而,在实际应用中,随着业务规模的不断扩大和访问量的持续增长,缓存系统也面临着诸多挑战,其中最为典型的便是缓存穿透、缓存击穿和缓…...

实时数字人DH_live使用案例
参看: https://github.com/kleinlee/DH_live ubuntu 测试 apt install ffmpeg 下载安装: git clone https://github.com/kleinlee/DH_live.git cd DH_liveconda create -n dh_live python=3.12 conda activate dh_live pip install -r requirements.txt pip install torch -…...

线上环境排故思路与方法GC优化策略
前言 这是针对于我之前[博客]的一次整理,因为公司需要一些技术文档的定期整理与分享,我就整理了一下。(https://blog.csdn.net/TT_4419/article/details/141997617?spm1001.2014.3001.5501) 其实,nginx配置 服务故障转移与自动恢复也是可以…...

硬件设计很简单?合宙低功耗4G模组Air780E—开机启动及外围电路设计
Air780E是合宙低功耗4G-Cat.1模组经典型号之一,上期我们解答了大家关心的系列问题,并讲解了选型的注意要点。 有朋友问:能不能讲些硬件设计相关的内容? 模组的上电开机,是硬件设计调试的第一步。 本期特别分享——Ai…...

初试AngularJS前端框架
文章目录 一、框架概述二、实例演示(一)创建网页(二)编写代码(三)浏览网页(四)运行结果 三、实战小结 一、框架概述 AngularJS 是一个由 Google 维护的开源前端 JavaScript 框架&am…...

【学习笔记】手写 Tomcat 六
目录 一、线程池 1. 构建线程池的类 2. 创建任务 3. 执行任务 测试 二、URL编码 解决方案 测试 三、如何接收客户端发送的全部信息 解决方案 测试 四、作业 1. 了解工厂模式 2. 了解反射技术 一、线程池 昨天使用了数据库连接池,我们了解了连接池的优…...

打靶记录18——narak
靶机: https://download.vulnhub.com/ha/narak.ova 推荐使用 VM Ware 打开靶机 难度:中 目标:取得 root 权限 2 Flag 攻击方法: 主机发现端口扫描信息收集密码字典定制爆破密码Webdav 漏洞PUT 方法上传BF 语言解码MOTD 注入CVE-2021-3…...

LabVIEW编程能力如何能突飞猛进
要想让LabVIEW编程能力实现突飞猛进,需要采取系统化的学习方法,并结合实际项目进行不断的实践。以下是一些提高LabVIEW编程能力的关键策略: 1. 扎实掌握基础 LabVIEW的编程本质与其他编程语言不同,它是基于图形化的编程方式&…...

代码随想录算法训练营第四四天| 1143.最长公共子序列 1035.不相交的线 53. 最大子序和 392.判断子序列
今日任务 1143.最长公共子序列 1035.不相交的线 53. 最大子序和 392.判断子序列 1143.最长公共子序列 题目链接: . - 力扣(LeetCode) class Solution {public int longestCommonSubsequence(String text1, String text2) {int[][] dp ne…...

2024.9.26 作业 +思维导图
一、作业 1、什么是虚函数?什么是纯虚函数 虚函数:函数前加关键字virtual,就定义为虚函数,虚函数能够被子类中相同函数名的函数重写 纯虚函数:把虚函数的函数体去掉然后加0;就能定义出一个纯虚函数。 2、基…...

WSL进阶体验:gnome-terminal启动指南与中文显示问题一网打尽
起因 我们都知道 wsl 启动后就死一个纯命令行终端,一直以来我都是使用纯命令行工具管理Linux的。今天看到网上有人在 wsl 中启动带图形界面的软件。没错,就是在wsl中启动带有图形界面的Linux软件。比如下面这个编辑器。 出于好奇,我就…...

recoil和redux之间的选择
Recoil 和 Redux 是两个流行的 JavaScript 状态管理库,它们各自有不同的设计理念和使用场景。选择哪一个更好用,取决于你的具体需求、项目规模和个人偏好。 1. 设计理念 Redux 单向数据流:Redux 采用单向数据流模型,所有的状态变…...

无人机的作战指挥中心-地面站!
无人机与地面站的关系 指挥与控制:地面站是无人机系统的核心控制部分,负责对无人机进行远程指挥和控制。无人机根据地面站下达的任务自主完成飞行任务,并实时向地面站反馈飞行状态和任务执行情况。 任务规划与执行:地面站具备任…...

Vue 23进阶面试题:(第八天)
目录 29.vue2.0和vue3.0区别? 30.事件中心的原理 31.使用基于token的登录流程 32.防抖和节流 防抖(debounce) 节流(throttle) 29.vue2.0和vue3.0区别? 1.由选项API转变为组合API。 2.vue3将全局配置…...

Acwing 最小生成树
最小生成树 最小生成树:由n个节点,和n-1条边构成的无向图被称为G的一棵生成树,在G的所有生成树中,边的权值之和最小的生成树,被称为G的最小生成树。(换句话说就是用最小的代价把n个点都连起来) Prim 算法…...

VIM简要介绍
安装 大多数 Linux 发行版和 macOS 都预装了 VIM。如果没有,你可以通过包管理器安装: Ubuntu/Debian: sudo apt-get install vimFedora: sudo dnf install vimmacOS: brew install vim(使用 Homebrew)Windows: 可以从 VIM 官网下…...

.NET 6.0 使用log4net配置日志记录方法
1.包管理器引入相关包 2.添加Log4net文件夹和log4net.config配置文件(配置文件属性设为始终复制)。 3.替换 log4net.config的内容(3.1与3.2选择一个就好,只是创建日志文件有所区别) 3.1: <?xml version"1.0" encoding"utf-8"?> <configuration…...

Unity角色控制及Animator动画切换如走跑跳攻击
Unity角色控制及 Animator动画切换如走跑跳攻击 目录 Unity角色控制及 一、 概念 1、角色控制 1) CharacterController(角色控制器) 2) CapsuleCollider + Rigidbody(使用物理刚体控制) 2、角色动画-Animation、Animator 1) 旧版动画系统...

JSP+Servlet+Mybatis实现列表显示和批量删除等功能
前言 使用JSP回显用户列表,可以进行批量删除(有删除确认步骤),和修改用户数据(用户数据回显步骤)使用servlet处理传递进来的请求参数,并调用dao处理数据并返回使用mybatis,书写dao层…...

Cannot read properties of undefined (reading ‘upgrade‘)
前端开发工具:VSCODE 报错信息: INFO Starting development server...10% building 2/2 modules 0 active ERROR TypeError: Cannot read properties of undefined (reading upgrade)TypeError: Cannot read properties of undefined (reading upgrade…...