13-I2C通信.md 15 KB

13-I2C通信

原理

I2C 协议

  • 类型:同步半双工
  • 总线:SCL (时钟) + SDA (数据)
  • 设备:主设备 (Master) 发起通信,从设备 (Slave) 响应地址

         +---+
    SCL -----+   +---+
         |   |
    SDA -----+   +---------
         |   |
      Master    Slave
    

通信时序

        起始条件       字节传输(MSB first)     应答       停止条件
         _____       _____   _____   _____      _____    _____
SCL     |     |_____|     |_|     |_|     |____|     |__|     |
        _       ___     ___     ___     ___    ___       ___
SDA    X_| 7位地址 |_| R/W |_| 数据MSB |_|  ... |_| 应答 |_X_| 
       |_|       |_|     |_|        |_|      |_|     |_|    |_|

时序规则

  1. 起始条件 (Start):SCL 高电平时,SDA 下降沿
  2. 停止条件 (Stop):SCL 高电平时,SDA 上升沿
  3. 数据变化:SCL 低电平时 SDA 变化
  4. 数据采样:SCL 高电平时采样 SDA

数据帧格式

  • 起始条件
  • 7位从机地址 + R/W 位 (0=写,1=读)
  • 从机应答 (ACK/NACK)
  • 数据字节 (MSB first) + ACK/NACK
  • 停止条件

MPU6050

  • 类型:6轴运动跟踪 (3轴加速度 + 3轴陀螺仪)
  • 接口:I2C (最大 400kHz)
  • 从机地址:0x68 (7位地址)
  • 关键寄存器
寄存器 地址 说明
WHO_AM_I 0x75 读回 0x68
PWR_MGMT_1 0x6B 电源管理 (写 0x00 唤醒)
SMPLRT_DIV 0x19 采样率分频
CONFIG 0x1A 配置
GYRO_CONFIG 0x1B 陀螺仪配置
ACCEL_CONFIG 0x1C 加速度计配置
ACCEL_XOUT_H 0x3B 加速度 X 高字节
ACCEL_XOUT_L 0x3C 加速度 X 低字节
ACCEL_YOUT_H 0x3D 加速度 Y 高字节
ACCEL_YOUT_L 0x3E 加速度 Y 低字节
ACCEL_ZOUT_H 0x3F 加速度 Z 高字节
ACCEL_ZOUT_L 0x40 加速度 Z 低字节
GYRO_XOUT_H 0x43 陀螺仪 X 高字节
GYRO_XOUT_L 0x44 陀螺仪 X 低字节
GYRO_YOUT_H 0x45 陀螺仪 Y 高字节
GYRO_YOUT_L 0x46 陀螺仪 Y 低字节
GYRO_ZOUT_H 0x47 陀螺仪 Z 高字节
GYRO_ZOUT_L 0x48 陀螺仪 Z 低字节

API 表格

软件 I2C (GPIO 模拟)

函数 说明
void I2C_Start(void) 起始条件
void I2C_Stop(void) 停止条件
void I2C_SendByte(uint8_t Byte) 发送一个字节
uint8_t I2C_ReceiveByte(void) 接收一个字节
void I2C_SendAck(uint8_t AckBit) 发送应答 (0=ACK, 1=NACK)
uint8_t I2C_ReceiveAck(void) 接收应答
void MPU6050_Init(void) 初始化 MPU6050
uint8_t MPU6050_GetID(void) 读取 WHO_AM_I
void MPU6050_GetData(int16_t* accX, ..., int16_t* gyroZ) 读取全部原始数据

硬件 I2C (STM32 I2C 外设)

