上次我们学习了STM32的软件I2C读取MPU6050,这次我们使用I2C硬件外设来学习。
一.硬件接线图

软件I2C的两个引脚可以随意更改,但是硬件不可以。

由于I2C的PA6和PA7被oled占用了,所以只能选择PB10和PB11。
二.代码编写
1.编写准备

先复制一下软件I2C的代码,然后重命名。

因为我们用硬件I2C所以把软件的I2C给移除。

然后移除模块文件。

然后来到MOU6050的模块把没用的都给注释。

然后按照流程图,进行我们的配置。
2.库函数学习

三个代码很熟悉了,恢复缺省状态就是复位I2C,初始化I2C,给I2C结构体初始化参数,然后是启动I2C使能,这些我们都很熟悉。

生成起始条件。

可以看一下函数定义,如果新状态不等于disable,就把CR1寄存器的值置1,否则清零。

调用一下生成终止条件。

转到函数就是可以看到,操纵的是CR1寄存器的STOP位。


这个函数是配置CR1寄存器的ACK这一位

对照手册可以看到,就是应答使能。就是STM32作为主机,在接收到一个字节后,是给从机应答,还是非应答,就取决于ACK这一位,如果ACK为1就是给从机应答,如果ACK为0就是非应答

发送数据函数。

实际上就是把Data这个数据,直接写入到DR寄存器

可以看一下手册。这里写的是当一个字节写入到DR,自动开始传输。如果在上一位传输的过程中,及时把下一个数据,放在DR里等着,这样就能保证连续的数据流。

读取数据函数。

可以看到在接收模式下,接收到的字节被拷贝到DR寄存器中这时就是RXNE=1,接收寄存器非空,那么在接收到下一个数据之前读出数据寄存器,即可实现连续的数据传输。

发送七位地址的专用函数,

可以看到Address这个参数也是通过DR发送的,只不过在发送之前,帮我们设置了Address最低位的读写位。如果direction不是发送,那么就把Address最低位置1,也就是读,否则最低位清0也就是写。

这些是用来描述I2C的状态监控标志位的方案函数。

同时判断一个或多个标志位函数,来确定EV几,EV几的状态是否发生,


高级状态监控。实际上就是把SR1和SR2两个状态寄存器,拼接成16位的数据给你,想怎么处理随便都可以。


基于标志位的状态监控。可以判断某一个标志位是否置一了。

读取标志位,清除标志位,读取中断标志位,清除中断标志位,
3.代码编写
(1)I2C初始化

开启I2C的时钟。

开启GPIO的时钟

初始化一下GPIO,把输出模式改为复用开漏输出,开漏是I2C协议的要求,复用是GPIO的控制权要交给硬件外设,我们是硬件I2C所以控制引脚任务肯定得交给外设来做了。如果是之前的软件I2C的话,我们通过软件来控制引脚,就只用把模式改为开漏模式。然后端口改为PIN10和PIN11然后初始化GPIOB。

给结构体起一个新名字,然后引出结构体变量。最后把配置好的参数传给结构体。

配置I2C的模式。

第一个是I2C模式,第二个是SMBus的设备,第三个是SMBus的主机。这里我们选择I2C模式。

时钟速度参数配置。这个参数可以配置SCL的时钟频率,我们给个参数就可以。数值越大传输越快。

转到定义可以看到,这个值最大时400KHz一下的值

看一下PPT。写时钟频率在0~100kHz的话是标准速度,写100~400kHz是快速。

时钟占空比,这个时钟占空比,只有在时钟频率大于100KHz时候,也就是进入到快速模式下才有用。在小于100kHz的标准时钟下,占空比是固定的1:1,也就是为50%。

这里占空比有16:9和2。就是高电平和低电平的时间之比。按理说同步时序,SCL高电平和低电平多少时间应该都没有问题,那么为什么还需要占空比这个参数呢,其实这个占空比是为了快速传输设置的。

这是50kHz的波形上面是SCL下面是SDA。

最前面是起始信号,然后跟着是数据的输出。然后占空比就是1:1高电平低电平各占50%。

