05-I2C通信详解.md 30 KB

STM32 I2C 通信

1. I2C 总线协议基础

总线结构

I2C(Inter-Integrated Circuit)总线只有两根线:

  • SCL(Serial Clock):串行时钟线,由主机产生
  • SDA(Serial Data):串行数据线,双向传输

总线上可以挂载多个设备,每个设备都有唯一的7 位地址。STM32 通常作为主机(Master),外设作为从机(Slave)。

开漏输出与线与逻辑

I2C 的 SCL 和 SDA 都采用开漏输出(Open-Drain),必须外接上拉电阻(典型值 4.7kΩ):

  • 当设备输出低电平时,拉低总线(驱动能力强)
  • 当设备输出高电平时,释放总线(由上拉电阻拉高)

多个设备可以同时向总线输出:只要有任何一个设备输出低电平,总线就是低电平——这就是线与(Wire-AND)逻辑。上拉电阻的阻值决定了总线上升时间,阻值越小速度越快但功耗越大。

I2C 硬件电路 上图:I2C 总线硬件连接(开漏输出 + 上拉电阻)(来源:STM32入门教程 PPT 第128页)

对比 UART:

  • UART:点对点通信,推挽输出,一对一连接
  • I2C:总线式通信,开漏输出,一对多连接(一主多从)

协议时序

空闲状态:SCL = 高,SDA = 高

起始条件(Start Condition):SCL 高电平时,SDA 从高→低跳变

SCL  ──┬──────
       │
SDA  ──┘───

停止条件(Stop Condition):SCL 高电平时,SDA 从低→高跳变

SCL     ──┬──
          │
SDA  ────┘

数据传输:SCL 低电平时 SDA 可变化,SCL 高电平时 SDA 保持稳定(接收方在 SCL 上升沿采样)。

应答(ACK):每传输完 8 位数据,接收方在第 9 个 SCL 周期拉低 SDA 表示应答。

I2C 起始条件与停止条件时序 上图:I2C 起始条件(Start)与停止条件(Stop)时序(来源:STM32入门教程 PPT 第129页)

数据帧格式

标准的 I2C 写数据帧:

起始位 | 7位地址 | R/W(0) | ACK | 数据1 | ACK | ... | 数据N | ACK/NACK | 停止位

标准的 I2C 读数据帧:

起始位 | 7位地址 | R/W(1) | ACK | 数据1 | ACK | ... | 数据N | NACK | 停止位

注意读操作结束后,主机需要发送 NACK(即释放 SDA)告诉从机停止发送,再发停止位。

速度模式

模式 最大波特率
标准模式 (SM) 100 kbit/s
快速模式 (FM) 400 kbit/s
快速增强模式 (FM+) 1 Mbit/s
高速模式 (HS) 3.4 Mbit/s

STM32F103 的 I2C 外设支持标准模式和快速模式。


2. STM32 I2C 外设配置

标准库

STM32 I2C 模块框图 上图:STM32 I2C 模块框图(来源:STM32入门教程 PPT 第141页)

STM32 I2C 内部结构 上图:STM32 I2C 内部结构(时钟控制、数据寄存器、移位寄存器)(来源:STM32入门教程 PPT 第142页)

I2C 模块引脚

以 I2C1 为例:

默认引脚 重映射后
PB6 (SCL) PB8 (SCL)
PB7 (SDA) PB9 (SDA)

引脚模式:复用开漏输出(GPIO_Mode_AF_OD)

为什么用复用 + 开漏?

  • 复用:因为由 I2C 模块内部硬件控制引脚,不是软件 GPIO 控制
  • 开漏:为了实现线与逻辑

时钟与复位

I2C1 挂在 APB1 总线上(注意不是 APB2):

// 1. 开启时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1, ENABLE);

// 2. 复位 I2C 模块(类似重启)
RCC_APB1PeriphResetCmd(RCC_APB1Periph_I2C1, ENABLE);   // 按下复位
RCC_APB1PeriphResetCmd(RCC_APB1Periph_I2C1, DISABLE);  // 松开复位

I2C_Init 参数

I2C_InitTypeDef I2C_InitStructure;

// 1. 波特率:400kHz
I2C_InitStructure.I2C_ClockSpeed = 400000;

// 2. 模式:标准 I2C(非 SMBus)
I2C_InitStructure.I2C_Mode = I2C_Mode_I2C;

// 3. 占空比(快速模式下可选)
I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2;  // 2:1 或 16:9

// 4. 本机地址(本设备作为从机时才用,主机可填任意值)
I2C_InitStructure.I2C_OwnAddress1 = 0x00;

