STM32中I2C协议详解

发布于:2025-07-10 ⋅ 阅读:(18) ⋅ 点赞:(0)

前言

在嵌入式系统中,设备间的短距离通信协议中,I2C(Inter-Integrated Circuit,集成电路互连)以其信号线少、布线简单、支持多从机等特点,被广泛应用于传感器、EEPROM、OLED屏等中低速外设的通信场景。与SPI的高速全双工和UART的异步简单相比,I2C仅需2根线即可实现多设备间的半双工通信,在资源受限的嵌入式系统中极具优势。

本文将从I2C协议基础出发,系统解析STM32 I2C外设的工作原理、硬件设计要点、软件配置流程及实战案例,涵盖寄存器级编程、HAL库应用、中断与DMA传输等核心内容,并提供详细的调试技巧与常见问题解决方案,旨在帮助嵌入式开发者全面掌握STM32中I2C的应用。

一、I2C协议基础

1.1 什么是I2C?

I2C是一种由飞利浦(现恩智浦)公司开发的同步串行通信协议,主要用于短距离、低速设备间的通信(通常速率≤400kbps,高速模式可达3.4Mbps)。其核心特点包括:

  • 双线通信:仅需SCL(Serial Clock,串行时钟)和SDA(Serial Data,串行数据)两根信号线;
  • 多从机支持:通过7位或10位地址区分从机,总线上可连接多个从机(理论上最多127个7位地址设备);
  • 主从式架构:通信由主机(如STM32)发起,从机(如传感器)被动响应,主机负责产生时钟信号;
  • 半双工通信:同一时刻只能发送或接收数据,通过SDA线双向传输。

1.2 I2C信号线组成

I2C通信仅需两根信号线,所有设备的SCL和SDA线并联连接:

信号线 功能描述
SCL 时钟线,由主机产生,用于同步数据传输(高低电平切换的频率决定通信速率)。
SDA 数据线,双向传输数据,主机和从机通过该线发送或接收数据(需开漏输出+上拉电阻)。

关键特性

  • SDA和SCL均需通过上拉电阻(通常4.7kΩ~10kΩ)接电源(如3.3V),确保空闲时为高电平;
  • 信号线采用开漏输出(或集电极开路),支持“线与”逻辑(多个设备同时拉低时,总线为低电平;仅当所有设备释放时,总线才为高电平)。
    在这里插入图片描述

1.3 I2C通信速率

I2C定义了三种主要通信速率(不同版本协议略有差异):

  • 标准模式(Standard-mode):100kbps(最常用);
  • 快速模式(Fast-mode):400kbps;
  • 高速模式(Fast-mode Plus):1Mbps(部分设备支持);
  • 超高速模式(High-speed mode):3.4Mbps(高端设备支持)。

注意:总线上所有设备的最高支持速率必须≥主机使用的速率,否则需以最低速率通信(如主机支持400kbps,但某从机仅支持100kbps,则总线速率需设为100kbps)。

1.4 I2C核心时序信号

I2C协议通过特定的时序信号定义通信的开始、结束、数据传输和应答,是协议理解的核心。

1.4.1 起始位(S)与停止位(P)
  • 起始位(S):当SCL为高电平时,SDA从高电平跳变为低电平(下降沿),标志一次通信的开始;
  • 停止位(P):当SCL为高电平时,SDA从低电平跳变为高电平(上升沿),标志一次通信的结束。

在这里插入图片描述

(示意图:SCL高电平时,SDA下降沿为起始位,上升沿为停止位)

1.4.2 数据传输时序
  • 数据以8位为一帧,高位在前(MSB),低位在后(LSB);
  • 每传输1位数据,SCL需有一个高电平脉冲(数据在SCL高电平时保持稳定,避免信号跳变导致误读);
  • 8位数据传输完成后,紧跟一个应答位(ACK)或非应答位(NACK)。
1.4.3 应答信号(ACK/NACK)
  • 应答位(ACK):接收方在第9个SCL时钟周期内将SDA拉低,表示成功接收数据;
  • 非应答位(NACK):接收方在第9个SCL时钟周期内让SDA保持高电平,表示未接收数据(如数据错误、从机忙等)。

规则

  • 主机发送数据时,由从机产生ACK/NACK;
  • 主机接收数据时,由主机产生ACK/NACK(除最后一个字节,通常用NACK表示接收结束)。