100kHz的波形,仍然是标准速度,占空比仍然是1:1。
但是可以看到SCL和SDA下降沿变化的很快,几乎是述职的,但是上升沿确是缓慢的变化上去。这就说明因为我们用的是弱上拉,强下拉模式,所以上拉会缓慢的上去,下拉就会非常快速下来。

101KHz波形。101KHz波形和100KHz差不多,但是101KHz就进入了快速模式,这时I2C会对SCL占空比进行调节,低电平比高电平,由原来的1:1变为了2:1。增大了低电平时间占整个周期的比例。为什莫要增大低电平的比例呢,因为低电平数据变化,高电平数据读取。数据变化需要一定的时间进行翻转波形,尤其是这个数据的上升沿,变化的比较慢。所以在快速传输的状态下,要给低电平多分配一些资源。要不然低电平数据变化来不及,那么高电平数据读取也没用。就像是我们的硬盘一样,读取的速度是大于写入的速度的,所以要想快速传输,需要给低电平的写入多分配一些资源。
这就是标准状态下占空比接近1:1,快速状态下占空比接近2:1的原因。

200KHz的波形,由于时间轴尺度进一步缩小,所以这个缓慢的上升沿更加明显了。所以如果在SCL低电平时间不多给一些SDA变化的时间,都可能来不及数据变化。

极限速度400kHz的波形。

可以看到SCL释放之后还没有完全回弹到高电平,就立马被拉下来了,所以整个SCL波形就变成三角形了。

我们选择占空比为2:1,当然标准模式下,占空比没有用,会被固定在1:1。

应答位配置。这个参数也是配置寄存器的ACK位的。

和这个库函数一样,都是用来确定在接收一个字节后是否给从机应答。

我们给enable接收应答,然后需要更改的化调用库函数就好。

这个是STM32作为从机,可以响应几位的地址。

可以选择十位或者七位。

我们选择7位。

自身地址1,这个也是STM32作为从机使用的。用于指定STM32的自身地址方便别的主机呼叫它。如果上一个参数选择了响应7位地址,下面这里就可以给STM32指定一个自身的7位地址。如果上面的选择十位,这里就选择添加一个10位的地址。
但是还是那句话,我们STM32暂时不需要做从机被别人使唤,所以这个地址可以随便给一个。只要不和总线上其他设备的地址重复就可以了。

给个0x00吧,到这里STM32的I2C的初始化就完成了。

最后我们开启I2C2就可以了。
(2)指定地址写一个字节

我们对照着主机发送的序列图来写程序。
首先是起始条件

就是这个函数

生成起始条件。

对应着软件I2C的START,但是软件I2C这些函数,内部都是有delay的。是一种阻塞式的流程,也就是函数运行完成之后,对应的波形也肯定发送完毕了。所以上一个函数运行完成之后,就可以紧跟下一个函数了。

但是这个函数,包括后面的硬件I2C函数,都不是阻塞式的。这些硬件I2C函数,只管给寄存器的位置1。或者只在DR写入数据,就结束,退出函数。对于波形是否发送完毕,他是不管的。所以对于这种非阻塞式的函数,我们都需要在函数结束之后,等待相应的标志位。来确保这个函数的操作执行到位了。

当起始条件的波形确实发出来,会产生EV5事件。所以我们要等待EV5事件到来。

检查标志位

第二个参数指定检查什么事件,可以在这里面选择。

选择EV5参数,库函数还给EV5事件起来一个新名字,叫主机模式选择,因为STM32默认为从机,发送起始条件后变为主机,所以EV5事件也可以叫做,主机模式已选择的事件。

复制EV5事件进行选择。

看一下返回值,SUCCESS表示最后一次事件等于我们指定的事件。也就是指定事件发生了。或者ERROR指定事件没有发生。

如果不成功就一直等待。

发送从机地址 就是向DR寄存器写入一个值,我们用这个函数。

第一个参数I2C2,第二个参数从机地址,我们直接复制宏定义的从机地址。第三个参数是方向,也就是从机地址最低位,读写位

如果选择TRANSMITTRr是发送,他就会给你地址最低位清0。如果选择RECEIVE接收,就会给你的最低位置1。

