• 欢迎访问我的博客,推荐使用最新版火狐浏览器和Chrome浏览器访问本网站
  • 本网站关闭了评论功能,联系请点击→邮箱
  • Ctrl+D 可快捷收藏本站点

关于IIC总线使用示波器采集只观察到一半VCC电平的现象

C/C++ gql 3年前 (2022-02-17) 1831次浏览
文章目录[隐藏]

今天同事发现了一个有意思的现象,IIC主机发送8bit数据后,从机回复一个低电平。但是示波器却采集到了一个只有一半电压的电平。先上电路图。

IIC电路图没啥问题,再上发现问题时的时序图,如下。

首先我们先简单回顾一下IIC时序。SCL,SDA高电平总线空闲。SCL保持高电平时,SDA由高->低跳变为起始信号。SDA由低->高为结束信号。传输数据时只能在SCL为低电平是进行变化。SCL为高电平时SDA电平不变,对SDA采样。

一 看时序图

先看看时序图,红色为SCL,白色为SDA。先起始信号,再发送8个bit数据,这个时候从机应该回复低电平表示接收到了数据。但是我们发现上面出现一个一半VCC电压的方波。且在发送第三个字节的时候发现这个半波持续时间很长。这里就引出了两个问题

  1. 为什么 出现一个波形电平只有3.3V的一半左右。
  2. 这个半波有些长有些短。

二 看代码

首先初始化IIC的初始化,这里就发现了一个问题GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;GPIO的工作模式配置为了推挽输出。显然这里有问题,如果总线上的两个设备都是开漏输出时,处于总线空闲,两个都是高电平,问题不大,如果一个设备想要占用总线就要拉低SDA,那么这个时候就会造成短路。如下,VDD和地基本就算短接了,这个时候mos管只有不到1Ω的电阻不到,电流很大。所以一般总线设备都使用开漏输出。这时输出逻辑0,则N-MOS激活,输出低电平; 输出逻辑1,N-MOS不会激活, 不会输出高电平。这就需要一个外接上拉电阻,在输出输出逻辑1,IO也能输出高电平,这也是上面IIC电路图的原理。

回到问题1上,那么电压的一半又是怎么来的呢,当主机配置为推挽输出时,从机为开漏输出,当主机发送一个字节(8bit)数据后,从机回复一个ACK信号(SCL电平期间拉低SDA),主机等待从机的ACK信号,为了防止主机发送的最后一个bit为0,造成误以为收到从机的ACK,所以主机发送完数据后会拉高SDA。在等待ACK信号。查看代码如下

static uint8_t I2C_Wait_Ack(void)
{
    u8 i = 0;
    I2C_SDA_IN();   //配置为上拉输出
    I2C_SDA_WRITE = 1; //拉高SDA
    delay_us(1);
    I2C_SCL = 1;
    delay_us(1);
    while(I2C_SDA_READ)
    {
        i++;
        if(i > 250)
        {
            I2C_Stop();
            return 1;
        }
    }
    I2C_SCL = 0;
    return 0;
}ddisanb

三 看手册

这时,为了检测从机的ACK,程序配置为了上拉输入,又去控制ODR寄存器,拉高了SDA。查看手册IO端口的原理图

这里主机推挽输出,从机开漏输出,mos导通后等效为一个小电阻,不到1Ω,通过与IIC电路结合,在简化电路如下图左所示,我们可以认为推挽输出mos管等效电阻和上拉电阻并联,但是与上拉电阻差距过大,几乎可以忽略。再次简化就如右图所示,所以测量总线电压时,只有一半,就是两个电阻对3.3V分压。这个时候就是前面推挽输出提到的短路了,因为电阻很小,电流就会很大。但是时序图可以看出,其实持续时间就几个us,所以吗,没有烧坏电路,如果这时因为一些程序处理的问题,造成这里等待太久的时间,就可以会烧毁电路。

 

四 再看时序图

这时我们已经知道电压只有一半的原因了,通过分析我们可以得到,在主机发送一个字节后,最后一个bit为0,这时从机在SCL拉低后立即回复,也处于低电平,所以保持了一段时间的低电平。这时主机再次拉高SDA推挽输出高电平,而从机开漏输出电平,示波器就采集到一半的电压,而第三个字节最后一个bit为1推挽输出高电平,从机在SCL拉低后立即回复开漏输出电平,所以立马出现半波。这就是第二个问题的原因了。但是新的问题又出现了,查看程序I2C_SDA_WRITE = 1; //拉高SDA 。这里只拉高了SDA,并没有拉低SDA,为何半波只持续了一瞬间呢?而且为何设置为输入模式后还可以继续输出呢?

五 再看代码

