前面我们学习了如何软件去进行I2C通信,这次我们来进行STM32外设I2C通信的学习。我们可以了解到,I2C通信的时序是一个很重要的东西,我们只要理解清楚了时序的意义,就可以按照协议的规定,去翻转引脚的高低电平,只要我们翻转产生的这个时序波形,满足了通信协议的规定,那么通信双方就能理解并解析这个波形。这样通信自然而然就实现了。
之前我们用的是软件I2C手动拉低时钟,再手动对每个数据位进行判断,拉低或释放数据线产生波形。
一.I2C简介

1.I2C收发器
有了I2C收发器,就可以由硬件自动执行时钟生成,起始终止条件生成,应答位收发,数据收发等功能。也就是用硬件电路自动翻转电平,软件呢就是写入控制寄存器CR和数据寄存器DR,就可以实现协议了,当然为了实时监控时序的状态,软件还需要读取状态寄存器SR,来了解外设电路当前处于什么状态
2.多主机
STM32的I2C支持多主多从,一主多从。
一主多从就是假设一个I2C总线,只有一个唯一的主机,下面可以挂在多个从机。
多主多从还分为固定多主机和可变多主机,固定多主机就是这条总线上,有两个或者多个固定的主机,上面始终固定为主机,下面这个几个始终固定为从机。同一时间只能由一个主机进行控制,如果有两个主机想要同时控制,就是总线冲突状态,这时就要进行总线仲裁了,仲裁失败的一方,让出总线的控制权。

可变多主机就是,假如有一条总线,总线上没有固定的主机和从机,任何一个设备,都可以再总线空闲时跳出来,作为主机,然后指定其他任何一个设备进行通信,当这个通信完成之后这个跳出来的主机,就要退回到从机的位置,当有多个从机一起跳出来时,要进行总线仲裁,仲裁失败的一方让出总线控制权。
对于我们STM32而言,使用的是可变多主机模型,虽然我们只需要一主多从,但是STM32是按照可变多主机模型设计的,所以我们还需要按照,谁跳出来,谁就是主机的思路进行编程。
3.地址位模式

我们使用的是7位地址模式。也就是起始条件之后,紧跟的一个字节必须是七位地址+读写位,很显然七位地址只有128钟情况,如果设备非常多就不够用了,所以STM32的I2C总线就支持10位地址模式。最多有1024个设备。
10位地址是如何设计的呢,I2C起始之后的第一个字节必须是寻址+读写位,这个字节只能有七位地址,只需要再规定,起始之后的前两个字节都作为寻址,就可以了。

那么第一个字节有七个空位,第二个字节有八个,按理说是15位,但是I2C只有十位地址模式,还有五位去做了标志位。因为发送了一个字节之后,机器不知道你第二个字节是不是寻址,所以就需要再第一个字节写一个特定的数据,做为十位寻址模式的标志位,这个标志位就是11110。也就是如果说你第二个字节也是寻址,那么第一个字节的前五位必须是11110。

第一个字节剩下的两位,和第二个字节的八位,都作为寻址地址。

I2C协议的速度,如果某个设备声称支持快速I2C那么就是它的最大支持400KHz的时钟频率。当然作为一个同步协议,这个时钟并不严格,所以你只要不超过这个最大的频率多少都可以。

可以再多个字节传输时,提高传输效率,比如指定地址读多字节或者写多字节时候,如果想要连读读或者写多个字节的时候,那么用一下DMA自动帮我们转运,这个过程效率就会大大提升。

这个SMBus时系统管理总线,SMBus是基于I2C改进而来的。主要用于电源管理系统,SMBus和I2C非常像,所以STM32的I2C外设就顺便兼容了一下SMBus协议。

