软件
IIC通信

I2C(Inter-Integrated Circuit) 是一种由 Philips(现 NXP)在 1980 年代开发的同步、半双工、多主多从的串行通信总线。
1. 物理层特性(电气特征)
-
两根线:
-
SCL(Serial Clock):时钟线,用于同步数据传输。 -
SDA(Serial Data):数据线,用于传输实际的数据和地址。
-
-
开漏输出(Open-Drain)与上拉电阻 :I2C 总线上的所有设备引脚必须是开漏(或开集)输出。总线必须通过上拉电阻(通常为 4.7kΩ - 10kΩ)连接到 VCC。这意味着设备只能将总线拉低(输出0),不能主动拉高(输出1)。总线的高电平靠上拉电阻提供。这种设计实现了**"线与(Wired-AND)"**逻辑,也是多设备总线仲裁的基础。
-
总线电容限制:由于上拉电阻和线路寄生电容,I2C 总线通常适用于短距离通信(板级通信,通常不超过 1 米)。
2. 协议层特性(数据格式)
-
起始与停止条件(Start/Stop):
-
起始条件:SCL 为高电平时,SDA 由高变低。
-
停止条件:SCL 为高电平时,SDA 由低变高。
-
-
数据有效性:在 SCL 为高电平期间,SDA 上的数据必须保持稳定;SDA 的电平改变只能发生在 SCL 为低电平期间。
-
寻址机制 :主机发起起始条件后,发送的第一个字节是从机地址(7位或10位) + 读写位(R/W,1位)。
-
应答机制(ACK/NACK):每发送完 8 位数据(1个字节),发送方会释放 SDA 线。接收方必须在第 9 个时钟周期将 SDA 拉低,表示应答(ACK);如果不拉低(保持高电平),则为非应答(NACK)。
-
总线仲裁与时钟同步:支持多主机。如果两个主机同时发送数据,通过"线与"特性,发送低电平的主机会赢得总线控制权;时钟同步通过 SCL 线的线与特性自动完成。
3. 拓扑结构
-
一主多从(最常见):一个 Master(如 STM32),多个 Slave(如 OLED、EEPROM)。
-
多主多从:总线上有多个 Master,需要仲裁机制。
硬件电路

它直接解释了为什么我们在写I2C代码时,要把GPIO引脚配置成"开漏输出"。
第一部分:严谨的技术解释(硬件手册视角)
1. 总线拓扑结构
-
所有I2C设备的SCL(时钟线)和SDA(数据线)分别并联在一起。
-
因为所有设备都是"线与(Wired-AND)"逻辑,所以I2C总线属于多主多从结构。总线上任何一个设备拉低总线,整条线都会变成低电平。
2. 开漏输出(Open-Drain)
-
右侧的内构图展示了引脚内部:只有N沟道MOS管(下拉到GND),没有P沟道MOS管(上拉到VCC)。
-
当"输出控制"为高电平时,MOS管导通,引脚拉低到地(输出0)。
-
当"输出控制"为低电平时,MOS管截止,引脚悬空(高阻态)。此时引脚无法自己输出高电平,必须依靠外部上拉电阻提供高电平(输出1)。
3. 上拉电阻(Pull-up Resistor)
-
SCL和SDA必须各接一个上拉电阻(典型值4.7kΩ)到电源正极(+VDD)。
-
阻值计算考量:阻值不能太大(否则RC充电时间常数大,总线上升沿变缓,高频通信时波形会变成锯齿波);也不能太小(否则当MOS管导通拉低时,VCC到GND之间会有很大的灌电流,烧毁引脚)。4.7kΩ是速度和功耗的折中。
IIC时序基本单元
-
起始条件(Start):在 SCL 线为高电平期间,SDA 线由高电平切换到低电平。
-
终止条件(Stop):在 SCL 线为高电平期间,SDA 线由低电平切换到高电平。
-
意义:由于 I2C 总线平时默认都是高电平,这两种"在SCL高电平期间强行改变SDA"的异常动作,被定义为通信的"开始"和"结束"信号。其他普通的数据位传输,绝对不允许在SCL高电平期间改变SDA。
-
在 SCL 低电平期间,主机将数据位依次放到 SDA 线上(高位先行,即 MSB First)。
-
然后主机释放 SCL(SCL 变为高电平),从机将在 SCL 高电平期间读取数据位。
-
铁律:SCL 高电平期间,SDA 不允许有数据变化。
-
依次循环 8 次,即可发送一个字节。

-
主机在接收之前,需要释放 SDA(也就是主机把 SDA 置为高电平或输入模式,让外部上拉电阻把线拉高)。
-
在 SCL 低电平期间,从机将数据位依次放到 SDA 线上(高位先行)。
-
然后从机释放 SCL,主机将在 SCL 高电平期间读取数据位。
-
同样,SCL 高电平期间 SDA 不允许变化。
-
循环 8次,接收一个字节。

每传输完 8 位数据,必须跟 1 位应答位(第9个时钟周期)。
-
接收应答(主机发完8位数据后):主机释放 SDA(保持高电平),在下一个时钟(第9个时钟)接收一位数据。数据 0 表示应答(ACK),数据 1 表示非应答(NACK)。
-
发送应答(主机收完8位数据后):主机在接收完一个字节之后,在下一个时钟发送一位数据。数据 0 表示应答(ACK),数据 1 表示非应答(NACK)。
在你的逻辑分析仪截图中,每 8 个时钟周期(发送/接收完一个字节)后面,紧跟的第 9 个时钟周期,就是应答位(ACK/NACK)。
根据第4个基本单元(应答机制),应答分为两种情况:
-
RA (Receive Acknowledge) :主机接收从机的应答。当主机发送完 8 位数据后,主机释放 SDA,由从机在第 9 个时钟决定是否拉低 SDA。主机此时去"读"这个电平。
-
RA:0表示从机拉低了 SDA,是应答(ACK),意思是"我收到了,继续"。 -
RA:1表示从机没拉低,是非应答(NACK),意思是"我不在或出错啦"。
-
-
SA (Send Acknowledge) :主机发送应答给从机。当从机发送完 8 位数据后,从机释放 SDA,由主机在第 9 个时钟决定是否拉低 SDA。
-
SA:0表示主机拉低了 SDA,是应答(ACK),意思是"我收到了,你继续发"。 -
SA:1表示主机没拉低,是非应答(NACK),意思是"我接收完了,你别发了"。
-
IIC时序



