STM32-I2C

本内容基于江协科技STM32视频学习之后整理而得。

文章目录

  • [1. I2C通信](#1. I2C通信)
    • [1.1 I2C通信简介](#1.1 I2C通信简介)
    • [1.2 硬件电路](#1.2 硬件电路)
    • [1.3 I2C时序基本单元](#1.3 I2C时序基本单元)
      • [1.3.1 起始条件和终止条件](#1.3.1 起始条件和终止条件)
      • [1.3.2 发送一个字节](#1.3.2 发送一个字节)
      • [1.3.3 接收一个字节](#1.3.3 接收一个字节)
      • [1.3.4 发送应答和接收应答](#1.3.4 发送应答和接收应答)
    • [1.4 I2C时序](#1.4 I2C时序)
      • [1.4.1 指定地址写](#1.4.1 指定地址写)
      • [1.4.2 当前地址读](#1.4.2 当前地址读)
      • [1.4.3 指定地址读](#1.4.3 指定地址读)
  • [2. MPU6050](#2. MPU6050)
    • [2.1 MPU6050简介](#2.1 MPU6050简介)
    • [2.2 MPU6050参数](#2.2 MPU6050参数)
    • [2.3 硬件电路](#2.3 硬件电路)
    • [2.4 MPU6050框图](#2.4 MPU6050框图)
  • [3. 10-1软件I2C读写MPU6050](#3. 10-1软件I2C读写MPU6050)
    • [3.1 硬件连接](#3.1 硬件连接)
    • [3.2 运行结果](#3.2 运行结果)
    • [3.3 代码流程](#3.3 代码流程)
    • [3.4 代码](#3.4 代码)
  • [4. I2C外设](#4. I2C外设)
    • [4.1 I2C外设简介](#4.1 I2C外设简介)
    • [4.2 I2C框图](#4.2 I2C框图)
    • [4.3 I2C基本结构](#4.3 I2C基本结构)
    • [4.4 主机发送](#4.4 主机发送)
    • [4.5 主机接收](#4.5 主机接收)
    • [4.6 软件/硬件波形对比](#4.6 软件/硬件波形对比)
  • [5. 10-2 硬件I2C读写MPU6050](#5. 10-2 硬件I2C读写MPU6050)
    • [5.1 I2C库函数](#5.1 I2C库函数)
    • [5.2 硬件I2C读写MPU6050实现](#5.2 硬件I2C读写MPU6050实现)
      • [5.2.1 硬件连接](#5.2.1 硬件连接)
      • [5.2.2 运行结果](#5.2.2 运行结果)
      • [5.2.3 代码实现流程](#5.2.3 代码实现流程)
      • [5.2.4 代码](#5.2.4 代码)

1. I2C通信

1.1 I2C通信简介

  • I2C(Inter IC Bus)是由Philips公司开发的一种通用数据总线
  • 两根通信线:SCL(Serial Clock)串行时钟线、SDA(Serial Data)串行数据线
  • 同步,半双工,单端,多设备
  • 带数据应答
  • 支持总线挂载多设备(一主多从、多主多从)
    • 一主多从:单片机作为主机,主导I2C总线的运行,挂载在I2C总线的所有外部模块都是从机,从机只有被主机点名后才能控制I2C总线,不能在未经允许的情况下去碰I2C总线,防止冲突。
    • 多主多从:在总线上任何一个模块都可以主动跳出来,当主机。当总线冲突时,I2C协议会进行仲裁,仲裁胜利的一方取得总线控制权,失败的一方自动变回从机。

1.2 硬件电路

  • 所有I2C设备的SCL连在一起,SDA连在一起
  • 设备的SCL和SDA均要配置成开漏输出模式
  • SCL和SDA各添加一个上拉电阻,阻值一般为4.7KΩ左右


  • 一主多从:CPU是单片机,作为总线的主机,包括对SCL线的完全控制,任何时候都是主机完全掌控SCL线。另外在空闲状态下,主机可以主动发起对SDA的控制,只有在从机发送数据和从机应答的时候,主机才会转交SDA的控制权给从机,这是主机的权力。
  • 被控IC是挂载在I2C总线上的从机,可以是姿态传感器、OLED、存储器、时钟模块等。从机的权力比较小,对于SCL时钟线,在任何时刻都只能被动的读取,从机不允许控制SCL线。对于SDA数据线,从机不允许主动发起对SDA的控制。只有在主机发送读取从机的命令后,或者从机应答的时候,从机才能短暂地取得SDA的控制权。
  • 图2:左边是SCL,右边是SDA。所有的数据进来都可以通过一个数据缓冲器或者是施密特触发器,进行输入。
    • 因为输入对电路没有任何影响,所以任何设备在任何时刻都是可以输入的。
    • 输出采用的是开漏输出的配置,输出低电平,开关管导通,引脚直接接地,是强下拉;输出高电平,开关管断开,引脚什么都不接,处于浮空状态,这样所有的设备都只能输出低电平而不能输出高电平,为了避免高电平造成的浮空,就需要在总线外面,SCL和SDA各外置一个上拉电阻,是通过一个电阻拉到高电平的,所以是一个弱上拉。这样第一,完全杜绝了电源短路现象,保证电路的安全;第二,避免了引脚模式的频繁切换。开漏模式下,输出高电平就相当于断开引脚,所以在输入之前,可以直接输出高电平。第三,该模式有一个"线与"现象,只要有任意一个或多个设备输出了低电平,总线就处于低电平,只有所有的设备都输出高电平,总线才处于高电平。因此,I2C可以利用该现象,执行多主机模式下的时钟同步和总线仲裁。所以这里SCL虽然在一主多从模式下可以用推挽输出,但仍然采用了开漏加上拉输出的模式,

1.3 I2C时序基本单元

1.3.1 起始条件和终止条件

  • 起始条件:SCL高电平期间,SDA从高电平切换到低电平
  • 终止条件:SCL高电平期间,SDA从低电平切换到高电平
  • 起始条件状态下:在I2C总线处于空闲状态时,SCL和SDA都处于高电平状态,也就是没有任何一个设备去碰SCL和SDA,SCL和SDA由外挂的上拉电阻拉高至高电平,总线处于平静的高电平状态。当主机需要进行数据收发时,首先就要打破总线的宁静,产生一个起始条件,就是SCL处于高电平不去动它,然后把SDA拽下来,产生一个下降沿。当从机捕获到SCL高电平、SDA下降沿信号时,就会进行自身的复位,等待主机的召唤。在SDA下降沿之后,主机要再把SCL拽下来,拽下SCL,一方面是占用这个总线,另一方面也是为了方便基本单元的拼接。就是之后会保证,除了起始和终止条件,每个时序单元的SCL都是以低电平开始,低电平结束。
  • 终止条件状态下 :SCL先放手,回弹到高电平,SDA再放手,回弹高电平,产生一个上升沿,这个上升沿触发终止条件。同时终止条件之后,SCL和SDA都是高电平,回归到最初的平静状态。
    起始和终止都是由主机产生的,从机不允许产生起始和终止。所以在总线空闲状态时,从机必须始终双手放开,不允许主动跳出来,去碰总线。

1.3.2 发送一个字节

  • 发送一个字节:SCL低电平期间,主机将数据位依次放到SDA线上(高位先行),然后释放SCL,从机将在SCL高电平期间读取数据位,所以SCL高电平期间SDA不允许有数据变化,依次循环上述过程8次,即可发送一个字节。

低电平主机放数据,高电平从机读数据

起始条件之后,第一个字节也必须是主机发送的。SCL低电平,主机想发送0,就拉低SDA到低电平;如果想发送1,就放手,SDA回弹到高电平。在SCL低电平期间,允许改变SDA的电平,当放好数据之后,主机就松手时钟线,SCL回弹到高电平。在高电平期间,是从机读取SDA的时候,所以在高电平期间,SDA不允许变化。SCL处于高电平之后,从机需要尽快地读取SDA,一般都是在SCL上升沿这个时刻,从机就已经读取完成了。因为时钟是主机控制的,从机并不知道什么时候产生下降沿,因此在SCL上升沿时,从机就会把数据读走。当主机在放手SCL一段时间后,就可以继续拉低SCL,传输下一位了。主机也需要在SCL下降沿之后尽快把数据放到SDA上。但主机有时钟的主导权,所以只需要在低电平的任意时刻把数据放在SDA上就可以了。数据放完之后,主机再松手SCL,SCL高电平,从机读取这一位。循环该流程:主机拉低SCL,把数据放到SDA上,主机松开SCL,从机读取SDA数据。在SCL的同步下,依次进行主机发送和从机接收,循环8次,就发送了8位数据,也就是一个字节。

由于是高位先行,所以第一位是一个字节的最高位B7,最后发送最低位B0,

1.3.3 接收一个字节

  • 接收一个字节:SCL低电平期间,从机将数据位依次放到SDA线上(高位先行),然后释放SCL,主机将在SCL高电平期间读取数据位,所以SCL高电平期间SDA不允许有数据变化,依次循环上述过程8次,即可接收一个字节(主机在接收之前,需要释放SDA)

低电平从机放数据,高电平主机读数据

SDA线:主机在接收之前要释放SDA,这时从机获得SDA的控制权,从机需要发送0,就把SDA拉低,从机需要发送1,就放手,SDA回弹高电平。低电平变换数据,高电平读取数据。实线表示主机控制的电平,虚线表示从机控制的电平。SCL全程由主机控制,SDA主机在接收前要释放,交由从机控制。因为SCL时钟是由主机控制的,所以从机的数据变换基本上都是贴着SCL下降沿进行的,而主机可以在SCL高电平的任意时刻读取。

1.3.4 发送应答和接收应答

  • 发送应答:主机在接收完一个字节之后,在下一个时钟发送一位数据,数据0表示应答,数据1表示非应答
  • 接收应答:主机在发送完一个字节之后,在下一个时钟接收一位数据,判断从机是否应答,数据0表示应答,数据1表示非应答(主机在接收之前,需要释放SDA)

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

在接收一个字节时候,需要调用发送应答。发送应答的目的是告诉从机,你是不是要继续发。如果从机发送一个数据后,得到了主机的应答,那从机就还会继续发送,如果从机没有得到主机的应答,那从机就会认为发送了一个数据,但主机不理我,可能主机不想要吧,这时从机就是乖乖地释放SDA,交出SDA的控制权,防止干扰主机之后的操作。

1.4 I2C时序

1.4.1 指定地址写

  • 指定地址写
  • 对于指定设备(Slave Address),在指定地址(Reg Address)(即指定设备的寄存器地址)下,写入指定数据(Data)


流程:

(1)起始条件

(2)发送一个字节时序---0xD0(从机地址(7bit) +写(1bit)-0)(1101 0000)

(3)接收应答:RA = 0(接收从机的应答)

(4)指定地址:0x19(0001 1001)

(5)接收应答:RA = 0(接收从机的应答)

(6)写入指定数据:0xAA(1010 1010)

(7)接收应答:RA = 0

(8)停止位P(终止条件)

  • 在起始条件之后,必须是发送一个字节的时序,字节的内容必须是从机地址+读写位,从机地址是7位,读写位是1位,正好是8位。发送从机地址就是确定通信的对象,发送读写位是确认接下来是要写入还是要读出。现在就是主机发送了一个数据,字节的内容转换为16进制,高位先行,就是0xD0,紧跟着的单元就是接收从机的应答位(RA),第8位读写位结束SCL拉低之后,主机要释放SDA,然后就是应答位RA。
  • 在应答位RA结束后的高电平是从机释放SDA产生的,从机交出了SDA的控制权,因为从机要在SCL低电平尽快交换数据,所以SDA的上升沿和SCL的下降沿几乎是同时发生的。
  • 在应答结束后,要继续发送一个字节,第二个字节就可以送到指定设备的内部了,从机设备可以自己定义第二个自己和后续字节的用途。一般第二个字节可以是寄存器地址或者是指令控制字等,第三个字节是主机想要写入到寄存器地址(第二个字节)下的内容。
  • P是停止位。

该数据帧的目的是:对于指定从机地址为1101000的设备,在其内部0x19地址的寄存器中,写入0xAA这个数据。

0表示:之后的时序主机要进行写入操作;

1表示:之后的时序主机要进行读出操作;

1.4.2 当前地址读

  • 当前地址读
  • 对于指定设备(Slave Address),在当前地址指针指示的地址下,读取从机数据(Data)


流程:

(1)起始条件

(2)发送一个字节时序---0xD1(从机地址(7bit) +读(1bit)-1)(1101 0001)

(3)接收应答:RA = 0(接收从机的应答)

(4)读从机数据:0x0F(0000 1111)

(7)发送应答:SA = 0

(8)停止位P(终止条件)

  • 读写位是1,表示接下来要进行读出的操作。在从机应答之后(RA=0),数据的传输方向就要反过来了。主机要把SDA的控制权交给从机,主机调用接收一个字节的时序,进行接收操作。
  • 在第二个字节中,从机就得到了主机的允许,可以在SCL低电平期间写入SCL,主机在SCL高电平期间读取SDA,最终,主机在SCL高电平期间依次读取8位,就接收到了从机发送的一个字节数据,即0x0F。但0x0F是从机哪个寄存器的数据呢。在读的时序中,I2C的协议规定是主机进行寻址时,一旦读写标志位给1了。下一个字节就要立马转为读的时序。所以主机还来不及指定想要读哪个寄存器,就要开始接收了,所以这里没有指定地址这个环节。在从机中,所有的寄存器被分配到了一个线性区域中,并且会有一个单独的指针变量,指示着其中一个寄存器,这个指针上电默认,一般指向0地址,并且每写入一个字节和读出一个字节后,这个指针就会自动自增一次,移动到下一个位置,那么在调用当前地址读的时序时,主机没有指定要读哪个地址,从机就会返回当前指针指向的寄存器的值。

1.4.3 指定地址读

  • 指定地址读
  • 对于指定设备(Slave Address),在指定地址(Reg Address)下,读取从机数据(Data)

先起始、再重复起始、再停止
流程:

(1)起始条件

(2)发送一个字节时序---0xD0(从机地址(7bit) +写(1bit)-0)(1101 0000)

(3)接收应答:RA = 0(接收从机的应答)

(4)指定地址:0x19(0001 1001)

(5)接收应答:RA = 0(接收从机的应答)

(6)重复起始条件

(7)发送一个字节时序---0xD1(从机地址(7bit) +读(1bit)-1)(1101 0001)

(8)接收应答:RA = 0

(9)读取从机数据:0xAA(1010 1010)

(10)发送应答:SA = 0

(11)停止位P(终止条件)

  • 前面部分是指定地址写,但是只指定了地址,还没来得及写;后面部分是当前地址读,因为刚指定了地址,所以再调用当前地址读。
  • 指定从机地址是1101000,读写标志位是0,进行写操作,经过从机应答后,再写入一个字节(第二个字节),用于指定地址,0x19就写入到了从机的地址指针里了,也就是说,从机接收到该数据后,它的寄存器指针就指向了0x19这个位置。
  • Sr是重复起始条件,相当于另起一个时序,因为指定读写标志位只能跟着起始条件的第一个字节,所以想切换读写方向,只能再来个起始条件。
  • 然后起始条件后,重新寻址并且指定读写标志位,此时读写标志位是1,表示要读,接着主机接收一个字节,该字节是0x19地址下的数据0xAA。

2. MPU6050

2.1 MPU6050简介

  • MPU6050是一个6轴姿态传感器,可以测量芯片自身X、Y、Z轴的加速度、角速度参数,通过数据融合,可进一步得到姿态角(欧拉角),常应用于平衡车、飞行器等需要检测自身姿态的场景
  • 3轴加速度计(Accelerometer):测量X、Y、Z轴的加速度
  • 3轴陀螺仪传感器(Gyroscope):测量X、Y、Z轴的角速度
  • 以飞机机身为例,欧拉角就是飞机机身相对于初始3个轴的夹角,
    • 飞机机头下倾或上仰 ,这个轴的夹角叫做俯仰,Pitch
    • 飞机机身左翻滚或右翻滚 ,这个轴的夹角叫做滚转,Roll
    • 飞机机身保持水平机头向左转向或向右转向 ,这个轴的夹角叫做偏航,Yaw
    • 欧拉角就是表示了飞机此时的姿态,是上仰了还是下倾了,向左倾斜还是向右倾斜。
  • 常见的数据融合算法,一般有互补滤波、卡尔曼滤波等,惯性导航里的姿态解算。
  • 加速度计:中间的虚线是感应轴线,中间是一个具有一定质量、可以左右滑动的小滑块,左右各有一个弹簧顶着它。当滑块移动时,就会带动它上面的电位计移动,这个电位计就是一个分压电阻,测量电位计输出的电压,就能得到小滑块所受的加速度值了。这个加速度计,实际上就是一个弹簧测力计,根据牛顿第二定律,F = ma,想测量这个加速度a,就可以找一个单位质量的物体,测量所受的力F,就行了。在X、Y、Z轴,分别都有一个加速度计。加速度计具有静态稳定性,不具有动态稳定性。
  • 陀螺仪传感器:中间是一个具有一定质量的旋转轮,当旋转轮高速旋转时,根据角动量守恒的原理,这个旋转轮具有保持它原有角动量的趋势,这个趋势可以保持旋转轴方向不变。当外部物体的方向转动时,内部的旋转轴方向并不会转动,这就会在平衡环连接处产生角度偏差。如果在连接处放一个旋转的电位器,测量电位器的电压,就能得到旋转的角度了。陀螺仪应该是可以直接得到角度的,但这个MPU6050的陀螺仪,并不能直接测量角度,它是测量角速度,即芯片绕X轴、Y轴和绕Z轴旋转的角速度。角速度积分就是角度,但是当物体静止时,角速度值会因为噪声无法完全归零,然后经过积分的不断累积,这个小噪声就会导致计算出来的角度产生缓慢的漂移,也就是角速度积分得到的角度经不起时间的考验,但这个角度无论是静止还是运动,都是没有问题的,不会受物体运动的影响。陀螺仪具有动态稳定性,不具有静态稳定性。
  • 根据加速度计具有静态稳定性,不具有动态稳定性;陀螺仪具有动态稳定性,不具有静态稳定性,这两种特性,所以取长补短,进行一下互补滤波,就能融合得到静态和动态都稳定的姿态角了。

2.2 MPU6050参数

  • 16位ADC采集传感器的模拟信号,量化范围:-32768~32767
  • 加速度计满量程选择:±2、±4、±8、±16(g)(1g = 9.8m/s2)
  • 陀螺仪满量程选择: ±250、±500、±1000、±2000(°/sec,度/秒,角速度单位,每秒旋转了多少度)(满量程选的越大,测量范围就越广,满量程选的越小,测量分辨率越高)
  • 可配置的数字低通滤波器:可以配置寄存器来选择对输出数据进行低通滤波。
  • 可配置的时钟源
  • 可配置的采样分频:时钟源通过分频器的分频,可以为AD转换和内部其他电路提供时钟。控制分频系数,就可以控制AD转换的快慢了。
  • I2C从机地址:1101000(AD0=0) 或 1101001(AD0=1)
    • 110 1000转换为十六进制,就是0x68,所以有的说MPU6050的从机地址是0x68。但在I2C通信里,第一个字节的高7位是从机地址,最低位是读写位,所以如果认为0x68是从机地址的话,在发送第一个字节时,要先把0x68左移1位(0x68 << 1),再按位或上读写位,读1写0。
    • 还有一种就是把0x68左移1位(0x68 << 1)后的数据,当作从机地址,就是0xD0,那这样,MPU6050的从机地址就是0xD0。这时,在实际发送第一个字节时,如果你要写,就直接把0xD0当作第一个字节;如果你要读,就把0xD0或上0x01(0xD0 | 0x01),即0xD1当作第一个字节。这种表示方式就不需要左移的操作了,或者说这种表示方式,是把读写位也融入到从机地址里了。0xD0是写地址,0xD1是读地址。

2.3 硬件电路

引脚 功能
VCC、GND 电源
SCL、SDA I2C通信引脚
XCL、XDA 主机I2C通信引脚
AD0 从机地址最低位
INT 中断信号输出
  • LDO:低压差线性稳压器,3.3V稳压。
  • SCL和SDA:是I2C通信的引脚,模块已经内置了两个4.7K的上拉电阻,所以接线的时候,直接把SDA和SCL接在GPIO口上就行了,不需要再外接上拉电阻了。
  • XCL、XDA:主机I2C通信引脚,设计这两个引脚是为了扩展芯片功能。通常用于外接磁力计或者气压计,当接上这些扩展芯片时,MPU6050的主机接口就可以直接访问这些扩展芯片的数据,把这些扩展芯片的数据读取到MPU6050里,MPU6050里有DMP单元,进行数据融合和姿态解算。
    AD0引脚:是从机地址的最低位,接低电平的话,7位从机地址是1101000;接高电平的话,7位从机地址就是1101001。电路图中有一个电阻,默认弱下拉到低电平了,所以引脚悬空的话,就是低电平,如果想接高电平,可以把AD0直接引到VCC,强上拉至高电平。
  • INT:中断输出引脚,可以配置芯片内部的一些事件,来触发中断引脚的输出,如数据准备好了、I2C主机错误等。
  • 芯片内部还内置了:自由落体检测、运动检测、零运动检测等。这些信号都可以触发INT引脚产生电平跳变,需要的话可以进行中断信号的配置。
  • MPU6050芯片的供电是2.375-3.46V,属于3.3V供电的设备,不能直接接5V。因此加了3.3V的稳压器,输入端电压VCC_5V可以在3.3V~5V之间,然后经过3.3V的稳压器输出稳定的3.3V电压,给芯片端供电,只要3.3V端有电,电源指示灯就会亮。

2.4 MPU6050框图

  • CLKIN和CLKOUT是时钟输入引脚和时钟输出引脚,但我们一般使用内部时钟。
  • 灰色部分:是芯片内部的传感器,XYZ轴的加速度计,XYZ轴的陀螺仪。
  • 还内置了一个温度传感器,可以用来测量温度。
  • 这些传感器本质上相当于可变电阻,通过分压后,输出模拟电压,然后通过ADC进行模数转换,转换完成之后,这些传感器的数据统一都放到数据寄存器中,读取数据寄存器就能得到传感器测量的值了。这个芯片内部的转换都是全自动进行的。
  • 每个传感器都有个自测单元,这部分是用来验证芯片好坏的,当启动自测后,芯片内部会模拟一个外力施加在传感器上,这个外力导致传感器数据会比平时大一些。自测流程:可以先使能自测,读取数据,再使能自测,读取数据,两个数据一相减,得到的数据叫自测响应。对于这个自测响应,手册里给了一个范围,如果在这个范围内,就说明芯片没问题。
  • Charge Pump:是电荷泵或者充电泵,电荷泵是一种升压电路。
  • CPOUT引脚需要外接一个电容。
  • 中断状态寄存器:可以控制内部的哪些事件到中断引脚的输出,
  • FIFO:先入先出寄存器,可以对数据流进行缓存,
  • 配置寄存器:可以对内部的各个电路进行配置
  • 传感器寄存器:即数据寄存器,存储了各个传感器的数据,
  • 工厂校准:意思是内部的传感器都进行了校准。
  • 数字运动处理器:简称DMP,是芯片内部自带的一个姿态解算的硬件算法,配合官方的DMP库,可以进行姿态解算。
  • FSYNC:帧同步。

3. 10-1软件I2C读写MPU6050

3.1 硬件连接

通过软件I2C通信,对MPU6050芯片内部的寄存器进行读写,写入到配置寄存器,就可以对外挂的这个模块进行配置,读出数据寄存器,就可以获取外挂模块的数据,读出的数据会显示在OLED上,最上面的数据是设备的ID号,这个MPU6050的ID号固定为0x68。下面的,左边3个是加速度传感器的输出数据,分别是X轴、Y轴、Z轴的加速度,右边3个是陀螺仪传感器的输出数据,分别是X轴、Y轴、Z轴的角速度。

SCL接到STM32的PB10引脚,SDA接到PB11引脚。这里由于是软件翻转电平实现,所以可以任意连接两个GPIO口即可。

3.2 运行结果

3.3 代码流程

STM32是主机,MPU6050是从机,是一主一从模式。

  1. 建立I2C通信层的.c和.h模块
    1. 写好I2C底层的GPIO初始化
    2. 6个时序基本单元:起始、终止、发送一个字节、接收一个字节、发送应答、接收应答
  2. 建立MPU6050的.c和.h模块
    1. 基于I2C通信的模块,实现指定地址读、指定地址写、再实现写寄存器对芯片进行配置、读寄存器得到传感器数据
  3. main.c
    1. 调用MPU6050模块,初始化,拿到数据,显示数据

3.4 代码

  1. I2C代码:
c 复制代码
#include "stm32f10x.h"                  // Device header
#include "Delay.h"

void MyI2C_W_SCL(uint8_t BitValue)
{
	GPIO_WriteBit(GPIOB, GPIO_Pin_10,(BitAction)BitValue);
	Delay_us(10);
}

void MyI2C_W_SDA(uint8_t BitValue)
{
	GPIO_WriteBit(GPIOB, GPIO_Pin_11,(BitAction)BitValue);
	Delay_us(10);
}

uint8_t MyI2C_R_SDA(void)
{
	uint8_t BitValue;
	BitValue = GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_11);
	Delay_us(10);
	return BitValue;
}

void MyI2C_Init(void)
{
/*
软件I2C初始化:
	1. 把SCL和SDA都初始化为开漏输出模式;
	2. 把SCL和SDA置高电平;
输入时,先输出1,再直接读取输入数据寄存器就行了;
初始化结束后,调用SetBits,把GPIOB的Pin_10和Pin_11都置高电平,
此时I2C总线处于空闲状态
*/	
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
	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);
	
	GPIO_SetBits(GPIOB, GPIO_Pin_10 | GPIO_Pin_11);
	
}

/*
起始条件:SCL高电平期间,SDA从高电平切换到低电平。
如果起始条件之前,SDA和SCL都已经是高电平了,那先释放哪一个是一样的效果。
但是这个Start还要兼容重复起始条件Sr,Sr最开始,SCL是低电平,SDA电平不敢确定,
所以为保险起见,在SCL低电平时,先确保释放SDA,再释放SCL。
这时SDA和SCL都是高电平,然后再拉低SDA、拉低SCL。
这样这个Start就可以兼容起始条件和重复起始条件了。
*/
void MyI2C_Start(void)
{
	MyI2C_W_SDA(1);
	MyI2C_W_SCL(1);
	
	MyI2C_W_SDA(0);
	MyI2C_W_SCL(0);
}

/*
终止条件:SCL高电平期间,SDA从低电平切换到高电平
如果Stop开始时,SCL和SDA都已经是低电平了,那就先释放SCL,再释放SDA。
但在这个时序单元开始时,SDA并不一定是低电平,所以为了确保之后释放
SDA能产生上升沿,要在时序单元开始时,先拉低SDA,然后再释放SCL、释放SDA。
*/
void MyI2C_Stop(void)// 终止条件
{
	MyI2C_W_SDA(0);
	MyI2C_W_SCL(1);
	MyI2C_W_SDA(1);
}

/*
发送一个字节:发送一个字节时序开始时,SCL是低电平。
除了终止条件SCL以高电平结束,所有的单元都会保证SCL以低电平结束。
SCL低电平变换数据;高电平保持数据稳定。由于是高位先行,所以变换数据的时候,
按照先放最高位,再放次高位,...,最后最低位的顺序,依次把每一个字节的每一位放在SDA线上,
每放完一位后,执行释放SCL,拉低SCL的操作,驱动时钟运转。
程序:趁SCL低电平,先把Byte的最高位放在SDA线上,
*/

void MyI2C_SendByte(uint8_t Byte) // 发送一个字节
{
	uint8_t i;
	for (i = 0; i < 8; i ++)
	{
		MyI2C_W_SDA(Byte & (0x80 >> i));// 右移i位
	    MyI2C_W_SCL(1);
	    MyI2C_W_SCL(0);
	}
}

/*
接收一个字节:时序开始时,SCL低电平,此时从机需要把数据放到SDA上,
为了防止主机干扰从机写入数据,主机需要先释放SDA,释放SDA相当于切换为输入模式,
那在SCL低电平时,从机会把数据放到SDA上,如果从机想发1,就释放SDA,想发0,就拉低SDA,
主机释放SCL,在SCL高电平期间,读取SDA,再拉低SCL,低电平期间,从机就会把下一位数据放到SDA上,重复8次,
主机就能读到一个字节了。
SCL低电平变换数据,高电平读取数据,实际上是一种读写分离的操作,低电平时间定义为写的时间,高电平时间定义为读的时间,

*/
uint8_t MyI2C_ReceiveByte(void) // 接收一个字节
{
	uint8_t i, Byte = 0x00;
	MyI2C_W_SDA(1);
	for (i = 0; i < 8; i ++)
	{
		MyI2C_W_SCL(1); // 主机读取数据
	    if (MyI2C_R_SDA() == 1) // 如果if成立,接收的这一位为1,
	    {
		    Byte |= (0x80 >> i);   // 最高位置1
	    }
        MyI2C_W_SCL(0);	
	}
	return Byte;
}
/*
问题:反复读取SDA,for循环中又没写过SDA,那SDA读出来应该始终是一个值啊?
回答:I2C是在进行通信,通信是有从机的,当主机不断驱动SCL时钟时,
从机就有义务去改变SDA的电平,所以主机每次循环读取SDA的时候,
这个读取到的数据是从机控制的,这个数据也正是从机想要给我们发送的数据,
所以这个时序叫做接收一个字节。
*/

void MyI2C_SendAck(uint8_t AckBit) // 发送应答
{
	// 函数进来,SCL低电平,主机把AckBit放到SDA上,
	MyI2C_W_SDA(AckBit);
	MyI2C_W_SCL(1);  // 从机读取应答
	MyI2C_W_SCL(0);  // 进入下一个时序单元
	
}

uint8_t MyI2C_ReceiveAck(void) // 接收应答
{
	// 函数进来,SCL低电平,主机释放SDA,防止从机干扰
	uint8_t AckBit;
	MyI2C_W_SDA(1);  // 主机释放SDA
	MyI2C_W_SCL(1);  // SCL高电平,主机读取应答位
	AckBit = MyI2C_R_SDA(); 
	MyI2C_W_SCL(0);	 // SCL低电平,进入下一个时序单元
	return AckBit;
}

/*问题:在程序里,主机先把SDA置1了,然后再读取SDA,
这应答位肯定是1啊,
回答:第一,I2C的引脚是开漏输出+弱上拉的配置,主机输出1,
并不是强制SDA为高电平,而是释放SDA,
第二,I2C是在通信,主机释放了SDA,从机是有义务在此时把SDA再拉低的,
所以,即使主机把SDA置1了,之后再读取SDA,读到的值也可能是0,
读到0,代表从机给了应答,读到1,代表从机没给应答,这就是接收应答的流程。


*/
  1. MPU6050代码:
c 复制代码
#ifndef __MPU6050_REG_H
#define __MPU6050_REG_H

// 宏定义: 寄存器的名称   对应的地址

#define	MPU6050_SMPLRT_DIV		0x19  // 采样率分频
#define	MPU6050_CONFIG			0x1A  // 配置外部帧同步(FSYNC)引脚采样和数字低通滤波器(DLPF)设置
#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  // 电源管理寄存器1
#define	MPU6050_PWR_MGMT_2		0x6C  // 电源管理寄存器2
#define	MPU6050_WHO_AM_I		0x75  // 用于验证设备身份

#endif
c 复制代码
#include "stm32f10x.h"                  // Device header
#include "MyI2C.h"
#include "MPU6050_Reg.h"

// 宏定义:从机地址
#define MPU6050_ADDRESS  0xD0

// 指定地址写
void MPU6050_WriteReg(uint8_t RegAddress, uint8_t Data)
{
	MyI2C_Start();
	MyI2C_SendByte(MPU6050_ADDRESS);// 发送从机地址后,接收应答
	MyI2C_ReceiveAck();// 寻址找到从机之后,继续发送下一个字节
	MyI2C_SendByte(RegAddress); // 指定寄存器地址,存在MPU6050的当前地址指针里,用于指定具体读写哪个寄存器
	MyI2C_ReceiveAck();
	MyI2C_SendByte(Data);// 指定写入指定寄存器地址下的数据
	MyI2C_ReceiveAck();
	MyI2C_Stop();
}

// 指定地址读
uint8_t MPU6050_ReadReg(uint8_t RegAddress)
{
	uint8_t Data;
	
	MyI2C_Start();
	MyI2C_SendByte(MPU6050_ADDRESS);
	MyI2C_ReceiveAck();
	MyI2C_SendByte(RegAddress); // 指定地址:就是设置了MPU6050的当前地址指针
	MyI2C_ReceiveAck();
	// 转入读的时序,重新指定读写位,就必须重新起始
	MyI2C_Start();// 重复起始条件
	MyI2C_SendByte(MPU6050_ADDRESS | 0x01);// 指定从机地址和读写位,0xD0是写地址,或上0x01变为0xD1,读写位为1,接下来要读从机的数据
	MyI2C_ReceiveAck(); // 接收应答后,总线控制权就正式交给从机了,从机开始发送一个字节
	Data = MyI2C_ReceiveByte();// 主机接收一个字节,该函数返回值就是接收到的数据
	// 主机接收一个字节后,要给从机发送一个应答
	MyI2C_SendAck(1);// 参数为0,就是给从机应答,参数给1,就是不给从机应答
	// 如果想继续读多个字节,就要给应答,从机收到应答之后,就会继续发送数据,如果不想继续读了,就不能给从机应答了。
	// 主机收回总线的控制权,防止之后进入从机以为你还想要,但你实际不想要的冲突状态,
	// 这里,只需要读取1个字节,所以就给1,不给从机应答,
	MyI2C_Stop();
	return Data;
}

void MPU6050_Init(void)
{
	MyI2C_Init();
	// 写入一些寄存器对MPU6050硬件电路进行初始化配置
	// 电源管理寄存器1:设备复位:0,不复位;睡眠模式:0,解除睡眠:循环模式:0,不循环;无关位i:0;温度传感器失能:0,不失能;最后三位选择时钟:000,选择内部时钟,001,选择x轴的陀螺仪时钟,
	MPU6050_WriteReg(MPU6050_PWR_MGMT_1,0x01);// 解除睡眠,选择陀螺仪时钟
	// 电源管理寄存器2:前两位,循环模式唤醒频率:00,不需要;后6位,每一个轴的待机位:全为0,不需要待机;
	MPU6050_WriteReg(MPU6050_PWR_MGMT_2,0x00); // 均不待机
	// 采样率分频:该8位决定了数据输出的快慢,值越小越快
	MPU6050_WriteReg(MPU6050_SMPLRT_DIV,0x09);// 采样分频:10分频
	// 配置寄存器:外部同步:全为0,不需要;数字低通滤波器:110,最平滑的滤波
	MPU6050_WriteReg(MPU6050_CONFIG,0x06);// 滤波参数给最大
	// 陀螺仪配置寄存器:前三位,自测使能:全为0,不自测;满量程选择:11,最大量程;后三位无关位:为0
	MPU6050_WriteReg(MPU6050_GYRO_CONFIG,0x18);// 陀螺仪和加速度计都选最大量程
	// 加速度计配置寄存器:前三位,自测使能:全为0,不自测;满量程选择:11,最大量程;后三位高通滤波器:用不到,为000
	MPU6050_WriteReg(MPU6050_ACCEL_CONFIG,0x18);
		
}

// 获取芯片的ID号
uint8_t MPU6050_GetID(void)
{
	return MPU6050_ReadReg(MPU6050_WHO_AM_I);
}

// 获取寄存器数据的函数,返回6个int16_t的数据,分别表示XYZ的加速度值和陀螺仪值
// 指针地址传递的方法,返回多值
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;
	// 加速度计X
	DataH = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_H);
	DataL = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_L);
	*AccX = (DataH << 8) | DataL; // 高8位左移8位,再或上低8位,得到加速度计X轴的16位数据
	// 加速度计Y
	DataH = MPU6050_ReadReg(MPU6050_ACCEL_YOUT_H);
	DataL = MPU6050_ReadReg(MPU6050_ACCEL_YOUT_L);
	*AccY = (DataH << 8) | DataL;
	// 加速度计Z
	DataH = MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_H);
	DataL = MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_L);
	*AccZ = (DataH << 8) | DataL;
	// 陀螺仪X
	DataH = MPU6050_ReadReg(MPU6050_GYRO_XOUT_H);
	DataL = MPU6050_ReadReg(MPU6050_GYRO_XOUT_L);
	*GyroX = (DataH << 8) | DataL;
	// 陀螺仪Y
	DataH = MPU6050_ReadReg(MPU6050_GYRO_YOUT_H);
	DataL = MPU6050_ReadReg(MPU6050_GYRO_YOUT_L);
	*GyroY = (DataH << 8) | DataL;
	// 陀螺仪Z
	DataH = MPU6050_ReadReg(MPU6050_GYRO_ZOUT_H);
	DataL = MPU6050_ReadReg(MPU6050_GYRO_ZOUT_L);
	*GyroZ = (DataH << 8) | DataL;
}
c 复制代码
#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "MyI2C.h"
#include "MPU6050.h"

uint8_t ID;
int16_t AX, AY, AZ, GX, GY, GZ;// 接收XYZ轴的加速度值和陀螺仪值



int main(void)
{
	OLED_Init();
//	MyI2C_Init();
	MPU6050_Init();
//	
	OLED_ShowString(1,1,"ID:");
	ID = MPU6050_GetID();
	OLED_ShowHexNum(1, 4, ID, 2);
	
//	// 指定地址写
//	MyI2C_Start(); // 产生起始条件,开始一次传输
//	// 主机首先发送一个字节,内容是从机地址+读写位,进行寻址
//	MyI2C_SendByte(0xD0);  // 1101 000 0,0代表即将进行写入操作
//	// 发送一个字节后,要接收一下应答位,看看从机有没有收到刚才的数据
//	uint8_t Ack = MyI2C_ReceiveAck();
//	// 接收应答之后,要继续发送一个字节,写入寄存器地址
//	MyI2C_Stop();
//	
//	OLED_ShowNum(1, 1, Ack, 3);
	
//	// 指定地址读
//	uint8_t ID = MPU6050_ReadReg(0X75);// 返回值是0x68
//	OLED_ShowHexNum(1, 1, ID, 2);
	
//	// 指定地址写,需要先解除睡眠模式,否则写入无效
//	// 睡眠模式是电源管理寄存器1的这一位SLEEP控制的,把该寄存器写入0x00,解除睡眠模式
//	// 该寄存器地址是0x6B
//	MPU6050_WriteReg(0x6B, 0x00);
//	// 采样率分频寄存器,地址是0x19,值的内容是采样分频
//	MPU6050_WriteReg(0x19, 0xAA);
//	
//	uint8_t ID = MPU6050_ReadReg(0X19);
//	OLED_ShowHexNum(1, 1, ID, 2);//显示0x19地址下的内容,应该是0xAA
	
	while(1)
	{
		MPU6050_GetData(&AX, &AY, &AZ, &GX, &GY, &GZ);
		OLED_ShowSignedNum(2, 1, AX, 5);
		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);
	}
}

4. I2C外设

4.1 I2C外设简介

  • STM32内部集成了硬件I2C收发电路,可以由硬件自动执行时钟生成、起始终止条件生成、应答位收发、数据收发等功能,减轻CPU的负担
  • 支持多主机模型
  • 支持7位/10位地址模式
  • 支持不同的通讯速度,标准速度(高达100 kHz),快速(高达400 kHz)
  • 支持DMA
  • 兼容SMBus协议
  • STM32F103C8T6 硬件I2C资源:I2C1、I2C2

4.2 I2C框图

  • 左边是通信引脚:SDA和SCL;SMBALERT是SMBus用的;
    一般外设引出来的引脚,一般是借用GPIO口的复用模式与外部世界相连的,(查表)
  • 上面是数据控制部分:SDA,数据收发的核心部分是数据寄存器DR(DATA REGISTER)和数据移位寄存器。当需要发送数据时,可以把一个字节数据写到数据寄存器DR,当移位寄存器没有数据移位时,数据寄存器的值就会进一步转到移位寄存器里。在移位的过程中,就可以直接把下一个数据放到数据寄存器里等着了。一旦前一个数据移位完成,下一个数据就可以无缝衔接,继续发送。当数据由数据寄存器转到移位寄存器时,就会置状态寄存器的TXE位为1,表示发送寄存器为空。
  • 接收:输入的数据一位一位地从引脚移入到移位寄存器里,当一个字节的数据收齐之后,数据就整体从移位寄存器转到数据寄存器,同时置标志位RXNE,表示接收寄存器非空,这时就可以把数据从数据寄存器读出来了。至于什么时候收、什么时候发,需要写入控制寄存器的对应位进行操作,对于起始条件、终止条件、应答位等通过数据控制完成。
  • 比较器和地址寄存器是从机模式使用的。
  • SCL :时钟控制是用来控制SCL线的。在时钟控制寄存器写对应的位,电路就会执行对应的功能。控制逻辑电路,写入控制寄存器可以对整个电路进行控制。读取状态寄存器可以得知电路的工作状态。
  • 在进行很多字节收发时,可以配合DMA来提高效率。

4.3 I2C基本结构

  • SDA:由于I2C是高位先行,所以这个移位寄存器是向左移位。在发送时,高位先移出去,然后次高位。一个SCL时钟移位一次,移位8次,就能把8个字节从高位到低位,依次放到SDA线上了。在接收时,数据通过GPIO口从右边依次移进来,最终移8次,一个字节就接收完成了。输出的数据通过GPIO口,输出到端口。输入数据通过GPIO口,输入到移位寄存器,
  • GPIO口需要配置成复用开漏输出的模式;复用就是GPIO口的状态是交由片上外设来控制的,开漏输出是I2C协议要求的端口配置。即使是开漏输出模式,GPIO口也是可以输入的。
  • SCL:时钟控制器通过GPIO去控制时钟线。

4.4 主机发送

当STM32想要执行指定地址写的时候,需要按照着发送器传送序列图进行。

  • 7位地址:起始条件按后的一个字节是寻址
  • 10位地址:起始条件后的两个字节都是寻址,前一个字节是帧头,内容是5位的标志位11110+2位地址+1位读写位;后一个字节就是纯粹的8位地址。
  • 7位流程:起始、从机地址、应答、数据、应答、数据、应答 ··· 停止
  1. 初始化之后,总线默认空闲状态,STM默认是从模式,为了产生一个起始条件,STM32需要写入控制寄存器(CR1),写1,之后STM32由从模式转为主模式。
  1. EV5事件可以当作是标志位,SB是状态寄存器的一个位,表示了硬件的状态,SB=1,表示起始条件已发送。
  1. 然后就可以发送一个字节的从机地址了,从机地址需要写到数据寄存器DR中,写入DR之后,硬件电路就会自动将该地址字节转到移位寄存器里,再把该字节发送到I2C总线上,之后硬件会自动接收应答并判断,如果没有应答,硬件就会置应答失败的标志位,然后该标志位可以申请中断来提醒我们。
  2. 当寻址完成后,会发生EV6事件,ADDR标志位为1,该标志位在主模式下表示地址发送结束。
  1. EV8_1事件就是TxE标志位为1,移位寄存器空,数据寄存器空,需要我们写入数据寄存器DR进行数据发送了,写入DR之后,由于移位寄存器为空,DR就会立刻转到移位寄存器进行发送。就会进行EV8事件,移位寄存器非空,数据寄存器空,就是移位寄存器正在发送数据的状态,所以流程这里,数据1的时序就产生了。在该时刻数据2就会被写入到数据寄存器里等着了,接收应答位之后,数据位就转入移位寄存器进行发送,此时的状态是移位寄存器非空,数据寄存器空,因此此时EV8事件就又发生了。
  2. 之后数据2正在发送,但此次下一个数据就已经被写到数据寄存器等着了。一旦检测到EV8事件,就可以写入下一个数据了。
  3. 当想要发送的数据写完之后,这时就没有新的数据写入到数据寄存器里了,当移位寄存器当前的数据移位完成时,此时就是移位寄存器空,数据寄存器也空的状态,即EV8_2事件,TxE=1是移位寄存器空、数据寄存器空,BTF:字节发送结束标志位,在发送时,当一个新数据将被发送且数据寄存器还未被写入新的数据。当检测到EV8_2时,就可以产生终止条件Stop了。产生终止条件,显然,应该在控制寄存器里有相应的位可以控制。这样一个发送的时序就结束了。

4.5 主机接收

7位主接收:起始、从机地址+读、接收应答、接收数据、发送应答 ··· 接收数据、非应答、终止

  1. 首先,写入控制寄存器的Start位,产生起始条件,然后等待EV5事件(表示起始条件已发送)。
  2. 之后寻址,接收应答,结束后产生EV6事件(表示寻址已完成)。
  3. 数据1表示数据正在通过移位寄存器进行输入。
  4. EV6_1表明数据还在进行移位,在接收应答之后,说明移位寄存器已经成功移入一个字节的数据1了,这时移入的一个字节就整体转移到数据寄存器,同时置RxNE标志位,表示数据寄存器非空,也就是收到了一个字节的数据,该状态是EV7事件,RxNE=1,读DR寄存器清除该事件,也就说收到数据了,当我们把数据读走之后,该事件就没有了。
  5. 当然数据1还没被读走时,数据2就可以直接移入移位寄存器了,之后,数据2移位完成,收到数据2,产生EV7事件,读走数据2,EV7事件没有了。
  6. 当不需要再接收时,需要在最后一个时序单元发生时,提前把应答位控制寄存器ACK置0,并且设置终止条件请求,即EV7_1事件,之后就会给出非应答NA,由于设置STOP位,所以产生终止条件。

4.6 软件/硬件波形对比

5. 10-2 硬件I2C读写MPU6050

5.1 I2C库函数

c 复制代码
void I2C_DeInit(I2C_TypeDef* I2Cx);
void I2C_Init(I2C_TypeDef* I2Cx, I2C_InitTypeDef* I2C_InitStruct);
void I2C_StructInit(I2C_InitTypeDef* I2C_InitStruct);
void I2C_Cmd(I2C_TypeDef* I2Cx, FunctionalState NewState);

// 生成起始条件、终止条件
void I2C_GenerateSTART(I2C_TypeDef* I2Cx, FunctionalState NewState);
void I2C_GenerateSTOP(I2C_TypeDef* I2Cx, FunctionalState NewState);

// 配置CR1的ACK这一位,0:无应答,1:应答
void I2C_AcknowledgeConfig(I2C_TypeDef* I2Cx, FunctionalState NewState);

// 发送数据,把Data数据直接写入到DR寄存器
void I2C_SendData(I2C_TypeDef* I2Cx, uint8_t Data);
// 读取DR,接收数据
uint8_t I2C_ReceiveData(I2C_TypeDef* I2Cx);

// Address参数也是通过DR发送的,但在发送之前,设置了Address最低位的读写位,
// I2C_Direction不是发送,是把Address的最低位置1(读),否则最低位清0(写)
void I2C_Send7bitAddress(I2C_TypeDef* I2Cx, uint8_t Address, uint8_t I2C_Direction);

5.2 硬件I2C读写MPU6050实现

5.2.1 硬件连接

SCL接到STM32的PB10引脚,SDA接到PB11引脚。这里由于是软件翻转电平实现,所以可以任意连接两个GPIO口即可。

OLED最上面的数据是设备的ID号,这个MPU6050的ID号固定为0x68。下面的,左边3个是加速度传感器的输出数据,分别是X轴、Y轴、Z轴的加速度,右边3个是陀螺仪传感器的输出数据,分别是X轴、Y轴、Z轴的角速度。

5.2.2 运行结果

5.2.3 代码实现流程

  1. 配置I2C外设,对I2C外设进行初始化,替换MyI2C_Init
    (1)开启I2C外设和对应GPIO口的时钟,
    (2)把I2C外设对应的GPIO口初始化为复用开漏模式
    (3)使用结构体,对整个I2C进行配置
    (4)I2C_Cmd,使能I2C
  2. 控制外设电路,实现指定地址写的时序,替换WriteReg
  3. 控制外设电路,实现指定地址读的时序,替换ReadReg

5.2.4 代码

  1. MPU6050代码:
c 复制代码
#include "stm32f10x.h"                  // Device header
#include "MPU6050_Reg.h"

/*
1. 配置I2C外设,对I2C外设进行初始化,替换MyI2C_Init
   (1)开启I2C外设和对应GPIO口的时钟,
   (2)把I2C外设对应的GPIO口初始化为复用开漏模式
   (3)使用结构体,对整个I2C进行配置
   (4)I2C_Cmd,使能I2C
2. 控制外设电路,实现指定地址写的时序,替换WriteReg
3. 控制外设电路,实现指定地址读的时序,替换ReadReg
*/


// 宏定义:从机地址
#define MPU6050_ADDRESS  0xD0
 
// 超时退出
void MPU6050_WaitEvent(I2C_TypeDef* I2Cx, uint32_t I2C_EVENT)
{
	uint32_t TimeOut;
	TimeOut = 10000;
	while (I2C_CheckEvent(I2Cx, I2C_EVENT) != SUCCESS) 
	{   
		TimeOut --;
		if (TimeOut == 0)
		{
			break;// 跳出循环,直接执行后面的程序
		}
	}
}

// 指定地址写:发送器传送时序
void MPU6050_WriteReg(uint8_t RegAddress, uint8_t Data)
{
//	MyI2C_Start();
//	MyI2C_SendByte(MPU6050_ADDRESS);// 发送从机地址后,接收应答
//	MyI2C_ReceiveAck();// 寻址找到从机之后,继续发送下一个字节
//	MyI2C_SendByte(RegAddress); // 指定寄存器地址,存在MPU6050的当前地址指针里,用于指定具体读写哪个寄存器
//	MyI2C_ReceiveAck();
//	MyI2C_SendByte(Data);// 指定写入指定寄存器地址下的数据
//	MyI2C_ReceiveAck();
//	MyI2C_Stop();
	
	I2C_GenerateSTART(I2C2, ENABLE); // 起始条件
	MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT); //EV5事件
	
	// 发送从机地址,接收应答。该函数自带了接收应答,如果应答错误,硬件会通过标志位和中断来提示我们
	I2C_Send7bitAddress(I2C2, MPU6050_ADDRESS, I2C_Direction_Transmitter);
	
	MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED); //EV6事件
	
	// 直接写入DR,发送数据
	I2C_SendData(I2C2, RegAddress);
	// 写入了DR,DR立刻转移到移位寄存器进行发送,EV8事件出现的非常快,基本不用等。因为有两级缓存,
	// 第一个数据写进DR了,会立刻跑到移位寄存器,这时不用等第一个数据发完,第二个数据就可以写进去等着了。
	MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTING); //EV8事件
	
	I2C_SendData(I2C2, Data);
	MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTED); //EV8_2事件
	
	I2C_GenerateSTOP(I2C2, ENABLE);
}

// 指定地址读:接收器传送序列
uint8_t MPU6050_ReadReg(uint8_t RegAddress)
{
	uint8_t Data;
	
//	MyI2C_Start();
//	MyI2C_SendByte(MPU6050_ADDRESS);
//	MyI2C_ReceiveAck();
//	MyI2C_SendByte(RegAddress); // 指定地址:就是设置了MPU6050的当前地址指针
//	MyI2C_ReceiveAck();
//	// 转入读的时序,重新指定读写位,就必须重新起始
//	MyI2C_Start();// 重复起始条件
//	MyI2C_SendByte(MPU6050_ADDRESS | 0x01);// 指定从机地址和读写位,0xD0是写地址,或上0x01变为0xD1,读写位为1,接下来要读从机的数据
//	MyI2C_ReceiveAck(); // 接收应答后,总线控制权就正式交给从机了,从机开始发送一个字节
//	Data = MyI2C_ReceiveByte();// 主机接收一个字节,该函数返回值就是接收到的数据
//	// 主机接收一个字节后,要给发送从机一个应答
//	MyI2C_SendAck(1);// 参数为0,就是给从机应答,参数给1,就是不给从机应答
//	MyI2C_Stop();
	
	
	I2C_GenerateSTART(I2C2, ENABLE); // 起始条件
	MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT); //EV5事件
	
	// 发送从机地址,接收应答。该函数自带了接收应答,如果应答错误,硬件会通过标志位和中断来提示我们
	I2C_Send7bitAddress(I2C2, MPU6050_ADDRESS, I2C_Direction_Transmitter);
	
	MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED); //EV6事件
	
	// 直接写入DR,发送数据
	I2C_SendData(I2C2, RegAddress);
	// 写入了DR,DR立刻转移到移位寄存器进行发送,EV8事件出现的非常快,基本不用等。因为有两级缓存,
	// 第一个数据写进DR了,会立刻跑到移位寄存器,这时不用等第一个数据发完,第二个数据就可以写进去等着了。
	MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTED); //EV8_2事件
	
	I2C_GenerateSTART(I2C2, ENABLE);// 重复起始条件
	
	// 主机接收
	MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT); //EV5事件
	// 接收地址
	I2C_Send7bitAddress(I2C2, MPU6050_ADDRESS, I2C_Direction_Receiver); // 函数内部就自动将该地址MPU6050_ADDRESS的最低位置1
	
	MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED); //EV6事件
	
	// 在最后一个数据之前就要把应答位ACK置0,同时把停止条件生成位STOP置1
	I2C_AcknowledgeConfig(I2C2, DISABLE);
	I2C_GenerateSTOP(I2C2, ENABLE);
	
	MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_RECEIVED); //EV7事件
	// 等EV7事件产生后,一个字节的数据就已经在DR里面了。
	// 读取DR就可拿出该字节
	Data = I2C_ReceiveData(I2C2); // 返回值就是DR的数据
	// 在接收函数的最后,要恢复默认的ACK = 1。
	// 默认状态下ACK就是1,给从机应答,在收最后一个字节之前,临时把ACK置0,给非应答,
	// 所以在接收函数的最后,要恢复默认的ACK = 1,这个流程是为了方便指定地址收多个字节。
	I2C_AcknowledgeConfig(I2C2, ENABLE);
	
	return Data;
}

void MPU6050_Init(void)
{
	
//	MyI2C_Init();
	
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C2, ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
	
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD; // 复用开漏
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10 | GPIO_Pin_11;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOB, &GPIO_InitStructure);
	
	I2C_InitTypeDef I2C_InitStructure;
	I2C_InitStructure.I2C_Mode = I2C_Mode_I2C; // 模式
	I2C_InitStructure.I2C_ClockSpeed = 50000; // 时钟速度,最大400kHz的时钟频率
	I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2;	// 时钟占空比,只有在时钟频率大于100kHz,也就是进入到快速状态时才有用,小于100kHz,占空比是固定的1:1,
	I2C_InitStructure.I2C_Ack = I2C_Ack_Enable; //
	I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit; // STM32作为从机,可以响应几位的地址
	I2C_InitStructure.I2C_OwnAddress1 = 0x00; // 自身地址1,也是作为从机使用,
	I2C_Init(I2C2, &I2C_InitStructure); 
	
	I2C_Cmd(I2C2,ENABLE);
	
	// 写入一些寄存器对MPU6050硬件电路进行初始化配置
	// 电源管理寄存器1:设备复位:0,不复位;睡眠模式:0,解除睡眠:循环模式:0,不循环;无关位i:0;温度传感器失能:0,不失能;最后三位选择时钟:000,选择内部时钟,001,选择x轴的陀螺仪时钟,
	MPU6050_WriteReg(MPU6050_PWR_MGMT_1,0x01);// 解除睡眠,选择陀螺仪时钟
	// 电源管理寄存器2:前两位,循环模式唤醒频率:00,不需要;后6位,每一个轴的待机位:全为0,不需要待机;
	MPU6050_WriteReg(MPU6050_PWR_MGMT_2,0x00); // 均不待机
	// 采样率分频:该8位决定了数据输出的快慢,值越小越快
	MPU6050_WriteReg(MPU6050_SMPLRT_DIV,0x09);// 采样分频:10分频
	// 配置寄存器:外部同步:全为0,不需要;数字低通滤波器:110,最平滑的滤波
	MPU6050_WriteReg(MPU6050_CONFIG,0x06);// 滤波参数给最大
	// 陀螺仪配置寄存器:前三位,自测使能:全为0,不自测;满量程选择:11,最大量程;后三位无关位:为0
	MPU6050_WriteReg(MPU6050_GYRO_CONFIG,0x18);// 陀螺仪和加速度计都选最大量程
	// 加速度计配置寄存器:前三位,自测使能:全为0,不自测;满量程选择:11,最大量程;后三位高通滤波器:用不到,为000
	MPU6050_WriteReg(MPU6050_ACCEL_CONFIG,0x18);
		
}

// 获取芯片的ID号
uint8_t MPU6050_GetID(void)
{
	return MPU6050_ReadReg(MPU6050_WHO_AM_I);
}

// 获取寄存器数据的函数,返回6个int16_t的数据,分别表示XYZ的加速度值和陀螺仪值
// 指针地址传递的方法,返回多值
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;
	// 加速度计X
	DataH = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_H);
	DataL = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_L);
	*AccX = (DataH << 8) | DataL; // 高8位左移8位,再或上低8位,得到加速度计X轴的16位数据
	// 加速度计Y
	DataH = MPU6050_ReadReg(MPU6050_ACCEL_YOUT_H);
	DataL = MPU6050_ReadReg(MPU6050_ACCEL_YOUT_L);
	*AccY = (DataH << 8) | DataL;
	// 加速度计Z
	DataH = MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_H);
	DataL = MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_L);
	*AccZ = (DataH << 8) | DataL;
	// 陀螺仪X
	DataH = MPU6050_ReadReg(MPU6050_GYRO_XOUT_H);
	DataL = MPU6050_ReadReg(MPU6050_GYRO_XOUT_L);
	*GyroX = (DataH << 8) | DataL;
	// 陀螺仪Y
	DataH = MPU6050_ReadReg(MPU6050_GYRO_YOUT_H);
	DataL = MPU6050_ReadReg(MPU6050_GYRO_YOUT_L);
	*GyroY = (DataH << 8) | DataL;
	// 陀螺仪Z
	DataH = MPU6050_ReadReg(MPU6050_GYRO_ZOUT_H);
	DataL = MPU6050_ReadReg(MPU6050_GYRO_ZOUT_L);
	*GyroZ = (DataH << 8) | DataL;
}
  1. main.c
c 复制代码
#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "MPU6050.h"

uint8_t ID;
int16_t AX, AY, AZ, GX, GY, GZ;// 接收XYZ轴的加速度值和陀螺仪值



int main(void)
{
	OLED_Init();
	MPU6050_Init();
	
	OLED_ShowString(1,1,"ID:");
	ID = MPU6050_GetID();
	OLED_ShowHexNum(1, 4, ID, 2);
	
//	// 指定地址写
//	MyI2C_Start(); // 产生起始条件,开始一次传输
//	// 主机首先发送一个字节,内容时从机地址+读写位,进行寻址
//	MyI2C_SendByte(0xD0);  // 1101 000 0,0代表即将进行写入操作
//	// 发送一个字节后,要接收一下应答位,看看从机有没有收到刚才的数据
//	uint8_t Ack = MyI2C_ReceiveAck();
//	// 接收应答之后,要继续发送一个字节,写入寄存器地址
//	MyI2C_Stop();
//	
//	OLED_ShowNum(1, 1, Ack, 3);
	
//	// 指定地址读
//	uint8_t ID = MPU6050_ReadReg(0X75);// 返回值是0x68
//	OLED_ShowHexNum(1, 1, ID, 2);
	
//	// 指定地址写,需要先解除睡眠模式,否则写入无效
//	// 睡眠模式是电源管理寄存器1的这一位SLEEP控制的,把该寄存器写入0x00,解除睡眠模式
//	// 该寄存器地址是0x6B
//	MPU6050_WriteReg(0x6B, 0x00);
//	// 采样率分频寄存器,地址是0x19,值的内容是采样分频
//	MPU6050_WriteReg(0x19, 0xAA);
//	
//	uint8_t ID = MPU6050_ReadReg(0X19);
//	OLED_ShowHexNum(1, 1, ID, 2);//显示0x19地址下的内容,应该是0xAA
	
	
	
	while(1)
	{
		MPU6050_GetData(&AX, &AY, &AZ, &GX, &GY, &GZ);
		OLED_ShowSignedNum(2, 1, AX, 5);
		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);
	}
}
相关推荐
美式小田3 小时前
单片机学习笔记 9. 8×8LED点阵屏
笔记·单片机·嵌入式硬件·学习
兰_博3 小时前
51单片机-独立按键与数码管联动
单片机·嵌入式硬件·51单片机
时光の尘3 小时前
C语言菜鸟入门·关键字·float以及double的用法
运维·服务器·c语言·开发语言·stm32·单片机·c
嵌入式大圣5 小时前
单片机结合OpenCV
单片机·嵌入式硬件·opencv
日晨难再6 小时前
嵌入式:STM32的启动(Startup)文件解析
stm32·单片机·嵌入式硬件
yufengxinpian7 小时前
集成了高性能ARM Cortex-M0+处理器的一款SimpleLink 2.4 GHz无线模块-RF-BM-2340B1
单片机·嵌入式硬件·音视频·智能硬件
__基本操作__8 小时前
历遍单片机下的IIC设备[ESP--0]
单片机·嵌入式硬件
网易独家音乐人Mike Zhou14 小时前
【卡尔曼滤波】数据预测Prediction观测器的理论推导及应用 C语言、Python实现(Kalman Filter)
c语言·python·单片机·物联网·算法·嵌入式·iot
zy张起灵14 小时前
48v72v-100v转12v 10A大功率转换电源方案CSM3100SK
经验分享·嵌入式硬件·硬件工程
PegasusYu17 小时前
STM32CUBEIDE FreeRTOS操作教程(九):eventgroup事件标志组
stm32·教程·rtos·stm32cubeide·free-rtos·eventgroup·时间标志组