STM32-I2C

本内容基于江协科技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);  // 主机释放SDAMyI2C_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,最大量程;后三位无关位:为0MPU6050_WriteReg(MPU6050_GYRO_CONFIG,0x18);// 陀螺仪和加速度计都选最大量程// 加速度计配置寄存器:前三位,自测使能:全为0,不自测;满量程选择:11,最大量程;后三位高通滤波器:用不到,为000MPU6050_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;// 加速度计XDataH = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_H);DataL = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_L);*AccX = (DataH << 8) | DataL; // 高8位左移8位,再或上低8位,得到加速度计X轴的16位数据// 加速度计YDataH = MPU6050_ReadReg(MPU6050_ACCEL_YOUT_H);DataL = MPU6050_ReadReg(MPU6050_ACCEL_YOUT_L);*AccY = (DataH << 8) | DataL;// 加速度计ZDataH = MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_H);DataL = MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_L);*AccZ = (DataH << 8) | DataL;// 陀螺仪XDataH = MPU6050_ReadReg(MPU6050_GYRO_XOUT_H);DataL = MPU6050_ReadReg(MPU6050_GYRO_XOUT_L);*GyroX = (DataH << 8) | DataL;// 陀螺仪YDataH = MPU6050_ReadReg(MPU6050_GYRO_YOUT_H);DataL = MPU6050_ReadReg(MPU6050_GYRO_YOUT_L);*GyroY = (DataH << 8) | DataL;// 陀螺仪ZDataH = 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地址下的内容,应该是0xAAwhile(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的最低位置1MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED); //EV6事件// 在最后一个数据之前就要把应答位ACK置0,同时把停止条件生成位STOP置1I2C_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,最大量程;后三位无关位:为0MPU6050_WriteReg(MPU6050_GYRO_CONFIG,0x18);// 陀螺仪和加速度计都选最大量程// 加速度计配置寄存器:前三位,自测使能:全为0,不自测;满量程选择:11,最大量程;后三位高通滤波器:用不到,为000MPU6050_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;// 加速度计XDataH = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_H);DataL = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_L);*AccX = (DataH << 8) | DataL; // 高8位左移8位,再或上低8位,得到加速度计X轴的16位数据// 加速度计YDataH = MPU6050_ReadReg(MPU6050_ACCEL_YOUT_H);DataL = MPU6050_ReadReg(MPU6050_ACCEL_YOUT_L);*AccY = (DataH << 8) | DataL;// 加速度计ZDataH = MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_H);DataL = MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_L);*AccZ = (DataH << 8) | DataL;// 陀螺仪XDataH = MPU6050_ReadReg(MPU6050_GYRO_XOUT_H);DataL = MPU6050_ReadReg(MPU6050_GYRO_XOUT_L);*GyroX = (DataH << 8) | DataL;// 陀螺仪YDataH = MPU6050_ReadReg(MPU6050_GYRO_YOUT_H);DataL = MPU6050_ReadReg(MPU6050_GYRO_YOUT_L);*GyroY = (DataH << 8) | DataL;// 陀螺仪ZDataH = 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地址下的内容,应该是0xAAwhile(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.mzph.cn/news/867884.shtml

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

相关文章

Python实战训练(方程与拟合曲线)

1.方程 求e^x-派&#xff08;3.14&#xff09;的解 用二分法来求解&#xff0c;先简单算出解所在的区间&#xff0c;然后用迭代法求逼近解&#xff0c;一般不能得到精准的解&#xff0c;所以设置一个能满足自己进度的标准来判断解是否满足 这里打印出解x0是因为在递归过程中…

什么是PPG(光电容积描记)传感器?

PPG&#xff08;光电容积描记&#xff09;传感器是一种用于测量血液容量变化的设备。PPG传感器利用光学技术&#xff0c;通过检测皮肤下血液的反射光量变化来获取心率、血氧饱和度和其他生理参数。以下是PPG传感器的工作原理和应用&#xff1a;

python语句性能分析

1、for语句性能优于while import timeif __name__ __main__:start_time time.time()for i in range(10 ** 8):passend_time time.time()run_time end_time - start_timeprint(run_time)i 0start_time time.time()while i < 10 ** 8:i 1end_time time.time()run_tim…

强化学习的数学原理:时序差分算法

概述 之前第五次课时学习的 蒙特卡洛 的方法是全课程当中第一次介绍的第一种 model-free 的方法&#xff0c;而本次课的 Temporal-Difference Learning 简称 TD learning &#xff08;时序差分算法&#xff09;就是第二种 model-free 的方法。而对于 蒙特卡洛方法其是一种 non…

IntelliJ IDEA 同时多行同时编辑操作快捷键

首先 点击要编辑的地方,长按鼠标左键不放,同时按住 Ctrl Shift Alt,然后就可以进行多行编辑了

Android项目中,查看项目依赖树的多种方式

1.使用预设的Task来进行查看 1.1 命令行 查看某个模块的所有依赖树&#xff1a; gradlew [模块名称]:dependencies 例如&#xff1a;gradlew app:dependencies查看某个模块的某功能的依赖树&#xff1a; gradlew [模块名称]:dependencies --configuration [功能名称] 例如&…

k8s学习之cobra命令库学习

1.前言 打开k8s代码的时候&#xff0c;我发现基本上那几个核心服务都是使用cobra库作为命令行处理的能力。因此&#xff0c;为了对代码之后的代码学习的有比较深入的理解&#xff0c;因此先基于这个库写个demo&#xff0c;加深对这个库的一些理解吧 2.cobra库的基本简介 Git…

