STM32标准库——(14)I2C通信协议、MPU6050简介

目录

1.I2C通信

2.I2C硬件电路

3.I2C时序单元

3.1 起始和终止

3.2 发送字节

3.3 接收字节

3.4 发送应答及接收应答

4.I2C时序

4.1 指定地址写

4.2 当前地址读

4.3 指定地址读

5.MPU6050

5.1 简介

 5.2 自身参数

5.3 硬件电路

5.4 框图

5.5 常用寄存器

​编辑

6.软件I2C读写MPU6050

6.1 接线图

6.2相关代码

6.3 现象


 

1.I2C通信

c9d9f9e2ee654ef48bffaac42db8fe36.png

  1. I2C 通讯协议(Inter-Integrated Circuit)是由Phiilps公司开发的,由于它引脚少,硬件实现简单,可扩展性强, 不需要USART、CAN等通讯协议的外部收发设备,现在被广泛地使用在系统内多个集成电路(IC)间的通讯。
  2. I2C总线是一种用于芯片之间进行通信的串行总线。它由两条线组成:串行时钟线(SCL)和串行数据线(SDA)。SDA(Serial data)是数据线,D代表Data也就是数据,Send Data 也就是用来传输数据的;SCL(Serial clock line)是时钟线,C代表Clock 也就是时钟 也就是控制数据发送的时序的。这种总线允许多个设备在同一条总线上进行通信。
  3. 作为一个通信协议,它必须要在硬件和软件上都做出规定,硬件上的规定,就是你的电路应该如何连接,端口的输入输出模式都是啥样的,软件上的规定,就是你的时序是怎么定义的,字节如何传输,高位先行还是低位先行,一个完整的时序有哪些部分构成这些东西,硬件的规定和软件的规定配合起来,就是一个完整的通信协议。

2.I2C硬件电路

• 所有I2C设备的SCL连在一起,SDA连在一起

• 设备的SCL和SDA均要配置成开漏输出模式

• SCL和SDA各添加一个上拉电阻,阻值一般为4.7KΩ左右

4455da27e3bd402fa40d4aca69406206.png

05c5992eb4434644814ae63773841605.png

  1.  第一个就是I2C的典型电路模型,这个模型采用了一主多从的结构。我们可以看到CPU作为主设备,控制着总线并拥有很大的权利。其中,主机对SCL线拥有完全的控制权,无论何时何地,主机都负责掌控SCL线。在空闲状态下,主机还可以主动发起对SDA的控制。但是,从机发送数据或应答时,主机需要将SDA的控制权转交给从机。
  2. 我们看到了一系列被控IC,它们是挂载在I2C总线上的从机设备,如姿态传感器、OLED、存储器、时钟模块等。这些从机的权利相对较小。对于SCL时钟线,它们在任何时刻都只能被动的读取,不允许控制SCL线;对于SDA数据线,从机也不允许主动发起控制,只有在主机发送读取从机的命令后,或从机应答时,从机才能短暂地取得SDA的控制权。这就是一主多从模型中协议的规定。

  3. 由于现在是一主多从结构,主机拥有SCL的绝对控制权,因此主机的SCL可以配置成推挽输出,所有从机的SCL都配置成浮空输入或上拉输入。数据流向为主机发送、所有从机接收。但是到SDA线这里就比较复杂了,因为这是半双工协议,所以主机的SDA在发送时是输出,在接收时是输入。同样地,从机的SDA也会在输入和输出之间反复切换。如果能够协调好输入输出的切换时机就没有问题。但是这样做的话,如果总线时序没有协调好,就极有可能发生两个引脚同时处于输出的状态。如果此时一个引脚输出高电平,一个引脚输出低电平,就会造成电源短路的情况,这是要极力避免的。为了避免这种情况的发生,I2C的设计规定所有设备不输出强上拉的高电平,而是采用外置弱上拉电阻加开漏输出的电路结构。这两点规定对应于前面提到的“设备的SCL和SDA均要配置成开漏输出模式”以及“SCL和SDA各添加一个上拉电阻,阻值一般为4.7KΩ左右”。对应第二个图。

  4. 推挽输出:上面一个开关管连接正极,下面一个开关管连接负极。当上面导通时,输出高电平;下面导通时,输出低电平。因为这是通过开关管直接连接到正负极的,所以这是强上拉和强下拉的模式。
    开漏输出:就是去掉这个强上拉的开关管,输出低电平时,下管导通,是强下拉,输出高电平时,下管断开,但是没有上管了,此时引脚处于浮空的状态,这就是开漏输出。                 (我们之前的弹簧和杆子的模型来解释,就是SCL或SDA就是一根杆子,为了防止有人向上推杆子,有人向下拉杆子造成冲突,我们就规定所有的人不准向上推杆子,只能选择向下拉或者放手,然后我们再外置一根弹簧向上拉,你要输出低电平就往下拽,这个弹簧肯定拽不赢你,所以弹簧被拉伸杆子处于低电平状态,你要输出高电平就放手,杆子在弹簧的拉力下回弹到高电平,这就是一个弱上拉的高电平,但是完全不影响数据传输。)

  5. 就是这个模式会有一个“线与”的现象。就是只要有任意一个或多个设备输出了低电平,总线就处于低电平,只有所有设备都输出高电平,总线才处于高电平。I2C可以利用这个电路特性执行多主机模式下的时钟同步和总线仲裁,所以这里SCL虽然在一主多从模式下可以用推挽输出,但是它仍然采用了开漏加上拉输出的模式,因为在多主机模式下会利用到这个特征。

