找回密码
 立即注册

QQ登录

只需一步,快速开始

搜索
查看: 5543|回复: 20
打印 上一主题 下一主题
收起左侧

关于I2C总线读写应答机制

  [复制链接]
跳转到指定楼层
楼主
ID:266429 发表于 2021-10-26 10:06 | 只看该作者 回帖奖励 |倒序浏览 |阅读模式
I2C总线中的应答机制,属于该总线规格中的关键内容,本来并不复杂,也很容易理解,但问题在于现有的教材基本都是含糊不清,有的甚至会引起误解。因此,有必要详细说明。
先说明两个I2C总线中很重要的规则:
1. 总线在正常工作期间,从器件对时钟线无控制权,即从器件的时钟端口输出应始终保持在高电位,时钟线的状态,只能由主器件进行操作;至于数据线,则主器件与从器件都有权参与操作。
2. 总线正常工作时,时钟线处于高电位期间,不允许改变数据线的状态,即此时无论主器件还是从器件,都不能对其数据端口进行状态改变,除非是起始和终止操作。
记住这两个规则,很重要。
以下均通过AT24C的验证。

一、应答原理
I2C总线,有点像我们用的对讲机,尽管可以双向通讯,但双方不能同时说话,于是,就必须设有应答机制,而且,这个应答机制,必须是双方互动的,一方的话说完了,就要问对方收到没有,对方如果收到了这个问话,还得要再回个话。

二、向从器件写入时的应答机制
I2C与这种对讲机的应答的具体方式还是有些不一样的,规定是这样:在主机向从机发送数据时,规定每次发送一字节后就进行应答,主机每发送一字节数据后就停一下等待从机的反应,从机在收到一字节后的反应就是将其自身的数据端输出低电位,也就是向主机发出信号说自己刚收到一字节,并等待主机的回应;主机接下来的操作就是拉高自身的数据端口电位,再检测端口电位是否为低,如果检测到为低,则认为从机已经收到了一个字节,然后要告诉从机我知道了也就是给从机一个回应,回应的方法是将时钟线操作一个完整的总线时钟周期即先拉高再拉低;从器件在收到主器件的这个回应后,就将其自身数据端口的电位拉高即交出对数据线的控制权,这样就完成了一次完整的应答。
以上的时序是不能弄错的,比如主器件在发送完成一字节数据后,拉高数据线端以检测从器件的应答信号的操作,按I2C总线的规则必须先将时钟线拉低才能再改变数据线,而一字节从发送到完成应答,时钟线的操作只能是9个总线周期,这9个总线周期中减除最后一个完整的应答总线时钟周期,那这个拉低的时间只能是安排在第8个周期。
另外,我们可以做一个实验,就是监测数据线的电位高低变化,从实验结果来看,在写数据时,如果发送的数据的第0位为1的话,在时钟线第8个总线周期的高电位时,数据线的电位也为高电位;而接下来时钟线电位一旦变低,则数据线的电位也同时变为低电平,也就是说,这时从器件将对其自身的数据端口输出低电位,这个就是从器件在写入数据时的应答信号,这个信号是在第8个总线时钟周期的低电位时发出的,而非有些教材说的在第9个时钟周期时发出。

三、从发送方与接收方的角度来说明
对于i2c总线来说,在数据传输时,无论是主器件还是从器件,无论是读还是写,接收方在收到一字节数据后,规定是在时钟线处于低电位时,立即在其数据端输出低电平,而发送方则立即在其数据端口输出高电平,然后发送方对数据线的电位进行检测,所以,我们在编写程序时,对于读和写的应答,程序是不一样的,写入时,程序是将数据线拉高;读取时,是将数据线拉低。如果你在读取时仍编写成拉高数据线的电位,那就没法连续读取数据了。
以上操作之后,时钟线发出第9个周期来完成双方的互动式应答并复位主从器件的数据端口输出值,即接收方释放对数据线的控制权,将这个控制权交还给发送方,以便发送方向数据线输出数据。根据总线正常工作期间,时钟线处于高电位时不得改变数据线状态的规则,接收方只能在第9个周期的低电位期间向其自身的数据端口输出高电位。

