前言
在嵌入式系统中,设备间的短距离通信协议中,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:使能时钟
c
// 使能GPIOB、I2C1和AFIO时钟
RCC->APB2ENR |= RCC_APB2ENR_IOPBEN | RCC_APB2ENR_AFIOEN;
RCC->APB1ENR |= RCC_APB1ENR_I2C1EN;
步骤2:配置GPIO引脚(开漏复用输出)
c
// 配置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)
c
// 禁用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:实现基本读写函数
c
// 等待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库读写函数实现
c
// 主机向从机写数据(阻塞式)
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 关键操作
-
写入数据(页写入):
- AT24C02的页大小为8字节,单次写入不能超过一页;
- 需先发送"设备地址+写"→"存储地址"→"数据"。
c// 向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; } }
-
读取数据:
- 发送"设备地址+写"→"存储地址"→"重复起始位"→"设备地址+读"→"数据"。
c// 从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 通信流程
-
初始化传感器:发送软复位命令(0x30A2),确保传感器处于就绪状态。
cvoid SHT30_Reset(void) { uint8_t cmd[2] = {0x30, 0xA2}; I2C1_Write(0x44, cmd, 2); // 0x44为7位地址,写操作 HAL_Delay(10); }
-
触发测量:发送测量命令(0x2400为周期性测量,0x2C06为单次高精度测量)。
-
读取测量结果:传感器返回6字节数据(温度高8位、温度低8位、温度CRC、湿度高8位、湿度低8位、湿度CRC)。
c// 读取温湿度数据(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库)
c
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发送示例(写入多个传感器配置)
c
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外设可配置为从机模式,接收其他主机的读写操作,适用于多主机系统或作为从设备被控制。
从机模式配置要点(寄存器级):
- 配置从机地址:
I2C_OAR1 = (addr << 1) | I2C_OAR1_ADDMODE;
(7位地址); - 使能地址匹配中断:
I2C_CR1 |= I2C_CR1_ITEVTEN;
; - 在中断服务函数中处理地址匹配、数据收发事件。
c
// 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通信中常见错误(如应答失败、仲裁丢失)需及时处理,避免总线锁定:
c
// 检测并清除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 调试工具与方法
-
逻辑分析仪:
- 推荐使用带I2C解码功能的逻辑分析仪(如Saleae),直接解析SDA/SCL线上的地址、数据和应答信号;
- 重点观察:起始位/停止位是否正确、地址帧是否匹配、ACK是否存在、数据时序是否符合模式要求。
-
最小系统验证:
- 用"双线连接"验证:仅连接STM32与一个从机(如AT24C02),排除其他设备干扰;
- 编写简单测试函数(如读取从机ID),确认基本通信正常后再扩展功能。
-
软件调试技巧:
- 在关键步骤添加日志输出(通过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不仅是嵌入式开发的基础技能,更是理解同步通信协议设计的关键。通过结合硬件调试工具(如逻辑分析仪)和软件优化技巧,可有效解决实际开发中的通信问题,提升系统可靠性。
附录:常用代码片段
- I2C总线释放函数(解决总线锁定):
c
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;
}
- CRC8校验函数(用于SHT30、AT24C02等):
c
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;
}
- 多从机地址扫描函数:
c
// 扫描总线上所有响应的从机地址
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);
}
}