// 5. 应答使能
I2C_InitStructure.I2C_Ack = I2C_Ack_Enable;

// 6. 7 位地址模式
I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;

I2C_Init(I2C1, &I2C_InitStructure);

使能 I2C

I2C_Cmd(I2C1, ENABLE);   // 闭合总开关

完整初始化示例

void MyI2C_Init(void) {
    // 1. 开启 GPIOB 时钟
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
    
    // 2. 开启 AFIO 时钟(重映射需要)
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);
    
    // 3. 重映射 I2C1 到 PB8/PB9
    GPIO_PinRemapConfig(GPIO_Remap_I2C1, ENABLE);
    
    // 4. 初始化 PB8(SCL) 和 PB9(SDA) 为复用开漏输出
    GPIO_InitTypeDef GPIO_InitStructure;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD;
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8 | GPIO_Pin_9;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_2MHz;
    GPIO_Init(GPIOB, &GPIO_InitStructure);
    
    // 5. 开启 I2C1 时钟
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1, ENABLE);
    
    // 6. 复位 I2C1
    RCC_APB1PeriphResetCmd(RCC_APB1Periph_I2C1, ENABLE);
    RCC_APB1PeriphResetCmd(RCC_APB1Periph_I2C1, DISABLE);
    
    // 7. 初始化 I2C1
    I2C_InitTypeDef I2C_InitStructure;
    I2C_InitStructure.I2C_ClockSpeed = 400000;
    I2C_InitStructure.I2C_Mode = I2C_Mode_I2C;
    I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2;
    I2C_InitStructure.I2C_OwnAddress1 = 0x00;
    I2C_InitStructure.I2C_Ack = I2C_Ack_Enable;
    I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;
    I2C_Init(I2C1, &I2C_InitStructure);
    
    // 8. 使能 I2C1
    I2C_Cmd(I2C1, ENABLE);
}

HAL库

使用 STM32CubeMX 生成的 HAL 库代码,I2C 初始化通过 I2C_HandleTypeDef 结构体完成:

/* HAL库 — I2C 句柄定义与初始化 */
I2C_HandleTypeDef hi2c1;

void MX_I2C1_Init(void)
{
    hi2c1.Instance = I2C1;
    hi2c1.Init.ClockSpeed = 400000;          // 400kHz
    hi2c1.Init.DutyCycle = I2C_DUTYCYCLE_2;  // 占空比 2:1
    hi2c1.Init.OwnAddress1 = 0;              // 本机地址(主机模式不用)
    hi2c1.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT;
    hi2c1.Init.DualAddressMode = I2C_DUALADDRESS_DISABLE;
    hi2c1.Init.OwnAddress2 = 0;
    hi2c1.Init.GeneralCallMode = I2C_GENERALCALL_DISABLE;
    hi2c1.Init.NoStretchMode = I2C_NOSTRETCH_DISABLE;
    HAL_I2C_Init(&hi2c1);
}

/* HAL 库的 MSP 初始化(引脚与时钟)由 HAL_I2C_MspInit 回调完成 */
void HAL_I2C_MspInit(I2C_HandleTypeDef* hi2c)
{
    GPIO_InitTypeDef GPIO_InitStruct = {0};
    if (hi2c->Instance == I2C1) {
        __HAL_RCC_GPIOB_CLK_ENABLE();
        __HAL_RCC_I2C1_CLK_ENABLE();

        GPIO_InitStruct.Pin = GPIO_PIN_6 | GPIO_PIN_7;           // PB6=SCL, PB7=SDA
        GPIO_InitStruct.Mode = GPIO_MODE_AF_OD;
        GPIO_InitStruct.Pull = GPIO_NOPULL;
        GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
        HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
    }
}

关键区别:

  • 结构体:标准库用 I2C_InitTypeDef + I2C_Init();HAL 库用 I2C_HandleTypeDef + HAL_I2C_Init()
  • MSP 回调:HAL 库通过 HAL_I2C_MspInit() 统一管理引脚时钟和 GPIO 初始化
  • 地址处理:HAL 库的 HAL_I2C_Mem_Write/Read 要求传入7 位原始地址(不含 R/W 位),如 MPU6050 传入 0x68;而标准库中传的是左对齐的 0xD0

3. I2C 写数据

I2C 模块内部结构

关键部件:

  • 发送数据寄存器 (DR):写入要发送的字节
  • 接收数据寄存器 (DR):读取接收到的字节
  • 移位寄存器:逐位移位发送/接收
  • SR1 (状态寄存器 1):包含 SB、ADDR、BTF、TXE、RXNE、AF 等标志位
  • SR2 (状态寄存器 2):包含 BUSY、MSL 等标志位
  • CR1 (控制寄存器 1):包含 START、STOP、ACK、PE 等控制位
  • SCL 控制电路:产生时钟信号,配置波特率和占空比

