本内容基于江协科技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协议会进行仲裁,仲裁胜利的一方取得总线控制权,失败的一方自动变回从机。
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是从机,是一主一从模式。
- 建立I2C通信层的.c和.h模块
- 写好I2C底层的GPIO初始化
- 6个时序基本单元:起始、终止、发送一个字节、接收一个字节、发送应答、接收应答
- 建立MPU6050的.c和.h模块
- 基于I2C通信的模块,实现指定地址读、指定地址写、再实现写寄存器对芯片进行配置、读寄存器得到传感器数据
- main.c
- 调用MPU6050模块,初始化,拿到数据,显示数据
3.4 代码
- 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,代表从机没给应答,这就是接收应答的流程。
*/
- 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框图
- 左边是通信引脚: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位流程:起始、从机地址、应答、数据、应答、数据、应答 ··· 停止
- 初始化之后,总线默认空闲状态,STM默认是从模式,为了产生一个起始条件,STM32需要写入控制寄存器(CR1),写1,之后STM32由从模式转为主模式。
- EV5事件可以当作是标志位,SB是状态寄存器的一个位,表示了硬件的状态,SB=1,表示起始条件已发送。
- 然后就可以发送一个字节的从机地址了,从机地址需要写到数据寄存器DR中,写入DR之后,硬件电路就会自动将该地址字节转到移位寄存器里,再把该字节发送到I2C总线上,之后硬件会自动接收应答并判断,如果没有应答,硬件就会置应答失败的标志位,然后该标志位可以申请中断来提醒我们。
- 当寻址完成后,会发生EV6事件,ADDR标志位为1,该标志位在主模式下表示地址发送结束。
- EV8_1事件就是TxE标志位为1,移位寄存器空,数据寄存器空,需要我们写入数据寄存器DR进行数据发送了,写入DR之后,由于移位寄存器为空,DR就会立刻转到移位寄存器进行发送。就会进行EV8事件,移位寄存器非空,数据寄存器空,就是移位寄存器正在发送数据的状态,所以流程这里,数据1的时序就产生了。在该时刻数据2就会被写入到数据寄存器里等着了,接收应答位之后,数据位就转入移位寄存器进行发送,此时的状态是移位寄存器非空,数据寄存器空,因此此时EV8事件就又发生了。
- 之后数据2正在发送,但此次下一个数据就已经被写到数据寄存器等着了。一旦检测到EV8事件,就可以写入下一个数据了。
- 当想要发送的数据写完之后,这时就没有新的数据写入到数据寄存器里了,当移位寄存器当前的数据移位完成时,此时就是移位寄存器空,数据寄存器也空的状态,即EV8_2事件,TxE=1是移位寄存器空、数据寄存器空,BTF:字节发送结束标志位,在发送时,当一个新数据将被发送且数据寄存器还未被写入新的数据。当检测到EV8_2时,就可以产生终止条件Stop了。产生终止条件,显然,应该在控制寄存器里有相应的位可以控制。这样一个发送的时序就结束了。
4.5 主机接收
7位主接收:起始、从机地址+读、接收应答、接收数据、发送应答 ··· 接收数据、非应答、终止
- 首先,写入控制寄存器的Start位,产生起始条件,然后等待EV5事件(表示起始条件已发送)。
- 之后寻址,接收应答,结束后产生EV6事件(表示寻址已完成)。
- 数据1表示数据正在通过移位寄存器进行输入。
- EV6_1表明数据还在进行移位,在接收应答之后,说明移位寄存器已经成功移入一个字节的数据1了,这时移入的一个字节就整体转移到数据寄存器,同时置RxNE标志位,表示数据寄存器非空,也就是收到了一个字节的数据,该状态是EV7事件,RxNE=1,读DR寄存器清除该事件,也就说收到数据了,当我们把数据读走之后,该事件就没有了。
- 当然数据1还没被读走时,数据2就可以直接移入移位寄存器了,之后,数据2移位完成,收到数据2,产生EV7事件,读走数据2,EV7事件没有了。
- 当不需要再接收时,需要在最后一个时序单元发生时,提前把应答位控制寄存器ACK置0,并且设置终止条件请求,即EV7_1事件,之后就会给出非应答NA,由于设置STOP位,所以产生终止条件。
4.6 软件/硬件波形对比
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 运行结果
5.2.3 代码实现流程
- 配置I2C外设,对I2C外设进行初始化,替换MyI2C_Init
(1)开启I2C外设和对应GPIO口的时钟,
(2)把I2C外设对应的GPIO口初始化为复用开漏模式
(3)使用结构体,对整个I2C进行配置
(4)I2C_Cmd,使能I2C - 控制外设电路,实现指定地址写的时序,替换WriteReg
- 控制外设电路,实现指定地址读的时序,替换ReadReg
5.2.4 代码
- 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;
}
- 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);
}
}