STM32 I2C详解
I2C简介
-
I2C(Inter IC Bus)是由Philips公司开发的一种通用数据总线
-
两根通信线:
- SCL(Serial Clock)串行时钟线,使用同步的时序,降低对硬件的依赖,同时同步的时序稳定性也比异步的时序更高。
- SDA(Serial Data)串行数据线,半双工,一根线兼具发送和接收,最大化利用资源。
-
同步,半双工
-
带数据应答
-
支持总线挂载多设备(一主多从、多主多从)
- 一主多从 单片机作为主机,主导I2C总线的运行,挂载在I2C总线的所有外部模块都是从机,从机只有被主机点名之后才能控制I2C总线,不能在未经允许的情况下去碰I2C总线,防止冲突。
这就像是在教室里,老师是主机主导课程的进行,所有学生都是从机,所有从机可以同时被动地听老师讲课,但是从机只有在被老师点名之后才能说话,不可以在未经允许的情况下说话,这样课堂才能有条不紊地进行。
- 多主多从 在总线上任何一个模块都可以主动跳出来,成为新的主机。
这就像是在教室里,老师正在讲课,突然有个学生站起来说,老师打断一下,接下来让我来说,所有同学听我指挥,但是,同一个时间只能有一个人说话。这时就相当于发生了总线冲突,在总线冲突时,I2C协议会进行仲裁,仲裁胜利的一方取得总线控制权,失败的一方自动变成从机。由于时钟线也是由主机控制的,所以在多主机的模型下,还要进行时钟同步。
硬件电路
-
所有I2C设备的SCL连在一起,SDA连在一起
-
设备的SCL和SDA均要配置成开漏输出模式
-
SCL和SDA各添加一个上拉电阻,阻值一般为4.7KΩ左右
-
禁止所有设备输出强上拉的高电平,采用外置弱上拉电阻和开漏输出的结构。
- 完全杜绝了电源短路的现象,保证电路的安全。
- 避免了引脚模式的频繁切换。
- 这个模式会有一个线与的现象,只要有任意一个或多个设备输出了低电平,总线就处于低电平,只有所有的设备都输出高电平,总线才处于高电平,I2C可以利用这个电路特性,执行多主机模式下的时钟同步和总线仲裁。
一主多从模型
- 主机 CPU就是单片机,作为总线的主机,主机的权利很大,包括对SCL线的完全控制,任何时候,都是主机完全掌控SCL线,另外在空闲状态下,主机可以主动发起对SDA的控制,只有在从机发送数据和从机应答的时候,主机才会转交SDA的控制权给从机。
- 从机 被控IC也就是挂载在I2C总线上的从机,这些从机可以是姿态传感器、OLED、存储器、时钟模块等等,从机的权利比较小,对于SCL时钟线,在任何时刻都只能被动地读取,从机不允许控制SCL线,对于SDA数据线,从机不允许主动发起对SDA的控制,只有在主机发送读取从机的命令后,或者从机应答的时候,从机才能短暂地取得SDA的控制权。
I2C时序基本单元
- 起始条件:SCL高电平期间,SDA从高电平切换到低电平
I2C总线处于空闲状态时,SCL和SDA都处于高电平状态,也就是没有任何一个设备去碰SCL和SDA,SCL和SDA由外挂的上拉电阻拉高至高电平,总线处于平静的高电平状态,当主机需要进行数据收发时,首先要打破总线的宁静,产生一个起始条件,这个起始条件就是:
- SCL处于高电平不去动它,然后把SDA拽下来,产生一个下降沿,当从机捕获到这个SCL高电平,SDA下降沿信号时,就会进行自身的复位,等待主机的召唤。
- 然后在SDA下降沿之后,主机要再把SCL拽下来,拽下SCL,一方面是占用这个总线,另一方面也是为了方便我们这些基本单元的拼接,除了起始和终止条件,每个时序单元的SCL都是以低电平开始,低电平结束。,这样这些单元拼接起来,SCL才能连续。
- 终止条件:SCL高电平期间,SDA从低电平切换到高电平
SCL先放手,回弹到高电平,SDA再放手,回弹到高电平,产生一个上升沿,这个上升沿触发终止条件,同时终止条件之后,SCL和SDA都是高电平,回归到最初的平静状态。
这个起始和终止条件就类似串口时序里的起始位和停止位,一个完整的数据帧,总是以起始条件开始,终止条件结束,另外,起始和终止,都是由主机产生的,从机不允许产生起始和终止,所以在总线空闲状态时,从机必须始终双手放开,不允许主动跳出来,去碰总线。
- 主机发送一个字节:SCL低电平期间,主机将数据位依次放到SDA线上(高位先行),然后释放SCL,从机将在SCL高电平期间读取数据位,所以SCL高电平期间SDA不允许有数据变化,依次循环上述过程8次,即可发送一个字节。
最开始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全程由主机掌控,从机只能被动读取
- 主机接收一个字节:SCL低电平期间,从机将数据位依次放到SDA线上(高位先行),然后释放SCL,主机将在SCL高电平期间读取数据位,所以SCL高电平期间SDA不允许有数据变化,依次循环上述过程8次,即可接收一个字节(主机在接收之前,需要释放SDA)
释放SDA其实就相当于切换成输入模式,或者可以这样理解,所有设备包括主机都始终处于输入模式,当主机需要发送的时候,就可以主动去拉低SDA,而主机在被动接收的时候,就必须先释放SDA,不要去动它,以免影响别人发送,因为总线是线与的特征,任何一个设备拉低了,总线就是低电平,如果你接收的时候,还拽着SDA不放手,那别人无论发什么数据,总线都始终是低电平。
主机在接收之前要释放SDA,这时从机就取得了SDA的控制权,从机需要发送0,就把SDA拉低,从机需要发送1,就放手,SDA回弹到高电平,然后同样的,低电平变换数据,高电平读取数据,实线部分表示主机控制的电平,虚线部分表示从机控制的电平,SCL全程由主机控制,SDA主机在接收前要释放,交由从机控制,之后还是一样,因为SCL时钟是由主机控制的,所以从机的数据变换基本上都是贴着SCL下降沿进行的,而主机可以在SCL高电平的任意时刻读取,这就是接收一个字节的时序。
- 主机发送应答:主机在接收完一个字节之后,在下一个时钟发送一位数据,数据0表示应答,数据1表示非应答。
主机在接收从机发来的一个字节之后,我们也要给从机发送一个应答位,发送应答位的目的是告诉从机,是否还要继续发送,如果从机发送一个数据之后,得到了主机的应答,那从机就还会继续发送,如果从机没得到主机的应答,那从机就会认为,我发送了一个数据,主机不想接收,这时从机就会释放SDA,交出SDA的控制权,防止干扰主机之后的操作。
- 从机接收应答:主机在发送完一个字节之后,在下一个时钟接收一位数据,判断从机是否应答,数据0表示应答,数据1表示非应答(主机在接收之前,需要释放SDA)。
我们在调用发送一个字节之后,就要紧跟着调用接收应答的时序,用来判断从机有没有收到刚才给它的数据,如果从机收到了,那在应答位这里,主机释放SDA的时候,从机就应该立刻把SDA拉下来,然后在SCL高电平期间,主机读取应答位,如果应答位为0,就说明从机确实收到了。
I2C时序
-
指定地址写
-
对于指定设备(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,这就是主机给从机发送应答位的作用。
- 当前地址读
- 对于指定设备(Slave Address),在当前地址指针指示的地址下,读取从机数据(Data)
- 最开始,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是同步时序,对每一位的持续时间要求不严格,某一位的时间长点短点,或者中途暂停一会儿时序,影响都不大。
串口是异步时序,每一位的时间要求很严格,不能过长也不能过短,更不能中途暂停一会。
I2C 外设简介
-
STM32内部集成了硬件I2C收发电路,可以由硬件自动执行时钟生成、起始终止条件生成、应答位收发、数据收发等功能,减轻CPU的负担(由硬件电路来自动翻转引脚电平,软件只需要写入控制寄存器CR和数据寄存器DR,就可以实现协议了,为了实时监控时序的状态,软件还得读取状态寄存器SR,来获取外设电路的状态信息)。
-
支持多主机模型(I2C通信,分为主机和从机,主机,就是拥有主动控制总线的权利,而从机,只能在主机允许的情况下,才能控制总线,在一主多从的模型下,只有唯一一个主机,可以挂载多个从机,主机操控所有从机。
-
固定多主机 有两个或更多固定的主机,挂载多个从机,从机可以被任意一个主机控制,当有多个主机都想控制总线时,就是总线冲突状态,这时就要进行总线仲裁了,仲裁失败的一方让出总线控制权。
-
可变多主机 可以在总线上挂载多个设备,总线上没有固定的主机和从机,任何一个设备,都可以在总线空闲时跳出来作为主机,然后指定其他任何一个设备进行通信,当这个通信完成之后,这个跳出来的主机就要退回到从机的位置。当有多个从机想要跳出来作为主机时,就是总线冲突状态,这时就要进行总线仲裁,仲裁失败的一方让出总线控制权。
-
-
支持7位/10位地址模式
- 7位地址只有128种情况,如果挂载的设备非常多,会不够用。如果一条总线必须要挂载128个以上的设备,那7位地址必然是不够用的,另外,如果有非常多的厂商都来申请I2C的地址,那也必然会有部分型号的芯片,它们的地址是一样的,对于不同芯片地址一样,由于地址的低位通常是可配置的,地址高位都一样,配置低位不一样即可,只要不在一个总线上挂载过多的设备就行。(另外确实需要很多的设备,条件允许的情况下,也可以开辟多条I2C总线,所以地址的问题,一般好解决)
- 10位地址,最多就有1024种情况了
- I2C起始之后的第一个字节,必须是寻址+读写位,这一个字节只能有7位地址,那只需要再规定,起始之后的前两个字节,都作为寻址,就可以实现10位地址模式了,并将第一个字节的前5位作为标志位。因为,当发送第一个字节之后,无法判断后面这个字节还是不是寻址,所以这就需要再第一个字节写个特定的数据,作为10位寻址模式的标志位,这个标志位就是11110,也就是如果你第二个字节也是寻址,那第一个字节的前5位就必须是11110,那么第一个字节剩下的两位,和第二个字节的8位都作为寻址,就实现了10位地址寻址。
-
支持不同的通讯速度,标准速度(高达100 kHz),快速(高达400 kHz)
-
支持DMA 在多字节传输的时候可以提高传输效率,比如指定地址读多字节、写多字节的时序,如果想要连续读或写非常多的字节,那用一下DMA自动帮我们转运数据,可以提高整个过程的效率。
-
兼容SMBus协议 SMBus(System Management Bus),系统管理总线,SMBus是基于I2C总线改进而来的,主要用于电源管理系统中。
-
STM32F103C8T6 硬件I2C资源:I2C1、I2C2
I2C框图
- I2C的通信引脚SDA和SCL,(SMBus的通信引脚SMBALERT),这种外设模块引出来的引脚,一般都是借用GPIO口的复用模式与外部电路相连,查询引脚定义表可知,I2C2_SCL和I2C2_SDA分别复用在了PB10和PB11这两个端口,I2C1_SCL和I2C1_SDA分别复用在了PB6和PB7这两个端口,另外I2C1这两个引脚,还可以重映射到PB8和PB9这两个引脚。
- I2C是半双工,数据收发是同一组数据寄存器和移位寄存器
- 当我们需要发送数据时,可以把一个字节数据写到数据寄存器DR,当移位寄存器没有数据移位时,这个数据寄存器中的值就会转到移位寄存器里,在移位的过程中,就可以直接把下一个数据放到数据寄存器里等待了,一旦前一个数据移位完成,下一个数据就可以无缝衔接,继续发送,当数据由数据寄存器转到移位寄存器时,就会置状态寄存器的TXE位为1,表示发送寄存器为空,这就是发送的流程。
- 当我们要接收数据时,输入的数据,一位一位地从引脚移入到移位寄存器里,当一个字节的数据收齐之后,数据就整体地从一位寄存器转到数据寄存器,同时置标志位RXNE,表示接收寄存器非空,这时我们就可以把数据从数据寄存器读出来了。
- 至于什么时候收发数据,需要我们写入控制寄存器的对应位进行操作,对于起始条件、终止条件、应答位也都有控制电路可以完成。
- 比较器和地址寄存器是从机模式使用的,因为STM32的I2C是基于可变多主机模型设计的,STM32不进行通信的时候,就是从机,既然作为从机,就可以被主机召唤,所以就应该有从机地址,而这个从机地址就可以由自身地址寄存器指定,我们可以自定一个从机地址,写到这个寄存器。当STM32作为从机,在被寻址时,如果收到的寻址通过比较器判断,和自身地址相同,那STM32就作为从机,响应外部主机的召唤,并且STM32支持同时响应两个从机地址,所以就有自身地址寄存器和双地址寄存器。
- 数据校验模块,当我们发送一个多字节的数据帧时,硬件可以自动执行CRC校验计算,CRC是一种很常见的数据校验算法,它会根据前面这些数据,进行各种数据运算,然后会得到一个字节的校验位,附加在这个数据帧的后面,在接收到这一帧数据后,STM32的硬件也可以自动执行校验的判定,如果数据在传输的过程中出错了,CRC检验算法就通不过,硬件就会置校验错误标志位,告知数据错误。
- 时钟控制,用来控制SCL线,在时钟控制寄存器写对应的位,电路就会执行对应的功能。
- 控制逻辑寄存器,写入控制寄存器,可以对整个电路进行控制。
- 读取状态寄存器,可以得知电路的工作状态。
- 中断,当内部有一些标志位置1之后,可能事件比较紧急,就可以申请中断。如果开启了这个中断,那当这个事件发生后,程序就可以跳到中断函数来处理这个事件了。
- DMA请求与响应,在进行很多字节的收发时,可以配合DMA来提高效率。
I2C基本结构
- 移位寄存器和数据寄存器DR的配合,是通信的核心部分。由于I2C是高位先行,所以这个移位寄存器是向左移位。
- 在发送的时候,最高位先移出去,然后是次高位等等,一个SCL时钟,移位一次,移位8次,这样就能把一个字节,由高位到低位,依次放到SDA线上了。
- 在接收的时候,数据通过GPIO口从右边依次移进来,最终移8次,一个字节就接收完成了。
- GPIO口这里,使用硬件I2C的时候,都要配置成复用开漏输出的模式,复用就是GPIO口的状态是交由片上外设来控制的,开漏输出是I2C协议要求的端口配置,即使配置成开漏输出模式,GPIO口也是可以进行输入的。
- SCL这里,时钟控制器通过GPIO去控制时钟线。
- SDA这里,输出数据,通过GPIO,输出到端口,输入数据,也是通过GPIO,输入到移位寄存器。
- 开漏输出时,P_MOS无效,移位寄存器输出的数据,通向GPIO,就接在了来自片上外设——复用功能输出这个位置,之后控制N_MOS的通断,进而控制这个I/O引脚,是拉低到低电平还是释放悬空。
- 对于输入部分,可以看到,虽然这是复用开漏输出,但是输入这一路仍然有效,I/O引脚的高低电平,通过复用功能输入,进入片上外设,来进行复用功能的输入。
- 开关控制 I2C_Cmd()使能外设
主机发送
- 初始化之后,总线默认空闲状态,STM32默认是从模式,为了产生一个起始条件,需要写入控制寄存器(在START位写1),之后STM32由从模式转为主模式。
- 控制完硬件电路之后,我们要检查标志位,来看看硬件有没有达到我们想要的状态,在这里,起始条件之后,会发生EV5事件,这个EV5事件,可以当作是标志位。(EV5事件,就是SB(Start Bit)标志位置1,SB是状态寄存器的一个位,表示了硬件的状态,置一代表起始条件已发送,软件读取SR1寄存器后,也就是查看了该位,然后写数据寄存器的操作将清楚该位)
手册中都是用EVx(Event)这几个事件来代替标志位的。为什么要设计这个EVx事件,而不直接产生标志位呢,这是因为,有的状态会同时产生多个标志位,所以这个EVx事件,就是组合了多个标志位的一个大标志位,在库函数中,也就对应的,检查EVx事件是否发生的函数,把它当成一个大标志位即可。
- 检测起始条件已发送时,就可以发送一个字节的从机地址了,从机地址需要写到数据寄存器DR中,写入DR之后,硬件电路就会自动把这一个字节,转到移位寄存器里,再把这一个字节发送到I2C总线上,之后硬件会自动接收应答并判断,如果没有应答,硬件就会置应答失败的标志位,这个标志位可以申请中断来告知我们。当寻址完成之后,会发生EV6事件(EV6事件,就是ADDR标志位为1,表示地址发送结束),EV6事件结束后,是EV8_1事件(EV8_1事件就是TxE标志位=1,移位寄存器空,数据寄存器空),这时需要我们写入数据寄存器DR进行数据发送了,一旦写入DR之后,因为移位寄存器也是空,所以DR会立刻转到移位寄存器进行发送,这时就会发生EV8事件(EV8事件就是TxE标志位=1,移位寄存器非空,数据寄存器空),这时就是移位寄存器正在发送数据的状态。
- 流程这里,数据1的时序就产生了,这个数据寄存器和移位寄存器的配合,就是发送的时候,数据先写入数据寄存器,如果移位寄存器没有数据,再转到移位寄存器进行发送。
- 在EV8结束的位置,对应写入DR将清除该事件,所以在这个位置应该写入了下一个数据,也就是数据2,在这个时刻数据2就被写入到数据寄存器里等待了,接收应答位之后,数据2就转入移位寄存器进行发送,此时的状态是移位寄存器非空,数据寄存器空,所以这时,EV8事件就又发生了,EV8事件结束后,数据2还在移位发送,但此时下一个数据,已经被写道数据寄存器里等待了,之后再次应答,产生EV8事件,写入数据寄存器,EV8事件消失。按这个流程来,一旦我们检测到EV8事件,就可以写入下一个数据了。
- 最后,当我们想要发送的数据读写完之后,这时就没有新的数据可以写入到数据寄存器了,当一位寄存器当前的数据移位完成时,此时就是移位寄存器空,数据寄存器也空的状态,这时就会产生EV8_2事件。
EV8_2 就是
- TxE=1,也就是数据寄存器空
- BTF (Byte Transfer Finished 字节发送结束标志位)=1,表示字节发送结束 。当一个新数据将被发送且数据寄存器还未被写入新数据,这个意思就是当前的移位寄存器已经移完了,该找数据寄存器要下一个数据了,但是数据寄存器没有数据,这就说明主机不想发了,这时就代表字节发送结束,是时候停止了。
- 当检测到EV8_2时,就可以产生终止条件了。在控制寄存器CR中的STOP位写1,就会在当前字节传输或当前起始条件发出后产生终止条件。
- 整个过程,简单来说就是,写入控制寄存器CR或数据寄存器DR,可以控制时序单元的发生,比如产生起始条件,发送一个字节数据,时序单元发生后,检查相应的EV事件,起始就是检查状态寄存器SR,来等待时序单元发送完成,然后依次按照这个流程,操作、等待、操作、等待……这样就能实现时序了。
主机接收
- 写入控制寄存器的START位,产生起始条件,然后等待EV5事件(起始条件已发送)
- 寻址,接收应答,产生RV6事件(寻址已完成)
- 发送数据1,代表数据正在通过移位寄存器进行输入,EV6_1事件(没有对应的事件标志,只适用于接收1个字节的情况),之后当这个时序单元完成时,硬件会自动根据我们的配置,把应答位发送出去。这个时序单元结束后,就说明移位寄存器就已经成功移入一个字节的数据1了,这时,移入的一个字节就整体转移到数据寄存器中,同时置RxNE标志位,表示数据寄存器非空,也就是收到了一个字节的数据。这个状态就是EV7事件(EV7事件 就是RxNE=1,数据寄存器非空,读DR寄存器清除该事件,也就是,收到数据,当我们把数据读走之后,EV7事件结束)
- EV7事件结束后,说明数据1被读走,当然数据1还没读走的时候,数据2就可以直接移入移位寄存器了,之后,数据2移位完成,收到数据2,产生EV7事件,读走数据2,EV7事件结束。按照这个流程就可以一直接收数据了。
- 当我们不需要继续接收时,需要在最后一个时序单元发生时,提前把应答位控制寄存器ACK置0,并且设置终止条件请求,这就是EV7_1事件(设置ACK=0和STOP请求),也就是我们想要结束了。在这个时序完成后,由于设置了ACK=0,所以就会给出非应答,最后,由于设置STOP位,所以产生终止条件,这样接收一个字节的时序就完成了。
- 整体流程,写入控制寄存器CR和读取数据寄存器DR,产生时序单元,然后等待相应事件,来确保时序单元的完成。
如何配置是否要给应答,在控制寄存器CR中的ACK(应答使能)位写1,表示,在接收一个字节后返回一个应答(匹配的地址或数据),写0,表示无应答返回。
软件/硬件波形对比(指定地址读)
软件I2C
- 由于操作引脚之后,都加了延时,这个延时有时候加的多,有时候加的少,所以软件时序的时钟周期、占空比可能不规整,不过由于I2C是同步时序,这些不规整也没有什么影响。
- SCL低电平写,高电平读,虽然整个低电平的任意时候都可以写,整个高电平的任意时刻都可以读,但是一般要求保证尽早的原则,所以可以直接认为是SCL下降沿写,SCL上升沿读。软件I2C在下降沿之后,因为操作端口之后有一些延时,所以等了一会才进行写入操作,SDA才变换数据。
- 应答结束,从机在SCL下降沿立刻释放了SDA,但是软件I2C的主机,过了一会儿才变换数据,因此会出现一个短暂的高电平。
硬件I2C
- 硬件I2C的波形更加规整,每个时钟的周期、占空比都非常一致。
- 数据的写入,都是紧贴下降沿的,SCL下降沿,SDA马上切换数据。
- 应答结束后,SCL下降沿,从机立刻释放SDA,同时zhu机也立刻拉低SDA。所以这里就出现一个小尖峰。