第一张图:指定地址写(带 RA/SA 深度解析版)
这张 PPT 把你学过的"四个基本单元"串成了一条完整的"流水线"------指定地址写(向从机的某个寄存器写入数据)。
你仔细盯着逻辑分析仪图(黄色是 SCL,蓝色是 SDA),心里默念口诀:"低准备,高读取,起止上下坡" 。注意看图中下方标注的绿色数字(二进制位)和红色的 RA:0。
第 1 步:起始条件(Start)
-
看最左边红色的
S线。 -
黄色 SCL 是高电平,蓝色 SDA 出现了一个下坡(高 ➔ 低)。
-
口诀验证:"起止上下坡"。Start 就是往下掉。
第 2 步:发送从机地址 + 读写位(Send Byte: 0xD0)
-
主机开始发 8 位数据。看波形下方标注的数字
1 1 0 1 0 0 0 0。 -
高低电平验证:你仔细看蓝色 SDA 的跳变,是不是全都发生在黄色 SCL 处于低电平(波谷)的时候?一旦黄色 SCL 变成高电平(波峰),蓝色 SDA 就变成一条平直的线,绝对不动。这就是"低准备,高读取"。
-
0xD0 的二进制是
11010000,最后一位是 0,表示写操作(Write)。 -
🔍 RA 登场 :发完 8 位后,第9个时钟周期 ,主机释放 SDA(不拉低)。此时从机拉低 SDA(图上的
RA:0)。这里的 RA (Receive Acknowledge) 意思是"主机接收从机的应答"。RA:0表示从机成功拉低了 SDA,给出了 ACK 应答,意思是"0xD0这个设备收到啦"。
第 3 步:发送寄存器地址(Send Byte: 0x19)
-
主机接着发 0x19。看图上的二进制:
0 0 0 1 1 0 0 1。 -
同样,蓝线跳变全在黄线低电平时。发完后,从机再次拉低 SDA 给出应答(
RA:0)。 -
为什么要有这一步? 因为 I2C 设备内部通常有很多个寄存器(比如 MPU6050 有上百个寄存器)。0xD0 只是找到了这个设备,0x19 是告诉它:"我要操作你家里的第 0x19 号房间(寄存器)"。
第 4 步:发送数据(Send Byte: 0xAA)
-
主机发真正要写的内容 0xAA,二进制
1 0 1 0 1 0 1 0。 -
同样遵循"低电平变化,高电平稳定"。发完后,从机再次应答(
RA:0)。表示"数据我已经放到 0x19 号房间了"。
第 5 步:终止条件(Stop)
-
看最右边红色的
P线。 -
黄色 SCL 是高电平,蓝色 SDA 出现了一个上坡(低 ➔ 高)。
-
口诀验证:"起止上下坡"。Stop 就是往上升。
🎯 针对这张图,关于 RA 和 SA 的硬核总结:
-
为什么这张图只有 RA,没有 SA?
-
因为这是**"写"操作** 。整个过程中,主机是发送方,从机是接收方。
-
RA(Receive Acknowledge) :是主机发完8位后,接收 从机的应答。这张图里出现了三次
RA:0,对应三次发送(设备地址、寄存器地址、数据),从机都用拉低 SDA 的方式给出了 ACK。 -
SA(Send Acknowledge) :是主机接收完8位后,发送 给从机的应答。这张图主机全程在说话,没有接收数据,所以根本没有 SA 出现。SA 只会出现在"读"操作的末尾(如你后面两张图)。
-
-
终极防混心法(针对这张图):
-
只要是
Send Byte(主机发),紧跟的一定是RA(从机应答)。 -
只要是
Receive Byte(主机收),紧跟的一定是SA(主机应答)。 -
你不需要去背什么时候 SCL 是 0,什么时候 SCL 是 1。你只需要记住:蓝色的 SDA 线,永远只能在黄色的 SCL 波谷(低电平)里上下跳动。当黄色的 SCL 在波峰(高电平)时,蓝色的 SDA 必须像死鱼一样平坦。
-
MPU6050简介

第一部分:严谨的技术解释
1. 什么是 MPU6050?
-
它是一个 6轴姿态传感器(6-Axis MotionTracking Device)。
-
所谓"6轴",是因为它内部集成了两个传感器,每个传感器都有 X、Y、Z 三个轴:3轴加速度计 + 3轴陀螺仪传感器。
-
它可以测量芯片自身的加速度、角速度参数 。通过主控芯片进行数据融合(Data Fusion) 算法,可以进一步计算出设备的姿态角(欧拉角:俯仰、横滚、偏航)。
2. 核心组件(看下边的三张图):
-
图1(左):MPU6050 芯片实物。标注了 X、Y、Z 轴的方向(注意它的轴方向,Z轴是垂直于芯片表面的)。
-
图2(中):3轴加速度计(Accelerometer)示意图。
- 原理:基于牛顿第二定律。图中是一个质量块连着弹簧,外面是电位计。当芯片有加速度时,质量块会移动,带动电位计输出电压变化,从而测量出加速度。
-
图3(右):3轴陀螺仪传感器(Gyroscope)示意图。
- 原理:基于科里奥利力(或者角动量守恒)。图中是一个旋转轮(转子),当外部旋转时,旋转轴会发生偏转,通过测量这个偏转量来计算角速度。
3. 应用场景:
- 平衡车(需要知道车身前倾/后仰角度)、飞行器/无人机(需要实时知道姿态以保持稳定)、手机屏幕旋转、游戏手柄等。
🗣️ 第二部分:大白话解释(生活化比喻)
1. 它是干嘛的?
你闭上眼睛,怎么知道自己是站着、躺着还是歪着?怎么知道自己是在走路还是在原地转圈?
MPU6050 就是给机器装上的一只"耳朵"里的**"平衡感知器"**。
2. 加速度计(测倾角和震动):
-
你拿一根带弹簧的筷子,上面放个重物。你倾斜筷子,重物会拉动弹簧。
-
加速度计测的就是这个"弹簧被拉动的力度和方向"。
-
当你静止时,它其实在测量重力加速度,所以能算出你倾斜了多少度;当你运动时,它能测出你动得多快。
3. 陀螺仪(测旋转):
-
想象一个正在高速旋转的陀螺。你如果用手去推它,它会歪。
-
陀螺仪测的就是"你转得有多快"。
-
它不测倾斜角度,它只测"角速度"(比如:每秒转 90 度)。你要想知道转了多少度,需要主控芯片用代码把角速度"积分"起来(累加)。
4. 为什么要"数据融合"?
加速度计在静止时很准,但运动时会因为震动产生大量噪声;陀螺仪对旋转很准,但时间长了会有"漂移"(微小的误差累积),最后算出来会偏到姥姥家。
所以需要把两者的数据**"融合"**起来(比如卡尔曼滤波),用加速度计来修正陀螺仪的漂移,才能得到精准的"姿态角"。
MPU6050参数

