嵌入式开发之IIC接口详解-STM32

目录

  1. IIC接口概述
  2. IIC工作原理
  3. IIC通信协议详解
  4. [STM32 IIC硬件架构](#STM32 IIC硬件架构)
  5. [STM32 IIC软件实现](#STM32 IIC软件实现)
  6. 常见IIC设备应用实例
  7. 调试技巧与常见问题
  8. 总结

1. IIC接口概述

1.1 什么是IIC

IIC (Inter-Integrated Circuit,也常写作I2C)是由Philips(现NXP)公司于1982年开发的一种串行、同步、半双工通信总线协议。它仅使用两根信号线即可实现设备间的数据交换,广泛应用于嵌入式系统中连接各种外设。

1.2 IIC的主要特点

特性 说明
线数少 仅需SDA(数据线)和SCL(时钟线)两根线
多主多从 支持一主多从、多主多从架构
地址寻址 通过7位或10位地址识别从设备
速率灵活 标准模式100Kbps,快速模式400Kbps,高速模式3.4Mbps
硬件简单 开漏输出,需外接上拉电阻
仲裁机制 多主模式下具有总线仲裁功能

1.3 IIC与SPI、UART对比

特性 IIC SPI UART
信号线 2根(SDA+SCL) 4根(MOSI+MISO+SCK+CS) 2根(TX+RX)
通信方式 半双工 全双工 全双工
设备数量 多从机(地址寻址) 多从机(片选信号) 点对点
时钟 同步(SCL) 同步(SCK) 异步
速率 100K~3.4M bps 可达几十Mbps 常见9600~115200 bps
复杂度 协议较复杂 协议简单 协议简单
应用场景 传感器、EEPROM 存储器、显示屏 调试、蓝牙模块

2. IIC工作原理

2.1 物理层结构

复制代码
        ┌─────────┐                    ┌─────────┐
        │  Master │                    │  Slave  │
        │ (STM32) │                    │ (Device)│
        └────┬────┘                    └────┬────┘
             │                              │
    SDA ─────┼──────────────────────────────┼───── 数据线(Serial Data)
             │         ┌─────┐              │
             │         │ 4.7k│              │
             │         │  Ω  │              │
             │         └─────┘              │
             │           │                  │
    SCL ─────┼──────────────────────────────┼───── 时钟线(Serial Clock)
             │         ┌─────┐              │
             │         │ 4.7k│              │
             │         │  Ω  │              │
             │         └─────┘              │
             │           │                  │
            GND         VCC(3.3V/5V)       GND

注意:SDA和SCL必须外接上拉电阻(通常4.7kΩ),因为IIC设备输出为开漏(Open-Drain)结构。

2.2 开漏输出与线与逻辑

IIC使用开漏输出(Open-Drain/Open-Collector)结构:

  • 输出高电平:通过外部上拉电阻实现(释放总线)
  • 输出低电平:主动拉低(NMOS导通)

这种结构天然支持线与(Wired-AND)逻辑:

  • 任一设备拉低,总线即为低电平

  • 所有设备释放,总线才为高电平

    复制代码
      设备A输出 ──┐
                ├──→ 线与 ──→ 总线状态
      设备B输出 ──┘
    
      设备A=0, 设备B=1 → 总线=0
      设备A=1, 设备B=0 → 总线=0
      设备A=0, 设备B=0 → 总线=0
      设备A=1, 设备B=1 → 总线=1(由上拉电阻拉高)

2.3 总线速度模式

模式 速率 说明
标准模式(Standard-mode) 100 Kbps 最基础模式
快速模式(Fast-mode) 400 Kbps 常用模式
快速模式+(Fast-mode Plus) 1 Mbps 需要更小的上拉电阻
高速模式(High-speed mode) 3.4 Mbps 需要电流源上拉
超快速模式(Ultra Fast-mode) 5 Mbps 单向传输

3. IIC通信协议详解

3.1 信号类型

IIC协议定义了以下几种信号:

3.1.1 起始信号(START)

复制代码
SCL:  ──────┐     ┌─────┐     ┌─────┐
            │     │     │     │     │
            └─────┘     └─────┘     └─────
SDA:  ─────────┐
               │
               └───── 在SCL高电平时,SDA从高变低

条件:SCL为高电平期间,SDA从高电平跳变为低电平。

3.1.2 停止信号(STOP)

复制代码
SCL:  ──────┐     ┌─────┐     ┌─────┐
            │     │     │     │     │
            └─────┘     └─────┘     └─────
SDA:  ──────┐
            │
            └───────── 在SCL高电平时,SDA从低变高

条件:SCL为高电平期间,SDA从低电平跳变为高电平。

3.1.3 应答信号(ACK)

复制代码
SCL:  ──────┐     ┌─────┐
            │     │     │
            └─────┘     └─────
SDA:  ──────┐           ┌─────────
            │           │
            └───────────┘  第9个时钟周期,SDA为低电平

ACK:接收方在第9个时钟周期将SDA拉低,表示成功接收数据。

3.1.4 非应答信号(NACK)

复制代码
SCL:  ──────┐     ┌─────┐
            │     │     │
            └─────┘     └─────
SDA:  ──────────────────────────
                                第9个时钟周期,SDA保持高电平

NACK:接收方在第9个时钟周期保持SDA为高电平,表示未接收或接收错误。

3.1.5 数据位传输

复制代码
SCL:  ──────┐     ┌─────┐     ┌─────┐     ┌─────┐
            │     │     │     │     │     │     │
            └─────┘     └─────┘     └─────┘     └─────
SDA:  ────┐           ┌─────────┐           ┌─────
          │           │         │           │
          └───────────┘         └───────────┘
              数据位1              数据位0
          (SCL低电平时变化,高电平时稳定)

规则:数据在SCL低电平时改变,在SCL高电平时保持稳定。

3.2 完整数据帧格式

3.2.1 写操作帧格式

复制代码
    START    设备地址+W    ACK    数据1    ACK    数据2    ACK    ...    STOP
    ───┐    ┌────────┐   ─┐   ┌────┐   ─┐   ┌────┐   ─┐          ───┐
       │    │ 7bit+0 │    │   │    │    │   │    │    │              │
       └────┘        └────┘   └────┘    └────┘    └────┘              └────
              ↑
         0表示写操作

3.2.2 读操作帧格式

复制代码
    START    设备地址+R    ACK    数据1    ACK    数据2    NACK    STOP
    ───┐    ┌────────┐   ─┐   ┌────┐   ─┐   ┌────┐   ──┐    ───┐
       │    │ 7bit+1 │    │   │    │    │   │    │     │      │
       └────┘        └────┘   └────┘    └────┘    └─────┘      └────
              ↑
         1表示读操作

3.2.3 复合操作(先写后读)

复制代码
    START    设备地址+W    ACK    寄存器地址    ACK    
    START    设备地址+R    ACK    数据    NACK    STOP
    ─────────────────────────────────────────────────────────────
    这种格式常用于:先指定寄存器地址,再读取该寄存器数据

3.3 7位地址与10位地址

7位地址格式

复制代码
    ┌─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┐
    │ A6  │ A5  │ A4  │ A3  │ A2  │ A1  │ A0  │ R/W │
    └─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┘
      └────────── 7位设备地址 ──────────┘   └─ 读写位
  • 7位地址范围:0x00 ~ 0x7F(实际可用0x08~`0x77`)
  • 广播地址:0x00(General Call)

10位地址格式

复制代码
    第一字节:  1    1    1    1    0    A9   A8   R/W
    第二字节:  A7   A6   A5   A4   A3   A2   A1   A0
  • 11110开头标识10位地址模式
  • 支持更多设备连接

4. STM32 IIC硬件架构

4.1 STM32 I2C外设特性

STM32系列MCU集成了功能强大的I2C外设,主要特性包括:

  • 支持多主模式:具备总线仲裁功能
  • 支持7位/10位地址:兼容各种从设备
  • 支持DMA传输:减轻CPU负担
  • 支持SMBus/PMBus:兼容系统管理总线协议
  • 可编程时钟:灵活配置通信速率
  • 状态标志丰富:便于调试和错误处理

4.2 STM32 I2C时钟源

复制代码
    ┌─────────────┐
    │   APB1总线  │  ← 通常36MHz(STM32F1)或42MHz(STM32F4)
    │   时钟(PCLK)│
    └──────┬──────┘
           │
           ▼
    ┌─────────────┐
    │  I2C时钟    │
    │  分频器     │  ← 配置CCR寄存器
    └──────┬──────┘
           │
           ▼
    ┌─────────────┐
    │  SCL时钟    │  ← 输出到I2C总线
    │  (100K/400K)│
    └─────────────┘

4.3 关键寄存器

寄存器 名称 功能
I2C_CR1 控制寄存器1 使能I2C、配置ACK、START/STOP生成等
I2C_CR2 控制寄存器2 时钟频率配置、DMA使能、中断使能
I2C_OAR1 自身地址寄存器1 配置本机地址(从模式)
I2C_OAR2 自身地址寄存器2 双地址模式配置
I2C_DR 数据寄存器 发送/接收数据缓冲
I2C_SR1 状态寄存器1 各种事件标志位
I2C_SR2 状态寄存器2 主从模式标志、总线忙标志等
I2C_CCR 时钟控制寄存器 配置SCL时钟分频
I2C_TRISE 上升时间寄存器 配置SCL最大上升时间

4.4 事件标志详解(SR1寄存器)

标志 含义
Bit 0 SB 起始条件已发送
Bit 1 ADDR 地址已发送/已匹配
Bit 2 BTF 字节传输完成
Bit 3 ADD10 10位地址已发送
Bit 4 STOPF 检测到停止条件(从模式)
Bit 6 RxNE 接收数据寄存器非空
Bit 7 TxE 发送数据寄存器空
Bit 8 BERR 总线错误
Bit 9 ARLO 仲裁丢失(多主模式)
Bit 10 AF 应答失败
Bit 11 OVR 过载/欠载

5. STM32 IIC软件实现

5.1 硬件I2C实现(HAL库)

5.1.1 初始化配置

复制代码
#include "stm32f1xx_hal.h"

I2C_HandleTypeDef hi2c1;

void I2C1_Init(void)
{
    hi2c1.Instance = I2C1;
    hi2c1.Init.ClockSpeed = 100000;        // 100KHz标准模式
    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;

    if (HAL_I2C_Init(&hi2c1) != HAL_OK)
    {
        Error_Handler();
    }
}

// GPIO初始化(通常在HAL_I2C_MspInit中)
void HAL_I2C_MspInit(I2C_HandleTypeDef* hi2c)
{
    GPIO_InitTypeDef GPIO_InitStruct = {0};

    if(hi2c->Instance == I2C1)
    {
        __HAL_RCC_I2C1_CLK_ENABLE();
        __HAL_RCC_GPIOB_CLK_ENABLE();

        // PB6 -> SCL, PB7 -> SDA
        GPIO_InitStruct.Pin = GPIO_PIN_6 | GPIO_PIN_7;
        GPIO_InitStruct.Mode = GPIO_MODE_AF_OD;    // 开漏输出!
        GPIO_InitStruct.Pull = GPIO_PULLUP;        // 内部上拉
        GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
        HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
    }
}

重要:GPIO必须配置为**开漏输出(Open-Drain)**模式,并启用内部上拉或外接上拉电阻。

5.1.2 发送数据(轮询方式)

复制代码
/**
 * @brief 向I2C设备写入数据
 * @param devAddr: 设备地址(7位地址,左移1位)
 * @param pData: 待发送数据指针
 * @param Size: 数据长度
 * @retval HAL状态
 */
HAL_StatusTypeDef I2C_Write(uint16_t devAddr, uint8_t *pData, uint16_t Size)
{
    // HAL_I2C_Master_Transmit 参数说明:
    // &hi2c1: I2C句柄
    // devAddr: 设备地址(需要左移1位,HAL库自动处理读写位)
    // pData: 数据缓冲区
    // Size: 数据长度
    // 1000: 超时时间(ms)

    return HAL_I2C_Master_Transmit(&hi2c1, devAddr << 1, pData, Size, 1000);
}

// 使用示例:向地址0x50的EEPROM写入数据
uint8_t writeData[] = {0x00, 0x10, 0xAB, 0xCD}; // 寄存器地址+数据
I2C_Write(0x50, writeData, sizeof(writeData));

5.1.3 接收数据(轮询方式)

复制代码
/**
 * @brief 从I2C设备读取数据
 * @param devAddr: 设备地址
 * @param regAddr: 寄存器地址(可选)
 * @param pData: 接收数据缓冲区
 * @param Size: 读取长度
 * @retval HAL状态
 */
HAL_StatusTypeDef I2C_Read(uint16_t devAddr, uint8_t regAddr, 
                           uint8_t *pData, uint16_t Size)
{
    // 先发送寄存器地址(写操作)
    HAL_StatusTypeDef status;

    status = HAL_I2C_Master_Transmit(&hi2c1, devAddr << 1, 
                                      &regAddr, 1, 1000);
    if (status != HAL_OK) return status;

    // 再读取数据(读操作)
    status = HAL_I2C_Master_Receive(&hi2c1, devAddr << 1, 
                                     pData, Size, 1000);
    return status;
}

// 使用示例:从MPU6050读取加速度数据
uint8_t accelData[6];
I2C_Read(0x68, 0x3B, accelData, 6);  // 从寄存器0x3B开始读取6字节

5.1.4 内存地址读写(常用封装)

复制代码
/**
 * @brief 向指定寄存器写入单字节
 */
HAL_StatusTypeDef I2C_WriteReg(uint8_t devAddr, uint8_t regAddr, uint8_t value)
{
    uint8_t data[2] = {regAddr, value};
    return HAL_I2C_Master_Transmit(&hi2c1, devAddr << 1, data, 2, 1000);
}

/**
 * @brief 从指定寄存器读取单字节
 */
uint8_t I2C_ReadReg(uint8_t devAddr, uint8_t regAddr)
{
    uint8_t value = 0;
    HAL_I2C_Master_Transmit(&hi2c1, devAddr << 1, &regAddr, 1, 1000);
    HAL_I2C_Master_Receive(&hi2c1, devAddr << 1, &value, 1, 1000);
    return value;
}

/**
 * @brief 向指定寄存器连续写入多字节
 */
HAL_StatusTypeDef I2C_WriteReg_Multi(uint8_t devAddr, uint8_t regAddr, 
                                      uint8_t *pData, uint16_t len)
{
    // 使用MemWrite更简洁(HAL库封装)
    return HAL_I2C_Mem_Write(&hi2c1, devAddr << 1, regAddr,
                              I2C_MEMADD_SIZE_8BIT, pData, len, 1000);
}

/**
 * @brief 从指定寄存器连续读取多字节
 */
HAL_StatusTypeDef I2C_ReadReg_Multi(uint8_t devAddr, uint8_t regAddr,
                                     uint8_t *pData, uint16_t len)
{
    return HAL_I2C_Mem_Read(&hi2c1, devAddr << 1, regAddr,
                             I2C_MEMADD_SIZE_8BIT, pData, len, 1000);
}

5.1.5 中断方式传输

复制代码
// 中断发送
HAL_StatusTypeDef I2C_Write_IT(uint16_t devAddr, uint8_t *pData, uint16_t Size)
{
    return HAL_I2C_Master_Transmit_IT(&hi2c1, devAddr << 1, pData, Size);
}

// 中断接收
HAL_StatusTypeDef I2C_Read_IT(uint16_t devAddr, uint8_t *pData, uint16_t Size)
{
    return HAL_I2C_Master_Receive_IT(&hi2c1, devAddr << 1, pData, Size);
}

// 中断回调函数
void HAL_I2C_MasterTxCpltCallback(I2C_HandleTypeDef *hi2c)
{
    if (hi2c->Instance == I2C1)
    {
        // 发送完成处理
        printf("I2C Transmit Complete!\n");
    }
}

void HAL_I2C_MasterRxCpltCallback(I2C_HandleTypeDef *hi2c)
{
    if (hi2c->Instance == I2C1)
    {
        // 接收完成处理
        printf("I2C Receive Complete!\n");
    }
}

void HAL_I2C_ErrorCallback(I2C_HandleTypeDef *hi2c)
{
    if (hi2c->Instance == I2C1)
    {
        // 错误处理
        printf("I2C Error! ErrorCode = %lu\n", hi2c->ErrorCode);
    }
}

5.1.6 DMA方式传输

复制代码
// DMA发送(适合大数据量传输)
HAL_StatusTypeDef I2C_Write_DMA(uint16_t devAddr, uint8_t *pData, uint16_t Size)
{
    return HAL_I2C_Master_Transmit_DMA(&hi2c1, devAddr << 1, pData, Size);
}

// DMA接收
HAL_StatusTypeDef I2C_Read_DMA(uint16_t devAddr, uint8_t *pData, uint16_t Size)
{
    return HAL_I2C_Master_Receive_DMA(&hi2c1, devAddr << 1, pData, Size);
}

// DMA传输完成回调
void HAL_I2C_MasterTxCpltCallback(I2C_HandleTypeDef *hi2c)
{
    // 与中断方式共用回调
}

5.2 软件模拟I2C实现(GPIO模拟)

当硬件I2C出现问题或引脚不足时,可使用GPIO模拟I2C。

5.2.1 引脚定义与基础操作

复制代码
#include "stm32f1xx_hal.h"

// 引脚定义(可根据实际修改)
#define I2C_SCL_PORT    GPIOB
#define I2C_SCL_PIN     GPIO_PIN_6
#define I2C_SDA_PORT    GPIOB
#define I2C_SDA_PIN     GPIO_PIN_7

#define I2C_SCL_H()     HAL_GPIO_WritePin(I2C_SCL_PORT, I2C_SCL_PIN, GPIO_PIN_SET)
#define I2C_SCL_L()     HAL_GPIO_WritePin(I2C_SCL_PORT, I2C_SCL_PIN, GPIO_PIN_RESET)
#define I2C_SDA_H()     HAL_GPIO_WritePin(I2C_SDA_PORT, I2C_SDA_PIN, GPIO_PIN_SET)
#define I2C_SDA_L()     HAL_GPIO_WritePin(I2C_SDA_PORT, I2C_SDA_PIN, GPIO_PIN_RESET)
#define I2C_SDA_READ()  HAL_GPIO_ReadPin(I2C_SDA_PORT, I2C_SDA_PIN)

// 延时函数(根据主频调整)
static void I2C_Delay(void)
{
    for(volatile uint8_t i = 0; i < 20; i++);  // 约5us@72MHz
}

// 初始化GPIO
void Soft_I2C_Init(void)
{
    GPIO_InitTypeDef GPIO_InitStruct = {0};

    __HAL_RCC_GPIOB_CLK_ENABLE();

    // SCL: 推挽输出
    GPIO_InitStruct.Pin = I2C_SCL_PIN;
    GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
    GPIO_InitStruct.Pull = GPIO_NOPULL;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
    HAL_GPIO_Init(I2C_SCL_PORT, &GPIO_InitStruct);

    // SDA: 开漏输出(可输入)
    GPIO_InitStruct.Pin = I2C_SDA_PIN;
    GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_OD;  // 开漏!
    GPIO_InitStruct.Pull = GPIO_PULLUP;
    HAL_GPIO_Init(I2C_SDA_PORT, &GPIO_InitStruct);

    I2C_SCL_H();
    I2C_SDA_H();
}

5.2.2 起始与停止信号

复制代码
/**
 * @brief 产生起始信号
 */
void Soft_I2C_Start(void)
{
    I2C_SDA_H();
    I2C_SCL_H();
    I2C_Delay();
    I2C_SDA_L();    // SCL高时,SDA从高变低
    I2C_Delay();
    I2C_SCL_L();    // 拉低SCL,准备发送数据
    I2C_Delay();
}

/**
 * @brief 产生停止信号
 */
void Soft_I2C_Stop(void)
{
    I2C_SDA_L();
    I2C_SCL_H();
    I2C_Delay();
    I2C_SDA_H();    // SCL高时,SDA从低变高
    I2C_Delay();
}

5.2.3 发送与接收字节

复制代码
/**
 * @brief 发送一个字节
 * @param byte: 待发送数据
 * @retval 0: 收到ACK, 1: 收到NACK
 */
uint8_t Soft_I2C_SendByte(uint8_t byte)
{
    for(uint8_t i = 0; i < 8; i++)
    {
        if(byte & 0x80)
            I2C_SDA_H();
        else
            I2C_SDA_L();

        byte <<= 1;
        I2C_Delay();
        I2C_SCL_H();    // 产生时钟上升沿
        I2C_Delay();
        I2C_SCL_L();    // 产生时钟下降沿
        I2C_Delay();
    }

    // 释放SDA,准备接收ACK
    I2C_SDA_H();
    I2C_Delay();
    I2C_SCL_H();
    I2C_Delay();

    uint8_t ack = I2C_SDA_READ();  // 读取ACK/NACK

    I2C_SCL_L();
    I2C_Delay();

    return ack;  // 0=ACK, 1=NACK
}

/**
 * @brief 接收一个字节
 * @param ack: 1=发送ACK, 0=发送NACK
 * @retval 接收到的数据
 */
uint8_t Soft_I2C_ReceiveByte(uint8_t ack)
{
    uint8_t byte = 0;

    I2C_SDA_H();  // 释放SDA

    for(uint8_t i = 0; i < 8; i++)
    {
        byte <<= 1;
        I2C_SCL_H();    // 时钟上升沿
        I2C_Delay();

        if(I2C_SDA_READ())
            byte |= 0x01;

        I2C_SCL_L();    // 时钟下降沿
        I2C_Delay();
    }

    // 发送ACK/NACK
    if(ack)
        I2C_SDA_L();  // ACK: 拉低SDA
    else
        I2C_SDA_H();  // NACK: 保持高

    I2C_Delay();
    I2C_SCL_H();      // 第9个时钟
    I2C_Delay();
    I2C_SCL_L();
    I2C_Delay();

    return byte;
}

5.2.4 完整读写函数

复制代码
/**
 * @brief 向设备写入数据
 */
uint8_t Soft_I2C_Write(uint8_t devAddr, uint8_t regAddr, 
                       uint8_t *pData, uint8_t len)
{
    Soft_I2C_Start();

    if(Soft_I2C_SendByte(devAddr << 1 | 0x00) != 0)  // 发送地址+写
    {
        Soft_I2C_Stop();
        return 1;  // 无应答
    }

    if(Soft_I2C_SendByte(regAddr) != 0)  // 发送寄存器地址
    {
        Soft_I2C_Stop();
        return 2;
    }

    for(uint8_t i = 0; i < len; i++)
    {
        if(Soft_I2C_SendByte(pData[i]) != 0)
        {
            Soft_I2C_Stop();
            return 3;
        }
    }

    Soft_I2C_Stop();
    return 0;  // 成功
}

/**
 * @brief 从设备读取数据
 */
uint8_t Soft_I2C_Read(uint8_t devAddr, uint8_t regAddr,
                      uint8_t *pData, uint8_t len)
{
    // 先发送寄存器地址
    Soft_I2C_Start();

    if(Soft_I2C_SendByte(devAddr << 1 | 0x00) != 0)
    {
        Soft_I2C_Stop();
        return 1;
    }

    if(Soft_I2C_SendByte(regAddr) != 0)
    {
        Soft_I2C_Stop();
        return 2;
    }

    // 重复起始,发送读命令
    Soft_I2C_Start();

    if(Soft_I2C_SendByte(devAddr << 1 | 0x01) != 0)
    {
        Soft_I2C_Stop();
        return 3;
    }

    // 读取数据
    for(uint8_t i = 0; i < len; i++)
    {
        pData[i] = Soft_I2C_ReceiveByte(i < len - 1 ? 1 : 0);  
        // 最后一个字节发送NACK
    }

    Soft_I2C_Stop();
    return 0;
}

5.3 硬件I2C vs 软件模拟I2C

对比项 硬件I2C 软件模拟I2C
CPU占用 低(有独立外设) 高(占用CPU时间)
速率 可达400KHz+ 通常100KHz以下
引脚灵活性 固定引脚(复用功能) 任意GPIO
可靠性 高(硬件处理时序) 依赖代码精度
多主支持 硬件支持 需软件实现
调试难度 较难(时序不可控) 较易(可单步调试)
适用场景 正式产品、高速通信 调试、引脚受限

6. 常见IIC设备应用实例

6.1 EEPROM(AT24C02/04/08/16/32/64)

设备特性

参数 说明
地址 0x50 ~ 0x57(由A0/A1/A2引脚决定)
容量 2Kbit ~ 64Kbit
页大小 8/16/32字节(依型号)
写周期 最大5ms

驱动代码

复制代码
#define EEPROM_ADDR     0x50    // A0=A1=A2=GND
#define EEPROM_PAGE_SIZE 8     // AT24C02页大小

/**
 * @brief 向EEPROM写入数据(支持跨页写入)
 */
HAL_StatusTypeDef EEPROM_Write(uint16_t memAddr, uint8_t *pData, uint16_t len)
{
    HAL_StatusTypeDef status;
    uint16_t pageRemain = EEPROM_PAGE_SIZE - (memAddr % EEPROM_PAGE_SIZE);

    while(len > 0)
    {
        uint16_t writeLen = (len > pageRemain) ? pageRemain : len;

        status = HAL_I2C_Mem_Write(&hi2c1, EEPROM_ADDR << 1, memAddr,
                                   I2C_MEMADD_SIZE_8BIT, pData, writeLen, 1000);
        if(status != HAL_OK) return status;

        HAL_Delay(6);  // 等待写入完成(>5ms)

        len -= writeLen;
        memAddr += writeLen;
        pData += writeLen;
        pageRemain = EEPROM_PAGE_SIZE;  // 后续按整页处理
    }
    return HAL_OK;
}

/**
 * @brief 从EEPROM读取数据
 */
HAL_StatusTypeDef EEPROM_Read(uint16_t memAddr, uint8_t *pData, uint16_t len)
{
    return HAL_I2C_Mem_Read(&hi2c1, EEPROM_ADDR << 1, memAddr,
                            I2C_MEMADD_SIZE_8BIT, pData, len, 1000);
}

// 使用示例
uint8_t writeBuf[] = "Hello I2C!";
uint8_t readBuf[20] = {0};

EEPROM_Write(0x00, writeBuf, sizeof(writeBuf));
EEPROM_Read(0x00, readBuf, sizeof(writeBuf));

6.2 MPU6050(六轴传感器)

设备特性

参数 说明
地址 0x68(AD0=GND)或 0x69(AD0=VCC)
功能 3轴加速度 + 3轴陀螺仪
寄存器 0x3B~0x48(加速度+温度+陀螺仪数据)
量程 加速度±2g/±4g/±8g/±16g,陀螺仪±250°/s~±2000°/s

驱动代码

复制代码
#define MPU6050_ADDR    0x68

// 关键寄存器
#define MPU6050_REG_PWR_MGMT_1  0x6B
#define MPU6050_REG_WHO_AM_I    0x75
#define MPU6050_REG_ACCEL_XOUT_H 0x3B
#define MPU6050_REG_GYRO_XOUT_H  0x43
#define MPU6050_REG_CONFIG      0x1A
#define MPU6050_REG_GYRO_CONFIG 0x1B
#define MPU6050_REG_ACCEL_CONFIG 0x1C

/**
 * @brief MPU6050初始化
 */
uint8_t MPU6050_Init(void)
{
    uint8_t id = I2C_ReadReg(MPU6050_ADDR, MPU6050_REG_WHO_AM_I);
    if(id != 0x68) return 1;  // 设备ID错误

    // 解除休眠模式
    I2C_WriteReg(MPU6050_ADDR, MPU6050_REG_PWR_MGMT_1, 0x00);
    HAL_Delay(100);

    // 配置采样率
    I2C_WriteReg(MPU6050_ADDR, 0x19, 0x07);  // SMPLRT_DIV = 7

    // 配置低通滤波器
    I2C_WriteReg(MPU6050_ADDR, MPU6050_REG_CONFIG, 0x06);

    // 配置陀螺仪量程:±2000°/s
    I2C_WriteReg(MPU6050_ADDR, MPU6050_REG_GYRO_CONFIG, 0x18);

    // 配置加速度量程:±2g
    I2C_WriteReg(MPU6050_ADDR, MPU6050_REG_ACCEL_CONFIG, 0x00);

    return 0;
}

/**
 * @brief 读取加速度数据
 */
void MPU6050_ReadAccel(int16_t *ax, int16_t *ay, int16_t *az)
{
    uint8_t buf[6];
    I2C_ReadReg_Multi(MPU6050_ADDR, MPU6050_REG_ACCEL_XOUT_H, buf, 6);

    *ax = (int16_t)((buf[0] << 8) | buf[1]);
    *ay = (int16_t)((buf[2] << 8) | buf[3]);
    *az = (int16_t)((buf[4] << 8) | buf[5]);
}

/**
 * @brief 读取陀螺仪数据
 */
void MPU6050_ReadGyro(int16_t *gx, int16_t *gy, int16_t *gz)
{
    uint8_t buf[6];
    I2C_ReadReg_Multi(MPU6050_ADDR, MPU6050_REG_GYRO_XOUT_H, buf, 6);

    *gx = (int16_t)((buf[0] << 8) | buf[1]);
    *gy = (int16_t)((buf[2] << 8) | buf[3]);
    *gz = (int16_t)((buf[4] << 8) | buf[5]);
}

// 使用示例
int16_t ax, ay, az, gx, gy, gz;
MPU6050_Init();
while(1)
{
    MPU6050_ReadAccel(&ax, &ay, &az);
    MPU6050_ReadGyro(&gx, &gy, &gz);
    printf("Accel: %d, %d, %d | Gyro: %d, %d, %d\n", ax, ay, az, gx, gy, gz);
    HAL_Delay(100);
}

6.3 OLED显示屏(SSD1306)

设备特性

参数 说明
地址 0x3C(SA0=GND)或 0x3D(SA0=VCC)
分辨率 128x64 或 128x32
控制方式 命令/数据通过Co/Dc位区分

驱动代码(关键部分)

复制代码
#define OLED_ADDR       0x3C
#define OLED_CMD_MODE   0x00    // 命令模式
#define OLED_DATA_MODE  0x40    // 数据模式

/**
 * @brief 向OLED发送命令
 */
void OLED_WriteCmd(uint8_t cmd)
{
    uint8_t data[2] = {OLED_CMD_MODE, cmd};
    HAL_I2C_Master_Transmit(&hi2c1, OLED_ADDR << 1, data, 2, 1000);
}

/**
 * @brief 向OLED发送数据
 */
void OLED_WriteData(uint8_t *pData, uint16_t len)
{
    uint8_t buf[129];
    buf[0] = OLED_DATA_MODE;
    memcpy(&buf[1], pData, len);
    HAL_I2C_Master_Transmit(&hi2c1, OLED_ADDR << 1, buf, len + 1, 1000);
}

/**
 * @brief OLED初始化序列
 */
void OLED_Init(void)
{
    HAL_Delay(100);  // 等待上电稳定

    OLED_WriteCmd(0xAE);  // 关闭显示
    OLED_WriteCmd(0xD5);  // 设置时钟分频
    OLED_WriteCmd(0x80);
    OLED_WriteCmd(0xA8);  // 设置多路复用
    OLED_WriteCmd(0x3F);  // 1/64 duty
    OLED_WriteCmd(0xD3);  // 设置显示偏移
    OLED_WriteCmd(0x00);
    OLED_WriteCmd(0x40);  // 设置显示起始行
    OLED_WriteCmd(0x8D);  // 使能电荷泵
    OLED_WriteCmd(0x14);
    OLED_WriteCmd(0x20);  // 设置内存寻址模式
    OLED_WriteCmd(0x00);  // 水平寻址模式
    OLED_WriteCmd(0xA1);  // 设置段重映射
    OLED_WriteCmd(0xC8);  // 设置COM扫描方向
    OLED_WriteCmd(0xDA);  // 设置COM引脚配置
    OLED_WriteCmd(0x12);
    OLED_WriteCmd(0x81);  // 设置对比度
    OLED_WriteCmd(0xCF);
    OLED_WriteCmd(0xD9);  // 设置预充电周期
    OLED_WriteCmd(0xF1);
    OLED_WriteCmd(0xDB);  // 设置VCOMH
    OLED_WriteCmd(0x30);
    OLED_WriteCmd(0xA4);  // 全局显示开启
    OLED_WriteCmd(0xA6);  // 正常显示(非反色)
    OLED_WriteCmd(0xAF);  // 开启显示
}

/**
 * @brief 清屏
 */
void OLED_Clear(void)
{
    uint8_t buf[128] = {0};
    for(uint8_t page = 0; page < 8; page++)
    {
        OLED_WriteCmd(0xB0 + page);  // 设置页地址
        OLED_WriteCmd(0x00);         // 设置列低地址
        OLED_WriteCmd(0x10);         // 设置列高地址
        OLED_WriteData(buf, 128);    // 写入128字节0
    }
}

6.4 其他常见IIC设备

设备 地址 功能 典型应用
BMP280 0x76/0x77 气压/温度传感器 海拔测量、气象站
SHT30 0x44/0x45 温湿度传感器 环境监测
BH1750 0x23/0x5C 光照强度传感器 自动亮度调节
PCF8574 0x20~0x27 IO扩展芯片 扩展GPIO
ADS1115 0x48~0x4B 16位ADC 高精度模拟采集
DS3231 0x68 实时时钟RTC 时间记录
HMC5883L 0x1E 电子罗盘 方向检测

7. 调试技巧与常见问题

7.1 调试工具

7.1.1 逻辑分析仪

使用逻辑分析仪抓取I2C波形是最有效的调试手段:

复制代码
推荐工具:
- Saleae Logic(8通道,支持I2C协议解析)
- DSLogic(国产,性价比高)
-  cheap 8通道逻辑分析仪(淘宝,约30元)

7.1.2 示波器

观察SCL和SDA波形:

  • 检查电平幅度(应为3.3V或5V)
  • 检查上升沿/下降沿(上升沿不应过缓)
  • 检查时序是否符合标准

7.2 常见问题排查

问题1:总线一直忙(BUSY)

现象I2C_SR2BUSY位一直为1

原因与解决

  1. SDA被拉低:检查是否有设备故障将SDA拉低

  2. 起始/停止条件不完整:用软件发送9个时钟脉冲+STOP恢复

  3. 上拉电阻问题:检查或更换上拉电阻

    // 总线恢复程序
    void I2C_BusRecovery(void)
    {
    // 配置SDA为GPIO输出
    GPIO_InitTypeDef GPIO_InitStruct = {0};
    GPIO_InitStruct.Pin = GPIO_PIN_7;
    GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_OD;
    HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);

    复制代码
     // 发送9个时钟脉冲
     for(int i = 0; i < 9; i++)
     {
         HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6, GPIO_PIN_SET);
         HAL_Delay(1);
         HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6, GPIO_PIN_RESET);
         HAL_Delay(1);
     }
    
     // 发送STOP条件
     HAL_GPIO_WritePin(GPIOB, GPIO_PIN_7, GPIO_PIN_RESET);
     HAL_Delay(1);
     HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6, GPIO_PIN_SET);
     HAL_Delay(1);
     HAL_GPIO_WritePin(GPIOB, GPIO_PIN_7, GPIO_PIN_SET);
     HAL_Delay(1);
    
     // 重新初始化I2C
     HAL_I2C_Init(&hi2c1);

    }