1.5 I2C通信帧结构

一次完整的I2C通信由“起始位→地址帧→数据帧→停止位”组成,根据方向(读/写)不同,帧结构略有差异。

1.5.1 主机向从机写数据(写操作)

帧结构:S → [从机地址+W] → ACK → [数据1] → ACK → [数据2] → ACK → ... → P

  • S:起始位;
  • [从机地址+W]:7位从机地址+1位写标志(0),共8位;
  • ACK:从机应答;
  • [数据n]:主机发送的n字节数据;
  • P:停止位。
1.5.2 主机从从机读数据(读操作)

帧结构:S → [从机地址+R] → ACK → [数据1] → ACK → [数据2] → ACK → ... → [数据n] → NACK → P

  • [从机地址+R]:7位从机地址+1位读标志(1),共8位;
  • 最后一个数据字节后,主机发送NACK,表示不再接收数据,随后发送停止位。
1.5.3 复合操作(先写后读,如读指定寄存器)

部分从机(如EEPROM、传感器)需先写入寄存器地址,再读取数据,帧结构为:
S → [从机地址+W] → ACK → [寄存器地址] → ACK → S → [从机地址+R] → ACK → [数据] → NACK → P

  • 中间的S为“重复起始位”(Repeated Start),用于连续通信而不释放总线。

1.6 I2C地址机制

I2C通过地址区分总线上的从机,地址长度有两种:

  • 7位地址:最常用,范围0127(其中0为广播地址,1127为有效地址);
  • 10位地址:扩展地址,支持更多从机(仅部分设备支持)。

地址映射:从机地址由硬件引脚和固定地址组成,例如EEPROM AT24C02的固定地址为0xA0,其A0/A1/A2引脚接高/低电平可配置低3位地址(如A0=0、A1=0、A2=0时,地址为0xA0)。

二、STM32 I2C外设详解

STM32系列芯片(如F1、F4、H7等)普遍集成多个I2C外设(如F103有2个I2C,F407有3个I2C),支持主机模式、从机模式及多种高级特性。

2.1 I2C外设主要特性

以STM32F103(中低端型号)为例,其I2C外设核心特性如下:

  • 支持主机模式和从机模式;
  • 支持7位和10位地址;
  • 支持标准模式(100kbps)和快速模式(400kbps);
  • 支持软件或硬件应答控制;
  • 支持中断和DMA传输(减少CPU占用);
  • 支持 SMBus(系统管理总线)协议(兼容I2C);
  • 内置仲裁和时钟同步机制(多主机场景);
  • 支持数据校验和错误检测(如应答错误、仲裁丢失)。

高端型号(如F4、H7)的I2C外设性能更强,例如F407支持快速模式Plus(1Mbps),H7系列支持超时检测和更多错误处理机制。

2.2 引脚映射

I2C外设的SCL/SDA引脚通过复用功能配置,不同型号的映射不同。以STM32F103C8T6为例,I2C1的默认引脚为:

  • SCL:PB6(复用开漏输出);
  • SDA:PB7(复用开漏输出)。

若默认引脚被占用,可通过重映射功能切换(如I2C1可重映射到PB8/PB9),配置时需:

  • 使能AFIO时钟(RCC->APB2ENR |= RCC_APB2ENR_AFIOEN);
  • 通过AFIO重映射寄存器(AFIO->MAPR)配置映射关系。

2.3 时钟源与速率计算

I2C的时钟源来自APB1总线(所有I2C外设均挂载APB1):

  • STM32F103的APB1时钟最高36MHz;
  • 速率计算公式:I2C时钟频率 = APB1时钟频率 / (16 + 2*CCR*TRISE)
    其中:
    • CCR(时钟控制寄存器):配置分频系数,决定SCL高/低电平时间;
    • TRISE(上升时间寄存器):配置SDA/SCL信号的上升时间(与速率相关)。

示例:标准模式(100kbps)配置(APB1=36MHz):

  • TRISE = 36 + 1 = 37(标准模式下,TRISE=APB1时钟频率(MHz) + 1);
  • CCR = 36000000 / (2 * 100000 * 2) = 90(标准模式下,高/低电平时间相等,总周期=1/100000=10μs,故高电平时间=5μs,CCR=5μs/(1/36MHz)=180?此处需根据手册精确计算,不同模式公式略有差异)。

2.4 核心寄存器解析