函数 说明
I2C_InitTypeDef 配置结构体
void I2C_Init(I2C_TypeDef* I2Cx, I2C_InitTypeDef* I2C_InitStruct) 初始化 I2C
void I2C_Cmd(I2C_TypeDef* I2Cx, FunctionalState NewState) 使能 I2C
void I2C_GenerateSTART(I2C_TypeDef* I2Cx, FunctionalState NewState) 产生起始条件
void I2C_GenerateSTOP(I2C_TypeDef* I2Cx, FunctionalState NewState) 产生停止条件
void I2C_SendData(I2C_TypeDef* I2Cx, uint8_t Data) 发送数据
uint8_t I2C_ReceiveData(I2C_TypeDef* I2Cx) 接收数据
void I2C_AcknowledgeConfig(I2C_TypeDef* I2Cx, FunctionalState NewState) 配置应答
ErrorStatus I2C_CheckEvent(I2C_TypeDef* I2Cx, uint32_t I2C_EVENT) 检测事件
ITStatus I2C_GetITStatus(I2C_TypeDef* I2Cx, uint32_t I2C_IT) 获取中断状态
void I2C_Send7bitAddress(I2C_TypeDef* I2Cx, uint8_t Address, uint8_t I2C_Direction) 发送7位地址

关键事件标志

事件宏 说明
I2C_EVENT_MASTER_MODE_SELECT EV5: 起始条件已发送
I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED EV6: 地址已发送 (主发模式)
I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED EV6: 地址已发送 (主收模式)
I2C_EVENT_MASTER_BYTE_TRANSMITTING EV8: 数据已发送 (DR 空)
I2C_EVENT_MASTER_BYTE_TRANSMITTED EV8_2: 数据已发送 (BTF=1)
I2C_EVENT_MASTER_BYTE_RECEIVED EV7: 数据已接收 (RXNE=1)

I2C_InitTypeDef

成员 可选值 说明
I2C_Mode I2C_Mode_I2C I2C 模式
I2C_ClockSpeed 0~400000 SCL 时钟频率 (Hz)
I2C_DutyCycle I2C_DutyCycle_16_9 / I2C_DutyCycle_2 快速模式占空比
I2C_Ack I2C_Ack_Enable / I2C_Ack_Disable 应答使能
I2C_AcknowledgedAddress I2C_AcknowledgedAddress_7bit / _10bit 应答地址位数

完整代码示例

软件 I2C 读写 MPU6050

/*------------------------------------------------
 * SoftI2C_MPU6050.c
 * 软件 I2C (GPIO 模拟) 读写 MPU6050
 * GPIO: PB0(SCL), PB1(SDA)
 *------------------------------------------------*/

#include "stm32f10x.h"
#include <stdint.h>

#define MPU6050_ADDR     0x68    // 7 位地址
#define MPU6050_ADDR_W   (MPU6050_ADDR << 1)      // 写地址 0xD0
#define MPU6050_ADDR_R   ((MPU6050_ADDR << 1) | 1) // 读地址 0xD1

/* 引脚定义 */
#define I2C_SCL_PORT     GPIOB
#define I2C_SCL_PIN      GPIO_Pin_0
#define I2C_SDA_PORT     GPIOB
#define I2C_SDA_PIN      GPIO_Pin_1

/* 简易延时 (约 5us @72MHz) */
static void delay_us(void)
{
    uint32_t i;
    for (i = 0; i < 10; i++);
}

/* ---- I2C 时序函数 ---- */

/* 起始条件:SCL 高时 SDA 下降沿 */
void I2C_Start(void)
{
    GPIO_SetBits(I2C_SDA_PORT, I2C_SDA_PIN);
    GPIO_SetBits(I2C_SCL_PORT, I2C_SCL_PIN);
    delay_us();
    GPIO_ResetBits(I2C_SDA_PORT, I2C_SDA_PIN);  // SDA 下降沿
    delay_us();
    GPIO_ResetBits(I2C_SCL_PORT, I2C_SCL_PIN);
}

/* 停止条件:SCL 高时 SDA 上升沿 */
void I2C_Stop(void)
{
    GPIO_ResetBits(I2C_SDA_PORT, I2C_SDA_PIN);
    GPIO_SetBits(I2C_SCL_PORT, I2C_SCL_PIN);
    delay_us();
    GPIO_SetBits(I2C_SDA_PORT, I2C_SDA_PIN);     // SDA 上升沿
    delay_us();
}