选择发送。然后接收应答,这里不需要一个库函数来操作,在这个库函数中,发送数据都自带接收应答的过程。同样接收数据也自带了发送应答的过程。如果发送错误,硬件会通过中断和置标志位来提醒我们。所以发送地址之后,应答位就不需要处理了。我们直接等待事件。

可以看到当地址发送接收应答之后,会产生一个EV6事件。

但是这里有2个EV6事件,我们看一下啊。通过名字可以看出,第一个是发送位置已选择,第二个是接收位置已选择。目前我们是主机发送的时序,所以要复制上面一个。

替换一下。
这里EV6事件结束之后有一个EV8_1事件,是告诉你该写入DR发送数据了。我们并不需要等待这个EV8_1事件。

在库函数里可以看一下,这里也是没有EV8_1事件的参数,所以这时我们直接写入DR寄存器。

复制,然后发送数据。

这个就是这个字节会存在MPU6050的当前地址指针里,用于指定读写哪个寄存器

之歌时刻我们写入了DR,DR立刻转移到移位寄存器进行发送,此时波形产生。我们写入DR后需要等待的是EV8事件。可以看出EV8事件出现的非常快,基本是不用等的。因为有两级缓存。第一个数据写入DR会立刻被转移到移位寄存器。这时不用等第一个数据发完,第二个数据就可以写入到数据寄存器等着了,那么在程序中,我们写完呢DR之后,还是要理性检查一下EV8事件的。

复制一下EV8事件。

例行检查EV8。他的名字是字节正在发送。

如果等到了EV8事件可以发送下一个数据,发送第三个字节 就是指定要写入的寄存器地址下的数据了。
等待事件,因为我们的Data是最后一个字节 发送完了就需要停止了

当我们有连续的数据需要发送时,在发送过程中我们需要等待的时EV8事件,而当我们发送完最后一个字节时需要等待的就是EV8_2事件了。什么时候会产生EV8_2事件呢

就是BTF标志位为1。也就是移位完成了并且没有新的数据可以发送的时候。置BTF,也就是EV8_2事件。
所以在这个时序的最后我们需要等待硬件把两级缓存,所有数据清空。才能产生终止条件。

找到EV8_2事件。他的名字是字节已经发送完成。

最后总结:在发送过程中我们需要等待的时EV8事件,而当我们发送完最后一个字节时需要等待的就是EV8_2事件了

发送完成后就可以终止了。


这样我们就用硬件I2C代码,替换掉了软件I2C代码,着两种方式产生的波形是一样的。完成的任务也是一样的。不过可以看出两者代码上的区别还很很大的。但是只要我们把I2C协议理解了,硬件外设也研究清除了,这些操作其实还是不难的。
(3)指定地址读一个字节
那么我们来写一下指定地址读寄存器函数

上面这里复合格式的前一部分和上面是一样的

所以我们复制一下。在指定地址之后要生成重复起始条件。

这里可以看一下,是用mitting还是mitted呢,如果用mitting的化那实际这个事件发生时RegADDRESS的波形还没有发送完成,这时直接产生重复起始条件的化,会不会把这个数据截断呢,答案是不会的。当我们调用起始条件之后,如果当前还有字节正在移位,那这个起始条件就会延迟,等待当前字节发送完毕后,才能产生。所以上面这里是用mitting还是mitted呢都没有问题。如果用mitting那下面重复起始条件之后将会等待,如果是mitted就是在上面等待。

等波形全部发送完成再产生新的起始条件。

按照设计要求来,我们上面用MITTED。

接下来我们参考主机接收的序列图。

在这里起始条件之后,需要等待EV5事件。

所以复制这一条就可以。

第三个参数改为读取。再指定了Receiver这个参数之后,函数内部就自动把这个地址最低位置1,就不需要我们手动 | 0x01了。然后再寻址之后我们也是等待EV6事件。

复制一下。但是主机接收的EV6并不是主机发送的EV6,所我们转到定义

可以看到有两个EV6一个是主机发送EV6事件一个主机接收EV6事件。所以我们要用下面这个RECEIVER这个参数。

