STM32 学习 —— 个人学习笔记10-2(I2C 通信外设 & 硬件 I2C 读写 MPU6050)

声明

文中内容为观看 BiliBili 视频【STM32入门教程-2023版 细致讲解 中文字幕】后学习并扩展总结。

本文章为个人学习使用,版面观感若有不适请谅解,文中知识仅代表个人观点,若出现错误,欢迎各位批评指正。

一、I2C 通信外设

1.1 I2C 外设简介

I2C(Inter-Integrated Circuit)总线 作为一种通用的串行通信总线,凭借其接口简单、占用 IO 资源少、通信可靠等优势,广泛应用于微控制器与外围器件(如传感器、存储器、显示器等)之间的短距离数据交互场景。STM32 系列微控制器内部集成了高性能硬件 I2C 收发电路,该外设通过硬件逻辑自动完成 I2C 通信过程中的关键操作,显著降低中央处理器(CPU)的负载,提升系统整体通信效率与稳定性,为嵌入式系统的低功耗、高可靠性设计提供有力支撑。​

STM32 硬件 I2C 外设的核心优势在于其高度的自动化通信能力,无需 CPU 持续介入即可完成完整的 I2C 通信流程,其核心自动化功能如下表所示:

自动化功能类型 具体功能描述 功能优势
时钟生成 通过硬件逻辑自动生成 I2C 通信所需的 SCL 时钟信号,精准控制时钟频率的稳定性与准确性 无需 CPU 手动控制时钟时序,降低指令开销
起始终止条件生成 自动生成起始条件(SDA 高电平拉低、SCL 保持高电平)与终止条件(SDA 低电平拉高、SCL 保持高电平) 确保通信时序规范,避免因手动控制失误导致的通信异常
应答位收发 自动完成应答位(ACK)与非应答位(NACK)的发送与检测 减少 CPU 中断响应次数,提升通信效率
数据收发 硬件自动完成数据的发送与接收,无需 CPU 逐字节干预 释放 CPU 资源,使其专注于核心任务执行

为适配不同的应用场景与通信需求,STM32 硬件 I2C 外设支持灵活的工作模式、地址配置及通信速率,相关参数规范如下表所示:

参数类别 具体参数 参数说明 应用场景
工作模式 多主机模型 同一 I2C 总线上可存在多个主机,通过总线仲裁机制避免通信冲突 多设备协同工作的复杂嵌入式系统
地址模式 7 位地址模式 标准地址格式,支持最多 128 个从设备地址 大多数常规 I2C 通信场景
地址模式 10 位地址模式 扩展地址空间,支持更多从设备接入 需大量从设备接入的复杂系统
通信速率 标准速度 最高可达 100 kHz,速率稳定、功耗较低 低速传感器数据采集等低功耗场景
通信速率 快速速度 最高可达 400 kHz,传输效率高 大容量存储器数据读写等高速传输场景

除上述核心特性外,STM32硬件I2C外设还具备以下关键功能,进一步提升系统通信性能与通用性:

  • 支持直接存储器访问(DMA)功能: DMA 控制器可与 I2C 外设协同工作,在无需 CPU 干预的情况下,实现数据在 I2C 外设与片内存储器(如 SRAM)之间的直接传输,不仅减少了 CPU 的中断响应次数,还避免了数据传输过程中的 CPU 中转开销,尤其适用于大量数据的连续收发场景,有效提升了系统的数据处理能力与实时性。
  • 兼容 SMBus 协议: SMBus 作为 I2C 总线的一种衍生协议,在时序、地址分配等方面与 I2C 总线保持兼容,同时增加了设备唤醒、总线超时检测等额外功能,使 STM32 硬件 I2C 外设可与支持 SMBus 协议的外围器件无缝通信,扩展了外设的应用范围,提升了系统的通用性。

不同型号的 STM32 微控制器所集成的硬件 I2C 资源存在差异,其中 STM32F103C8T6 型号微控制器作为应用广泛的入门级产品,其硬件 I2C 资源配置如下表所示:

微控制器型号 硬件 I2C 外设数量 具体外设标识 外设特性
STM32F103C8T6 2 个 I2C1、I2C2 可独立工作,支持多主机协同,可独立配置地址模式、通信速率、DMA 传输等参数

综上所述,STM32 系列微控制器的硬件 I2C 外设凭借其自动化程度高、工作模式灵活、通信速率可调、支持 DMA 传输及兼容 SMBus 协议等特性,有效降低了 CPU 负担,提升了系统的通信效率与可靠性,而 STM32F103C8T6 所集成的 I2C1 与 I2C2 外设,为中低端嵌入式系统的 I2C 通信应用提供了简洁、高效的硬件解决方案,广泛应用于智能检测、数据采集、工业控制等诸多领域。

1.2 I2C 框图

I2C(Inter-Integrated Circuit,集成电路总线)硬件外设是嵌入式系统中实现两线式串行通信的核心片上模块,其架构以主从式通信为核心,通过串行数据线(SDA)与串行时钟线(SCL)实现多设备间的双向数据传输,同时集成了时钟管理、数据处理、错误校验与中断 / DMA 控制等完整功能单元,以下基于架构图进行解析:

  • 1、核心硬件架构与模块划分
      I2C 外设的硬件架构可划分为物理接口层、数据处理层、时钟控制层、配置与状态层、控制与交互层五大功能域,各模块协同完成通信协议的硬件实现:

(1)物理接口与数据通路模块

SDA/SCL 物理接口: SDA(串行数据线)为双向开漏引脚,负责串行数据的发送与接收;SCL(串行时钟线)为双向开漏引脚,由主设备驱动通信时钟,从设备通过时钟同步实现数据采样。SMBALERT 引脚为系统管理总线(SMBus)扩展接口,用于从设备向主设备发送中断告警,实现总线事件的异步通知。

数据控制模块: 实现 SDA 引脚的双向驱动控制,完成并行 - 串行数据的转换与收发缓冲,协调数据移位寄存器与物理总线的时序匹配,保障数据在总线与片上寄存器间的可靠传输。

数据移位寄存器: 作为数据收发的核心缓冲单元,实现并行数据(来自 / 发往 DATA REGISTER 数据寄存器)与串  行比特流的双向转换:发送时将并行数据逐位串行输出至 SDA 总线,接收时将 SDA 总线上的串行比特流组装为并行数据并写入数据寄存器。

数据寄存器(DATA REGISTER): CPU 与 I²C 外设的交互接口,用于暂存待发送或已接收的并行数据,实现 CPU 与串行通信的异步解耦。

(2)地址匹配与错误校验模块

比较器: 将总线上接收的从设备地址与自身地址寄存器、双地址寄存器中的预设地址进行逐位比较,完成从设备的地址匹配与寻址,仅当地址匹配时,外设才响应主设备的通信请求,实现多从设备的总线仲裁。

自身地址寄存器 / 双地址寄存器: 存储从设备的 7 位 / 10 位从地址,支持双地址模式,使单个外设可响应两个不同的从地址,提升总线设备的灵活性。

帧错误校验(PEC)计算模块与 PEC 寄存器: 基于 CRC-8 多项式实现通信数据的硬件校验,在发送端生成 PEC 校验字节并附加至数据帧尾,在接收端对接收数据进行 CRC 校验,通过 PEC 寄存器存储校验结果,实现数据传输的完整性检测,符合 SMBus 协议的可靠性要求。

(3)时钟控制模块

时钟控制模块: 根据时钟控制寄存器(CCR)的配置,生成符合 I²C 协议时序的 SCL 时钟信号,支持标准模式(100 kbps)、快速模式(400 kbps)等多速率通信;同时实现时钟同步与拉伸(Clock Stretching)功能,从设备可拉低 SCL 时钟以暂停通信,保障数据处理的时序裕量。