STM32 I2C的配置与操作通过以下核心寄存器实现(以F103为例):

2.4.1 控制寄存器1(I2C_CR1)
位段 功能描述
PE[0] 外设使能:1=使能I2C;0=禁用(配置前需禁用)。
ACK[10] 应答使能:1=使能应答(接收数据后自动产生ACK);0=禁用(产生NACK)。
START[8] 起始位生成:1=产生起始位(自动清0)。
STOP[9] 停止位生成:1=产生停止位(自动清0)。
ITEVTEN[14] 事件中断使能:1=使能事件中断(如起始位发送、地址匹配等)。
ITBUFEN[15] 缓冲区中断使能:1=使能数据缓冲区中断(如TXE、RXNE)。
2.4.2 控制寄存器2(I2C_CR2)
位段 功能描述
FREQ[5:0] 外设输入时钟频率:配置APB1时钟频率(单位MHz,如36MHz则设为0x24)。
DMAEN[11] DMA请求使能:1=使能DMA传输。
2.4.3 时钟控制寄存器(I2C_CCR)
位段 功能描述
F/S[15] 模式选择:0=标准模式(100kbps);1=快速模式(400kbps)。
CCR[11:0] 时钟控制:决定SCL线的高/低电平时间(与速率相关)。
2.4.4 状态寄存器1(I2C_SR1)
位段 功能描述
SB[0] 起始位发送完成:1=起始位已发送(仅主机模式)。
ADDR[1] 地址发送完成:1=地址帧已发送且收到ACK(需结合SR2的ADDR位确认)。
TXE[7] 发送数据寄存器空:1=DR寄存器为空(可写入下一字节)。
RXNE[6] 接收数据寄存器非空:1=DR寄存器有数据(可读取)。
BTF[2] 字节传输完成:1=数据传输完成(最后一字节已发送/接收)。
AF[4] 应答失败:1=接收方未产生ACK(需软件清0)。
2.4.5 数据寄存器(I2C_DR)
  • 8位寄存器,发送时写入数据,接收时读取数据;
  • 写入DR会触发数据发送,读取DR会清除RXNE标志。

三、I2C硬件设计要点

I2C硬件设计的核心是确保总线信号稳定,避免噪声干扰和电平不匹配,以下是关键设计要点:

3.1 上拉电阻选择

SDA和SCL线必须通过上拉电阻接电源,电阻值选择需考虑:

  • 推荐值:4.7kΩ~10kΩ(标准模式常用4.7kΩ,快速模式可减小至2.2kΩ);
  • 总线上设备数量:设备越多,负载电容越大,需减小电阻值(但不宜过小,避免电流过大);
  • 电源电压:3.3V系统常用4.7kΩ,5V系统可选用10kΩ。

布局建议:上拉电阻尽量靠近I2C主机,缩短信号线到电源的路径,减少噪声。

3.2 信号线布局

  • 长度限制:标准模式下,信号线长度建议≤1米;快速模式下≤0.5米,过长会导致信号延迟和反射;
  • 走线规范:SDA和SCL线应平行走线,长度尽可能一致,避免交叉或靠近高速信号线(如SPI的SCK、电机驱动线);
  • 接地处理:信号线下方铺地平面,增强抗干扰能力;总线上所有设备需共地,避免地电位差。

3.3 电平匹配

若总线上存在不同电平的设备(如3.3V的STM32和5V的EEPROM),需进行电平转换:

  • 方案1:使用专用电平转换芯片(如PCA9306),支持双向电平转换;
  • 方案2:利用开漏输出特性,将3.3V设备的SDA/SCL通过上拉电阻接5V(需确保3.3V设备的GPIO容忍5V输入)。

3.4 多从机地址冲突处理

当多个从机的默认地址冲突时,可通过硬件引脚修改从机地址:

  • 多数从机(如AT24C02、SHT30)提供地址配置引脚(如A0/A1/A2),通过接高/低电平改变地址的低几位;
  • 例如:AT24C02的固定地址为0xA0,A0引脚接GND时地址为0xA0,接VCC时为0xA2。

四、I2C软件配置步骤

本节以STM32F103为主机,实现与从机的I2C通信,分别介绍寄存器级和HAL库的配置方法,以“标准模式(100kbps)、7位地址、主机模式”为基础配置。

4.1 寄存器级配置(I2C1,100kbps)