3.I2C时序单元

3.1 起始和终止

40417ed170214a6da35577df80db72eb.png

  •  起始条件是指SCL高电平期间,SDA从高电平切换到低电平。在I2C总线处于空闲状态时,SCL和SDA都处于高电平状态,也就是没有任何一个设备去碰SCL和SDA,由外挂的上拉电阻拉高至高电平,总线处于平静的高电平状态。当主机需要数据收发时打破平静,会首先产生一个起始条件。这个起始条件是,SCL保持高电平,然后把SDA拉低,产生一个下降沿。当从机捕获到这个SCL高电平,SDA下降沿信号时,就会进行自身的复位,等待主机的召唤。之后,主机需要将SCL拉低。这样做一方面是占用这个总线,另一方面也是为了方便这些基本单元的拼接。这样,除了起始和终止条件,每个时序单元的SCL都是以低电平开始,低电平结束。
  • 终止条件是SCL高电平期间,SDA从低电平切换到高电平。SCL先放开并回弹到高电平,SDA再放开并回弹高电平,产生一个上升沿。这个上升沿触发终止条件,同时终止条件之后,SCL和SDA都是高电平,回归到最初的平静状态。这个起始条件和终止条件就类似串口时序里的起始位和停止位。一个完整的数据帧总是以起始条件开始、终止条件结束。另外,起始和终止都是由主机产生的。因此,从机必须始终保持双手放开,不允许主动跳出来去碰总线。如果允许从机这样做,那么就会变成多主机模型,不在本节的讨论范围之内。这就是起始条件和终止条件的含义。

3.2 发送字节

fbafc0af88414b749c987a1ccf966cc4.png

 就是SCL低电平期间,主机将数据位依次放到SDA线上,高位先行,然后释放SCL,从机将在SCL高电平期间读取数据位,所以SCL高电平期间,SDA不允许有数据变化,依次循环上述过程8次即可发送一个字节,起始条件之后,第一个字节也必须是主机发送的,主机如何发送呢,就是最开始SCL低电平,主机如果想发送0,就拉低SDA到低电平,如果想发送1就放手,SDA回弹到高电平,在SCL低电平期间允许改变SDA的电平,当这一位放好之后,主机就松手时钟线,SCL回弹到高电平,在高电平期间是从机读取SDA的时候,所以高电平期间SDA不允许变化,那主机在放手SCL一段时间后就可以继续拉低SCL传输下一位了,主机也需要在SCL下降沿之后,尽快把数据放在SDA上,但是主机有时钟的主导权哈,所以主机并不需要那么着急,只需要在低电平的任意时刻把数据放在SDA上就行了,晚点也没关系,数据放完之后,主机再松手SCL,SCL高电平,从机读取这一位。就这样的流程,主机拉低SCL,把数据放在SDA上,主机松开SCL,从机读取SDA的数据,在SCL的同步下,依次进行主机发送和从机接收,循环8次,就发送了8位数据,也就是一个字节,另外注意,这里是高位先行,所以第一位是一个字节的最高位B7,然后依次是次高位B6…这个和串口是不一样的,串口时序是低位先行,这里I2C是高位先行。

