【江科大STM32学习笔记-10】I2C通信协议 - 10.1 软件I2C读写MPU6050

目录

[1 I2C 通信协议概述](#1 I2C 通信协议概述)

[1.1 硬件 I2C](#1.1 硬件 I2C)

[1.2 软件 I2C](#1.2 软件 I2C)

[2 I2C 物理层](#2 I2C 物理层)

[2.1 总线结构](#2.1 总线结构)

[2.2 开漏输出](#2.2 开漏输出)

[2.2.1 推挽输出的电气冲突](#2.2.1 推挽输出的电气冲突)

[2.2.2 开漏输出(Open-Drain)的工作原理](#2.2.2 开漏输出(Open-Drain)的工作原理)

[2.2.3 上拉电阻的作用](#2.2.3 上拉电阻的作用)

[2.2.4 线与(Wired-AND)特性](#2.2.4 线与(Wired-AND)特性)

[3 I2C 协议层](#3 I2C 协议层)

[3.1 数据有效性规定](#3.1 数据有效性规定)

[3.2 起始和停止信号](#3.2 起始和停止信号)

[3.3 应答响应](#3.3 应答响应)

[3.4 总线的寻址方式](#3.4 总线的寻址方式)

[3.4.1 地址匹配机制](#3.4.1 地址匹配机制)

[3.4.2 7 位寻址格式](#3.4.2 7 位寻址格式)

[3.4.3 10 位寻址](#3.4.3 10 位寻址)

[3.5 数据传输](#3.5 数据传输)

[3.5.1 写数据(主机发送至从机)](#3.5.1 写数据(主机发送至从机))

[3.5.2 读数据(主机由从机接收)](#3.5.2 读数据(主机由从机接收))

[3.5.3 读写复合传输(方向切换)](#3.5.3 读写复合传输(方向切换))

[4 MPU6050 传感器介绍](#4 MPU6050 传感器介绍)

[4.1 MPU6050 概述](#4.1 MPU6050 概述)

[4.2 MPU6050 内部结构框图](#4.2 MPU6050 内部结构框图)

[4.2.1 传感与数字信号处理链路](#4.2.1 传感与数字信号处理链路)

[4.2.2 核心控制与数据缓存机制](#4.2.2 核心控制与数据缓存机制)

[4.2.3 通信总线与扩展接口架构](#4.2.3 通信总线与扩展接口架构)

[4.2.4 电源与内部时钟管理](#4.2.4 电源与内部时钟管理)

[4.3 MPU6050 工作原理](#4.3 MPU6050 工作原理)

[4.3.1 MEMS 传感器测量原理](#4.3.1 MEMS 传感器测量原理)

[4.3.2 三轴加速度计测量原理](#4.3.2 三轴加速度计测量原理)

[4.3.3 三轴陀螺仪测量原理](#4.3.3 三轴陀螺仪测量原理)

[4.3.4 姿态表示与姿态角](#4.3.4 姿态表示与姿态角)

[4.3.4.1 地理坐标系](#4.3.4.1 地理坐标系)

[4.3.4.2 载体坐标系](#4.3.4.2 载体坐标系)

[4.3.4.3 姿态角](#4.3.4.3 姿态角)

[4.3.5 传感器数据融合](#4.3.5 传感器数据融合)

[4.3.5.1 常见姿态融合算法](#4.3.5.1 常见姿态融合算法)

[4.3.5.2 MPU6050 的数字运动处理器(DMP)](#4.3.5.2 MPU6050 的数字运动处理器(DMP))

[4.3.5.3 DMP 的输出数据形式](#4.3.5.3 DMP 的输出数据形式)

[4.3.5.4 工程应用优势](#4.3.5.4 工程应用优势)

[4.3.6 MPU6050 内部信号处理流程](#4.3.6 MPU6050 内部信号处理流程)

[4.3.7 ADC 量化与量程换算](#4.3.7 ADC 量化与量程换算)

[5 软件 I2C 驱动与 MPU6050 接口实现](#5 软件 I2C 驱动与 MPU6050 接口实现)

[5.1 软件 I2C 协议层实现](#5.1 软件 I2C 协议层实现)

[5.1.1 GPIO 开漏输出初始化](#5.1.1 GPIO 开漏输出初始化)

[5.1.2 GPIO 接口函数设计](#5.1.2 GPIO 接口函数设计)

[5.1.3 起始与终止信号的生成](#5.1.3 起始与终止信号的生成)

[5.1.4 字节的串行发送与接收](#5.1.4 字节的串行发送与接收)

[5.1.5 ACK 应答机制](#5.1.5 ACK 应答机制)

[5.2 MPU6050 设备层:地址寻址与寄存器读写](#5.2 MPU6050 设备层:地址寻址与寄存器读写)

[5.2.1 寄存器写入流程](#5.2.1 寄存器写入流程)

[5.2.2 寄存器读取流程](#5.2.2 寄存器读取流程)

[5.3 MPU6050 初始化配置](#5.3 MPU6050 初始化配置)

[5.4 MPU6050 数据读取](#5.4 MPU6050 数据读取)

[5.4.1 数据寄存器结构](#5.4.1 数据寄存器结构)

[5.4.2 六轴数据读取函数](#5.4.2 六轴数据读取函数)

[6 相关元器件简介](#6 相关元器件简介)

[6.1 MPU-6050 角度姿态传感器模块](#6.1 MPU-6050 角度姿态传感器模块)

[6.1.1 器件简介](#6.1.1 器件简介)

[6.1.2 接口描述](#6.1.2 接口描述)

[6.1.3 模块结构](#6.1.3 模块结构)

[6.1.4 工作原理](#6.1.4 工作原理)

[7. 本章节实验](#7. 本章节实验)

[7.1 软件I2C读写MPU6050](#7.1 软件I2C读写MPU6050)

[7.1.1 实验目标](#7.1.1 实验目标)

[7.1.2 硬件设计](#7.1.2 硬件设计)

[7.1.3 软件设计](#7.1.3 软件设计)

[7.1.4 实验现象](#7.1.4 实验现象)


1 I2C 通信协议概述

I2C(Inter-Integrated Circuit)是一种同步串行通信总线,由飞利浦公司提出,广泛应用于微控制器与传感器、存储器及各种外围模块之间的数据交换。I2C 采用主从式通信结构,仅需少量信号线即可实现多个器件之间的互联。

在 I2C 总线中,通信双方依赖同步时钟机制。通信过程中的时钟信号由主机产生,从机在该时钟节拍下完成数据的发送与接收。

在 STM32 等微控制器中,I2C 通信的实现方式分为硬件 I2C软件 I2C 两种。

1.1 硬件 I2C

硬件 I2C 是指通过 MCU 内部集成的 I2C 外设模块完成通信。该外设在硬件层面实现了 I2C 协议所需的时序控制、边界信号生成、地址匹配及应答检测。开发者通过配置相关寄存器即可驱动总线。硬件 I2C 具有运行效率高、时序稳定、CPU 占用率低的特点,适用于通信性能要求较高的场景。其局限性在于需占用特定的硬件引脚,且外设资源数量固定。

1.2 软件 I2C

软件 I2C(Bit-banging I2C)是指利用普通 GPIO 引脚,由软件代码按照 I2C 协议的时序规范,精确控制引脚的电平翻转,从而模拟出完整的 I2C 通信波形。程序需主动控制 GPIO 输出高低电平以生成时钟信号(SCL)、数据信号(SDA)以及起始、停止和应答等协议时序。

软件 I2C 的主要特点:

  • 引脚灵活:可分配任意 GPIO 引脚作为通信线。
  • 移植性强:底层逻辑完全由软件实现,不依赖特定硬件外设,适用于绝大多数 MCU。
  • 资源消耗:通信过程需软件实时维持时序,CPU 占用率相对较高。

在教学实验与引脚资源受限的工程设计中,软件 I2C 是一种实用的替代方案。本节后续内容基于软件 I2C 驱动 MPU6050 传感器的实现逻辑展开。


2 I2C 物理层

I2C 协议的物理层规定了总线的 电气特性硬件连接方式,包括信号线结构、设备连接方式以及总线电平的实现机制。

2.1 总线结构

I2C 总线通过两根信号线实现设备之间的通信:SCL(Serial Clock Line)SDA(Serial Data Line) 。其中,SCL 时钟信号始终由主机产生并控制。无论是写操作(主机发送数据)还是读操作(从机发送数据),通信的时序节奏都由主机提供的 SCL 时钟决定。从机仅在需要延迟数据处理时,才会通过 时钟拉伸 (Clock Stretching) 机制暂时拉低 SCL,以延缓时钟节拍,但不会主动产生时钟信号。

信号线名称 功能定义
SCL(Serial Clock Line) 串行时钟线,由主机产生并提供通信同步时钟
SDA(Serial Data Line) 串行数据线,用于主机与从机之间的双向数据传输

I2C 通信设备通常采用 单主多从(Single-Master Multi-Slave) 的连接方式。所有设备都会并联连接到 SCL 和 SDA 两条信号线上,从而形成一种 多设备共享的总线结构。如下图所示:

在这种结构中,各类设备的角色如下:

  • 主机(Master):通常为 MCU,负责产生时钟信号并发起通信请求;
  • 从机(Slave):各种外设器件(如传感器、存储器或显示模块),根据主机指令进行数据收发。

为了保证总线能够稳定地恢复到高电平状态,通常需要在 SCL 与 SDA 两条信号线上各连接一个上拉电阻,将总线连接到电源电压。

I2C 总线能够实现多设备共享通信,依赖于一种特殊的输出结构------开漏输出(Open-Drain)。接下来将对这种结构进行详细说明。

2.2 开漏输出

I2C 总线是一种多设备共享的通信拓扑结构,所有主机与从机设备均并联于 SCL 和 SDA 两条信号线上。为了理解 I2C 协议为何强制规范使用开漏输出,需要先剖析传统推挽输出在此类拓扑中面临的底层硬件缺陷

2.2.1 推挽输出的电气冲突

在推挽输出结构中,端口内部同时包含上拉驱动管和下拉驱动管,使其具备主动输出高电平或低电平的能力:

  • 上拉驱动管导通: 端口主动输出高电平(直连 VCC)。

  • 下拉驱动管导通: 端口主动输出低电平(直连 GND)。

当推挽结构被应用于多设备共享总线时,若不同设备同时驱动线路且输出状态不一致,将引发严重的硬件级驱动冲突。 例如:

  • 设备 A 输出高电平
  • 设备 B 输出低电平

此时,设备A试图将线路连接到 VCC,而设备B则试图将线路连接到 GND。VCC 与 GND 之间将通过共用的总线网络形成一条低阻抗的直通路径:

cpp 复制代码
电流流向:VCC → 设备 A 上拉管 → 总线 → 设备 B 下拉管 → GND

这种现象在工程上被称为总线冲突(Bus Contention)。它不仅会导致总线电平逻辑严重畸变,更会产生巨大的瞬时短路电流,造成器件异常发热,严重时将永久性击穿损坏硅片结构。因此,在多设备并联的总线系统中,绝对禁止采用推挽输出作为信号驱动拓扑。

2.2.2 开漏输出(Open-Drain)的工作原理

为从硬件物理层面彻底消除总线冲突的隐患,I2C 协议规范要求所有连接至 SDA 和 SCL 总线的设备引脚必须采用**开漏输出(Open-Drain)**结构。如下图所示:

从电路拓扑分析,开漏输出与推挽输出的核心差异在于:开漏结构移除了内部的上拉驱动管,仅保留下拉驱动管。 这一结构的精简,彻底重塑了端口的输出行为模式,其具体逻辑映射如下表所示:

内部逻辑输入 硬件执行动作 引脚物理状态 外部总线表现
输出逻辑 0 下拉驱动管导通 与 GND 连通 主动拉低(低电平)
输出逻辑 1 下拉驱动管截止 节点悬空 高阻态(High-Z)/ 释放总线

基于上述底层机制,得出核心结论:开漏端口仅具备主动输出低电平的能力,而绝对无法主动输出高电平。 当通信逻辑需要输出高电平时,硬件层面的实质操作是关闭下拉管,使引脚呈现高阻抗状态(即**"释放总线"**)。

这种非对称的驱动特性完美契合了 I2C 的多机通信需求:由于任何设备都无法主动将总线强制拉至 VCC,从而彻底杜绝了不同设备间 VCC 与 GND 直通的可能性,从硬件底层确保了多设备共享总线的电气安全性。

2.2.3 上拉电阻的作用

由于开漏结构无法主动输出高电平,如果总线上没有额外的电路提供拉升能力,那么当所有设备都释放总线时,线路将处于 悬空状态(Floating),电平无法确定。

因此,在 I2C 总线中必须为 SCL 和 SDA 各连接一个上拉电阻,将线路拉至电源电压。典型连接方式如下:

上拉电阻的作用是:当所有设备均释放总线时,将总线恢复为高电平。因此总线电平的形成机制为:

总线状态 电平来源
任意设备拉低 设备主动驱动
所有设备释放 上拉电阻提供

在实际应用中,上拉电阻的阻值通常在 2 kΩ ~ 10 kΩ 范围内,常见取值约为 4.7 kΩ。其具体选择需要综合考虑:

  • 总线电容
  • 通信速度
  • 上升沿时间要求

2.2.4 线与(Wired-AND)特性

当多个开漏设备通过同一组上拉电阻连接到总线时,会在电气层面形成一种特殊的逻辑关系------线与(Wired-AND)特性。

其基本规则是:

  • 只要任意设备将总线拉低,总线电平立即变为低电平
  • 只有当所有设备均释放总线时,总线才恢复为高电平

例如:

设备A 设备B 总线结果
高阻 高阻 高电平
低电平 高阻 低电平
高阻 低电平 低电平
低电平 低电平 低电平

可以看到,在这种结构中:

低电平具有优先权,而高电平是被动形成的状态。

线与特性带来了两个重要优势:

  • **避免驱动冲突:**由于所有设备都只能主动拉低总线,不会出现一个设备输出高电平、另一个设备输出低电平的直接对抗,从而避免了硬件短路问题。

  • 支持多设备共享总线: 设备在驱动总线的同时,可以实时读取总线电平,从而判断线路状态。这一特性正是 I2C ACK 应答机制多主机仲裁机制 能够实现的硬件基础。

因此,开漏结构、上拉电阻以及线与特性 共同构成了 I2C 总线能够通过两根信号线连接多个设备的核心电气基础。


3 I2C 协议层

I2C 的协议层定义了通信过程中各类控制信号和传输规则,主要包括:数据有效性规定、起始和停止信号、应答响应、总线寻址方式以及数据传输流程。这些规则共同保证了 I2C 总线在多设备共享场景下仍能稳定、可控地工作。

3.1 数据有效性规定

I2C 总线使用 SDA 信号线传输数据,使用 SCL 信号线进行数据同步。在数据传输过程中,协议对数据的有效性有着严格的电平状态要求:

  • 数据有效(采样阶段):当 SCL 时钟线为高电平时,SDA 数据线上的电平状态必须保持稳定。此时的 SDA 高电平表示数据 1,低电平表示数据 0。

  • 数据变化(准备阶段):只有当 SCL 时钟线为低电平时,SDA 数据线上的高低电平状态才允许发生切换,为下一次数据传输做准备。如下图所示:

也就是说,数据的采样与判定发生在 SCL 高电平期间,而 SDA 的切换通常安排在 SCL 低电平期间。这种设计使得数据传输能够严格与时钟同步,避免因电平抖动造成误判。

此外,I2C 以 字节为单位进行传输。每个字节长度固定为 8 位,但总线一次传输的字节数没有固定限制,可以根据实际需要连续传输多个字节。

3.2 起始和停止信号

I2C 通信的开始与结束由特定的电平跳变信号标识。这两种特殊状态均由主机(Master)产生:

  • 起始信号(S, Start):当 SCL 线维持高电平时,SDA 线由高电平向低电平切换,标志着一次通信的起始。起始信号发出后,总线进入被占用状态,连接在总线上的所有从机开始等待地址广播。

  • 停止信号(P, Stop):当 SCL 线维持高电平时,SDA 线由低电平向高电平切换,标志着本次通信的终止。停止信号发出后,总线恢复为空闲状态。如下图所示:

3.3 应答响应

I2C 的数据和地址传输均带有确认机制。数据传输时,按照从高位(MSB)到低位(LSB)的顺序发送。每当发送端传输完 8 位数据后,必须在第 9 个时钟周期紧跟 1 位响应位(即一帧共 9 个时钟周期)。

在第 9 个时钟周期,数据发送端会释放对 SDA 线的控制权,交由数据接收端控制。响应信号分为以下两种:

响应类型 SDA 电平 含义
ACK(应答) 低电平 接收方希望继续接收后续数据
NACK(非应答) 高电平 接收方不再继续接收,通信应结束
  • **ACK 的含义:**当接收方确认已经成功接收当前字节,并希望发送方继续传输时,会在应答位期间将 SDA 拉低,形成 ACK。发送方检测到 ACK 后,继续发送下一个字节。

  • **NACK 的含义:**当接收方不再希望继续接收数据时,会在应答位期间让 SDA 保持高电平,形成 NACK。发送方检测到 NACK 后,通常会结束当前传输流程,并在适当时机发出停止信号。

注意:图里的 SDA(主机) 和 SDA(从机) 分开画只是为了说明 谁在控制总线,I2C 物理上只有一根 SDA 线,所有设备都是 开漏输出 + 上拉电阻。


3.4 总线的寻址方式

I2C 总线上的每个从机设备都具有唯一的设备地址。主机在发起通信时,需要首先发送设备地址以确定通信对象,这一过程称为寻址(Addressing) 。由于 I2C 总线是一种多设备共享总线结构,所有从机都连接在同一条 SDA 和 SCL 总线上,因此必须通过地址机制区分不同设备。

I2C 协议支持两种地址格式:

  • 7 位地址
  • 10 位地址

其中,7 位地址是最常见的寻址方式,在大多数实际应用中均采用该模式,而 10 位地址主要用于设备数量较多或需要更大地址空间的场景。

3.4.1 地址匹配机制

当主机发出起始信号及寻址字节后,总线上的所有从机设备都会接收该字节,并将其中的前 7 位地址与自身的硬件预设地址进行比对:

  • 地址匹配:匹配成功的从机判定自己被主机寻址,并在第 9 个时钟周期拉低 SDA 线反馈应答信号(ACK),随后参与后续的通信。

  • 地址不匹配:地址不同的从机将忽略后续的所有数据信号,保持高阻态直至下一次起始信号出现。

这种"地址广播、单设备响应"的机制确保了在多设备并联的总线上,数据交换具有确定的指向性。

3.4.2 7 位寻址格式

在 7 位寻址模式下,起始信号(Start)产生后,主机发送的第一个字节称为寻址字节,其结构如图所示,由 7 位设备地址和 1 位方向控制位组成:

该字节的位段定义如下:

(1)D7 ~ D1(从机地址位):代表目标从机的 7 位物理地址。这 7 位数据决定了总线上最多可挂载 2\^7=128 个理论地址(扣除协议保留地址后,实际可用数量略少)。

(2)D0(传输方向位 :用于规定后续数据的流向。

  • 置 0:表示主机控制 SDA 线,向从机写入数据。

  • 置 1:表示主机释放 SDA 控制权,由从机驱动 SDA 线,从从机读取数据。

因此,在读操作过程中:

  • 主机负责产生 SCL 时钟
  • 从机负责输出 SDA 数据

3.4.3 10 位寻址

为了支持更多设备,I2C 协议还定义了 10 位地址模式 。与 7 位地址相比,10 位地址能够提供 1024 个地址空间 ,适用于大型系统或设备数量较多的应用场景。10 位寻址方式与 7 位寻址方式是完全兼容的,两种类型的设备可以连接在同一条 I2C 总线上共同工作。

在 10 位地址模式下,寻址过程需要发送 两个地址字节,其结构如图所示:

(1)第一个字节

cpp 复制代码
1 1 1 1 0 A9 A8 R/W
含义
11110 10 位地址标识前缀
A9 A8 地址高两位
R/W 读写方向

(2)第二个字节

cpp 复制代码
A7 A6 A5 A4 A3 A2 A1 A0

表示地址的低 8 位。

因此,完整的 10 位地址由:A9 A8 A7 A6 A5 A4 A3 A2 A1 A0 共 10 位组成。


3.5 数据传输

I2C 总线上传送的数据序列由寻址字节 (从机地址 + 方向位)和后续的若干个数据字节 组成。全过程始终由主机发起起始信号 S 开始,并由主机发起终止信号 P 结束。

在 I2C 底层硬件协议层面,寻址字节后的所有内容统称为"数据"。但在实际的工程应用中,从机设备会自行定义这些数据字节的功能逻辑:

  • 紧随从机地址后的第一个数据字节(即整个通信序列中的第二个字节):通常被解析为从机内部的**寄存器地址(Reg Address)**或指令码。

  • 后续字节:则根据具体设备协议,定义为实际写入的数据、存储器地址的高/低位或配置参数等。

结合实际工程应用,I2C 的通信过程主要细分为以下三种标准的时序模型:

3.5.1 指定地址写数据

在该模式下,数据单向由主机流向从机。在实际应用中,这种时序最常用于向指定的从机寄存器写入配置数据。

  1. 寻址阶段:主机产生起始信号 S,随后发送 7 位从机地址,并将方向位置为 0(即写操作)。

  2. 地址应答:从机拉低 SDA 线反馈应答信号 ACK(A)。

  3. 发送寄存器地址:主机发送 8 位的寄存器地址(在底层协议中这属于第一个数据字节),从机接收后再次反馈 ACK,并将其内部地址指针指向该寄存器。

  4. 写入数据:主机继续发送 8 位实际数据,从机接收并写入对应的寄存器,随后反馈 ACK。若有连续写入需求,从机内部地址指针会自动递增。

  5. 结束阶段:数据发送完成后,主机发起终止信号 P,释放总线。

下图展示了真实的"指定地址写"逻辑分析仪波形。主机寻址从机(0xD0),向其内部的 0x19 寄存器写入了数据 0xAA。

3.5.2当前地址读数据

在该模式下,数据由从机流向主机。这种时序通常用于读取从机内部当前地址指针所指向的数据(例如连续读取 FIFO 缓存区)。

  1. 寻址阶段:主机产生起始信号 S,发送从机地址并将方向位置为 1(即读操作)。

  2. 地址应答与交接:从机反馈应答信号 ACK。随后主机释放 SDA 线,数据驱动权移交给从机。

  3. 数据发送:从机按照主机的时钟脉冲,输出当前地址指针所指寄存器中的 8 位数据。

  4. 接收应答:主机接收完一个字节后,若需继续读取则反馈 ACK;若需停止读取,则反馈非应答信号 NACK(/A)。

  5. 结束阶段:主机在发出 NACK后,紧接着发起终止信号 P,强制结束从机发送。

下图展示了"当前地址读"的波形。主机以读模式寻址从机(0xD1),从机立刻输出了数据 0x0F,随后主机反馈 NACK(高电平)并停止通信。在此过程中,没有指定寄存器地址的步骤。

3.5.3指定地址读数据

在大多数场景下,我们不能盲目读取当前地址,而是需要明确读取某一个特定寄存器的数据(例如读取特定的加速度传感器输出轴)。这需要将"写"与"读"结合,采用包含重复起始信号(Sr)的复合时序。

  1. 写阶段(设置地址指针):主机以写模式(方向位为 0)发起通信,发送目标寄存器地址,并接收从机应答。此步骤仅为了改变从机的内部地址指针,不写入实际数据。

  2. 方向切换:主机在收到寄存器地址的应答后,不产生停止信号 P,而是直接再次发起重复起始信号 Sr。

  3. 读阶段(获取数据):主机重新广播从机地址,并将方向位置为 1。从机响应后,开始输出刚刚指定的寄存器中的数据。

  4. 结束阶段:主机接收数据后反馈 NACK,并产生终止信号 P。

下图完整展示了"指定地址读"的全过程。主机先进行写操作寻址(0xD0),指定目标寄存器为 0x19;随后直接发起 Sr 信号,进行读操作寻址(0xD1),最终成功读取到该寄存器内的数据 0xAA。


4 MPU6050 传感器介绍

4.1 MPU6050 概述

在嵌入式系统与运动控制系统中,经常需要获取物体在三维空间中的运动状态信息,例如设备是否发生倾斜、旋转速度是多少,以及当前姿态方向如何变化等。这类信息通常由 惯性传感器(Inertial Sensor) 提供。惯性传感器通过测量物体在空间中的 线性加速度角速度,从而推断出物体的运动状态及姿态变化。

MPU6050 是 InvenSense 公司推出的一款常见的 六轴惯性测量单元(IMU,Inertial Measurement Unit)。所谓"六轴",是指芯片内部集成了两类三轴传感器:

传感器类型 测量物理量
三轴加速度计(Accelerometer) 测量 X、Y、Z 三个方向的线性加速度
三轴陀螺仪(Gyroscope) 测量绕 X、Y、Z 三个坐标轴旋转的角速度

通过同时测量三轴加速度与三轴角速度,可以获得物体在空间中的基本运动信息。在实际工程中,通过对这些原始数据进行进一步计算与融合,还可以得到物体的姿态角度,例如 俯仰角(Pitch)横滚角(Roll)偏航角(Yaw)

MPU6050 的主要特点包括:

  • 集成三轴加速度计与三轴陀螺仪
  • 支持多种量程配置
  • 内置数字低通滤波器(DLPF)
  • 内置 1024 字节 FIFO 缓冲区
  • 支持最高 400 kHz 的 I2C 通信速率
  • 内置数字运动处理器(DMP)

由于其体积小、成本低、接口简单,MPU6050 在教学实验、姿态检测系统以及小型运动控制设备中得到了广泛应用。

为了实现上述功能,MPU6050 在芯片内部集成了完整的传感检测与数字处理结构。下面将通过内部结构框图,对其主要模块组成进行说明。

4.2 MPU6050 内部结构框图

MPU6050 的内部结构框图如下所示:

MPU6050 在单一芯片内部完成了传感检测、模数转换、数字信号处理以及通信接口的设计。根据数据流向与硬件功能划分,其内部结构主要由以下四大核心子系统构成:

4.2.1 传感与数字信号处理链路

该链路是 MPU6050 的核心测量路径,严格遵循"模拟传感 -> 数字化 -> 数字信号调理 -> 寄存器存储"的硬件逻辑:

  • MEMS 传感器(X/Y/Z Accel & Gyro): 包含三轴 MEMS 加速度计与三轴 MEMS 陀螺仪,分别负责感应三维空间中的线性加速度与角速度,并输出微弱的模拟电压信号。

  • 16 位模数转换器(16-bit ADC): 芯片为每一路传感器信号(3路加速度、3路角速度)均配备了独立的 16 位 ADC。模拟信号在此处直接进行数字化采样,这保证了多通道数据的同步性。

  • 信号调理模块(Signal Conditioning): 数字化后的原始数据进入信号调理阶段。该模块内部包含数字低通滤波器(DLPF)与灵敏度比例因子补偿电路,用于滤除高频机械噪声并进行出厂校准参数的修正。

  • 传感器数据寄存器(Sensor Registers): 经过滤波与缩放的最终测量数据被写入对应的只读寄存器中,等待外部微控制器(MCU)读取。

4.2.2 核心控制与数据缓存机制

为了降低外部 MCU 的运算与实时轮询压力,MPU6050 内部集成了专用的硬件加速与缓存机制:

  • 数字运动处理器(DMP, Digital Motion Processor): 内部固化的硬件算法引擎。DMP 能够直接在片上读取原始传感器数据,并执行复杂的姿态解算(如四元数输出)。该机制将密集的运动处理计算需求从系统主控处理器上卸载,大幅节约了 MCU 的算力。

  • 先进先出缓冲区(FIFO): 片上集成了一个 1024 字节的 FIFO 数据缓存队列。该机制允许 MPU6050 在高采样率下连续采集并暂存多组数据,外部 MCU 可以通过 I2C 接口以突发模式(Burst Mode)分批次读取。这种架构降低了总线通信频率,有利于系统进入低功耗休眠模式。

  • 配置寄存器(Config Registers): 存储由 MCU 写入的控制指令,用于动态配置设备的满量程范围、采样率、时钟源及中断使能等运行参数。

4.2.3 通信总线与扩展接口架构

MPU6050 采用了主/从双 I2C 总线架构,支持灵活的系统级传感器扩展:

  • 从机通信接口(Slave I2C Interface): 对外提供标准的 I2C 从设备接口,信号线为 SCL (串行时钟)和 SDA(串行数据),支持最高 400kHz 的快速模式。MCU 作为总线主机,通过该接口完成寄存器配置与数据读取。

    • 设备地址配置: MPU6050 的 7 位 I2C 基地址由 AD0 引脚的电平状态决定。当 AD0 接地(GND)时,设备地址为 0x68 ;当 AD0 接高电平(VDD/VLOGIC)时,设备地址为 0x69。(注:实际通信中,该 7 位地址需由 MCU 附加最低位的读/写控制位)。
  • 主机扩展接口(Master I2C Interface): 引脚为 AUX_CLAUX_DA。MPU6050 可作为 I2C 主机,通过该接口挂载第三方数字传感器(如三轴磁力计 HMC5883L)。采集到的外部传感器数据会统一整合至 MPU6050 的寄存器或 FIFO 中,从而向 MCU 提供完整的九轴惯性测量数据(MotionFusion)。

  • 旁路多路复用器(Bypass Mux): 这是一个硬件开关逻辑。当旁路模式使能时,主 I2C 总线(SCL/SDA)与辅助 I2C 总线(AUX_CL/AUX_DA)将在芯片内部被直接短接。此时,主控 MCU 可以绕过 MPU6050,直接与挂载在辅助总线上的外部传感器进行通信与调试。

4.2.4 电源与内部时钟管理

  • VDD 与 VLOGIC 电源引脚: VDD 是芯片的模拟主电源输入(典型范围 2.375 V 至 3.46 V)。VLOGIC 则专门用于为数字 I/O 接口提供参考电压,最低支持 1.8 V 逻辑电平。这种双电源轨设计使得 MPU6050 能够直接与低电压等级的微控制器(如 1.8V 系统)进行通信,而无需外加电平转换芯片。若系统整体逻辑电平为 3.3 V,通常将 VLOGIC 直接与 VDD 短接即可。

  • 内部时钟(Clock & Charge Pump): 提供系统驱动时序,并在工作温度范围内保证 ±1% 的高频稳定性,以维持内部逻辑电路与数字接口的正常运转。


4.3 MPU6050 工作原理

MPU6050 的核心功能是测量物体在三维空间中的运动状态,例如设备的倾斜、旋转以及姿态变化。为了理解 MPU6050 的工作方式,需要从 惯性传感器测量原理、姿态表示方法以及芯片内部信号处理流程三个方面进行分析。

MPU6050 内部主要通过 三轴加速度计与三轴陀螺仪获取运动信息,并在芯片内部完成信号调理、模数转换和数字滤波等处理,最终将数据存储在寄存器中供 MCU 读取。


4.3.1 MEMS 传感器测量原理

MPU6050 内部的加速度计与陀螺仪均采用 MEMS(Micro-Electro-Mechanical Systems)微机电系统技术制造。MEMS 技术通过半导体微加工工艺,在硅片上制造出微米级的机械结构,使传感器能够在极小体积内实现对物理运动的检测。

在 MPU6050 中,惯性测量的基本过程可以概括为:

通过检测这种微小电信号变化,就可以得到物体的运动信息。


4.3.2 三轴加速度计测量原理

MPU6050 内部集成了一组 三轴 MEMS 加速度计 ,用于测量设备在 X、Y、Z 三个方向上的线性加速度

MEMS 加速度计通常采用 微机械结构 + 电容检测方式实现加速度测量,其基本结构主要包括:

  • 微型质量块(Proof Mass)
  • 弹性支撑结构(Springs)
  • 差分电容检测电极(C1、C2)
  • 固定电极(Fixed Plates)

在 MEMS 加速度计中,质量块、弹性结构和电极均通过 MEMS 微加工工艺 在硅片上制造,其尺寸通常只有 微米级(μm)。其结构示意如图所示。

在该结构中,质量块通过弹性支撑结构悬挂在硅基底上,两侧布置有固定电极,与质量块上的活动电极共同形成差分电容结构。

当传感器受到外部 加速度(Acceleration) 作用时,质量块会在弹性结构的支撑下产生极其微小的位移,从而改变两侧电容之间的间距:

  • 一侧电容 C1 减小
  • 另一侧电容 C2 增大

这种电容变化具有 差分特性 。芯片内部的检测电路通过测量 C1 与 C2 的差值变化,即可计算出对应方向上的加速度大小。如图所示:

同时,由于地球表面始终存在 重力加速度 g(约 9.8 m/s²) ,因此即使设备处于静止状态,加速度计仍然能够检测到 重力在各轴方向上的分量

当设备发生倾斜时,重力在 X、Y、Z 三个轴方向上的分量也会随之变化。通过分析这些分量关系,可以估算设备相对于重力方向的姿态角度。因此,在静态或低速运动情况下,加速度计通常可以用于估算设备的:

  • 俯仰角(Pitch)
  • 横滚角(Roll)

需要注意的是,加速度计 无法测量偏航角(Yaw) 。因为当设备绕 重力方向轴线旋转 时,重力在各轴方向上的分量不会发生变化,因此无法通过加速度数据判断旋转角度。

此外,在设备发生 快速运动或剧烈振动 时,加速度计测量到的实际上是:

cpp 复制代码
总加速度 = 重力加速度 + 运动加速度

此时加速度数据会受到运动加速度的干扰,从而影响姿态计算的准确性。因此,在实际应用中,通常需要 结合陀螺仪数据进行融合计算(如互补滤波或卡尔曼滤波),以获得更加稳定可靠的姿态信息。


4.3.3 三轴陀螺仪测量原理

MPU6050 内部同时集成了 三轴陀螺仪 ,用于测量设备绕三个坐标轴的 角速度(Angular Velocity)

MEMS 陀螺仪的工作原理基于 科里奥利效应(Coriolis Effect)。在芯片内部存在一个持续振动的微型结构,当设备发生旋转时,振动结构会受到科里奥利力作用,从而在垂直方向产生微小位移。该位移同样会引起电容变化,检测电路通过测量这种变化即可计算出角速度。如图所示:

陀螺仪输出的是 角速度值 ,其单位通常为 °/s (度每秒)。通过对角速度进行时间积分,可以得到设备的旋转角度

因此,陀螺仪能够较好地反映设备在短时间内的姿态变化过程。

但由于传感器存在 零偏误差(Bias) ,在长时间积分过程中会产生 累计漂移误差。这种误差会随着时间逐渐累积,导致计算得到的姿态角逐渐偏离真实值。


4.3.4 姿态表示与姿态角

在三维空间中描述物体的姿态,需要建立 参考坐标系。通常情况下,会定义两个坐标系:

4.3.4.1 地理坐标系

地理坐标系是相对于地球表面某一点为原点建立的参考坐标系,其 Z 轴通常与重力方向一致,X、Y 轴与当地经纬线相切。如图所示:

4.3.4.2 载体坐标系

载体坐标系固定在设备本体上,其三个轴的方向由设备结构决定,例如:

  • X 轴:设备左右方向
  • Y 轴:设备前后方向
  • Z 轴:设备上下方向
4.3.4.3 姿态角

当设备发生旋转时,载体坐标系相对于地理坐标系的方向会发生变化,两者之间的角度关系即表示设备的空间姿态。

在工程中,通常使用 欧拉角(Euler Angles) 来描述这种姿态关系。欧拉角由三个旋转角组成:

姿态角 旋转轴 物理意义
Pitch(俯仰角) 绕 X 轴旋转 设备前后倾斜
Roll(横滚角) 绕 Y 轴旋转 设备左右倾斜
Yaw(偏航角) 绕 Z 轴旋转 水平面内的航向

需要注意的是,MPU6050 并不会直接输出这些姿态角 。芯片输出的数据仅为 加速度和角速度,姿态角需要通过后续的数据处理或姿态解算算法计算得到。


4.3.5 传感器数据融合

在姿态测量系统中,单独使用某一种传感器通常难以获得稳定且长期可靠的姿态信息。因此,惯性测量系统通常会 同时使用加速度计与陀螺仪,并通过算法对两者的数据进行融合处理,以充分利用各自的优势并弥补其局限。

从测量原理上看,两类传感器的特性存在明显差异:

传感器 优点 局限
加速度计 能够感知重力方向,可用于确定空间姿态的绝对参考,不存在积分累计误差 在运动状态下会受到线性加速度与震动干扰,测量结果容易波动
陀螺仪 动态响应速度快,能够精确测量旋转角速度,适合描述短时间姿态变化 姿态角需要通过角速度积分获得,长期运行会产生零偏漂移与累计误差

从上述特性可以看出:

  • 加速度计适合提供长期稳定的姿态参考(低频信息)
  • 陀螺仪适合描述快速姿态变化(高频信息)

因此,在工程实践中通常需要通过 姿态融合(Sensor Fusion)算法 ,将两类传感器的数据进行综合处理,从而获得 既稳定又具有良好动态响应的姿态结果

4.3.5.1 常见姿态融合算法

在惯性导航与姿态测量系统中,常见的数据融合算法主要包括:

(1)互补滤波(Complementary Filter)

互补滤波是一种计算量较小、实现简单的姿态融合方法。其基本思想是:

  • 利用陀螺仪数据描述短时间姿态变化(高频部分)
  • 利用加速度计数据校正长期漂移(低频部分)

两者通过滤波器进行权重叠加,从而获得稳定的姿态估计结果。

(2)卡尔曼滤波(Kalman Filter)

卡尔曼滤波是一种基于状态估计理论的最优滤波算法。通过建立系统状态模型和噪声模型,对多源传感器数据进行概率估计,从而得到最优姿态解算结果。该方法精度较高,但计算复杂度也相对较大。

(3)四元数姿态解算(Quaternion-based Attitude Estimation)

在实际姿态解算中,常采用 四元数(Quaternion) 表示空间姿态。与传统欧拉角相比,四元数具有以下优势:

  • 计算效率更高
  • 数值稳定性更好
  • 能够避免欧拉角在特定姿态下出现的 万向节死锁(Gimbal Lock) 问题

因此,在许多惯性测量系统中,姿态解算算法内部通常采用 四元数进行姿态表示与更新


4.3.5.2 MPU6050 的数字运动处理器(DMP)

为了降低主控微控制器(MCU)的计算负担,MPU6050 在芯片内部集成了一套专用的硬件处理单元------数字运动处理器(Digital Motion Processor,DMP)

DMP 可以看作是一种 专门用于惯性数据处理的嵌入式协处理器,其核心功能是:

  • 在芯片内部直接读取 加速度计与陀螺仪原始数据
  • 执行姿态融合算法
  • 输出处理后的姿态信息

其基本工作机制如下:

  1. MEMS 传感器采集加速度与角速度数据
  2. 数据经 ADC 转换并进入数字处理单元
  3. DMP 调用 InvenSense 提供的内部固件执行姿态解算算法
  4. 输出处理结果并存入寄存器或 FIFO 缓冲区
  5. MCU 通过 I2C 接口读取结果

4.3.5.3 DMP 的输出数据形式

DMP 完成姿态计算后,通常不会直接输出 欧拉角(Pitch / Roll / Yaw) ,而是输出 四元数数据 。在 MPU6050 中,该四元数通常以 Q30 定点格式表示。

四元数一般写作:

cpp 复制代码
q = (q0,q1,q2,q3)

其中:

  • q0 为实部
  • q1,q2,q3 为虚部

MCU 读取四元数后,只需通过标准转换公式即可计算出:

  • Pitch(俯仰角)
  • Roll(横滚角)
  • Yaw(偏航角)

4.3.5.4 工程应用优势

通过在芯片内部集成 DMP,MPU6050 在工程应用中具有明显优势:

  • 降低 MCU 计算负担
    姿态融合算法通常涉及大量矩阵运算与滤波计算,由 DMP 完成后,主控 MCU 仅需读取结果。
  • 提高系统实时性
    数据处理在芯片内部完成,可减少 MCU 运算延迟。
  • 缩短开发周期
    开发者无需从零实现复杂的姿态融合算法。

4.3.6 MPU6050 内部信号处理流程

根据前面介绍的内部结构框图,MPU6050 内部的数据处理流程大致如下:

具体过程如下:

  1. 运动感知 (MEMS Sensing): 内部微机械结构感知运动,输出微弱模拟信号。

  2. 模拟前端放大 (AFE Processing): 对微弱的模拟电荷/电压进行初级放大。

  3. 16位 ADC 转换 (ADC Conversion): 将连续的模拟量离散化,转换为原始数字信号。

  4. DLPF 滤波与数字校准 (DLPF & Calibration): 硬件数字低通滤波器滤除高频振动噪声,并完成初步的内部数据校准。

  5. 存储至 数据寄存器/FIFO (Store Data): 经过处理的有效数据被更新至只读寄存器中(同时触发硬件中断如 INT 引脚信号)。

  6. MCU 通过 I2C 读取 (I2C Read): 主控芯片(如 STM32)发起总线时序,读取寄存器内的 16 位补码数据。

  7. 物理量转换 (Physical Scale Conversion): 在 MCU 的软件代码中,将原始 ADC 值除以量程分辨率(如 16.4 LSB/°/s),转换为实际的加速度 (g) 或角速度 (°/s)。

  8. 结束 / 数据就绪


4.3.7 ADC 量化与量程换算

MCU 通过 I2C 读取到的传感器数据,本质上是 ADC 输出的数字量。这些数值需要根据传感器的量程参数,才能转换为实际的物理量。

MPU6050 的 ADC 分辨率为 16 位,数据寄存器输出的值采用有符号二进制补码表示,数值范围为 -32768 至 +32767。此原始数据需依据配置的满量程范围(Full Scale Range, FSR)与对应的灵敏度比例因子进行换算。

(1)陀螺仪量程(由寄存器 0x1B 控制)

满量程设定(°/s) 灵敏度比例因子 (LSB/(°/s)) 寄存器配置值
±250 131 LSB/(°/s) 0x00
±500 65.5 LSB/(°/s) 0x08
±1000 32.8 LSB/(°/s) 0x10
±2000 16.4 LSB/(°/s) 0x18

角速度计算公式:

(2)加速度计量程(由寄存器 0x1C 控制)

满量程设定(g) 灵敏度比例因子 (LSB/g) 寄存器配置值
±2 16384 0x00
±4 8192 0x08
±8 4096 0x10
±16 2048 0x18

加速度计算公式:

需要注意的是,MPU6050 的加速度和角速度数据在寄存器中以 高字节在前、低字节在后 的形式存储。MCU 读取数据时需要先读取高字节,再读取低字节,并组合为 16 位有符号整数,随后才能进行量程换算。

(3)温度数据换算

MPU6050 内部包含数字温度传感器,可通过读取 0x41 与 0x42 寄存器获取。其换算物理温度的公式为:

其中,regval 为 16 位温度寄存器的读取值。


5 软件 I2C 驱动与 MPU6050 接口实现

在前述章节已介绍 I2C 总线物理架构与 MPU6050 传感器工作原理的基础之上,本章将结合具体的程序代码,详述如何通过微控制器的通用输入输出端口(GPIO)模拟 I2C 时序,并最终实现对 MPU6050 的寄存器配置与传感器数据提取。

从工程软件结构划分,本实验的底层驱动与业务逻辑分为三个独立层级:

架构层级 功能定位 对应源文件
应用层 调度底层接口获取原始数据,并执行数据转换与屏幕显示逻辑。 main.c
设备驱动层 封装 MPU6050 特有的寄存器地址寻址、初始化序列及多字节数据提取逻辑。 MPU6050.c / MPU6050_Reg.h
I2C 协议层 直接操控 GPIO 引脚电平,构建标准的 I2C 通信时序单元(起始、终止、收发、应答)。 MyI2C.c

这种分层解耦设计消除了上层业务代码对底层硬件平台的强依赖,显著提高了代码的可移植性与后期维护效率。

5.1 软件 I2C 协议层实现

由于某些硬件平台资源限制或引脚布局需求,开发中常采用普通 GPIO 引脚模拟 I2C 时序信号,此方法称为软件模拟 I2C(Bit-Banging)。本实验选用微控制器的 PB10 作为串行时钟线(SCL),PB11 作为串行数据线(SDA)。


5.1.1 GPIO 开漏输出初始化

为满足 I2C 总线多节点"线与"的物理约束,SCL 与 SDA 引脚必须配置为开漏输出模式(Open-Drain)。在开漏架构下,

  • 向引脚输出寄存器写入 0 将激活内部下拉晶体管,强制拉低总线;
  • 写入 1 将关闭下拉晶体管,引脚呈现高阻态,此时总线电平由外部上拉电阻拉升至高电平。
cpp 复制代码
/**
  * 函    数:I2C 引脚底层初始化
  * 说    明:配置 PB10 (SCL) 与 PB11 (SDA) 为通用开漏输出模式,并置为高电平空闲态
  */
void MyI2C_Init(void)
{
    /* 1. 开启 GPIOB 端口外设时钟 */
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
    
    /* 2. 配置引脚工作模式 */
    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);
    
    /* 3. 输出默认高电平,释放总线,建立空闲状态 */
    GPIO_SetBits(GPIOB, GPIO_Pin_10 | GPIO_Pin_11);
}

5.1.2 GPIO 接口函数设计

为规范后续复杂时序单元的构建,需先将单根信号线的读写操作封装为独立函数。在每次电平跳变后,必须插入适量的微秒级延时,以确保输出方波的周期宽度满足目标器件手册中定义的建立时间(Setup Time)与保持时间(Hold Time)要求。

函数 作用
MyI2C_W_SCL 控制 SCL 时钟线
MyI2C_W_SDA 控制 SDA 数据线
MyI2C_R_SDA 读取 SDA 电平
cpp 复制代码
/**
  * 函    数:控制 SCL 时钟线输出电平
  * 参    数:BitValue 目标电平状态(0 或 1)
  */
void MyI2C_W_SCL(uint8_t BitValue)
{
    GPIO_WriteBit(GPIOB, GPIO_Pin_10, (BitAction)BitValue);
    Delay_us(10);    // 延时 10us,控制总线频率不超过器件支持上限
}

/**
  * 函    数:控制 SDA 数据线输出电平
  * 参    数:BitValue 目标电平状态(0 或 1)
  */
void MyI2C_W_SDA(uint8_t BitValue)
{
    GPIO_WriteBit(GPIOB, GPIO_Pin_11, (BitAction)BitValue);
    Delay_us(10);
}

/**
  * 函    数:读取 SDA 数据线当前输入电平
  * 返 回 值:引脚当前逻辑电平(0 或 1)
  */
uint8_t MyI2C_R_SDA(void)
{
    uint8_t BitValue;
    BitValue = GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11);
    Delay_us(10);
    return BitValue;
}

5.1.3 起始与终止信号的生成

I2C 通信以 起始信号(Start) 开始,以 停止信号(Stop) 结束。

  • 起始信号(Start):通信发起标志。在 SCL 维持高电平时,控制 SDA 产生由高至低的下降沿跳变,表示产生起始信号。

  • 终止信号(Stop):通信结束的标志。在 SCL 维持高电平期间,控制 SDA 产生由低至高的上升沿跳变。此时,SDA 与 SCL 两根信号线均被释放并依靠外部上拉电阻保持在高电平状态,标志着总线回归空闲。

cpp 复制代码
/**
  * 函    数:产生 I2C 起始信号
  */
void MyI2C_Start(void)
{
    MyI2C_W_SDA(1);         // 确保 SDA 初始状态为释放态
    MyI2C_W_SCL(1);         // 确保 SCL 初始状态为释放态
    MyI2C_W_SDA(0);         // 核心动作:在 SCL 高电平期间拉低 SDA
    MyI2C_W_SCL(0);         // 钳住总线,为后续发送数据位做准备
}

/**
  * 函    数:产生 I2C 终止信号
  */
void MyI2C_Stop(void)
{
    MyI2C_W_SDA(0);         // 确保 SDA 初始状态为拉低态
    MyI2C_W_SCL(1);         // 释放 SCL 至高电平
    MyI2C_W_SDA(1);         // 核心动作:在 SCL 高电平期间释放 SDA 产生上升沿
}

5.1.4 字节的串行发送与接收

I2C 以 字节(8 位)为单位进行数据传输 ,并遵循 高位先行(MSB First) 的规则。

发送逻辑 :主机利用掩码逐位提取待发字节的比特值,在 SCL 低电平时更新 SDA 电平,随后拉高 SCL 完成数据位锁存。此处引入 !! 逻辑双取反操作,旨在将掩码提取出的非零宏观数值(如 0x80)严格转换为布尔值 1,以匹配标准外设库 BitAction 枚举类型的输入规范。

cpp 复制代码
/**
  * 函    数:I2C 串行发送单字节数据
  * 参    数:Byte 待发送的 8 位无符号数据
  */
void MyI2C_SendByte(uint8_t Byte)
{
    uint8_t i;
    for (i = 0; i < 8; i++)                 
    {
        /* 1. 提取第(7-i)位数据,通过双取反(!!)规整为 0 或 1 后输出至 SDA */
        MyI2C_W_SDA(!!(Byte & (0x80 >> i)));
        /* 2. 释放 SCL,触发从机侧的数据采样事件 */
        MyI2C_W_SCL(1);                     
        /* 3. 拉低 SCL,结束当前位周期,允许下一次数据变更 */
        MyI2C_W_SCL(0);                     
    }
}

接收逻辑:主机在接收前必须主动释放 SDA 控制权。随后生成时钟方波,在 SCL 高电平的稳定期内,读取总线输入状态并按位存入本地变量。通过循环读取 8 次即可获得一个完整字节。

cpp 复制代码
/**
  * 函    数:I2C 串行接收单字节数据
  * 返 回 值:从总线上重组的 8 位无符号数据
  */
uint8_t MyI2C_ReceiveByte(void)
{
    uint8_t i, Byte = 0x00;                 // 接收容器必须初始化清零
    
    MyI2C_W_SDA(1);                         // 主动写 1 释放总线,交由从机驱动 SDA
    for (i = 0; i < 8; i++)                 
    {
        MyI2C_W_SCL(1);                     // 释放 SCL 提供读取窗口
        if (MyI2C_R_SDA())                  // 检测 SDA 当前瞬态电平
        {
            Byte |= (0x80 >> i);            // 若为高,则将 Byte 对应位掩码置 1
        }
        MyI2C_W_SCL(0);                     // 拉低 SCL 推进至下一位
    }
    return Byte;
}

5.1.5 ACK 应答机制

在 I2C 通信中,每发送一个字节后都需要进行 应答(ACK)确认。此时,发送方必须释放总线,接收方需在第 9 个时钟周期内将 SDA 拉低以确认接收(ACK)。若未拉低,总线由于上拉电阻作用保持高电平,即被判定为非应答(NACK),通常表示数据链末端或接收异常。

应答规则如下:

应答位 含义
0 ACK,应答成功
1 NACK,无应答
cpp 复制代码
/**
  * 函    数:主机向从机发送应答位
  * 参    数:AckBit 0表示应答(ACK),1表示非应答(NACK)
  */
void MyI2C_SendAck(uint8_t AckBit)
{
    MyI2C_W_SDA(AckBit);                    // 提前将应答电平放置于总线
    MyI2C_W_SCL(1);                         // 供从机采样
    MyI2C_W_SCL(0);                         // 结束应答周期
}

/**
  * 函    数:主机读取从机的应答位反馈
  * 返 回 值:0表示收到应答(ACK),1表示收到非应答(NACK)
  */
uint8_t MyI2C_ReceiveAck(void)
{
    uint8_t AckBit;
    MyI2C_W_SDA(1);                         // 主机释放数据线
    MyI2C_W_SCL(1);                         // 拉高时钟探测从机反馈
    AckBit = MyI2C_R_SDA();                 // 捕获电平状态
    MyI2C_W_SCL(0);                         
    return AckBit;
}

5.2 MPU6050 设备层:地址寻址与寄存器读写

建立基础协议函数库后,即可针对 MPU6050 芯片的具体规范编写逻辑。芯片引脚 AD0 决定其 7 位物理基地址。在本实验硬件连线中,AD0 默认接地,对应基地址为 0x68。转换为 I2C 的 8 位寻址字节时需左移一位,最低位作为读写位:

  • 写地址(0x68 << 1) | 0 = 0xD0

  • 读地址(0x68 << 1) | 1 = 0xD1

5.2.1 寄存器写入流程

向特定寄存器写值的时序属于典型的"指定地址写"。流程依次为:起始信号 ---> 发送设备写地址 ---> 发送目标寄存器指针 ---> 写入数据内容 ---> 终止信号。

cpp 复制代码
#define MPU6050_ADDRESS 0xD0                // 定义写基地址宏

/**
  * 函    数:向 MPU6050 指定内部寄存器写入单字节数据
  * 参    数:RegAddress 目标寄存器地址
  * 参    数:Data 要写入的具体数值
  */
void MPU6050_WriteReg(uint8_t RegAddress, uint8_t Data)
{
    MyI2C_Start();
    MyI2C_SendByte(MPU6050_ADDRESS);        // 寻址设备并下达写命令
    MyI2C_ReceiveAck();
    
    MyI2C_SendByte(RegAddress);             // 设定内部寄存器地址指针
    MyI2C_ReceiveAck();
    
    MyI2C_SendByte(Data);                   // 载入待写数据
    MyI2C_ReceiveAck();
    MyI2C_Stop();
}

5.2.2 寄存器读取流程

读取寄存器数据必须采用复合操作格式。主机需首先发起一次"伪写"流程,向从机输送目标寄存器地址(改变内部指针);随后,不发送 Stop 信号,而是直接重发 Start 信号(即重复起始),重新下达读指令,最后反转总线方向收取数据。

cpp 复制代码
/**
  * 函    数:从 MPU6050 指定内部寄存器读取单字节数据
  * 参    数:RegAddress 目标寄存器地址
  * 返 回 值:提取出的数据内容
  */
uint8_t MPU6050_ReadReg(uint8_t RegAddress)
{
    uint8_t Data;
    
    /* 1. 伪写阶段:调整芯片内部地址指针 */
    MyI2C_Start();
    MyI2C_SendByte(MPU6050_ADDRESS);
    MyI2C_ReceiveAck();
    MyI2C_SendByte(RegAddress);
    MyI2C_ReceiveAck();
    
    /* 2. 读取阶段:重设总线方向为接收 */
    MyI2C_Start();                          // 重发起始条件 (Repeated Start)
    MyI2C_SendByte(MPU6050_ADDRESS | 0x01); // 寻址设备并下达读命令 (0xD1)
    MyI2C_ReceiveAck();
    
    /* 3. 数据提取与会话截断 */
    Data = MyI2C_ReceiveByte();             // 接收目标数据
    MyI2C_SendAck(1);                       // 回复 NACK,强制从机停止后续输出
    MyI2C_Stop();                           // 释放总线
    
    return Data;
}

5.3 MPU6050 初始化配置

MPU6050 在上电后默认处于 低功耗睡眠模式 。在该状态下,芯片内部的各个传感器模块均处于关闭状态,仅保留 I2C 通信接口 以响应外部访问。因此,在正式使用传感器之前,必须通过寄存器配置对器件进行初始化。

初始化的目的在于使芯片进入正常工作状态,并根据应用需求设置合适的工作参数。通常需要完成以下几项配置:

  • 唤醒设备并启动内部传感器模块
  • 选择合适的内部时钟源
  • 设置系统采样频率
  • 配置传感器的测量量程
  • 设置数字低通滤波等信号处理参数

完成上述初始化配置后,MPU6050 将进入正常工作状态,并按照设定的采样频率持续更新各传感器的测量数据。

其主要配置寄存器如下:

寄存器 地址 配置值 功能
PWR_MGMT_1 0x6B 0x01 退出睡眠
PWR_MGMT_2 0x6C 0x00 启用所有轴
SMPLRT_DIV 0x19 0x09 设置采样率
CONFIG 0x1A 0x06 配置低通滤波
GYRO_CONFIG 0x1B 0x18 ±2000°/s
ACCEL_CONFIG 0x1C 0x18 ±16g
cpp 复制代码
/**
  * 函    数:MPU6050 初始化配置
  * 说    明:建立底层 I2C 环境并按序写入关键配置寄存器
  */
void MPU6050_Init(void)
{
    MyI2C_Init();                           // 底层 GPIO 端口初始化
    
    /* 解除芯片睡眠,将时钟源切换为高精度的 X 轴陀螺仪 PLL */
    MPU6050_WriteReg(MPU6050_PWR_MGMT_1, 0x01);
    
    /* 电源管理2配置,确保 6 个轴的测量单元均处于活跃状态 */
    MPU6050_WriteReg(MPU6050_PWR_MGMT_2, 0x00);
    
    /* 设置采样频率分频系数。采样率 = 1kHz / (1 + 9) = 100Hz */
    MPU6050_WriteReg(MPU6050_SMPLRT_DIV, 0x09);
    
    /* 启用数字低通滤波器(DLPF),滤除高频机械噪声 */
    MPU6050_WriteReg(MPU6050_CONFIG, 0x06);
    
    /* 配置陀螺仪满量程范围为最大的 ±2000°/s */
    MPU6050_WriteReg(MPU6050_GYRO_CONFIG, 0x18);
    
    /* 配置加速度计满量程范围为最大的 ±16g */
    MPU6050_WriteReg(MPU6050_ACCEL_CONFIG, 0x18);
}

5.4 MPU6050 数据读取

MPU6050 内部集成了 3 轴加速度计与 3 轴陀螺仪,共需读取六个通道的传感器数据。由于其内部模数转换器(ADC)输出为 16 位精度,而 I2C 总线的单次传输宽度以及寄存器空间仅为 8 位,因此每个维度的物理量需跨越两个连续的寄存器存储。

5.4.1 数据寄存器结构

传感器将 16 位数据的"高 8 位"与"低 8 位"分别存放在地址相邻的两个寄存器中。在驱动程序中,必须严格按照硬件手册定义的地址序列进行读取,以确保数据的完整性。

传感器轴向 高字节寄存器 (High) 低字节寄存器 (Low)
加速度 X 轴 (AccX) 0x3B (ACCEL_XOUT_H) 0x3C (ACCEL_XOUT_L)
加速度 Y 轴 (AccY) 0x3D (ACCEL_YOUT_H) 0x3E (ACCEL_YOUT_L)
加速度 Z 轴 (AccZ) 0x3F (ACCEL_ZOUT_H) 0x40 (ACCEL_ZOUT_L)
陀螺仪 X 轴 (GyroX) 0x43 (GYRO_XOUT_H) 0x44 (GYRO_XOUT_L)
陀螺仪 Y 轴 (GyroY) 0x45 (GYRO_YOUT_H) 0x46 (GYRO_YOUT_L)
陀螺仪 Z 轴 (GyroZ) 0x47 (GYRO_ZOUT_H) 0x48 (GYRO_ZOUT_L)

5.4.2 六轴数据读取函数

由于单次 I2C 读取仅能获得 8 位局部数据,驱动程序需要通过逻辑运算还原真实的传感器数值,并将其整合输出。

1. 拼接原理与补码还原

获取高、低字节后,通过位移运算进行合成。首先将高 8 位数据左移 8 位(<< 8),使其占据 16 位变量的高位空间,随后利用按位或(|)运算将低 8 位填入低位空间。

由于 MPU6050 的原始输出采用有符号补码格式,其数值范围为 -3276832767。在代码实现中,拼接后的结果必须存储在 int16_t(有符号 16 位整型)变量中,利用 C 语言的数据类型特性自动完成从补码到实际正负数值的映射。

2. 六轴聚合提取函数的封装

受限于 C 语言函数单一返回值的限制,驱动程序采用**指针传递(输出参数)**的方案。这种方式允许函数直接修改主程序中变量地址对应的内容,从而实现在一次函数调用中同步更新六个轴的传感器数据。

cpp 复制代码
/**
  * 函    数:获取 MPU6050 六轴原始传感器数据
  * 参    数:AccX...GyroZ 均为指向 16 位有符号整型变量的指针,用于存储返回的数据
  * 实现逻辑:依次读取每个轴的高低字节寄存器,完成拼接后通过指针写入外部变量
  */
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; // 定义临时变量,用于存储读取的高、低 8 位原始字节

    /* --- 加速度计数据提取 --- */
    // 读取 X 轴加速度
    DataH = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_H);  // 获取高 8 位
    DataL = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_L);  // 获取低 8 位
    *AccX = (DataH << 8) | DataL;                   // 数据拼接并写入指针指向的内存

    // 读取 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;
}

3. 应用层调用与数据呈现

main.c 的业务逻辑中,需预先定义 16 位整型变量。在调用 MPU6050_GetData 时,对变量使用取地址符 &。这样,底层驱动获取的原始数据会直接填充到对应的变量内存中,随后即可由 OLED 显示函数展示。

cpp 复制代码
/* 定义主程序变量,用于存放六个轴的 16 位有符号原始数据 */
int16_t AX, AY, AZ, GX, GY, GZ;

/* 在主循环 while(1) 中持续调用,通过传入变量地址实现数据的实时同步更新 */
MPU6050_GetData(&AX, &AY, &AZ, &GX, &GY, &GZ);

/* 将获取到的数据通过 OLED 屏幕进行显示 */
OLED_ShowSignedNum(2, 1, AX, 5); // 在指定位置显示有符号十进制数

通过分层提取与指针封装,程序将复杂的底层寄存器访问抽象为简洁的数据获取接口,完成了从字节流到物理量数值的转换聚合。


6 相关元器件简介

6.1 MPU-6050 角度姿态传感器模块

为实现对四轴飞行器、平衡小车等载体的空间姿态精确监测与控制,可采用GY-521六自由度(6DOF)姿态传感器模块。该模块基于MPU-6050芯片设计,具有集成度高、精度优良的特点,可稳定输出三轴加速度和三轴角速度数据。

6.1.1 器件简介

MPU-6050 是由 InvenSense 公司推出的整合性 6 轴运动处理组件。芯片内部高度集成了 3 轴微机电系统(MEMS)陀螺仪、3 轴 MEMS 加速度计以及一个独立的数字运动处理器(DMP, Digital Motion Processor)。实物如图所示:

相较于传统的分立式传感器方案,MPU-6050 的核心优势在于其消除了加速度计与陀螺仪轴间的交叉敏感度,显著降低了封装与安装过程带来的空间轴向误差。器件具备如下显著特征:

  • 高精度模数转换: 内置 16 位 ADC(模数转换器),可输出高分辨率的 16 位数字量。

  • 可编程测量范围: 陀螺仪的满量程范围支持跨度配置(±250、±500、±1000、±2000°/s),加速度计的满量程范围同样支持动态设定(±2g、±4g、±8g、±16g)。

  • 硬件加速解算: 自带 DMP 硬件引擎,可直接输出融合演算后的旋转矩阵、四元数或欧拉角数据,极大减轻了主控 MCU 的运算负荷。

  • 辅助接口扩展: 具备独立的第二 I2C 接口(AUX_DA / AUX_CL),支持外接磁力计等从设备,从而构建完整的 9 轴姿态融合系统。

  • 数据缓存机制: 内置 1024 字节的 FIFO(先入先出)缓冲区,有助于降低主控系统高频轮询带来的功耗开销。

6.1.2 接口描述

结合 GY-521 模块的硬件原理图与实物布局,模块在 MPU-6050 裸片基础上外围配置了电源管理与电平匹配电路,对外引出 8 个标准间距(2.54mm)引脚,其电气连接规范如下:

  • VCC / GND(供电系统): 模块板载低压差线性稳压器(3.3V LDO),支持 3V 至 5V 的宽电压输入。输入的 VCC 经 LDO 降压并由滤波电容平滑后,为 MPU-6050 提供稳定的 3.3V 工作电压。同时板载一颗 LED 用于电源状态指示。

  • SCL / SDA(主 I2C 通信接口): 模块与主控 MCU 之间的数据通信链路。采用标准 I2C 串行协议。模块内部已在 SCL 和 SDA 信号线上分别并联了 4.7KΩ 的上拉电阻连接至 3.3V 轨道,确保空闲状态下总线电平稳定,外部 MCU 无需重复配置上拉。

  • XDA / XCL(辅助 I2C 主接口): 此为 MPU-6050 的第二 I2C 接口(对应芯片原厂引脚 AUX_DA 与 AUX_CL)。该接口配置为 I2C 主机模式(Master I2C),专用于直连外部从设备(如三轴磁力计 HMC5883L 等)。通过该旁路复用器(Bypass Mux),MPU-6050 可以直接读取外部传感器数据并暂存内部寄存器,从而构建完整的 9 轴空间数据采集系统,无需主控 MCU 额外占用总线资源。

  • **AD0(从机地址配置引脚):**AD0 引脚决定了传感器在 I2C 总线上的从机地址(7位格式,不含读写位)。模块内部已通过 4.7KΩ 电阻将 AD0 下拉至 GND,因此,器件物理地址为 0x68。若将 AD0 接入高电平(VCC),则器件地址切换为 0x69

  • 中断输出 (INT): 可编程中断引脚,用于向主控单元指示数据就绪(Data Ready)、FIFO 溢出或特定手势动作的触发状态。

6.1.3 模块结构

GY-521 模块在物理形态上是一块高集成度的印刷电路板(PCB)。为了保障 MPU-6050 裸片的稳定运行并简化主控微控制器的硬件外围设计,该模块将核心传感器与必要的辅助电路进行了系统性整合。从电路功能层级划分,该模块的硬件结构主要由以下四个单元构成:

  • 核心传感单元: 即 MPU-6050 芯片本体,作为整个模块的数据处理中枢,负责多自由度空间运动状态的感知、高精度模数转换与内部姿态解算。

  • 电源管理单元: 包含一颗低压差线性稳压器(LDO),负责将外部输入的 3V~5V 宽范围供电稳压降至标称的 3.3V,为传感器核心逻辑与总线上拉网络提供纯净的电源。此外,电源输入与输出端配置了旁路滤波电容组(如 10μF 与 0.1μF 电容并联),用于有效滤除电源轨道上的高频纹波噪声,确保 ADC 采样的精确度。

  • 总线上拉网络: 针对 I2C 协议的开漏输出(Open-Drain)物理层特性,模块在主 I2C 接口(SCL、SDA)与辅助 I2C 接口(XCL、XDA)的信号走线上,均硬件预置了 4.7KΩ 的上拉电阻。此设计直接构建了总线空闲时的默认高电平状态,使得外部 MCU 在接入时无需再次搭建上拉电路。

  • 配置与状态指示单元: 包含用于设备寻址的下拉电阻网络(将 AD0 引脚默认下拉至逻辑低电平以设定 0x68 地址),以及一条由 LED 二极管与 1KΩ 限流电阻串联组成的状态指示支路,用于直观反馈模块的电源接通状态。

6.1.4 工作原理

MPU-6050 内部的数据流转严格遵循标准工程逻辑。在获取载体物理姿态的过程中,其底层信号处理链路表现为以下时序步骤:

  1. 运动感知 (MEMS Sensing): 内部微机械结构感知加速度与角速度变化,输出微弱的模拟电荷/电压信号。

  2. 模拟前端放大 (AFE Processing): 对探头产生的微弱模拟信号进行初级放大调理。

  3. 16位 ADC 转换 (ADC Conversion): 将连续的模拟信号离散化,转换为原始数字信号。

  4. DLPF 滤波与数字校准 (DLPF & Calibration): 原始数字信号进入硬件数字低通滤波器(DLPF)滤除高频振动噪声,并完成内部的零偏补偿与初步校准。

  5. 存储至 数据寄存器/FIFO (Store Data): 经过清洗和校准的最终有效数据被更新锁存至数据寄存器中(地址 0x3B0x48),此时可触发 INT 引脚的中断信号。

  6. MCU 通过 I2C 读取 (I2C Read): 主控制器发起 I2C 寻址与读时序,连续提取 16 位补码形式的原始数据。

  7. 物理量转换 (Physical Scale Conversion): 在软件业务层,根据初始化时配置的量程分辨率,将原始 ADC 值除以灵敏度系数(例如设置陀螺仪满量程为 ±2000°/s 时,灵敏度为 16.4 LSB/(°/s)),从而解算出真实的物理数值。

除直接读取原始数据外,MPU-6050 的另一核心原理在于利用 DMP 进行空间姿态解算。DMP 能够提取加速度计和陀螺仪的数据,通过内部融合算法直接输出格式为 q30(放大 倍的浮点数)的四元数。

主控获取该四元数 后,需先将其转换为标准浮点数格式 ,随后利用姿态矩阵转换公式,即可解出载体的欧拉角。其核心数学推导公式如下:

(注:式中常数 57.3 用于将弧度制转换为角度制)

此外,芯片内部集成的数字温度传感器数据也采用同样的流转机制,其物理温度计算遵循公式:

其中 regval 为从温度数据寄存器(0x410x42)中读取的 16 位 ADC 原始值。通过上述严谨的硬件链路与数学解算,系统即可建立起对三维空间运动状态的精确观测模型。


7. 本章节实验

7.1 软件I2C读写MPU6050

7.1.1 实验目标

  • 掌握软件模拟I2C通信原理:理解如何利用普通 GPIO 端口的开漏输出模式,通过软件控制电平翻转,严格按照 I2C 协议生成起始、停止、发送、接收及应答等基础通信时序。

  • 掌握MPU6050寄存器级操作流程:学习如何通过 I2C 协议完成对 MPU6050 电源管理、采样率分频、数字低通滤波(DLPF)以及满量程(FSR)等核心硬件寄存器的初始化配置。

  • 实现多字节传感器数据读取与解析:掌握通过"指定地址读"复合时序连续获取加速度计与陀螺仪原始数据的流程,理解 16 位二进制补码的拼接与有符号整数的内存映射关系。

  • 建立分层软件架构思想:通过划分底层通信层(MyI2C)、硬件驱动层(MPU6050)与业务应用层(main),构建高内聚、低耦合的嵌入式驱动代码结构。

7.1.2 硬件设计

7.1.3 软件设计

本实验采用分层驱动架构,将底层通信时序与上层设备操作解耦,具体流程如下:

(1)底层通信模块(基于 GPIO 模拟)

  • 引脚状态配置:初始化 PB10(SCL)与 PB11(SDA)为通用开漏输出模式(Out_OD),依靠外部弱上拉电阻维持总线的高阻空闲状态。

  • 基本时序封装 :通过软件手动置位/复位引脚电平,封装出 StartStopSendByteReceiveByte 及应答校验等核心时序单元,建立严密的读写分离逻辑。

(2)设备驱动模块(基于 MPU6050 寄存器映射)

  • 复合数据帧构建:调用底层时序单元,组合生成符合 I2C 规范的"指定地址写"与"指定地址读"复合时序帧。

  • 传感器初始化序列 :依次向 PWR_MGMT_1 等控制寄存器写入特定配置掩码,解除芯片休眠模式并确立内部时钟源与滤波参数。

(3)应用层数据处理逻辑

  • 地址指针传递机制:在数据获取子函数中定义多变量地址指针(传入引用),以单次调用连续提取六轴的原始加速度与角速度数据,突破单一返回值的限制。

  • 动态刷新展示:主循环中周期性提取解算后的有符号十进制数据,并实时推送至 OLED 显示终端。

具体代码如下:

main.c文件:

cpp 复制代码
#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "MPU6050.h"

uint8_t ID;								//定义用于存放ID号的变量
int16_t AX, AY, AZ, GX, GY, GZ;			//定义用于存放各个数据的变量

int main(void)
{
	/*模块初始化*/
	OLED_Init();		//OLED初始化
	MPU6050_Init();		//MPU6050初始化
	
	/*显示ID号*/
	OLED_ShowString(1, 1, "ID:");		//显示静态字符串
	ID = MPU6050_GetID();				//获取MPU6050的ID号
	OLED_ShowHexNum(1, 4, ID, 2);		//OLED显示ID号
	
	while (1)
	{
		MPU6050_GetData(&AX, &AY, &AZ, &GX, &GY, &GZ);		//获取MPU6050的数据
		OLED_ShowSignedNum(2, 1, AX, 5);					//OLED显示数据
		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);
	}
}

MPU6050.c文件:

cpp 复制代码
#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "MPU6050.h"

uint8_t ID;								//定义用于存放ID号的变量
int16_t AX, AY, AZ, GX, GY, GZ;			//定义用于存放各个数据的变量

int main(void)
{
	/*模块初始化*/
	OLED_Init();		//OLED初始化
	MPU6050_Init();		//MPU6050初始化
	
	/*显示ID号*/
	OLED_ShowString(1, 1, "ID:");		//显示静态字符串
	ID = MPU6050_GetID();				//获取MPU6050的ID号
	OLED_ShowHexNum(1, 4, ID, 2);		//OLED显示ID号
	
	while (1)
	{
		MPU6050_GetData(&AX, &AY, &AZ, &GX, &GY, &GZ);		//获取MPU6050的数据
		OLED_ShowSignedNum(2, 1, AX, 5);					//OLED显示数据
		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);
	}
}

MPU6050_Reg.c文件:

cpp 复制代码
#ifndef __MPU6050_REG_H
#define __MPU6050_REG_H

#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

#endif

MyI2C.c文件:

cpp 复制代码
#include "stm32f10x.h"                  // Device header
#include "Delay.h"

/*引脚配置层*/

/**
  * 函    数:I2C写SCL引脚电平
  * 参    数:BitValue 协议层传入的当前需要写入SCL的电平,范围0~1
  * 返 回 值:无
  * 注意事项:此函数需要用户实现内容,当BitValue为0时,需要置SCL为低电平,当BitValue为1时,需要置SCL为高电平
  */
void MyI2C_W_SCL(uint8_t BitValue)
{
	GPIO_WriteBit(GPIOB, GPIO_Pin_10, (BitAction)BitValue);		//根据BitValue,设置SCL引脚的电平
	Delay_us(10);												//延时10us,防止时序频率超过要求
}

/**
  * 函    数:I2C写SDA引脚电平
  * 参    数:BitValue 协议层传入的当前需要写入SDA的电平,范围0~1
  * 返 回 值:无
  * 注意事项:此函数需要用户实现内容,当BitValue为0时,需要置SDA为低电平,当BitValue为1时,需要置SDA为高电平
  */
void MyI2C_W_SDA(uint8_t BitValue)
{
	GPIO_WriteBit(GPIOB, GPIO_Pin_11, (BitAction)BitValue);		//根据BitValue,设置SDA引脚的电平,BitValue要实现非0即1的特性
	Delay_us(10);												//延时10us,防止时序频率超过要求
}

/**
  * 函    数:I2C读SDA引脚电平
  * 参    数:无
  * 返 回 值:协议层需要得到的当前SDA的电平,范围0~1
  * 注意事项:此函数需要用户实现内容,当前SDA为低电平时,返回0,当前SDA为高电平时,返回1
  */
uint8_t MyI2C_R_SDA(void)
{
	uint8_t BitValue;
	BitValue = GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11);		//读取SDA电平
	Delay_us(10);												//延时10us,防止时序频率超过要求
	return BitValue;											//返回SDA电平
}

/**
  * 函    数:I2C初始化
  * 参    数:无
  * 返 回 值:无
  * 注意事项:此函数需要用户实现内容,实现SCL和SDA引脚的初始化
  */
void MyI2C_Init(void)
{
	/*开启时钟*/
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);	//开启GPIOB的时钟
	
	/*GPIO初始化*/
	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);					//将PB10和PB11引脚初始化为开漏输出
	
	/*设置默认电平*/
	GPIO_SetBits(GPIOB, GPIO_Pin_10 | GPIO_Pin_11);			//设置PB10和PB11引脚初始化后默认为高电平(释放总线状态)
}

/*协议层*/

/**
  * 函    数:I2C起始
  * 参    数:无
  * 返 回 值:无
  */
void MyI2C_Start(void)
{
	MyI2C_W_SDA(1);							//释放SDA,确保SDA为高电平
	MyI2C_W_SCL(1);							//释放SCL,确保SCL为高电平
	MyI2C_W_SDA(0);							//在SCL高电平期间,拉低SDA,产生起始信号
	MyI2C_W_SCL(0);							//起始后把SCL也拉低,即为了占用总线,也为了方便总线时序的拼接
}

/**
  * 函    数:I2C终止
  * 参    数:无
  * 返 回 值:无
  */
void MyI2C_Stop(void)
{
	MyI2C_W_SDA(0);							//拉低SDA,确保SDA为低电平
	MyI2C_W_SCL(1);							//释放SCL,使SCL呈现高电平
	MyI2C_W_SDA(1);							//在SCL高电平期间,释放SDA,产生终止信号
}

/**
  * 函    数:I2C发送一个字节
  * 参    数:Byte 要发送的一个字节数据,范围:0x00~0xFF
  * 返 回 值:无
  */
void MyI2C_SendByte(uint8_t Byte)
{
	uint8_t i;
	for (i = 0; i < 8; i ++)				//循环8次,主机依次发送数据的每一位
	{
		/*两个!可以对数据进行两次逻辑取反,作用是把非0值统一转换为1,即:!!(0) = 0,!!(非0) = 1*/
		MyI2C_W_SDA(!!(Byte & (0x80 >> i)));//使用掩码的方式取出Byte的指定一位数据并写入到SDA线
		MyI2C_W_SCL(1);						//释放SCL,从机在SCL高电平期间读取SDA
		MyI2C_W_SCL(0);						//拉低SCL,主机开始发送下一位数据
	}
}

/**
  * 函    数:I2C接收一个字节
  * 参    数:无
  * 返 回 值:接收到的一个字节数据,范围:0x00~0xFF
  */
uint8_t MyI2C_ReceiveByte(void)
{
	uint8_t i, Byte = 0x00;					//定义接收的数据,并赋初值0x00,此处必须赋初值0x00,后面会用到
	MyI2C_W_SDA(1);							//接收前,主机先确保释放SDA,避免干扰从机的数据发送
	for (i = 0; i < 8; i ++)				//循环8次,主机依次接收数据的每一位
	{
		MyI2C_W_SCL(1);						//释放SCL,主机机在SCL高电平期间读取SDA
		if (MyI2C_R_SDA()){Byte |= (0x80 >> i);}	//读取SDA数据,并存储到Byte变量
													//当SDA为1时,置变量指定位为1,当SDA为0时,不做处理,指定位为默认的初值0
		MyI2C_W_SCL(0);						//拉低SCL,从机在SCL低电平期间写入SDA
	}
	return Byte;							//返回接收到的一个字节数据
}

/**
  * 函    数:I2C发送应答位
  * 参    数:Byte 要发送的应答位,范围:0~1,0表示应答,1表示非应答
  * 返 回 值:无
  */
void MyI2C_SendAck(uint8_t AckBit)
{
	MyI2C_W_SDA(AckBit);					//主机把应答位数据放到SDA线
	MyI2C_W_SCL(1);							//释放SCL,从机在SCL高电平期间,读取应答位
	MyI2C_W_SCL(0);							//拉低SCL,开始下一个时序模块
}

/**
  * 函    数:I2C接收应答位
  * 参    数:无
  * 返 回 值:接收到的应答位,范围:0~1,0表示应答,1表示非应答
  */
uint8_t MyI2C_ReceiveAck(void)
{
	uint8_t AckBit;							//定义应答位变量
	MyI2C_W_SDA(1);							//接收前,主机先确保释放SDA,避免干扰从机的数据发送
	MyI2C_W_SCL(1);							//释放SCL,主机机在SCL高电平期间读取SDA
	AckBit = MyI2C_R_SDA();					//将应答位存储到变量里
	MyI2C_W_SCL(0);							//拉低SCL,开始下一个时序模块
	return AckBit;							//返回定义应答位变量
}

7.1.4 实验现象

复位系统后,OLED 屏幕第一行显示 MPU6050 的固定 ID 号(通常为 0x68)。 屏幕下方实时刷新 XYZ 三轴加速度(AX, AY, AZ)与 XYZ 三轴角速度(GX, GY, GZ)的数值表现:

  • 模块静止平放:AX 与 AY 数值趋近于 0,AZ 因承受重力加速度,显示接近满量程比例下的正值(例如满量程 ±16g 对应的原始数据约 2048)。陀螺仪三轴数据均在 0 附近浮动。

  • 模块倾斜或转动:对应的加速度数值或角速度数值会根据空间姿态的变化及旋转的剧烈程度,产生对应正负符号的线性幅值偏移。

相关推荐
降临-max13 分钟前
Git 协同开发与冲突解决
笔记·git
cmpxr_21 分钟前
【单片机】位域非原子写的风险
单片机·嵌入式硬件
FPGA-ADDA1 小时前
第二篇:RFSoC芯片架构详解——处理系统(PS)与可编程逻辑(PL)
嵌入式硬件·fpga开发·信号处理·fpga·47dr
知识分享小能手1 小时前
MongoDB入门学习教程,从入门到精通,MongoDB的选择片键 - 完整知识点(16)
数据库·学习·mongodb
北京耐用通信1 小时前
工业自动化领域耐中达讯自动化CC-Link IE转EtherCAT技术解决方案
人工智能·物联网·网络协议·自动化·信息与通信
知识分享小能手1 小时前
MongoDB入门学习教程,从入门到精通,MongoDB分片配置完全指南(15)
数据库·学习·mongodb
Dyanic1 小时前
AMSFusion:一种基于注意力机制的自适应多尺度红外与可见光图像融合网络
图像处理·人工智能·学习
少许极端2 小时前
算法奇妙屋(四十三)-贪心算法学习之路10
学习·算法·贪心算法
恒森宇电子有限公司2 小时前
南麟LN1151 超低静态功耗 CMOS 低压差线性稳压器 多种封装形式
单片机·嵌入式硬件
南境十里·墨染春水2 小时前
Linux学习进展 进程管理命令 及文件压缩解压
linux·运维·笔记·学习