关键标志位

标志位 含义
BUSY (SR2) 总线忙标志,=1 时总线正在通信
SB (SR1) 起始位已发送
ADDR (SR1) 地址已发送且收到 ACK(寻址成功)
AF (SR1) 应答失败(NACK received)
TXE (SR1) 发送数据寄存器空,可写入下一个数据
BTF (SR1) Byte Transfer Finished,字节传输完成(移位寄存器和 DR 均空)
RXNE (SR1) 接收数据寄存器非空,有数据可读

写数据完整流程

/**
 * 通过 I2C 发送多个字节
 * @param I2Cx  I2C 外设地址(I2C1 或 I2C2)
 * @param Addr  从机 7 位地址(左对齐)
 * @param pData 要发送的数据数组
 * @param Size  要发送的字节数
 * @return 0=成功, 1=寻址失败, 2=数据被拒收
 */
int MyI2C_SendBytes(I2C_TypeDef* I2Cx, uint8_t Addr, uint8_t *pData, uint16_t Size) {
    // === 阶段一:等待总线空闲 ===
    while (I2C_GetFlagStatus(I2Cx, I2C_FLAG_BUSY));
    
    // === 阶段二:发送起始位 ===
    I2C_GenerateSTART(I2Cx, ENABLE);
    while (!I2C_GetFlagStatus(I2Cx, I2C_FLAG_SB));  // 等待起始位发送完成
    
    // === 阶段三:寻址阶段 ===
    I2C_ClearFlag(I2Cx, I2C_FLAG_AF);               // 先清除 AF 标志位
    I2C_SendData(I2Cx, Addr & 0xFE);                 // 地址 + R/W=0(写)
    
    // 等待寻址结果
    while (1) {
        if (I2C_GetFlagStatus(I2Cx, I2C_FLAG_ADDR))     // 寻址成功
            break;
        if (I2C_GetFlagStatus(I2Cx, I2C_FLAG_AF)) {     // 寻址失败
            I2C_GenerateSTOP(I2Cx, ENABLE);
            return 1;
        }
    }
    
    // 清除 ADDR 标志位(必须:先读 SR1,再读 SR2)
    I2C_ReadRegister(I2Cx, I2C_Register_SR1);
    I2C_ReadRegister(I2Cx, I2C_Register_SR2);
    
    // === 阶段四:发送数据 ===
    for (uint16_t i = 0; i < Size; i++) {
        // 等待可以发送下一个字节
        while (1) {
            if (I2C_GetFlagStatus(I2Cx, I2C_FLAG_AF)) {  // 数据被拒收
                I2C_GenerateSTOP(I2Cx, ENABLE);
                return 2;
            }
            if (I2C_GetFlagStatus(I2Cx, I2C_FLAG_TXE))   // DR 空,可以写入
                break;
        }
        I2C_SendData(I2Cx, pData[i]);                // 写入发送数据寄存器
    }
    
    // 等待最后一个字节发送完成
    while (1) {
        if (I2C_GetFlagStatus(I2Cx, I2C_FLAG_AF)) {
            I2C_GenerateSTOP(I2Cx, ENABLE);
            return 2;
        }
        if (I2C_GetFlagStatus(I2Cx, I2C_FLAG_BTF))   // 整个字节传输完成
            break;
    }
    
    // === 阶段五:发送停止位 ===
    I2C_GenerateSTOP(I2Cx, ENABLE);
    return 0;   // 成功
}

关键点:

  • 左对齐地址:7 位地址存放在高 7 位,最低位为 R/W 位。如 OLED 地址 0x78 = 0111 1000,高 7 位 0111 100 即 0x3C 左移一位
  • ADDR 清零:必须先读 SR1 再读 SR2,这是 STM32 的硬件规定
  • BF 标志位:当 TXE=1(DR 空)且移位寄存器空时,BTF=1,表示整个传输完成
  • AF 检查时机:地址阶段和数据阶段都要检查 AF 标志位

示例:点亮 OLED

uint8_t Command[] = {0x00, 0x8D, 0x14, 0xAF, 0xA5};  // OLED 开启命令

int main(void) {
    OLED_Init();    // 内部调用 MyI2C_Init()
    
    MyI2C_SendBytes(I2C1, 0x78, Command, 5);
    
    // OLED 屏幕点亮
    while (1);
}