🚨 第一部分:ADC 到底是个啥?
你的疑问:"ADC不是数模转换吗我去?"
-
ADC (Analog-to-Digital Converter) = 模数转换器(模拟 ➔ 数字)。
-
DAC (Digital-to-Analog Converter) = 数模转换器(数字 ➔ 模拟)。
什么是模拟信号?什么是数字信号?
-
模拟信号:像波浪一样连续变化的电压。比如真实的温度、声音、重力导致的微小电压变化。
-
数字信号:像台阶一样蹦跶的 0 和 1。比如单片机内部处理的二进制。
MPU6050 内部发生了什么?
MPU6050 里面有三个微小的机械结构(MEMS),当你移动或旋转它时,这些机械结构会移位,导致电压发生极其微小的变化(产生模拟信号 )。
但是,你的 STM32 单片机只认 0 和 1,没法直接处理这波浪般的电压。怎么办?
MPU6050 内部自带了 16位 ADC!
它把测量出来的模拟电压,转换成了 -32768 到 32767 之间的整数(这就是数字信号),然后你才能通过 I2C 读出来。
(生活例子:你用麦克风唱歌,声音是模拟信号;电脑声卡把声音录进去,靠的就是 ADC 把声音变成数字文件;你想听歌,声卡把数字文件变成喇叭的电流,靠的是 DAC。)
📖 第二部分:一张图看懂 MPU6050 参数(严谨+大白话)
现在我们回来看这张 PPT,它其实是在告诉你:这颗芯片能测多大范围,以及精度有多高。
- 16位ADC,量化范围:-32768 ~ 32767
-
严谨:MPU6050 内部集成了 16位 ADC。2 的 16 次方等于 65536,因为有正负,所以是 -32768 到 32767。
-
大白话:不管外界实际的加速度或旋转有多快,芯片内部都会把它切分成 65536 个等级。等级越高(比如32767),代表实际数值越大;负数代表反方向。
- 加速度计满量程选择:±2、±4、±8、±16 (g)
-
严谨:g 是重力加速度。通过配置寄存器,可以设置加速度计的最大测量范围。
-
大白话:你拿一把尺子,你想用它量桌子还是量房子?
-
选 ±2g:只能测两倍重力以内的加速度(适合测静置时的倾角,精度极高)。
-
选 ±16g :可以测 16 倍重力(适合测火箭发射、剧烈碰撞,但因为总分只有 65536,量程大了,细微的变化就测不出来了,精度变低)。
-
-
关键算法:如果你选了 ±2g,那么把读取到的数字 32767 除以 16384,就等于 2g(灵敏度是 16384 LSB/g)。你选什么量程,就要除以对应的灵敏度。
- 陀螺仪满量程选择:±250、±500、±1000、±2000 (°/sec)
-
同理。°/sec 代表"度每秒"(旋转速度)。
-
如果你测的是慢悠悠的平衡车,选 ±250 就够了,精度高;如果你测的是疯狂旋转的无人机,必须选 ±2000,不然数据瞬间就爆表(饱和)了。
- 可配置的数字低通滤波器
- 大白话:当你手抖或电机震动时,会混入很多杂波噪声。这个滤波器就是一个"筛子",把高频的抖动筛掉,只留下真实的倾斜数据。通过配置寄存器可以调节滤波的强度。
- 可配置的时钟源
- 芯片内部是有个时钟在滴答滴答走才工作的。默认靠内部 RC 振荡器,但不太准。你可以配置它使用外部时钟(比如外接晶振),或者直接借用一个轴的陀螺仪时钟。
- 可配置的采样分频
- 芯片内部采样频率可能高达 8000Hz(每秒采8000次),太快了,STM32 根本读不过来。采样分频就相当于"降速",配置一下,让它每秒只输出 50 次或 100 次数据。
- I2C 从机地址
-
1101000 (AD0=0)也就是 0x68。 -
1101001 (AD0=1)也就是 0x69。 -
大白话:看 MPU6050 模块上的那个 AD0 引脚。如果你把它接 GND(低电平),设备地址就是 0x68;接 VCC(高电平),地址就是 0x69。
-
为什么有这个设计?因为一个 I2C 总线上可以挂两个 MPU6050!把一个 AD0 接高电平,一个接低电平,STM32 就能靠不同的地址区分它俩。
硬件电路

🔌 一、 电源部分(左侧和中间)
严谨版:
-
输入
VCC_5V,经过低压差线性稳压器(LDO,图中的 Q2)降压,输出VCC_3.3V。 -
电容 C10 (10uF) 和 C12 (0.1uF) 起滤波作用,稳定电压。
-
LED 灯 D1 串联限流电阻 R201 (1KΩ) 作为电源指示灯。
大白话版 :
MPU6050是个娇贵的芯片,只能吃3.3V的电,你要是直接给它5V,它会当场烧给你看。所以需要一个LDO(相当于变压器)把5V变成3.3V。电容就像水库,把电压的小波动给滤平。LED亮起,说明电源接通了,心里踏实。
🎯 二、 核心通信引脚:SCL, SDA(黄金知识点回归!)
严谨版:
-
MPU6050 的引脚 23 (SCL) 和 24 (SDA) 是I2C通信引脚。
-
原理图中明确画出了两个 4.7KΩ 的上拉电阻 R4 和 R5,连接到 VCC_3.3V。
-
外部接口 J2 引出了 VCC, GND, SCL, SDA。
大白话版(结合之前的比喻) :
这就是你的"教室"接口!STM32 连上 J2 的 SCL 和 SDA,就可以跟它对话了。
最激动人心的是 R4 和 R5! 我们之前反复强调"I2C必须用开漏输出,必须外接上拉电阻",你看,人家官方设计电路板上已经把这两个4.7K的"弹簧"焊好了。所以你在写STM32代码配置引脚时,必须配置为开漏模式(GPIO_Mode_Out_OD)。
📍 三、 地址引脚:AD0(决定代码中0x68的关键)
严谨版:
-
引脚 9 (AD0) 通过电阻 R6 (4.7KΩ) 下拉接地(GND)。
-
根据上节课的公式:AD0=0,从机地址为
1101000,即 0x68。
大白话版 :
你未来写代码时,I2C_SendByte(0x68) 里的 0x68 是怎么来的?就是看这个电路图来的!因为 AD0 接了地(低电平),所以它的名字叫 68 号。如果 R6 接的是 VCC_3.3V,那代码里就必须写 0x69。
🧩 四、 其他引脚(拓展与辅助)
1. XDA, XCL(引脚 6, 7)------ 主I2C通信
-
严谨:辅助I2C总线(Auxiliary I2C)。
-
大白话:MPU6050还可以当"小老师",自己作为主机去控制其他外设(比如连接HMC5883L磁力计,组成9轴传感器)。这里空着没接,不用管它。
2. INT(引脚 12)------ 中断信号输出
-
严谨:当MPU6050有新的数据或检测到运动时,输出电平跳变。
-
大白话:MPU6050的一个"举手"引脚。平时STM32需要不停地问"数据好了没?"(轮询)。接了这根线,MPU6050数据好了就会"举手"打断STM32:"喂,数据准备好了,快来读!"(触发中断)。这里没接,说明你代码里只能用轮询方式读。
3. CLKIN, CLKOUT(引脚 1, 22)------ 时钟
- 大白话:内部心跳。这里没接外部晶振,说明它用自己的内部RC振荡器作为时钟。
4. CPOUT, REGOUT(引脚 20, 19)------ 电荷泵和稳压
- 大白话:内部模拟电路的供电和滤波,接了几个小电容(C14, C13),不用你操心。
5. FSYNC(引脚 11)------ 帧同步
- 大白话:做复杂图像/姿态同步用的,这里也用不到。