完成编写。
进入到主机接收的模式之后,就可以接受从机发送的数据波形了

看一下序列图,再接收一个数据时,会触发EV6_1事件。

这个事件没有标志位,也不需要我们等待。只适用于接受一个字节的情况。正好我们是指定地址读一个字节。所以EV6之后恰好是清除响应和停止条件的产生。
也就是告诉我们要把应答位ACK置0,同时把停止条件生成位SYOP置1。那你可能会问,这时不应该是接受数据吗,数据都没有收到就要产生停止条件吗。答案确实如此,这里规定在接受最后一个字节之前,就要提前把ACK置0,同时设置停止位STOP。因为我们是接收一个字节。所以再进入接受模式之后就要立刻ACK置0,同时设置停止位STOP。
为什么这样设计呢,我们看一下序列图

这里如果你不提前再数据还没有收到的时候给ACK置0。

那么等到了这里数据已经收到了,你再说要置0,要给非应答。这时就晚了,数据收到之前应答位就已经发送出去了,时序不等人。硬件在ACK时钟周期开始时会立即使用预设的ACK状态。当从设备(如MPU6050)收到ACK=1时,它会预先准备好下一个数据字节,所以我们需要再最后一个字节提起发送非应答,因为传送数据会在DR数据寄存器放入移位数据寄存器的时候,另一个数据会准备再数据寄存器里面。即使我们提前设置了ACK=0,硬件也不会立即执行,而是执行完当前的逻辑,在进行ACK=0。
同时这里也建议我们提前设置STOP终止条件。这个终止条件也不会截断当前字节他会等当前字节接收完成后,再产生终止条件的波形。
总结一下就是,如果你是读取多个字节,那么直接等待EV7事件,读取DR就能收到数据了,这样依次接受,在接受最后一个字节之前就是EV7_1事件需要提取把ACK置0,STOP置1。
如果你是读取一个字节,那么在EV6事件之后,就要立刻ACK置0,STOP置1。要不然设置晚了,时序上会多出一个字节。
因为我们只需要读取一个字节,所以在EV6事件之后,就要把ACK置0

复制函数。

ACK置0。

给一个停止位。

这些做完之后我们再等待EV7事件,EV7事件就是接收一个字节后会产生。

然后是EV7事件。

等待EV7产生。产生EV7后一个字节数据就已经再DR里面了。

我们复制读取DR寄存器函数。

他的返回值就是DR数据寄存器的值,我们存起来。

最后把ACK置1,不要影响我们其他的数据,我们的想法是默认状态下,ACK就是1,给从机应答再收最后一个字节之前,把ACK临时变为0,告诉从机非应答。所以再接收函数的最后在恢复默认的ACK=1,这个流程是为了方便指定地址收多个字节的。
虽然我们程序中自始至终,都是只有收一个字节的,我们没有给过应答,但是形式还要写一下的,方便我们后续改代码。因为默认ACK=1,是为了方便收多个字符的。
这样整个代码由软件I2C到硬件I2C的转变就完成了。
(4)测试一下

编译一下0错误0警告。

下载一下,发现没有问题,ID和各轴数据显示完整。
(6)解决死循环问题

可以看到程序中,存在了大量的死循环等待,这种死循环是非常危险的,一旦有一个事件一直没有产生,就会让整个程序卡死。所以这种死循环等待,我们可以加一个超时退出机制。

定义变量给一个较大的数。

然后重写while语句,完成一个超时退出的机制。

也可以封装一下。

把形参传过来,变为一个通用的等待函数。

这样封装之后把所有的checkevent都变为waitevent就可以了。

直接修改函数,让他们再函数内部进行比较。

所有的while都可以替换为我们新写的函数。这样看着代码比较整洁清晰。

编译一下0错误。下载测试

发现程序现象也是没有问题的。

如果再项目中,这个超时退出的时刻就不能是break了。这里还要做一些相应的错误处理。比如说打印错误日志,或者进行系统复位。如果项目涉及危险的机械结构,是不是就要进行紧急停止的操作等等。
这样代码才会更加健全,更加安全。