OLED 地址是 0x78,这里传的是已经左对齐的地址(最低位为 R/W 位,函数内做了 & 0xFE 处理)。

HAL库写操作

HAL 库将 I2C 写操作封装为两个高级函数,无需手动操作 EV5/EV6/EV7 事件标志位:

/* HAL库 — I2C 主机发送(直接发送数据,无寄存器地址) */
// 原型:HAL_I2C_Master_Transmit(&hi2c1, DevAddress, pData, Size, Timeout)
// DevAddress:7位从机地址左移1位(即含R/W位的8位地址)
// pData:要发送的数据缓冲区
// Size:字节数
// Timeout:超时时间(ms)
HAL_StatusTypeDef status = HAL_I2C_Master_Transmit(
    &hi2c1,
    0xD0,           // MPU6050 地址(左对齐,含R/W位)
    Command,        // 数据缓冲区
    5,              // 字节数
    100             // 超时100ms
);
if (status != HAL_OK) {
    Error_Handler();  // 处理错误
}

/* HAL库 — I2C 写寄存器(Mem Write,带寄存器地址) */
// 原型:HAL_I2C_Mem_Write(&hi2c1, DevAddress, MemAddress, MemAddSize, pData, Size, Timeout)
// DevAddress:7位从机地址左移1位
// MemAddress:寄存器地址
// MemAddSize:寄存器地址长度(8位或16位)
HAL_StatusTypeDef ret = HAL_I2C_Mem_Write(
    &hi2c1,
    0xD0,           // 从机地址
    0x6B,           // 寄存器地址(PWR_MGMT_1)
    I2C_MEMADD_SIZE_8BIT,  // 寄存器地址为8位
    (uint8_t[]){0x01},     // 写入的数据
    1,              // 1个字节
    100
);

/* HAL库 — 同时写多个寄存器 */
uint8_t configData[] = {0x6B, 0x01, 0x6C, 0x00, 0x19, 0x09};
HAL_I2C_Master_Transmit(&hi2c1, 0xD0, configData, 6, 100);

HAL 库内部自动处理了:

  1. 生成起始条件(EV5:SB 标志位)
  2. 发送地址 + 等待 ADDR 标志(EV6)
  3. 逐个字节发送数据 + 等待 TXE/BTF
  4. 生成停止条件
  5. 超时重试与错误状态返回

4. I2C 读数据

读数据流程

读操作的流程和写类似,但有三个关键区别:

  1. 发送地址时 RW=1Addr | 0x01
  2. 接收最后一个字节后主机发送 NACK,告诉从机停止发送
  3. 需要重复起始位(Repeated Start):先写寄存器地址,再切换为读模式

    uint8_t MyI2C_ReadByte(I2C_TypeDef* I2Cx, uint8_t Addr, uint8_t RegAddr) {
    uint8_t Data;
        
    // 阶段 1-2:起始位
    while (I2C_GetFlagStatus(I2Cx, I2C_FLAG_BUSY));
    I2C_GenerateSTART(I2Cx, ENABLE);
    while (!I2C_GetFlagStatus(I2Cx, I2C_FLAG_SB));
        
    // 阶段 3:写从机地址(写方向)—— 先写寄存器地址
    I2C_ClearFlag(I2Cx, I2C_FLAG_AF);
    I2C_SendData(I2Cx, Addr & 0xFE);
    while (!I2C_GetFlagStatus(I2Cx, I2C_FLAG_ADDR));
    I2C_ReadRegister(I2Cx, I2C_Register_SR1);
    I2C_ReadRegister(I2Cx, I2C_Register_SR2);
        
    // 发送寄存器地址
    while (!I2C_GetFlagStatus(I2Cx, I2C_FLAG_TXE));
    I2C_SendData(I2Cx, RegAddr);
    while (!I2C_GetFlagStatus(I2Cx, I2C_FLAG_BTF));
        
    // 阶段 4:重复起始位 —— 切换方向
    I2C_GenerateSTART(I2Cx, ENABLE);
    while (!I2C_GetFlagStatus(I2Cx, I2C_FLAG_SB));
        
    // 阶段 5:写从机地址(读方向)
    I2C_ClearFlag(I2Cx, I2C_FLAG_AF);
    I2C_SendData(I2Cx, Addr | 0x01);
    while (!I2C_GetFlagStatus(I2Cx, I2C_FLAG_ADDR));
    I2C_ReadRegister(I2Cx, I2C_Register_SR1);
    I2C_ReadRegister(I2Cx, I2C_Register_SR2);
        
    // 阶段 6:接收数据 + 发 NACK
    I2C_AcknowledgeConfig(I2Cx, DISABLE);   // 不发 ACK
    while (!I2C_GetFlagStatus(I2Cx, I2C_FLAG_RXNE));
    Data = I2C_ReceiveData(I2Cx);
        
    // 阶段 7:停止位
    I2C_GenerateSTOP(I2Cx, ENABLE);
    I2C_AcknowledgeConfig(I2Cx, ENABLE);    // 恢复 ACK
        
    return Data;
    }
    