另外由于这里有时钟线进行同步,所以如果主机一个字节发送一半,突然进中断了,不操作SCL和SDA的,那时序就会在中段的位置不断拉长,SCL和SDA电平都暂停变化,传输也完全暂停,等中段结束后,主机回来继续操作,传输仍然不会出问题,这就是同步时序的好处。

3.3 接收字节

bbfd50c8939545ea945af969fb72b8f3.png

和上面接收字节基本一样,区别就是SDA线,主机在接收之前需要释放SDA,然后这时从机就取得了SDA的控制权,从机需要发送0就把SDA拉低,从机需要发送1就放手,SDA回弹高电平,然后同样的,低电平变换数据,高电平读取数据,这里实线部分表示主机控制的电平,虚线部分表示从机控制的电平,SCL全程由主机控制,SDA主机在接收前要释放,交由从机控制,之后还是一样,因为SCL始终是有主机控制的,所以从机的数据变换基本上都是贴着SCL下降沿进行的,而主机可以在SCL高电平的任意时刻读取,这是接收一个字节的时序。

3.4 发送应答及接收应答

d4cad32b0ec1428f8b58f45b19481d31.png

 发送应答和接收应答就是当我们在调用发送一个字节之后,就要紧跟着调用接收应答的时序,用来判断从机有没有收到刚才给它的数据,如果从机收到了,那在应答位这里,主机释放SDA的时候,从机就应该立刻把SDA拉下来,然后在SCL高电平期间,主机读取应答位,如果应答位为0,就说明从机确实收到了

4.I2C时序

4.1 指定地址写

ddd707a777e54b4187e2419231cc8aae.png

  •  在这里上面的线是SCL,下面的线是SDA空闲状态都是高电平,然后主机需要给从机写入数据的时候,首先SCL高电平期间,拉低SDA产生起始条件,在起始条件之后,紧跟着的时序,必须是发送一个字节的时序,字节的内容必须是从机地址+读写位,正好从机地址是7位,读写位是1位,加起来是一个字节8位,发送从机地址,就是确定通信的对象,发送读写位,就是确认我接下来是要写入还是要读出,具体发送的时候呢,在这里低电平期间SDA变换数据,高电平期间从机读取SDA,这里我用绿色的线来标明了从机读到的数据,比如这样的波形,那从机收到的第一位就是高电平1,然后SCL低电平期间主机继续变换数据,因为第二位还是1,所以这里SDA电平并没有变换,然后SCL高电平,从机读到第二位是1,之后继续低电平变换数据,高电平读取数据,第三位就是0,这样持续8次就发送了一个字节数据,其中这个数据的定义:高7位表示从机地址,比如这个波形下,主机寻找的统计地址就是1101000,这个就是MPU6050的地址,然后最低位表示读写位,0表示之后的时序主机要进行写入操作,1表示之后的时序主机要进行读出操作,这里是0,说明之后我们要进行写入操作,那目前主机是发生了一个字节,字节内容转化为16进制,高位先行就是0xD0 ,然后根据协议规定,紧跟着的单元就得是接收从机的应答位(Receive Ack(RA)),在这个时刻主机要释放SDA,释放SDA之后引脚电平回弹到高电平,.但是根据协议规定,从机要在这个位拉低SDA,所以单看从机的波形,该应答的时候从机立刻拽住SDA,然后应答结束之后,从机再放开SDA,那现在综合两者的波形,结合线与的特性,在主机释放SDA之后,由于SDA也被从机拽住了,所以主机松手后,SDA并没有回弹高电平。
  • 应答结束后,我们要继续发送一个字节,同样的时序再来一遍,第二个字节就可以送到指定设备的内部来,从机设备可以自己定义第二个字节和后续字节的用途,一般第二个字节可以是寄存器地址,或者是指令控制字等,比如MPU16050定义的第二个字节就是寄存器地址,比如AD转换器,第二个字节可能就是指令控制字,比如存储器,第二个字节可能就是存储器地址,那图示这里主机发送这样一个波形,我们一一判定,数据为00011001,即主机向从机发送了0x19 这个数据,第一部分解读的前七位出来的是MPU6050,在MPU6050 里,就表示我要操作你0x19地址下的寄存器了,接着同样是从机应答,主机释放SDA,从机拽住SDA,SDA表现为低电平,主机收到应答位为0,表示收到了从机的应答,然后继续同样的流程。

  • 再来一遍,主机再发送一个字节,这个字节就是主机想要写入到0x19地址下寄存器的内容了,比如这里发送了0xAA的波形,就表示我在0x19地址下写入0xAA,最后是接收应答位,如果主机不需要继续传输了,就可以产生停止条件,在停止条件之前先拉低SDA,为后续SDA的上升沿做准备,然后释放SCL,再释放SDA,这样就产生了SCL高电平期间SDA的上升沿,这样一个完整的数据帧就拼接完成了,那套用上面这句话呢,这个数据帧的目的,就是对于指定从机地址为1001000的设备,在其内部0x19地址下的寄存器中,写入0xAA这个数据。

