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协议会进行仲裁,仲裁胜利的一方取得总线控制权,失败的一方自动变回从机。
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); // 主机释放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,代表从机没给应答,这就是接收应答的流程。*/
- 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框图
- 左边是通信引脚: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的最低位置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;
}
- 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);}
}
相关文章:

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 指定地址读…...

04.ffmpeg打印音视频媒体信息
目录 1、相关头文件 2、相关结构体 3、相关函数 4、函数详解 5、源码附上 1、相关头文件 #include <libavformat/avformat.h> 包含格式相关的函数和数据结构 #include <libavutil/avutil.h> 包含一些通用实用函数 2、相关结构体 AV…...

微信开发授权登录梳理总结
授权登录流程对比 微信公众号/网页 微信文档地址:https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/Wechat_webpage_authorization.html 流程图如下: 特殊说明: 步骤1拼接的微信地址是:https://open.weixin.qq…...

HTML5实现我的音乐网站源码
文章目录 作者:[xcLeigh](https://blog.csdn.net/weixin_43151418) 1.设计来源1.1 界面效果1.2 轮播图界面1.3 音乐播放界面1.4 视频播放界面 2.效果和源码2.1 动态效果2.2 源代码 源码下载万套模板,程序开发,在线开发,在线沟通 作…...

UNI_App平台调试指南 debug(十五)
App平台调试指南 debug 常规开发里,在 HBuilderX 的运行菜单里运行 App,手机端的错误或 console.log 日志信息会直接打印到控制台。 如果需要更多功能,比如审查元素、打断点 debug,则需要启动调试模式。自 HBuilderX 2.0.3+ 版本起开始支持 App 端的调试。 #打开调试窗口…...

LLM之RAG实战(四十一)| 使用LLamaIndex和Gemini构建高级搜索引擎
Retriever 是 RAG(Retrieval Augmented Generation)管道中最重要的部分。在本文中,我们将使用 LlamaIndex 实现一个结合关键字和向量搜索检索器的自定义检索器,并且使用 Gemini大模型来进行多个文档聊天。 通过本文,我…...

【错题集-编程题】AOE还是单体?(贪心)
牛客对应链接:AOE还是单体? (nowcoder.com) 一、分析题目 如果使用一次 AOE 造成的伤害比消耗的蓝量多,那就使用。否则就一直使用单体伤害。 二、代码 //值得学习的代码 #include <iostream> #include <algorithm>using namespa…...

怎么办?我的C盘又爆红了!别慌!博主手把手带你管理你的C盘空间~
怎么办?我的C盘又爆红了!别慌!博主手把手带你管理你的C盘空间~ 文章目录 怎么办?我的C盘又爆红了!别慌!博主手把手带你管理你的C盘空间~0. 在开始清理之前1. 推荐执行的操作1.1 清理系统缓存文件1.2 磁盘清…...

react启用mobx @decorators装饰器语法
react如果没有经过配置,直接使用decorators装饰器语法会报错: Support for the experimental syntax ‘decorators’ isn’t currently enabled 因为react默认是不支持装饰器语法,需要做一些配置来启用装饰器语法。 step1: 在 tsconfig.js…...
计算机如何学习
1. 不要只盯着计算机语言学习,你现在已经学习了C语言和Java,暑假又规划学习Python,最后你掌握的就是计算机语言包而已。 2. 建议你找一门想要深挖的语言,沿着这个方向继续往后学习知识就行。计算机语言是学不完的,而未…...
【Python 基础】函数 - 1
函数 从前面的章节中,你已经熟悉了 print()、input()和 len()函数。Python 提供了这样一些内建函数,但你也可以编写自己的函数。“函数”就像一个程序内的小程序。 为了更好地理解函数的工作原理,让我们来创建一个 函 数 。 在 文 件 编 辑器 中 输 入 下 面 的 程 序 , …...

从0到1开发一个Vue3的新手引导组件(附带遇到的问题以及解决方式)
1. 前言: 新手引导组件,顾名思义,就是强制性的要求第一次使用的用户跟随引导使用应用,可以让一些第一次使用系统的新手快速上手,正好我最近也遇到了这个需求,于是就想着开发一个通用组件拿出来使用(写完之后才发现element就有,后悔了哈哈哈😭😭) 示例图…...

概率统计(二)
二维离散型 联合分布律 样本总数为16是因为,两封信分别可以放在4个信箱 边缘分布律 条件分布律 独立性 选填才能用秒杀 联合概率乘积不等于边缘概率的乘积则不独立 二维连续型 区间用一重积分面积用二重积分 离散型随机变量 常见6个分布的期望和方差 离散型随机变…...
文件类:如何将excel文件转为csv文件(且保留时间格式)?
最近有个场景,在ftp服务器上,读取csv文件并入库,但是客户提供的一部分文件却是xls文件,就得搞个将excel转为csv文件的方法,话不多说直接开干。 方法 public static void convertExcelToCSV(String excelFilePath, Str…...

FiddlerScript Rules修改-更改发包中的cookie
直接在fiddler script editor中增加如下处理代码即可 推荐文档oSession -- 参数说明 测试笔记 看云...
直升机停机坪的H代表什么
可为什么直升机的停机坪为什么要用“H”来表示呢? Helicopter 直升机停机坪的“H”来自直升机的英文Helicopter的首字母,也是停机坪的识别标志,表示可用于直升机的垂直起降,方便于直升机飞行员在空中能快速识别降落位置。 另外…...

hyperworks软件许可优化解决方案
Hyperworks软件介绍 Altair 仿真驱动设计改变了产品开发,使工程师能够减少设计迭代和原型测试。提升科学计算能力扩大了应用分析的机会,使大型设计研究能够在限定的项目时间完成。现在,人工智能在工程领域的应用再次改变了产品开发。基于物理…...

四川赤橙宏海商务信息咨询有限公司抖音电商服务靠谱吗?
在数字化浪潮席卷全球的今天,电商行业蓬勃发展,各种新兴电商平台层出不穷。其中,抖音电商以其独特的社交属性和庞大的用户基础,迅速崛起为行业新星。四川赤橙宏海商务信息咨询有限公司,作为专注于抖音电商服务的佼佼者…...

鸿蒙开发:Universal Keystore Kit(密钥管理服务)【密钥派生(C/C++)】
密钥派生(C/C) 以HKDF256密钥为例,完成密钥派生。具体的场景介绍及支持的算法规格,请参考[密钥生成支持的算法]。 在CMake脚本中链接相关动态库 target_link_libraries(entry PUBLIC libhuks_ndk.z.so)开发步骤 生成密钥 指定密钥别名。 初始化密钥属…...
【ARMv8/v9 GIC 系列 6 -- 中断优先级详细介绍】
请阅读【ARM GICv3/v4 实战学习 】 文章目录 Interrupt prioritizationInterrupt Priority ValueSGI And PPI Priority SetSecure And Non-secure Priority AccessInterrupt prioritization 在ARM GICv3和GICv4架构中,中断的优先级化(prioritization)是通过以下几种方式来描…...
基于算法竞赛的c++编程(28)结构体的进阶应用
结构体的嵌套与复杂数据组织 在C中,结构体可以嵌套使用,形成更复杂的数据结构。例如,可以通过嵌套结构体描述多层级数据关系: struct Address {string city;string street;int zipCode; };struct Employee {string name;int id;…...

观成科技:隐蔽隧道工具Ligolo-ng加密流量分析
1.工具介绍 Ligolo-ng是一款由go编写的高效隧道工具,该工具基于TUN接口实现其功能,利用反向TCP/TLS连接建立一条隐蔽的通信信道,支持使用Let’s Encrypt自动生成证书。Ligolo-ng的通信隐蔽性体现在其支持多种连接方式,适应复杂网…...

React19源码系列之 事件插件系统
事件类别 事件类型 定义 文档 Event Event 接口表示在 EventTarget 上出现的事件。 Event - Web API | MDN UIEvent UIEvent 接口表示简单的用户界面事件。 UIEvent - Web API | MDN KeyboardEvent KeyboardEvent 对象描述了用户与键盘的交互。 KeyboardEvent - Web…...
反射获取方法和属性
Java反射获取方法 在Java中,反射(Reflection)是一种强大的机制,允许程序在运行时访问和操作类的内部属性和方法。通过反射,可以动态地创建对象、调用方法、改变属性值,这在很多Java框架中如Spring和Hiberna…...

什么是Ansible Jinja2
理解 Ansible Jinja2 模板 Ansible 是一款功能强大的开源自动化工具,可让您无缝地管理和配置系统。Ansible 的一大亮点是它使用 Jinja2 模板,允许您根据变量数据动态生成文件、配置设置和脚本。本文将向您介绍 Ansible 中的 Jinja2 模板,并通…...
Python 包管理器 uv 介绍
Python 包管理器 uv 全面介绍 uv 是由 Astral(热门工具 Ruff 的开发者)推出的下一代高性能 Python 包管理器和构建工具,用 Rust 编写。它旨在解决传统工具(如 pip、virtualenv、pip-tools)的性能瓶颈,同时…...
Go 并发编程基础:通道(Channel)的使用
在 Go 中,Channel 是 Goroutine 之间通信的核心机制。它提供了一个线程安全的通信方式,用于在多个 Goroutine 之间传递数据,从而实现高效的并发编程。 本章将介绍 Channel 的基本概念、用法、缓冲、关闭机制以及 select 的使用。 一、Channel…...

深度学习水论文:mamba+图像增强
🧀当前视觉领域对高效长序列建模需求激增,对Mamba图像增强这方向的研究自然也逐渐火热。原因在于其高效长程建模,以及动态计算优势,在图像质量提升和细节恢复方面有难以替代的作用。 🧀因此短时间内,就有不…...
苹果AI眼镜:从“工具”到“社交姿态”的范式革命——重新定义AI交互入口的未来机会
在2025年的AI硬件浪潮中,苹果AI眼镜(Apple Glasses)正在引发一场关于“人机交互形态”的深度思考。它并非简单地替代AirPods或Apple Watch,而是开辟了一个全新的、日常可接受的AI入口。其核心价值不在于功能的堆叠,而在于如何通过形态设计打破社交壁垒,成为用户“全天佩戴…...
Python 训练营打卡 Day 47
注意力热力图可视化 在day 46代码的基础上,对比不同卷积层热力图可视化的结果 import torch import torch.nn as nn import torch.optim as optim from torchvision import datasets, transforms from torch.utils.data import DataLoader import matplotlib.pypl…...