今天同事发现了一个有意思的现象,IIC主机发送8bit数据后,从机回复一个低电平。但是示波器却采集到了一个只有一半电压的电平。先上电路图。
IIC电路图没啥问题,再上发现问题时的时序图,如下。
首先我们先简单回顾一下IIC时序。SCL,SDA高电平总线空闲。SCL保持高电平时,SDA由高->低跳变为起始信号。SDA由低->高为结束信号。传输数据时只能在SCL为低电平是进行变化。SCL为高电平时SDA电平不变,对SDA采样。
一 看时序图
先看看时序图,红色为SCL,白色为SDA。先起始信号,再发送8个bit数据,这个时候从机应该回复低电平表示接收到了数据。但是我们发现上面出现一个一半VCC电压的方波。且在发送第三个字节的时候发现这个半波持续时间很长。这里就引出了两个问题
- 为什么 出现一个波形电平只有3.3V的一半左右。
- 这个半波有些长有些短。
二 看代码
首先初始化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的配置是如何完成的。请记住在配置前
/* 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(电阻很小可以认定为短路了),造成只有一半的电压检测到。
一般来说,总线为了防止过流都应当采用开漏输出模式,并外接上拉电阻。