HAL库读操作

/* HAL库 — I2C 主机接收(直接读取从机数据,无寄存器地址) */
// 原型:HAL_I2C_Master_Receive(&hi2c1, DevAddress, pData, Size, Timeout)
uint8_t rxBuffer[6];
HAL_StatusTypeDef status = HAL_I2C_Master_Receive(
    &hi2c1,
    0xD0,           // 从机地址(左对齐)
    rxBuffer,       // 接收缓冲区
    6,              // 接收6个字节
    100
);

/* HAL库 — I2C 读寄存器(Mem Read,先写寄存器地址,再读数据) */
// 原型:HAL_I2C_Mem_Read(&hi2c1, DevAddress, MemAddress, MemAddSize, pData, Size, Timeout)
// 内部自动完成:Write(RegAddr) → Repeated Start → Read
uint8_t data;
HAL_StatusTypeDef ret = HAL_I2C_Mem_Read(
    &hi2c1,
    0xD0,                       // 从机地址
    0x3B,                       // 起始寄存器地址(ACCEL_XOUT_H)
    I2C_MEMADD_SIZE_8BIT,       // 8位寄存器地址
    &data,                      // 接收缓冲区
    1,                          // 读取1个字节
    100
);

/* HAL库 — 连续读取多个寄存器(MPU6050 获取全部6轴数据) */
uint8_t mpuBuffer[14];  // ACCEL(6) + TEMP(2) + GYRO(6) = 14 bytes
HAL_I2C_Mem_Read(&hi2c1, 0xD0, 0x3B, I2C_MEMADD_SIZE_8BIT,
                 mpuBuffer, 14, 100);

// 解析数据
int16_t AccX = (mpuBuffer[0] << 8) | mpuBuffer[1];
int16_t AccY = (mpuBuffer[2] << 8) | mpuBuffer[3];
int16_t AccZ = (mpuBuffer[4] << 8) | mpuBuffer[5];
int16_t GyroX = (mpuBuffer[8] << 8) | mpuBuffer[9];
int16_t GyroY = (mpuBuffer[10] << 8) | mpuBuffer[11];
int16_t GyroZ = (mpuBuffer[12] << 8) | mpuBuffer[13];

HAL 库的 HAL_I2C_Mem_Read 内部自动执行:Start → 地址+W → 寄存器地址 → Repeated Start → 地址+R → NACK → Stop,完全封装了重复起始位和 NACK 发送逻辑。


5. 软件 I2C(GPIO 模拟)

如果单片机没有硬件 I2C 外设,或者引脚不够灵活,可以用 GPIO 模拟 I2C 时序。

引脚配置

PB10 (SCL) 和 PB11 (SDA) 配置为开漏输出(也可用推挽输出模拟):

GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;

时序模拟

#define I2C_SCL_HIGH()  GPIO_SetBits(GPIOB, GPIO_Pin_10)
#define I2C_SCL_LOW()   GPIO_ResetBits(GPIOB, GPIO_Pin_10)
#define I2C_SDA_HIGH()  GPIO_SetBits(GPIOB, GPIO_Pin_11)
#define I2C_SDA_LOW()   GPIO_ResetBits(GPIOB, GPIO_Pin_11)
#define I2C_SDA_READ()  GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11)

void MyI2C_Start(void) {
    I2C_SDA_HIGH();
    I2C_SCL_HIGH();
    Delay_us(4);
    I2C_SDA_LOW();      // SCL 高电平时 SDA 下降沿 → 起始位
    Delay_us(4);
    I2C_SCL_LOW();      // 拉低 SCL 准备传输数据
}

void MyI2C_Stop(void) {
    I2C_SDA_LOW();
    I2C_SCL_HIGH();
    Delay_us(4);
    I2C_SDA_HIGH();     // SCL 高电平时 SDA 上升沿 → 停止位
    Delay_us(4);
}

void MyI2C_SendByte(uint8_t Byte) {
    for (uint8_t i = 0; i < 8; i++) {
        if (Byte & 0x80) I2C_SDA_HIGH();
        else             I2C_SDA_LOW();
        Byte <<= 1;
        I2C_SCL_HIGH();     // SCL 高电平采样
        Delay_us(4);
        I2C_SCL_LOW();
        Delay_us(4);
    }
}

