首先先推荐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都是低电平维持一段时间大于等于5us
I2C_SDA = 0;
I2CDelay(); //延迟4个机器周期
//先拉高SCL并维持5us. 11.0592M晶振1个机器周期的时间大概是1us左右,赋值运算是1个机器周期
I2C_SCL = 1;
I2CDelay();
//在拉高SDA并维持5us
I2C_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已操作
else
I2C_SDA = 1;
I2CDelay();
//以下这两句是读数据的过程
I2C_SCL = 1;
I2CDelay();
I2C_SCL = 0; //再拉低SCL完成一个周期,拉低SCL是为了下个SDA输出数据,SDA只有在SCL是低电平的时候才能改变电平
}
I2C_SDA = 1;//8位数据发送完后,主机释放SDA,以检测从机应答
I2CDelay();
I2C_SCL = 1;//拉高SCL
ack = 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 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);//显示器开 ,光标关闭
}
该程序,主机发出了两个地址一个是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已操作
else
I2C_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;//拉高SCL
ack = 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都是低电平维持一段时间大于等于5us
I2C_SDA = 0;
I2CDelay(); //延迟4个机器周期
//先拉高SCL并维持5us. 11.0592M晶振1个机器周期的时间大概是1us左右,赋值运算是1个机器周期
I2C_SCL = 1;
I2CDelay();
//在拉高SDA并维持5us
I2C_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都是低电平维持一段时间大于等于5us
I2C_SDA = 0;
I2CDelay(); //延迟4个机器周期
到此对于I2C的主要时序功能都做了基本介绍,
但是还有一个问题要确认即:在字节传输过程中,如果在SCL为高电平的时候,电平信号受到到干扰,有0变1或者由1变0,它会影响最终的结果吗,由前文得知它数据是在高电平只有最小值4.0us,因此读取必然是发生在这4us中。如果我把这个高脉冲再延迟4个机器周期,而在这个4个机器周期中把它的电平信号改动一下,那么最终I2C期间会接受到的数据会是什么?
结果是出错,,而且由于跳变电平发生在高电平期间,因此被I2C器件认为是重复开始或者结束的信号。
看图
修改的程序:
再测验一次另外一种电平跳动。
可以看到它从高电平变成了重复起始条件信号。
由于笔者的24c02是可以工作在400KB下的,事实上所有的延迟语句I2CDelay()删除 其实都不影响工作。
结语:本文描述了I2C 的器件寻址,以及笔者的一些个人的不成熟结论