目录
[1 STM32 硬件 I2C 基础](#1 STM32 硬件 I2C 基础)
[1.1 STM32 硬件 I2C 简介](#1.1 STM32 硬件 I2C 简介)
[1.2 软件 I2C vs 硬件 I2C](#1.2 软件 I2C vs 硬件 I2C)
[2 I2C 总线工作机制](#2 I2C 总线工作机制)
[2.1 多主机模型](#2.1 多主机模型)
[2.2 单主多从结构](#2.2 单主多从结构)
[2.3 I2C 地址机制(7位 / 10位)](#2.3 I2C 地址机制(7位 / 10位))
[2.3.1 7 位地址模式](#2.3.1 7 位地址模式)
[2.3.2 10 位地址模式](#2.3.2 10 位地址模式)
[3 STM32 I2C 外设详解](#3 STM32 I2C 外设详解)
[3.1 I2C 外设内部结构](#3.1 I2C 外设内部结构)
[3.1.1 通信引脚](#3.1.1 通信引脚)
[3.1.2 时钟控制逻辑](#3.1.2 时钟控制逻辑)
[3.1.3 数据控制逻辑](#3.1.3 数据控制逻辑)
[3.1.3.1 数据寄存器(DR)](#3.1.3.1 数据寄存器(DR))
[3.1.3.2 移位寄存器](#3.1.3.2 移位寄存器)
[3.1.3.3 地址匹配与 PEC 校验](#3.1.3.3 地址匹配与 PEC 校验)
[3.1.3.4 双缓冲机制](#3.1.3.4 双缓冲机制)
[3.1.3.5 核心状态标志说明](#3.1.3.5 核心状态标志说明)
[3.1.4 整体控制逻辑](#3.1.4 整体控制逻辑)
[3.2 主机通信流程(写 / 读)](#3.2 主机通信流程(写 / 读))
[3.2.1 主机写流程](#3.2.1 主机写流程)
[3.2.2 主机读流程](#3.2.2 主机读流程)
[3.3 EV 事件机制(状态机驱动)](#3.3 EV 事件机制(状态机驱动))
[3.3.1 EV 事件定义](#3.3.1 EV 事件定义)
[3.3.2 核心事件概述](#3.3.2 核心事件概述)
[3.3.3 预取机制与特殊事件干预(EV7_1)](#3.3.3 预取机制与特殊事件干预(EV7_1))
[3.3.4 事件机制的工程优势](#3.3.4 事件机制的工程优势)
[4. I2C 驱动实现(基于MPU6050)](#4. I2C 驱动实现(基于MPU6050))
[4.1 I2C 初始化与 MPU6050 配置](#4.1 I2C 初始化与 MPU6050 配置)
[4.1.1 开启外设时钟](#4.1.1 开启外设时钟)
[4.1.2 GPIO 引脚配置](#4.1.2 GPIO 引脚配置)
[4.1.3 I2C 外设初始化](#4.1.3 I2C 外设初始化)
[4.1.4 MPU6050 内部寄存器预设配置](#4.1.4 MPU6050 内部寄存器预设配置)
[4.2 事件轮询与超时保护](#4.2 事件轮询与超时保护)
[4.3 I2C 读写接口实现](#4.3 I2C 读写接口实现)
[4.3.1 写寄存器操作](#4.3.1 写寄存器操作)
[4.3.2 读寄存器操作](#4.3.2 读寄存器操作)
[4.4 应用层接口封装](#4.4 应用层接口封装)
[4.4.1 设备 ID 读取](#4.4.1 设备 ID 读取)
[4.4.2 六轴原始数据解算](#4.4.2 六轴原始数据解算)
[5. 本章节实验](#5. 本章节实验)
[5.2 硬件I2C读写MPU6050](#5.2 硬件I2C读写MPU6050)
[5.2.1 实验目标](#5.2.1 实验目标)
[5.2.2 硬件设计](#5.2.2 硬件设计)
[5.2.3 软件设计](#5.2.3 软件设计)
[5.2.4 实验现象](#5.2.4 实验现象)
1 STM32 硬件 I2C 基础
1.1 STM32 硬件 I2C 简介
I2C(Inter-Integrated Circuit)是一种广泛应用于嵌入式系统中的同步串行通信总线。该协议通过 SCL(Serial Clock)时钟线与 SDA(Serial Data)数据线实现设备之间的数据交换。I2C 通信以字节为基本单位进行传输,每个设备通过唯一地址进行识别与寻址。
在实现方式上,I2C 通信可以通过软件或硬件两种路径完成。传统的软件实现方式是利用普通 GPIO 引脚手动控制 SDA 和 SCL 的电平变化,以模拟协议时序,这种方式称为软件 I2C(Bit-banging)。
而所谓 STM32 硬件 I2C,本质上是指:由芯片内部专用 I2C 外设(I2C Peripheral)提供的一套硬件协议执行单元。该单元通过内置状态机、时序发生器及数据移位电路,自动完成 I2C 协议规定的底层时序(如 START / STOP、位时序控制、ACK/NACK 响应等),从而将"时序生成"从软件中剥离出来。换言之,在硬件 I2C 模式下:
- 软件负责"控制流程"(配置寄存器、选择通信方向、读写数据);
- 硬件负责"执行协议"(生成时序、驱动总线、完成位级通信);
开发者只需配置相关控制寄存器,并基于状态寄存器(SR1 / SR2)提供的标志位判断通信阶段,即可完成完整的数据收发过程,而无需直接干预 SDA / SCL 的电平变化。STM32 的 I2C 接口功能框图如下图所示:

STM32 的 I2C 外设通常具有以下功能特点:
- 支持 主机模式(Master)与从机模式(Slave)
- 支持 7位地址与10位地址
- 支持 标准模式(100 kbit/s)与快速模式(400 kbit/s)
- 支持 中断与DMA数据传输
- 符合标准 I2C 与 SMBus 通信规范
当 I2C 外设完成初始化后,其内部控制逻辑可以自动产生以下关键通信信号:
- 起始条件(START)
- 停止条件(STOP)
- 地址发送
- 应答信号(ACK / NACK)
- 数据移位与接收
在这一机制下,CPU 的主要工作仅限于:
- 向数据寄存器(DR)写入发送数据
- 从数据寄存器(DR)读取接收数据
- 轮询或响应状态事件(EV 事件)
这种"硬件协议引擎 + 软件状态驱动"的协同模式,使得 I2C 通信在保证严格时序一致性的同时,大幅降低了 CPU 的参与度,从而显著提升系统的稳定性与工程可靠性。
1.2 软件 I2C vs 硬件 I2C
在实际嵌入式开发中,I2C通信通常存在两种实现方式:
- 软件模拟 I2C
- 硬件外设 I2C
两者在实现方式与系统资源占用方面存在明显差异,如下表所示:
| 对比项目 | 软件I2C | 硬件I2C |
|---|---|---|
| 实现方式 | GPIO模拟时序 | 专用外设自动生成时序 |
| CPU占用 | 较高,需要持续控制引脚 | 较低,由硬件自动完成 |
| 时序精度 | 依赖软件延时 | 由硬件保证 |
| 通信速度 | 一般较低 | 可达100kHz或400kHz |
| 实现复杂度 | 编程简单 | 需要理解外设状态机 |
| 稳定性 | 易受中断影响 | 较稳定 |
软件 I2C 的优势在于实现简单,对硬件资源要求较低,适合初学者学习协议原理或在没有 I2C 外设的情况下使用。
硬件 I2C 则更适合工程应用。由于通信时序由外设控制,CPU 只需处理关键事件,因此系统整体效率更高,并且通信可靠性更好。
2 I2C 总线工作机制
I2C 总线允许多个节点共享同一对信号线(SDA 和 SCL)进行通信。为确保数据传输的确定性与避免冲突,协议严格定义了节点的硬件角色、通信拓扑结构以及基于地址的节点识别规范。
2.1 多主机模型
I2C 总线在协议底层支持 多主机(Multi-Master) 结构。但节点能否发起通信,取决于其硬件本身是否具备主机控制逻辑:
-
具备主机能力的节点(如 MCU、DSP): 这些设备内部集成了时钟发生器与仲裁逻辑,可在总线空闲时主动生成起始条件(START),从而接管总线,成为瞬时主机。
-
纯从机节点(如传感器、EEPROM): 这类设备硬件上不具备时钟生成与控制逻辑,只能被动侦听总线,不会主动发起通信,也不会参与总线仲裁。

在多主机模型下,总线默认处于空闲状态**,** 未发起通信的节点均以 从机(Slave) 身份驻留。
当任一具备主机功能的节点发起通信时,将通过总线仲裁机制争夺控制权;一旦获得总线占用权,即进入**主机(Master)**状态并开始后续数据传输。
如果多个主机同时尝试接管总线,则会触发 总线仲裁(Arbitration):
- 仲裁基于 SDA 线的线与(Wired-AND)特性实现:尝试输出高电平却检测到低电平的节点,会被判定为优先级较低。
- 仲裁失败的节点会 立即停止自身时钟输出,强制回退至从机(监听)模式。
- 胜出节点继续进行通信,保证数据传输完整,同时避免电平冲突对硬件造成损坏。
在多主机结构中,只有具备主机功能的节点才能发起通信,从机仅被动响应请求,不参与总线控制;仲裁机制用于解决多主机同时发起通信时的冲突问题。
2.2 单主多从结构
尽管 I2C 支持多主机竞争,但在绝大多数实际嵌入式系统中,为了降低总线时序的复杂度和固件开发成本,通常采用 单主多从(Single-Master Multiple-Slave) 的固定拓扑结构。
在这种模式下,系统层级被严格固化:
-
微控制器 (MCU) 作为唯一主机: 负责提供 SCL 时钟节拍,并主导所有的读/写数据传输序列。
-
外设作为永久从机: 仅在接收到匹配的地址信号时进行被动响应。
常见挂载于主控 MCU 下的 I2C 从机外设包括:
-
存储介质(如 EEPROM)
-
数据采集节点(如各类传感器)
-
输出节点(如 OLED 显示模块)
-
系统外围(如电源管理芯片)
2.3 I2C 地址机制(7位 / 10位)
为了在共享总线上精准路由数据,I2C 协议为每个从设备分配了独立的硬件地址。主机在发起通信时,必须首先在总线上广播目标从机的地址。协议定义了两种地址格式:
2.3.1 7 位地址模式
这是工业界最通用、最基础的寻址方式。通信建立时,主机发送的第一个字节(8 bit)由两部分组成:[7位设备地址] + [1位读/写控制位 R/W],如下图所示:

读写控制位(R/W)位于最低位,用于指示后续的数据传输方向:
-
0: 主机向从机写入数据 (Write)
-
1: 主机从从机读取数据 (Read)
因此,实际发送的地址字节为:8bit = 7bit地址 + R/W位
绝大多数 I2C 外设均采用 7 位地址。例如 MPU6050 传感器的基础地址通常为 0x68 或 0x69,其具体地址值由硬件引脚(AD0)的电平状态决定。
2.3.210 位地址模式
随着总线挂载节点数量的增加,为解决 7 位地址空间(最多 128 个节点)可能枯竭的问题,协议扩展了 10 位寻址模式。该模式需要主机连续发送两个地址字节:
-
字节 1:
1111 0(10位地址模式标志) +A9 A8(地址高两位) +R/W(读写控制位) -
字节 2:
A7 A6 A5 A4 A3 A2 A1 A0(地址低八位)

这种寻址方式在常规的 MCU 外设传感器开发中较为罕见,通常应用于大规模通信网络或特殊专用芯片组中。
3 STM32 I2C 外设详解
I2C 通信对时序一致性要求严格,软件模拟方式在实现复杂度与实时性方面均存在明显局限。因此,STM32 在芯片内部集成了 I2C 硬件外设,用于承接协议层以下的时序生成与总线控制任务。
在该架构下,软件仅需通过寄存器接口与外设交互:配置控制寄存器启动通信流程,通过数据寄存器(DR)完成字节级收发,并依据状态寄存器(SR1 / SR2)提供的标志位推进通信阶段。
I2C 外设内部包含多个功能单元协同工作,其行为由硬件状态机统一调度。为准确理解事件机制与驱动实现方式,有必要对其内部结构进行拆解分析。
3.1 I2C 外设内部结构
从实现角度看,STM32 的 I2C 外设由多个功能模块构成,这些模块围绕通信时序与数据路径协同运行,共同完成协议执行。其整体结构可划分为以下四类功能单元:
- 通信引脚接口单元
- 时钟生成与控制单元
- 数据收发通路单元
- 状态监测与控制单元

上述单元通过内部总线与控制逻辑相互配合,使外设能够在无需 CPU 参与位级控制的情况下完成完整 I2C 事务处理。
3.1.1 通信引脚
I2C 硬件架构的物理层基础是两条总线引脚:串行时钟线SCL(Serial Clock Line) 和串行数据线SDA(Serial Data Line)。其中,SMBA(SMBALERT)用于 SMBus 扩展协议中的告警信号,在标准 I2C 通信中通常不使用。
STM32 中拥有多个 I2C 外设,它们的信号线通过 GPIO 复用功能(Alternate Function) 与特定的 GPIO 引脚连接。例如,STM32F103C8T6的引脚定义:

为了满足多节点共享总线的电气特性,这些引脚在初始化时必须配置为复用开漏输出模式(AF_OD)。
3.1.2 时钟控制逻辑
I2C 通信是同步通信协议,数据的采样和移位必须严格依赖 SCL 时钟。因此,STM32 I2C 外设内部包含完整的时钟生成与控制模块。该模块的输入时钟来源于 APB1 外设时钟 PCLK1。通过内部的分频和时序生成电路,PCLK1 被转换为符合 I2C 协议要求的 SCL 时钟信号。
核心配置寄存器为 CCR(Clock Control Register),该寄存器决定了 SCL 时钟周期的长度,从而决定总线通信速率。
STM32 I2C 支持两种标准通信模式:
| 模式 | 最大速率 |
|---|---|
| 标准模式(Standard Mode) | 100 kbit/s |
| 快速模式(Fast Mode) | 400 kbit/s |
在快速模式下,还可以进一步配置 SCL 的占空比,可选Tlow/Thigh = 2或Tlow/Thigh = 16/9模式。占空比控制的意义在于:
- 确保 SDA 数据在 SCL 高电平期间稳定
- 为接收设备提供足够的采样时间
除了 CCR 之外,TRISE 寄存器还用于配置 SCL 上升时间,以满足 I2C 规范对信号边沿时间的限制。
通过 CCR 与 TRISE 的配合配置,I2C 外设能够生成满足协议要求的稳定时钟信号。
3.1.3 数据控制逻辑
I2C 的数据通信采用串行传输方式,即数据以逐位形式通过 SDA 线发送或接收;而 CPU 访问外设时则以字节为单位进行并行读写。为了实现串行数据与并行数据之间的高效转换,STM32 I2C 外设内部构建了以数据寄存器(DR) 与 **数据移位寄存器(Shift Register)**为核心的双级数据通路结构:
cpp
CPU <------> 数据寄存器DR <------> 数据移位寄存器 <------> SDA
在此基础上,数据移位寄存器还与地址寄存器(OAR1/OAR2) 及PEC 寄存器存在数据交互关系,从而支持地址识别与数据校验等扩展功能。
3.1.3.1 数据寄存器(DR)
数据寄存器(Data Register)是 CPU 与 I2C 外设之间的直接数据接口,其本质为一个 8 位并行寄存器:
- CPU 写入 DR → 数据进入发送路径
- CPU 读取 DR → 获取接收数据
在发送过程中,DR 提供数据源;在接收过程中,DR 作为数据缓冲区供 CPU 读取。
3.1.3.2 移位寄存器
移位寄存器是 I2C 数据通路的核心执行单元,直接连接 SDA 数据线,其主要功能包括:
- 在发送模式下,将数据逐位串行输出到 SDA
- 在接收模式下,对 SDA 进行逐位采样并重组数据
移位操作完全由 SCL 时钟驱动,因此其工作节拍严格受 I2C 时序控制。
此外,移位寄存器的数据来源与去向并不局限于 DR,而是构成一个多源-多目标的数据交换节点,其数据来源包括:
- 数据寄存器(DR)
- SDA 数据线(接收路径)
数据去向包括:
- 数据寄存器(DR)
- PEC 寄存器(数据校验)
- 地址比较逻辑(从机模式)
3.1.3.3 地址匹配与 PEC 校验
在基础的数据收发功能之外,移位寄存器还参与以下两类关键硬件行为:
(1)地址匹配机制(从机模式)
当 STM32 工作在从机模式时,总线上的地址帧同样通过 SDA 输入,并进入移位寄存器。移位寄存器在接收到完整地址后,会将该地址与内部的 **自身地址寄存器(OAR1 / OAR2)**进行比较:
-
若匹配成功 → 产生应答(ACK)
-
若不匹配 → 忽略该通信
STM32 支持配置两个从机地址:
-
OAR1:主地址
-
OAR2:辅助地址
该机制允许设备响应多个地址,提高系统灵活性。
(2)PEC 校验机制(可选功能)
当启用 PEC(Packet Error Checking)功能时,数据接收路径会增加校验处理流程:
- 移位寄存器接收数据字节
- 数据送入 PEC 计算单元进行累加运算
- 计算结果存储在 PEC 寄存器中
该机制用于检测通信过程中的数据完整性,常见于对可靠性要求较高的应用场景。
3.1.3.4 双缓冲机制
I2C 外设采用 DR + 移位寄存器 的双缓冲结构,实现数据处理与物理传输的解耦,其核心目标是提高数据吞吐能力并降低 CPU 等待时间。具体运作模式如下:
(1)发送模式(流水线化发送)
当 CPU 将数据写入 DR 后,硬件会在满足发送条件时,将该数据从 DR 转移至移位寄存器。一旦该数据转移完成,DR 即被清空,并立即置位 TXE(发送数据寄存器空)标志。此时,尽管该字节可能尚未在总线上完成逐位发送,但由于 DR 已空,CPU 可以继续写入下一字节数据,而无需等待当前字节发送结束。
随后,移位寄存器在 SCL 时钟驱动下,将已装载的数据逐位通过 SDA 线发送。通过这种前一字节发送、后一字节预装载的机制,I2C 外设实现了发送路径上的流水线化处理,从而提高总线利用率与数据吞吐效率。
(2)接收模式(并行化转储)
移位寄存器在 SCL 时钟驱动下,对 SDA 信号线进行逐位采样,并按位累积数据。当接收到完整的 8 位数据后,该字节会被整体并行转移至 DR 寄存器,同时置位 RXNE(接收数据寄存器非空)标志,指示 CPU 有新数据可读。
在 DR 被读取之前,该数据会保持稳定,不会被后续数据覆盖。因此,软件需在 RXNE 置位后及时读取 DR,以保证接收过程的连续性。
(3)数据安全保障
双缓冲结构在接收路径中提供基本的数据一致性保护机制。当 DR 中已有未被读取的数据时,新的接收字节在移位寄存器完成后无法立即写入 DR。此时:
- 若移位寄存器与 DR 同时持有有效数据,则可能置位 BTF(Byte Transfer Finished)标志,表示存在"已完成但未取走"的数据状态
- 若软件仍未及时读取 DR,则后续新数据到达时可能引发溢出(Overrun)风险
因此,软件必须基于 RXNE / BTF 等状态标志,及时完成数据读取操作,以避免数据丢失并维持接收状态机的正常推进。
3.1.3.5核心状态标志说明
数据控制逻辑中最关键的状态标志包括:
- **TXE(Transmit Data Register Empty):**表示 DR 为空,可写入新数据
- **RXNE(Receive Data Register Not Empty):**表示 DR 中已有接收数据,需及时读取
- **BTF(Byte Transfer Finished):**表示当前字节在硬件层面的传输已完成,即数据寄存器(DR)与移位寄存器均为空,且该字节的 ACK/NACK 已处理完毕
3.1.4 整体控制逻辑
I2C 外设还包含一个 中央控制逻辑单元(Control Logic) 。该单元内部集成了协议状态机(Protocol State Machine),用于按照 I2C 协议规范自动推进通信流程。其主要功能包括:
- 起始条件生成(START)
- 停止条件生成(STOP)
- 地址发送
- 应答控制(ACK / NACK)
- 总线忙检测
- 仲裁检测
- 错误检测
软件主要通过以下寄存器与该控制逻辑交互:
| 寄存器 | 作用 |
|---|---|
| CR1 | 外设使能、START、STOP、ACK 控制 |
| CR2 | 中断与 DMA 控制 |
| SR1 | 主要状态标志 |
| SR2 | 辅助状态标志 |
- CR1 / CR2 用于配置和控制 I2C 外设行为
- SR1 / SR2 用于反映当前通信阶段及硬件状态
在通信过程中,当协议状态机进入关键阶段(如起始完成、地址匹配、数据收发完成等)时,硬件会通过状态标志位更新的方式对外提供状态信息;同时,也可以根据配置:
- 向 NVIC 产生中断请求
- 向 DMA 控制器 发起数据传输请求
基于上述状态标志机制、中断响应机制 以及DMA 请求机制,软件可以采用不同方式驱动 I2C 通信流程:
- 轮询模式(Polling):CPU 主动读取状态寄存器并推进通信
- 中断模式(Interrupt):由 I2C 事件触发中断,在中断服务函数中处理通信
- DMA 模式(DMA):由 DMA 控制器自动完成数据寄存器与内存之间的数据搬运
3.2 主机通信流程(写 / 读)
在绝大多数嵌入式应用场景中,STM32 通常作为 I2C 主机(Master)发起与外围节点的通信,例如:
- 读取 MPU6050 传感器
- 读写 EEPROM 存储器
- 驱动外置 ADC / DAC 及显示控制器等
从底层通信协议的角度来看,作为主机的通信过程被严格划分为两种基本模式:主发送器(Master Transmitter) 与主接收器(Master Receiver)。二者的本质区别在于总线数据流的物理传输方向,即 SDA 线上数据的驱动方不同。
由于实际工程中 STM32 作为从机的应用场景相对较少,且本篇实验未涉及从机通信实现,因此本章仅对主机模式下的通信过程进行分析,不展开从机模式相关内容。
3.2.1 主机写流程
当 STM32 需要向从设备主动写入数据(如配置传感器内部寄存器 或 向 EEPROM 写入页数据)时,I2C 外设工作在主发送器模式。其完整的通信建立过程及对应的硬件事件序列如下图所示:

结合序列图,完整的发送流程可拆解为以下阶段:
(1)起始条件产生与寻址建立
通信的起点是主机在总线上生成起始条件(S)。在软件操作中,通过置位控制寄存器I2C_CR1 的 START 位,硬件逻辑会自动在总线上产生起始位时序。当 START 信号成功发送后,状态寄存器I2C_SR1中的 SB (Start Bit) 标志位 被置 1,标志着起始条件发送完成并已成功占用总线,此时触发 EV5 事件。
捕获到 EV5 事件后,软件需向数据寄存器 I2C_DR 写入目标从机的 7 位物理地址及写方向位(R/W = 0)。外设将地址串行移出,并在第 9 个时钟周期检测从机应答(A)。若从机正确响应,I2C_SR1 中的 ADDR 标志位 将被置 1,标志着地址发送结束且寻址成功(主模式),同时触发 EV6 事件。此时,软件必须按顺序执行读取 SR1 寄存器,随后读取 SR2 寄存器的组合操作,以此清除 ADDR 标志位并解锁时钟线,放行后续数据传输(此清零操作在调用 I2C_CheckEvent() 函数读取状态寄存器时由硬件隐式完成)。

(2)数据连续发送
地址阶段完成后进入数据发送阶段。当 I2C_SR1 中的 TXE (Transmit Data Register Empty) 标志位置 1 时,触发 EV8_1 (仅在第一个数据写入前)或 EV8 事件。TXE 置 1 意味着数据寄存器 DR 为空,软件可以写入新的数据字节。硬件随后自动将数据从 DR 加载至内部移位寄存器,并依靠 SCL 时钟逐位移出总线。在此过程中,只要 TXE 持续保持置位状态,CPU 即可循环写入数据,实现高效的流水式连续发送。
(3)通信终止
当计划发送的所有数据均已写入 DR,且移位寄存器已将最后一比特数据完整移出总线并收到从机 ACK 后,I2C_SR1 中的 TXE 与BTF (Byte Transfer Finished)标志位将同时被置 1,触发 EV8_2 事件。BTF 置 1 表示当前字节在硬件层面的传输已完全结束,即数据寄存器与移位寄存器均已空闲,且该字节的 ACK/NACK 过程已经完成。此时,软件需通过置位 I2C_CR1 的STOP 位请求产生停止条件(P)。硬件随后在 SCL 为高电平期间释放 SDA(由外部上拉电阻拉至高电平),从而生成 STOP 条件并释放 I2C 总线。
3.2.2 主机读流程
主接收器模式主要用于从目标设备读取数据载荷。与发送模式相比,接收模式在硬件底层的应答控制(ACK/NACK)与停止条件生成上具有更为严苛的时序要求,这是由 STM32 硬件接收逻辑的预取特性决定的。

结合主接收器序列图,其核心流程如下:
(1)寻址与接收建立
接收模式的前期流程与发送模式类似:产生 START 信号(触发 EV5 ),随后发送目标地址及读方向位(R/W = 1)。若从机应答成功,I2C_SR1 的 ADDR 位被置 1(触发 EV6),确立主机接收数据通路。此时同样需要读取 SR1 和 SR2 序列来清除 ADDR 标志。
(2)数据流接收
链路建立后,从机接管 SDA 控制权并开始输出。I2C 外设将接收到的串行位拼接成完整字节并存入 I2C_DR 。当 I2C_SR1 中的 RXNE (Receive Data Register Not Empty) 标志位被置 1 时,触发 EV7 事件。RXNE 置 1 提示 DR 寄存器中已有新数据就绪,CPU 读取 DR 后,该标志位由硬件自动清零。若需接收多字节,需确保 I2C_CR1 中的 ACK 位为 1,以便硬件在每个字节后自动回复应答。
(3)关键的最后字节处理机制(EV7_1 事件)
针对 STM32 硬件的预取特性,主机必须在接收最后一个字节之前对状态机进行提前干预。如序列图中的 EV7_1 所示:在准备接收最后一个数据字节(数据 N)时,软件应在倒数第二个字节(数据 N-1)接收完成并读取 DR 后,在最后一个字节(数据 N)进入移位寄存器阶段之前,通过 I2C_CR1 清除 ACK 位(发送 NACK)并提前置位 STOP 请求。
上述操作的物理意义在于:通过提前配置 NACK 与 STOP,硬件在接收完最后一个字节的第 8 位后,会在第 9 个时钟周期自动发送 NACK,并随后生成停止条件(P)。若该操作滞后于最后一个字节的采样时刻,硬件将因预取机制默认发送 ACK 并继续发起下一字节接收,从而导致多读数据或从机状态异常。因此,严格遵循 EV7_1 的时序约束是实现稳定读取的关键。
3.3 EV 事件机制(状态机驱动)
在 STM32 的 I2C 外设架构中,通信时序的推进由硬件内部的 **协议状态机(Protocol State Machine)**自动完成。软件并不直接参与时序生成,而是通过读取状态信息,在合适的时机介入并执行相应操作。需要注意,EV 事件并非硬件寄存器中的独立标志位,而是由 I2C_SR1 与 I2C_SR2 中多个状态位组合形成的软件抽象。
为了让软件能够在不破坏时序的前提下准确参与通信过程,STM32 在软件层面引入了基于**事件(Event)**的驱动机制。该机制对底层状态进行抽象,使软件能够以明确的阶段划分来控制通信流程。这一机制是实现稳定 I2C 驱动的基础,适用于轮询、中断以及 DMA 等不同工作模式。
3.3.1 EV 事件定义
在 I2C 通信的不同阶段,硬件会持续更新状态寄存器(I2C_SR1 与 I2C_SR2)中的各类标志位,例如 SB、ADDR、TXE 等。这些标志位用于反映当前硬件的具体状态,但通常只描述某一个局部条件,而不能直接表示完整的通信阶段。
在实际通信过程中,仅依赖单一标志位往往无法准确判断当前所处的整体状态。例如,TXE=1 仅表示发送数据寄存器为空,该状态可能出现在以下多种场景中:
- 准备发送第一个字节之前
- 连续发送过程中
- 数据发送已经完成之后
因此,仅凭 TXE=1 无法确定下一步操作是继续写入数据,还是结束通信,软件也无法据此安全推进通信流程。
为了解决这一问题,STM32 在参考手册中对 I2C 通信过程中的关键阶段进行了划分,并定义了一系列 **事件(Event)**用于描述这些阶段。标准外设库在此基础上,将这些事件进一步封装为软件可直接使用的宏定义(如 EV5、EV6 等),并提供统一的检测接口。
因此,所谓 EV 事件,本质上是对多个状态标志位组合关系的抽象表达,用于描述 I2C 协议状态机在某一时刻所处的确定阶段。通过这种方式:
- 分散的寄存器标志被组合为具有明确语义的状态
- 底层位判断被统一封装为事件标识
- 硬件状态被映射为软件可直接处理的阶段节点
基于事件机制,驱动程序可以按照固定的顺序推进通信流程:
bash
检测当前事件 → 执行对应操作 → 等待下一事件
该模型将底层复杂的状态判断转化为清晰的阶段控制逻辑,使软件能够在正确的时机执行操作,从而保证 I2C 通信过程的时序一致性与状态可靠性。
3.3.2 核心事件概述
在 I2C 主机模式(主发送与主接收)下,状态机的推进依赖一系列关键事件。每个事件不仅对应特定的标志位组合,也对应必须执行的软件操作。
| 逻辑事件 | 硬件标志位组合 (SR1 / SR2) | 状态含义 | 软件必须执行的推进动作 |
|---|---|---|---|
| EV5 | SB = 1 | 起始条件已发送:START 已在总线上产生,主机获得总线控制权 | 向 DR 写入从机地址 + R/W 位(写 DR 的同时完成对 SB 的清除) |
| EV6 | ADDR = 1 | 地址阶段完成:地址已发送且收到从机 ACK | 先读 SR1 再读 SR2 清除 ADDR(同时释放 SCL 拉伸,允许进入数据阶段) |
| EV7 | RXNE = 1 | 接收数据就绪:一个字节已接收并转移至 DR | 读取 DR 获取数据(读取操作清除 RXNE,并触发下一字节接收) |
| EV8 / EV8_1 | TXE = 1 | 发送缓冲可写:DR 为空(数据已装入移位寄存器或尚未写入) | 向 DR 写入下一字节数据(启动或维持发送流水线) |
| EV8_2 | TXE = 1 且 BTF = 1 | 字节传输完成:移位寄存器与 DR 均为空,当前字节已完成(含 ACK/NACK) | 根据流程控制:• 若结束通信 → 置 STOP• 若继续发送 → 写 DR(或发 Re-START) |
| EV9 | ADDR10 = 1 | 10 位地址阶段:已发送高位地址帧(11110xx) | 向 DR 写入地址低 8 位(完成 10 位地址发送序列) |
3.3.3 预取机制与特殊事件干预(EV7_1)
在标准 EV 事件序列之外,主接收模式中还存在一个必须额外处理的关键阶段,即最后一个字节的接收过程(通常称为 EV7_1)。该阶段不对应标准的事件组合,但对通信能否正确结束具有决定性影响。
STM32 的 I2C 接收路径具有预取特性(Prefetch)。在硬件实现上,外设在接收当前字节的同时,会提前为下一字节的接收做好准备。如果软件不进行干预,外设会在当前字节接收完成后自动发送 ACK,并继续发起下一字节的接收请求。
这种行为在连续读取数据时是必要的,但在只需接收固定长度数据的场景中会引发问题。当主机仅需接收最后一个字节时,若仍保持 ACK 状态,硬件将继续请求下一个字节,从而导致数据越界读取,甚至破坏从机内部状态机。
因此,在接收流程的末尾阶段,软件必须在特定时刻对硬件行为进行约束。该时刻定义为:**倒数第二个字节已经接收完成,而最后一个字节尚未完全进入数据寄存器(DR)之前。**在这一窗口内,需要执行以下操作:
- 关闭 ACK(发送 NACK)
- 置位 STOP 位,请求产生停止条件
cpp
I2C_AcknowledgeConfig(I2C2, DISABLE); //在接收最后一个字节之前提前将应答失能
I2C_GenerateSTOP(I2C2, ENABLE); //在接收最后一个字节之前提前申请停止条件
上述操作的作用在于:
- 明确通知从机当前传输即将结束
- 阻止硬件继续发起下一字节接收
- 确保最后一个字节成为本次通信的终止数据
需要注意的是,该控制过程并不对应某一个标准 EV 事件,而是基于对硬件接收时序的精确控制。因此,在驱动实现中必须显式处理该阶段,否则容易出现多读数据或通信异常的问题。
3.3.4 事件机制的工程优势
相比基于延时控制或单一标志位轮询的实现方式,基于 EV 事件的驱动机制在工程实践中具有更好的结构性与可靠性。其优势主要体现在以下几个方面。
(1)时序一致性
事件机制以硬件状态标志为触发条件,软件仅在对应事件成立时执行读写操作。这种方式使软件行为始终与 I2C 总线的实际时序保持一致,避免因 CPU 执行速度过快或判断时机不当而引发的时序错误,从而提高通信的稳定性。
(2)状态表达清晰
通过事件抽象,底层分散的寄存器标志被组织为具有明确语义的通信阶段。软件无需直接处理复杂的位组合关系,只需围绕"当前处于哪个事件"来推进流程,使驱动逻辑更加直观,同时也降低了调试和维护的复杂度。
(3)驱动模式易于扩展
EV 事件本质上与硬件状态变化一一对应,这些状态变化不仅可以通过软件轮询检测,也可以直接作为中断或 DMA 的触发条件。例如:
- 可映射为 NVIC 中断触发源,由中断服务函数推进通信流程
- 可触发 DMA 控制器,在数据寄存器与内存之间自动完成数据搬运
基于这一特性,采用事件机制编写的轮询驱动,在迁移到中断模式或 DMA 模式时,无需改变整体通信流程,仅需调整触发方式即可完成架构升级。
4. I2C 驱动实现(基于MPU6050)
本章节将通过 MPU6050 六轴传感器的驱动开发,详细阐述如何利用 STM32 的硬件 I2C 外设进行底层开发。相比于软件模拟,硬件外设能够通过内部硬件电路自动产生通信时序,显著降低 CPU 的运算负载,并提升通信的精确度与稳定性。
4.1 I2C 初始化与 MPU6050 配置
在进行 I2C 通信前,必须对 STM32 的硬件资源及外部从机设备进行系统化配置。其初始化流程主要分为以下几个阶段:
- 开启外设时钟
- 配置 GPIO 引脚
- 配置 I2C 外设
- 配置 MPU6050 内部寄存器
4.1.1 开启外设时钟
STM32 的外设时钟默认处于关闭状态以降低系统功耗。I2C2 外设控制器挂载于 APB1 总线上,而其对应的通信引脚(PB10、PB11)所在的 GPIOB 端口挂载于 APB2 总线上。在配置外设与引脚之前,必须分别使能 RCC(复位和时钟控制)模块中对应的 APB1 与 APB2 外设时钟,确保底层数字逻辑电路获得有效的工作时钟信号。
cpp
/* 1. 开启时钟 */
RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C2, ENABLE); // 开启I2C2时钟,属于APB1外设总线
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); // 开启GPIOB时钟,引脚PB10/PB11属于此总线
4.1.2 GPIO 引脚配置
I2C 协议要求总线支持"线与"逻辑,因此在硬件配置时,必须将 SCL(串行时钟)与 SDA(串行数据)引脚设置为复用开漏输出(AF_OD,Alternate Function Open-Drain)。
在此模式下,引脚的电平控制权从普通的 GPIO 端口寄存器移交给内部的 I2C 硬件控制器。同时,开漏输出结构允许总线在空闲状态下通过外部上拉电阻维持高电平状态,从而满足多节点设备共享总线的电气特性规范,避免硬件短路冲突。
cpp
/* 2. GPIO初始化 */
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD; // 复用开漏输出:I2C协议标准的硬件驱动模式
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10 | GPIO_Pin_11; // PB10对应SCL,PB11对应SDA
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);
4.1.3 I2C 外设初始化
该阶段通过 I2C_InitTypeDef 结构体定义通信底层的核心特性,并将参数固化至 I2C 控制寄存器中:
-
通信速率(Clock Speed):设置 SCL 时钟总线的频率,如标准模式下的 50kHz 或 100kHz,以及快速模式下的 400kHz。
-
占空比(Duty Cycle):定义 SCL 时钟周期内低电平(Tlow)与高电平(Thigh)的时间比例。
-
地址模式(Acknowledged Address):设定主机在寻址阶段识别目标设备时,采用 7 位地址格式还是 10 位地址格式。
-
应答使能(Acknowledge Enable):配置 I2C 硬件接收器在接收到一个完整的字节数据后,是否在第 9 个时钟脉冲期间自动拉低 SDA 引脚以向发送方回复 ACK(应答)信号。
-
自身地址(Own Address):设定 STM32 在作为从机设备被其他主机寻址时的匹配地址。在纯主机工作模型下,该参数通常置为 0x00。
完成上述硬件底层参数配置后,必须通过控制寄存器(CR1)的 PE(Peripheral Enable)位开启 I2C 外设模块。外设使能后,内部硬件状态机正式投入运行,具备了接管总线、响应通信事件及产生时序信号的能力。
cpp
/* 3. I2C核心参数初始化 */
I2C_InitTypeDef I2C_InitStructure;
I2C_InitStructure.I2C_Mode = I2C_Mode_I2C; // 选择基本I2C通信协议
I2C_InitStructure.I2C_ClockSpeed = 50000; // 设定时钟速率为50KHz
I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2; // 时钟占空比,选择Tlow/Thigh = 2
I2C_InitStructure.I2C_Ack = I2C_Ack_Enable; // 主机接收数据后自动回复应答信号
I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit; // 寻址模式设定为7位地址
I2C_InitStructure.I2C_OwnAddress1 = 0x00; // 设置STM32自身地址,仅在从机模式使用
I2C_Init(I2C2, &I2C_InitStructure);
I2C_Cmd(I2C2, ENABLE); // 使能外设,开启硬件I2C2,电路开始工作
4.1.4 MPU6050 内部寄存器预设配置
底层 I2C 硬件通信接口建立后,必须对目标从机设备进行业务层面的参数初始化。MPU6050 传感器在上电复位后默认处于睡眠模式(Sleep Mode),需通过 I2C 总线向其内部控制寄存器写入相应的配置指令:
-
电源管理配置 :向
PWR_MGMT_1寄存器写入指令以解除睡眠状态,并通常选择 X 轴陀螺仪作为系统参考时钟源,以获取更高的时钟稳定性。同时配置PWR_MGMT_2寄存器使各轴传感器退出待机状态。 -
采样率设定 :配置
SMPLRT_DIV寄存器,设定传感器内部 ADC(模数转换器)的数据输出频率。 -
滤波器配置 :配置
CONFIG寄存器以启用数字低通滤波器(DLPF),用于滤除高频机械噪声干扰。 -
量程配置 :分别写入
GYRO_CONFIG与ACCEL_CONFIG寄存器,设定陀螺仪与加速度计的满量程范围(例如陀螺仪设定为 ±2000°/s,加速度计设定为 ±16g)。该量程设定直接决定了传感器输出原始数据的解析度(灵敏度 LSB/g 或 LSB/°/s)。
cpp
/* 4. MPU6050内部寄存器预设配置 */
MPU6050_WriteReg(MPU6050_PWR_MGMT_1, 0x01); // 解除睡眠,选择X轴陀螺仪作为内部时钟源
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); // 设定加速度计满量程范围
4.2 事件轮询与超时保护
STM32 硬件 I2C 内部运行着一个复杂的状态机。每当硬件完成一个协议动作(如发送起始位、发送地址),状态寄存器中相应的标志位就会置起。这些标志位被标准库封装为 EV事件(Event)。典型的硬件事件如下所示:
-
EV5:主机起始位发送完成标志。
-
EV6:地址发送完成,并已接收到从机的应答标志。
-
EV8:数据发送寄存器(DR)为空,等待写入新数据。
-
EV8_2:数据移位寄存器也已发送完成,总线空闲。
在实际工程中,为了防止总线硬件故障或线路断路导致程序因无限死循环等待而卡死,驱动中必须引入超时保护机制,即通过计数自减的方式限定等待时间。
cpp
/**
* 函 数:MPU6050等待事件(带超时保护)
* 参 数:I2Cx 选择外设,I2C_EVENT 等待的标志位
* 返 回 值:无
*/
void MPU6050_WaitEvent(I2C_TypeDef* I2Cx, uint32_t I2C_EVENT)
{
uint32_t Timeout = 10000; // 初始化超时计数器
while (I2C_CheckEvent(I2Cx, I2C_EVENT) != SUCCESS) // 轮询检查目标事件是否发生
{
Timeout--; // 计数自减
if (Timeout == 0) // 若降至0则判断为超时
{
/* 此处可添加错误记录或系统复位逻辑 */
break; // 强制跳出循环,防止死机
}
}
}
4.3 I2C 读写接口实现
4.3.1 写寄存器操作
向 MPU6050 内部特定寄存器写入数据需要严格遵循 I2C 硬件通信标准时序。在此模式下,STM32 的 I2C 硬件外设会自动管理 SCL 时钟周期与底层的位移位逻辑,软件应用层的核心任务是通过操作控制寄存器(CR)依次触发通信状态的跃迁,并向数据寄存器(DR)填入有效的载荷。标准的单字节写驱动流程如下:
-
产生起始信号(START):主机向总线发出起始条件,申请占用 I2C 总线的控制权,此时所有挂载在总线上的从机设备进入监听状态。
-
发送设备地址(写方向):主机发送完整的 8 位数据帧。该 8 位数据由 7 位从机物理地址(对于 MPU6050,通常为 0x68)左移一位后,与最低位的读写控制标志位(写操作为 0)拼接而成(即 0xD0)。此步骤用于在总线上寻址目标 MPU6050 器件。
-
发送寄存器地址:目标从机应答后,主机继续发送 8 位的内部寄存器地址数据。该步骤的目的是设定从机内部的地址指针,精确指定即将执行写入操作的硬件存储单元。
-
发送数据内容:主机将 8 位的具体配置数据写入总线。从机接收到该数据后,其内部逻辑会自动将此字节固化至上一步地址指针所指向的寄存器中。
-
产生停止信号(STOP):数据传输完毕且接收到从机的有效应答后,主机向总线发出停止条件,主动释放 I2C 总线的控制权,宣告本次单字节写传输周期结束。
cpp
/**
* 函 数:MPU6050写寄存器
* 参 数:RegAddress 目标寄存器地址,Data 写入数据
* 返 回 值:无
*/
void MPU6050_WriteReg(uint8_t RegAddress, uint8_t Data)
{
I2C_GenerateSTART(I2C2, ENABLE); // 1. 生成起始条件
MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT); // 等待EV5:确认起始位发送成功
I2C_Send7bitAddress(I2C2, 0xD0, I2C_Direction_Transmitter); // 2. 发送从机地址0xD0,设置方向为写
MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED); // 等待EV6:从机已应答
I2C_SendData(I2C2, RegAddress); // 3. 将寄存器地址写入DR寄存器发送
MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTING);// 等待EV8:确认数据正在发送
I2C_SendData(I2C2, Data); // 4. 将实际数据内容写入DR寄存器
MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTED); // 等待EV8_2:数据及ACK均处理完毕
I2C_GenerateSTOP(I2C2, ENABLE); // 5. 生成终止条件
}
4.3.2 读寄存器操作
读取寄存器是 I2C 通信中相对复杂的环节,其核心在于采用了 重复起始条件(Re-Start) 构成的 复合帧 时序。其底层物理逻辑要求主机先建立 写 连接以设定从机内部的读取基址,随后在不释放总线的前提下,通过翻转传输方向建立 读连接以获取数据。标准的单字节读驱动流程如下:
-
产生起始信号(START):主机首先发出起始条件,获取 I2C 总线的控制权。
-
发送设备地址(写方向):主机发送 8 位数据(包含 7 位从机地址及 1 位写标志位,即 0xD0),寻址目标硬件设备。
-
发送寄存器地址(伪写操作):主机向从机发送 8 位的内部寄存器地址数据。这一步虽然走的是写时序,但并不写入实际配置数据,其唯一目的是将 MPU6050 内部的读取指针定位到目标寄存器。
-
产生重复起始信号(Re-Start):在发送完寄存器地址并收到应答后,主机不发送停止信号,而是再次发送起始条件。此机制确保主机持续占有总线,防止多主机环境下总线被其他设备抢占。
-
发送设备地址(读方向):主机再次发送完整的 8 位数据帧。此次数据由 7 位从机地址与最低位的读写控制标志位(读操作为 1)拼接而成(即 0xD1)。此步骤将 I2C 总线的数据流向切换为从机发送、主机接收状态。
-
接收数据并处理应答:主机等待硬件将串行数据从移位寄存器转储至数据寄存器(DR)。针对 STM32 I2C 外设的双缓冲预取特性,在单字节读取场景下,软件必须在数据完全进入 DR(即 EV7 事件发生)之前,提前配置应答失能(NACK)与停止请求(STOP)。该操作通过干预底层硬件状态机,确保硬件在当前字节接收完成后的第 9 个时钟周期自动产生非应答信号并触发停止位,从而有效防止硬件因默认的连续读取逻辑而误触发下一字节的时钟脉冲。
cpp
/**
* 函 数:MPU6050读寄存器
* 参 数:RegAddress 目标寄存器地址
* 返 回 值:读取到的8位数据
*/
uint8_t MPU6050_ReadReg(uint8_t RegAddress)
{
uint8_t Data;
/* 阶段1:发送寄存器地址(伪写) */
I2C_GenerateSTART(I2C2, ENABLE);
MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT); // 等待起始位完成
I2C_Send7bitAddress(I2C2, 0xD0, I2C_Direction_Transmitter); // 发送地址+写方向
MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED);
I2C_SendData(I2C2, RegAddress); // 告知从机要读取的内部地址
MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTED); // 等待发送完成
/* 阶段2:切换为读模式(重复起始) */
I2C_GenerateSTART(I2C2, ENABLE); // 重复起始位
MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT);
I2C_Send7bitAddress(I2C2, 0xD0, I2C_Direction_Receiver); // 发送地址+读方向
MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED); // 等待EV6:接收模式已选定
/* 阶段3:接收数据处理(针对单字节读取的特殊流程) */
I2C_AcknowledgeConfig(I2C2, DISABLE); // 提前配置:读取该字节后不回传应答
I2C_GenerateSTOP(I2C2, ENABLE); // 提前配置:读取该字节后立即生成停止位
MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_RECEIVED); // 等待EV7:数据已到达DR寄存器
Data = I2C_ReceiveData(I2C2); // 读取DR中的数据
I2C_AcknowledgeConfig(I2C2, ENABLE); // 恢复应答使能,以便后续通信使用
return Data;
}
4.4 应用层接口封装
在底层的 I2C 硬件通信时序和寄存器读写接口构建完成后,需要在此基础上封装应用层 API。应用层的作用在于屏蔽底层的总线通信细节与硬件跃迁状态,直接面向业务逻辑提供标准化的功能函数,从而提升代码的可移植性与可读性。
4.4.1 设备 ID 读取
MPU6050 内部包含一个只读的身份验证寄存器 WHO_AM_I(地址 0x75),其出厂硬编码值固定为 0x68(默认状态下,该值不包含最低位的 AD0 引脚状态)。
在系统初始化阶段读取该寄存器是一项标准的工程实践。其核心目的是在全面接管传感器之前,验证硬件 I2C 通信链路的电气连接(如 SDA/SCL 上拉电阻、导线连通性)是否正常,以及从机硬件地址寻址是否准确响应。正常情况下,该函数返回值应严格匹配 0x68。
cpp
/**
* 函 数:MPU6050获取ID号
* 参 数:无
* 返 回 值:MPU6050的设备ID号(正常应为0x68)
*/
uint8_t MPU6050_GetID(void)
{
return MPU6050_ReadReg(MPU6050_WHO_AM_I); // 访问身份验证寄存器并返回结果
}
4.4.2 六轴原始数据解算
传感器的三轴加速度计和三轴陀螺仪产生的模拟电信号经过内部 ADC 转换后,每个轴的量化数据均被存储为 16 位的有符号整数(采用补码格式)。由于 I2C 总线的数据传输基本单位为 8 位,MPU6050 采用**大端模式(Big-Endian)**的内存模型来存储这些数据:即 16 位数据的高 8 位(High Byte)存储在低地址寄存器中,低 8 位(Low Byte)存储在相邻的高地址寄存器中。
cpp
/**
* 函 数:MPU6050获取全轴数据
* 参 数:AccX AccY AccZ 加速度计X、Y、Z轴的数据,使用输出参数的形式返回
* 参 数:GyroX GyroY GyroZ 陀螺仪X、Y、Z轴的数据,使用输出参数的形式返回
* 返 回 值:无
*/
void MPU6050_GetData(int16_t *AccX, int16_t *AccY, int16_t *AccZ,
int16_t *GyroX, int16_t *GyroY, int16_t *GyroZ)
{
uint8_t DataH, DataL;
/* 加速度计 X 轴数据解算 */
DataH = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_H); // 读取高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;
}
相关知识点:位运算拼接与补码符号位机制
从 I2C 总线接收到的原始数据表现为两个独立的 8 位无符号整型(uint8_t)。为了在微控制器内存中还原其原本的 16 位物理含义,必须在软件层面进行位运算拼接。
-
位权提升 :将读取到的高 8 位数据左移 8 位(
DataH << 8),使其占据 16 位数据的高半部分。 -
按位或操作 :将移位后的高 8 位与低 8 位数据进行逻辑按位或(
| DataL),完成 16 位数据帧的组装。 -
符号解析 :将拼接完成的 16 位二进制数据通过指针赋值给
int16_t类型的变量。由于现代微控制器架构(如 ARM Cortex-M3)底层以补码形式表示有符号数,C 语言编译器会根据目标变量的数据类型(int16_t)自动将最高位(第 15 位)严格解析为符号位,开发者无需额外编写判断正负号的条件分支语句。
5. 本章节实验
5.2 硬件I2C读写MPU6050
5.2.1 实验目标
-
掌握STM32片上I2C外设工作机制:理解硬件移位寄存器与数据寄存器(DR)构成的双缓冲架构在总线串行收发中的协同作用。
-
熟练配置硬件I2C外设参数:掌握利用标准外设库初始化 I2C 波特率、占空比配置、寻址模式等底层硬件行为的规范化流程。
-
实现基于状态机事件的异步时序控制:学习轮询外设状态寄存器特定标志位(EV5、EV6、EV8、EV7等事件)以推动总线收发状态机的工程方法,并引入超时防御机制防范系统死锁。
5.2.2 硬件设计

5.2.3 软件设计
本实验采用 STM32 片上硬件 I2C 外设替代软件模拟时序,硬件驱动层与业务应用层逻辑与"软件I2C"实验保持高度一致,具体流程如下:
(1)硬件I2C外设配置模块
-
外设总线与引脚复用:使能 I2C 外设对应的 APB1 时钟及 GPIO 时钟。将 SCL 与 SDA 引脚配置为复用开漏输出模式(Alternate Function Open-Drain),将引脚的电气控制权转移至内部 I2C 控制器。
-
控制器参数初始化 :填充
I2C_InitTypeDef结构体,配置通信速率(如 100kHz 标准模式或 400kHz 快速模式)、ACK 应答使能状态及自身从机地址参数,最终调用I2C_Init()固化硬件配置。
(2)硬件级时序控制逻辑
-
状态标志位轮询 :在发送起始位(Start)、传输器件地址/寄存器地址、收发数据流及生成停止位(Stop)的各个阶段,利用
I2C_CheckEvent()函数严格检查硬件产生的状态事件(如 EV5、EV6、EV8、EV7)。 -
读写机制的重构 :重写 MPU6050 的
WriteReg与ReadReg接口。特别是在单字节读取逻辑中,需在读取数据寄存器前,提前通过外设硬件接口清除 ACK 响应位并生成 Stop 条件,严格遵守硬件 I2C 的接收关断规范。
(3)上层数据解算模块
- 跨层代码复用:直接复用 6.1 节中 MPU6050 硬件驱动层的寄存器地址映射宏、初始化配置字流以及应用层的 16 位补码拼接逻辑。通信底层的硬件化替换对高层业务逻辑完全透明。
具体代码如下:
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 "MPU6050_Reg.h"
#define MPU6050_ADDRESS 0xD0 //MPU6050的I2C从机地址
/**
* 函 数:MPU6050等待事件
* 参 数:同I2C_CheckEvent
* 返 回 值:无
*/
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) //自减到0后,等待超时
{
/*超时的错误处理代码,可以添加到此处*/
break; //跳出等待,不等了
}
}
}
/**
* 函 数:MPU6050写寄存器
* 参 数:RegAddress 寄存器地址,范围:参考MPU6050手册的寄存器描述
* 参 数:Data 要写入寄存器的数据,范围:0x00~0xFF
* 返 回 值:无
*/
void MPU6050_WriteReg(uint8_t RegAddress, uint8_t Data)
{
I2C_GenerateSTART(I2C2, ENABLE); //硬件I2C生成起始条件
MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT); //等待EV5
I2C_Send7bitAddress(I2C2, MPU6050_ADDRESS, I2C_Direction_Transmitter); //硬件I2C发送从机地址,方向为发送
MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED); //等待EV6
I2C_SendData(I2C2, RegAddress); //硬件I2C发送寄存器地址
MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTING); //等待EV8
I2C_SendData(I2C2, Data); //硬件I2C发送数据
MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTED); //等待EV8_2
I2C_GenerateSTOP(I2C2, ENABLE); //硬件I2C生成终止条件
}
/**
* 函 数:MPU6050读寄存器
* 参 数:RegAddress 寄存器地址,范围:参考MPU6050手册的寄存器描述
* 返 回 值:读取寄存器的数据,范围:0x00~0xFF
*/
uint8_t MPU6050_ReadReg(uint8_t RegAddress)
{
uint8_t Data;
I2C_GenerateSTART(I2C2, ENABLE); //硬件I2C生成起始条件
MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT); //等待EV5
I2C_Send7bitAddress(I2C2, MPU6050_ADDRESS, I2C_Direction_Transmitter); //硬件I2C发送从机地址,方向为发送
MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED); //等待EV6
I2C_SendData(I2C2, RegAddress); //硬件I2C发送寄存器地址
MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTED); //等待EV8_2
I2C_GenerateSTART(I2C2, ENABLE); //硬件I2C生成重复起始条件
MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT); //等待EV5
I2C_Send7bitAddress(I2C2, MPU6050_ADDRESS, I2C_Direction_Receiver); //硬件I2C发送从机地址,方向为接收
MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED); //等待EV6
I2C_AcknowledgeConfig(I2C2, DISABLE); //在接收最后一个字节之前提前将应答失能
I2C_GenerateSTOP(I2C2, ENABLE); //在接收最后一个字节之前提前申请停止条件
MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_RECEIVED); //等待EV7
Data = I2C_ReceiveData(I2C2); //接收数据寄存器
I2C_AcknowledgeConfig(I2C2, ENABLE); //将应答恢复为使能,为了不影响后续可能产生的读取多字节操作
return Data;
}
/**
* 函 数:MPU6050初始化
* 参 数:无
* 返 回 值:无
*/
void MPU6050_Init(void)
{
/*开启时钟*/
RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C2, ENABLE); //开启I2C2的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); //开启GPIOB的时钟
/*GPIO初始化*/
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); //将PB10和PB11引脚初始化为复用开漏输出
/*I2C初始化*/
I2C_InitTypeDef I2C_InitStructure; //定义结构体变量
I2C_InitStructure.I2C_Mode = I2C_Mode_I2C; //模式,选择为I2C模式
I2C_InitStructure.I2C_ClockSpeed = 50000; //时钟速度,选择为50KHz
I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2; //时钟占空比,选择Tlow/Thigh = 2
I2C_InitStructure.I2C_Ack = I2C_Ack_Enable; //应答,选择使能
I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit; //应答地址,选择7位,从机模式下才有效
I2C_InitStructure.I2C_OwnAddress1 = 0x00; //自身地址,从机模式下才有效
I2C_Init(I2C2, &I2C_InitStructure); //将结构体变量交给I2C_Init,配置I2C2
/*I2C使能*/
I2C_Cmd(I2C2, ENABLE); //使能I2C2,开始运行
/*MPU6050寄存器初始化,需要对照MPU6050手册的寄存器描述配置,此处仅配置了部分重要的寄存器*/
MPU6050_WriteReg(MPU6050_PWR_MGMT_1, 0x01); //电源管理寄存器1,取消睡眠模式,选择时钟源为X轴陀螺仪
MPU6050_WriteReg(MPU6050_PWR_MGMT_2, 0x00); //电源管理寄存器2,保持默认值0,所有轴均不待机
MPU6050_WriteReg(MPU6050_SMPLRT_DIV, 0x09); //采样率分频寄存器,配置采样率
MPU6050_WriteReg(MPU6050_CONFIG, 0x06); //配置寄存器,配置DLPF
MPU6050_WriteReg(MPU6050_GYRO_CONFIG, 0x18); //陀螺仪配置寄存器,选择满量程为±2000°/s
MPU6050_WriteReg(MPU6050_ACCEL_CONFIG, 0x18); //加速度计配置寄存器,选择满量程为±16g
}
/**
* 函 数:MPU6050获取ID号
* 参 数:无
* 返 回 值:MPU6050的ID号
*/
uint8_t MPU6050_GetID(void)
{
return MPU6050_ReadReg(MPU6050_WHO_AM_I); //返回WHO_AM_I寄存器的值
}
/**
* 函 数:MPU6050获取数据
* 参 数:AccX AccY AccZ 加速度计X、Y、Z轴的数据,使用输出参数的形式返回,范围:-32768~32767
* 参 数:GyroX GyroY GyroZ 陀螺仪X、Y、Z轴的数据,使用输出参数的形式返回,范围:-32768~32767
* 返 回 值:无
*/
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位和低8位的变量
DataH = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_H); //读取加速度计X轴的高8位数据
DataL = MPU6050_ReadReg(MPU6050_ACCEL_XOUT_L); //读取加速度计X轴的低8位数据
*AccX = (DataH << 8) | DataL; //数据拼接,通过输出参数返回
DataH = MPU6050_ReadReg(MPU6050_ACCEL_YOUT_H); //读取加速度计Y轴的高8位数据
DataL = MPU6050_ReadReg(MPU6050_ACCEL_YOUT_L); //读取加速度计Y轴的低8位数据
*AccY = (DataH << 8) | DataL; //数据拼接,通过输出参数返回
DataH = MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_H); //读取加速度计Z轴的高8位数据
DataL = MPU6050_ReadReg(MPU6050_ACCEL_ZOUT_L); //读取加速度计Z轴的低8位数据
*AccZ = (DataH << 8) | DataL; //数据拼接,通过输出参数返回
DataH = MPU6050_ReadReg(MPU6050_GYRO_XOUT_H); //读取陀螺仪X轴的高8位数据
DataL = MPU6050_ReadReg(MPU6050_GYRO_XOUT_L); //读取陀螺仪X轴的低8位数据
*GyroX = (DataH << 8) | DataL; //数据拼接,通过输出参数返回
DataH = MPU6050_ReadReg(MPU6050_GYRO_YOUT_H); //读取陀螺仪Y轴的高8位数据
DataL = MPU6050_ReadReg(MPU6050_GYRO_YOUT_L); //读取陀螺仪Y轴的低8位数据
*GyroY = (DataH << 8) | DataL; //数据拼接,通过输出参数返回
DataH = MPU6050_ReadReg(MPU6050_GYRO_ZOUT_H); //读取陀螺仪Z轴的高8位数据
DataL = MPU6050_ReadReg(MPU6050_GYRO_ZOUT_L); //读取陀螺仪Z轴的低8位数据
*GyroZ = (DataH << 8) | DataL; //数据拼接,通过输出参数返回
}
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
5.2.4 实验现象
硬件复位并初始化后,OLED 屏幕第一行同样稳定显示设备 ID 号(0x68)。 对模块进行多轴静置、倾斜与翻转测试时,屏幕上 6 轴姿态数据的数值特征与刷新响应规律,与"软件I2C读写MPU6050"实验呈现出完全一致的预期结果。硬件 I2C 外设在解放 CPU 引脚翻转开销的同时,保证了无差异的通信可靠性与感测精度。