别怕,这张图看着像迷宫,其实就是 MPU6050 芯片的**"内部世界地图"**。你把这张图看懂了,以后写代码就不再是盲目地抄寄存器地址,而是清楚地知道自己操作的是芯片的哪一块零件。
接下来,我们继续沿用"严谨 + 大白话"的讲解方式,按照信号流向(从左到右),把这张框图拆解为八个部分:
🔬 第一部分:感官与翻译官(左侧,模拟信号世界)
严谨版:
-
最左侧是
Self test(自测)和三个轴的加速度计(X/Y/Z Accel)、三个轴的陀螺仪(X/Y/Z Gyro),以及一个Temp Sensor(温度传感器)。 -
这些物理传感器输出的都是微弱的模拟电压。
-
模拟电压进入
ADC(模数转换器),转化为 16 位数字信号。 -
随后进入
Signal Conditioning(信号调理),进行滤波、降噪和温度补偿。 -
还有
Charge Pump(电荷泵)连接到引脚 20(CPOUT),用于给内部模拟电路提供更高的工作电压。
🧠 第二部分:大脑与仓库(中间,数字处理世界)
严谨版:
-
调理后的数据被存入
Sensor Registers(传感器寄存器)和FIFO(先入先出缓冲区)。 -
Config Registers(配置寄存器)接收外部主机的指令(比如设置量程)。 -
Factory Calibration(出厂校准)提供芯片出厂的误差修正数据。 -
Digital Motion Processor (DMP)(数字运动处理器):芯片内置的硬件算法核心。
最厉害的是那个 DMP(数字运动处理器)。如果没有它,你需要自己写代码做复杂的"数据融合算法"。有了它,MPU6050 自己就能算出姿态角,直接把结果告诉你。
📡 第三部分:通信与对外接口(右侧,接口世界)
严谨版:
-
Slave I2C and SPI Serial Interface(从机 I2C/SPI 接口):包含SCL、SDA、AD0引脚。这是 STM32 与 MPU6050 通信的"大门"。 -
Interrupt Status Register(中断状态寄存器):连接到INT引脚,用于通知 STM32 数据准备好了。 -
Master I2C Serial Interface(主机 I2C 接口):包含AUX_CL、AUX_DA引脚。MPU6050 可以当主机,去控制外部的其他传感器。 -
Serial Interface Bypass Mux(旁路复用器):允许外部主控直接访问挂在 AUX 上的其他传感器。 -
底部是
Bias & LDO:给内部提供稳定的电源。
IIC软件读写MPU6050
cpp
main.c
#include "stm32f10x.h"
#include "Delay.h"
#include "OLED.h"
#include "IIC.h"
#include "MPU6050.h"
int main(void)
{
/*uint8_t ack;
OLED_Init();
i2c_init();
i2c_start();
i2c_send_byte(0xD0)//根据MPU6050的地址寻址,直接呼叫硬件;
ack = i2c_recv_ACK();
i2c_stop();
OLED_ShowNum(1, 1, ack, 2);
查看是否IIC建立成功
*/
uint8_t data = 0;
//初始化
OLED_Init();
MPU6050_init();
// data = MPU6050_getId();
// OLED_ShowString(1, 1, "ID: ");
// OLED_ShowHexNum(2, 1, data, 3);
// 看看ID是否是正版MPU6050,是否芯片ID是068
//
int16_t AX, AY, AZ, GX, GY, GZ;
while(1)
{
MPU6050_getData(&AX, &AY, &AZ, &GX, &GY, &GZ);
OLED_ShowSignedNum(1, 1, AX, 5);
OLED_ShowSignedNum(2, 1, AY, 5);
OLED_ShowSignedNum(3, 1, AZ, 5);
OLED_ShowSignedNum(1, 8, GX, 5);
OLED_ShowSignedNum(2, 8, GY, 5);
OLED_ShowSignedNum(3, 8, GZ, 5);
Delay_ms(100);
}
return 0;
}
IIC.C
#include "stm32f10x.h" // Device header
#include "Delay.h"
void i2c_W_SCL(uint8_t value)//写SCL
{
GPIO_WriteBit(GPIOB, GPIO_Pin_10, (BitAction)value);
Delay_us(10);
}
void i2c_W_SDA(uint8_t value)//写SDA
{
GPIO_WriteBit(GPIOB, GPIO_Pin_11, (BitAction)value);
Delay_us(10);
}
uint8_t i2c_R_SDA(void) //读SDA
{
uint8_t value;
value = GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11);
Delay_us(10);
return value;
}
void i2c_init(void)
{
GPIO_InitTypeDef GPIO_InitStruct;
//使能时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
//SCL B10 SDA B11
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_OD;//必须开漏输出
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_10 | GPIO_Pin_11;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStruct);
//初始状态为高电平,准备进入状态
GPIO_SetBits(GPIOB, GPIO_Pin_10 | GPIO_Pin_11);
}
//起始条件
void i2c_start(void)
{
i2c_W_SDA(1);
i2c_W_SCL(1);
i2c_W_SDA(0);
i2c_W_SCL(0);
}
//结束条件
void i2c_stop(void)
{
i2c_W_SDA(0);
i2c_W_SCL(1);
i2c_W_SDA(1);
}
void i2c_send_byte(uint8_t value)//发送一个字节
{
uint8_t i = 0;
for (i = 0; i < 8; i++)
{
i2c_W_SDA(value & (0x80 >> i));//移位即高位先发
i2c_W_SCL(1);
i2c_W_SCL(0);
}
}
//接收一个字节
uint8_t i2c_recv_byte(void)//接收一个字节
{
uint8_t i = 0;
uint8_t value = 0;
//主设备释放SDA I2C 总线是开漏输出,SDA 线可能被主机或从机拉低。
//当主机要接收数据时,SDA 线必须由从机控制。
//如果主机此时还输出低电平(SDA = 0),就会和从机输出的电平冲突,甚至损坏器件。
//所以主机必须先把 SDA 写成 1:
//在开漏模式下,写 1 不是强推高,而是释放总线(高阻态);
//外部上拉电阻会把 SDA 拉高;
//这样从机就可以自由地拉低或释放 SDA 来发送数据。
i2c_W_SDA(1);
for (i = 0; i < 8; i++)
{
i2c_W_SCL(1);
if (i2c_R_SDA() == 1)
{
value = value | 0x80 >> i;
}
i2c_W_SCL(0);
}
return value;
}
//发送ACK
void i2c_send_ACK(uint8_t ack)
{
i2c_W_SDA(ack);
i2c_W_SCL(1);
//delay_us(5);
i2c_W_SCL(0);
}
//接收ACK
uint8_t i2c_recv_ACK(void)
{
uint8_t value = 0;
//主设备释放SDL
i2c_W_SDA(1);
i2c_W_SCL(1);
value = i2c_R_SDA();
i2c_W_SCL(0);
return value;
}
MPU6050.C
#include "stm32f10x.h" // Device header
#include "IIC.H"
#define MPU6050_ADDRESS 0xD0 //MPU6050 I2C写地址(7位地址0x68左移一位,最低位0表示写)
#define MPU6050_SMPLRT_DIV 0x19 //采样率分频。采样率 = 1kHz / (1 + 值)
#define MPU6050_CONFIG 0x1A //配置数字低通滤波器 DLPF。决定加速度和陀螺仪的输出带宽
#define MPU6050_GYRO_CONFIG 0x1B //设置陀螺仪量程
#define MPU6050_ACCEL_CONFIG 0x1C //设置加速度量程
#define MPU6050_ACCEL_XOUT_H 0x3B //加速度X轴高8位数据
#define MPU6050_ACCEL_XOUT_L 0x3C //加速度X轴低8位数据
#define MPU6050_ACCEL_YOUT_H 0x3D //加速度Y轴高8位数据
#define MPU6050_ACCEL_YOUT_L 0x3E //加速度Y轴低8位数据
#define MPU6050_ACCEL_ZOUT_H 0x3F //加速度Z轴高8位数据
#define MPU6050_ACCEL_ZOUT_L 0x40 //加速度Z轴低8位数据
#define MPU6050_TEMP_OUT_H 0x41 //温度高8位数据
#define MPU6050_TEMP_OUT_L 0x42 //温度低8位数据
#define MPU6050_GYRO_XOUT_H 0x43 //陀螺仪X轴高8位数据
#define MPU6050_GYRO_XOUT_L 0x44 //陀螺仪X轴低8位数据
#define MPU6050_GYRO_YOUT_H 0x45 //陀螺仪Y轴高8位数据
#define MPU6050_GYRO_YOUT_L 0x46 //陀螺仪Y轴低8位数据
#define MPU6050_GYRO_ZOUT_H 0x47 //陀螺仪Z轴高8位数据
#define MPU6050_GYRO_ZOUT_L 0x48 //陀螺仪Z轴低8位数据
#define MPU6050_PWR_MGMT_1 0x6B //电源管理1:控制睡眠模式、时钟源、复位、温度传感器使能等
#define MPU6050_PWR_MGMT_2 0x6C //电源管理2:控制各轴是否进入待机
#define MPU6050_WHO_AM_I 0x75 //设备身份ID寄存器,正常读回0x68(十进制104)
void MPU6050_WriteReg(uint8_t regAddr, uint8_t data)
{
i2c_start();
i2c_send_byte(MPU6050_ADDRESS);//MPU6050的地址
i2c_recv_ACK();
i2c_send_byte(regAddr);//操作MPU6050的哪个寄存器
i2c_recv_ACK();
i2c_send_byte(data);//发送数据
i2c_recv_ACK();
i2c_stop();
}
uint8_t MPU6050_ReadReg(uint8_t regAddr)
{
uint8_t data = 0;
i2c_start();
i2c_send_byte(MPU6050_ADDRESS);
i2c_recv_ACK();
i2c_send_byte(regAddr);
i2c_recv_ACK();
i2c_start();
/*START → 写地址 0xD0 → ACK → 寄存器地址 → ACK
→ 重复 START → 读地址 0xD1 → ACK → 读数据 → NACK → STOP*/
i2c_send_byte(MPU6050_ADDRESS | 0x1);
i2c_recv_ACK();
data = i2c_recv_byte();
i2c_send_ACK(1);
i2c_stop();
return data;
}
void MPU6050_init(void)//初始化 STM32 的 I2C 引脚,并且配置 MPU6050 内部的关键寄存器,
//让 MPU6050 从"休眠状态"进入"正常工作状态",并设定好测量范围、采样率等参数。
{
i2c_init();
MPU6050_WriteReg(MPU6050_PWR_MGMT_1, 0x01);
MPU6050_WriteReg(MPU6050_PWR_MGMT_2, 0x00);
MPU6050_WriteReg(MPU6050_SMPLRT_DIV, 0x09);
MPU6050_WriteReg(MPU6050_CONFIG, 0x06);
MPU6050_WriteReg(MPU6050_GYRO_CONFIG, 0x18);
MPU6050_WriteReg(MPU6050_ACCEL_CONFIG, 0x18);
}
uint8_t MPU6050_getId(void)//读取 MPU6050 的身份 ID 寄存器,用来确认芯片是不是 MPU6050,
//以及 I2C 通信是否正常。
{
return MPU6050_ReadReg(MPU6050_WHO_AM_I);
}
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;
DataH = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_H);
DataL = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_L);
*AccX = (DataH << 8) | DataL;
DataH = MPU6050_ReadReg(MPU6050_ACCEL_YOUT_H);
DataL = MPU6050_ReadReg(MPU6050_ACCEL_YOUT_L);
*AccY = (DataH << 8) | DataL;
DataH = MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_H);
DataL = MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_L);
*AccZ = (DataH << 8) | DataL;
DataH = MPU6050_ReadReg(MPU6050_GYRO_XOUT_H);
DataL = MPU6050_ReadReg(MPU6050_GYRO_XOUT_L);
*GyroX = (DataH << 8) | DataL;
DataH = MPU6050_ReadReg(MPU6050_GYRO_YOUT_H);
DataL = MPU6050_ReadReg(MPU6050_GYRO_YOUT_L);
*GyroY = (DataH << 8) | DataL;
DataH = MPU6050_ReadReg(MPU6050_GYRO_ZOUT_H);
DataL = MPU6050_ReadReg(MPU6050_GYRO_ZOUT_L);
*GyroZ = (DataH << 8) | DataL;
}
硬件
IIC外设简介

