13-I2C通信.md 18 KB

13-I2C通信

原理

PPT 参考图 -- I2C协议与时序

详细工作原理

I2C 总线物理层:SCL 和 SDA 均为开漏输出 + 外部上拉电阻(典型 4.7kΩ),实现"线与"逻辑——任何设备拉低总线时,总线即为低电平。多主机仲裁时,多个主机同时发送数据,谁先拉低 SDA 谁就赢得仲裁(低电平覆盖高电平),仲裁失败的主机自动转为从机模式。

时钟拉伸(Clock Stretching):从机可以在任何时刻将 SCL 拉低,强制主机等待,直到从机准备好继续通信。这允许慢速从机适配快速主机的时钟速度。标准 I2C 通信中,主机在每个字节传输后释放 SCL,从机拉低 SCL 延长时钟周期。

起始条件和停止条件的严格时序

  • 起始条件(Start):SCL 高电平期间,SDA 从高到低跳变
  • 停止条件(Stop):SCL 高电平期间,SDA 从低到高跳变
  • 起始和停止条件由主机独占产生,从机不能产生

数据字节传输:每个字节 8 位,MSB first。SCL 低电平时 SDA 变化,SCL 高电平时接收方采样 SDA。每字节后跟一个应答位(SCL 第 9 个时钟周期):

  • ACK:接收方在 SCL 高电平期间将 SDA 拉低,表示继续传输
  • NACK:接收方在 SCL 高电平期间保持 SDA 高,表示停止传输或最后一个字节

应答机制详解

  • 主机发送地址后,从机必须应答(ACK = 0),表示地址匹配;无应答则 NACK,主机发送停止条件
  • 主机发送数据时,从机每字节应答
  • 主机接收数据时,主机在最后一个字节发 NACK 表示不再接收,然后发停止条件

多字节读流程(写寄存器地址后读数据)

Start → 设备地址(W) → ACK → 寄存器地址 → ACK → Restart → 设备地址(R) → ACK → 读字节(N-1个都发ACK) → 最后一个字节发NACK → Stop

此流程称为"组合格式",通过重复起始条件(Restart)在不释放总线的情况下切换读写方向。

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

PPT 参考图 -- 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 外设)

PPT 参考图 -- 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 (读)