四、从接收方应答的必须动作的角度来说明
我们再换一个角度来作说明,数据接收器件的应答有两个动作,一个是发出表示自己收到了一字节的信号,就是向其数据端口输出低电平,第二个动作是释放数据端口。这两个动作的发生时间均需由主器件的时钟线发出的信号来控制,其发生的时序只能分配在第8、第9这最后两个时钟周期中执行,具体是第一个动作在第8个时钟周期时执行,第二个动作是在第9个时钟周期时执行。按照总线正常工作期间,时钟线处于高电位时不得改变数据线的状态的原则,这两个动作只能在时钟线处于低电位时执行。
所以,读数据操作时,从器件在发送完成一字节数据后,在第8个总线时钟周期中时钟线处于低电位时,会立即释放数据线的控制权,然后等待主器件的继续读取或停止指令。

五、读指令发出后的应答机制
按照读取数据时,先在时钟线低电位时期放数据,再拉高时钟线进行读取的规定,则我们可以知道,在主器件读取第一个字节的操作中,一旦从器件收到一字节的读取指令,从器件就会与主器件互动进行应答,这个应答自然是按写状态进行操作,然后在第9个总线时钟周期的低电平时期,从器件就会把所要读取的这一字节数据的第7位数据放在数据线上,然后主器件就会拉高时钟线电位后进行读取,然后拉低时钟线以让从器件放上第6位数据,依此顺推。
读数据时,第一个应答信号中各动作后的电位变化如下:
i2cstart();//再次启动总线,开始读
I2cSendByte(0xa1); //向从器件写入读指令,读取某一存储单元中为10010110的数据。此动作后,数据线为低
   I2CSDA=1;delayms(1);//此动作后,数据线仍为低
I2CSCL=1;delayms(1);//此动作后,数据线仍为低
I2CSCL=0;delayms(1); //此动作后,数据线变为高,说明应答一完成,从器件即将第7位数据(为1)放在了数据线上
说明一下,以上试验,是本人在数据端口和时钟端口分别接了一个灯(串了限流电阻)进行观察所得。

六、关于非应答信号的质疑
前面说过了,数据发送方在发送完成一字节后,会立即释放对数据线的控制权,所以,有些教材中“当主器件接收数据时,在最后一个数据字节,必须发送一个非应答位,使受控器件释放数据线,以便主器件产生一个停止信号来终止总线数据传输”的说法是有问题的,因为最后一个字节发出去之后,受控器件已经主动释放了数据线。当然了,你也可以这样做,并不影响程序的运行,但我们不能这样理解。
我们知道,每一次的数据传输之后,从器件的存储器单元地址指针值都会加1,这个从器件存储单元地址值加1的时序时间,是在从器件发送或接收到一个完整的字节后就立即进行的,与有无应答操作无关,所以,读取最后一个数据后,我们根本无需进行所谓的非应答操作,而是可以直接发出停止信号退出总线。关于这一点,你可以进行试验,在直接退出后,紧跟着输入一个读指令进行读一个从器件单元中的数据的操作,一是看看这个退出是否是正常退出,二是看看读出的结果是上一次操作中的最后一个单元的数据还是下一个单元的数据。

七、中断应答
对于从器件这个情况比较少见,但也不是不可能发生,而主器件则不奇怪。
主器件对于自身中断的处理比较容易,没什么特殊的。
从器件在收到一字节数据后,如果暂时不想继续接收数据比如其要先处理中断,则可以将其时钟线置低电位以获取对时钟的控制权。因此,严格来说,主器件每向从器件发送完成一字节后,主器件都应该检测时钟线及数据线的电位,以判断从器件的状态并确定其下一步的动作。

分享到:  QQ好友和群QQ好友和群 QQ空间QQ空间 腾讯微博腾讯微博 腾讯朋友腾讯朋友
收藏收藏4 分享淘帖 顶 踩
回复

使用道具 举报

沙发
ID:266429 发表于 2021-10-26 21:15 | 只看该作者
读应答有以下四个动作:
i2csda=0;delayus(4);
   i2cscl=1;delayus(4);
   i2cscl=0;delayus(4);
   i2csda=1;delayus(4);
最后一个动作是主器件释放对数据线的控制权,千万别忘了。
回复

使用道具 举报

板凳
ID:262 发表于 2021-10-27 17:26 | 只看该作者
好资料,51黑有你更精彩!!!
回复

