1.I2C
- I2C总线(Inter l0 BUs)是由Philips公司开发的一种通用数据总线
- 两根通信线:SCL(Serial Clock)、SDA(Serial Data)
- 同步,半双工
- 带数据应答
- 支持总线挂载多设备(一主多从、多主多从)
MPU6050模块:可以进行姿态测量,使用了12C通信协议
第3个图片是AT24C02,存储器模块
第4个图片是DS3231,实时时钟模块
2.硬件电路
- 所有I2C设备的SCL连在一起,SDA连在一起
- 设备的SCL和SDA均要配置成开漏输出模式
- SCL和SDA各添加一个上拉电阻,阻值一般为4.7KQ左右
这个图就是I2C的一个典型电路模型,这是一个一主多从的模型,左边CPU就是我们的单片机,作为总线的主机,主机的权力很大,包括,对SCL线的完全控制,任何时候,都是主机完全掌控SCL线,另外在空闲状态下,主机可以主动发起对SDA的控制,只有在从机发送数据和从机应答的时候,主机才会转交SDA的控制权给从机,这就是主机的权力
然后看下面,这一系列都是被控IC,也就是,挂载在I2C总线上的从机,这些从机可以是姿态传感器、OLED、存储器、时钟模块等,从机的权利比较小,对于SCL时钟线,在任何时刻都只能被动的读取,从机不允许控制SCL线,对于SDA数据线,从机不允许主动发起对SDA的控制,只有在主机发送读取从机的命令后,或者从机应答的时候,从机才能短暂地取得SDA的控制权,这就是一主多从模型中协议的规定
然后看接线,这里第一条写的是,所有I2C设备的SCL连在一起,SDA连在一起,这就是接线要求,在这个图里可以看到,主机SCL线一条拽出来,所有从机的SCL都接在这上面,主机SDA线也是一样,拽出来,所有从机的SDA接在这上面,这就是SCL和SDA的接线方式
先忽略上面两个电阻,假设我们就这样连接,那如何规定每个设备SCL和SDA的输入输出模式呢,SCL应该好规定,因为现在是一主多从,主机拥有SCL的绝对控制权,所以主机的SCL可以配置成推挽输出,所有从机的SCL都配置成浮空输入或者上拉输入,数据流向是,主机发送,所有从机接收,但是到SDA线这里,就比较麻烦了,因为这是半双工的协议,所以主机的SDA在发送的时候是输出,同样,从机的SDA也会在输入和输出之间反复切换,如果你能协调好输入输出的切换时机,那其实也没问题,但是这样做,如果总线时序没协调好,极有可能发生两个引脚同时处于输出的状态,如果这时又正好是一个输出高电平,一个输出低电平,那这个状态就是电源短路,这个状态是要极力避免的,所以为了避免总线没协调好导致电源短路这个问题,12C的设计是,禁止所有设备输出强上拉的高电平,采用外置弱上拉电阻加开漏输出的电路结构,这两点规定就是上面的这两条,设备的SCL和SDA均要配置成开漏输出模式,SCL和SDA各添加一个上拉电阻,阻值一般为4.7KQ左右
所有的设备,包括CPU和被控IC,它引脚的内部结构都是右图这样的,左边这一块是SCL的结构,这里SCLK就是SCL的意思,右边这一块是SDA的结构,这里DATA就是SDA的意思,首先引脚的信号进来,都可以通过一个数据缓冲器或者是施密特触发器,进行输入,因为输入对电路没有任何影响,所以任何设备在任何时刻都是可以输入的,但是在输出的这部分,采用的是开漏输出的配置,图为推挽输出
开漏输出就是去掉这个强上拉的开关管,输出低电平时,下管导通,是强下拉,输出高电平时,下管断开,但是没有上管了,此时引脚处于浮空的状态,这就是开漏输出
和这里图示是一样的,输出低电平,这个开关管导通,引脚直接接地,是强下拉,输出高电平,这个开关管断开,引脚什么都不接,处于浮空状态,这样的话,所有的设备都只能输出低电平而不能输出高电平,为了避免高电平造成的引脚浮空,这时就需要在总线外面,SCL和SDA各外置一个上拉电阻,这是通过一个电阻拉到高电平的,所以这是一个弱上拉
用我们之前将的弹簧和杆子的模型来解释就是,SCL或SDA就是一根杆子,为了防止有人向上推杆子,有人向下拉杆子,造成冲突,我们就规定,所有的人,不准向上推杆子,只能选择往下拉或者放手,然后,我们再外置一根弹簧向上拉,你要输出低电平,就往下拽,这根弹簧肯定拽不赢你,所以弹簧被拉伸,杆子处于低电平状态,你要输出高电平,就放手,杆子在弹簧的拉力下,回弹到高电平,这就是一个弱上拉的高电平,但是完全不影响数据传输,这样做有什么好处?
- 第一,完全杜绝了电源短路现象,保证电路的安全,你看所有人无论怎么拉杆子或者放手,杆子都不会处于一个被同时强拉和强推的状态,即使有多个人同时往下拉杆子,也没问题
- 第二,避免了引脚模式的频繁切换,开漏加弱上拉的模式,同时兼具了输入和输出的功能,你要是想输出,就去拉杆子或放手,操作杆子变化就行了,你要是想输入,就直接放手,然后观察杆子高低就行了,因为开漏模式下,输出高电平就相当于断开引脚,所以在输入之前,可以直接输出高电平,不需要再切换成输入模式了
- 第三,就是这个模式会有一个“线与”的现象,就是只要有任意一个或多个设备输出了低电平,总线就处于低电平,只有所有设备都输出高电平,总线才处于高电平,12C可以利用这个电路特性,执行多主机模式下的时钟同步和总线仲裁,所以这里SCL虽然在一主多从模式下可以用推挽输出,但是它仍然采用了开漏加上拉输出的模式,因为在多主机模式下会利用到这个特征
3.I2C时序基本单元
起始和终止,都是由主机产生的,从机不允许产生起始和终止,所以在总线空闲状态时,从机必须始终双手放开,不允许主动跳出来去碰总线,如果允许的话,那就是多主机模型了
高电平期间,SDA不允许变化,SCL处于高电平之后,从机需要尽快地读取SDA,一般都是在上升沿这个时刻,从机就已经读取完成了,因为时钟是主机控制的,从机并不知道什么时候就会产生下降沿了,你从机要是磨磨唧唧的,主机可不会等你的,所以从机在上升沿时,就会立刻把数据读走,那主机在放手SCL一段时间后,就可以继续拉低SCL,传输下一位了,主机也需要在SCL下降沿之后尽快把数据放在SDA上,但是主机有时钟的主导权,所以主机并不需要那么着急,只需要在低电平的侄意时刻把数据放在SDA上就行了
另外注意,这里是高位先行,所以第一位是一个字节的最高位B7,然后依次是次高位B6,这个和串口不一样,另外,由于这里有时钟线进行同步,所以如果主机一个字节发送一半,突然进中断了,不操作SCL和SDA了,那时序就会在中断的位置不断拉长,SCL和SDA电平都暂停变化,传输也完全暂停,等中断结束后,主机回来继续操作,传输仍然不会出问题,这就是同步时序的好处
最后就是,由于这整个时序是主机发送一个字节,所以在这个单元里,SCL和SDA全程都由主机掌控,从机只能被动读取,这就是发送一个字节的时序
刚才我们说了,释放SDA其实就相当于切换成输入模式,或者这样来理解,所有设备包括主机都始终处于输入模式,当主机需要发送的时候,就可以主动去拉低SDA,而主机在被动接收的时候,就必须先释放SDA,不要去动它,以免影响别人发送,因为总线是线与的特征,任何一个设备拉低了,总线就是低电平,如果你接收的时候,还拽着SDA不放手,那别人无论发什么数据,总线都始终是低电平,你自己给它拽着不放,还让别人怎么发送呢,是吧,所以主机在接收之前,需要释放SDA
当我们在调用发送一个字节之后,就要紧跟着调用接收应答的时序,用来判断从机有没有收到刚才给它的数据,如果从机收到了,那在应答位这里,主机释放SDA的时候,从机就应该立刻把SDA拉下来,然后在SCL高电平期间,主机读取应答位,如果应答位为0,就说明从机确实收到了,这个场景就是,主机刚发送一个字节,然后说,有没有人收到啊,我现在把SDA放手了哈,如果有人收到的话,你就把SDA拽下来,然后主机高电平读取数据,发现,唉,确实有人给它拽下来了,那就说明有人收到了,如果主机发现,我松手了,结果这个SDA就跟着回弹到高电平了,那就说明没有人回应我,或者它收到了但是没给我回应,这就是发送一个字节,接收应答的流程
同理,在接收一个字节之后,我们也要给从机发送一个应答位,发送应答位的目的是告诉从机,你是不是还要继续发,如果从机发送一个数据后,得到了主机的应答,那从机就还会继续发送,如果从机没得到主机的应答,那从机就会认为,啊,我发送了一个数据,但是主机不理我,可能主机不想要了吧,这时从机就会乖乖地释放SDA,交出SDA的控制权,防止干扰主机之后的操作,这是应答位的执行流程
4.I2C时序
4.1指定地址写
起始条件开始后,主机必须首先调用发送一个字节,来进行从机的寻址和指定读写标志位,如图示的波形,表示本次寻址的目标是1101000的设备,同时,最后一位读写标志为0,表示主机接下来想要发送数据,接着从机发送应答,
在这个时刻,主机要释放SDA,所以如果单看主机的波形,应该是这样,释放SDA之后,引脚电平回弹到高电平,
但是根据协议规定,从机要在这个位拉低SDA,所以单看从机的波形,应该是这样,该应答的时候,从机立刻拽佳SDA,然后应答结束之后,从机再放开SDA
那现在综合两者的波形,结合线与的特性,在主机释放SDA之后,由于SDA也被从机拽住了,所以主机松手后,SDA并没有回弹高电平,这个过程,就代表从机产生了应答,最终高电平期间,主机读取SDA,发现是0,就说明,我进行寻址,有人给我应答了,传输没问题,如果主机读取SDA,发现是1就说明,我进行寻址,应答位期间,我松手了,但是没人拽住官,没人给我应答,那就直接产生停止条件吧,并提示一些信息
然后这个上升沿,就是应答位结束后,从机释放SDA产生的,从机交出了SDA的控制权,因为从机要在低电平尽快变换数据,所以这个上升沿和SCL的下降沿,几乎是同时发生的
由于之前我们读写位给了0,
所以应答结束后,我们要继续发送一个字节,同样的时序再来一把,第二个字节,就可以送到指定设备的内部了,从机设备可以自己定义第二个字节和后续字节的用途,一般第二个字节可以是寄存器地址或者是指令控制字等,比如MPU6050定义的第二个字节就是寄存器地址,比如AD转换器,第二个字节可能就是指令控制字,比如存储器,第二个字节可能就是存储器地址
那图示这里,主机发送这样一个波形,我们--判定,数据为0001 1001,即,主机向从机发送了0x19这个数据,在MPU6050里,就表示我要操作你0x19地址下的寄存器了,接着同样,是从机应答,主机释放SDA,从机拽住SDA,SDA表现为低电平,主机收到应答位为0,表示收到了从机的应答,然后继续,同样的流程再来遍,主机再发送一个字节,这个字节就是主机想要写入到0x19地址下寄存器的内容了,就表示,我要在0x19地址下,写入0xAA,
最后是接收应答位,如果主机不需要继续传输了,就可以产生停止条件(Stop,P),在停止条件之前,先拉低SDA,为后续SDA的上升沿作准备,然后释放SCL,再释放SDA,这样就产生了SCL高电平期间,SDA的上升沿,这样一个完整的数据帧就拼接完成了
这个数据帧的目的就是,对于指定从机地址为1101000的设备,在其内部0x19地址的寄存器中,写入0XAA这个数据,这就是指定地址写的时序
4.2当前地址读
起始条件开始后,主机必须首先调用发送一个字节,来进行从机的寻址和指定读写标志位,
如图示的波形,表示本次寻址的目标是1101000的设备,同时,最后一位读写标志为1,表示主机接下来想要读取数据,紧跟着,发送一个字节之后,接收一下从机应答位,从机应答0,代表从机收到了第一个字节,在从机应答之后,
从这里开始,数据的传输方向就要反过来了,因为刚才主机发出了读的指令,所以这之后,主机就不能继续发送了,要把SDA的控制权交给从机,主机调用接收一个字节的时序,进行接收操作,然后在这一块,从机就得到了主机的允许可以在SCL低电平期间写入SDA,然后主机在SCL高电平期间读取SDA,那最终,主机在SCL高电平期间依次读取8位,就接收到了从机发送的一个字节数据,0000 1111,也就是0x0F
那现在问题就来了,这个0xOF是从机哪个寄存器的数据呢,我们看到,在读的时序中12C协议的规定是,主机进行寻址时,一旦读写标志位给1了,下一个字节就要立马转为读的时序,所以主机还来不及指定,我想要读哪个寄存器,就得开始接收了,所以这里就没有指定地址这个环节,那主机并没有指定寄存器的地址,从机到底该发哪个寄存器的数据呢,这就需要用到我们上面说的当前地址指针了
在从机中,所有的寄存器被分配到了一个线性区域中,并且,会有一个单独的指针变量,指示这其中一个寄存器,这个指针上电默认,一般指向0地址,并且,每写入一个字节和读出一个字节后,这个指针就会自动自增一次,移动到下一个位置,那么在调用当前地址读的时序时,主机没有指定要读哪个地址,从机就会返回当前指针指向的寄存器的值,那假设,我刚刚调用了这个指定地址写的时序,在0x19的位置写入了0xAA,那么指针就会+1,移动到0x1A的位置,我再调用这个当前地址读的时序,返回的就是0x1A地址下的值,如果再调用一次呢,返回的就是0x1B地址下的值,这就是当前地址读时序的操作逻辑,由于当前地址读并不能指定读的地址,所以这个时序用的不是很多
4.3指定地址读
下面的时序在这里分隔一下,前面的部分是指定地址写,但是只指定了地址,还没来得及写,后面的部分是当前地址读,因为我们刚指定了地址,所以再调用当前地址读,两者加在一起,就是指定地址读了
首先,最开始,仍然是启动条件,然后发送一个字节,进行寻址,这里指定从机地址是1101000,读写标志位是0,代表我要进行写的操作,经过从机应答之后,再发送一个字节,第二个字节,用来指定地址,这个数据就写入到了从机的地址指针里了,也就是说,从机接收到这个数据之后,它的寄存器指针就指向了0x19这个位置,之后,我们要写入的数据,不给它发,而是直接再来个起始条件,
这个Sr(Start Repeat)的意思就是重复起始条件,相当于另起一个时序,因为指定读写标志位只能是跟着起始条件的第一个字节,所以如果想切换读写方向,只能再来个起始条件,然后起始条件后,重新寻址并且指定读写标志位,此时读写标志位是1,代表我要开始读了,接着主机接收一个字节,这个字节是不是就是0x19地址下的数据,这就是指定地址读
另外在这里,你也可以再加一个停止条件,这样也行,这样的话,就是两个完整的时序了,先起始,写入地址,停止,因为写入的地址会存在地址指针里面,所以这个地址并不会因为时序的停止而消失,我们就可以再起始,读当前位置,停止,这样两条时序也可以完成任务
但是12C协议官方规定的复合格式是一整个数据帧,就是先起始、再重复起始、再停止,相当于把两条时序拼接成一条了
4.4高级一点
如果你只想写一个字节,那就停止,就行了,如果你想写多个字节,就可以把这最后一部分,多重复几次,写入一次后,地址指针会自动+1,变成0x1A,所以这第二个数据就写入到了0x1A的位置
这样这个时序就进阶为,在指定的位置开始,按顺序连续写入多个字节
然后同理,当前位置读和指定位置读,也可以多次执行这最后一部分时序,由于地址指针在读后也会自增,所以这样就可以连续读出一片区域的寄存器
如果你只想读一个字节就停止的话,在读完一个字节之后,一定要给从机发个非应答(Send Ack, SA),非应答,就是该主机应答的时候,主机不把SDA拉低,从机读到SDA为1,就代表主机没有应答,从机收到非应答之后,就知道主机不想要继续了,从机就会释放总线,把SDA控制权交还给主机,如果主机读完仍然给从机应答了,从机就会认为主机还想要数据,就会继续发送下一个数据,而这时,主机如果想产生停止条件,SDA可能就会因为被从机拽住了,而不能正常弹回高电平
如果主机想连续读取多个字节,就需要在最后一个字节给非应答,而之前的所有字节都要给应答,简单来说就是主机给应答了,从机就会继续发,主机给非应答了,从机就不会再发了,交出SDA的控制权,从机控制SDA发送一个字节的权利,开始于读写标志位为1,结束于主机给应答为1,这就是主机给从机发送应答位的作用