问题2:发送地址后无ACK

现象AF标志置位,设备无响应

排查步骤

  1. 检查设备地址:确认地址正确(注意7位地址vs 8位地址)
  2. 检查设备供电:确保设备已上电
  3. 检查接线:SDA和SCL是否接反或接触不良
  4. 检查上拉电阻:用万用表测量空闲时电平是否为高
  5. 检查设备是否损坏:换一块芯片测试

问题3:数据接收乱码

现象:读取的数据不正确

原因与解决

  1. 寄存器地址错误:确认要读取的寄存器地址
  2. 数据格式错误:注意大小端(STM32为小端)
  3. 读取长度错误:确保读取长度与数据大小匹配
  4. 设备未准备好:某些设备需要延时等待

问题4:硬件I2C卡死

现象:程序卡在I2C等待循环中

解决方案

  1. 使用超时机制:HAL库已内置超时,可调整超时时间

  2. 使用中断/DMA:避免轮询阻塞

  3. 错误处理:在错误回调中复位I2C外设

    // I2C错误处理与复位
    void I2C_Error_Handler(I2C_HandleTypeDef *hi2c)
    {
    HAL_I2C_DeInit(hi2c);
    HAL_Delay(10);
    HAL_I2C_Init(hi2c);
    }

7.3 上拉电阻选择