带着疑问继续查看代码,在代码中,我们并没有发现主机再次主动拉低SDA。在配置完GPIO为上拉输入后,通过配置ODR寄存器设置IO口输出1。这就很奇怪,手册清楚的描述了在设置为输入模式后,输出被禁用。写一个测试代码找了一个没有使用的GPIO,初始化IO为上拉输入模式。定时读取GPIO状态,如果为高电平则打开LED灯,且拉低该GPIO(GPIO_ResetBits()),如果为低电平则关闭LED,且拉高该GPIO(GPIO_SetBits())。通过查看LED状态,确认是否设置高低电平成功。最后LED竟然真的开始闪烁。到这里我就开始怀疑手册写错了。但是这个问题应该很常见才对,为什么网上也没有资料呢?

六 再看手册

没办法只有再继续查看手册,看看是否有什么遗漏的。直到我发现这一张表。

如果看了STM32手册或者源码的都应该知道,GPIO可以通过设置BSRR、BRR、ODR这三个寄存器来控制GPIO输出高低电平。而我们使用GPIO_ResetBits和GPIO_SetBits这些库函数就是控制的这些寄存器。但是很少人知道ODR也可以用来配置输入模式下的上拉电阻和下拉电阻。而ODR寄存器也说明了对GPIOx_BSRR(x = A…E),可以分别地对各个ODR位进行独立的设置/清除。知道这些知识点后,我就明白了,其实调用GPIO_SetBits函数后并不是输出了高电平。而是,设置为上拉输入,由于有上拉电阻,对外采集到高电平。GPIO_ResetBits同理设置为下拉。简化如下,这时候采集的只是输入内部的上拉电阻/下拉电阻后的,并不是输出的电平信号。只是我们一般通过库函数配置GPIO为输入模式时,没有去查看控制的那个寄存器,而配置完后,也不会使用GPIO_SetBits等函数去让输入口输出,所以就没有发现这个问题所在。

第七步 再看源码

原理已经知道了,设置为上拉输入后,I2C_SDA_WRITE = 1; 只是去设置ODR,但是这个时候,该位已经是1,代表上拉电阻。那这一句话就没有任何意义了呀。那拉高的那个电平信号来自哪呢? 这时候就要查看源码了。GPIO的配置是如何完成的。请记住在配置前

GPIO还是处于推挽输出模式
。查看库函数GPIO_Init()实现,我们可以看到这么一段代码(F1的GPIO配置分为高8位和低8位,内容是相同的),我们这里看低8位的。

 
  /* Configure the eight low port pins */
  if (((uint32_t)GPIO_InitStruct->GPIO_Pin & ((uint32_t)0x00FF)) != 0x00)
  {
    tmpreg = GPIOx->CRL;
    for (pinpos = 0x00; pinpos < 0x08; pinpos++)
    {
      pos = ((uint32_t)0x01) << pinpos; /* Get the port pins position */ currentpin = (GPIO_InitStruct->GPIO_Pin) & pos;
      if (currentpin == pos)
      {
        pos = pinpos << 2;
        /* Clear the corresponding low control register bits */
        pinmask = ((uint32_t)0x0F) << pos;
        tmpreg &= ~pinmask;
        /* Write the mode configuration in the corresponding bits */
        tmpreg |= (currentmode << pos); /* Reset the corresponding ODR bit */ if (GPIO_InitStruct->GPIO_Mode == GPIO_Mode_IPD)
        {
          GPIOx->BRR = (((uint32_t)0x01) << pinpos); 
        } 
        else 
        {
          /* Set the corresponding ODR bit */ 
          if (GPIO_InitStruct->GPIO_Mode == GPIO_Mode_IPU)
          {
            GPIOx->BSRR = (((uint32_t)0x01) << pinpos); 
           } 
        }
      } 
     }
     GPIOx->CRL = tmpreg;
  }

从这里看到如果是配置为GPIO_Mode_IPU上拉输入模式,GPIOx->BSRR = (((uint32_t)0x01) << pinpos),就会去控制相应位置的BSRR(这就是前面说输入模式下的去控制ODR寄存器达到控制输入上拉下拉),再修改GPIOx->CRL寄存器,达到修改工作模式的效果。但是我们要记得在修改BSRR寄存器这个时候我们还处于推挽输出模式中,首先改变了GPIOx->BSRR ,就相当于输出设置推挽输出高电平。再修改为GPIOx->CRL寄存器,配置为输入模式,这个时候ODR寄存器才对上下拉生效。最后在使用I2C_SDA_WRITE = 1; 其实这时候已经是上拉了,这一句没有意义了。

八 总结

出现上述原因总结下来就是,IIC主机被配置为推挽输出,在从机回复ACK时开漏输出低电平,为了检测SDA状态,IO又被配置数上拉输入,但是库函数先配置了ODR寄存器(在输出时控制输出高低电平,输入数控制上下拉电阻),造成推挽输出高电平。而这时造成两个mos管导通分压,一段VCC一段GND(电阻很小可以认定为短路了),造成只有一半的电压检测到。

一般来说,总线为了防止过流都应当采用开漏输出模式,并外接上拉电阻。


如未注明 , 均为原创。转载请注明原文链接:关于IIC总线使用示波器采集只观察到一半VCC电平的现象
喜欢 (6)