步骤1:使能时钟
// 使能GPIOB、I2C1和AFIO时钟
RCC->APB2ENR |= RCC_APB2ENR_IOPBEN | RCC_APB2ENR_AFIOEN;
RCC->APB1ENR |= RCC_APB1ENR_I2C1EN;
步骤2:配置GPIO引脚(开漏复用输出)
// 配置PB6(SCL)和PB7(SDA)为复用开漏输出
GPIOB->CRL &= ~(GPIO_CRL_MODE6 | GPIO_CRL_CNF6);
GPIOB->CRL |= GPIO_CRL_MODE6_1;  // 输出速率2MHz(低速,I2C无需高速)
GPIOB->CRL |= GPIO_CRL_CNF6_1;   // 复用开漏输出

GPIOB->CRL &= ~(GPIO_CRL_MODE7 | GPIO_CRL_CNF7);
GPIOB->CRL |= GPIO_CRL_MODE7_1;  // 输出速率2MHz
GPIOB->CRL |= GPIO_CRL_CNF7_1;   // 复用开漏输出
步骤3:配置I2C1参数(标准模式100kbps)
// 禁用I2C1(配置前必须禁用)
I2C1->CR1 &= ~I2C_CR1_PE;

// 配置CR2:APB1时钟36MHz(0x24=36)
I2C1->CR2 &= ~I2C_CR2_FREQ;
I2C1->CR2 |= 0x24;

// 配置CCR:标准模式(100kbps),高/低电平时间相等
I2C1->CCR &= ~I2C_CCR_FS;  // 标准模式
I2C1->CCR |= 0x5A;         // CCR=90(36MHz/(2*100000*2)=90)

// 配置TRISE:标准模式下TRISE=36+1=37
I2C1->TRISE = 0x25;

// 使能I2C1,使能应答
I2C1->CR1 |= I2C_CR1_ACK | I2C_CR1_PE;
步骤4:实现基本读写函数
// 等待I2C事件(用于判断通信状态)
void I2C1_WaitEvent(I2C_TypeDef* I2Cx, uint32_t event) {
    uint32_t timeout = 0xFFFF;
    while ((I2Cx->SR1 & event) == 0) {
        if (timeout-- == 0) return;  // 超时退出
    }
}

// 发送起始位
void I2C1_Start(void) {
    I2C1->CR1 |= I2C_CR1_START;          // 产生起始位
    I2C1_WaitEvent(I2C1, I2C_SR1_SB);   // 等待起始位发送完成
}

// 发送停止位
void I2C1_Stop(void) {
    I2C1->CR1 |= I2C_CR1_STOP;           // 产生停止位
}

// 发送从机地址(7位地址+读写标志)
void I2C1_SendAddr(uint8_t addr, uint8_t rw) {
    addr = (addr << 1) | (rw & 0x01);    // 地址左移1位,最低位为读写标志(0=写,1=读)
    I2C1->DR = addr;
    I2C1_WaitEvent(I2C1, I2C_SR1_ADDR); // 等待地址发送完成
    (void)I2C1->SR1; (void)I2C1->SR2;   // 清除ADDR标志(读SR1和SR2)
}

// 向从机发送1字节数据
void I2C1_SendByte(uint8_t data) {
    I2C1_WaitEvent(I2C1, I2C_SR1_TXE);  // 等待发送缓冲区空
    I2C1->DR = data;
    I2C1_WaitEvent(I2C1, I2C_SR1_BTF);  // 等待字节传输完成
}

// 从从机接收1字节数据(最后一字节用NACK)
uint8_t I2C1_ReceiveByte(uint8_t is_last) {
    if (is_last) {
        I2C1->CR1 &= ~I2C_CR1_ACK;      // 最后一字节,禁用应答(NACK)
    }
    I2C1_WaitEvent(I2C1, I2C_SR1_RXNE); // 等待接收数据
    return I2C1->DR;
}

// 主机向从机写数据(addr:7位地址,data:数据,len:长度)
void I2C1_Write(uint8_t addr, uint8_t *data, uint16_t len) {
    I2C1_Start();                       // 起始位
    I2C1_SendAddr(addr, 0);             // 发送写地址
    for (uint16_t i = 0; i < len; i++) {
        I2C1_SendByte(data[i]);         // 发送数据
    }
    I2C1_Stop();                        // 停止位
    I2C1->CR1 |= I2C_CR1_ACK;           // 恢复应答使能
}