总线条件 推荐上拉电阻 说明
标准模式100KHz,短距离 4.7kΩ 通用选择
快速模式400KHz 2.2kΩ~4.7kΩ 减小电阻提高上升沿
长距离/多设备 1kΩ~2.2kΩ 驱动能力更强
低功耗应用 10kΩ 降低静态电流

计算公式:R_max = t_r / (0.8473 × C_b)

  • t_r: 最大上升时间(标准模式1000ns,快速模式300ns)
  • C_b: 总线电容(通常<400pF)

7.4 布线建议

  1. SDA和SCL尽量平行布线,减少串扰
  2. 避免与高频信号线平行走线
  3. 总线长度尽量短(标准模式<1米,快速模式<0.5米)
  4. 多设备时采用星型或链型拓扑
  5. 在总线两端加适当电容滤波(<100pF)

8. 总结

8.1 IIC核心要点回顾

复制代码
┌─────────────────────────────────────────────────────────────┐
│                      IIC总线核心要点                          │
├─────────────────────────────────────────────────────────────┤
│  1. 两线制:SDA(数据)+ SCL(时钟)                          │
│  2. 开漏输出 + 上拉电阻 = 线与逻辑                           │
│  3. 起始条件:SCL高时SDA下降沿                               │
│  4. 停止条件:SCL高时SDA上升沿                               │
│  5. 数据有效性:SCL高电平期间SDA稳定                          │
│  6. 应答机制:每字节后第9个时钟为ACK/NACK                     │
│  7. 7位/10位地址寻址从设备                                   │
│  8. 支持多主模式,具有仲裁机制                               │
└─────────────────────────────────────────────────────────────┘

