1.I2C介绍
I2C是一种多主机、两线制、低速串行通信总线,广泛用于微控制器和各种外围设备之间的通信。它使用两条线路:串行数据线(SDA)和串行时钟线(SCL)进行双向传输。
2.时序
启动条件:SCL高电平时、SDA由高电平变为低电平
停止条件:SCL高电平时、SDA由低电平变为高电平
除此之外,不允许在SCL高电平时变换SDA电平。
数据传输:
主机发送起始信号后,通信开始,每次传输一字节数据,传输完毕后应答(ACK)
主机拉低SCL电平,此时可改变SDA的电平
主机拉高SCL电平,此时开始读取SDA电平
如此重复8次,一字节数据传输完毕,数据接收方在下一个CLK低电平在SDA上进行应答,0代表数据接收成功,1数据接收失败或通信结束。
3.通信时序
如图,主机发送完起始信号后,再次拉低SCL,并改变SDA的电平为0或1,电平改变完成后,主机拉高(释放)SCL,代表此时从机可以读取SDA数据,之后主机再拉高SLC重复操作,如此重复8次,就发送了一个字节。
主机发送完字节最后一位数据后,拉高(释放)SCL等待从机读取完成,然后拉低SCL(代表主机准备好接受ACK)并且拉高(释放)SDA,将SDA的控制权交给从机,此时收到该数据的从机会对SDA进行操作,0(拉低SDA)代表存在从机收到该字节数据,可以继续发送数据,1(拉高,或者无从机操作SDA)代表从机数据接收完毕或根本没有从机,主机发送停止信号结束通信。
读取与写入数据
仔细研究以上通信时序,我们发现时序为
Start(起始信号)+8位数据+ACK(应答)+Stop(结束信号)
那么我们怎么知道主机跟从机通信是要读取数据还是写入数据呢?
实际使用中,我们会按照约定格式进行通信。
①读取数据
a.主机发送一个起始信号Start
这时通信总线上的设备准备接受数据
b.接着发送7位从设备ID+读写位(0写1读)
从机开始解析数据,发现该ID是自己ID那就发送ACK(0,代表有从机对SDA进行了操作)拉低SDA线,继续进行通信。
如果SDA保持1,即没有任何从机作出反应,那主机就发送Stop信号结束通信
c.发送要访问的从设备寄存器地址
主设备告诉从设备我要访问你该地址的数据,从设备收到地址后再次回应一个ACK(拉低SDA,告诉主设备我收到地址数据了)
d.根据之前发送发送的读写位进行数据交换
如果是7位地址+0,即写操作,则数据为主机发送给从机,从机接收到后回复一个ACK(0,即有从机拉低SDA),直到主机发送完成,产生一个Stop信号。
如果是7位地址+1,即读操作。则此时数据为从机发送给主机,主机接收完后向从机发送ACK位,当主机读取完成后就产生一个NACK(1),之后发送Stop信号结束通信。
e.产生一个结束信号Stop
主机在SCL高时拉高SDA(本质上为释放SCL与SDA两条线路,即不对该两条线路产生任何操作)
线与特性:
I2C的时钟线(SCL)与数据线(SDA)都使用开漏输出(OD)+上拉电阻的方式进行驱动,即线上连接的IO口全都只有拉低电平的能力,当线上连接的任意一个引脚输出低电平时,整条线路都会接地处于低电平状态。而IO口输出高电平时,引脚处于浮空状态,此时线上为高电平,但只要线上连接的任意一个IO口处于低电平,那整条线路都会处于低电平。
主机引脚电平 | 从机引脚电平 | 传输线电平 |
---|---|---|
0 | 0 | 0 |
1 | 0 | 0 |
0 | 1 | 0 |
1 | 1 | 1 |
为何使用开漏输出上拉电阻?
既然都是简单的电平变换,那为什么I2C要使用开漏输出呢?直接使用推挽输出,不但省下一个上拉电阻,IO口还不会因上拉电阻影响到传输速率不是更好吗?这是我们就要仔细研究一下I2C时序了。
I2C因为被设计运用在多主机多从机的场景,所以不可避免地会遇到一个问题——总线冲突。
若因某种原因时序混乱导致一个IO口输出高电平,一个输出低电平,那么就会出现短路现象,有烧坏设备的风险。
正点原子软件模拟I2C引脚使用推挽输出?
那么问题来了,正点原子例程中软件模拟I2C的GPIO配置代码如下:
GPIO_Initure.Pin=GPIO_PIN_4|GPIO_PIN_5; GPIO_Initure.Mode=GPIO_MODE_OUTPUT_PP; GPIO_Initure.Pull=GPIO_PULLUP; GPIO_Initure.Speed=GPIO_SPEED_FREQ_VERY_HIGH;
我们惊讶的发现,他们将引脚的输出方式设置为了推挽输出,这是为什么?
查阅了资料,理由如下:
1.该例程的使用场景为单主-单从,即整条线上只有一个主机、一个从机,SCL不存在争抢控制权的现象。
2.在主机交出SDA控制权时,SDA引脚被设置为了输入模式
u8 IIC_Read_Byte(unsigned char ack) { unsigned char i,receive=0; SDA_IN();//SDA输入模式 for(i=0;i<8;i++ ) { IIC_SCL(0); delay_us(2); IIC_SCL(1); receive<<=1; if(READ_SDA)receive++; delay_us(1); } if (!ack) IIC_NAck(); else IIC_Ack(); return receive; }
输入模式时,主机SDA引脚浮空,只对SDA上的电平进行读取,此时从机改变SDA上的电平是安全的。
综上,使用推挽输出只适用于单主从模式,并且该方式需要对SDA输入输出状态进行切换。
问题探究:
1.推挽模式读取引脚状态需要改变引脚输入输出状态,开漏难道不需要吗?
请看GPIO基本构造:
如图所示STM32引脚输出靠下方的两个MOS管进行强上拉、下拉。当使用推挽输出时会用到两个MOS管,无论输出高电平还是低电平会是引脚保持强上拉、下拉状态,因此引脚输出高电平就会读到高电平,引脚输出低电平就会读到低电平。
而使用开漏输出时只会用到下方连接GND的NMOS管,也就是说该模式只有强下拉,如果给引脚输出高电平,那么此时引脚实际处于浮空状态,受连接线上的电平影响,此时读取的电平即为连接线上的电平。