// 主机从从机读数据(addr:7位地址,data:接收缓冲区,len:长度)
void I2C1_Read(uint8_t addr, uint8_t *data, uint16_t len) {
    I2C1_Start();                       // 起始位
    I2C1_SendAddr(addr, 1);             // 发送读地址
    for (uint16_t i = 0; i < len; i++) {
        data[i] = I2C1_ReceiveByte(i == len-1); // 接收数据(最后一字节用NACK)
    }
    I2C1_Stop();                        // 停止位
    I2C1->CR1 |= I2C_CR1_ACK;           // 恢复应答使能
}

4.2 HAL库配置(基于STM32CubeMX)

步骤1:创建工程与时钟配置
  • 打开STM32CubeMX,选择芯片型号(如STM32F103C8T6);
  • 配置RCC:选择HSE时钟,配置系统时钟为72MHz(APB1时钟36MHz)。
步骤2:配置I2C1
  • 在“Pinout & Configuration”中,左侧选择“Connectivity”→“I2C1”;
  • 模式选择“I2C”(主机模式);
  • 参数配置:
    • I2C Speed Mode:Standard Mode(100kbps);
    • I2C Clock Speed:100000;
    • Addressing Mode:7-bit;
  • 确认引脚:I2C1_SCL=PB6,I2C1_SDA=PB7(默认引脚)。
步骤3:生成代码
  • 配置工程路径和IDE(如Keil MDK);
  • 生成代码(确保I2C初始化函数MX_I2C1_Init()被正确生成)。
步骤4:HAL库读写函数实现
// 主机向从机写数据(阻塞式)
HAL_StatusTypeDef I2C1_Write(uint8_t addr, uint8_t *data, uint16_t len) {
    // 等待总线空闲
    while (HAL_I2C_GetState(&hi2c1) != HAL_I2C_STATE_READY);
    // 发送数据(7位地址,无寄存器地址)
    return HAL_I2C_Master_Transmit(&hi2c1, (addr << 1) | 0, data, len, 100);
}

// 主机从从机读数据(阻塞式)
HAL_StatusTypeDef I2C1_Read(uint8_t addr, uint8_t *data, uint16_t len) {
    while (HAL_I2C_GetState(&hi2c1) != HAL_I2C_STATE_READY);
    // 接收数据(7位地址)
    return HAL_I2C_Master_Receive(&hi2c1, (addr << 1) | 1, data, len, 100);
}

// 先写寄存器地址再读数据(如读传感器指定寄存器)
HAL_StatusTypeDef I2C1_WriteRead(uint8_t addr, uint8_t reg, uint8_t *data, uint16_t len) {
    while (HAL_I2C_GetState(&hi2c1) != HAL_I2C_STATE_READY);
    // 发送寄存器地址,再读取数据(重复起始位)
    return HAL_I2C_Mem_Read(&hi2c1, (addr << 1) | 0, reg, I2C_MEMADD_SIZE_8BIT, data, len, 100);
}

五、实战案例:I2C外设通信

5.1 案例1:与AT24C02 EEPROM通信

AT24C02是一款2KB的I2C EEPROM,常用于存储掉电不丢失的数据(如设备参数、校准值),地址可通过A0/A1/A2引脚配置(默认0xA0)。在这里插入图片描述

5.1.1 关键操作
  1. 写入数据(页写入)

    • AT24C02的页大小为8字节,单次写入不能超过一页;
    • 需先发送“设备地址+写”→“存储地址”→“数据”。
    // 向AT24C02指定地址写入数据(寄存器级)
    void AT24C02_Write(uint8_t addr, uint8_t *data, uint16_t len) {
        uint16_t pos = 0;
        uint8_t page_remain;
        while (len > 0) {
            // 计算当前页剩余空间(页大小8字节)
            page_remain = 8 - (addr % 8);
            if (len < page_remain) page_remain = len;
            
            I2C1_Start();
            I2C1_SendAddr(0xA0 >> 1, 0);  // 从机地址0xA0(7位为0x50)
            I2C1_SendByte(addr);          // 存储地址
            for (uint8_t i = 0; i < page_remain; i++) {
                I2C1_SendByte(data[pos++]);
            }
            I2C1_Stop();
            HAL_Delay(5);  // 等待EEPROM内部写入完成(典型5ms)
            
            addr += page_remain;
            len -= page_remain;
        }
    }
    
  2. 读取数据

    • 发送“设备地址+写”→“存储地址”→“重复起始位”→“设备地址+读”→“数据”。
    // 从AT24C02指定地址读取数据(HAL库)
    void AT24C02_Read(uint8_t addr, uint8_t *data, uint16_t len) {
        HAL_I2C_Mem_Read(&hi2c1, 0xA0, addr, I2C_MEMADD_SIZE_8BIT, data, len, 100);
        HAL_Delay(1);
    }
    