4.2 当前地址读

d20bb9f0c7da4da792763b580c9a0a8c.png

 如果主机想要读取从机的数据,就可以执行这个时序,那最开始还是SCL高电平期间,拉低SDA产生起始条件,起始条件开始后,主机必须首先调用发送一个字节,来进行从机的寻址和指定读写标志位,比如图示的波形,表示本次寻址的目标是1101000的设备,同时最后一位读写标志为1,表示主机接下来想要读取数据,紧跟着发送一个字节之后接收一下从机应答位,从机应答为0代表从机收到了第一个字节,在从机应答之后,从这里开始数据的传输方向就要反过来了,因为刚才主机发出了读的命令,所以这之后主机就不能继续发送了,要把SDA的控制权交给从机,主机调用接收一个字节的时序进行接收操作,然后在这一块从机就得到了主机的允许,可以在SCL低电平之间写入SDA,然后主机在SCL高电平期间读取SDA,那最终主机在SCL高电平期间,依次读取8位,就接收到了从机发送的一个字节数据,00001111,也就是0x0f,那现在问题就来了,这个0x0f是从机哪个寄存器的数据呢,我们看到在读的时序中,I2C协议的规定是,主机进行寻址时,一旦读写标志位给1了,下一个字节就要立马转为读的时序,所以主机还来不及指定,我想要读哪个寄存器就得开始接收了,所以这里就没有指定地址这个环节,那主机并没有指定寄存器的地址,从机到底该发哪个寄存器的数据呢,这需要用到我们上面说的当前地址指针了,在从机中,所有的寄存器被分配到了一个线性区域中,并且会有个单独的指针变量,指示着其中一个寄存器,这个指针上电默认一般指向0地址,并且每写入一个字节和读出一个字节后,这个指针就会自动自增一次,移动到下一个位置,主机没有指定要读哪个地址,从机就会返回当前指针指向的寄存器的值,那假设我刚刚调用了这个指定地址写的时序,在0x19 的位置写出了0xAA,那么指针就会加1移动到0x1A(0x19+1=0x1A)的位置,我再调用这个当前地址读的时序,返回的就是0x1A地址下的值,如果再调用一次,返回的就是0x1B地址下的值,以此类推,这就是当前地址读时序的操作逻辑,由于当前地址读并不能指定读的地址,所以这个时序用的不是很多。

4.3 指定地址读

733b7cae6f594144a78070eb001fe994.png

首先最开始仍然是启动条件,然后发送一个字节进行寻址,这里指定从机地址是1101000,读写标志位是0,代表我要进行写的操作,经过从机应答之后,再发送一个字节,第二个字节用来指定地   址,这个数据就写入到了从机的地址指针里了,也就是说从机接收到这个数据之后,它的寄存器指针就指向了0x19 这个位置,之后我们要写入的数据,不给他发,而是直接再来个起始条件,这个Sr的意思就是重复起始条件,相当于另起一个时序,因为指定读写标志位,只能是跟着起始条件的第一个字节,所以如果想切换读写方向,只能再来个起始条件,然后起始条件后重新寻址并且指定读写标志位,此时读写标志位是1代表我要开始读了,接着主机接收一个字节,这个字节是不是就是0x19 地址下的数据,这就是指定地址读,你也可以再加一个停止条件,这样也行哈,这样的话就是两个完整的时序了,先起始写入地址停止,因为写入的地址会存在地址指针里面,所以这个地址并不会因为时序的停止而消失,我们就可以再提示读当前位置停止,这样两条时序也可以完成任务,但是I2C协议官方规定的复合格式是一整个数据帧,就是先起始再重复起始再停止,相当于把两条时序拼接成一条。

5.MPU6050

5.1 简介

224424ad61434aee8a7a95a82be63a53.png

 5.2 自身参数