前端JS特效第22波:jQuery滑动手风琴内容切换特效

jQuery滑动手风琴内容切换特效&#xff0c;先来看看效果&#xff1a; 部分核心的代码如下&#xff1a; <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xm…

linux RTC时钟时间出现了明显的偏移

RTC时钟时间出现了明显的偏移 1、开发环境2、问题阐述3、验证问题3.1、首先去排查了硬件电路和芯片电压不稳定的问题。3.2、晶振的问题。3.3、芯片本身3.4、芯片寄存器 4、代码修改 1、开发环境 平台&#xff1a;imx6ul kernel版本&#xff1a;linux4.1.5 RTC芯片&#xff1a;…

机械键盘有哪些分类

机械键盘是一种比传统的薄膜键盘更耐用、更快捷、更具有手感的键盘。它的键帽和按键是独立的&#xff0c;能够提供更好的反应速度和操作感。机械键盘在现代化生活中得到了广泛的应用。根据其特性和使用场景&#xff0c;机械键盘可以分为以下几类&#xff1a; 1.轴体分类 机械…

设计模式探索:建造者模式

1. 什么是建造者模式 建造者模式 (Builder Pattern)&#xff0c;也被称为生成器模式&#xff0c;是一种创建型设计模式。 定义&#xff1a;将一个复杂对象的构建与表示分离&#xff0c;使得同样的构建过程可以创建不同的表示。 建造者模式要解决的问题&#xff1a; 建造者模…

誉天教育7月开班计划:为梦想插上腾飞的翅膀!

随着夏日的脚步渐近&#xff0c;誉天教育也迎来了新一轮的学习热潮。在这个充满活力和希望的季节里&#xff0c;我们精心策划了7月的开班计划&#xff0c;旨在为广大学子提供一个优质、高效的学习平台&#xff0c;助力他们追逐梦想&#xff0c;实现自我价值。 本月 Linux云计算…

探讨3D沉浸式在线会议系统的研发 - Meta演示的元宇宙虚拟化身多人对话场景,Web端现在也可以实现了 !

要实现一个元宇宙多人会议系统&#xff0c;关键技术有&#xff1a; 1. 3D虚拟空间的构建&#xff08;含光影特效、虚拟现实和增强现实&#xff09; 2. 3D虚拟化身的构建&#xff08;含动画、表情、语音&#xff09; 3. 多人角色管理 4. 会话控制和信息同步 5. 语音合成 6…

目标检测2--yolov1中相关基础知识(边框回归、交并比、nms)介绍

文章目录 前言回归介绍基本概念线性回归非线性回归边框回归 交并比介绍定义程序实现 NMS介绍定义与原理工作原理代码实现 前言 在上篇博客目标检测1–Pytorch目标检测之yolov1中介绍了yolov1的原理&#xff0c;里面提到几个知识点现在详细介绍一下。 回归介绍 在上篇博客中提…

【Qt】Qt开发环境搭建

目录 一. Qt SDK的下载&安装 二. Qt相关工具介绍 Qt的常用开发工具有&#xff1a; Qt CreatorVisual StudioEclipse 一. Qt SDK的下载&安装 Qt 下载官网&#xff1a; http://download.qt.io/archive/qt/ 国内清华源: https://mirrors.tuna.tsinghua.edu.cn/qt/arc…

实战某大型连锁企业域渗透

点击星标&#xff0c;即时接收最新推文 本文选自《内网安全攻防&#xff1a;红队之路》 扫描二维码五折购书 实战域渗透测试流程 对黑客来说&#xff0c;拿下域控制器是终极目标。然而攻击者空间是如何通过采取信息收集、权限提升、横向移动等一系列手段&#xff0c;从而一步步…

《基于 defineProperty 实现前端运行时变量检测》

&#x1f4e2; 大家好&#xff0c;我是 【战神刘玉栋】&#xff0c;有10多年的研发经验&#xff0c;致力于前后端技术栈的知识沉淀和传播。 &#x1f497; &#x1f33b; 近期刚转战 CSDN&#xff0c;会严格把控文章质量&#xff0c;绝不滥竽充数&#xff0c;欢迎多多交流~ &am…

STM32CubeMX实现4X5矩阵按键(HAL库实现)

为了实现计算器键盘&#xff0c;需要使用4X5矩阵按键&#xff0c;因此&#xff0c;我在4X4矩阵键盘上重新设计了一个4X5矩阵按键。原理图如下&#xff1a; 原理描述&#xff1a; 4X5矩阵按键&#xff0c;可以设置4个引脚为输出&#xff0c;5个引脚为输入模式&#xff0c;4个引…

【云原生】Prometheus监控Docker指标并接入Grafana

目录 一、前言 二、docker监控概述 2.1 docker常用监控指标 2.2 docker常用监控工具 三、CAdvisor概述 3.1 CAdvisor是什么 3.2 CAdvisor功能特点 3.3 CAdvisor使用场景 四、CAdvisor对接Prometheus与Grafana 4.1 环境准备 4.2 docker部署CAdvisor 4.2.2 docker部署…

【Java探索之旅】初识多态_概念_实现条件

文章目录 &#x1f4d1;前言一、多态1.1 概念1.2 多态的实现条件 &#x1f324;️全篇总结 &#x1f4d1;前言 多态作为面向对象编程中的重要概念&#xff0c;为我们提供了一种灵活而强大的编程方式。通过多态&#xff0c;同一种操作可以应用于不同的对象&#xff0c;并根据对象…