5.2 案例2:与SHT30温湿度传感器通信

SHT30是一款高精度I2C温湿度传感器,地址为0x44(A引脚接GND)或0x45(A引脚接VCC),支持测量温度(-40125℃)和湿度(0100%RH)。
在这里插入图片描述

5.2.1 通信流程
  1. 初始化传感器:发送软复位命令(0x30A2),确保传感器处于就绪状态。

    void SHT30_Reset(void) {
        uint8_t cmd[2] = {0x30, 0xA2};
        I2C1_Write(0x44, cmd, 2);  // 0x44为7位地址,写操作
        HAL_Delay(10);
    }
    
  2. 触发测量:发送测量命令(0x2400为周期性测量,0x2C06为单次高精度测量)。

  3. 读取测量结果:传感器返回6字节数据(温度高8位、温度低8位、温度CRC、湿度高8位、湿度低8位、湿度CRC)。

    // 读取温湿度数据(HAL库)
    HAL_StatusTypeDef SHT30_Read(float *temp, float *humi) {
        uint8_t cmd[2] = {0x2C, 0x06};  // 单次高精度测量命令
        uint8_t data[6];
        
        // 发送测量命令
        if (HAL_I2C_Master_Transmit(&hi2c1, 0x44 << 1, cmd, 2, 100) != HAL_OK) {
            return HAL_ERROR;
        }
        HAL_Delay(50);  // 等待测量完成
        
        // 读取6字节数据
        if (HAL_I2C_Master_Receive(&hi2c1, 0x44 << 1 | 1, data, 6, 100) != HAL_OK) {
            return HAL_ERROR;
        }
        
        // 校验CRC(简化版,实际应计算CRC)
        if (data[2] != SHT30_CRC8(data, 2) || data[5] != SHT30_CRC8(data+3, 2)) {
            return HAL_ERROR;
        }
        
        // 转换温度(公式参考SHT30数据手册)
        uint16_t temp_raw = (data[0] << 8) | data[1];
        *temp = (temp_raw * 175.0f / 65535.0f) - 45.0f;
        
        // 转换湿度
        uint16_t humi_raw = (data[3] << 8) | data[4];
        *humi = humi_raw * 100.0f / 65535.0f;
        
        return HAL_OK;
    }
    

5.3 案例3:I2C中断与DMA传输(批量数据优化)

对于需要频繁读写大量数据的场景(如从多个传感器轮询数据),使用中断或DMA可减少CPU阻塞时间。

5.3.1 中断接收配置(HAL库)
uint8_t rx_buf[16];  // 接收缓冲区

// 初始化中断接收
void I2C1_IT_Init(void) {
    HAL_I2C_Receive_IT(&hi2c1, (0x44 << 1) | 1, rx_buf, 6);  // 从SHT30接收6字节
}

// I2C中断接收完成回调
void HAL_I2C_RxCpltCallback(I2C_HandleTypeDef *hi2c) {
    if (hi2c == &hi2c1) {
        // 处理接收数据(如解析温湿度)
        rx_complete_flag = 1;
        // 重新开启中断接收
        HAL_I2C_Receive_IT(&hi2c1, (0x44 << 1) | 1, rx_buf, 6);
    }
}
5.3.2 DMA发送示例(写入多个传感器配置)
uint8_t tx_buf[32];  // 存储多个传感器的配置命令

// 使用DMA发送配置数据
void I2C1_DMA_Send(uint8_t addr, uint16_t len) {
    HAL_I2C_Master_Transmit_DMA(&hi2c1, (addr << 1) | 0, tx_buf, len);
}

// DMA发送完成回调
void HAL_I2C_MasterTxCpltCallback(I2C_HandleTypeDef *hi2c) {
    if (hi2c == &hi2c1) {
        // 发送完成处理
        tx_complete_flag = 1;
    }
}