使用道具 举报

地板
ID:266429 发表于 2021-10-27 19:45 | 只看该作者
本篇有些内容,显然与教材是相冲突的,反正,要么教材错了,要么我错了,到底谁对谁错呢?希望大家吱个声。
回复

使用道具 举报

5#
ID:123289 发表于 2021-10-28 10:40 | 只看该作者
总结的很优秀,值得对I2C通讯认识模糊的人认真读一下。
给个
回复

使用道具 举报

6#
ID:401564 发表于 2021-10-28 11:26 | 只看该作者
二、向从器件写入时的应答机制
这个就是在第9个时钟周期的时候,输出应答位的,说破天了也是第9个
24Cxx是上升沿写入的,在写入完8个位的数据之后,SCL是低电平
然后把CSL拉高,这就是一个上升沿,就是第9个时钟周期,如果再把时钟拉低,那就是时钟周期已经完成,从机就不再响应了

至于释放数据线主要权什么,SDA=1;之类的,这仅仅只是8051端口的操作而已
有的别的单片机不是这样操作,不存在把数据线拉高的说话
先学会看时序图,配合时序看别人的代码,这就行了,IIC很简单
回复

使用道具 举报

7#
ID:123289 发表于 2021-10-28 15:00 | 只看该作者
【七、中断应答】这一点能考虑到的人不多。
在I2C通讯过程序中,无论是哪一方,发生了中断,中断服务返回后(可能执行了一段时间),如何确保双方的通讯接续上去而不受影响。是需要事先做预案的。
回复

使用道具 举报

8#
ID:266429 发表于 2021-10-29 11:49 | 只看该作者
Y_G_G 发表于 2021-10-28 11:26
二、向从器件写入时的应答机制
这个就是在第9个时钟周期的时候,输出应答位的,说破天了也是第9个
24Cxx是 ...

第9个周期是发送器件的回应周期,目的是通知接收器件释放对数据线的控制权;第8个周期才是接收器件的回应周期,告诉对方我收到一个字节了。
我都是站在这个总线协议设计者的角度来思考验证的。很疑惑教材的说法,反正我在读应答编程时,如果与写应答的程序相同的话,在AT24C上是没法连续读数据的。
回复

使用道具 举报

9#
ID:401564 发表于 2021-10-29 14:01 | 只看该作者
慢慢思考 发表于 2021-10-29 11:49
第9个周期是发送器件的回应周期,目的是通知接收器件释放对数据线的控制权;第8个周期才是接收器件的回 ...

你数一下是不是第9个

写入的时候一样的是第九个时钟教材是有人审核的,不可能几乎所有的教材都是错的
随便找个时序图来看,它也是第9个
是最后一个字节发送完之后,时钟是低电平,然后,时钟线拉高,之后是等待SDA低电平
24Cxx是上升沿写入,所以,这已经是第9个时钟了,你随便找一个IIC的写函数看一下是不是这样的
  • for(i = 0;i < 8;i++)
  • //        {
  • //                if(dat & 0x80)        IIC_SDA = 1;        //判断发送位,先发送高位
  • //                else        IIC_SDA = 0;
  • //                IIC_Delay();
  • //                IIC_SCL = 1;        //为SCL下降做准备
  • //                IIC_Delay();
  • //                IIC_SCL = 0;        //时钟是低电平
  • //                dat<<=1;
  • //        }
  •         IIC_SDA = 1;        //释放数据线
  •         IIC_Delay();
  •         IIC_SCL = 1;        //这是不是一个时钟的上升沿?,这就是第8个时钟完成之后的第9个时钟!!!
  •         IIC_Delay();
  •         iic_ack |= IIC_SDA;        //读入应答位
  •         IIC_SCL = 0;
  •         return iic_ack;        //返回应答信号


回复

使用道具 举报

10#
ID:624769 发表于 2021-10-29 17:17 | 只看该作者
Y_G_G 发表于 2021-10-29 14:01
你数一下是不是第9个

写入的时候一样的是第九个时钟教材是有人审核的,不可能几乎所有的教材都是错的

16 和17 应该交换一下吧?

虽然有个 Delay 在前面, 即便SCL高电平也已经能读到ACK了,但是从规范上来讲,应该先拉低时钟,然后再读ACK才可以吧?
回复