这里是硬件I2C的设置,可以看出这也是硬件I2C和软件I2C的区别,因为硬件I2C必须要有硬件电路的支持,所以硬件I2C的资源是有限的。比如说我们这个型号的STM32最多只能有两路I2C设备总线。但是对于软件I2C,资源一般没有很大的限制,我们只需要复制一下代码,就可以开辟一条新的,I2C总线,所以软件I2C只要代码可以存放的下,基本上是想开几路就可以开几路。
二.I2C外设框图
1.外设框图


左边是我们的SDA和SCL就是I2C通信的两个引脚。

这个是我们SMBus用的。
像这种外设引出来的引脚一般都是借助,GPIO口的复用模式与外部连接。

打开引脚定义表我们可以看到,I2C2的SCL和SDA接再励PB10和PB11端口。

然后I2C1的SCL和SDA复用在了PB6和PB7两个引脚,

另外I2C1的两个引脚还有重映射的机会。

上面是SDA,是我们的数据控制部分,这里数据收发的核心部分是数据寄存器和数据移位寄存器
当我们需要发送数据时,可以把一个字节数据写到数据寄存器DR,当移位寄存器没有数据移位时,这个数据寄存器的值就会进一步,转到移位寄存器里,再移位的过程中,我们就可以把下一个数据放在移位寄存器里面等待了。一旦前一个数据移位完成,下一个数据就可以无缝衔接,继续发送,当数据由数据寄存器转到移位寄存器时,就会置状态寄存器的TXE位为1,表示发送寄存器为空,这就是发送的过程。
那么接收的话,也是这一路,输入的数据一位一位的,从引脚移入到移位寄存器内,当一个字节的移位寄存器收齐之后,数据就整体从移位寄存器,转到数据寄存器。同时置标志位RXNE,表示接收寄存器非空,这时候就可以把数据从数据寄存器读出来了。基本和串口的框图差不多。


串口这里,数据收发也是由数据寄存器和移位寄存器实现的,只不过串口是全双工,发送和接收是分开的,I2C是半双工,数据收发是同一组寄存器。但是这种数据寄存器和移位寄存器的配合,都是异曲同工。有了这一块SDA的数据收发就完成了,我们只需要写入控制寄存器的对应位进行操作就可以了。对于起始条件,终止条件,应答位什么的,这里都有控制电路帮我们完成。

数据收发之后这里还有两个功能,一个是比较器和自身地址寄存器,双地址寄存器。另一个是帧错误校验(PEC)计算和帧错误校验(PEC)寄存器。
比较器和地址寄存器是从机模式使用的,刚刚说明STM32的I2C是根据可变多主机模型设计的,STM32不进行通信的时候就是从机,既然作为从机,就可以被别人招换,想被别人召唤就需要有从机地址,从机地址是多少呢。就可以由这个自身地址寄存器决定,我们可以自定一个从机地址,写道这里,当STM32作为从机再被寻址时,如果收到的寻址,通过比较器判断,和自身地址相同那么STM32就作为从机,响应外部主机的召唤,并且STM32支持同时响应两个从机地址,所以就有自身地址寄存器和双地址寄存器。
这一块我们需要在多主机模型下来理解,把角色转换一下,STM32作为从机,才需要有这一部分,当然我们只要求一主多从的模型。STM32不会作为从机出现,所以这块暂时不需要。

然后右边也是进阶的部分,这是STM32设计的一个校验模块,当我们发送一个多字节的数据帧时,在这里硬件可以自动执行CRC校验计算。CRC是一种很常见的数据校验算法。他会根据前面的这些数据,进行各种数据运算,然后会得到一个字节的校验位,附加在这个数据帧后面,在接受到这一帧的数据后,STM32的硬件也可以自动执行校验的判断。如果数据在传输的过程中出错了,CRC算法校验就会不通过,硬件就会置校验错误标志位。告诉你数据错了,用的时候注意点。这个校验过程就和串口的奇偶校验差不多。也是用于数据有效性校验的。了解即可。
那么SDA这一块我们就只用考虑,这个数据寄存器和移位寄存器配合的这部分就可以了。