uint8_t MyI2C_ReceiveByte(void) {
    uint8_t Byte = 0;
    I2C_SDA_HIGH();         // 释放 SDA,让从机控制
    for (uint8_t i = 0; i < 8; i++) {
        Byte <<= 1;
        I2C_SCL_HIGH();
        Delay_us(2);
        if (I2C_SDA_READ()) Byte |= 0x01;
        I2C_SCL_LOW();
        Delay_us(2);
    }
    return Byte;
}

void MyI2C_SendAck(uint8_t AckBit) {
    if (AckBit) I2C_SDA_HIGH();   // NACK
    else        I2C_SDA_LOW();    // ACK
    I2C_SCL_HIGH();
    Delay_us(4);
    I2C_SCL_LOW();
    I2C_SDA_HIGH();               // 释放 SDA
    Delay_us(4);
}

uint8_t MyI2C_ReceiveAck(void) {
    uint8_t AckBit;
    I2C_SDA_HIGH();               // 释放 SDA,让从机拉低
    I2C_SCL_HIGH();
    Delay_us(4);
    AckBit = I2C_SDA_READ();      // 0=ACK, 1=NACK
    I2C_SCL_LOW();
    Delay_us(4);
    return AckBit;
}

软件 I2C 与 MPU6050

MPU6050 是常用的六轴传感器(三轴加速度 + 三轴陀螺仪),I2C 从机地址为 0xD0(含 R/W 位,即 7 位地址 0x68 左移一位)。

#define MPU6050_ADDRESS     0xD0      // 含 R/W 位

// 写寄存器
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();
}

// 读寄存器
uint8_t MPU6050_ReadReg(uint8_t RegAddress) {
    uint8_t Data;
    
    MyI2C_Start();
    MyI2C_SendByte(MPU6050_ADDRESS);   // 写方向
    MyI2C_ReceiveAck();
    MyI2C_SendByte(RegAddress);        // 指定寄存器地址
    MyI2C_ReceiveAck();
    
    MyI2C_Start();                     // 重复起始位
    MyI2C_SendByte(MPU6050_ADDRESS | 0x01);  // 读方向
    MyI2C_ReceiveAck();
    Data = MyI2C_ReceiveByte();
    MyI2C_SendAck(1);                  // 发 NACK
    MyI2C_Stop();
    
    return Data;
}

// 初始化 MPU6050
void MPU6050_Init(void) {
    MyI2C_Init();
    // 电源管理:取消睡眠模式,选择 X 轴陀螺仪为时钟源
    MPU6050_WriteReg(0x6B, 0x01);     // PWR_MGMT_1
    // 使能所有轴
    MPU6050_WriteReg(0x6C, 0x00);     // PWR_MGMT_2
    // 采样率:1kHz
    MPU6050_WriteReg(0x19, 0x09);     // SMPLRT_DIV
    // DLPF 配置
    MPU6050_WriteReg(0x1A, 0x06);     // CONFIG
    // 陀螺仪满量程 ±2000°/s
    MPU6050_WriteReg(0x1B, 0x18);     // GYRO_CONFIG
    // 加速度计满量程 ±16g
    MPU6050_WriteReg(0x1C, 0x18);     // ACCEL_CONFIG
}

// 读取原始数据
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(0x3B);   // ACCEL_XOUT_H
    DataL = MPU6050_ReadReg(0x3C);
    *AccX = (DataH << 8) | DataL;
    
    DataH = MPU6050_ReadReg(0x3D);   // ACCEL_YOUT_H
    DataL = MPU6050_ReadReg(0x3E);
    *AccY = (DataH << 8) | DataL;
    
    DataH = MPU6050_ReadReg(0x3F);   // ACCEL_ZOUT_H
    DataL = MPU6050_ReadReg(0x40);
    *AccZ = (DataH << 8) | DataL;
    
    DataH = MPU6050_ReadReg(0x43);   // GYRO_XOUT_H
    DataL = MPU6050_ReadReg(0x44);
    *GyroX = (DataH << 8) | DataL;
    
    DataH = MPU6050_ReadReg(0x45);   // GYRO_YOUT_H
    DataL = MPU6050_ReadReg(0x46);
    *GyroY = (DataH << 8) | DataL;
    
    DataH = MPU6050_ReadReg(0x47);   // GYRO_ZOUT_H
    DataL = MPU6050_ReadReg(0x48);
    *GyroZ = (DataH << 8) | DataL;
}

硬件 I2C(标准库 — EV5/EV6/EV7 事件驱动)

