STM32-I2C

news2024/9/21 14:52:32

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

文章目录

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

1. I2C通信

1.1 I2C通信简介

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

image.png

1.2 硬件电路

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

图1图2
image.png

  • 一主多从: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从低电平切换到高电平

image.png

  • 起始条件状态下:在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次,即可发送一个字节。

低电平主机放数据,高电平从机读数据
image.png
起始条件之后,第一个字节也必须是主机发送的。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)

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

1.3.4 发送应答和接收应答

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

image.png
就是在调用发送一个字节的时序之后,就要紧跟着调用接收应答的时序,用来判断从机有没有收到刚才给它的数据。如果从机收到了,那在应答位这里,主机释放SDA的时候,从机就应该立刻把SDA拉下来,然后在SCL高电平期间,主机读取应答位。如果应答位为0,就说明从机确实收到了。
在接收一个字节时候,需要调用发送应答。发送应答的目的是告诉从机,你是不是要继续发。如果从机发送一个数据后,得到了主机的应答,那从机就还会继续发送,如果从机没有得到主机的应答,那从机就会认为发送了一个数据,但主机不理我,可能主机不想要吧,这时从机就是乖乖地释放SDA,交出SDA的控制权,防止干扰主机之后的操作。

1.4 I2C时序

1.4.1 指定地址写

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

image.png
流程:
(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)

image.png
流程:
(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)

image.png
先起始、再重复起始、再停止
流程:
(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轴的角速度

image.png

  • 以飞机机身为例,欧拉角就是飞机机身相对于初始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 硬件电路

image.png

引脚功能
VCC、GND电源
SCL、SDAI2C通信引脚
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框图

image.png

  • 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 运行结果

IMG_20240406_155156.jpg

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代码:
#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代码:
#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

#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;
}
#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框图

image.png

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

4.3 I2C基本结构

image.png

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

4.4 主机发送

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

  • 7位地址:起始条件按后的一个字节是寻址
  • 10位地址:起始条件后的两个字节都是寻址,前一个字节是帧头,内容是5位的标志位11110+2位地址+1位读写位;后一个字节就是纯粹的8位地址。
  • 7位流程:起始、从机地址、应答、数据、应答、数据、应答 ··· 停止
  1. 初始化之后,总线默认空闲状态,STM默认是从模式,为了产生一个起始条件,STM32需要写入控制寄存器(CR1),写1,之后STM32由从模式转为主模式。

image.png

  1. EV5事件可以当作是标志位,SB是状态寄存器的一个位,表示了硬件的状态,SB=1,表示起始条件已发送。

image.png

  1. 然后就可以发送一个字节的从机地址了,从机地址需要写到数据寄存器DR中,写入DR之后,硬件电路就会自动将该地址字节转到移位寄存器里,再把该字节发送到I2C总线上,之后硬件会自动接收应答并判断,如果没有应答,硬件就会置应答失败的标志位,然后该标志位可以申请中断来提醒我们。
  2. 当寻址完成后,会发生EV6事件,ADDR标志位为1,该标志位在主模式下表示地址发送结束。

image.png

  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 主机接收

image.png
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 软件/硬件波形对比

image.png

image.png

5. 10-2 硬件I2C读写MPU6050

5.1 I2C库函数


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 运行结果

IMG_20240406_172128.jpg

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1904285.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

利用docker搭建漏洞环境,使用SSRF+Redis写入centos以及ubuntu的公钥,实现免密登录

一、实验环境 kali:在kali中搭建docker容器环境&#xff0c;这里我主要是使用第一个&#xff1b; redis作为一种数据库&#xff0c;它可以将数据写入内存中去&#xff0c;我们通过利用ssrf请求&#xff0c;实现服务器对自己的公钥写入&#xff0c;从而实验免密登录&#xff1b;…

el-table 树形数据与懒加载 二级数据不展示

返回的数据中 children和hasChildren只能有一个&#xff0c;不能同时存在&#xff0c;否则加载数据会失败

优化后Day53 动态规划part11

LC1143最长公共子序列 1.dp数组的含义&#xff1a;dp[i][j]表示以下标i结尾的text1子序列和以下标j结尾的text2子序列的最长公共子序列 2. 初始化&#xff1a;跟LC718一样&#xff0c;i结尾的需要初始化&#xff0c;i-1结尾不需要初始化 3. 递推公式 如果charAt(i)charAt(j)&…

集成测试技术栈

前端 浏览器操作&#xff1a;playwright、selenium 后端 testcontainercucumbervitestcypressmsw

Threejs将场景生成全景图导出

实现思路&#xff1a; 创建全景相机CubeCamera&#xff08;六个方位的透视相机&#xff09;并渲染场景 读取六个面的纹理数据 填充进canvas中 即可按照常规的canvas导出图片了 demo https://gitee.com/honbingitee/three-template-next.js/tree/HDR 核心代码 const cubeRender…

《python程序语言设计》2018版第5章第53题利用turtle绘制sin和cos函数 sin蓝色,cos红色和52题类似

直接上题和代码 5.53 &#xff08;Turtle&#xff1a;绘制sin和cos函数&#xff09;编写程序绘制蓝色的sin函数和红色的cos函数。 代码和结果 turtle.speed(10) turtle.penup() # sin 用蓝色 turtle.color("blue") #这道题和上道题一样&#xff0c;先把turtle放到起始…

数列结构(3.9)——队列应用

