对于I2C通信会分为两大块来讲解,第一块,就是介绍协议规则,然后用软件模拟的形式来实现协议,第二块,就是介绍STM32的12C外设,然后用硬件来实现协议,因为12C是同步时序,软件模拟协议也非常方便。
在学12C之前,我们已经学习了串口通信,串口通信,就是从TX引脚向RX引脚发送数据流,数据流以字节为单位,数据流以字节为单位,我们可以组合多个字节,另外串口通信的设计是,一条发送线、一条接收线,没有时钟线的异步全双工的协议。
如果现在需要设计一种在单片机和外部模块连接少量的几根线,实现单片机读写外部模块寄存器的功能的通信协议(基本要求)。其中
-
要求1:删掉一根通信线,只能在同一根线上进行发送和接收,也就是把全双工变成半双工。
-
要求2:所以为了安全起见,要求增加应答机制,要求每发送一个字节,对方都要给我个应答,每接收一个字节,我也要给对方一个应答。
-
第3个要求:一根线上能同时接多个模块,单片机可以指定,和任意一个模块通信,同时单片机在跟某个模块进行通信时,其他模块不能对正常的通信产生干扰。
-
第4个要求:串口是异步的时序,也就是发送方和接收方约定的传输速率是非常严格的,时钟不能有过大的偏差,也不能说在传输过程中,单片机有点事,进中断了,对于异步时序来说,这是不能中断的,单片机一个字节发一半暂停了,接收方可是不知道的,它仍然会按照原来那个约定的速率读取,这就会导致传输出错,所以异步时序的缺点就是,非常依赖硬件外设的支持,必须要有USART电路才能方便地使用,如果没有USART硬件电路的支持,那么串口是很难用软件来模拟的。所以要求要把这个协议改成同步的协议,另外加一条时钟线来指导对方读写,由于存在时钟线,对传输的时间要求就不高了,单片机也可以随时暂停传输,去处理其他事情,因为暂停传输的同时,时钟线也暂停了,所以传输双方都能定格在暂停的时刻,可以过一段时间再来继续,不会对传输造成影响,这就是同步时序的好处,使用同步时序就可以极大地降低单片机对硬件电路的依赖,即使没有硬件电路的支持,也可以很方便地用软件手动翻转电平来实现通信。异步时序的好处就是省一根时钟线,节省资源,缺点就是对时间要求严格,对硬件电路的依赖比较严重,同步时序的好处就是对时间要求不严格,对硬件电路不怎么依赖,在一些低端单片机,没有硬件资源的情况下,也很容易使用软件来模拟时序,缺点就是多一根时钟线。
一、I2C通信简介
-
基本定义与开发者1:
-
I2C(Inter-Integrated Circuit)总线是由 Philips 公司开发的一种简单、双向二线制同步串行总线,用于在集成电路之间进行短距离数据传输。也有人将其原缩写 IIC 写成 I²C,后因输入不便简化为 I2C,但意思相同。
-
-
硬件组成与连接方式:
-
总线线路:I2C 总线只需要两根线,即串行数据线(SDA)和串行时钟线(SCL)2。
-
SDA 线是双向的数据传输线,用于主机与从机之间收发数据通信,一根线兼具发送和接收,最大化利用资源。同一时间只能进行数据发送或者接收,不能同时进行。(满足要求1)
-
SCL 线由主机控制,是用于主机与从机间的时钟同步信号线。(满足要求4)
-
-
上拉电阻:SDA 和 SCL 信号引脚是开漏输出,只能输出低电平不能输出高电平,所以需要通过上拉电阻接电源 VCC,以保证总线空闲时为高电平。上拉电阻阻值一般为 4.7KΩ 左右,常用的还有 1K、1.5K、2.2K、5.1K、10K 等3。
-
设备连接:所有设备的 SDA 线并接在一起,所有设备的 SCL 线也并接在一起。总线上的每个设备都有一个特定的设备地址,以区分同一 I2C 总线上的其他设备。
-
-
通信模式1:
-
主从模式:I2C 总线采用一主多从的通讯模式。主器件用于启动总线传送数据,并产生时钟以开放传送,被寻址的器件均被认为是从器件。在总线上主和从、发和收的关系不是恒定的,取决于此时数据传送方向。单片机作为主机,主导I2C总线的运行,挂载在12C总线的所有外部模块都是从机,从机只有被主机点名之后才能控制I2C总线,不能在未经允许的情况下去碰12C总线,防止冲突。
-
多主模式:多主多从,I2C 总线是一个真正的多主机总线,如果两个或多个主机同时初始化数据传输,可以通过冲突检测和仲裁防止数据破坏。在总线上任何一个模块都可以主动跳出来说,接下来我就是主机,你们都得听我的,但是,同一个时间只能有一个人说话,这时就相当于发生了总线冲突,在总线冲突时,12C协议会进行仲裁,仲裁胜利的一方取得总线控制权,失败的一方自动变回从机,当然由于时钟线也是由主机控制的,所以在多主机的模型下,还要进行时钟同步,多主机的情况下,协议是比较复杂的。(满足要求3)
-
-
数据传输过程:
-
起始条件与终止条件23:
-
起始条件是在 SCL 高电平期间,SDA 从高电平切换到低电平;终止条件是在 SCL 高电平期间,SDA 从低电平切换到高电平。起始和终止都是由主机产生,在总线空闲状态时,从机不允许主动去操作总线。
-
-
数据传输格式1:发送到 SDA 线上的每个字节必须为 8 位,每次传输可以发送的字节数量不受限制。每个字节后必须跟一个响应位。首先传输的是数据的最高位(MSB)。
-
应答机制1:数据传输必须带响应,相关的响应时钟脉冲由主机产生。在响应的时钟脉冲期间发送器释放 SDA 线(高),接收器必须将 SDA 线拉低,使它在这个时钟脉冲的高电平期间保持稳定的低电平。(满足要求2)
-
时钟同步:所有主机在 SCL 线上产生它们自己的时钟来传输 I2C 总线上的报文。数据只在时钟的高电平周期有效,时钟同步通过线与连接 I2C 接口到 SCL 线来执行1。
-
-
传输速率1:
-
标准模式下可达 100kbit/s;快速模式下可达 400kbit/s;高速模式下可达 3.4Mbit/s。
-
-
应用场景:I2C 总线已经被广泛地应用于连接各种传感器、液晶屏幕、存储器等电子设备当中。如MPU6050模块,OLED模块,AT24C02存储器模块,DS3231实时时钟模块等等。
了解了I2C之后,为了了解I2C是怎么样实现这么多功能的,而且作为通信协议,它必须要在硬件(电路应该如何连接、端口的输入输出模式都是啥样的)和软件(时序是怎么定义的、字节如何传输,高位先行还是低位先行、一个完整的时序由哪些部分构成)上都作出规定,硬件的规定和软件的规定配合起来,就是一个完整的通信协议
二、I2C的硬件规定
-
连接方式
-
所有 I2C 设备的 SCL(串行时钟线)应连接在一起,SDA(串行数据线)也应连接在一起。
-
这意味着所有 I2C 设备在硬件层面上共享 SCL 和 SDA 线,形成一个总线结构。
-
-
输出模式
-
设备的 SCL 和 SDA 均应配置成开漏输出模式。
-
开漏输出模式意味着这些引脚在输出低电平时可以直接拉低,但在输出高电平时需要外部上拉电阻来拉高。
-
因为现在是一主多从,主机拥有SCL的绝对控制权,所以主机的SCL可以配置成推挽输出,所有从机的SCL都配置成浮空输入或者上拉输入,数据流向是,主机发送,所有从机接收。但是SDA线,因为这是半双工的协议,所以主机的SDA在发送的时候是输出,在接收的时候是输入,同样,从机的SDA也会在输入和输出之间反复切换,如果能协调好输入输出的切换时机,那其实也没问题,但是这样做,如果总线时序没协调好,极有可能发生两个引脚同时处于输出的状态,如果这时又正好是一个输出高电平,个输出低电平,那这个状态就是电源短路,这个状态需要避免。所以为了避免总线没协调好导致电源短路这个问题,12C的设计是,禁止所有设备输出强上拉的高电平,采用外置弱上拉电阻加开漏输出的电路结构,
-
-
在接收的时候是输入上拉电阻
-
SCL 和 SDA 各需添加一个上拉电阻,阻值一般为 4.7KΩ 左右。
-
上拉电阻的作用是在没有设备拉低 SCL 或 SDA 线时,将其拉高到高电平状态。这是通过一个电阻拉到高电平的,所以这是一个弱上拉。
-
用之前将的弹簧和杆子的模型来解释就是,SCL或SDA就是一根杆子,为了防止有人向上推杆子,有人向下拉杆子,造成冲突,就规定,所有的人,不准向上推杆子,只能选择往下拉或者放手,然后,再外置一根弹簧向上拉,要输出低电平,就往下拽,弹簧被拉伸,杆子处于低电平状态;要输出高电平,就放手,杆子在弹簧的拉力下,回弹到高电平,这就是一个弱上拉的高电平,但是完全不影响数据传输。
-
这样做的好处:第一,完全杜绝了电源短路现象,保证电路的安全。第二,避免了引脚模式的频繁切换,开漏加弱上拉的模式,同时兼具了输入和输出的功能,要是想输出,就去拉杆子或放手,操作杆子变化就行了;要是想输入,就直接放手,然后观察杆子高低就行了,因为开漏模式下,输出高电平就相当于断开引脚,所以在输入之前,可以直接输出高电平,不需要再切换成输入模式了。第三,就是这个模式会有"线与"的现象,就是只要有任意一个或多个设备输出了低电平,总线就处于低电平,只有所有设备都输出高电平,总线才处于高电平。
-
12C可以利用这个电路特性,执行多主机模式下的时钟同步和总线仲裁,所以这里SCL虽然在一主多从模式下可以用推挽输出,但是它仍然采用了开漏加上拉输出的模式,因为在多主机模式下会利用到这个特征。
-
图示内容
-
左侧电路模型
-
图中左侧展示了一个典型的 I2C 总线连接模型,一主多从的模式。
-
从 CPU(中央处理器)作为总线的主机引出 SCL 和 SDA 线,连接到 I2C 总线上。主机的权利很大,包括,对SCL线的完全控制,任何时候,都是主机完全掌控SCL线,另外在空闲状态下,主机可以主动发起对SDA的控制,只有在从机发送数据和从机应答的时候,主机才会转交SDA的控制权给从机。
-
在 SCL 和 SDA 线上各有一个上拉电阻(R),连接到电源(Vdd)。
-
总线上连接了多个被控 IC(集成电路),挂载在I2C总线上的从机,这些从机可以是姿态传感器、OLED、存储器、时钟模块等等,从机的权利比较小,对于SCL时钟线,在任何时刻都只能被动的读取,从机不允许控制SCL线,对于SDA数据线,从机不允许主动发起对SDA的控制,只有在主机发送读取从机的命令后,或者从机应答的时候,从机才能短暂地取得SDA的控制权,每个 IC 都有自己的 SCL 和 SDA 引脚。
-
-
右侧电路模型
-
图中右侧展示了所有的设备,包括CPU和被控IC,它引脚的内部结构。
-
有 SCL 和 SDA 两条线,分别连接到 SCL IN 和 DATA IN(输入)以及 SCL OUT 和 DATA OUT(输出)。
-
这表示数据和时钟信号的输入和输出方向。
-
首先引脚的信号进来,都可以通过一个数据缓冲器或者是施密特触发器,进行输入,因为输入对电路没有任何影响,所以任何设备在任何时刻都是可以输入的,但是在输出的这部分,采用的是开漏输出的配置,这里输出低电平,这个开关管导通,引脚直接接地,是强下拉,输出高电平,这个开关管断开,引脚什么都不接,处于浮空状态,这样的话,所有的设备都只能输出低电平而不能输出高电平,为了避免高电平造成的引脚浮空,这时就需要在总线外面,SCL和SDA各外置一个上拉电阻。
-
三、I2C的软件规定
基本时序单元
-
起始条件
-
当 SCL(时钟线)处于高电平期间,SDA(数据线)从高电平切换到低电平,表示 I2C 通信的开始。
-
图中用波形图展示了这一过程,SCL 保持高电平,SDA 从高电平下降到低电平。
-
在12C总线处于空闲状态时,SCL和SDA都处于高电平状态,也就是没有任何一个设备去碰SCL和SDA,SCL和SDA由外挂的上拉电阻拉高至高电平,总线处于平静的高电平状态,当主机需要进行数据收发时,首先就要打破总线的宁静,产生一个起始条件,这个起始条件就是:SCL处于高电平不去动它,然后把SDA拽下来,产生一个下降沿,当从机捕获到这个SCL高电平,SDA下降沿信号时,就会进行自身的复位,等待主机的召唤,然后在SDA下降沿之后,主机要再把SCL拽下来;拽下SCL,一方面是占用这个总线,另一方面也是为了方便我们这些基本单元的拼接,就是之后会保证,除了起始和终止条件,每个时序单元的SCL都是以低电平开始,低电平结束。这样这些单元拼接起来,SCL才能续得上。
-
-
终止条件
-
当 SCL(时钟线)处于高电平期间,SDA(数据线)从低电平切换到高电平,表示 I2C 通信的结束。
-
图中用波形图展示了这一过程,SCL 保持高电平,SDA 从低电平上升到高电平。
-
SCL先放手,回弹到高电平,SDA再放手,回弹高电平,产生一个上升沿,这个上升沿触发终止条件,同时终止条件之后,SCL和SDA都是高电平,回归到最初的平静状态。
-
-
总结
-
这个起始条件和终止条件就类似串口时序里的起始位和停止位,一个完整的数据帧,总是以起始条件开始、终止条件结束,另外,起始和终止,都是由主机产生的,从机不允许产生起始和终止,所以在总线空闲状态时,从机必须始终双手放开,不允许主动跳出来,去碰总线,如果允许的话,那就是多主机模型了。
-
-
发送一个字节
-
在 SCL(时钟线)低电平期间,主机将数据位依次放到 SDA(数据线)上,从高位(B7)开始,依次到低位(B0)。主机在 SCL 低电平期间改变 SDA 的数据位。然后释放 SCL,从机将在 SCL 高电平期间读取数据位。在 SCL 高电平期间,SDA 不允许有数据变化。所以高电平期间,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位数据,也就是一个字节,另外注意,这里是高位先行,所以第一位是一个字节的最高位B7,与串口不同。另外,由于这里有时钟线进行同步所以如果主机一个字节发送一半,突然进中断了,不操作SCL和SDA了,那时序就会在中断的位置不断拉长,SCL和SDA电平都暂停变化,传输也完全暂停,等中断结束后,主机回来继续操作,传输仍然不会出问题,这就是同步时序的好处,最后就是,由于这整个时序是主机发送一个字节,所以在这个单元里,SCL和SDA全程都由主机掌控,从机只能被动读取,这就是发送一个字节的时序。
-
-
接受一个字节
-
SCL低电平期间,从机将数据位依次放到SDA线上(高位先行),然后释放SCL,主机将在SCL高电平期间读取数据位,所以SCL高电平期间SDA不允许有数据变化,依次循环上述过程8次,即可接收一个字节(主机在接收之前,需要释放SDA,让从机取得SDA的控制权)
-
为什么主机在接收之前,需要释放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,就说明从机确实收到了,这个场景就是,主机刚发送一个字节,然后说,有没有人收到啊,我现在把SDA放手了哈,如果有人收到的话,你就把SDA拽下来,然后主机高电平读取数据,发现确实有人给它拽下来了,那就说明有人收到了;如果主机发现,我松手了,结果这个SDA就跟着回弹到高电平了,那就说明没有人回应,刚发的一个字节可能没人收到,或者它收到了但是没给我回应。
-
完整时序(数据帧)
本文12C是一主多从的模型,主机可以访问总线上的任何一个设备,那如何发出指令,来确定要访问的是哪个设备呢?这就需要首先把每个从设备都确定一个唯一的设备地址,从机设备地址就相当于每个设备的名字,主机在起始条件之后,要先发送一个字节叫一下从机名字,所有从机都会收到第一个字节,和自己的名字进行比较,如果不一样,则认为主机没有叫我,之后的时序我就不管了,如果一样,就说明,主机现在在叫我,那我就响应之后主机的读写操作,在同一条12C总线里,挂载的每个设备地址必须不一样,否则,主机叫一个地址,有多个设备都响应,就会造成混乱。从机设备地址,在12C协议标准里分为7位地址和10位地址,本文只讨论7位地址的模式,因为7位地址比较简单而且应用范围最广。在每个12C设备出厂时,厂商都会为它分配一个7位的地址,这个地址具体是什么,可以在芯片手册里找到,比如MPU6050这个芯片的7位地址是1101 000,一般不同型号的芯片地址都是不同的,相同型号的芯片地址都是一样的,那如果有相同的芯片挂载在同一条总线怎么办呢,这就需要用到地址中的可变部分了,般器件地址的最后几位是可以在电路中改变的,比如MPU6050地址的最后一位,就可以由板子上的ADO引脚确定,这个引脚接低电平,那它的地址就是1101000,这个引脚接高电平,那它的地址就是1101001。这样,即使相同型号的芯片,挂载在同一个总线上,也可以通过切换地址低位的方式,保证每个设备的地址都不一样,这就是I2C设备的从机地址。
-
指定地址写
-
对于指定设备(Slave Address 从机地址来确定),在指定地址(Reg Address 就是某个设备内部的RegAddress,寄存器地址)下,写入指定数据(Data 写入寄存器的数据)
-
对于时序图的解析:在这里,上面的线是SCL,下面的线是SDA,空闲状态,它俩都是高电平,然后主机需要给从机写入数据的时候,首先,SCL高电平期间,拉低SDA,产生起始条件(Start,S),在起始条件之后,紧跟着的时序,必须是发送一个字节的时序,字节的内容,必须是从机地址+读写位,正好从机地址是7位,读写位是1位,加起来是一个字节(8位),发送从机地址,就是确定通信的对象,发送读写位,就是确认我接下来是要写入还是要读出。具体发送的时候,低电平期间,SDA变换数据高电平期间,从机读取SDA,这里用绿色的线,来标明了从机读到的数据。
-
时序案例:
-
首个字节:高7位,表示从机地址,比如这个波形下,主机寻找的从机地址就是11010000,这个就是MPU6050的地址,然后最低位,表示读写位,0表示,之后的时序主机要进行写入操作,1表示,之后的时序主机要进行读出操作,这里,是0,说明之后我们要进行写入操作那目前主机是发送了一个字节,字节的内容转换为16进制,高位先行,就是0xD0。
-
应答位:然后根据协议规定,紧跟着的单元,就得是接收人机的应答位(Receive Ack,RA),在这个时刻,主机要释放SDA,所以如果单看主机的波形,应该是释放SDA之后,引脚电平回弹到高电平,但是根据协议规定,从机要在这个位拉低SDA,所以单看从机的波形,应该是该应答的时候,从机立刻拽住SDA,然后应答结束之后,从机再放开SDA,那现在综合两者的波形,结合线与的特性,在主机释放SDA之后,由于SDA也被从机拽住了,所以主机松手后,SDA并没有回弹高电平,这个过程,就代表从机产生了应答,最终高电平期间,主机读取SDA,发现是0,就说明,我进行寻址,有人给我应答了,传输没问题。如果主机读取SDA,发现是1就说明,我进行寻址,应答位期间,我松手了但是没人拽住它,没人给我应答,那就直接产生停止条件吧,并提示一些信息。后面的上升沿,就是应答位结束后,从机释放SDA产生的,从机交出了SDA的控制权,因为从机要在低电平尽快变换数据,所以这个上升沿和SCL的下降沿,几乎是同时发生的。
-
发送字节:同样的时序,再来一遍,第二个字节,就可以送到指定设备的内部了,从机设备可以自己定义第二个字节和后续字节的用途,般第二个字节可以是寄存器地址或者是指令控制字等;比如MPU6050定义的第二个字节就是寄存器地址,比如AD转换器,第二个字节可能就是指令控制字;比如存储器,第二个字节可能就是存储器地址,那图示这里,主机发送这样一个波形,一一判定,数据为00011001,即主机向从机发送了0x19这个数据,在MPU6050里,就表示我要操作你0x19地址下的寄存器了,接着同样,是从机应答,接着同样,是从机应答。
-
应答位:主机释放SDA,从机拽住SDA,SDA表现为低电平,主机收到应答位为0,表示收到了从机的应答。
-
发送字节:这个字节就是主机想要写入到0x19地址下寄存器的内容了,比如我这里发送了0xAA的波形,就表示,我要在0x19地址下,写入0xAA。
-
接收应答位:如果主机不需要继续传输了,就可以产生停止条件(Stop,P)。
-
停止位:先拉低SDA,为后续SDA的上升沿作准备然后释放SCL,再释放SDA,这样就产生了SCL高电平期间,SDA的上升沿。
-
这样完整的时序就结束了。这个数据帧的目的就是,对于指定从机地址为1101000的设备,在其内部0x19地址的寄存器中,写入OXAA这个数据,这就是指定地址写的时序。
-
-
-
当前地址读
-
对于指定设备(Slave Address),在当前地址指针指示的地址下,读取从机数据(Data)
-
时序案例:
-
起始位:那最开始,还是SCL高电平期间,拉低SDA,产生起始条件。
-
发送字节:起始条件后,主机必须首先调用发送一个字节,来进行从机的寻址和指定读写标志位,比如图示的波形,表示本次寻址的目标是1101000的设备,同时,最后一位读写标志为1,表示主机接下来想要读取数据。。
-
应答位:紧跟着,发送一个字节之后,接收一下从机应答位,人机应答0,代表从机收到了第一个字节。
-
主机接受字节:从应答位结束之后,数据的传输方向就要反过来了因为刚才主机发出了读的指令,所以这之后,主机就不能继续发送了,要把SDA的控制权交给从机,主机调用接收一个字节的时序,进行接收操作。后面从机就得到了主机的允许,可以在SCL低电平期间写入SDA,然后主机在SCL高电平期间读取SDA,那最终,主机在SCL高电平期间依次读取8位,就接收到了从机发送的一个字节数据,00001111,也就是0x0F。
-
补充(当前地址读时序的操作逻辑):并且,会有一个单独的指针变量那这个数据是从机哪个寄存器的数据呢?可以知道在读的时序中,12C协议的规定是,主机进行寻址时,一旦读写标志位给1了,下一个字节就要立马转为读的时序,所以主机还来不及指定,想要读哪个寄存器,就得开始接收了,所以这里就没有指定地址这个环节,那主机并没有指定寄存器的地址,从机到底该发哪个寄存器的数据呢,这就需要用到上面说的当前地址指针了,在从机中,所有的寄存器被分配到了一个线性区域中,指示着其中一个寄存器,这个指针上电默认,一般指向0地址,并且,每写入一个字节和读出一个字节后,这个指针就会自动自增一次,移动到下一个位置,那么在调用当前地址读的时序时,主机没有指定要读哪个地址,从机就会返回当前指针指向的寄存器的值。那假设,我刚刚调用了这个指定地址写的时序,在0x19的位置写入了0xAA,那么指针就会+1,移动到0x1A的位置,我再调用这个当前地址读的时序,返回的就是0x1A地址下的值,如果再调用一次呢,返回的就是0x1B地址下的值,以此类推。由于不能指定读的地址,这个时序使用的频率也不是很高。
-
-
-
指定地址读:
-
对于指定设备(Slave Address),在指定地址(Reg Address)下,读取从机数据(Data)
-
将指定地址写,后面写入数据的那一部分时序拿掉,然后把前面一段设置地址,还没有指定写什么数据的时序,给它追加到这个当前地址读时序的前面,就得到了指定地址读的时序,一般把它称作复合格式。前面有指定地址写,但是只指定了地址,还没有写入数据 ;后面的部分是当前地址读,因为刚刚指定了地址,所以在调用当前地址读两者就加在一起了,就是制定地址读了,所以,指定地址读的时序会复杂一些。
-
时序案例:
-
起始位:首先,最开始,仍然是启动条件。
-
发送字节:然后发送一个字节,进行寻址,这里指定从机地址是1101000,读写标志位是0,代表要进行写的操作。
-
应答位:从机应答。(也可以在这里再加入一个停止条件,因为写入的地址会存在地址指针里面,所以这个地址并不会因为时序的停止而消失)
-
发送字节:第二个字节,用来指定地址,这个数据就写入到了从机的地址指针里了,也就是说,从机接收到这个数据之后,它的寄存器指针就指向了0x19这个位置。之后,我们要写入的数据,不给它发,而是直接再来个起始条件。
-
重复起始位:这个Sr(Start Repeat)的意思就是重复起始条件,相当于另起一个时序,因为指定读写标志位只能是跟着起始条件的第一个字节,所以如果想切换读写方向只能再来个起始条件。
-
发送字节:然后起始条件后,重新寻址并且指定读写标志位,此时读写标志位是1,代表这就是指定地址读要开始读了,接着,主机接收一个字节这个字节就是0x19地址下的数据,这就是指定地址读。
-
应答位:主机非应答。在读完一个字节之后,一定要给从机发个非应答,(Send Ack,SAD),就是该主机应答的时候,主机不把SDA拉低,从机读到SDA为1,就代表主机没有应答,从机收到非应答之后,就知道主机不想要继续了,从机就会释放总线,把SDA控制权交还给主机,如果主机读完仍然给从机应答了,从机就会认为主机还想要数据,就会继续发送下一个数据,而这时,主机如果想产生停止条件,SDA可能就会因为被从机拽住了,而不能正常弹回高电平。如果主机想连续读取多个字节,就需要在最后一个字节给非应答,而之前的所有字节都要给应答。简单来说就是主机给应答了,从机就会继续发主机给非应答了,从机就不会再发了,交出SDA的控制权从机控制SDA发送一个字节的权利,开始于读写标志位为1,结束于主机给应答为1。
-
停止位:结束时序。
-
补充:也可以在第一个应答位再加入一个停止条件,因为写入的地址会存在地址指针里面,所以这个地址并不会因为时序的停止而消失,这样时序就是两条完整的时序,就可以再起始,读当前位置,停止,这样两条时序也可以完成任务但是12C协议官方规定的复合格式是一整个数据帧。相当于把两条时序拼接成一条了就是先起始、再重复起始、再停止,相当于把两条时序拼接成一条了。
-
-
-
指定地址读/写多个字节
-
指定地址写多个字节:在指定地址写的时序的基础上,如果你只想写一个字节,那就停止,就行了如果你想写多个字节,就可以把这最后一部分,多重复几次,比如这里,重复三遍发送一个字节和接收应答,这样第一个数据就写入到了指定地址0x19的位置,这样这个时序就进阶为,在指定的位置开始,按顺序连续写入多个字节。比如你需要连续写入多个寄存器,就可以这样操作,这样在一条数据帧里,就可以同时写入多个字节,执行效率高。
-
当前位置/指定位置读多个字节:也可以多次执行这最后一部分时序,由于地址指针在读后也会自增,所以这样就可以连续读出一片区域的寄存器,效率也会非常高。
-
以上就是12C总线的硬件规定和软件规定了,有了这些规定,我们就可以按照硬件规定来连接线路,用软件规定来操作总线,以此实现指定位置写寄存器和指定位置读寄存器,有了这两个功能,主机就可以完全掌控外挂模块的运行了,也就实现了设计这个协议的目的。
四、MPU6050简介
-
基本概念:
-
MPU6050是一个6轴姿态传感器,可以测量芯片自身X、Y、Z轴的加速度、角速度参数,通过数据融合,可进一步得到姿态角(欧拉角),常应用于平衡车、飞行器等需要检测自身姿态的场景
-
在现实的三维空间里,只有XYZ,3个轴,这个MPU6050芯片里面,有加速度计和陀螺仪两种传感器,分别可以测量XYZ,3个轴的加速度和角速度,加起来总共就是6个轴,所以这个芯片是6轴姿态传感器,如果芯片里再集成一个3轴的磁场传感器,测量XYZ轴的磁场强度,那就叫作9轴姿态传感器,如果再集成一个气压传感器,测量气压大小,那就叫作10轴姿态传感器,一般气压值反映的是高度信息,海拔越高,气压越低,所以气压计是单独测量垂直地面的高度信息的。
-
加速度计和陀螺仪的基本原理都是设计一种装置,当传感器所感应的参数变化时,这个装置能带动电位器滑动,或者装置本身的电阻,可以随感应参数变化而变化,这样再外接一个电源,通过电阻分压,就能把现实世界的各种状态用电压表示出来了。总之电子的传感器最终也是输出一个随姿态变化而变化的电压,要想量化这个电压信号,
-
-
基本构成:
-
三轴陀螺仪(Gyroscope):用于测量物体在三维空间中绕 X、Y、Z 三个轴的角速度,能够感知物体的旋转运动状态,比如设备的转动、旋转等动作。例如,当手机进行旋转操作时,陀螺仪可以检测到相应的角速度变化。(陀螺仪具有动态稳定性,不具有静态稳定性)
-
三轴加速度计(Accelerometer):可测量物体在 X、Y、Z 三个方向上的加速度,能反映物体的倾斜、震动等运动情况。比如当设备处于静止状态时,加速度计可以检测到重力加速度在三个轴上的分量;当设备发生震动时,加速度计能够测量到震动产生的加速度变化。(加速度计具有静态稳定性,不具有动态稳定性)
-
数字运动处理器(DMP):这是 MPU6050 的核心处理单元,能够对陀螺仪和加速度计采集到的数据进行融合演算等处理,减轻主控制器(如单片机、微处理器等)的运算负担,提高数据处理效率。
-
-
引脚功能:
-
SCL 和 SDA:这是连接微控制器(MCU)的 IIC 接口(Inter-Integrated Circuit,集成电路总线)。MCU 通过这个接口与 MPU6050 进行通信,实现对传感器的控制和数据读取。
-
XCL 和 XDA:辅助 IIC 接口,用于连接外部的从设备,比如磁力传感器。当连接磁力传感器后,MPU6050 可以组成一个九轴传感器,输出更全面的运动数据。
-
AD0:地址管脚,用于控制 IIC 地址的最低位。当 AD0 接 GND 时,MPU6050 的 IIC 从机地址是 0x68;当 AD0 接 VDD 时,IIC 从机地址是 0x69。
-
INT:数据输出的中断引脚,当 MPU6050 准备好数据之后,可以通过该中断引脚通知主控制器来获取数据,提高数据传输的实时性。
-
-
感测范围与精度:
-
陀螺仪感测范围:角速度全格感测范围通常有 ±250、±500、±1000 与 ±2000°/sec(度每秒)等可选择的量程,不同量程下精度不同,用户可根据实际应用需求进行编程设置。例如在对旋转速度要求不高但需要较高分辨率的场景下,可以选择较小的量程;在需要测量高速旋转的场景下,则选择较大的量程。满量程选的越大,测量范围就越广;满量程选的越小,测量分辨率越高。
-
加速度计感测范围:用户可程式控制的加速度全格感测范围为 ±2g、±4g、±8g 与 ±16g,其中 “g” 是重力加速度单位,1g 约等于 9.8 米每平方秒。例如在测量轻微震动或倾斜时,可能选择较小的量程;在测量剧烈运动或高加速度场景时,选择较大的量程。
-
16位ADC采集传感器的模拟信号量化范围:因为加速度计和陀螺仪的设计,可以知道电子的传感器最终也是输出一个随姿态变化而变化的电压,要想量化这个电压信号,就需要AD转换器,所以这个芯片内部也是自带了AD转换器,可以对各个模拟参量进行量化,这个ADC是16位的,那量化输出的数据变化范围就是2^16,如果作为无符号数的话,就是0~65535,这里因为传感器每个轴都有正负的数据,所以这个输出结果是一个有符号数,量化范围是-32768~32767。数据是16位的,会分为2个字节存储。
-
例子:比如选择加速度计满量程为±16g,当读取AD值为最大值,32768时,那就表示此时测量的加速度为满量程16g,AD值为32768的一半时,就表示加速度为8g;如果选择满量程为±2g的话,那此时32768就对应2g的加速度,32768的一半就对应1g的加速度所以满量程选的越小,测量就会越细腻,因为AD值的范围是一定的。另外AD值和加速度值是线性关系、一一对应的由AD值求加速度,就是乘一个系数就可以了,这和学习ADC时,由AD值直接求电压是一个道理。
-
-
可配置的功能:
-
可配置的数字低通滤波器:可以对采集到的数据进行滤波处理,去除高频噪声,提高数据的稳定性和准确性。通过设置不同的滤波参数,可以让低频部分的数据通过,使输出的数据更加平滑。如果输出数据抖动太厉害,就可以加一点低通滤波,这样输出数据就会平缓一些。
-
可配置的时钟源:与可配置的采样分频配合使用。MPU6050 可以选择不同的时钟源,以满足不同的应用需求。例如,可以选择内部时钟源或外部时钟源,并且可以根据需要对时钟频率进行调整。
-
可配置的采样分频:与可配置的时钟源配合使用。用于设置采样速率,也就是数据的刷新率。通过调整采样分频寄存器的值,可以改变 MPU6050 的数据输出频率,以适应不同的应用场景对数据更新速度的要求。
-
时钟源经过分频器的分频,可以为AD转换和内部其他电路提供时钟,控制分频系数,就可以控制AD转换的快慢了。
-
-
工作原理及数据输出:
-
工作原理:通过陀螺仪和加速度计分别采集物体的角速度和加速度数据,然后将这些原始数据传输到数字运动处理器(DMP)中进行处理。DMP 可以将原始的角速度数据转化为四元数,再通过简单的数学公式计算出欧拉角,从而得到物体在三维空间中的姿态信息,即物体绕坐标系三个坐标轴(X、Y、Z)的旋转角度。
-
数据输出:MPU6050 可以以数字输出的形式提供融合演算后的数据,包括旋转矩阵、四元数、欧拉角等格式的数据,方便主控制器进行读取和进一步处理。
-
-
从机地址:
-
当AD0=0时,地址为1101000
-
当AD0=1时,地址为1101001
-
AD0就是板子引出来的一个引脚,可以调节12C从机地址的最低位,这里地址是7位的,如果像这样,用二进制来表示的话,一般没问题,如果在程序中,用十六进制表示的话,一般会有两种表示方式。以这个1101000的地址为例,第一种,就是单纯地把这7位二进制数转换为十六进制,这里1101000,低4位和高3位切开,转换十六进制,就是0x68,所以有的地方就说MPU6050的从机地址是0x68,所以将0x68为从机地址的话,在发送一个字节时,要把0x68左移一位,再按位或上读写位,读1写0。第二种表示方式,把0x68左移1位后的数据,当做从机地址,0x68左移1位之后,是0xD0,那这样,MPU6050的从机地址就是0xD0,这时,在实际发送第一个字节时,如果你要写,就直接把0xD0当作第一个字节,表示这里的从机地址;如果你要读,就把0xD0或上0x01,即OxD1当作第一个字节,这种表示方式,是把读写位也融入到从机地址里了,0xD0是写地址,0xD1是读地址。但是无论那种地址I2C收到的第一个字节都是一样的。
-
-
应用场景:
-
姿态控制领域:如平衡车、无人机等设备,需要实时监测设备的姿态变化,并根据姿态信息进行精确的控制和调整,以保持设备的平衡和稳定。
-
运动追踪领域:可用于运动手环、智能手表等穿戴设备,对用户的运动状态进行监测和追踪,如步数统计、跑步距离测量、运动轨迹记录等。
-
手势识别领域:通过识别用户手部的动作和姿态,实现对电子设备的手势控制,如手机的手势操作、智能电视的手势控制等。
-
游戏领域:在体感游戏中,玩家的身体动作可以通过 MPU6050 等传感器转化为游戏中的操作指令,增强游戏的趣味性和沉浸感。
-
六、MPU6050硬件电路
-
硬件电路图
-
其中右边这个是MPU6050的芯片,左下角是一个8针的排针,左上角是一个LDO,低压差线性稳压器。
-
MPU6050芯片:芯片本身的引脚是非常多的,包括时钟、12C通信引脚、供电、帧同步等等,不过大部分引脚本文还是用不到的,还有一些引脚,是这个芯片最小系统里的固定连接。
-
排针:
-
VCC、GND:这些引脚用于提供电源。
-
SCL、SDA:这些是 I2C 通信引脚,用于模块与其他设备之间的通信。在MPU6050芯片的右上角,SCL、SDA在模块中已经内置了两个4.7K的上拉电阻了,所以在接线的时候,直接把SCL和SDA接在GPIO口就行了,不需要再在外面另外接上拉电阻了。
-
XCL、XDA:这些是主机 I2C 通信引脚,用于连接到主机设备。设计这两个引脚是为了扩展芯片功能,由于MPU6050是一个6轴姿态传感器,但是只有加速度计和陀螺仪的6个轴,融合出来的姿态角是有缺陷的,这个缺陷就是绕Z轴的角度,也就是偏航角,它的漂移无法通过加速度计进行纠正,这就相当于让你蒙眼坐在车里,让你辨别车子行驶的方向,短时间内,你可以通过陀螺仪得知方向的变化,从而确定变化后的行驶方向,但是时间一长,车子到处转弯,你没有稳定的参考了,就肯定会迷失方向,所以这时候,你就要带个指南针在身边,提供长时间的稳定偏航角进行参考,来对陀螺仪感知的方向进行纠正,这就是9轴姿态传感器多出的磁力计的作用。另外,如果要制作无人机,需要定高飞行,这时候就还需要增加气压计,扩展为10轴,提供一个高度信息的稳定参考。所以根据要求6轴传感器可能不符合要求,需要增加拓展,此时XCL、XDA这两个引脚就提现到他们的作用了,XCL和XDA,通常就是用于外接磁力计或者气压计,当接上磁力计或气压计之后,MPU6050的主机接口可以直接访问这些扩展芯片的数据,把这些扩展芯片的数据读取到MPU6050里面,在利用DMP单元,进行数据融合和姿态解算。
-
AD0:这个引脚用于从机地址最低位。接低电平的话,7位从机地址就是1101000,接高电平的话,7位从机地址就是1101001。这里电路中,可以看到,有一个电阻,默认弱下拉到低电平了,所以引脚悬空的话,就是低电平,如果想接高电平,可以把ADO直接引到VCC,强上拉至平高电平。
-
INT:这个引脚用于中断信号输出。可以配置芯片内部的一些事件,来触发中断的输出,比如数据准备好了、I2C主机错误等。另外芯片内部还内置了一些实用的小功能,比如自由落体检测、运动检测、零运动检测等,这些信号都可以触发INT引脚产生电平跳变,需要的话可以进行中断信号的配置。
-
-
LDO:这部分是供电的逻辑,MPU6050芯片的VDD供电是2.375-3.46V,属于是3.3V供电的设备,不能直接接5V,所以为了扩大供电范围,这个模块就加了个3.3V的稳压器,输入端电压VCC 5V可以在3.3V到5V之间,然后经过3.3V的稳压器,输出稳定的3.3V电压,给芯片端供电。右侧是电源指示灯,只要3.3V端有电,电源指示灯就会亮。所以这一部分可以根据相应的需求进行,如果你已经有稳定的3.3V电源了,就不再需要这一部分了。
-
在本实验中,直接VCC、GND接上电,SCL和SDA接上12C通信的GPIO口就行了。
-
七、MPU6050的模块框图
-
模块框图概述
MPU6050 框图顶部有绿色标题 “MPU6050 框图”,并且有注释表明此电路图等同于芯片内部封装。该框图展示了 MPU - 60X0 模块的内部结构,涵盖多个功能模块和引脚。
-
引脚功能
1. 时钟相关引脚
-
CLKIN 和 CLKOUT
-
功能:CLKIN 是时钟输入引脚,外部时钟信号由此进入模块,为模块内的各种操作提供基准时钟。CLKOUT 是时钟输出引脚,它将模块内部产生的时钟信号输出,可用于同步与该模块连接的其他外部设备的操作。这两个引脚对于确保模块操作的同步性至关重要。例如,在一个需要多个传感器协同工作的系统中,MPU6050 通过 CLKOUT 引脚输出时钟信号给其他传感器,使它们能在相同的时间基准下工作。
-
在框图中的作用:属于时钟模块的输入输出引脚,但是一般使用内部时钟。在硬件电路中,CLKIN直接接了GND,CLKOUT没有引出,所以这部分可以不用关心。
-
-
X0
-
功能:作为外部时钟输入引脚,它提供了另一种外部时钟接入的途径。在某些应用场景下,当系统有特定的外部时钟源可用时,可通过 X0 引脚将该时钟信号输入到 MPU6050 模块中,使模块能根据此外部时钟进行工作。
-
2. 电源相关引脚
-
VDD、GND
-
功能:VDD 是电源正极输入引脚,为 MPU6050 提供工作所需电能。GND 为接地引脚,与电源负极相连,构成电流回路。稳定的电源供应是模块正常运行的基础,在连接电源时,必须确保 VDD 和 GND 连接正确且稳定。
-
-
REGOUT
-
功能:稳压电源输出引脚,它可以为外部的一些对电源稳定性要求较高的小功率电路提供稳定的电源。例如,在一个小型传感器网络中,MPU6050 的 REGOUT 引脚可以为一些辅助的低功耗传感器供电。
-
-
VLOGIC
-
功能:独立的逻辑电源引脚,可以支持供电和IO口不一样的电平等级,为模块内的逻辑电路部分提供特定的电源,确保逻辑电路的正常工作。不同的电源引脚有助于将模块内不同功能部分的电源进行分离,提高电路的稳定性和可靠性。
-
3. 其他引脚
-
CPOUT
-
功能:电荷泵输出引脚,电荷泵在模块中通常用于产生高于电源电压的电压或者用于一些特殊的电源转换操作。CPOUT 引脚输出的电荷泵信号可能用于驱动某些需要较高电压的内部电路或者外部连接的设备。
-
电荷泵的升压原理:比如我有个电池,电压是5V,再来个电容,首先电池和电容并联,电池给电容充电,充满之后,电容也相当于一个5V的电池了,然后再把电池和电容串联起来,电池5V,电容也是5V,这样输出就是10V的电压了,就可以凭空把电池电压升高至两倍了,不过由于这个电容电荷比较少,用一下就不行了,所以这个并联、串联的切换速度要快,趁电容还没放电完,就要及时并联充电这样一直持续,并联充电,串联放电,并联充电,串联放电,然后续再加个电源滤波,就能进行平稳的升压了。
-
在这里,由于陀螺仪内部是需要个高电压支持的,所以这里设计了一个电荷泵进行升压,当然这个升压过程是自动的,不需要管。
-
-
INT
-
功能:中断输出引脚。当模块内发生特定事件,如数据采集完成、传感器检测到异常等符合预设中断条件的情况时,模块会通过 INT 引脚向外部设备发送中断信号。外部设备接收到中断信号后,可以及时对相应事件进行处理。例如,当加速度计检测到超过预设阈值的加速度时,会通过 INT 引脚向微控制器发送中断信号,以便微控制器立即采取相应措施。
-
-
FSYNC
-
功能:帧同步。本文不涉及。
-
4. 通信接口相关的引脚
-
(CS) AD0 / (SDO) SCL / (SCLK) SDA / (SD)
- 功能:是从机的12C和SPI通信接口,用于和STM32通信。
-
AUX_CL / AUX_DA
- 功能:是主机的12C通信接口,用于和MPU6050扩展的设备进行通信。
-
功能模块
1. 通信接口模块
-
Slave I2C and SPI Serial Interface
-
功能:从机 I2C 和 SPI 串行接口,使 MPU6050 能够作为从设备与外部的主设备进行通信。在 I2C 模式下,通过 SCL(时钟线)和 SDA(数据线)与主设备交换数据,可用于传输传感器采集的数据和接收主设备发送的配置指令等。在 SPI 模式下,通过多个引脚(如片选、时钟、数据输入和输出引脚)实现高速数据传输,适用于对数据传输速度要求较高的应用场景。
-
-
Master I2C Serial Interface
-
功能:主机 I2C 串行接口,允许 MPU6050 作为主机与其他 I2C 从设备进行通信。这在一些需要 MPU6050 控制其他 I2C 设备的系统中非常有用,例如在一个集成了多个传感器的系统中,MPU6050 可以通过主机 I2C 接口读取其他从机传感器的数据。
-
-
Serial Interface Bypass
-
功能:串行接口旁路模块,该模块可能用于在特定情况下实现对串行接口数据的直接传输或特殊处理。例如,在某些调试或特定的数据传输模式下,可以通过该旁路模块实现更灵活的数据操作。
-
工作流程:相当于开关,如果拨到上面,辅助的12C引脚就和正常的I2C引脚接到一起,这样两路总线就合在一起了,STM32可以控制所有设备(包括MOU6050的扩展设备);如果拨到下面,辅助的I2C引脚就由MPU6050控制,两条12C总线独立分开,这时STM32是MPU6050的大哥,MPU6050又是扩展设备的大哥。
-
2. 寄存器模块
-
Interrupt Status Register
-
功能:中断状态寄存器,可以控制内部的哪些事件到中断引脚的输出。
-
引脚:INT(控制内部的哪些事件到中断引脚的输出)
-
-
FIFO
-
功能:是先入先出寄存器,可以对数据流进行缓存。本文暂时不使用。
-
-
Config Registers
-
功能:配置寄存器,可以对内部的各个电路进行配置,
-
-
Sensor Registers
-
功能:传感器寄存器,也就是数据寄存器,存储了各个传感器的数据。
-
3. 数据处理和校准模块
-
Factory Calibration
-
功能:工厂校准模块,用于校准传感器数据。在芯片生产过程中,由于制造工艺的偏差,传感器的测量数据可能存在一定的误差。工厂校准模块通过在生产时对传感器进行校准,将校准参数存储在芯片内部,在实际应用中可以根据这些参数对传感器测量的数据进行校正,以提高测量的准确性。
-
-
Digital Motion Processor (DMP)
-
功能:数字运动处理器,主要用于处理传感器数据。它可以对加速度计和陀螺仪采集的数据进行复杂的算法处理,例如姿态解算。通过 DMP,可以在不依赖外部微控制器大量计算资源的情况下,快速得到物体的姿态信息,如倾斜角度、旋转方向等,广泛应用于体感游戏、无人机姿态控制等领域。
-
4. 电源和测试模块
-
Bias & LDO
-
功能:偏置和低压差线性稳压器,用于为模块内的电路提供稳定的电源。偏置电路为传感器等元件提供合适的工作偏置电压,确保其正常工作。低压差线性稳压器则可以在输入电源有一定波动的情况下,输出稳定的电压,保证模块内各电路的稳定运行。
-
-
Self - test
-
功能:自检模块,用于测试传感器的功能,是用来验证芯片好坏的。在系统启动或定期维护时,可以通过自检模块对加速度计、陀螺仪等传感器进行功能测试,以确保传感器能够正常工作。例如,自检模块可以模拟特定的输入信号给传感器,然后检查传感器的输出是否符合预期,若发现传感器存在故障,可以及时采取相应措施。
-
运作流程(如何实现自测):当启动自测后,芯片内部会模拟一个外力施加在传感器上,这个外力导致传感器数据会比平时大一些。先使能自测,读取数据,再失能自测,读取数据,两个数据一相减,得到的数据叫自测响应,这个自测响应,芯片手册里给出了一个范围,如果自测响应在这个范围内,就说明芯片没问题,如果不在,就说明芯片可能坏了,使用的话就要注意。
-
5. 传感器模块
-
Accel(X、Y、Z)
-
功能:加速度计,用于测量加速度。它可以测量物体在 X、Y、Z 三个方向上的加速度,在惯性导航、运动检测、振动分析等领域有广泛应用。例如,在手机中,加速度计可以检测手机的运动状态,如摇晃、倾斜等,进而实现屏幕方向切换、计步等功能。
-
-
Gyro(X、Y、Z)
-
功能:陀螺仪,用于测量角速度。它能够测量物体绕 X、Y、Z 轴旋转的角速度,在无人机姿态控制、机器人导航、虚拟现实等领域起到关键作用。例如,在无人机飞行过程中,陀螺仪可以实时测量无人机的旋转角速度,结合加速度计的数据,通过飞行控制器对无人机的飞行姿态进行精确控制。
-
-
Temp Sensor
-
功能:温度传感器,用于测量模块温度。模块内部的温度变化可能会影响传感器的测量精度和其他电路的性能。通过温度传感器,可以实时监测模块温度,在温度变化较大时,可以采取相应的补偿措施来保证传感器测量的准确性,或者在温度过高时采取散热措施以保护模块。
-
-
总结:
-
这些传感器模块,本质上都相当于可变电阻,通过分压后,输出模拟电压,然后通过ADC,进行模数转换,转换完成之后,这些传感器的数据统一都放到数据寄存器中,读取数据寄存器就能得到传感器测量的值了,这个芯片内部的转换,都是全自动进行的,就类似之前学的AD连续转换+DMA转运,每个ADC输出,对应16位的数据寄存器,不存在数据覆盖的问题,配置好转换频率之后,每个数据就自动以我们设置的频率刷新到数据寄存器,需要数据的时候,直接来读就行了,其他的都不用管,还是非常方便的。
-
-
数据传输和处理
-
加速度计和陀螺仪采集到的数据首先要经过 ADC(模数转换器)进行转换。ADC 将传感器获取的模拟信号转换为数字信号,这些数字信号会被传输到相应的寄存器中。
-
数据可以通过 I2C 或 SPI 接口传输到外部设备,这使得外部的微控制器或其他处理设备能够获取传感器数据进行进一步的分析和处理。例如,在一个物联网环境监测系统中,MPU6050 通过 I2C 接口将采集到的加速度和温度数据传输给微控制器,微控制器再将数据通过网络发送到远程服务器进行存储和分析。
-
此外,数据还可以通过 DMP 进行内部处理,如上述提到的 DMP 可以进行姿态解算等操作,能够在不依赖外部设备大量计算资源的情况下,快速得到有用的运动信息。这种内部处理方式在对实时性要求较高的应用场景中非常重要,比如在体感游戏设备中,DMP 能够快速处理传感器数据,使游戏能够实时响应玩家的动作。
-
八、MPU6050芯片的相关寄存器
-
Sample Rate Divider(采样率分频器)
1. 寄存器功能
-
这个寄存器的功能是设置采样率分频器(SMPRT_DIV),可以配置采样频率的分频系数。它用于控制 MPU6050 内部传感器(如加速度计和陀螺仪)的采样率。通过调整这个寄存器的值,可以改变传感器数据的采样频率。分频越小,内部的AD转换就越快,数据寄存器刷新就越快,反之就越慢。
2. 寄存器类型
-
类型为可读 / 可写(Read/Write),这意味着外部设备(如微控制器)可以读取该寄存器的当前值,也可以向该寄存器写入新的值来改变其设置。
3. 寄存器位
-
该寄存器有 8 个位(Bit0 - Bit7),所有位共同构成了采样率分频器的值(SMPRT_DIV [7:0])。这些位用于设置具体的分频系数,从而影响采样率。例如,写入不同的值到这些位,可以使传感器以不同的频率进行采样。
4. 寄存器地址
-
寄存器地址为 0x19(十六进制)或 25(十进制)。
-
Configuration(配置寄存器)
1. 寄存器功能
-
这个寄存器的功能是配置 MPU6050 的工作模式和一些相关参数。它用于控制 MPU6050 内部传感器(如加速度计和陀螺仪)的一些特性。
2. 寄存器类型
-
类型为可读 / 可写(Read/Write),这意味着外部设备(如微控制器)可以读取该寄存器的当前值,也可以向该寄存器写入新的值来改变其设置。
3. 寄存器位
-
该寄存器有 8 个位(Bit0 - Bit7),具体如下:
-
Bit7 - Bit6:图中未标明具体功能,用 “-” 表示。
-
Bit5 - Bit3(EXT_SYNC[2:0]):这些位用于配置外部帧同步。例如,可以设置外部信号如何与 MPU6050 内部时钟进行同步。(外部同步设置)
-
Bit2 - Bit0(DLPF_CFG[2:0]):这些位用于配置数字低通滤波器(Digital Low - Pass Filter,DLPF)。通过设置这些位,可以调整滤波器的参数,以减少噪声对传感器数据的影响,配置滤波器参数越大,输出数据抖动就越小。0是不使用低通滤波器,陀螺仪时钟为8KHz,然后这个最大的参数,是保留位,没有用到。(低通滤波器配置)
-
4. 寄存器地址
-
其寄存器地址为 0x1A(十六进制)或 26(十进制)。
-
Gyroscope Configuratio(陀螺仪配置寄存器)
1. 寄存器功能
- 是专门用于配置 MPU6050 中陀螺仪相关参数的寄存器。
2. 寄存器类型
-
类型为可读 / 可写(Read/Write),这意味着外部设备(如微控制器)可以读取该寄存器的当前值,也可以向该寄存器写入新的值来改变其设置。
3. 寄存器位
-
该寄存器有 8 个位(Bit0 - Bit7),每个位的功能如下:
-
Bit7(XG_ST):X 轴陀螺仪自测试(X - Axis Gyroscope Self - Test)。该位用于控制 X 轴陀螺仪的自测试功能。
-
Bit6(YG_ST):Y 轴陀螺仪自测试(Y - Axis Gyroscope Self - Test)。该位用于控制 Y 轴陀螺仪的自测试功能。
-
Bit5(ZG_ST):Z 轴陀螺仪自测试(Z - Axis Gyroscope Self - Test)。该位用于控制 Z 轴陀螺仪的自测试功能。
-
Bit4 - Bit3(FS_SEL[1:0]):陀螺仪满量程选择(Gyroscope Full - Scale Select)。这两位用于选择陀螺仪的满量程范围,不同的设置会影响陀螺仪的测量范围和灵敏度。量程越大,范围越广;量程越小,分辨率越高。
-
Bit2 - Bit0:图中未标明具体功能,用 “-” 表示。
-
4. 寄存器地址
-
其寄存器地址为 0x1B(十六进制)或 27(十进制)。
-
Accelerometer Configuration(加速度计配置寄存器)
1. 寄存器功能
-
是专门用于配置 MPU6050 中加速度计相关参数的寄存器。
2. 寄存器类型
-
类型为可读 / 可写(Read/Write),这意味着外部设备(如微控制器)可以读取该寄存器的当前值,也可以向该寄存器写入新的值来改变其设置。
3. 寄存器位
-
该寄存器有 8 个位(Bit0 - Bit7),每个位的功能如下:
-
Bit7(XA_ST):X 轴加速度计自测试(X - Axis Accelerometer Self - Test)。该位用于控制 X 轴加速度计的自测试功能。
-
Bit6(YA_ST):Y 轴加速度计自测试(Y - Axis Accelerometer Self - Test)。该位用于控制 Y 轴加速度计的自测试功能。
-
Bit5(ZA_ST):Z 轴加速度计自测试(Z - Axis Accelerometer Self - Test)。该位用于控制 Z 轴加速度计的自测试功能。
-
Bit4 - Bit3(AFS_SEL[1:0]):加速度计满量程选择(Accelerometer Full - Scale Select)。这两位用于选择加速度计的满量程范围,不同的设置会影响加速度计的测量范围和灵敏度。
-
Bit2 - Bit0(ACCEL_HPF[2:0]):加速度计高通滤波器配置(Accelerometer High - Pass Filter Configuration)。这些位用于配置加速度计的高通滤波器参数。
-
4. 寄存器地址
-
其寄存器地址为 0x1C(十六进制)或 28(十进制)。
-
ACCEL_XOUT_H, ACCEL_XOUT_L, ACCEL_YOUT_H, ACCEL_YYOUT_L, ACCEL_ZOUT_H, and ACCEL ZOUT_L (加速度计的数据寄存器(Accelerometer Measurements))
1. 寄存器功能
-
用于获取加速度计在 X、Y、Z 轴上的测量值。用于读取数据,数据是16 位的有符号数,进制补码的方式存储,读出高 8 位和低 8 位,高位左移8次,或上低位数据,最后再存在一个int16_t的变量里,这样就可以得到数据。
2. 寄存器类型
-
类型为只读(Read Only),这意味着外部设备(如微控制器)只可以读取该寄存器的当前值。
3. 寄存器位
-
每个寄存器有 8 个位(Bit0 - Bit7),具体如下:
-
寄存器 59(0x3B,十进制为 59)
-
存储加速度计 X 轴测量值的高 8 位(ACCEL_XOUT [15:8])
-
-
寄存器 60(0x3C,十进制为 60)
-
存储加速度计 X 轴测量值的低 8 位(ACCEL_XOUT [7:0])
-
-
寄存器 61(0x3D,十进制为 61)
-
存储加速度计 Y 轴测量值的高 8 位(ACCEL_YOUT [15:8])
-
-
寄存器 62(0x3E,十进制为 62)
-
存储加速度计 Y 轴测量值的低 8 位(ACCEL_YOUT [7:0])
-
-
寄存器 63(0x3F,十进制为 63)
-
存储加速度计 Z 轴测量值的高 8 位(ACCEL_ZOUT [15:8])
-
-
寄存器 64(0x40,十进制为 64)
-
存储加速度计 Z 轴测量值的低 8 位(ACCEL_ZOUT [7:0])
-
-
4. 寄存器地址
-
其寄存器地址为 0x3B~0x40(十六进制)或 59~64(十进制)。
-
Temperature Measurement (温度传感器测量值)
与上述加速度计的数据寄存器相类似,不做过多介绍。
-
Gyroscopo Measurements(陀螺仪测量值)
与上述加速度计的数据寄存器相类似,不做过多介绍。
-
Power Management 1(电源管理 1 寄存器)
1. 寄存器功能
-
配置电源管理相关参数。
2. 寄存器类型
-
类型为可读 / 可写(Read/Write),这意味着外部设备(如微控制器)可以读取该寄存器的当前值,也可以向该寄存器写入新的值来改变其设置。
3. 寄存器位
-
该寄存器有 8 个位(Bit0 - Bit7),每个位的功能如下:
-
Bit7(DEVICE_RESET):设备复位。当该位置 1 时,会对 MPU6050 进行复位操作。所有寄存器恢复到默认值。
-
Bit6(SLEEP):睡眠模式。该位用于控制 MPU6050 是否进入睡眠模式。当置 1 时,设备进入睡眠模式,芯片不工作,以降低功耗。
-
Bit5(CYCLE):循环模式。该位用于使 MPU6050 进入循环模式,在该模式下,设备可以在睡眠和唤醒状态之间循环切换,以在一定程度上节省功耗。这一位写1,设备进入低功耗,过一段时间,启动一次,并且唤醒的频率由下一个寄存器的BIT7和BIT6这两位确定。
-
Bit4:图中未标明具体功能,用 “-” 表示。
-
Bit3(TEMP_DIS):温度传感器禁用。当该位置 1 时,会禁用 MPU6050 内部的温度传感器。
-
Bit2 - Bit0(CLKSEL[2:0]):时钟选择。这三位用于选择 MPU6050 的时钟源,不同的设置会选择不同的内部或外部时钟源来驱动设备。
-
当
CLKSEL = 0
时:时钟源为 “Internal 8MHz oscillator”,即内部 8MHz 振荡器。这是默认的时钟源选项,适用于大多数一般应用场景。(内部晶振) -
当
CLKSEL = 1
时:时钟源为 “PLL with X axis gyroscope reference”,即使用 X 轴陀螺仪参考的锁相环(PLL)。这种时钟源选择适用于需要高精度且基于 X 轴陀螺仪数据的应用。(X轴陀螺仪晶振) -
当
CLKSEL = 2
时:时钟源为 “PLL with Y axis gyroscope reference”,即使用 Y 轴陀螺仪参考的锁相环(PLL)。适用于依赖 Y 轴陀螺仪数据的高精度应用。(Y轴陀螺仪晶振) -
当
CLKSEL = 3
时:时钟源为 “PLL with Z axis gyroscope reference”,即使用 Z 轴陀螺仪参考的锁相环(PLL)。适用于需要基于 Z 轴陀螺仪数据的高精度应用。(Z轴陀螺仪晶振) -
当
CLKSEL = 4
时:时钟源为 “PLL with external 32.768kHz reference”,即使用外部 32.768kHz 参考的锁相环(PLL)。这种选择适用于需要外部高精度时钟参考的应用。(外部引脚的方波) -
当
CLKSEL = 5
时:时钟源为 “PLL with external 19.2MHz reference”,即使用外部 19.2MHz 参考的锁相环(PLL)。适用于需要外部高频时钟参考的应用。(外部引脚的方波) -
当
CLKSEL = 6
时:标注为 “Reserved”,即保留未用。此选项可能在未来的版本中用于特定功能,但目前不建议使用。 -
当
CLKSEL = 7
时:时钟源为 “Stops the clock and keeps the timing generator in reset”,即停止时钟并使定时发生器保持复位状态。这种状态通常用于设备休眠或低功耗模式,以节省能源。 -
总结:一般选择内部晶振或者陀螺仪晶振,但是这里手册里面有非常建议我们选择陀螺仪的晶振,因为陀螺仪的晶振更加精确。
-
-
4. 寄存器地址
-
其寄存器地址为 0x6B(十六进制)或 107(十进制)。
-
Power Management 2(电源管理 2 寄存器)
1. 寄存器功能
-
配置电源管理相关参数。
2. 寄存器类型
-
类型为可读 / 可写(Read/Write),这意味着外部设备(如微控制器)可以读取该寄存器的当前值,也可以向该寄存器写入新的值来改变其设置。
3. 寄存器位
-
该寄存器有 8 个位(Bit0 - Bit7),每个位的功能如下:
-
Bit7 - Bit6(LP_WAKE_CTRL[1:0]):低功耗唤醒控制。这两位用于控制 MPU6050 在低功耗模式下的唤醒机制。
-
Bit5(STBY_XA):X 轴加速度计待机模式。该位用于控制 X 轴加速度计是否进入待机模式,以节省功耗。如果只需要该轴的数据,可以让其他轴待机。
-
Bit4(STBY_YA):Y 轴加速度计待机模式。该位用于控制 Y 轴加速度计是否进入待机模式。如果只需要该轴的数据,可以让其他轴待机
-
Bit3(STBY_ZA):Z 轴加速度计待机模式。该位用于控制 Z 轴加速度计是否进入待机模式。如果只需要该轴的数据,可以让其他轴待机
-
Bit2(STBY_XG):X 轴陀螺仪待机模式。该位用于控制 X 轴陀螺仪是否进入待机模式。如果只需要该轴的数据,可以让其他轴待机
-
Bit1(STBY_YG):Y 轴陀螺仪待机模式。该位用于控制 Y 轴陀螺仪是否进入待机模式。如果只需要该轴的数据,可以让其他轴待机
-
Bit0(STBY_ZG):Z 轴陀螺仪待机模式。该位用于控制 Z 轴陀螺仪是否进入待机模式。如果只需要该轴的数据,可以让其他轴待机
-
4. 寄存器地址
-
其寄存器地址为 0x6C(十六进制)或 108(十进制)。
-
Who Am I(设备识别寄存器)
1. 寄存器功能
-
读取设备ID号,ID号是不可以修改的
2. 寄存器类型
-
类型为只读(Read Only),这意味着外部设备(如微控制器)只可以读取该寄存器的当前值。
3. 寄存器位
-
该寄存器有 8 个位(Bit0 - Bit7),其中:
-
Bit7 、 Bit1:图中未标明具体功能,用 “-” 表示。
-
Bit6 - Bit1(WHO_AM_I[6:1]):这些位用于存储设备的识别码。通过读取这些位的值,可以识别出设备是否为 MPU6050。 ID号中间这6位,固定为110100,实际上这个ID号,就是MPU6050的12C地址,它的最高位和最低位,其实都是0,那读出这个寄存器,值就固定为0x68。其中,ADO引脚的值,并不反映在这个寄存器上,意思是,因为I2C的地址可以通过AD0的引脚进行配置,但是,这里的ID号的最低位,是不随ADO引脚变化而变化的,读出ID号,始终都是0x68。
-
4. 寄存器地址
-
其寄存器地址为 0x6C(十六进制)或 108(十进制)。
注意:
所有的寄存器上电默认值都是0x00,除了107号寄存器(电源管理寄存器1),上电默认0x40,也就是次高位为1,这里次高位是SLEEP,所以这个芯片上电默认睡眠模式,所以在使用时要先解除睡眠模式否则操作其他寄存器是无效的。117号寄存器,上电模式0x68,117号,就是ID号,默认0x68。
下面就是通过I2C通信,读与写寄存器来操控MPU6050。
九、软件I2C读写MPU6050
-
引言
在嵌入式系统开发中,使用软件 I²C 读写 MPU6050 是一项常见且重要的操作。下文将详细介绍这一过程,包括硬件电路、程序架构、软件初始化、端口操作封装和 I²C 时序单元等内容。首先在软件别写部分,分两个部分,第一部分,完成软件12C协议的时序;第二部分,基于12C协议读写寄存器,来操控MPU6050。
-
硬件电路介绍
(一)连接方式
MPU6050 模块与 STM32 的连接较为关键。VCC 和 GND 分别连接电源的正负极,这为模块提供了必要的供电支持。SCL 和 SDA 分别连接到 STM32 的 PB10 和 PB11 引脚,这种连接方式使得两者之间能够进行数据通信。
(二)端口指定优势
本电路设计的一大优势在于端口可任意指定。这意味着在不同的应用场景下,如果需要更改连接的端口,操作相对灵活。例如,当其他外设已经占用了 PB10 和 PB11 引脚时,可以根据实际情况选择其他可用引脚进行连接,而不需要对整个硬件电路进行大规模的改动。由于我们这个代码使用的是软件12C,就是用普通的GPIO口,手动翻转电平实现的协议,它并不需要STM32内部的外设资源支持,所以这里的端口,其实可以任意指定,不局限于这两个端口,也可以SCL接PAO,SDA接PB12,或者SCL接PA8,SDA接PA9等等等等,接在任意的两个普通的GPIO口就可以,然后我们只需要在程序中,配置并操作SCL和SDA对应的端口就行了,这算是软件12C相比硬件12C的一大优势,就是端口不受限,可以任意指定。
(三)上拉电阻情况
根据12C协议的硬件规定,SCL和SDA都应该外挂一个上拉电阻,但是这里并没有外挂上拉电阻,但是值得注意的是,MPU6050 模块内部自带上拉电阻。这就使得在硬件电路搭建时,外部无需再连接上拉电阻。这样不仅简化了硬件电路的设计,还减少了因上拉电阻选型不当或连接错误可能导致的问题。
(四)主机从机与其他引脚
在该电路中,STM32 作为主机,MPU6050 作为从机。除了 SCL 和 SDA 引脚外,硬件电路还涉及扩展接口和中断信号输出脚。扩展接口可以用于连接其他外部设备,进一步扩展系统功能;中断信号输出脚则在某些特定的应用场景下,如当 MPU6050 检测到特定事件时,可以向 STM32 发送中断信号,以便 STM32 及时做出响应。在本程序中后面XCL和XDA用于扩展的接口和INT中断信号输出脚是用不到的。
-
程序整体架构
(一) I2C 通信层建立
程序的整体框架与51单片机教程里的 I2C 类似。首先要建立 I2C 通信层的.c 和.h 模块。在这个模块中,需要编写好 I2C 底层的 GPIO 初始化和六个时序基本单元(起始、终止、发送一个字节、接收一个字节、发送应答和接收应答)。
(二)MPU6050 模块层建立
接着建立 MPU6050 的.c 和.h 模块。基于已经建立好的 I2C 通信模块,实现指定地址读、写操作,配置芯片以及获取传感器数据等功能。
(三)主函数调用
最后在 main.c 中进行调用,调用MPU6050的模块。具体操作包括 MPU6050 模块初始化,获取数据并将数据显示出来。例如,在获取传感器数据后,可以通过 OLED 显示屏或者串口将数据展示给用户,方便用户对 MPU6050 的工作状态进行监测和分析。
-
I2C代码部分详解
-
模块的建立
(一)文件名的建立
首先在Hardware目录下添加文件,为了防止和库函数里面的函数重名,这里模块名称,统一叫作MyI2C。
-
软件 I2C 初始化
那由于本代码要使用软件12C,这个i2c的库函数,就不用看了,软件12C,只需要用gpio的读写函数就行了,库函数是不需要看的。
(一)引脚模式设置
软件 I2C 初始化首先要把 SCL 和 SDA 初始化为开漏输出模式(任务一)。这是因为在 I2C 通信中,开漏输出模式能够更好地满足多设备连接时的电气特性要求。在初始化过程中,需要对STM32 的相关寄存器进行配置。
(二)引脚电平设置
在将 SCL 和 SDA 初始化为开漏输出模式后,需要将它们置高电平(任务二)。通过复制 GPIO 初始化代码,并修改时钟和引脚配置,实现对 PB10 和 PB11 的初始化。初始化结束后,调用函数将这两个引脚置高电平,这样做的目的是使 I2C 总线处于空闲状态。只有当 I2C 总线处于空闲状态时,后续的通信操作才能顺利进行。
(三)I2C软件初始化的编写
void MyI2C_Init(void)
1. 函数功能概述
MyI2C_Init
函数用于实现 I²C 的初始化操作,主要针对的是与 I²C 通信相关的 SCL 和 SDA 引脚(在本代码中是 PB10 和 PB11 引脚)进行初始化设置,使其满足 I²C 通信的电气特性要求。
2. 代码逐行分析
(1)开启时钟
-
在 STM32 中,使用任何外设之前都需要先开启其对应的时钟。这里通过
RCC_APB2PeriphClockCmd
函数来开启 GPIOB 的时钟。因为 PB10 和 PB11 引脚属于 GPIOB 端口,只有开启了 GPIOB 的时钟,才能对这两个引脚进行配置操作。
(2)GPIO 初始化
-
首先定义了一个
GPIO_InitTypeDef
类型的结构体GPIO_InitStructure
,这个结构体用于配置 GPIO 引脚的各种参数。 -
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;
:将引脚的模式设置为开漏输出(Open - Drain)模式。在 I²C 通信中,开漏输出模式是很重要的,因为 I²C 总线上可以连接多个设备,开漏输出可以实现线与逻辑,保证数据传输的正确性。(开漏输出也可以输入,输入时,先输出1,再直接读取输入数据寄存器就行了) -
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10 | GPIO_Pin_11;
:指定要配置的引脚为 PB10 和 PB11。这里使用位或操作来同时配置两个引脚。 -
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
:设置引脚的输出速度为 50MHz。这个速度参数会影响引脚电平转换的快慢。 -
最后通过
GPIO_Init
函数,将上述配置应用到 GPIOB 端口,完成对 PB10 和 PB11 引脚的初始化,使其成为开漏输出模式。
(3)设置默认电平
-
在 I²C 通信中,当总线处于空闲状态时,SCL 和 SDA 引脚通常需要保持高电平。通过
GPIO_SetBits
函数将 PB10 和 PB11 引脚设置为高电平,使 I²C 总线处于释放状态(空闲状态),这样可以确保在进行后续的 I²C 通信操作之前,总线处于正确的初始状态。
/**
* 函 数:I2C初始化
* 参 数:无
* 返 回 值:无
* 注意事项:此函数需要用户实现内容,实现SCL和SDA引脚的初始化
*/
void MyI2C_Init(void)
{
/*开启时钟*/
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); //开启GPIOB的时钟
/*GPIO初始化*/
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10 | GPIO_Pin_11;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure); //将PB10和PB11引脚初始化为开漏输出
/*设置默认电平*/
GPIO_SetBits(GPIOB, GPIO_Pin_10 | GPIO_Pin_11); //设置PB10和PB11引脚初始化后默认为高电平(释放总线状态)
}
-
端口操作封装与改名
在下面书写I2C时序时,可以不断地调用SetBits和ResetBits,来手动翻转高低电平,但是这样做的话,会在后面的程序中,出现非常多的地方,来指定这个GPIO端口号,一方面,这样做语义不是很明显,另一方面,如果我们之后需要换一个端口,那就需要改动非常多的地方,所以这时,就需要在上面做个定义,把这个端口号统一替换一个名字,这样无论是语义还是端口的修改,都会非常方便,所以采用宏定义的方式定义端口。(在51单片机中,我们一般用sbit来定义端口的名称,但是sbit并不是标准C语言的语法,STM32也不支持这样做,这里宏定义就是一个合适的方法)
(一)宏定义与封装
为了方便操作端口和修改时序延时,采用宏定义和封装函数的方法。对操作端口的库函数进行分装,这种分装操作可以实现函数名称端口号的替换。
#define SCL_PORT GPIOB
#define SCL_PIN GPIO_Pin_10
如果觉得每次都需要定义PORT和PIN,比较麻烦,还可以把这整个函数用宏定义进行替换.
//在OLED文件中涉及了有参宏的相关知识点
#define OLED_W_SCL(x) GPIO_WriteBit(GPIOB, GPIO_Pin_8, (BitAction)(x))
#define OLED_W_SDA (x) GPIO WriteBit(GPIOB, GPIO Pin 9, (BitAction)(x))
但是仍可能有些人看不懂这些代码的含义,并且这种方法,在移植到其他库或者其他种类单片机时,很多人都不知道怎么修改,另外还有,这种宏定义的方法,如果换到一个主频很高的单片机中,需要对软件时序进行延时操作的时候,也不太方便进一步修改,所以综合以上宏定义替换的缺点,在这里还可以在封装一个函数,这样既容易理解,又方便加软件延时,所以这里,直接对操作端口的库函数进行封装。
/**
* 函 数:I2C写SCL引脚电平
* 参 数:BitValue 协议层传入的当前需要写入SCL的电平,范围0~1
* 返 回 值:无
* 注意事项:此函数需要用户实现内容,当BitValue为0时,需要置SCL为低电平,当BitValue为1时,需要置SCL为高电平
*/
void MyI2C_W_SCL(uint8_t BitValue)
{
GPIO_WriteBit(GPIOB, GPIO_Pin_10, (BitAction)BitValue); //根据BitValue,设置SCL引脚的电平
Delay_us(10); //延时10us,防止时序频率超过要求
}
这样套一个函数替换之后,我后面再调用这个W_SCL,参数给1或0,就可以释放或拉低SCL了,如果说要把这个程序移植到别的单片机,就可以把这个函数里的操作,替换为其他单片机对应的操作,比如SCL是51单片机P10口,就可以把语句替换为P10=BitValue,就可以了。另外,如果单片机主频比较快,这里也非常方便加一些延时,比如这里要求每次操作引脚之后,都要延时10us。
/**
* 函 数:I2C写SDA引脚电平
* 参 数:BitValue 协议层传入的当前需要写入SDA的电平,范围0~0xFF
* 返 回 值:无
* 注意事项:此函数需要用户实现内容,当BitValue为0时,需要置SDA为低电平,当BitValue非0时,需要置SDA为高电平
*/
void MyI2C_W_SDA(uint8_t BitValue)
{
GPIO_WriteBit(GPIOB, GPIO_Pin_11, (BitAction)BitValue); //根据BitValue,设置SDA引脚的电平,BitValue要实现非0即1的特性
Delay_us(10); //延时10us,防止时序频率超过要求
}
/**
* 函 数:I2C读SDA引脚电平
* 参 数:无
* 返 回 值:协议层需要得到的当前SDA的电平,范围0~1
* 注意事项:此函数需要用户实现内容,当前SDA为低电平时,返回0,当前SDA为高电平时,返回1
*/
uint8_t MyI2C_R_SDA(void)
{
uint8_t BitValue;
BitValue = GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11); //读取SDA电平
Delay_us(10); //延时10us,防止时序频率超过要求
return BitValue; //返回SDA电平
}
因为STM32库函数中,读和写不是同一个寄存器,所以需要两个函数进行封装。
(二)操作便利性
在实际的嵌入式开发过程中,这种方法带来了很大的便利性。例如,当需要将程序移植到其他硬件平台或者修改端口连接时,只需要对宏定义进行修改,而不需要对大量的底层代码进行逐一更改。同时,对于时序的延时修改也变得更加容易,可以根据实际的通信速度要求,灵活地调整时序延时参数。
有了上面三个函数的封装,就实现了函数名称、端口号的替换,同时,也可以很方便地修改时序的延时,当我们需要替换端口,或者把这个程序移植到别的单片机中时,就只需要对这前4个函数里的操作对应更改即可。
-
I2C 六个时序基本单元
(一)起始条件
起始条件是 I²C 通信的开始标志。具体操作是先确保释放 SDA,再释放 SCL,然后拉低 SDA 和 SCL。这一系列操作需要严格按照顺序进行,并且在操作过程中要注意对引脚电平变化的控制,以确保起始条件能够被正确地产生。如果起始条件之前SCL和SDA已经是高电平了,那先释放哪一个是一样的效果,但是在指定地址读的条件中这个Start还要兼容这里的重复起始条件Sr,Sr最开始,SCL是低电平,SDA电平不敢确定,所以保险起见,我们趁SCL是低电平,先确保释放SDA,再释放SCL,这时SDA和SCL都是高电平,然后再拉低SDA、拉低SCL,这样这个Start就可以兼容起始条件和重复起始条件了,
/**
* 函 数:I2C起始
* 参 数:无
* 返 回 值:无
*/
void MyI2C_Start(void)
{
MyI2C_W_SDA(1); //释放SDA,确保SDA为高电平
MyI2C_W_SCL(1); //释放SCL,确保SCL为高电平
MyI2C_W_SDA(0); //在SCL高电平期间,拉低SDA,产生起始信号
MyI2C_W_SCL(0); //起始后把SCL也拉低,即为了占用总线,也为了方便总线时序的拼接
}
(二)终止条件
终止条件与起始条件相对应。先拉低 SDA,再释放 SCL,最后释放 SDA。正确产生终止条件可以确保在数据传输结束后,I2C 总线能够正常释放,避免影响下一次通信操作。同样,如果Stop开始时,SCL和SDA都已经是低电平了,那就先释放SCL,再释放SDA就行了,但是在这个时序单元开始时,SDA并不一定是低电平,所以为了确保之后释放SDA能产生上升沿,要在时序单元开始时,先拉低SDA,然后再释放SCL、释放SDA。终止条件后,SCL和SDA都恢复高电平。
/**
* 函 数:I2C终止
* 参 数:无
* 返 回 值:无
*/
void MyI2C_Stop(void)
{
MyI2C_W_SDA(0); //拉低SDA,确保SDA为低电平
MyI2C_W_SCL(1); //释放SCL,使SCL呈现高电平
MyI2C_W_SDA(1); //在SCL高电平期间,释放SDA,产生终止信号
}
(三)发送一个字节
发送一个字节按照特定的时序进行,高位先行,将一个字节的每一位按顺序放在SDA线上,每放完一位后,执行释放SCL,拉低SCL的操作,驱动时钟运转。通过循环和位操作来实现字节的发送。在发送过程中,要对每一位数据进行准确的处理,包括设置 SDA 引脚的电平以及按照 I2C 时序要求控制 SCL 引脚的电平变化。发送一个字节时序开始时,SCL是低电平,实际上,除了终止条件,SCL以高电平结束,所有的单元都会保证SCL以低电平结束,这样方便各个单元的拼接。
/**
* 函 数:I2C发送一个字节
* 参 数:Byte 要发送的一个字节数据,范围:0x00~0xFF
* 返 回 值:无
*/
void MyI2C_SendByte(uint8_t Byte)
{
uint8_t i;
for (i = 0; i < 8; i ++) //循环8次,主机依次发送数据的每一位
{
MyI2C_W_SDA(Byte & (0x80 >> i)); //使用掩码的方式取出Byte的指定一位数据并写入到SDA线
MyI2C_W_SCL(1); //释放SCL,从机在SCL高电平期间读取SDA
MyI2C_W_SCL(0); //拉低SCL,主机开始发送下一位数据
}
}
(四)接收一个字节
接收一个字节同样按照特定的时序。在接收过程中,需要在合适的时机读取 SDA 引脚的电平状态,并且通过位操作将读取到的数据组合成一个完整的字节。接收一个字节时序开始时,SCL低电平,此时从机需要把数据放到SDA上,为了防止主机干扰从机写入数据,主机需要先释放SDA,释放SDA也相当于切换为输入模式,那在SCL低电平时,从机会把数据放到SDA,如果从机想发1,就释放SDA,如果从机想发0,就拉低SDA,然后主机释放SCL,在SCL高电平期间,读取SDA,再拉低SCL,低电平期间,从机就会把下一位数据放到SDA上,这样重复8次,主机就能读到一个字节了。
例子:SCL低电平变换数据,高电平读取数据,实际上就是一种读写分离的设计,低电平时间定义为写的时间,高电平时间定义为读的时间,如果那在SCL高电平期间,如果非要动SDA,那这个信号就是起始条件或者终止条件,SCL高电平时,SDA下降沿为起始条件,SDA上升沿为终止条件,这个设计也保证了起始和终止的特异性,能够让我们在连续不断的波形中,快速地定位起始和终止,因为,起始终止和数据传输的波形有本质区别,数据传输,SCL高电平不许动SDA,起始终止,SCL高电平必须动SDA。
在接收一个字节时,if语句中不断读取SDA,但是for循环中又没有写过SDA,那SDA读出来应该始终是一个值啊,这有什么意义呢?
12C是在进行通信,通信是有从机的,当主机不断驱动SCL时钟时,从机就有义务去改变SDA的电平,所以主机每次循环读取SDA的时候,这个读取到的数据是从机控制的,这个数据也正是从机想要给主机发送的数据。
/**
* 函 数:I2C接收一个字节
* 参 数:无
* 返 回 值:接收到的一个字节数据,范围:0x00~0xFF
*/
uint8_t MyI2C_ReceiveByte(void)
{
uint8_t i, Byte = 0x00; //定义接收的数据,并赋初值0x00,此处必须赋初值0x00,后面会用到
MyI2C_W_SDA(1); //接收前,主机先确保释放SDA,避免干扰从机的数据发送
for (i = 0; i < 8; i ++) //循环8次,主机依次接收数据的每一位
{
MyI2C_W_SCL(1); //释放SCL,主机机在SCL高电平期间读取SDA
if (MyI2C_R_SDA() == 1){Byte |= (0x80 >> i);} //读取SDA数据,并存储到Byte变量
//当SDA为1时,置变量指定位为1,当SDA为0时,不做处理,指定位为默认的初值0
MyI2C_W_SCL(0); //拉低SCL,从机在SCL低电平期间写入SDA
}
return Byte; //返回接收到的一个字节数据
}
(五)发送应答和接收应答
发送应答和接收应答是发送和接收一个字节的简化版。在 I²C 通信中,应答信号用于确认数据的正确接收和发送,确保通信的可靠性。
在接受应答时,主机先将SDA置1,在读取SDA不会一定是1吗?
第一,12C的引脚都是开漏输出+弱上拉的配置,主机输出1,并不是强置SDA为高电平,而是释放SDA;第二,需要明白I2C是在进行通信,主机释放了SDA,那从机又不是在外面看戏,从机如果在的话,它是有义务在此时把SDA再拉低的,从机如果在的话,它是有义务在此时把SDA再拉低的,之后再读取SDA,读到的值也可能是0,读到0,代表从机给了应答,读到1,代表从机没给应答。
/**
* 函 数:I2C发送应答位
* 参 数:Byte 要发送的应答位,范围:0~1,0表示应答,1表示非应答
* 返 回 值:无
*/
void MyI2C_SendAck(uint8_t AckBit)
{
MyI2C_W_SDA(AckBit); //主机把应答位数据放到SDA线
MyI2C_W_SCL(1); //释放SCL,从机在SCL高电平期间,读取应答位
MyI2C_W_SCL(0); //拉低SCL,开始下一个时序模块
}
/**
* 函 数:I2C接收应答位
* 参 数:无
* 返 回 值:接收到的应答位,范围:0~1,0表示应答,1表示非应答
*/
uint8_t MyI2C_ReceiveAck(void)
{
uint8_t AckBit; //定义应答位变量
MyI2C_W_SDA(1); //接收前,主机先确保释放SDA,避免干扰从机的数据发送
MyI2C_W_SCL(1); //释放SCL,主机机在SCL高电平期间读取SDA
AckBit = MyI2C_R_SDA(); //将应答位存储到变量里
MyI2C_W_SCL(0); //拉低SCL,开始下一个时序模块
return AckBit; //返回定义应答位变量
}
-
测试代码
(一)函数声明
将六个函数(起始条件、终止条件、发送一个字节、接收一个字节、发送应答和接收应答)和初始化函数放在头文件声明。这样做可以在其他模块中方便地调用这些函数。
(二)主函数测试
在主函数中对 I²C 模块进行测试。按照指定地址写的时序结构拼接完整时序,测试从机应答功能。具体操作是先产生起始条件,发送从机地址加读写位进行寻址,接收应答位,再发送寄存器地址(暂未测试写入寄存器功能),最后停止并通过 OLED 显示应答位的值。通过这种测试方法,可以验证 I²C 通信的正确性,以及软件 I²C 读写 MPU6050 功能是否正常实现。
可以用点名时序+for循环可以遍历所有从机地址,查看挂载在总线上的设备(应答位为0表明该设备挂载在总线上),不过注意一下,遍历的时候,只遍历前7位地址就行了,最后一位要始终保持为0,否则一旦交出了总线控制权,从机就会干扰后续的遍历。
还可以验证AD0引脚改名的功能,利用飞线更改从机地址,再在程序中严重两个地址名观察从机是否应答,即可验证。
-
MPU6050代码部分详解
-
模块的建立
(一)文件名的建立
首先在Hardware目录下添加文件,这里模块名称,统一叫作MPU6050。
-
封装两个重要时序
封装指定地址写和指定地址读的时序,在后文的MPU6050初始化中仍有涉及。
(一)void MPU6050_WriteReg(uint8_t RegAddress, uint8_t Data) MPU6050写寄存器
1. 函数功能概述
MPU6050_WriteReg
函数用于向 MPU6050 的指定寄存器写入数据。它通过软件 I²C 协议来实现数据传输,在函数执行过程中,按照 I²C 的通信时序完成数据写入操作。参数是八位寄存器地址,和八位数据。
2. 代码逐行分析
(1)I²C 起始
-
在 I²C 通信中,起始条件用于表示一次数据传输的开始。
MyI2C_Start
函数实现了 I²C 的起始条件。通常,起始条件的产生是通过先确保释放 SDA(数据线),再释放 SCL(时钟线),然后拉低 SDA 和 SCL 来实现的。这个操作告诉 MPU6050(从机),主机(STM32)要开始进行数据传输了。
(2)发送从机地址+读写位
-
这一步是向 I²C 总线发送 MPU6050 的从机地址。
MPU6050_ADDRESS
是 MPU6050 在 I²C 总线上的地址,这里为了方便修改参数,并且突出它是从机地址,用宏定义替换一下这个数据。同时,这里的读写位被设置为 0,表示当前操作是向 MPU6050 写入数据。MyI2C_SendByte
函数按照 I²C 的发送字节时序来发送地址数据,即高位先行,通过循环和位操作实现每一位数据的发送。
(3)接收应答
-
在发送完从机地址后,需要等待 MPU6050 返回应答信号。
MyI2C_ReceiveAck
函数用于接收这个应答信号。如果接收到正确的应答信号,表示 MPU6050 已经准备好接收后续的数据;如果没有接收到应答信号,则可能表示通信出现了问题,例如从机未正常工作或者地址错误等。为了保证时序结构的清晰,在程序中这里就不对返回值进行判断了。
(4)发送寄存器地址
-
首先通过
MyI2C_SendByte
函数发送要写入数据的寄存器地址RegAddress
。这个地址是在函数调用时传入的参数,范围需要参考 MPU6050 手册的寄存器描述。发送完寄存器地址后,再次调用MyI2C_ReceiveAck
函数接收 MPU6050 的应答信号,以确保寄存器地址被正确接收。
(5)发送要写入寄存器的数据
-
接着,通过
MyI2C_SendByte
函数将需要写入寄存器的数据Data
(参数传入,范围是 0x00 - 0xFF)发送到 I²C 总线上。发送完数据后,再次调用MyI2C_ReceiveAck
函数接收 MPU6050 的应答信号,确保数据被正确接收。 -
如果想指定地址写多个字节,就可以用for循环,把这两句套起来,多执行几遍,然后依次把一个数组的各个字节都发送出去。
(6)I²C 终止
-
当数据写入操作完成后,通过
MyI2C_Stop
函数产生 I²C 的终止条件。终止条件通常是先拉低 SDA,再释放 SCL,最后释放 SDA。这个操作表示本次 I²C 数据传输结束,释放 I²C 总线,使 MPU6050 和 STM32 都知道数据传输已经完成。
/**
* 函 数:MPU6050写寄存器
* 参 数:RegAddress 寄存器地址,范围:参考MPU6050手册的寄存器描述
* 参 数:Data 要写入寄存器的数据,范围:0x00~0xFF
* 返 回 值:无
*/
void MPU6050_WriteReg(uint8_t RegAddress, uint8_t Data)
{
MyI2C_Start(); //I2C起始
MyI2C_SendByte(MPU6050_ADDRESS); //发送从机地址,读写位为0,表示即将写入
MyI2C_ReceiveAck(); //接收应答
MyI2C_SendByte(RegAddress); //发送寄存器地址
MyI2C_ReceiveAck(); //接收应答
MyI2C_SendByte(Data); //发送要写入寄存器的数据
MyI2C_ReceiveAck(); //接收应答
MyI2C_Stop(); //I2C终止
}
(二)uint8_t MPU6050_ReadReg(uint8_t RegAddress) MPU6050读寄存器
1. 函数功能概述
MPU6050_ReadReg
函数用于从 MPU6050 的指定寄存器中读取数据。它基于软件 I²C 协议实现数据读取操作,按照特定的 I²C 时序进行通信。参数是指定读的地址。
2. 代码逐行分析
(1)变量定义
-
定义了一个无符号 8 位整数
Data
,用于存储从 MPU6050 寄存器中读取到的数据。
(2)I²C 起始和发送从机地址(写入模式)
-
首先产生 I²C 起始条件,通知 MPU6050 主机要开始进行数据传输。
-
接着发送 MPU6050 的从机地址
MPU6050_ADDRESS
,并且将读写位设置为 0,表示这是一个写入操作。这一步类似于告诉 MPU6050,主机要向它发送一些信息。 -
发送完从机地址后,调用
MyI2C_ReceiveAck
函数接收 MPU6050 返回的应答信号,确保从机已经接收到主机的地址信息。
(3)发送寄存器地址
-
通过
MyI2C_SendByte
函数发送要读取数据的寄存器地址RegAddress
(该地址是函数的输入参数,范围需参考 MPU6050 手册)。 -
发送完寄存器地址后,再次调用
MyI2C_ReceiveAck
函数接收 MPU6050 的应答信号,确保寄存器地址被正确接收。
(4)I²C 重复起始和发送从机地址(读取模式)
-
再次产生 I²C 起始条件,这里的重复起始是 I²C 协议中的一种操作,用于在不释放总线的情况下切换读写模式。
-
发送从机地址也就是 MPU6050 的地址,并将读写位设置为 1,表示这次是读取操作,即主机要从 MPU6050 读取数据。
-
同样,发送完从机地址后,调用
MyI2C_ReceiveAck
函数接收 MPU6050 的应答信号。
(5)接收数据和发送应答
-
通过
MyI2C_ReceiveByte
函数从 MPU6050 接收指定寄存器的数据,并将数据存储到Data
变量中。 -
调用
MyI2C_SendAck
函数发送应答信号,这里参数为 1 表示发送非应答信号(NACK)。由于这里只需要读一个字节,所以发送非应答信号的目的是告诉 MPU6050 主机不再需要接收更多数据,从而终止从机的数据输出。 -
同样如果需要接收多个字节,可以用for循环把这两行套起来,重复读取多次,就能读取多个数据了,当然要注意,读完最后一个字节给非应答,之前都给应答。
(6)I²C 终止
-
最后产生 I²C 终止条件,释放 I²C 总线,表示本次数据读取操作结束。
(7)返回数据
-
将读取到的寄存器数据
Data
返回给调用该函数的地方。
/**
* 函 数:MPU6050读寄存器
* 参 数:RegAddress 寄存器地址,范围:参考MPU6050手册的寄存器描述
* 返 回 值:读取寄存器的数据,范围:0x00~0xFF
*/
uint8_t MPU6050_ReadReg(uint8_t RegAddress)
{
uint8_t Data;
MyI2C_Start(); //I2C起始
MyI2C_SendByte(MPU6050_ADDRESS); //发送从机地址,读写位为0,表示即将写入
MyI2C_ReceiveAck(); //接收应答
MyI2C_SendByte(RegAddress); //发送寄存器地址
MyI2C_ReceiveAck(); //接收应答
MyI2C_Start(); //I2C重复起始
MyI2C_SendByte(MPU6050_ADDRESS | 0x01); //发送从机地址,读写位为1,表示即将读取
MyI2C_ReceiveAck(); //接收应答
Data = MyI2C_ReceiveByte(); //接收指定寄存器的数据
MyI2C_SendAck(1); //发送应答,给从机非应答,终止从机的数据输出
MyI2C_Stop(); //I2C终止
return Data;
}
-
MPU6050的初始化
由于MPU6050这个模块建立在 My12C 之上的,所以要包含底层函数。也要先初始化底层函数。
void MPU6050_Init(void)
1. 函数功能概述
MPU6050_Init
函数用于对 MPU6050 进行初始化操作。它主要完成了两个部分的工作:一是初始化底层的 I²C 通信(通过调用MyI2C_Init
函数),二是根据 MPU6050 数据手册对 MPU6050 的关键寄存器进行配置。
2. 代码逐行析
(1)I²C 初始化
-
MPU6050 是通过 I²C 总线与 STM32 进行通信的。在对 MPU6050 进行任何操作之前,需要先确保 I²C 通信正常。
MyI2C_Init
函数负责初始化 I²C 相关的引脚(如之前所述,可能是将 SCL 和 SDA 引脚初始化为开漏输出模式并设置为高电平),使 I²C 总线处于可用状态,为后续与 MPU6050 的通信做好准备。
(2)MPU6050 寄存器初始化
-
以下是对各个寄存器配置的详细解释:
-
MPU6050 的电源管理寄存器 1(
MPU6050_PWR_MGMT_1
)用于控制设备的电源模式和时钟源选择等。写入0x01
有两个主要作用:-
取消睡眠模式:确保 MPU6050 处于正常工作状态,而不是处于低功耗的睡眠模式。
-
选择时钟源:选择 X 轴陀螺仪作为时钟源,这对于 MPU6050 内部各传感器和模块的同步工作有重要意义。
-
-
电源管理寄存器 2(
MPU6050_PWR_MGMT_2
)在此处被配置为0x00
,这意味着所有轴(可能包括加速度计、陀螺仪等的各个轴)都不会进入待机状态,保证它们能够正常采集数据。 -
采样率分频寄存器(
MPU6050_SMPLRT_DIV
)用于设置 MPU6050 的采样率。写入0x09
这个值是根据具体的应用需求来确定采样率,也就是十分频。不同的采样率会影响数据的采集频率和系统的性能,通过合理配置这个寄存器,可以使 MPU6050 以合适的频率采集数据。决定了数据输出的快慢,值越小越快。 -
配置寄存器(
MPU6050_CONFIG
)在这里被写入0x06
,主要用于配置数字低通滤波器(DLPF)。DLPF 的配置会影响传感器数据的滤波效果,合适的 DLPF 设置可以滤除传感器数据中的高频噪声,提高数据的准确性和稳定性。 -
陀螺仪配置寄存器(
MPU6050_GYRO_CONFIG
)被设置为0x18
,此配置用于选择陀螺仪的满量程范围为 ±2000°/s。根据实际应用场景选择合适的满量程范围可以优化陀螺仪数据的测量精度和分辨率。 -
加速度计配置寄存器(
MPU6050_ACCEL_CONFIG
)设置为0x18
,这是用于选择加速度计的满量程范围为 ±16g。与陀螺仪类似,合理选择加速度计的满量程范围有助于提高加速度测量的准确性和可靠性。
-
-
目前对寄存器的配置是:解除睡眠,选择陀螺仪时钟,6个轴均不待机,采样分频为10,滤波参数给最大,陀螺仪和加速度计都选择最大量程。
-
配置完之后,陀螺仪内部就在连续不断地进行数据转换了,输出的数据,就存放在这里的数据寄存器里,接下来想获取数据的话,只需要再写一个获取数据寄存器的函数即可。
/**
* 函 数:MPU6050初始化
* 参 数:无
* 返 回 值:无
*/
void MPU6050_Init(void)
{
MyI2C_Init(); //先初始化底层的I2C
/*MPU6050寄存器初始化,需要对照MPU6050手册的寄存器描述配置,此处仅配置了部分重要的寄存器*/
MPU6050_WriteReg(MPU6050_PWR_MGMT_1, 0x01); //电源管理寄存器1,取消睡眠模式,选择时钟源为X轴陀螺仪
MPU6050_WriteReg(MPU6050_PWR_MGMT_2, 0x00); //电源管理寄存器2,保持默认值0,所有轴均不待机
MPU6050_WriteReg(MPU6050_SMPLRT_DIV, 0x09); //采样率分频寄存器,配置采样率
MPU6050_WriteReg(MPU6050_CONFIG, 0x06); //配置寄存器,配置DLPF
MPU6050_WriteReg(MPU6050_GYRO_CONFIG, 0x18); //陀螺仪配置寄存器,选择满量程为±2000°/s
MPU6050_WriteReg(MPU6050_ACCEL_CONFIG, 0x18); //加速度计配置寄存器,选择满量程为±16g
}
-
利用宏定义封装寄存器地址
由于在初始化函数中需要多次调用寄存器地址,所以这里我们使用宏定义,把寄存器的地址都用一个字符串表示,避免每次使用都查手册的麻烦,而且光写一个数据的地址放这,也不容易理解,寄存器如果比较少的话,可以直接在模块内进行宏定义,如果比较多的话,可以再新建一个单独的头文件存放。
(一)头文件的建立
在Hardware文件夹下,再添加一个头文件,名字叫MPU6050_Reg,在指定一下存放路径。
(二)头文件的内容
1. 这里写一下防止重复包含的固定格式
#ifndef __MPU6050_REG_H
#define __MPU6050_REG_H
#endif
2. 在中间在写入宏定义
#define MPU6050_SMPLRT_DIV 0x19
#define MPU6050_CONFIG 0x1A
#define MPU6050_GYRO_CONFIG 0x1B
#define MPU6050_ACCEL_CONFIG 0x1C
#define MPU6050_ACCEL_XOUT_H 0x3B
#define MPU6050_ACCEL_XOUT_L 0x3C
#define MPU6050_ACCEL_YOUT_H 0x3D
#define MPU6050_ACCEL_YOUT_L 0x3E
#define MPU6050_ACCEL_ZOUT_H 0x3F
#define MPU6050_ACCEL_ZOUT_L 0x40
#define MPU6050_TEMP_OUT_H 0x41
#define MPU6050_TEMP_OUT_L 0x42
#define MPU6050_GYRO_XOUT_H 0x43
#define MPU6050_GYRO_XOUT_L 0x44
#define MPU6050_GYRO_YOUT_H 0x45
#define MPU6050_GYRO_YOUT_L 0x46
#define MPU6050_GYRO_ZOUT_H 0x47
#define MPU6050_GYRO_ZOUT_L 0x48
#define MPU6050_PWR_MGMT_1 0x6B
#define MPU6050_PWR_MGMT_2 0x6C
#define MPU6050_WHO_AM_I 0x75
(三)该文件的使用
完成该头文件后,就可以在主函数中使用,先把寄存器地址定义的头文件包含进来,就可以在下面的程序中使用宏名调用该寄存器。
-
获取寄存器数据的函数
上述初始化函数结束后,为了获取储存在寄存器中的数据还需要一个获取寄存器数据的函数。
void MPU6050_GetData(int16_t *AccX, int16_t *AccY, int16_t *AccZ,
int16_t *GyroX, int16_t *GyroY, int16_t *GyroZ)
1. 函数功能概述
MPU6050_GetData
函数用于从 MPU6050 获取加速度计和陀螺仪的数据。加速度计的数据包括 X、Y、Z 轴,陀螺仪的数据也包括 X、Y、Z 轴,获取的数据通过输出参数的形式返回给调用函数的地方,这个函数需要返回6个int16t的数据,所以数据范围为 - 32768 到 32767。
2. C语言的知识补充
由于这个函数需要返回6个数据,分别表示XYZ的加速度值和陀螺仪值,但是C语言中,函数的返回值只能有一个,所以,这里就需要一些特殊操作来实现返回6个值的任务,多返回值函数的设计,的方法:第一种,最简单的方法就是,在函数外面定义6个全局变量,子函数读到的数据直接写到全局变量里,然后6个全局变量在主函数里进行共享,这样就相当于返回了6个值,这是一种比较简单且直接的方法,比较适合用在规模比较小的项目中,但这种方法不太利于封装;那第二种,进阶一点的方法,是用指针,进行变量的地址传递,来实现多返回值;然后第三种,更进一步,更高阶的方法,就是用结构体,对多个变量进行打包,然后再统一进行传递,这种方法,就是STM32的库函数里,这里使用到的。但是在这里本文使用第二种方法,利用指针的地址传递,所以在函数参数中写上六个输出参数。这6个参数,均是int16t的指针类型,之后会在主函数里定义变量,通过指针,把主函数变量的地址传递到子函数来,子函数中,通过传递过来的地址,操作主函数的变量,这样,子函数结束之后,主函数变量的值,就是子函数想要返回的值,这就是使用指针,实现函数多返回值的设计。然后子函数中,想要获取数据,就通过ReadReg函数,读取数据寄存器。
int16_t *AccX, int16_t *AccY, int16_t *AccZ,
int16_t *GyroX, int16_t *GyroY, int16_t *GyroZ
3. 代码逐行分析
(1)变量定义
-
定义了两个无符号 8 位整数
DataH
和DataL
,用于分别存储从 MPU6050 读取的数据的高 8 位和低 8 位。
(2)获取加速度计 X 轴数据
-
首先,通过
MPU6050_ReadReg
函数读取加速度计 X 轴数据的高 8 位。MPU6050_ACCEL_XOUT_H
是加速度计 X 轴数据高 8 位对应的寄存器地址。 -
接着,再通过
MPU6050_ReadReg
函数读取加速度计 X 轴数据的低 8 位。MPU6050_ACCEL_XOUT_L
是加速度计 X 轴数据低 8 位对应的寄存器地址。 -
最后,将读取到的高 8 位数据左移 8 位后与低 8 位数据进行按位或操作,实现数据拼接,得到 16 位的加速度计 X 轴数据,并通过
AccX
指针将数据返回给调用函数的地方。 -
这个DataH是8位的数据,它再左移8位,会不会出问题?经过测试,是没问题的,因为最终赋值的变量是16位的,所以8位数据左移之后,会自动进行类型转换,移出去的位并不会丢失,如果实在不放心的话,可以吧这两个变量改成16位的就没问题了。
-
另外,因为手册里说过,这个16位数据是一个用补码表示的有符号数,所以最终直接赋值给int16t,也是没问题的。
(3)获取加速度计 Y 轴数据
-
此部分与获取加速度计 X 轴数据的操作类似。先读取加速度计 Y 轴数据的高 8 位(对应寄存器
MPU6050_ACCEL_YOUT_H
)和低 8 位(对应寄存器MPU6050_ACCEL_YOUT_L
),然后进行数据拼接,通过AccY
指针返回 16 位的加速度计 Y 轴数据。
(4)获取加速度计 Z 轴数据
-
同样,先获取加速度计 Z 轴数据的高 8 位(对应寄存器
MPU6050_ACCEL_ZOUT_H
)和低 8 位(对应寄存器MPU6050_ACCEL_ZOUT_L
),再进行数据拼接,通过AccZ
指针返回 16 位的加速度计 Z 轴数据。
(5)获取陀螺仪 X 轴数据
-
先读取陀螺仪 X 轴数据的高 8 位(对应寄存器
MPU6050_GYRO_XOUT_H
)和低 8 位(对应寄存器MPU6050_GYRO_XOUT_L
),然后将高 8 位左移 8 位与低 8 位进行按位或操作,实现数据拼接,通过GyroX
指针返回 16 位的陀螺仪 X 轴数据。
(6)获取陀螺仪 Y 轴数据
-
这部分操作与获取陀螺仪 X 轴数据类似,先读取高 8 位(对应寄存器
MPU6050_GYRO_YOUT_H
)和低 8 位(对应寄存器MPU6050_GYRO_YOUT_L
),再拼接数据,通过GyroY
指针返回 16 位的陀螺仪 Y 轴数据。
(7)获取陀螺仪 Z 轴数据
-
首先读取陀螺仪 Z 轴数据的高 8 位(对应寄存器
MPU6050_GYRO_ZOUT_H
)和低 8 位(对应寄存器MPU6050_GYRO_ZOUT_L
),然后进行数据拼接,通过GyroZ
指针返回 16 位的陀螺仪 Z 轴数据。
/**
* 函 数:MPU6050获取数据
* 参 数:AccX AccY AccZ 加速度计X、Y、Z轴的数据,使用输出参数的形式返回,范围:-32768~32767
* 参 数:GyroX GyroY GyroZ 陀螺仪X、Y、Z轴的数据,使用输出参数的形式返回,范围:-32768~32767
* 返 回 值:无
*/
void MPU6050_GetData(int16_t *AccX, int16_t *AccY, int16_t *AccZ,
int16_t *GyroX, int16_t *GyroY, int16_t *GyroZ)
{
uint8_t DataH, DataL; //定义数据高8位和低8位的变量
DataH = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_H); //读取加速度计X轴的高8位数据
DataL = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_L); //读取加速度计X轴的低8位数据
*AccX = (DataH << 8) | DataL; //数据拼接,通过输出参数返回
DataH = MPU6050_ReadReg(MPU6050_ACCEL_YOUT_H); //读取加速度计Y轴的高8位数据
DataL = MPU6050_ReadReg(MPU6050_ACCEL_YOUT_L); //读取加速度计Y轴的低8位数据
*AccY = (DataH << 8) | DataL; //数据拼接,通过输出参数返回
DataH = MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_H); //读取加速度计Z轴的高8位数据
DataL = MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_L); //读取加速度计Z轴的低8位数据
*AccZ = (DataH << 8) | DataL; //数据拼接,通过输出参数返回
DataH = MPU6050_ReadReg(MPU6050_GYRO_XOUT_H); //读取陀螺仪X轴的高8位数据
DataL = MPU6050_ReadReg(MPU6050_GYRO_XOUT_L); //读取陀螺仪X轴的低8位数据
*GyroX = (DataH << 8) | DataL; //数据拼接,通过输出参数返回
DataH = MPU6050_ReadReg(MPU6050_GYRO_YOUT_H); //读取陀螺仪Y轴的高8位数据
DataL = MPU6050_ReadReg(MPU6050_GYRO_YOUT_L); //读取陀螺仪Y轴的低8位数据
*GyroY = (DataH << 8) | DataL; //数据拼接,通过输出参数返回
DataH = MPU6050_ReadReg(MPU6050_GYRO_ZOUT_H); //读取陀螺仪Z轴的高8位数据
DataL = MPU6050_ReadReg(MPU6050_GYRO_ZOUT_L); //读取陀螺仪Z轴的低8位数据
*GyroZ = (DataH << 8) | DataL; //数据拼接,通过输出参数返回
}
补充:这里,是用读取一个寄存器的函数,连续调用了12次,才读取完12个寄存器,但实际上,还有一种更高效的方法,就是使用12C读取多个字节的时序,从一个基地址开始,连续读取一片的寄存器,因为这里的寄存器的地址,是连续的,所以可以从第一个寄存器的地址0x3B开始,连续读取14个字节,这样就可以一次性地,把加速度值、陀螺仪值,还包括两个字节的温度值,都读出来了,这样,在时序上,读取效率就会大大提升。
-
获取ID号的函数
uint8_t MPU6050_GetID(void)
1. 函数功能概述
MPU6050_GetID
函数的目的是获取 MPU6050 的 ID 号。这个 ID 号对于识别设备是否正常工作以及是否是预期的 MPU6050 芯片非常重要。
2. 代码分析
-
在函数内部,仅有一行代码:
return MPU6050_ReadReg(MPU6050_WHO_AM_I);
。 -
这里调用了
MPU6050_ReadReg
函数来读取MPU6050_WHO_AM_I
寄存器的值。MPU6050_WHO_AM_I
是 MPU6050 芯片中的一个特殊寄存器,用于存储芯片的 ID 信息。 -
根据 MPU6050 的数据手册,通过读取
MPU6050_WHO_AM_I
寄存器,可以得到芯片的身份标识。正常情况下,这个值应该是一个特定的值(例如,对于 MPU6050 来说,这个值通常为 0x68),通过获取这个值并与预期值进行比较,可以验证芯片是否正常工作以及是否正确连接和配置。
/**
* 函 数:MPU6050获取ID号
* 参 数:无
* 返 回 值:MPU6050的ID号
*/
uint8_t MPU6050_GetID(void)
{
return MPU6050_ReadReg(MPU6050_WHO_AM_I); //返回WHO_AM_I寄存器的值
}
-
主函数部分代码详解
1. 头文件包含
-
stm32f10x.h
是 STM32F10x 系列微控制器的标准头文件,它包含了对芯片内部寄存器、数据类型等的定义,是进行 STM32 编程的基础。 -
Delay.h
应该是用户自定义的延时函数头文件,在程序中可能用于产生精确的时间延迟,比如在 I²C 通信时序中需要精确的延时操作。 -
OLED.h
是与 OLED 显示屏相关的头文件,用于控制 OLED 显示屏进行数据显示操作。 -
MPU6050.h
是与 MPU6050 传感器相关的头文件,包含了对 MPU6050 操作的函数声明等。
2. 变量定义
-
ID
是一个无符号 8 位整数变量,用于存储从 MPU6050 获取的 ID 号。通过获取 ID 号可以验证 MPU6050 是否正常工作以及是否是预期的芯片。 -
AX
、AY
、AZ
、GX
、GY
、GZ
是有符号 16 位整数变量,分别用于存储 MPU6050 的加速度计(X、Y、Z 轴)和陀螺仪(X、Y、Z 轴)的数据。
3. 主函数 - 模块初始化
-
OLED_Init
函数用于初始化 OLED 显示屏。初始化过程可能包括对 OLED 显示屏的引脚配置、通信参数设置等操作,使其能够正常接收和显示数据。 -
MPU6050_Init
函数用于初始化 MPU6050 传感器。这可能包括对 MPU6050 的 I²C 通信初始化(如之前提到的对 I²C 引脚的初始化、设置默认电平),以及对 MPU6050 内部关键寄存器的配置,例如电源管理寄存器、采样率分频寄存器、配置寄存器、陀螺仪和加速度计配置寄存器等,使其进入正常工作状态并按照设定的参数采集数据。
4. 主函数 - 显示 ID 号
-
首先通过
OLED_ShowString
函数在 OLED 显示屏的第 1 行第 1 列显示字符串 "ID:",用于标识后面显示的数据内容。 -
接着调用
MPU6050_GetID
函数获取 MPU6050 的 ID 号,并将其存储在变量ID
中。 -
最后通过
OLED_ShowHexNum
函数在 OLED 显示屏的第 1 行第 4 列以十六进制形式显示获取到的 ID 号,显示宽度为 2 个字符。
5. 主函数 - 循环获取并显示数据
-
在无限循环
while(1)
中:-
首先调用
MPU6050_GetData
函数获取 MPU6050 的加速度计和陀螺仪数据,并通过指针将数据分别存储到变量AX
、AY
、AZ
、GX
、GY
、GZ
中。 -
然后通过
OLED_ShowSignedNum
函数分别在 OLED 显示屏的不同位置显示这些数据。例如,在第 2 行第 1 列显示加速度计 X 轴数据AX
,显示宽度为 5 个字符,并且以有符号数形式显示。同样地,对其他轴的数据也进行相应的显示操作,以便用户能够直观地看到 MPU6050 采集到的数据。
-
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "MPU6050.h"
uint8_t ID; //定义用于存放ID号的变量
int16_t AX, AY, AZ, GX, GY, GZ; //定义用于存放各个数据的变量
int main(void)
{
/*模块初始化*/
OLED_Init(); //OLED初始化
MPU6050_Init(); //MPU6050初始化
/*显示ID号*/
OLED_ShowString(1, 1, "ID:"); //显示静态字符串
ID = MPU6050_GetID(); //获取MPU6050的ID号
OLED_ShowHexNum(1, 4, ID, 2); //OLED显示ID号
while (1)
{
MPU6050_GetData(&AX, &AY, &AZ, &GX, &GY, &GZ); //获取MPU6050的数据
OLED_ShowSignedNum(2, 1, AX, 5); //OLED显示数据
OLED_ShowSignedNum(3, 1, AY, 5);
OLED_ShowSignedNum(4, 1, AZ, 5);
OLED_ShowSignedNum(2, 8, GX, 5);
OLED_ShowSignedNum(3, 8, GY, 5);
OLED_ShowSignedNum(4, 8, GZ, 5);
}
}
总结:
这一节,文中用了多层的模块架构,最底层12C协议层,主要关注点是,引脚是哪两个,什么配置,时序什么时候高电平,什么时候低电平,这些协议相关的内容,之后协议层之上,是MPU6050的驱动层,主要关注点是如何读写寄存器,怎么配置寄存器,怎么读取数据,这些驱动相关的内容,最后就是主函数的应用层了,在这里,只需要调用GetData函数,得到数据就行了,剩下的,主要关注点是如何用这些数据,完成程序的功能设计,当然目前的功能只是简单地显示一下数据。这就是分层的框架,有了良好的分层结构,在写每一层的时候,就可以专注每一层的任务,其他层的事情不需要管,完成一层后,就对着一层进行测试,测试通过了,再进行后续的代码。