我们看SCL这部分。看着没啥东西,时钟控制就是用来控制SCL线的,至于控制的细节这里没有画,就把他当作一个黑盒子,在这个时钟控制寄存器写一个相应的位,电路就会执行对应的功能。
然后控制逻辑电路也是黑盒子。写入控制寄存器,可以对整个电路进行控制。读取状态寄存器可以得知电路的工作状态。

之后是中断,当内部有一些标志位置1之后,可能事件比较紧急,就可以申请中断。如果我们开启了这个中断,那么当这个事件发生后,程序就可以跳到这个中断函数来处理这个事件了。
最后是DMA请求和响应,在进行很多字节收发时,可以配合DMA来提高效率。这个也了解一下就可以。
那么这些就是这个I2C外设的框图了。其实没有很多东西,大多数都是黑盒结构。
2.I2C基本结构图

我们把上面的框图去掉简化,就是中心的框图部分。这里因为I2C是高位先行,所以这个移位是向左移位,在发送的时候,最高位先发送出去,一个SCL时钟移位一次,移位八次,这样就能把一个字节由高位到低位,依次放在总线上了。
那么接受时候,数据通过GPIO口,从右边依次进来,最终移位8次一个字节就完成了。
之后是GPIO口这里,使用硬件I2C的时候,都要注意这两个对应得GPIO口都要切换为复用开漏输出模式。复用就是GPIO口得状态交由片上外设来控制。开漏输出这时I2C协议要求的端口配置,之前说过,这里即使是开漏输出模式,GPIO口也是可以进行输入的。
然后SCL这里时钟控制器通过GPIO去控制时钟线。这里简化为了一主多从的模型,所以时钟这里只画了输出的方向

如果是多主机的模型,时钟线也是会进行输入的。
接着SDA部分输出数据通过GPIO输出到端口,输入数据也是通过GPIO输入到移位寄存器。

来到这个图可以看到,我们要使用复用开漏/推挽输出,上面的P-mos管是没有的。

移位寄存器输出就接再了这个位置。这里写的就是来自片上外设的复用功能输出。所以I2C的外设就接在这里,之后控制N-MOS的通断进而控制这个I/O引脚。

可以看到这里虽然是复用开漏输出,但是I/O引脚的输入部分依然有效,可以通向片上外设。在复用输出模式下,GPIO的输入输出不是由用户的代码直接控制的,而是片上外设。
然后数据控制器是黑河模型。最后还是有个开关控制也就是I2C_COM给I2C外设使能,上电。
三.硬件I2C的操作流程
1.主机发送
当STM32想要进行指定地址写的时候,就要按照主发送器传输序列图来进行

这里有七位地址的主发送,和十位地址的主发送。他们的区别就是,七位地址起始后的一个字节是寻址地址,十位主发送是,起始位后的两个字节是寻址地址。
10位主发送其中前一个字节这里写的是帧头,内容是5位的标志位(11110)+2位地址+1位读写位然后,后一个字节内容就是纯粹的8位地址了。两个字节加一起,构成十位的寻址。
7位主发送的流程就是,起始--从机地址--应答--数据1--应答--数据2--应答.......最后是停止。因为I2C协议只规定了起始后是寻址,至于后面数据的用途并没有明确的规定。这些可以由芯片厂商规定,比如MPU6050规定,寻址后数据1为指定寄存器地址,数据2为指定地址下寄存器的数据。之后数据N就是从指定寄存器地址开始,依次往后写。
我们过一遍流程,首先初始化之后,总线默认空闲状态,STM32默认是从模式,为了产生一个起始条件,STM32需要写入控制寄存器。这个需要看一下手册寄存器描述。


在控制寄存器CR中有一个START位,在这一位写1,就可以产生起始条件了。当起始条件发出后这一位可以由硬件进行清除,所以只要在这一位写1,STM32就自动产生起始条件了。之后STM32由从模式转为主模式,也就是多主机模型下,STM32有数据要发就要跳出来,这个意思。