88dfef2f68494822838826c13a1c4bbf.png

芯片进行I2C通信的从机地址,这个可以在手册里查到,当AD0等于0,地址为1001000,当AD0等于1时,地址为1001001,AD0就是板子引出来的一个引脚,可以调节I2C从机地址的最低位,这里地址是七位的。 如果像这样用二进制来表示的话,一般没啥问题,如果在程序中用16进制表示的话,一般会有两种表示方式,以这个1001000的地址为例,第一种就是单纯的把这七位的二进制转化为16进制,这里1001000低4位和高3位切开转换,16进制就是0x68(100 1000前面补了个0) ,所以有的地方就说MPU6050的从机地址是0x68 ,然后我们看一下之前I2C通信的时序,这里第一个字节的高7位是从机地址,最低位是读写位,所以如果你认为0x68是从机地址的话,在发送第一个字节时,要先把0x68 左移一位,再按位或上读写位,读1写0,这是认为从机地址是0x68 的操作,当然目前还有另一种常见的表示方式,就是把0x68 左移移位后的数据当做从机地址,0x68 左移1位之后是0xD0 ,那这样MPU6050的从机地址就是0xD0 ,这时在实际发送第一个字节时,如果你要写,就直接把0xD0 当做第一个字节;如果你要读就把0xd0或上0x01 即0xD1当做第一个字节,这种表示方式就不需要进行左移的操作了,或者说这种表示方式是把读写位也融入到了从机地址里来,0xD0 是写地址,0xD1是读地址,这样表示的,所以你之后看到有地方说0xD0是MPU6050的从机地址,那它就是融入了读写位的从机地址,如果你看到有地方说0x68是MPU6050的从机地址,这也不要奇怪,这种方式就是直接把7位地址转换16进制得到的,在实际发送第一个字节时,不要忘了先左移一位,再或上读写位,这是两种统计地址的表示方式。

5.3 硬件电路

d127c471fdae40c4a489918928d67bbf.png

75a1b24777ba433390fee8889f8caf2a.png

  1.  左上角是一个LDO低压差线性稳压器,这部分是供电的逻辑,手册里介绍这个MPU6050 芯片的VDD供电是2.375~3.46V属于3.3V供电的设备,不能直接接5V,所以为了扩大供电范围,这个模块的设计者就加了个3.3V的稳压器,输入端电压vcc_5v可以在3.3v到5v之间,然后经过3.3伏的稳压器输出稳定的3.3伏电压给芯片端供电,然后这一块是电源指示灯,只要3.3v端有电,电源指示灯就会亮.
  2. 左下角是一个八针的排针,有VCC和GND这两个引脚是电源供电,然后SCL和SDA这两个引脚是I2C通信的引脚,在这里可以看到,SCL和SDA模块已经内置了两个4.7k的上拉电阻了,所以在我们接线的时候,直接把SCL和SDA接在GPIO口就行了,不需要再在外面另外接上拉电阻了,接着下面有XCL和XDA这两个是芯片里面的主机I2C通信引脚,设计这两个引脚是为了扩展芯片功能,之前我们说过,MPU6050是一个六轴姿态传感器,这是九轴姿态传感器多出的磁力计的作用,另外如果你要制作无人机,需要定高飞行,这时候就还需要增加气压计,扩展为十轴提供一个高度信息的稳定参考,所以根据项目要求啊,这个六轴传感器可能不够用,需要进行扩展,那这个时候这个XCL和XDA就可以起作用了,XCL和XDA通常就是用于外接磁力计或者气压计,当接上磁力计或气压计之后,MPU6050的主机接口可以直接访问这些扩展芯片的数据,把这些扩展芯片的数据读取到MPU6050 里面,在MPU6050 里面会有DMP单元进行数据融合和姿态解算,如果你不需要按MPU6050 的解算功能的话,也可以把这个磁力计或者气压计直接挂载在XCL和XDA这条总线上,因为I2C本来就可以挂载多设备,所以把多个设备都挂载在一起也是没问题的。下面AD0引脚,这个之前说过,他是从机地址的最低位,接低电平的话七位从机地址就是1001000,接高电平的话七位从机地址就是1001001,这里电路中可以看到有一个电阻默认弱下拉到低电平了,所以引脚悬空的话就是低电平,如果想接高电平,就可以把AD0直接引到VCC,强上拉至高电平。最后一个引脚是INT,也就是中断输出引脚,可以配置芯片内部的一些事件来触发中断引脚的输出,比如数据准备好了、I2C主机错误等,另外芯片内部还内置了一些实用的小功能、比如自由落体检测、运动检测、零运动检测等,这些信号都可以触发INT引脚产生电平跳变,需要的话可以进行中断信号的配置,但如果不要的话,那也可以不配置这个引脚。