使用道具 举报

11#
ID:401564 发表于 2021-10-29 20:41 | 只看该作者
188610329 发表于 2021-10-29 17:17
16 和17 应该交换一下吧?

虽然有个 Delay 在前面, 即便SCL高电平也已经能读到ACK了,但是从规范上来 ...

低电平等于时钟完成了,读取不到电平状态,协议上也是高电平的时候读取的


回复

使用道具 举报

12#
ID:266429 发表于 2021-10-31 22:30 | 只看该作者
Y_G_G 发表于 2021-10-29 14:01
你数一下是不是第9个

写入的时候一样的是第九个时钟教材是有人审核的,不可能几乎所有的教材都是错的

我先问一句:按教材的做法,能连续读取数据么?这个实验本来极其简单,分分钟的事。
回复

使用道具 举报

13#
ID:266429 发表于 2021-11-1 10:17 | 只看该作者
Y_G_G 发表于 2021-10-29 14:01
你数一下是不是第9个

写入的时候一样的是第九个时钟教材是有人审核的,不可能几乎所有的教材都是错的

假设教材上写的方案行得通,如果让你来设计这个总线协议,你觉得哪种方案更合理?
总线协议,说起来总归是纯人为设计的东西,对于人为设计的东西,我们应该都可以质疑并找出更合理的方案,然后你自己就可以申请专利了,当然,I2C总线这么简单的东西,就不要想了,当初弄出这东西的人,都是一群高智商的人。
I2C总线的这个问题,本来只是个非常简单的问题,而且是个非常有意义的问题,没想到参与讨论的人这么少,只能叹口气了。
回复

使用道具 举报

14#
ID:401564 发表于 2021-11-1 10:49 | 只看该作者
慢慢思考 发表于 2021-11-1 10:17
假设教材上写的方案行得通,如果让你来设计这个总线协议,你觉得哪种方案更合理?
总线协议,说起来总归 ...

你看了我的代码图片没有,你怎么数,它都是第9个时钟
就算是你自己写的,只要是能连续读取的,它也是第9个时钟,你把你的代码文件上传,C也行,汇编也行
我帮你改注释,我让你找出第9个出来
协议这种东西,你怎么设计才是合理的?
你搞低作为应答,就有人就会问"你为什么不把高电平作为应答呢?"
那我高电平作为应答,那还是有人会问"你为什么不把低电平作为应答呢?
回复

使用道具 举报

15#
ID:266429 发表于 2021-11-1 16:36 | 只看该作者
Y_G_G 发表于 2021-11-1 10:49
你看了我的代码图片没有,你怎么数,它都是第9个时钟
就算是你自己写的,只要是能连续读取的,它也是第9个时 ...

当然看过了,类似的代码另外也看了不少。我手上提到I2C的纸质书就有四本,其中两本的有关应答的时序图中,在第8个时钟周期时钟线的低电位时,已有明确的应答低电位信号;一本则是第8周期时钟线下降沿末端,数据线电位开始下降,在第9周期前达到低电位;最后一本则是含混不清,似乎是在第9周期时钟线上升沿中达到低电位,真够乱套的。
至于应答是用高电平还是低电平,这个I2C总线规格设计是这样考虑的:器件在其端口输出高电平,就意味着其交出对线路的控制权,所以,用低电平作应答信号。
至于我编的程序,二楼有读出操作的应答程序。
下面是程序的主要部分,请指教:
void i2cstart()//开始程序,其实应分为开机初始化部分与总线开始部分这两个子函数
{
   i2cscl=1;delayus(4);
   i2csda=1;delayus(4);
   i2csda=0;delayus(4);
   i2cscl=0;delayus(4);
}
//////
void i2cend()//结束
{
    i2csda=0;delayus(4);
        i2cscl=1;delayus(4);
        i2csda=1;delayus(4);
}
//////
void i2cwack()//写入应答
{
           i2csda=1; delayus(4);//执行后数据线状态为低,说明从器件的应答信号已发出
        i2cscl=1;delayus(4);//数据线状态为低
           i2cscl=0;delayus(4);//数据线状态为高
}
//////
void i2crack()//读出应答
{
   i2csda=0;delayus(4);
   i2cscl=1;delayus(4);
   i2cscl=0;delayus(4);
   i2csda=1;delayus(4);//主器件释放对数据线的控制权
}
///////
void i2cwritebyte(unsigned char wdat)//写入一字节,加入应答
{          
    unsigned char i;
        P2=wdat;
        for(i=0;i<8;i++)
        {
           i2csda=0x01&wdat>>7;
           wdat=wdat<<1;
          
           i2cscl=1;delayus(4);
           i2cscl=0;delayus(4);
        }//完成后数据线状态为低,与被输入该字节数据0位的值无关
         i2cwack();
}
///////
unsigned char i2creadbyte()//读出一字节,加入应答
{
      
          unsigned char rdat=0,i;
          for(i=0;i<8;i++)
          {
          i2cscl=1;delayus(4);
          rdat=rdat<<1|i2csda;
          i2cscl=0;delayus(4);
          }                   //读出来的这一字节数据最后一位无论是0还是1,此时数据线状态均为1
           i2crack(); //应答完成后数据线状态与下一待读字节的最高位一致
           return(rdat);
}