1. 硬件自动执行,减轻 CPU 负担
-
含义:软件 I2C 需要 CPU 一步步去翻转 GPIO、延时;而硬件 I2C 只需要你把数据丢进寄存器,剩下的时钟生成、起始/终止条件、ACK 收发,硬件电路会自动完成。
-
对你的意义:这正是解决你"快速运动时变成 0"的根本方法。CPU 不用在死循环里死等延时,时序不受中断打断,通信极其稳定。
2. 支持多主机模型 / 7位和10位地址 / SMBus
-
含义:这是 STM32 硬件 I2C 的标准功能。
-
对你的意义 :MPU6050 使用的是 7位地址(0x68),这部分你不需要管,知道硬件支持就行。
3. 支持不同的通讯速度(标准 100kHz,快速 400kHz)
-
含义:硬件 I2C 可以配置时钟频率。
-
对你的意义:MPU6050 完全支持 400kHz。你之前软件 I2C 大概是几十 kHz,换成硬件后把速度设成 400kHz,读取数据会快很多。
4. 支持 DMA
-
含义:可以直接让 DMA 搬运数据,CPU 完全解放。
-
对你的意义 :如果你以后想连续读 14 个字节(加速度、温度、角速度),开启 DMA 会让效率直接起飞。
5.:STM32F103C8T6 硬件I2C资源:I2C1、I2C2
-
含义:你的芯片有两个硬件 I2C 外设。
-
对你的意义:
-
I2C1 :默认引脚是 PB6 (SCL) 和 PB7 (SDA)。
-
I2C2 :默认引脚是 PB10 (SCL) 和 PB11 (SDA)。
-
IIC框图