5.4 框图

440a55168c304621931438f6ea23eb30.png

  • 左上角是时钟系统,有时钟输入脚和输出脚,不过我们一般使用内部时钟,硬件电路这里CLKIN直接接了地,CLKOUT没有引出所以这部分不需要过多关心,然后下面这些灰色的部分就是芯片内部的传感器,包括x y z轴的陀螺仪陀螺仪,另外这个芯片还内置了一个温度传感器,你要是想用它来测量温度也是没问题的,那这么多传感器本质上也都相当于可变电阻,通过分压后输出模拟电压,然后通过ADC进行模数转换,转化完成之后呢,这些传感器的数据统一都放到数据寄存器中,我们读取数据寄存器就能得到传感器测量的值了,这个芯片内部的转换都是全自动进行的,就类似我们之前学的AD连续转换加DMA转运,每个ADC输出,对应16位的数据寄存器,不存在数据覆盖的问题,我们配置好转换频率之后,每个数据就自动以我们设置的频率刷新到数据寄存器,我们需要数据的时候直接来读就行

  • 接着每个传感器都有个自测单元self test,这部分是用来验证芯片好坏的,当启动自测后,芯片内部就会模拟一个外力施加在传感器上,这个外力导致传感器数据会比平时大一些,那如何进行自测呢,我们可以先使能自测读取数据,再失能自测读取数据,两个数据相减得到的数据叫自测响应,芯片手册里给出了一个范围,如果自测响应在这个范围内就说明芯片没问题,如果不在就说明芯片可能坏了,使用的时候就要小心点,这个是自测的功能。

  • 右边这一大块就是寄存器和通信接口部分了,中断状态寄存器可以控制内部的哪些事件到中断引脚的输出,FIFO是先入先出寄存器,可以对数据流进行缓存,我们本节暂时不用,配置寄存器,可以对内部的各个电路进行配置,传感器寄存器也就是数据寄存器,存储在各个传感器的数据,工厂校准这个意思就是内部的传感器都进行了校准我们不用了解,然后右边这个数字运动处理器简称DMP,还是芯片内部自带的一个姿态解算的硬件算法,配合官方的DMP库可以进行姿态解算,因为姿态解算还是比较难的,而且算法也很复杂,所以如果使用了内部的DMP进行姿态解算,姿态解算就会方便一些,暂时不涉及,这个FSYNC是帧同步,我们用不到,最后上面这块就是通信接口部分,上面一部分就是从机的I2C和SPI通信接口,用于和stm32通信,下面这一部分是主机的I2C通信接口,用于和MPU6050扩展的设备进行通信,这里有个接口旁路选择器(MUX)就是一个开关,如果拨到上面,辅助的I2C引脚就和正常的I2C引脚接到一起,这样两路总线就合在一起了,stm32可以控制所有设备,这时sm32就是大哥MPU6050和这个扩展设备都是stm32的小弟,如果拨到下面,辅助的I2C引脚就由mpu6050控制,两条I2C总线独立分开,这时stm32是mpu6050的大哥,MPU6050又是扩展设备的大哥,我们本节课程不会用到这个扩展功能。

5.5 常用寄存器

d56041f216e94d8b854d541beb5ae08c.png

ffd4ecf0b3894de382bf86ef98f0c788.png

bbdc64a4ef774a4290367de21d0ba77c.png

  1.  6B(电源管理寄存器1):设备复位:0(不复位) 睡眠模式:0(解除睡眠) 循环模式:0(不需要循环) 无关位:0  温度传感器使能:0  选择时钟:000(内部时钟)、001(x轴陀螺仪时钟(推荐))
  2. 6C(电源管理寄存器2):循环模式唤醒频率:00 每一个轴的待机位:全给0(不需要待机)
  3. 19(采样率分频):八位决定了数据输出的快慢 值越小越快 后续程序中给0x09 也就是10分频
  4. 1A(配置寄存器):外步同步:000(不需要)  数字低通滤波器:110(最平滑的滤波)
  5. 1B(陀螺仪配置寄存器):自测使能:000  满量程选择:11(最大量程)
  6. 1C(加速度计配置寄存器):自测使能:000  满量程选择:11(最大量程) 高通滤波器:000(不需要)