//////
void main()//连续读两个字节并送至P2口
{       
         i2cstart();
         i2cwritebyte(0xa0);
         i2cwritebyte(0x01);
         i2cend();

    i2cstart();
        i2cwritebyte(0xa1);
        P2=i2creadbyte();
        delayms(2000);
        P2=i2creadbyte();
        i2cend();

        while(1);
}
注:读出来的数据直接送到P2口,其上接有8只灯;数据线与时钟线上各接有一只灯用以观察实验过程中端口电位。
写入应答有三个动作,其三个动作分别执行后各自对应的数据线的状态实测值已在程序中标注,由此可知写入时从器件应答信号具体发出的时序时间。
读数据时,最后一字节数据没有用非应答,用的依然是应答,然后结束。
回复

使用道具 举报

16#
ID:975504 发表于 2021-11-1 16:49 | 只看该作者
51黑有你更精彩,感谢!
回复

使用道具 举报

17#
ID:401564 发表于 2021-11-1 18:23 | 只看该作者
慢慢思考 发表于 2021-11-1 16:36
当然看过了,类似的代码另外也看了不少。我手上提到I2C的纸质书就有四本,其中两本的有关应答的时序图 ...

以你的代码为例
void i2cwritebyte(unsigned char wdat)//写入一字节,加入应答
{         
    unsigned char i;
        P2=wdat;
        for(i=0;i<8;i++)
        {
           i2csda=0x01&wdat>>7;
           wdat=wdat<<1;
         
           i2cscl=1;delayus(4);
           i2cscl=0;delayus(4);//最后时钟线是低电平,第8个时钟已经结束
        }
         i2cwack();//应答中时钟线是高电平,这就是第9 个时钟
}
但是,你的应答并不对
应答是要等待SDA出现低电平,而不是简单延时一下
这个SDA的低电平是24C02给出的
理论上应该是:
        SCL=1;//时钟高电平,保持从机在第9个时钟                          
        Delay();//延时
        SDA=1;//释放SDA
        while(SDA) ;等待从机出现应答,重点在这里,延时是不行的,必需得是等待,这是协议规定的


但是,在实际情况中,考虑从机有故障或者什么的,可能不会应答,while(SDA) ;会卡死
所以,可以使用:
while((SDA==1)&(k<1000))         //超时就不再等待应答
                {
                        k++;
                        Delay();
                }




而你的程序,本身就是错误的:
void i2cwack()//写入应答
{
         i2csda=1; delayus(4);//执行后数据线状态为低,说明从器件的应答信号已发出
        i2cscl=1;delayus(4);//数据线状态为低
        i2cscl=0;delayus(4);//数据线状态为高
}
应该是:
void i2cwack()//写入应答
{
        i2csda=1; delayus(4);
        i2cscl=1;delayus(4);        while(i2csda);         //这里要等待,不是延时,重点!重点!重点!可以加入超时检测退出,防止卡死
        i2cscl=0;delayus(4);
}
而且,IIC停止读取之前要加一定不应答信号,这个信号要由单片机给出,告诉从机,不要再发送数据了
这个信号不是绝对需要,有的器件你直接停止就可以了,但有的不行,你要给出不应答才能正确读取下一次的数据
像你的代码,能正常就是运气好,因为有的IIC器件硬件电气性能很好,它的反应比单片机还快,它可能压根就不需要应答
回复