如果使用 STM32 硬件 I2C 外设 + 标准库,MPU6050 驱动通过 I2C 事件标志位控制时序(EV5 = SB起始已发送,EV6 = ADDR寻址成功,EV7 = RXNE数据可读):

/* 标准库 — 硬件 I2C MPU6050 驱动(基于 I2C2,PB10/PB11) */

#define MPU6050_ADDRESS     0xD0

/* 等待 I2C 事件,带超时 */
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) break;
    }
}

/* 硬件 I2C 写寄存器:发送起始位 → EV5 → 地址+W → EV6 → 数据 → EV8 → EV8_2 → 停止 */
void MPU6050_WriteReg(uint8_t RegAddress, uint8_t Data)
{
    I2C_GenerateSTART(I2C2, ENABLE);
    MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT);          // EV5

    I2C_Send7bitAddress(I2C2, MPU6050_ADDRESS, I2C_Direction_Transmitter);
    MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED); // EV6

    I2C_SendData(I2C2, RegAddress);
    MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTING);    // EV8

    I2C_SendData(I2C2, Data);
    MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTED);     // EV8_2

    I2C_GenerateSTOP(I2C2, ENABLE);
}

/* 硬件 I2C 读寄存器:起始 → EV5 → 地址+W → EV6 → 寄存器地址 → EV8_2 → 重复起始 → EV5 → 地址+R → EV6 → 数据 → EV7 → 停止 */
uint8_t MPU6050_ReadReg(uint8_t RegAddress)
{
    uint8_t Data;

    I2C_GenerateSTART(I2C2, ENABLE);
    MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT);          // EV5

    I2C_Send7bitAddress(I2C2, MPU6050_ADDRESS, I2C_Direction_Transmitter);
    MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED); // EV6

    I2C_SendData(I2C2, RegAddress);
    MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTED);     // EV8_2

    I2C_GenerateSTART(I2C2, ENABLE);                                // 重复起始位
    MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT);          // EV5

    I2C_Send7bitAddress(I2C2, MPU6050_ADDRESS, I2C_Direction_Receiver);
    MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED); // EV6

    I2C_AcknowledgeConfig(I2C2, DISABLE);                            // 最后一字节前失能ACK
    I2C_GenerateSTOP(I2C2, ENABLE);                                   // 提前请求停止

    MPU6050_WaitEvent(I2C2, I2C_EVENT_MASTER_BYTE_RECEIVED);         // EV7
    Data = I2C_ReceiveData(I2C2);

    I2C_AcknowledgeConfig(I2C2, ENABLE);                              // 恢复ACK

    return Data;
}

void MPU6050_Init(void)
{
    /* GPIO 初始化 */
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
    GPIO_InitTypeDef GPIO_InitStructure;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD;
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10 | GPIO_Pin_11;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOB, &GPIO_InitStructure);

    /* I2C2 初始化 */
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C2, ENABLE);
    I2C_InitTypeDef I2C_InitStructure;
    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);
    I2C_Cmd(I2C2, ENABLE);

    /* MPU6050 配置 */
    MPU6050_WriteReg(0x6B, 0x01);
    MPU6050_WriteReg(0x6C, 0x00);
    MPU6050_WriteReg(0x19, 0x09);
    MPU6050_WriteReg(0x1A, 0x06);
    MPU6050_WriteReg(0x1B, 0x18);
    MPU6050_WriteReg(0x1C, 0x18);
}

事件与标志位对应关系:

I2C 事件 标志位 说明
I2C_EVENT_MASTER_MODE_SELECT SB EV5:起始位已发送
I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED ADDR EV6:地址已发送且收到 ACK(发送模式)
I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED ADDR EV6:地址已发送且收到 ACK(接收模式)
I2C_EVENT_MASTER_BYTE_TRANSMITTING TXE EV8:数据寄存器空,可发送下一字节
I2C_EVENT_MASTER_BYTE_TRANSMITTED BTF EV8_2:字节传输完成(DR + 移位寄存器均空)
I2C_EVENT_MASTER_BYTE_RECEIVED RXNE EV7:收到一个字节,数据可读

HAL库下的 MPU6050 驱动

如果使用 HAL 库,MPU6050 驱动可简化为直接调用 HAL_I2C_Mem_Write/Read,不需要自己实现软件 I2C 时序:

/* HAL库 — MPU6050 驱动(基于 HAL_I2C_Mem_Write/Read) */

#define MPU6050_ADDR_7BIT   0x68      // 7位原始地址(不含R/W位)
#define MPU6050_ADDR       0xD0      // 左对齐地址(含R/W位),供 HAL 库函数使用