一、 数据收发路径(图的右上部分)
这是你读写 MPU6050 数据时,硬件内部发生的事情。
-
DATA REGISTER(数据寄存器):你往这里写数据,或者从这里读数据。
-
数据移位寄存器:硬件自动把数据寄存器里的 8 位数据,一位一位地推出去到 SDA 线上(或者从 SDA 线上采回来拼成 8 位)。
-
数据控制:控制 SDA 引脚是输入还是输出。
-
比较器 + 自身地址寄存器:当 STM32 作为从机时,硬件会对比收到的地址是不是自己的;当你作为主机(现在的场景),这部分不需要你操心。
二、 时钟控制(图的左侧中间)
-
SCL 引脚:由时钟控制电路直接驱动。
-
时钟控制寄存器 (CCR):决定 SCL 的频率。
三、 控制逻辑与状态机(图的右下部分)
这是硬件 I2C 的心脏,也是你切换硬件时最需要适应的地方。
-
控制寄存器 (CR1 & CR2):你要通过它来配置 I2C 外设(使能、ACK应答、起始条件生成等)。
-
状态寄存器 (SR1 & SR2):这里存放着硬件当前的状态(比如"起始条件已发送"、"地址已发送"、"收到一个字节"等)。
-
控制逻辑电路:图中间的箭头显示,逻辑电路根据状态寄存器和控制寄存器的值,指挥整个 I2C 运转。
四、 高级功能(图的最下方)
-
中断:硬件完成一件事(比如收到一字节),可以触发中断告诉 CPU。
-
DMA请求与响应 :可以直接让 DMA 把数据从内存搬到
DATA REGISTER,或者从DATA REGISTER搬到内存。 -
SMBALERT:SMBus 协议的警报线(你暂时用不到)。
-
帧错误校验 (PEC):自动计算 CRC 校验(MPU6050 通信不需要)。
IIC基本结构

一、 右侧:物理引脚(SCL与SDA)
-
SCL:时钟线,单向输出。
-
SDA:数据线,双向(箭头一进一出)。
二、 上半部分:时钟控制器 -> GPIO -> SCL
-
时钟控制器:硬件内部自动产生时钟脉冲。
-
对应你的代码 :你在初始化时写的
I2C_InitStruct.I2C_ClockSpeed = 400000;就是配置这个控制器。 -
对比软件 I2C :你再也不用写
Delay_us(10)去手动翻转 PB10 了,硬件会自动产生精准的 400kHz 方波。
三、 下半部分(核心):数据控制器 -> 数据寄存器DR -> 移位寄存器 -> GPIO -> SDA
这是整个图最核心的地方,它解释了你写的代码和物理总线是怎么对接的:
-
数据控制器:代表 CPU 总线接口。
-
数据寄存器 DR :这就是你代码直接操作的地方。
-
当你调用
I2C_SendData(I2C2, 0xD0)时,CPU 实际上是把这个 0xD0 塞进了DR寄存器。 -
当你调用
data = I2C_ReceiveData(I2C2)时,你是把DR里面的字节读走。
-
-
移位寄存器
<<:这是硬件自动干活的灵魂。-
发送时:数据从
DR移到移位寄存器,硬件自动一位一位地(高位先发)推到 SDA 线上。 -
接收时:SDA 线上的一位一位进入移位寄存器,攒够 8 位后,自动丢给
DR。 -
对比软件 I2C :你之前写
for(i=0; i<8; i++),手动用0x80 >> i提取每一位去写 SDA。现在这个for循环和位运算,完全由硬件电路替你做了。
-
-
GPIO :硬件外设和物理引脚的桥梁(配置成复用开漏
GPIO_Mode_AF_OD)。
四、 右下角:开关控制
-
这代表了 I2C 的使能(
I2C_Cmd)、ACK应答使能/失能(I2C_AcknowledgeConfig),以及起始/停止条件的控制逻辑。 -
当你调用
I2C_GenerateSTART(I2C2, ENABLE)时,这个开关控制电路会捕捉到并指挥数据线拉低。
主机发送

你之前用软件 I2C 写 MPU6050_WriteReg 时,是自己一步步操控 GPIO、加延时。现在用硬件 I2C,你不再需要手动翻引脚,但你必须按照这张图里的"事件(Event)"顺序去查状态。
结合你的 MPU6050,这张图里的 7 位主发送序列(上图第一行),对应的正是你写寄存器 0xD0 -> 寄存器地址 -> 数据 的完整过程。
一、 主发送时序与事件(EV)全解析
图中第一行 7位主发送,完美对应你写 MPU6050 寄存器的三步走:
-
起始条件(S) -> 产生事件 EV5
-
图注说明:
EV5: SB=1(Start Bit 被置1)。 -
代码动作 :硬件帮你把 SDA 拉低产生了起始信号。你需要
while(!I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT));来等待SB标志位。确认起始发完后,你要把从机地址(0xD0)写入 DR 寄存器。
-
-
发送地址 + 接收应答(地址 -> A) -> 产生事件 EV6
-
图注说明:
EV6: ADDR=1(地址发送完毕/匹配)。 -
代码动作 :硬件自动把地址打出去并接收了从机的 ACK。
while(!I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED));等待ADDR标志。重点:要依次读取 SR1 和 SR2 寄存器来清除这个事件 。然后,准备发送寄存器地址(比如0x6B)。
-
-
发送数据(数据1 -> A) -> 产生事件 EV8 或 EV8_1
-
图注说明:
EV8: TxE=1(发送数据寄存器空)。硬件把数据移到移位寄存器后,DR 空出来,就会触发这个事件。 -
代码动作 :
while(!I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTING));等待TxE标志置位。确认空出来后,把你要写的寄存器地址(0x6B)写入 DR。 -
随后发数据(0x01)也是同样的逻辑,继续等
EV8。
-
-
发送完成,准备停止(数据N -> A -> P) -> 产生事件 EV8_2
-
图注说明:
EV8_2: TxE=1, BTF=1(字节传输完成)。此时不能再发数据了,要准备停机。 -
代码动作 :发完最后一个数据后,等待
BTF标志。while(!I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTED));,然后调用I2C_GenerateSTOP(I2C2, ENABLE);产生停止条件(P)。
-
主机接收