六、I2C高级特性与优化

6.1 从机模式配置

STM32的I2C外设可配置为从机模式,接收其他主机的读写操作,适用于多主机系统或作为从设备被控制。

从机模式配置要点(寄存器级):
  1. 配置从机地址:I2C_OAR1 = (addr << 1) | I2C_OAR1_ADDMODE;(7位地址);
  2. 使能地址匹配中断:I2C_CR1 |= I2C_CR1_ITEVTEN;
  3. 在中断服务函数中处理地址匹配、数据收发事件。
// I2C1从机中断服务函数
void I2C1_IRQHandler(void) {
    if (I2C1->SR1 & I2C_SR1_ADDR) {  // 地址匹配
        (void)I2C1->SR1; (void)I2C1->SR2;  // 清除标志
        if (I2C1->SR2 & I2C_SR2_TRA) {  // 主机写,从机接收
            // 准备接收数据
        } else {  // 主机读,从机发送
            // 准备发送数据
        }
    } else if (I2C1->SR1 & I2C_SR1_RXNE) {  // 接收数据
        rx_data = I2C1->DR;
    } else if (I2C1->SR1 & I2C_SR1_TXE) {  // 发送数据
        I2C1->DR = tx_data;
    }
}

6.2 总线错误处理

I2C通信中常见错误(如应答失败、仲裁丢失)需及时处理,避免总线锁定:

// 检测并清除I2C错误
void I2C1_ClearError(void) {
    if (I2C1->SR1 & (I2C_SR1_AF | I2C_SR1_ARLO | I2C_SR1_BERR)) {
        I2C1->SR1 &= ~(I2C_SR1_AF | I2C_SR1_ARLO | I2C_SR1_BERR);  // 清除错误标志
        I2C1->CR1 &= ~I2C_CR1_PE;  // 禁用外设
        I2C1->CR1 |= I2C_CR1_PE;   // 重新使能
    }
}

6.3 低功耗模式下的I2C

在STM32低功耗模式(如STOP模式)下,可通过I2C唤醒芯片:

  • 配置I2C为从机模式,使能地址匹配唤醒;
  • 进入STOP模式前,确保I2C外设处于使能状态;
  • 当主机发送匹配地址时,I2C产生中断,唤醒芯片。

七、常见问题与调试技巧

7.1 通信失败的核心原因

7.1.1 总线无响应(从机无ACK)
  • 现象:发送地址后始终无ACK,程序卡在等待ACK的循环中;
  • 原因
    • 从机地址错误(未考虑读写标志位,或硬件引脚配置错误);
    • 上拉电阻缺失或阻值过大,SDA/SCL线空闲时不是高电平;
    • 从机未上电、接线错误(如SDA与SCL接反);
    • 从机忙(如EEPROM正在内部写入,需等待)。
  • 排查
    • 用万用表测量SDA/SCL线空闲电平,确认是否为高电平;
    • 用示波器观察地址帧是否正确发送(如地址0xA0的写帧应为0xA0);
    • 降低通信速率(如从100kbps降至10kbps),排除速率不匹配问题。
7.1.2 数据乱码或校验错误
  • 现象:能收到ACK,但数据错误(如温湿度值异常);
  • 原因
    • 时序错误(如快速模式下未正确配置TRISE/CCR);
    • 信号线噪声过大(靠近干扰源、未铺地平面);
    • 从机未正确初始化(如传感器未复位)。
  • 排查
    • 用逻辑分析仪抓取SDA/SCL波形,对比从机数据手册的时序要求;
    • 检查从机初始化流程(如发送复位命令、等待就绪)。
7.1.3 总线锁定(I2C无响应)
  • 现象:一次通信失败后,后续所有I2C操作均无反应;
  • 原因:通信中断(如突然断电、程序复位)导致SDA/SCL线被拉低,总线处于锁定状态;
  • 解决
    • 软件复位I2C外设(禁用后重新使能);
    • 若软件复位无效,可通过GPIO模拟SCL线产生多个时钟脉冲,释放总线。