8.2 STM32开发建议

  1. 优先使用HAL库:简化开发,提高可移植性
  2. 调试时先用软件模拟I2C:确认硬件连接无误
  3. 使用逻辑分析仪:快速定位时序问题
  4. 注意超时处理:防止程序卡死
  5. DMA用于大数据量:如OLED显存刷新
  6. 中断用于实时性要求高的场景

8.3 学习路径建议

复制代码
第一阶段:理解原理
    ↓ 学习I2C协议时序、信号类型
第二阶段:软件模拟
    ↓ 用GPIO实现,加深对时序的理解
第三阶段:硬件I2C
    ↓ 使用STM32硬件外设,掌握HAL库API
第四阶段:设备驱动
    ↓ 编写EEPROM、传感器等设备的驱动
第五阶段:项目实战
    ↓ 综合应用,解决实际问题

附录

附录A:常用I2C设备地址速查表

设备 地址(7位) 地址(8位写) 地址(8位读)
AT24C02 0x50 0xA0 0xA1
AT24C04 0x50 0xA0 0xA1
AT24C08 0x50 0xA0 0xA1
AT24C16 0x50 0xA0 0xA1
AT24C32 0x57 0xAE 0xAF
AT24C64 0x50 0xA0 0xA1
MPU6050 0x68 0xD0 0xD1
MPU9250 0x68 0xD0 0xD1
BMP280 0x76 0xEC 0xED
BMP280 0x77 0xEE 0xEF
SSD1306 0x3C 0x78 0x79
SSD1306 0x3D 0x7A 0x7B
SHT30 0x44 0x88 0x89
SHT30 0x45 0x8A 0x8B
BH1750 0x23 0x46 0x47
BH1750 0x5C 0xB8 0xB9
PCF8574 0x20~0x27 0x40~0x4E 0x41~0x4F
ADS1115 0x48 0x90 0x91
ADS1115 0x49 0x92 0x93
DS3231 0x68 0xD0 0xD1
HMC5883L 0x1E 0x3C 0x3D