6.软件I2C读写MPU6050

6.1 接线图

d13509cf6c0e4f908e046a9619e4021f.jpeg

6.2相关代码

6.2.1 MyI2C.c

#include "stm32f10x.h"                  // Device header
#include "Delay.h"

/*引脚配置层*/

/**
  * 函    数: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,防止时序频率超过要求
}

/**
  * 函    数: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电平
}

/**
  * 函    数: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起始
  * 参    数:无
  * 返 回 值:无
  */
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也拉低,即为了占用总线,也为了方便总线时序的拼接
}

/**
  * 函    数:I2C终止
  * 参    数:无
  * 返 回 值:无
  */
void MyI2C_Stop(void)
{
	MyI2C_W_SDA(0);							//拉低SDA,确保SDA为低电平
	MyI2C_W_SCL(1);							//释放SCL,使SCL呈现高电平
	MyI2C_W_SDA(1);							//在SCL高电平期间,释放SDA,产生终止信号
}

/**
  * 函    数: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线
					//Byte或上1000 0000等于Byte 接着右移一位 即Byte与上0100 0000 等于Byte
		MyI2C_W_SCL(1);						//释放SCL,从机在SCL高电平期间读取SDA
		MyI2C_W_SCL(0);						//拉低SCL,主机开始发送下一位数据
	}
}

/**
  * 函    数: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);//若Byte等于1 则与上1000 0000就等于1 若Byte等于0 则与上1000 0000还是为0
		}	//读取SDA数据,并存储到Byte变量
														//当SDA为1时,置变量指定位为1,当SDA为0时,不做处理,指定位为默认的初值0
		MyI2C_W_SCL(0);						//拉低SCL,从机在SCL低电平期间写入SDA
	}
	return Byte;							//返回接收到的一个字节数据
}

/**
  * 函    数: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;							//返回定义应答位变量
}

6.2.2 MyI2C.h

#ifndef __MYI2C_H
#define __MYI2C_H

void MyI2C_Init(void);
void MyI2C_Start(void);
void MyI2C_Stop(void);
void MyI2C_SendByte(uint8_t Byte);
uint8_t MyI2C_ReceiveByte(void);
void MyI2C_SendAck(uint8_t AckBit);
uint8_t MyI2C_ReceiveAck(void);

#endif

 6.2.3 MPU6050.c

#include "stm32f10x.h"                  // Device header
#include "MyI2C.h"
#include "MPU6050_Reg.h"

#define MPU6050_ADDRESS		0xD0		//MPU6050的I2C从机地址

/**
  * 函    数: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终止
}

/**
  * 函    数: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初始化
  * 参    数:无
  * 返 回 值:无
  */
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
}

/**
  * 函    数:MPU6050获取ID号
  * 参    数:无
  * 返 回 值:MPU6050的ID号
  */
uint8_t MPU6050_GetID(void)
{
	return MPU6050_ReadReg(MPU6050_WHO_AM_I);		//返回WHO_AM_I寄存器的值
}

/**
  * 函    数: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;						//数据拼接,通过输出参数返回
}

6.2.4 MPU6050.h

#ifndef __MPU6050_H
#define __MPU6050_H

void MPU6050_WriteReg(uint8_t RegAddress, uint8_t Data);
uint8_t MPU6050_ReadReg(uint8_t RegAddress);

void MPU6050_Init(void);
uint8_t MPU6050_GetID(void);
void MPU6050_GetData(int16_t *AccX, int16_t *AccY, int16_t *AccZ, 
						int16_t *GyroX, int16_t *GyroY, int16_t *GyroZ);

#endif

6.2.5 MPU6050_Reg.h

#ifndef __MPU6050_REG_H
#define __MPU6050_REG_H

#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  //加速度寄存器X轴的高八位
#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 //电源管理寄存器1
#define	MPU6050_PWR_MGMT_2		0x6C //电源管理寄存器2
#define	MPU6050_WHO_AM_I		0x75

#endif

6.2.6 main.c

#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);
	}
}

6.3 现象

67c3642bbd854c7296a98537cf4e29e9.pngOLED第一行显示MPU6050的ID号 下面三行分别是陀螺仪和加速度计X、Y、Z轴的的变化数据

 

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值