时钟控制寄存器(CCR): 用于配置 SCL 时钟的分频系数、占空比与通信速率,实现不同总线频率的灵活适配。

(4) 配置、状态与控制模块

控制寄存器(CR1&CR2): 用于配置 I²C 外设的工作模式(主 / 从模式、发送 / 接收模式)、总线使能、中断使能、PEC 校验使能、DMA 使能等核心参数,是 CPU 对外设进行功能配置的核心接口。

状态寄存器(SR1&SR2): 实时反馈外设的通信状态,包括地址匹配、数据发送 / 接收完成、总线忙闲、错误状态(如仲裁丢失、应答错误、PEC 错误)等,为 CPU 提供通信事件的状态查询与中断触发依据。

控制逻辑电路: I2C 外设的核心控制单元,协调所有模块的时序与工作流程,实现 I²C 协议的完整硬件状态机:包括起始 / 停止条件生成、应答位收发、总线仲裁、地址匹配、数据收发、错误处理等,同时生成中断信号与 DMA 请求,实现 CPU 中断驱动或 DMA 驱动的通信。

(5)交互与扩展模块

中断输出: 当通信事件(如数据收发完成、地址匹配、错误发生)触发时,控制逻辑生成中断信号,通知 CPU 进行相应处理,实现异步通信的事件驱动。

DMA 请求与响应: 支持直接内存访问(DMA)交互,在数据收发完成时生成 DMA 请求,由 DMA 控制器实现数据在内存与 I2C 数据寄存器间的自动传输,无需 CPU 干预,大幅提升大数据量通信的效率,降低 CPU 负载。

  • 2、核心工作原理

    I2C 硬件外设通过硬件状态机实现 I2C 协议的全流程自动化:

    (1)主设备模式: 控制逻辑驱动时钟控制模块生成 SCL 时钟,通过数据控制模块在 SDA 总线上发送起始条件、从地址、数据与停止条件,完成对从设备的寻址与数据传输;

    (2)从设备模式: 通过比较器完成地址匹配,匹配成功后响应主设备的通信请求,在 SCL 时钟的同步下完成数据的收发,支持时钟拉伸以适配从设备的处理速度;

    (3)可靠性保障: 集成硬件 PEC 校验、总线仲裁、错误检测等机制,保障多主设备总线环境下的通信可靠性与数据完整性;

    (4)高效交互: 通过中断与 DMA 双模式支持,适配不同场景的通信需求,兼顾实时性与 CPU 利用率。

    技术特性与应用价值(学术补充)

  • 3、技术特性与应用价值

    (1)协议兼容性: 完全兼容 I2C 总线标准(v2.1)与 SMBus 2.0 协议,支持 7 位 / 10 位寻址、多主设备仲裁、时钟同步等核心特性;

    (2)硬件加速: 所有协议时序、地址匹配、CRC 校验均由硬件实现,无需 CPU 软件模拟,大幅提升通信效率与实时性;

    (3)低功耗适配: 开漏总线结构与低速率通信特性,适配嵌入式系统的低功耗设计需求;

    (4)扩展性: 通过两线总线实现多设备互联,仅需两根信号线即可完成多传感器、外设的组网,简化硬件设计。

1.3 主机发送序列图

I2C 主机发送流程以起始条件(S)启动,支持 7 位与 10 位两种从机地址寻址模式:7 位模式下主机直接发送从机地址,10 位模式下则先发送扩展帧头、再传输完整地址,地址匹配后进入数据传输阶段;整个过程通过事件驱动的中断机制(EV5~EV9)实现时序控制,主机逐字节发送数据并等待从机应答(A),其中 EV5、EV6、EV9、EV8_1、EV8_2 事件通过拉低 SCL 时钟保障软件处理时间,EV8 事件要求软件在当前字节传输结束前完成数据寄存器写入以确保传输连续性,待所有数据发送完成后,主机发起停止条件(P),由硬件自动清除相关状态位,完成本次通信。