7.2 调试工具与方法

  1. 逻辑分析仪

    • 推荐使用带I2C解码功能的逻辑分析仪(如Saleae),直接解析SDA/SCL线上的地址、数据和应答信号;
    • 重点观察:起始位/停止位是否正确、地址帧是否匹配、ACK是否存在、数据时序是否符合模式要求。
  2. 最小系统验证

    • 用“双线连接”验证:仅连接STM32与一个从机(如AT24C02),排除其他设备干扰;
    • 编写简单测试函数(如读取从机ID),确认基本通信正常后再扩展功能。
  3. 软件调试技巧

    • 在关键步骤添加日志输出(通过UART),记录I2C状态(如“发送地址0xA0”“收到ACK”);
    • 使用HAL库的HAL_I2C_GetError()函数获取错误码(如HAL_I2C_ERROR_AF为应答失败)。

7.3 通信可靠性优化

  • 添加重试机制:对偶尔失败的操作(如传感器忙),重试2~3次;
  • 超时控制:所有I2C操作必须设置超时(如100ms),避免程序卡死;
  • CRC校验:对关键数据(如校准参数),在应用层添加CRC校验,弥补I2C无硬件校验的不足;
  • 避免频繁启停:连续读写时使用重复起始位(而非多次启停),减少总线开销。

八、总结与扩展

I2C协议以其简洁的硬件设计和灵活的多从机支持,在嵌入式系统中占据重要地位。本文从协议基础到实战案例,系统讲解了I2C的工作原理、STM32配置方法及调试技巧,核心要点包括:

  • I2C通过SCL和SDA双线通信,依赖起始位、地址帧、应答信号实现数据传输;
  • STM32 I2C外设支持主/从模式、中断/DMA传输,配置需关注时钟源、速率参数及时序;
  • 硬件设计需重视上拉电阻、信号线布局和电平匹配,直接影响通信稳定性;
  • 实战中需根据从机特性(如EEPROM的页写入、传感器的命令序列)设计通信流程。

未来学习可扩展至:

  • 多主机I2C系统的仲裁机制;
  • I2C与其他协议(如SPI、UART)的混合通信设计;
  • 基于I2C的传感器网络(如多个温湿度传感器组网);
  • 低功耗场景下的I2C休眠与唤醒策略。

掌握I2C不仅是嵌入式开发的基础技能,更是理解同步通信协议设计的关键。通过结合硬件调试工具(如逻辑分析仪)和软件优化技巧,可有效解决实际开发中的通信问题,提升系统可靠性。

附录:常用代码片段

  1. I2C总线释放函数(解决总线锁定)
void I2C1_ReleaseBus(void) {
    // 配置SDA/SCL为推挽输出
    GPIOB->CRL &= ~(GPIO_CRL_CNF6 | GPIO_CRL_CNF7);
    GPIOB->CRL |= GPIO_CRL_CNF6_0 | GPIO_CRL_CNF7_0;
    // 产生10个SCL时钟脉冲,释放总线
    for (uint8_t i = 0; i < 10; i++) {
        GPIOB->BSRR = GPIO_BSRR_BS6;  // SCL高
        HAL_Delay(1);
        GPIOB->BSRR = GPIO_BSRR_BR6;  // SCL低
        HAL_Delay(1);
    }
    // 恢复为复用开漏输出
    GPIOB->CRL &= ~(GPIO_CRL_CNF6 | GPIO_CRL_CNF7);
    GPIOB->CRL |= GPIO_CRL_CNF6_1 | GPIO_CRL_CNF7_1;
}
  1. CRC8校验函数(用于SHT30、AT24C02等)
uint8_t I2C_CRC8(uint8_t *data, uint8_t len) {
    uint8_t crc = 0xFF;
    for (uint8_t i = 0; i < len; i++) {
        crc ^= data[i];
        for (uint8_t j = 0; j < 8; j++) {
            if (crc & 0x80) {
                crc = (crc << 1) ^ 0x31;
            } else {
                crc <<= 1;
            }
        }
    }
    return crc;
}
  1. 多从机地址扫描函数
// 扫描总线上所有响应的从机地址
void I2C1_ScanSlaves(void) {
    uint8_t addr;
    for (addr = 0; addr < 128; addr++) {
        I2C1_Start();
        I2C1->DR = (addr << 1) | 0;  // 写地址
        HAL_Delay(1);
        if (!(I2C1->SR1 & I2C_SR1_AF)) {  // 无应答失败,即有从机响应
            printf("Found slave: 0x%02X\n", addr);
        }
        I2C1_Stop();
        HAL_Delay(10);
    }
}

网站公告

今日签到

点亮在社区的每一天
去签到