一、 主接收器传送序列图
1. 7位主接收
-
时序流:
S(起始条件) ->地址(7位地址+读位) ->A(应答) ->数据1->A->数据2->A-> ... ->数据N->NA(非应答) ->P(停止条件)。 -
事件对应: 整个过程触发了 EV5 -> EV6 -> EV7 (循环) -> EV7_1。
-
主要特点:整个流程比较简单,发送一次起始条件和地址后,直接开始连续接收数据。最后一个数据字节接收完毕后,主机必须发送非应答(NA)来告诉从机结束传输,然后发送停止条件(P)。
2. 10位主接收(下半部分)
-
时序流: 10位地址的接收分为两段。
-
首先,
S(起始) ->帧头(11110xx + 读写位,即写) ->A->地址(10位地址的低8位) ->A。 -
接着,发起
Sr(重复起始条件) ->帧头(11110xx + 读写位,即读) ->A->数据1->A-> ... ->数据N->NA->P。
-
-
事件对应: 触发 EV5 -> EV9 -> EV6 -> EV5 -> EV6 -> EV7 -> EV7_1。
-
主要特点:因为10位地址需要分两个字节发送,并且中间需要切换读写方向,所以必须使用重复起始条件(Sr)。
二、 事件标志与清除方法(说明部分)
这部分对于编写I2C中断服务函数(ISR)至关重要。EVx 事件通常在 ITEVFEN=1(中断事件使能)时产生。
图中各缩写含义:
-
S = Start(起始条件)
-
Sr = 重复的起始条件
-
P = Stop(停止条件)
-
A = 响应(ACK)
-
NA = 非响应(NACK)
-
EVx = 事件
具体事件解析:
-
EV5 (SB=1):
-
触发: 起始条件已发送。
-
清除方法: 读
SR1寄存器,然后将地址写入DR寄存器。 -
作用: 用于启动地址发送。
-
-
EV6 (ADDR=1):
-
触发: 地址发送完成,收到应答。
-
清除方法: 读
SR1然后读SR2。 -
特殊说明(重要): 在10位主接收模式 下,该事件(EV6)清除后,应设置
CR2寄存器的START=1,以发送重复起始条件(Sr)。
-
-
EV6_1:
- 说明: 这是一个没有对应事件标志 的特殊事件,只适用于接收1个字节 的情况(单字节接收)。必须在
EV6之后(即清除了ADDR之后)清除响应和停止条件的产生位(通常用于设置ACK=0和STOP=1)。
- 说明: 这是一个没有对应事件标志 的特殊事件,只适用于接收1个字节 的情况(单字节接收)。必须在
-
EV7 (RxNE=1):
-
触发: 接收数据寄存器非空(收到一个字节数据)。
-
清除方法: 读
DR寄存器。 -
作用: 接收常规数据。
-
-
EV7_1 (RxNE=1):
-
触发: 接收数据寄存器非空,且处于最后一个字节。
-
清除方法: 读
DR寄存器。 -
特殊说明: 在读DR清除事件的同时,需要设置
ACK=0和STOP请求。这用于结束最后一个字节的接收。
-
-
EV9 (ADDR=1):
-
触发: 在10位主接收模式下,接收到匹配的帧头后产生。
-
清除方法: 读
SR1,然后写入DR寄存器。 -
作用: 用于处理10位地址的头段接收
-
软件/硬件波形对比