1.4 主机接收序列图

I2C 主机接收流程以起始条件(S)启动,区分 7 位与 10 位两种寻址模式:7 位模式下主机发送地址后进入接收阶段,10 位模式则先发送帧头、写入地址高字节触发 EV9 清除事件,再发送地址低字节并触发 EV6 后需置位 CR2 的 START 位开启后续接收;全程通过 EV5、EV6、EV6_1、EV7、EV7_1、EV9 等事件驱动时序控制,EV5 为起始条件发送后的地址准备阶段,EV6 为地址匹配完成状态,EV6_1 适用于单字节接收场景需提前配置应答与停止位,EV7 为数据寄存器非空的接收就绪标志,读取 DR 即可清除该事件,EV7_1 为最后一字节接收标识,需配置 ACK=0 与停止位请求,待所有数据接收完毕后生成停止条件(P)完成通信,过程中相关事件通过拉低 SCL 时钟保障软件处理时序,确保数据接收的连续性与准确性。

二、硬件 I2C 读写 MPU6050

2.1 硬件 I2C 读写 MPU6050的实现
  • 首先,按下图接线方式,搭建面包板电路连接 OLED 显示屏,并将 MPU6050 的 SCL 和 SDA 分别与 PB10 和 PB11 连接,然后将 DAP-Link / ST-Link 连接到 STM32 最小系统板上,为使 OLED 显示屏的 VCC 和 GND 正确连接正负极,请先连接对应正负极跳线(或直接使用 GPIO 口进行供电)。

  • 直接复制先前演示的已有文件目录,重命名并双击后缀名为 .uvprojx 的文件打开工程文件,并对 main.c 进行修改,工程中所使用的全部头文件其详细内容已放于文末。

    #include "stm32f10x.h" // Device header
    #include "OLED.h"
    #include "MPU6050.h"

    int16_t AX, AY, AZ, GX, GY, GZ;

    int main(void)
    {
    OLED_Init();
    MPU_6050_Init();

    复制代码
      while (1)
      {
      	MPU6050_GetData(&AX, &AY, &AZ, &GX, &GY, &GZ);
          OLED_ShowSignedNum(2, 1, AX, 5);
          OLED_ShowSignedNum(3, 1, AY, 5);
          OLED_ShowSignedNum(4, 1, AZ, 5);
          OLED_ShowSignedNum(2, 8, GX, 5);
          OLED_ShowSignedNum(3, 8, GY, 5);
          OLED_ShowSignedNum(4, 8, GZ, 5);
      }

    }

  • 由于仅将软件 I2C 读写方式替换为硬件 I2C 读写方式,因此最终实现效果与上一节保持一致。

