I2C(Inter IC Bus)是由Philips公司开发的一种通用数据总线
两根通信线:
同步,半双工
带数据应答
支持总线挂载多设备(一主多从、多主多从)
这就像是在教室里,老师是主机主导课程的进行,所有学生都是从机,所有从机可以同时被动地听老师讲课,但是从机只有在被老师点名之后才能说话,不可以在未经允许的情况下说话,这样课堂才能有条不紊地进行。
这就像是在教室里,老师正在讲课,突然有个学生站起来说,老师打断一下,接下来让我来说,所有同学听我指挥,但是,同一个时间只能有一个人说话。这时就相当于发生了总线冲突,在总线冲突时,I2C协议会进行仲裁,仲裁胜利的一方取得总线控制权,失败的一方自动变成从机。由于时钟线也是由主机控制的,所以在多主机的模型下,还要进行时钟同步。
所有I2C设备的SCL连在一起,SDA连在一起
设备的SCL和SDA均要配置成开漏输出模式
SCL和SDA各添加一个上拉电阻,阻值一般为4.7KΩ左右
禁止所有设备输出强上拉的高电平,采用外置弱上拉电阻和开漏输出的结构。
一主多从模型
I2C总线处于空闲状态时,SCL和SDA都处于高电平状态,也就是没有任何一个设备去碰SCL和SDA,SCL和SDA由外挂的上拉电阻拉高至高电平,总线处于平静的高电平状态,当主机需要进行数据收发时,首先要打破总线的宁静,产生一个起始条件,这个起始条件就是:
SCL先放手,回弹到高电平,SDA再放手,回弹到高电平,产生一个上升沿,这个上升沿触发终止条件,同时终止条件之后,SCL和SDA都是高电平,回归到最初的平静状态。
这个起始和终止条件就类似串口时序里的起始位和停止位,一个完整的数据帧,总是以起始条件开始,终止条件结束,另外,起始和终止,都是由主机产生的,从机不允许产生起始和终止,所以在总线空闲状态时,从机必须始终双手放开,不允许主动跳出来,去碰总线。
最开始SCL是低电平,主机如果想发送0,就拉底SDA到低电平,如果想发送1,就放手,SDA就回弹到高电平,在SCL低电平期间,允许改变SDA的电平,当这一位放好之后,主机就松手时钟线,SCL回弹到高电平,在高电平期间,是从机读取SDA的时候,所以高电平期间,SDA不允许变化,SCL处于高电平之后,从机需要尽快地读取SDA,一般都是在上升沿这个时刻,从机就读取完成了,因为时钟是主机控制的,从机并不知道什么时候就会产生下降沿,所以从机在上升沿时,就会立刻把数据读走。主机在放手SCL一段时间后,就可以继续拉低SCL,传输下一位了。主机也需要在SCL下降沿之后尽快把数据放在SDA上,但是主机有时钟的主导权,所以主机并不需要很着急,只需要在低电平的任意时刻把数据放在SDA上就行了。数据放完之后,主机再松手SCL,SCL高电平,从机读取这一位。
主机拉低SCL,把数据放在SDA上,主机松开SCL,从机读取SDA的数据。在SCL的同步下,依次进行主机发送和从机接收,循环8次,就发送了8位数据,也就是一个字节。
高位先行,所以第一位时一个字节的最高位bit 7,然后依次是次高位 bit 6,最后发送最低位 bit 0,这与串口不同,串口时序是低位先行,I2C是高位先行。
由于这里有时钟线进行同步,如果主机一个字节发送一半,突然进中断了,不操作SCL和SDA了,那时序就会在中断的位置不断拉长,SCL和SDA都暂停变化,传输也完全暂停,等中断结束后,主机回来继续操作,传输仍然不会出问题,这就是同步时序的好处。
由于这整个时序是主机发送一个字节,所以在这个单元里,SCL和SDA全程由主机掌控,从机只能被动读取
释放SDA其实就相当于切换成输入模式,或者可以这样理解,所有设备包括主机都始终处于输入模式,当主机需要发送的时候,就可以主动去拉低SDA,而主机在被动接收的时候,就必须先释放SDA,不要去动它,以免影响别人发送,因为总线是线与的特征,任何一个设备拉低了,总线就是低电平,如果你接收的时候,还拽着SDA不放手,那别人无论发什么数据,总线都始终是低电平。
主机在接收之前要释放SDA,这时从机就取得了SDA的控制权,从机需要发送0,就把SDA拉低,从机需要发送1,就放手,SDA回弹到高电平,然后同样的,低电平变换数据,高电平读取数据,实线部分表示主机控制的电平,虚线部分表示从机控制的电平,SCL全程由主机控制,SDA主机在接收前要释放,交由从机控制,之后还是一样,因为SCL时钟是由主机控制的,所以从机的数据变换基本上都是贴着SCL下降沿进行的,而主机可以在SCL高电平的任意时刻读取,这就是接收一个字节的时序。
主机在接收从机发来的一个字节之后,我们也要给从机发送一个应答位,发送应答位的目的是告诉从机,是否还要继续发送,如果从机发送一个数据之后,得到了主机的应答,那从机就还会继续发送,如果从机没得到主机的应答,那从机就会认为,我发送了一个数据,主机不想接收,这时从机就会释放SDA,交出SDA的控制权,防止干扰主机之后的操作。
我们在调用发送一个字节之后,就要紧跟着调用接收应答的时序,用来判断从机有没有收到刚才给它的数据,如果从机收到了,那在应答位这里,主机释放SDA的时候,从机就应该立刻把SDA拉下来,然后在SCL高电平期间,主机读取应答位,如果应答位为0,就说明从机确实收到了。
指定地址写
对于指定设备(Slave Address 从机地址),在指定地址(Reg Address 寄存器地址)下,写入指定数据(Data)
空闲状态时,SCL和SDA都是高电平,然后主机需要给从机写入数据的时候,SCL高电平期间,拉低SDA,产生起始条件(Start S),在起始条件之后,紧跟着的时序,必须是发送一个字节的时序,字节的内容必须是从机地址+读写位,从机地址是7位,读写位是1位,加起来正好是1个字节8位,发送从机的地址,就是确定通信的对象,发送读写位,就是确认接下来是要写入还是要读出。
SCL低电平期间,SDA变换数据,SCL高电平期间,从机读取SDA,这里用绿色的线,来标明了从机读到的数据,比如一开始,从机收到的第一位就是高电平1,然后SCL低电平,主机继续变换数据,因为第二位还是1,所以这里SDA电平没有变换,然后SCL高电平,从机读到第二位是1,之后继续,低电平变换数据,高电平读取数据,第三位就是0,这样持续8次,就发送了一个字节数据,其中这个数据的定义是,高7位,表示从机地址,比如这个波形下,主机寻找的从机地址就是1101 000,这个就是MPU6050的地址。
然后最低位,表示读写位,0表示,之后的时序主机要进行写入操作,1表示,之后的时序主机要进行读出操作,这里是0,说明之后我们要进行写入操作, 目前,主机是发送了一个字节,字节的内容转换为16进制,高位先行,就是0xD0,然后根据协议规定,紧跟着的单元,就得是接收从机的应答位(Receive Ack,RA),在这个时刻,主机要释放SDA,所以如果单看主机的波形,释放SDA之后,引脚电平回弹到高电平,但是根据协议规定,从机要在这个位拉低SDA,所以单看从机波形,从机该应答的时候,立刻拽住SDA,然后应答结束之后,从机再放开SDA,那现在综合两者的波形,结合线与的特性,在主机释放SDA之后,由于SDA也被从机拽住了,所以主机松手后,SDA并没有回弹高电平,这个过程就代表从机产生了应答,最终高电平期间,主机读取SDA,发现是0,就说明,主机进行寻址,从机给主机应答,传输没问题。如果主机读取SDA,发现是1,就说明,主机进行寻址,应答位期间,主机放开SDA,但是没有从机拽住SDA,没有从机给主机应答,那就直接产生停止条件,并提示一些信息,这就是应答位。
然后这个上升沿,就是应答位结束之后,从机释放SDA产生的,从机交出了SDA的控制权,因为从机要在低电平尽快变换数据,所以这个SDA的上升沿和SCL的下降沿,几乎是同时发生的,由于之前我们读写位给了0,所以应答结束后,我们要继续发送一个字节,同样的时序再来一遍,第二个字节,就可以送到指定设备的内部了,从机设备可以自己定义第二个字节和后续字节的用途,一般第二个字节可以是寄存器地址或者是指令控制字等,比如MPU6050定义的第二个字节就是寄存器地址,比如AD转换器,第二个字节可能就是指令控制字,比如存储器,第二个字节可能就是存储器地址,图示这里,主机发送这样一个波形,数据位0001 1001,即,主机向从机发送了0x19这个数据,在MPU6050中就表示,主机要操作0x19地址下的寄存器了,接着同样,是从机应答,主机释放SDA,从机拽住SDA,SDA表现为低电平,主机收到应答位为0,表示收到了从机的应答。
然后继续,同样的流程再来一遍,主机再发送一个字节,这个字节就是主机想要写入到0x19地址下寄存器的内容了,比如我这里发送了0xAA的波形,就表示,主机要在0x19地址下,写入0xAA,最后是接收应答位,如果主机不需要继续传输了,就可以产生停止条件(Stop,P),在停止条件之前,先拉低SDA,为后续SDA的上升沿做准备,然后释放SCL,再释放SDA,这样就产生了SCL高电平期间,SDA的上升沿,这样一个完整的数据帧就拼接完成了。
这个数据帧的目的就是,对于指定从机地址为1101000的设备,在其内部0下0x19地址的寄存器,写入0xAA这个数据,这就是指定地址写的时序。
- 如果想写入多个字节,就可以重复多次发送一个字节和接收应答,这样第一个数据就写入到了指定地址0x19的位置,注意,写入一次数据后,地址指针会自动+1,变成0x1A,所以第二个数据就写入到了0x1A的位置,同理第三个数据就写入的是0x1B的位置,以此类推,这样这个时序就进阶为,在指定的位置开始,按顺序连续写入多个字节,比如你需要连续写入多个寄存器,就可以考虑这样操作,这样在一个数据帧里,就可以同时写入多个字节,执行效率会会比较高。
- 同理当前位置读和指定位置读,也可以多次执行最后一部分时序,由于地址指针在读后也会自增,所以这样就可以连续读出一片区域的寄存器,效率也会非常高。
- 注意,如果只想读一个字节就停止的话,在读完一个字节之后,一定要给从机发个非应答(Send Ack,SA),非应答,就是该主机应答的时候,主机不把SDA拉低,从机读到SDA为1,就代表主机没有应答,从机收到非应答之后,就知道主机不想要继续了,从机就会释放总线,把SDA控制权交还给主机,如果主机读完仍然给从机应答了,从机就会认为主机还想要数据,就会继续发送下一个数据,而这时,主机如果想产生停止条件,SDA可能就会因为被从机拽住了,而不能正常弹回到高电平,如果主机想连续读取多个字节,就需要在最后一个字节给非应答,而之前的所有字节都要给应答。
- 简单来说就是,主机给应答了,主机就会继续发,主机给非应答了,从机就不会再发了,交出SDA的控制权,从机控制SDA发送一个字节的权利,开始于读写标志位为1,结束于主机给应答位1,这就是主机给从机发送应答位的作用。
- 最开始,SCL高电平期间,拉低SDA,产生起始条件,起始条件开始后,主机必须首先调用发送一个字节,来进行从机的寻址和指定读写标志位,比如图示的波形,表示本次寻址的目标是1101 000的设备,同时最后一位读写位标志为1,表示主机接下来想要读取数据,紧跟着,发送一个字节之后,接受一下从机应答位,从机应答0,代表从机收到了第一个字节。
- 在从机应答位之后,从这里开始,数据的传输方向就要反过来了,因为刚才主机发出了读的指令,所以这之后,主机就不能继续发送了,要把SDA的控制权交给从机,主机调用接收的一个字节的时序,进行接受操作,然后在这一块,从机就得到了主机的允许,可以在SCL低电平期间写入SDA,然后主机在SCL高电平期间读取SDA,那最终,主机在SCL高电平期间依次读取8位,就接收到了从机发送的一个字节数据,0000 1111也就是0xF,也就是0x0F。
- 那现在问题就来了,这个0x0F是从机哪个寄存器的数据呢,我们看到,在读的时序中,I2C协议的规定是,主机进行寻址时,一旦读写标志位给了1,下一个字节就要立马转为读的时序,所以主机还来不及指定,我想要读哪个寄存器,就得开始接收了,所以这里就没有指定地址的这个环节,那主机并没有指定寄存器的地址,从机到底该发哪个寄存器的数据呢,这就需要用到我们上面说的当前地址指针了。
- 在从机中,所有的寄存器被分配到了一个线性区域中,并且,会有一个单独的指针变量,指示着其中一个寄存器,这个指针上电默认,一般指向0地址,并且,每写入一个字节和读出一个字节后,这个指针就会自动自增依次,移动到下一个位置,那么在调用当前地址读的时序时,主机没有指定要读哪个地址,从机就会返回当前指针指向的寄存器的值,那假设,我刚刚调用了这个指定地址写的时序,在0x19的位置写入了0xAA,那么指针就会+1,移动到0x1A的位置,我再调用这个当前地址读的时序,返回的就是0x1A地址下的值,如果再调用一次,返回的就是0x1B地址下的值,依次类推,这就是当前地址读时序的逻辑。由于当前地址读,并不能指定读的地址,所以这个时序用的不是很多。
指定地址读
对于指定设备(Slave Address),在指定地址(Reg Address)下,读取从机数据(Data)
将指定地址写中的前面一部分指定地址的时序(把最后面的写数据这一部分去掉),然后把前面这一段设置地址,还没有指定些什么数据的时序,给它追加到这个当前地址读时序的前面,就得到了指定地址读的时序,一般我们也把它称作复合格式,下面的时序可分为两部分,前面一部分是指定地址写,但是只指定了地址,还没来得及写,后面的部分是当前地址读,因为我们刚指定了地址,所以再调用当前地址读,两者加在一起,就是指定地址读了。
首先最开始,仍然是启动条件,然后发送一个字节,进行寻址,这里指定从机地址是1101000,读写标志位是0,代表我们要进行写操作,经过从机应答之后,再发送一个字节,第二个字节用来指定地址,这个数据就写入到从机的地址指针里了,也就是说,从机接收到这个数据之后,他的寄存器指针就指向了0x19这个位置。
之后,我们要写入的数据不给他发,而是直接再来个起始条件,这个Sr(Start Repeat)的意思就是重复起始条件,相当于另起一个时序,因为指定读写标志位只能跟着起始条件的第一个字节,所以如果想切换读写方向,只能再来个起始条件,然后起始条件后,重新寻址并且指定读写标志位,此时读写标志位是1,代表我要开始读了,接着,主机接收一个字节,这个字节就是0x19下的数据,这就是指定地址读。
另外,在Sr之前,也可以加一个停止条件,这样就是两个完整的时序了,先起始,写入地址,停止,因为写入的地址会存在地址指针里面,所以这个地址并不会因为时序的停止而消失,我们就可以再起始,都当前位置,停止,这样两条时序也可以完成任务,但是I2C协议官方规定的复合格式就是一整个数据帧,就是先起始再重复起始再停止,相当于把两条时序拼接成一条了。
通信协议的时序是一个很重要的东西,我们只要理解清楚了这个时序的意义,就可以按照它协议的规定,去翻转通信引脚的高低电平,只要我们反转产生的这个时序波形,满足了通信协议的规定,那通信双方就能理解并解析这个波形,这样,通信自然而然就实现了。
软件I2C,手动拉低或释放时钟线,然后再手动对每个数据位进行判断,拉低或释放数据线,这样来产生时序波形。由于I2C是同步时序,对每一位的持续时间要求不严格,某一位的时间长点短点,或者中途暂停一会儿时序,影响都不大。
串口是异步时序,每一位的时间要求很严格,不能过长也不能过短,更不能中途暂停一会。
STM32内部集成了硬件I2C收发电路,可以由硬件自动执行时钟生成、起始终止条件生成、应答位收发、数据收发等功能,减轻CPU的负担(由硬件电路来自动翻转引脚电平,软件只需要写入控制寄存器CR和数据寄存器DR,就可以实现协议了,为了实时监控时序的状态,软件还得读取状态寄存器SR,来获取外设电路的状态信息)。
支持多主机模型(I2C通信,分为主机和从机,主机,就是拥有主动控制总线的权利,而从机,只能在主机允许的情况下,才能控制总线,在一主多从的模型下,只有唯一一个主机,可以挂载多个从机,主机操控所有从机。
固定多主机 有两个或更多固定的主机,挂载多个从机,从机可以被任意一个主机控制,当有多个主机都想控制总线时,就是总线冲突状态,这时就要进行总线仲裁了,仲裁失败的一方让出总线控制权。
可变多主机 可以在总线上挂载多个设备,总线上没有固定的主机和从机,任何一个设备,都可以在总线空闲时跳出来作为主机,然后指定其他任何一个设备进行通信,当这个通信完成之后,这个跳出来的主机就要退回到从机的位置。当有多个从机想要跳出来作为主机时,就是总线冲突状态,这时就要进行总线仲裁,仲裁失败的一方让出总线控制权。
支持7位/10位地址模式
支持不同的通讯速度,标准速度(高达100 kHz),快速(高达400 kHz)
支持DMA 在多字节传输的时候可以提高传输效率,比如指定地址读多字节、写多字节的时序,如果想要连续读或写非常多的字节,那用一下DMA自动帮我们转运数据,可以提高整个过程的效率。
兼容SMBus协议 SMBus(System Management Bus),系统管理总线,SMBus是基于I2C总线改进而来的,主要用于电源管理系统中。
STM32F103C8T6 硬件I2C资源:I2C1、I2C2
手册中都是用EVx(Event)这几个事件来代替标志位的。为什么要设计这个EVx事件,而不直接产生标志位呢,这是因为,有的状态会同时产生多个标志位,所以这个EVx事件,就是组合了多个标志位的一个大标志位,在库函数中,也就对应的,检查EVx事件是否发生的函数,把它当成一个大标志位即可。
EV8_2 就是
- TxE=1,也就是数据寄存器空
- BTF (Byte Transfer Finished 字节发送结束标志位)=1,表示字节发送结束 。当一个新数据将被发送且数据寄存器还未被写入新数据,这个意思就是当前的移位寄存器已经移完了,该找数据寄存器要下一个数据了,但是数据寄存器没有数据,这就说明主机不想发了,这时就代表字节发送结束,是时候停止了。
如何配置是否要给应答,在控制寄存器CR中的ACK(应答使能)位写1,表示,在接收一个字节后返回一个应答(匹配的地址或数据),写0,表示无应答返回。
软件I2C
硬件I2C