控制完硬件之后我们就要检查标志位,来看看硬件有没有达到我们想要的状态,在这里起始条件之后会发生EV5事件,这个EV5事件可以把它当成一个标志位。

手册中都是用EV几这个事件来代替标志位的,为什莫要设置这个EV几事件。而不直接说产生什么标志位呢。这是因为,有的状态会同时产生多个标志位,所以这个EV几事件,融合了多个标志位的一个大标志位。在库函数中也有对应的检查EV几事件是否发生的函数。所以就当成一个大标志位理解就可以。

EV5事件就是SB寄存器为1,SB是状态寄存器的一个位。表示了硬件的状态。


在状态寄存器SR1中可以找到这一位。SB为1时,代表起始条件已发送。写数据寄存器也就是DR我们接下来的操作,所以说状态寄存器是不需要手动清除的。
当我们检测起始条件已发送时,就可以发送一个字节的从机地址了,从机地址需要写道数据寄存器DR中。写道DR中之后,硬件就会自动把这一个字节转到移位寄存器里面。再把这一个字节发送到I2C总线上。之后硬件会自动接收应答位然后进行判读。如果没有应答,硬件就会置应答失败的标志位。然后这个标志位可以申请中断来提醒我们。


在寻址完成后后发生EV6事件,然后ADDR标志位,在主模式状态下,代表地址发送结束。

之后说EV8_事件。这时需要我们写入数据寄存器DR,进行数据发送了,一旦写入DR之后,因为移位寄存器也是空,所以DR会立刻转到移位寄存器进行发送。

这时就是EV8事件,这时就是移位寄存器正在进行发数据的状态,所以数据1就产生了,这里要把数据寄存器和移位寄存器配合。

要把这个结构记好,就是发送的时候,数据先写入数据寄存器,如果移位寄存器没有数据,就会转到移位寄存器进行发送

然后写入DR就会清除这个事件,所以这里EV8消除了应该是写入了数据2。在数据1转移到移位寄存器的时刻,数据2就已经写入数据寄存器DR了然后,在数据1接收应答之后,数据2就转入移位寄存器进行发送。此时的状态是移位寄存器非空,数据寄存器空,所以此时EV8事件又发生了。之后数据2还在移位发送,此时数据3已经在数据寄存器DR里面等着了,所以这时候EV8事件消失。之后等数据2应答完成,又产生EV8事件。
也就是说一旦我们检测到EV8事件,就可以写入下一个数据了。最后当我们想要发送的数据写完之后,就没有新的数据可以写入到数据寄存器DR了,当移位寄存器当前的数据移位完成时,此时就是移位寄存器空,数据寄存器空状态

就是我们的EV8_2。TXE等于1,也就是数据寄存器空,BTF就是字节发送结束

在发送时,当一个新数据将被发送,且数据寄存器还未被写入新的数据(TxE=1)BTF标志位置1这个意思就是,当前的移位寄存器已经移完了,该找数据寄存器要下一个数据了。但是一看数据寄存器没有数据,这就说明主机不想发了,这时就代表字节发送结束,是时候停止了,所以在这里当检测到EV8_2时,就可以产生终止条件了。


控制寄存器CR1中,这一位的STOP写入1就会在当前字节的传输,或当前起始条件发出后产生停止条件。
到这里一个完整的时序就发送完成了。简单来说就是,写入控制寄存器CR或者数据寄存器DR,就可以控制时序单元的发生,比如产生一个起始条件,发送一个字节数据,时序单元发生后,检查相应的EV事件,其实就是检查状态寄存器SR。来等待时序单元发送完成。当然在程序中,我们有库函数,不需要实际再去配置寄存器。
2.主机接收

这里有7位主接收,和十位主接收。

从七位主接收来看,起始--从机地址+读--接收应答--接收数据--发送应答--接收数据--非应答。