/* 发送一个字节 (MSB first) */
void I2C_SendByte(uint8_t Byte)
{
    uint8_t i;
    for (i = 0; i < 8; i++)
    {
        GPIO_ResetBits(I2C_SCL_PORT, I2C_SCL_PIN);
        if (Byte & 0x80)
            GPIO_SetBits(I2C_SDA_PORT, I2C_SDA_PIN);
        else
            GPIO_ResetBits(I2C_SDA_PORT, I2C_SDA_PIN);
        Byte <<= 1;
        delay_us();
        GPIO_SetBits(I2C_SCL_PORT, I2C_SCL_PIN);
        delay_us();
    }
    GPIO_ResetBits(I2C_SCL_PORT, I2C_SCL_PIN);
}

/* 接收一个字节 (MSB first) */
uint8_t I2C_ReceiveByte(void)
{
    uint8_t i, Byte = 0;
    GPIO_ResetBits(I2C_SDA_PORT, I2C_SDA_PIN);   // 释放 SDA
    for (i = 0; i < 8; i++)
    {
        GPIO_ResetBits(I2C_SCL_PORT, I2C_SCL_PIN);
        delay_us();
        GPIO_SetBits(I2C_SCL_PORT, I2C_SCL_PIN);
        Byte <<= 1;
        if (GPIO_ReadInputDataBit(I2C_SDA_PORT, I2C_SDA_PIN))
            Byte |= 0x01;
        delay_us();
    }
    GPIO_ResetBits(I2C_SCL_PORT, I2C_SCL_PIN);
    return Byte;
}

/* 发送应答:0=ACK, 1=NACK */
void I2C_SendAck(uint8_t AckBit)
{
    GPIO_ResetBits(I2C_SCL_PORT, I2C_SCL_PIN);
    if (AckBit == 0)
        GPIO_ResetBits(I2C_SDA_PORT, I2C_SDA_PIN);   // ACK
    else
        GPIO_SetBits(I2C_SDA_PORT, I2C_SDA_PIN);     // NACK
    delay_us();
    GPIO_SetBits(I2C_SCL_PORT, I2C_SCL_PIN);
    delay_us();
    GPIO_ResetBits(I2C_SCL_PORT, I2C_SCL_PIN);
    GPIO_SetBits(I2C_SDA_PORT, I2C_SDA_PIN);         // 释放 SDA
}

/* 接收应答:返回 0=ACK, 1=NACK */
uint8_t I2C_ReceiveAck(void)
{
    uint8_t AckBit;
    GPIO_ResetBits(I2C_SDA_PORT, I2C_SDA_PIN);       // 释放 SDA
    GPIO_SetBits(I2C_SCL_PORT, I2C_SCL_PIN);
    delay_us();
    AckBit = GPIO_ReadInputDataBit(I2C_SDA_PORT, I2C_SDA_PIN);
    GPIO_ResetBits(I2C_SCL_PORT, I2C_SCL_PIN);
    return AckBit;
}

/* ---- MPU6050 操作 ---- */

/* 向 MPU6050 写一个字节 */
void MPU6050_WriteByte(uint8_t RegAddr, uint8_t Data)
{
    I2C_Start();
    I2C_SendByte(MPU6050_ADDR_W);
    I2C_ReceiveAck();
    I2C_SendByte(RegAddr);
    I2C_ReceiveAck();
    I2C_SendByte(Data);
    I2C_ReceiveAck();
    I2C_Stop();
}

/* 从 MPU6050 读一个字节 */
uint8_t MPU6050_ReadByte(uint8_t RegAddr)
{
    uint8_t Data;
    I2C_Start();
    I2C_SendByte(MPU6050_ADDR_W);
    I2C_ReceiveAck();
    I2C_SendByte(RegAddr);
    I2C_ReceiveAck();
    I2C_Start();                                // 重复起始
    I2C_SendByte(MPU6050_ADDR_R);
    I2C_ReceiveAck();
    Data = I2C_ReceiveByte();
    I2C_SendAck(1);                             // NACK
    I2C_Stop();
    return Data;
}