void MPU6050_WriteReg(uint8_t RegAddress, uint8_t Data)
{
    HAL_I2C_Mem_Write(&hi2c1, MPU6050_ADDR, RegAddress,
                      I2C_MEMADD_SIZE_8BIT, &Data, 1, 100);
}

uint8_t MPU6050_ReadReg(uint8_t RegAddress)
{
    uint8_t Data;
    HAL_I2C_Mem_Read(&hi2c1, MPU6050_ADDR, RegAddress,
                     I2C_MEMADD_SIZE_8BIT, &Data, 1, 100);
    return Data;
}

void MPU6050_Init(void)
{
    MX_I2C1_Init();                              // 初始化硬件 I2C
    MPU6050_WriteReg(0x6B, 0x01);               // PWR_MGMT_1
    MPU6050_WriteReg(0x6C, 0x00);               // PWR_MGMT_2
    MPU6050_WriteReg(0x19, 0x09);               // SMPLRT_DIV
    MPU6050_WriteReg(0x1A, 0x06);               // CONFIG
    MPU6050_WriteReg(0x1B, 0x18);               // GYRO_CONFIG (±2000°/s)
    MPU6050_WriteReg(0x1C, 0x18);               // ACCEL_CONFIG (±16g)
}

void MPU6050_GetData(int16_t *AccX, int16_t *AccY, int16_t *AccZ,
                     int16_t *GyroX, int16_t *GyroY, int16_t *GyroZ)
{
    uint8_t buf[14];
    HAL_I2C_Mem_Read(&hi2c1, MPU6050_ADDR, 0x3B,
                       I2C_MEMADD_SIZE_8BIT, buf, 14, 100);
    *AccX = (buf[0]  << 8) | buf[1];
    *AccY = (buf[2]  << 8) | buf[3];
    *AccZ = (buf[4]  << 8) | buf[5];
    *GyroX = (buf[8] << 8) | buf[9];
    *GyroY = (buf[10] << 8) | buf[11];
    *GyroZ = (buf[12] << 8) | buf[13];
}

对比标准库方案,HAL 库方案的优点:

  • 无需手动操作 EV5/EV6/EV7 事件——HAL 库内部封装
  • HAL_I2C_Mem_Read 一次性完成写寄存器地址 + 重复起始 + 读数据
  • 地址传入方式不同HAL_I2C_Mem_Write/Read 的 DevAddress 参数传左对齐地址(含 R/W 位,如 0xD0)

6. 硬件 I2C vs 软件 I2C

对比 硬件 I2C 软件 I2C
CPU 占用 低(硬件控制时序) 高(CPU 逐位模拟)
引脚灵活性 需指定引脚 任意 GPIO
速度 可达 400kHz 取决于延时精度
抗干扰 好(硬件滤波)
编程复杂度 较高(寄存器/标志位多) 简单直观
适用场景 高速、复杂系统 低速、原型验证、引脚不足

7. 常见问题

总线锁死

I2C 总线有时会出现 SDA 一直被拉低的情况。可能原因:

  • 从机异常:从机未完成操作就收到了新起始位
  • 时钟拉伸:从机拉低 SCL 延长时钟

解决方法:强制发送 9 个时钟脉冲,让从机释放 SDA。

上拉电阻

I2C 总线上必须接上拉电阻。如果未接或阻值过大,SCL/SDA 波形上升沿过缓,通信不可靠。建议阻值 4.7kΩ(标准模式)到 1kΩ(快速模式)。

地址冲突

多个 I2C 设备使用相同地址会导致冲突。MPU6050 的 AD0 引脚可改变地址:

  • AD0 = 0:地址 0x68
  • AD0 = 1:地址 0x69

8. 参考代码索引

  • 10-1 软件I2C读写MPU6050\Hardware\MyI2C.c — 软件 I2C GPIO 模拟实现
  • 10-1 软件I2C读写MPU6050\Hardware\MPU6050.c — MPU6050 驱动
  • 10-1 软件I2C读写MPU6050\User\main.c — 主程序示例
  • 14-[STM32 HAL库][I2C]基础知识 — I2C 总线协议
  • 15-[STM32 HAL库][I2C]简单数据收发实验 — OLED 使用 I2C 通信
  • 铁头山羊 I2C 系列(4.1~4.8)— 硬件 I2C 模块详解

本笔记综合整理自三套 STM32 教程:

  1. [STM32 HAL库] I2C 基础知识 / 数据收发实验
  2. 铁头山羊 I2C 系列(4.1~4.8)
  3. 江协科技 I2C 系列(10-1~10-5)