附录B:I2C标准时序参数(标准模式100KHz)

参数 符号 最小值 最大值 单位
SCL时钟频率 f_SCL 0 100 KHz
起始条件保持时间 t_HD;STA 4.0 - μs
SCL低电平时间 t_LOW 4.7 - μs
SCL高电平时间 t_HIGH 4.0 - μs
起始条件建立时间 t_SU;STA 4.7 - μs
数据保持时间 t_HD;DAT 0 3.45 μs
数据建立时间 t_SU;DAT 250 - ns
SDA/SCL上升时间 t_R - 1000 ns
SDA/SCL下降时间 t_F - 300 ns
停止条件建立时间 t_SU;STO 4.0 - μs
总线空闲时间 t_BUF 4.7 - μs

附录C:参考资源

  • STM32参考手册:RM0008(STM32F1)、RM0090(STM32F4)
  • STM32 HAL/LL驱动用户手册:UM1850
  • NXP I2C总线规范:UM10204(I2C-bus specification and user manual)
  • 正点原子/野火STM32开发指南
  • 《STM32库开发实战指南》 - 刘火良

文档说明:本教程基于STM32F1/F4系列HAL库编写,如需适配其他STM32系列,请查阅对应参考手册调整寄存器名称和时钟配置。

相关推荐
iCxhust3 小时前
MTK8088单板机制作(一)时钟电路
汇编·单片机·嵌入式硬件·微机原理·8088单板机
2601_958352903 小时前
双麦 DSP 音频拾音模块 A-68:多场景远场语音交互的声学解决方案
嵌入式硬件·音视频·降噪·回音消除·音频处理模块
崇山峻岭之间3 小时前
单片机直流有刷电机速度环PID控制实验
单片机·嵌入式硬件
xiangw@GZ3 小时前
智能锁浮空系统指纹头金属环ESD防护技术分析
单片机·嵌入式硬件
ACP广源盛139246256734 小时前
IX7008 PCIe 交换芯片@ACP#RTX Spark 经济型 8 口扩展芯片(对比 ASM1806)
大数据·人工智能·分布式·嵌入式硬件·gpt·spark·电脑
项目題供诗4 小时前
STM32-DMA直接存储器存储(二十)
stm32·单片机·嵌入式硬件
耳朵东先生4 小时前
STM32 开发利器:SEGGER RTT 日志打印与 Shell 实践解析
单片机·嵌入式硬件
ACP广源盛139246256734 小时前
IX6012 PCIe 交换芯片@ACP#RTX Spark 入门级 12 口存储外设扩展方案(对比 ASM1812)
大数据·人工智能·分布式·嵌入式硬件·gpt·spark·电脑
2601_958352904 小时前
对讲系统音频优化实战:解决回声、啸叫、环境噪音与远场拾音难题
嵌入式硬件·音视频·语音识别·降噪处理·音频处理模块·硬件开发模块