/* 读取多个连续字节 */
void MPU6050_ReadMultiBytes(uint8_t RegAddr, uint8_t* pData, uint8_t Len)
{
    uint8_t i;
    I2C_Start();
    I2C_SendByte(MPU6050_ADDR_W);
    I2C_ReceiveAck();
    I2C_SendByte(RegAddr);
    I2C_ReceiveAck();
    I2C_Start();
    I2C_SendByte(MPU6050_ADDR_R);
    I2C_ReceiveAck();
    for (i = 0; i < Len; i++)
    {
        pData[i] = I2C_ReceiveByte();
        if (i < Len - 1)
            I2C_SendAck(0);                     // ACK
        else
            I2C_SendAck(1);                     // NACK (最后一个字节)
    }
    I2C_Stop();
}

/* MPU6050 初始化 */
void MPU6050_Init(void)
{
    /* 唤醒:写 0x00 到 PWR_MGMT_1 */
    MPU6050_WriteByte(0x6B, 0x00);
    /* 关闭 SLEEP 模式 (已包含在 0x00) */
}

/* 读 WHO_AM_I 寄存器 */
uint8_t MPU6050_GetID(void)
{
    return MPU6050_ReadByte(0x75);
}

/* 读取 6 轴原始数据 */
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];
    MPU6050_ReadMultiBytes(0x3B, buf, 14);

    *accX  = ((int16_t)buf[0]  << 8) | buf[1];
    *accY  = ((int16_t)buf[2]  << 8) | buf[3];
    *accZ  = ((int16_t)buf[4]  << 8) | buf[5];
    *gyroX = ((int16_t)buf[8]  << 8) | buf[9];
    *gyroY = ((int16_t)buf[10] << 8) | buf[11];
    *gyroZ = ((int16_t)buf[12] << 8) | buf[13];
}

/* ---- GPIO 初始化 ---- */
void SoftI2C_Init(void)
{
    GPIO_InitTypeDef GPIO_InitStruct;

    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);

    GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_OD;       // 开漏输出
    GPIO_InitStruct.GPIO_Pin = I2C_SCL_PIN | I2C_SDA_PIN;
    GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOB, &GPIO_InitStruct);

    GPIO_SetBits(GPIOB, I2C_SCL_PIN | I2C_SDA_PIN);     // 释放总线 (高电平)
}

/* ---- 主函数 ---- */
int main(void)
{
    int16_t accX, accY, accZ;
    int16_t gyroX, gyroY, gyroZ;
    uint8_t id;

    SoftI2C_Init();
    MPU6050_Init();
    id = MPU6050_GetID();

    /* 若 id == 0x68, 表示 MPU6050 正常 */
    while (1)
    {
        MPU6050_GetData(&accX, &accY, &accZ, &gyroX, &gyroY, &gyroZ);
        /* 处理数据 ... */
    }
}

硬件 I2C 读写 MPU6050

/*------------------------------------------------
 * HW_I2C_MPU6050.c
 * 硬件 I2C1 (PB6=SCL, PB7=SDA)
 *------------------------------------------------*/

#include "stm32f10x.h"

#define MPU6050_ADDR    0x68

static void I2C_Config(void)
{
    I2C_InitTypeDef I2C_InitStruct;

    RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1, ENABLE);
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);

    /* I2C1: PB6=SCL, PB7=SDA, 复用开漏输出 */
    GPIO_InitTypeDef GPIO_InitStruct;
    GPIO_InitStruct.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7;
    GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_OD;        // 复用开漏
    GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOB, &GPIO_InitStruct);

    I2C_InitStruct.I2C_Mode = I2C_Mode_I2C;
    I2C_InitStruct.I2C_ClockSpeed = 100000;             // 100kHz 标准模式
    I2C_InitStruct.I2C_DutyCycle = I2C_DutyCycle_2;
    I2C_InitStruct.I2C_Ack = I2C_Ack_Enable;
    I2C_InitStruct.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;
    I2C_Init(I2C1, &I2C_InitStruct);

    I2C_Cmd(I2C1, ENABLE);
}