一、 通信协议时序解析(两张图逻辑相同)
这是一个标准的 "先写寄存器地址,再重复起始条件读数据" 的流程(常用于读取传感器或EEPROM的数据):
-
S(Start):主机发起起始条件。 -
Send Byte: 0xD0:主机发送从机地址+写操作(0xD0二进制为1101 000 0,最后一位0表示写)。随后收到从机应答RA:0(Acknowledge,0表示应答)。 -
Send Byte: 0x19:主机发送要读取的寄存器地址0x19。收到从机应答RA:0。 -
Sr(Repeated Start) :主机发起重复起始条件。这正是上一张图中提到的10位接收模式或者寄存器读取时常见的关键操作,用于在不变更总线控制权的情况下切换读写方向。 -
Send Byte: 0xD1:主机再次发送从机地址+读操作(0xD1二进制为1101 000 1,最后一位1表示读)。收到从机应答RA:0。 -
Receive Byte: 0xAA:从机向主机发送数据0xAA。 -
SA:1:主机接收到最后一个字节后,发送非应答(NACK),告诉从机数据接收完毕。 -
P(Stop):主机发送停止条件,结束通信。
注:波形中SDA的数据变化都发生在SCL低电平期间,并在SCL高电平期间保持稳定,完全符合I2C规范。
二、 软件/硬件波形对比分析
这是本图的核心对比点:
1. 上半部分:软件模拟I2C波形
-
SCL(时钟线) :高低电平的占空比不太均匀,周期也有些许波动。这是因为软件I2C是通过CPU直接翻转GPIO引脚(如
GPIO_SetBits()和GPIO_ResetBits())产生的,容易受中断、指令周期和任务调度的影响。 -
SDA(数据线):边沿相对生硬,切换速率受限于GPIO驱动能力和软件执行速度。
2. 下半部分:硬件I2C波形
-
SCL(时钟线):时钟频率非常稳定,高低电平占空比非常匀称,波形边缘干净、平滑,没有明显的抖动。
-
SDA(数据线):数据变化和时钟同步非常精准,几乎是完美的矩形波。
-
整体观感:硬件I2C由专用的硬件外设(如STM32的I2C外设)自动控制时序,不需要CPU干预每一个时钟脉冲,因此通讯速率更高、更稳定、抗干扰能力更强,波形也远比软件模拟的漂亮。
总结: 软件I2C灵活性高(可任意映射引脚),但波形差、速度慢;硬件I2C波形完美、速度快,但引脚固定且配置相对复杂。在实际项目中,对时序要求高、速度快的场景强烈建议使用硬件I2C。
cpp
#include "stm32f10x.h"
#include "Delay.h"
#include "oled.h"
#include "MPU6050.h"
int main(void)
{
uint8_t data = 0;
int16_t AX, AY, AZ, GX, GY, GZ;
//初始化
OLED_Init();
MPU6050_init();
//
// data = MPU6050_getId();
// OLED_ShowString(1, 1, "ID: ");
// OLED_ShowHexNum(1, 5, data, 3);
while(1)
{
MPU6050_getData(&AX, &AY, &AZ, &GX, &GY, &GZ);
OLED_ShowSignedNum(1, 1, AX, 5);
OLED_ShowSignedNum(2, 1, AY, 5);
OLED_ShowSignedNum(3, 1, AZ, 5);
OLED_ShowSignedNum(1, 8, GX, 5);
OLED_ShowSignedNum(2, 8, GY, 5);
OLED_ShowSignedNum(3, 8, GZ, 5);
Delay_ms(100);
}
return 0;
}
cpp
#include "stm32f10x.h" // Device header
#define MPU6050_ADDRESS 0xD0
#define MPU6050_SMPLRT_DIV 0x19
#define MPU6050_CONFIG 0x1A
#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
#define MPU6050_PWR_MGMT_2 0x6C
#define MPU6050_WHO_AM_I 0x75
void MPU6050_WaitEvent(I2C_TypeDef* I2Cx, uint32_t I2C_EVENT)
{
uint32_t timeout;
timeout = 100000; // 1. 初始化超时计数器(大约10万次循环)
// 2. 循环检查事件是否发生
while(I2C_CheckEvent(I2Cx, I2C_EVENT) != SUCCESS)
{
timeout--; // 每次未发生,计数器减1
if (0 == timeout)
{
break; // 3. 如果减到0还没等到,强制退出循环(防止死循环卡死)
}
}
}
void MPU6050_WriteReg(uint8_t regAddr, uint8_t data)
{
//发送起始条件
I2C_GenerateSTART(I2C2, ENABLE);
MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT);
//发送7位地址
I2C_Send7bitAddress(I2C2, MPU6050_ADDRESS, I2C_Direction_Transmitter);
MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED);
//发送寄存器地址
I2C_SendData(I2C2, regAddr);
MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTING);
//发送数据
I2C_SendData(I2C2, data);
MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTED);
//发送停止位
I2C_GenerateSTOP(I2C2, ENABLE);
}
uint8_t MPU6050_ReadReg(uint8_t regAddr)
{
uint8_t data = 0;
//发送起始条件
I2C_GenerateSTART(I2C2, ENABLE);
MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT);
//发送7位地址
I2C_Send7bitAddress(I2C2, MPU6050_ADDRESS, I2C_Direction_Transmitter);
MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED);
//发送寄存器地址
I2C_SendData(I2C2, regAddr);
MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTED);
//发送起始条件
I2C_GenerateSTART(I2C2, ENABLE);
MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT);
//发送7位地址
I2C_Send7bitAddress(I2C2, MPU6050_ADDRESS, I2C_Direction_Receiver);
MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED);
I2C_AcknowledgeConfig(I2C2, DISABLE);
I2C_GenerateSTOP(I2C2, ENABLE);
MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_RECEIVED);
data = I2C_ReceiveData(I2C2);
I2C_AcknowledgeConfig(I2C2, ENABLE);
return data;
}
void MPU6050_init(void)
{
GPIO_InitTypeDef GPIO_InitStruct;
I2C_InitTypeDef I2C_InitStruct;
//I2C初始化
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C2, ENABLE);
//PB10 PB11
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_OD;
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_11 | GPIO_Pin_10;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStruct);
I2C_InitStruct.I2C_Mode = I2C_Mode_I2C;
I2C_InitStruct.I2C_ClockSpeed = 50000;
I2C_InitStruct.I2C_DutyCycle = I2C_DutyCycle_2;
I2C_InitStruct.I2C_Ack = I2C_Ack_Enable;
I2C_InitStruct.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;
I2C_InitStruct.I2C_OwnAddress1 = 0x00;
I2C_Init(I2C2, &I2C_InitStruct);
//使能I2C
I2C_Cmd(I2C2, ENABLE);
MPU6050_WriteReg(MPU6050_PWR_MGMT_1, 0x01);
MPU6050_WriteReg(MPU6050_PWR_MGMT_2, 0x00);
MPU6050_WriteReg(MPU6050_SMPLRT_DIV, 0x09);
MPU6050_WriteReg(MPU6050_CONFIG, 0x06);
MPU6050_WriteReg(MPU6050_GYRO_CONFIG, 0x18);
MPU6050_WriteReg(MPU6050_ACCEL_CONFIG, 0x18);
}
uint8_t MPU6050_getId(void)
{
return MPU6050_ReadReg(MPU6050_WHO_AM_I);
}
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;
DataH = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_H);
DataL = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_L);
*AccX = (DataH << 8) | DataL;
DataH = MPU6050_ReadReg(MPU6050_ACCEL_YOUT_H);
DataL = MPU6050_ReadReg(MPU6050_ACCEL_YOUT_L);
*AccY = (DataH << 8) | DataL;
DataH = MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_H);
DataL = MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_L);
*AccZ = (DataH << 8) | DataL;
DataH = MPU6050_ReadReg(MPU6050_GYRO_XOUT_H);
DataL = MPU6050_ReadReg(MPU6050_GYRO_XOUT_L);
*GyroX = (DataH << 8) | DataL;
DataH = MPU6050_ReadReg(MPU6050_GYRO_YOUT_H);
DataL = MPU6050_ReadReg(MPU6050_GYRO_YOUT_L);
*GyroY = (DataH << 8) | DataL;
DataH = MPU6050_ReadReg(MPU6050_GYRO_ZOUT_H);
DataL = MPU6050_ReadReg(MPU6050_GYRO_ZOUT_L);
*GyroZ = (DataH << 8) | DataL;
}
cpp
#ifndef __MPU6050_H
#define __MPU6050_H
void MPU6050_WaitEvent(I2C_TypeDef* I2Cx, uint32_t I2C_EVENT);
void MPU6050_WriteReg(uint8_t regAddr, uint8_t data);
uint8_t MPU6050_ReadReg(uint8_t regAddr);
void MPU6050_init(void);
uint8_t MPU6050_getId(void);
void MPU6050_getData(int16_t *AccX, int16_t *AccY, int16_t *AccZ, int16_t *GyroX, int16_t *GyroY, int16_t *GyroZ);
#endif
MPU6050_WriteReg(写寄存器硬件顺序)
发送起始条件(S) -> 等待EV5(起始已发出) -> 发送7位地址+写(0xD0) -> 等待EV6(地址发送完成并收到ACK) -> 发送寄存器地址(regAddr) -> 等待TXE(发送寄存器为空) -> 发送数据(data) -> 等待BTF(字节传输完成) -> 发送停止条件(P)
MPU6050_ReadReg(读寄存器硬件顺序)
发送起始条件(S) -> 等待EV5 -> 发送7位地址+写(0xD0) -> 等待EV6 -> 发送寄存器地址(regAddr) -> 等待BTF(字节传输完成) -> 发送重复起始条件(Sr) -> 等待EV5 -> 发送7位地址+读(0xD1) -> 等待EV6(接收模式就绪) -> 配置ACK=0(非应答) -> 发送停止条件(P) -> 等待EV7(接收数据寄存器非空) -> 读取数据(data) -> 恢复ACK=1 -> 返回data
MPU6050_init(初始化硬件顺序)
开启GPIOB时钟 -> 开启I2C2时钟 -> 配置PB10/PB11(复用开漏模式) -> 配置I2C2参数(50kHz、7位地址等) -> 使能I2C2 -> 【调用写寄存器流程】写PWR_MGMT_1(0x01) -> 【调用写寄存器流程】写PWR_MGMT_2(0x00) -> 【调用写寄存器流程】写SMPLRT_DIV(0x09) -> 【调用写寄存器流程】写CONFIG(0x06) -> 【调用写寄存器流程】写GYRO_CONFIG(0x18) -> 【调用写寄存器流程】写ACCEL_CONFIG(0x18)
MPU6050_getData(获取数据硬件顺序)
【调用读寄存器流程】读ACCEL_XOUT_H -> 【调用读寄存器流程】读ACCEL_XOUT_L -> 拼接得到AccX -> 【调用读寄存器流程】读ACCEL_YOUT_H -> 【调用读寄存器流程】读ACCEL_YOUT_L -> 拼接得到AccY -> 【调用读寄存器流程】读ACCEL_ZOUT_H -> 【调用读寄存器流程】读ACCEL_ZOUT_L -> 拼接得到AccZ -> 【调用读寄存器流程】读GYRO_XOUT_H -> 【调用读寄存器流程】读GYRO_XOUT_L -> 拼接得到GyroX -> 【调用读寄存器流程】读GYRO_YOUT_H -> 【调用读寄存器流程】读GYRO_YOUT_L -> 拼接得到GyroY -> 【调用读寄存器流程】读GYRO_ZOUT_H -> 【调用读寄存器流程】读GYRO_ZOUT_L -> 拼接得到GyroZ
MPU6050_getId(获取ID顺序)
【调用读寄存器流程】读WHO_AM_I(0x75) -> 返回ID值