三、演示代码关联的头文件与源文件说明

  • OLED 相关头文件请从 STM32 学习 ------ 个人学习笔记4(OLED 显示屏及调试工具) 文末查看,此处不重复展示。

  • MPU6050.c

    #include "stm32f10x.h" // Device header
    #include "MPU6050_Reg.h"

    #define MPU6050_ADDRESS 0xD0

    void MPU6050_WaitEvent(I2C_TypeDef* I2Cx, uint32_t I2C_EVENT){
    uint32_t Timeout;
    Timeout = 10000;
    while (I2C_CheckEvent(I2Cx, I2C_EVENT) != SUCCESS){
    Timeout --;
    if (Timeout == 0){
    break;
    }
    }
    }

    void MPU6050_WriteReg(uint8_t RegAddress, uint8_t Data){
    I2C_GenerateSTART(I2C2, ENABLE);
    MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT);

    复制代码
      I2C_Send7bitAddress(I2C2, MPU6050_ADDRESS, I2C_Direction_Transmitter);
      MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED);
      
      I2C_SendData(I2C2, RegAddress);
      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 RegAddress){
    uint8_t Data;

    复制代码
      I2C_GenerateSTART(I2C2, ENABLE);
      MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT);
      
      I2C_Send7bitAddress(I2C2, MPU6050_ADDRESS, I2C_Direction_Transmitter);
      MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED);
      
      I2C_SendData(I2C2, RegAddress);
      MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTED);
      
      I2C_GenerateSTART(I2C2, ENABLE);
      MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT);
    
      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 MPU_6050_Init(void){
    // 提前声明需要使用到的结构体
    GPIO_InitTypeDef GPIO_InitStructure;
    I2C_InitTypeDef I2C_InitStructure;

    复制代码
      // 开启时钟
      RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C2, ENABLE);
      RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
      
      // 配置 PB10 和 PB11 为复用开漏模式
      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);
      
      // 初始化 I2C2 外设
      I2C_InitStructure.I2C_Mode = I2C_Mode_I2C;
      I2C_InitStructure.I2C_ClockSpeed = 50000;
      I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2;
      I2C_InitStructure.I2C_Ack = I2C_Ack_Enable;
      I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;       
      I2C_InitStructure.I2C_OwnAddress1 = 0x00;
      I2C_Init(I2C2, &I2C_InitStructure);
      
      // 配置 I2C2 使能
      I2C_Cmd(I2C2, ENABLE);
      
      // 配置电源管理寄存器1: 解除睡眠并选择陀螺仪时钟
      MPU6050_WriteReg(MPU6050_PWR_MGMT_1, 0x01);
      
      // 配置电源管理寄存器2: 6 个轴均不待机
      MPU6050_WriteReg(MPU6050_PWR_MGMT_2, 0x00);
      
      // 配置采样率分频寄存器: 采样分频为 10
      MPU6050_WriteReg(MPU6050_SMPLRT_DIV, 0x09);
      
      // 配置 配置寄存器: 滤波参数给最大
      MPU6050_WriteReg(MPU6050_CONFIG, 0x06);
      
      // 配置陀螺仪配置寄存器: 陀螺仪选择最大量程
      MPU6050_WriteReg(MPU6050_GYRO_CONFIG, 0x18);
      
      // 配置加速度配置寄存器: 加速度计选择最大量程
      MPU6050_WriteReg(MPU6050_ACCEL_CONFIG, 0x18);

    }

    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;

    }

  • MPU6050.h

    #ifndef __MPU6050_H
    #define __MPU6050_H

    void MPU6050_WriteReg(uint8_t RegAddress, uint8_t Data);
    void MPU_6050_Init(void);
    uint8_t MPU6050_ReadReg(uint8_t RegAddress);
    void MPU6050_GetData(int16_t *AccX, int16_t *AccY, int16_t *AccZ,
    int16_t *GyroX, int16_t *GyroY, int16_t *GyroZ);

    #endif

  • MPU6050_Reg.h

    #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


文中部分知识参考:B 站 ------ 江协科技;百度百科

相关推荐
chushiyunen2 小时前
ai人工智能笔记(二)
笔记
爱吃生蚝的于勒2 小时前
【Linux】重中之重!TCP协议
linux·运维·服务器·网络·学习·tcp/ip
zhensherlock2 小时前
Protocol Launcher 系列:1Writer iOS 上的 Markdown 文档管理
javascript·笔记·ios·typescript·node.js·iphone·ipad
旖-旎2 小时前
分治(计算右侧小于当前元素的个数)(7)
c++·学习·算法·leetcode·排序算法·归并排序
benpaodeDD2 小时前
JDBC内容学习
学习
EmmaXLZHONG2 小时前
Django By Example - 学习笔记
笔记·python·学习·django
prog_61033 小时前
【笔记】用cursor手搓cursor(五)再见claude
人工智能·笔记·大语言模型·agent
爱睡懒觉的焦糖玛奇朵3 小时前
【工业级落地算法之打架斗殴检测算法详解】
人工智能·python·深度学习·学习·算法·yolo·计算机视觉
扑火的小飞蛾3 小时前
OpenClaw Dashboard 部署与远程访问笔记
笔记