static void MPU6050_WriteByte(uint8_t RegAddr, uint8_t Data)
{
    while (I2C_GetFlagStatus(I2C1, I2C_FLAG_BUSY));

    I2C_GenerateSTART(I2C1, ENABLE);
    while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT));     // EV5

    I2C_Send7bitAddress(I2C1, MPU6050_ADDR, I2C_Direction_Transmitter);
    while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)); // EV6

    I2C_SendData(I2C1, RegAddr);
    while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED)); // EV8_2

    I2C_SendData(I2C1, Data);
    while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED));

    I2C_GenerateSTOP(I2C1, ENABLE);
}

static uint8_t MPU6050_ReadByte(uint8_t RegAddr)
{
    uint8_t Data;
    while (I2C_GetFlagStatus(I2C1, I2C_FLAG_BUSY));

    /* 写寄存器地址 */
    I2C_GenerateSTART(I2C1, ENABLE);
    while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT));

    I2C_Send7bitAddress(I2C1, MPU6050_ADDR, I2C_Direction_Transmitter);
    while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED));

    I2C_SendData(I2C1, RegAddr);
    while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED));

    /* 重复起始,读数据 */
    I2C_GenerateSTART(I2C1, ENABLE);
    while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT));

    I2C_Send7bitAddress(I2C1, MPU6050_ADDR, I2C_Direction_Receiver);
    while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED));

    /* 最后一个字节前 NACK + STOP */
    I2C_AcknowledgeConfig(I2C1, DISABLE);
    I2C_GenerateSTOP(I2C1, ENABLE);

    while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_RECEIVED));   // EV7
    Data = I2C_ReceiveData(I2C1);

    I2C_AcknowledgeConfig(I2C1, ENABLE);
    return Data;
}

int main(void)
{
    uint8_t id;

    I2C_Config();
    /* 唤醒 MPU6050 */
    MPU6050_WriteByte(0x6B, 0x00);
    id = MPU6050_ReadByte(0x75);

    while (1);
}

常见坑点

  1. 应答标志位处理

    • 主接收最后一个字节必须发 NACK(非应答),否则从机继续发数据导致总线异常
    • 硬件 I2C 下:最后一个字节接收前关 ACK (I2C_AcknowledgeConfig(DISABLE)),同时发 STOP
  2. 超时处理

    • While 轮询事件标志时务必加超时,否则从机无响应会死循环
    • 建议实现 while(!event && timeout--) 模式
  3. I2C 时钟不要超过 400kHz

    • 标准模式 100kHz,快速模式 400kHz
    • 超频可能导致通信不稳定
  4. 软件 I2C 的 GPIO 模式

    • SDA 和 SCL 必须用开漏输出 (GPIO_Mode_Out_OD) + 外部上拉电阻 (4.7kΩ)
    • 开漏才能实现"线与",多主机时不会短路
  5. 硬件 I2C 的 GPIO 模式

    • 使用复用开漏 (GPIO_Mode_AF_OD),不是普通推挽
    • 时钟使能注意:I2C1 挂 APB1,GPIOB 挂 APB2
  6. 重复起始条件

    • 读操作时必须发重复起始 (Restart),不可先停再启
    • 软件 I2C 只需再调一次 I2C_Start()
    • 硬件 I2C 重复调 I2C_GenerateSTART 即可
  7. MPU6050 唤醒

    • 上电后 MPU6050 默认 SLEEP 状态,必须写 PWR_MGMT_1 (0x6B) = 0x00 唤醒
  8. 地址左移

    • STM32 库函数 I2C_Send7bitAddress 要求7位地址(0x68),库内部左移加 R/W
    • 软件模拟时必须手动左移:0x68 << 1 = 0xD0 (写) / 0xD1 (读)