使用道具 举报

18#
ID:266429 发表于 2021-11-1 19:41 | 只看该作者
Y_G_G 发表于 2021-11-1 18:23
以你的代码为例
void i2cwritebyte(unsigned char wdat)//写入一字节,加入应答
{         

void i2cwritebyte(unsigned char wdat)//写入一字节,加入应答
{         
    unsigned char i;
        P2=wdat;
        for(i=0;i<8;i++)
        {
           i2csda=0x01&wdat>>7;
           wdat=wdat<<1;
         
           i2cscl=1;delayus(4);
           i2cscl=0;delayus(4);//最后时钟线是低电平,第8个时钟已经结束,但结束时数据线已经是低电平
        }
         i2cwack();//应答中时钟线是高电平,这就是第9 个时钟。是一高一低,低时数据线恢复高电平
}


SCL=1;//时钟高电平,保持从机在第9个时钟                          
        Delay();//延时
        SDA=1;//释放SDA
这里的延时,并不是用来等待从器件给出应答信号,而是为了让时钟线的高电位稳定一下。至于从器件的拉低数据线的应答信号,我在前面的实验已经明确指出,它在这个SCL=1执行之前,就已经发出了。原则上,在时钟线处于高电位时,是不允许改变数据线状态的,所以,在SCL=1执行之后,无论是从器件还是主器件,都不能对数据端口进行操作。
对应的,在读取数据的程序编写中,主器件的应答信号也就是拉低数据线的操作,也是在应答期SCL=1的操作之前进行的,也就是一个字节数据读完、SCL=0之后就立即执行SDA=0这个操作,只能这样安排顺序。

至于我的程序中的“错误”,是因为仅是一个试验应答规格的程序,所以嘛,没有设计专门的程序来检测,而是用人眼观察数据线上灯的亮灭。
回复

使用道具 举报

19#
ID:401564 发表于 2021-11-1 23:32 | 只看该作者
慢慢思考 发表于 2021-11-1 19:41
void i2cwritebyte(unsigned char wdat)//写入一字节,加入应答
{         
    unsigned char i;

太神奇了
错误就是错误,
1,到底是不是我说的第9个时钟?还是像你说的第8个?

2,我没有看到你代码中有等待应答的指令,一个没有等待应答的IIC,它能叫IIC吗?错误还加引号,敢情你还觉得这没有等待应答的IIC代码是对的?
你要说其它事,可能在每个人心里都有不一样的看法,我觉得是对的,你也可以觉得是错误的,我有我的标准,你有你的标准

但这不一样,这是单纯的技术问题,它是有对错的,有标准的
我可以肯定的告诉你:你这个IIC代码是错误的,放哪都是错误的,别加引号你告诉我一下,你等待从机应答的代码在哪里??????等待!等待!等待!
回复

使用道具 举报

20#
ID:266429 发表于 2021-11-2 08:41 | 只看该作者
Y_G_G 发表于 2021-11-1 23:32
太神奇了
错误就是错误,
1,到底是不是我说的第9个时钟?还是像你说的第8个?

其实第一个问题,是第8个还是第9个,这个就是个怎么计数的问题,各人看法不一样,无所谓,可以确定的是,在写入操作中,最后一位数放在数据线上,时钟线然后拉高读取数据,然后再拉低后,从器件就将数据线拉成低电位了。
第二个问题嘛,正常编程当然不能这样,我这本是根据我这个实验的具体情况偷了个懒省了这一步,加个引号并不是认为这不是错误。实际上这种做法也是可以通过的,它在功能单一且对运行速度无太多要求的场合中,你的每一步之间的延时足够长比如10ms,基本上出错的可能性也不大,但这样绝对不规范。
回复

使用道具 举报

21#
ID:976973 发表于 2021-11-2 11:57 | 只看该作者
好资料,51黑有你更精彩!!!
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

手机版|小黑屋|51黑电子论坛 |51黑电子论坛6群 QQ 管理员QQ:125739409;技术交流QQ群281945664

Powered by 单片机教程网

快速回复 返回顶部 返回列表