10位地址就复杂一些,起始--发送帧头(这里帧头的读写位应该还是写的)因为后面还要跟着发送第二个字节的地址--发送第二个字节八位地址--(要想转入读的时序,还要再发送起始条件--发送帧头(这时帧头的读写位就是读的了)因为发送读的指令,必须立刻转入读的时序,所以第二个字节的地址就没有了,直接转入接收数据的时序。
我们主要看七位的就可以,十位的了解即可

首先写入控制寄存器的START位产生起始条件,然后等待EV5事件

之后时寻址,接收应答产生EV6事件。

EV6事件代表寻址已完成,之后数据1,代表数据正在通过移位寄存器进行输入

因为EV6_1事件这里解释,数据1正在移位,还没有收到,所以这个事件没有标志位,之后当这个时序单元完成时,硬件会自动根据我们的配置,把应答位发送出去。如何配置是否要给应答呢。

也是看手册,控制寄存器CR1里,有一个ACK应答使能,如果写1,再接收一个字节后,就返回一个应答。
当这个时序单元结束后,就说明移位寄存器已经成功移入一个字节数据了,这时移入的一个字节数据就整体转移到数据寄存器。同时置RXNE标志位,表示数据寄存器非空,也就是收到了一个数据。

这个状态就是EV7事件。当我们把这个数据读取走之后,这个事件就没有了,当然数据1还没有被读走的时候,数据2就可以直接移入移位寄存器了,之后数据2移位完成收到数据2,产生EV7事件,读走数据2EV7事件就没有了。按照这个流程就可以一直接收数据了。

最后当我们不需要接收时候,需要再最后一个时序单元发生时,提前把刚才的应答位控制寄存器清0。并且设置终止条件请求,这就是EV7_1事件,然后在这个时序结束之后,由于设置了ACK=0,所以这里就会给出非应答,由于设置STOP位,所以产生终止条件。这样接收一个字节的时序就完成了。
就是写入控制寄存器CR和读取数据寄存器DR,产生时序单元,然后等待相应的事件,确保时单元完成就可以了。
3.软件I2C与硬件I2C波形对比

软件I2C波形

硬件I2C波形
首先从两个引脚电平变化的趋势来看,这两个引脚波形都是一样的,对应的数据也都是一样的。

然后从时钟线的波形程度上看,硬件I2C的波形会更加规整,这里硬件I2C每个时钟的周期,占空比都非常一致,而软甲I2C这里由于操作引脚之后,都加入了延时这个延时有时候加的多,有时候加的少,所以软件的时钟周期,占空比可能不规整,不过由于I2C是同步时序,所以这些不规整也没有影响。

还有就是SCL低电平写高电平读。虽然整个电平任意时候都可以读写但是一般要求保证,尽早的原则,所以可以直接认为SCL下降沿写,上升沿读,这里可以看到,软件I2C再下降沿之后,因为操作端口之后有一些延时,所以这里等了一会才进行写入操作。后面的写入也是等了一会。

但是硬件这里,数据写入都是紧贴下降沿的,这里SCL下降沿,SDA立马就切换数据了,再读的时候也是。

这里是应答结束,可以看到从机在SCL下降沿立刻就释放了SDA,但是软件I2C的主机有一个延时过了一会才变化数据。所以就出现了一个短暂的高电平。而硬件I2C应答结束后SCL下降沿,从机立刻释放SDA,同时主机也拉低SDA。
因为I2C是同步时序的原因,这些并不影响我们通信,这也正是同步时序的好处,可以容忍不标准的波形。
四.手册观看

最后可以查一下手册,来综合我们的学习知识点。

主要看的是I2C的主模式。之后是主发送器,主接收器。

多雾标志位,进一步可以触发中断。

发生一些事件也可以触发中断。

之后就是各种寄存器,大家可以看一下。到这里就结束本次学习了。