树的层次遍历 树的层次遍历&#xff0c;也称为树的广度优先遍历&#xff0c;是一种按照树的层次顺序&#xff0c;从上到下、从左到右遍历树中所有节点的算法。在二叉树中&#xff0c;这种遍历方式通常使用队列来实现。下面是层次遍历的基本步骤&#xff1a; 创建一个空队列&a…

排序(2)

我们在排序&#xff08;1&#xff09;中说到选择排序的代码&#xff1a; void SelectSort(int* a,int n) {int begin0,endn-1;int minibegin,maxbegin;for(int ibegin1;i<end;i){if(a[i]>a[max]){maxii;}if(a[i]<a[mini]){minii;}begin;--end;}Swap(&a[beign],&a…

UE5 05-利用 timeline 插值运动

理解成 unity Dotween DoMove 插值运动即可 AddTimeLine 节点 物体插值运动 物体插值缩放 一个timeline 可以K多个动画帧

C语言 printf 函数多种输出格式以及占位输出

一、输出格式 在C语言中&#xff0c;printf 函数提供了多种输出格式&#xff0c;用于控制不同类型数据的输出方式。 1.整数输出格式 %d&#xff1a;以十进制形式输出整数。 %o&#xff1a;以八进制形式输出整数&#xff08;无前导0&#xff09;。 %x 或 %X&#xff1a;以十六进…

包/final/权限修饰符/代码块

包package 1、包的作用 包用来管理不同的类。 2、包名 包名要全部小写&#xff0c;一般是域名反写&#xff0c;如com.liu。在Java中&#xff0c;java解释器会将package中的.解释为目录分隔符/&#xff0c;也就是说该文件的目录结构为&#xff1a;...com/liu/... 3、全类名…

Nginx 报错问题汇总

目录 一、nginx: [emerg] invalid number of arguments in "include" directive in C:\Program Files\nginx-1.15.4/conf/nginx.conf:61 总结&#xff1a; 二、nginx: [error] OpenEvent("Global\ngx_reload_2152") failed (5: Access is denied) 解决…

《昇思25天学习打卡营第14天|onereal》

第14天学习内容如下&#xff1a; Diffusion扩散模型 本文基于Hugging Face&#xff1a;The Annotated Diffusion Model一文翻译迁移而来&#xff0c;同时参考了由浅入深了解Diffusion Model一文。 本教程在Jupyter Notebook上成功运行。如您下载本文档为Python文件&#xff0c…

LabVIEW的JKI State Machine

JKI State Machine是一种广泛使用的LabVIEW架构&#xff0c;由JKI公司开发。这种状态机架构在LabVIEW中提供了灵活、可扩展和高效的编程模式&#xff0c;适用于各种复杂的应用场景。JKI State Machine通过状态的定义和切换&#xff0c;实现了程序逻辑的清晰组织和管理&#xff…

IDEA常用技巧荟萃:精通开发利器的艺术

1 概述 在现代软件开发的快节奏环境中,掌握一款高效且功能全面的集成开发环境(IDE)是提升个人和团队生产力的关键。IntelliJ IDEA,作为Java开发者的首选工具之一,不仅提供了丰富的编码辅助功能,还拥有高度可定制的界面和强大的插件生态系统。然而,要充分发挥其潜力,深…

[激光原理与应用-101]:南京科耐激光-激光焊接-焊中检测-智能制程监测系统IPM介绍 - 5 - 3C行业应用 - 电子布局类型

目录 前言&#xff1a; 一、激光在3C行业的应用概述 1.1 概述 1.2 激光焊接在3C-电子行业应用 二、3C电子行业中激光焊接 2.1 纽扣电池 2.2 均温板 2.3 指纹识别器 2.4 摄像头模组 2.5 IC芯片切割 三、3C行业中激光切割 四、激光在3C行业中的其他应用 4.1 涂层去除…

SpringBoot源码阅读(1)——环境搭建

SpringBoot官网 官网 https://spring.io/projects/spring-boot 代码仓库 github&#xff1a;https://github.com/spring-projects/spring-boot gitee: https://gitee.com/mirrors/spring-boot 下载代码 git clone https://gitee.com/mirrors/spring-boot.git下载的代码中有些…

STM32-OC输出比较和PWM

本内容基于江协科技STM32视频内容&#xff0c;整理而得。 文章目录 1. OC输出比较和PWM1.1 OC输出比较1.2 PWM&#xff08;脉冲宽度调制&#xff09;1.3 输出比较通道&#xff08;高级&#xff09;1.4 输出比较通道&#xff08;通用&#xff09;1.5 输出比较模式1.6 PWM基本结…

数据库逆向工程工具reverse_sql

reverse_sql 是一个用于解析和转换 MySQL 二进制日志&#xff08;binlog&#xff09;的工具。它可以将二进制日志文件中记录的数据库更改操作&#xff08;如插入、更新、删除&#xff09;转换为反向的 SQL 语句&#xff0c;以便对系统或人为产生的误操作进行数据回滚和恢复。 *…

LabVIEW的Actor Framework (AF) 结构介绍

LabVIEW的Actor Framework (AF) 是一种高级架构&#xff0c;用于开发并发、可扩展和模块化的应用程序。通过面向对象编程&#xff08;OOP&#xff09;和消息传递机制&#xff0c;AF结构实现了高效的任务管理和数据处理。其主要特点包括并发执行、动态可扩展性和强大的错误处理能…