嵌入式 I2C 通信协议详解(附 STM32 HAL 库实战代码)
引言
在嵌入式开发中,芯片间短距离通信是高频需求 —— 比如温湿度传感器(SHT30)、EEPROM 存储芯片(AT24C02)、OLED 显示屏等外设与 MCU 的交互,I2C 协议是实现这类需求的主流方案之一。它凭借 “仅需 2 根线、支持一主多从、自带仲裁机制” 的优势,广泛应用于消费电子、工业控制等领域。本文将从协议原理、关键概念、实战流程到代码实现,全方位解析 I2C 通信,帮助开发者快速掌握并落地应用。
1. 什么是 I2C 协议
I2C(Inter-Integrated Circuit,集成电路间总线)是由 Philips(现 NXP)提出的串行通信协议,核心是通过两根信号线实现多设备间的同步半双工通信。
1.1 I2C 协议核心特征
短距离通信:仅适用于板级或模块内设备(如 MCU 与板载传感器),不支持长距离传输(对比 RS232 的数米距离)。一主多从架构:1 个或多个 “主机”(如 STM32 MCU)可控制多个 “从机”(如传感器),从机通过唯一地址区分。半双工通信:数据可双向传输,但同一时刻仅能单向传输(对比 SPI 的全双工)。同步通信:依赖主机生成的时钟信号(SCL)实现收发同步(对比 UART 的异步通信)。极简硬件:仅需 2 根信号线(SCL+SDA),无需额外握手线。
1.2 I2C 硬件基础:SCL 与 SDA
I2C 的两根信号线功能固定,且均需外接上拉电阻(通常 4.7kΩ~10kΩ,具体需参考外设手册):
SCL(Serial Clock):时钟信号线,由主机生成,用于同步数据传输节奏(如时钟上升沿 / 下降沿采样数据)。SDA(Serial Data):数据输入 / 输出线,双向传输数据(主机→从机或从机→主机),空闲时由上拉电阻拉为高电平。
2. 关键术语解析
理解 I2C 前需先掌握 3 类核心通信术语:通信方向、同步 / 异步通信。
2.1 通信方向:单工、半双工、全双工对比
| 通信类型 | 数据传输方向 | 典型场景 | 优缺点 |
|---|---|---|---|
| 单工 | 仅单向(A→B) | 红外遥控、串口打印(仅 MCU 发数据) | 硬件简单,灵活性差 |
| 半双工 | 双向但不同时(A→B 或 B→A) | I2C、RS485 | 硬件简单,效率中等 |
| 全双工 | 双向且同时(A↔B) | SPI、UART(TX/RX 分离) | 效率高,需额外信号线 |
2.2 同步通信与异步通信对比
I2C 属于同步通信,其与异步通信(如 UART)的核心差异如下:
| 对比维度 | 同步通信(I2C/SPI) | 异步通信(UART/RS232) |
|---|---|---|
| 时钟依赖 | 主机提供统一时钟(SCL/SCK) | 收发双方独立时钟(需约定波特率) |
| 数据格式 | 连续比特流,无额外辅助位 | 每个字节带起始位(1 位)+ 停止位(1~2 位) |
| 效率 | 高(无冗余位) | 低(辅助位占比 10%~20%) |
| 复杂度 | 较高(需时钟同步) | 较低(仅需约定波特率) |
| 应用场景 | 板级高速通信(如传感器、存储) | 长距离低速通信(如 PC 串口调试) |
3. I2C 协议核心功能特点
除基础特征外,I2C 的以下功能是其广泛应用的关键:
无严格波特率要求:时钟由主机动态生成,可根据外设支持的速度调整(无需像 UART 那样严格匹配波特率)。软件寻址:每个从机有唯一地址(7 位或 10 位),主机通过发送地址选择通信对象,无需硬件片选线(对比 SPI 的 CS 线)。多主设备支持:总线可接入多个主机,通过仲裁机制避免冲突(如两个主机同时发数据时,确保仅一个主机占用总线)。完善应答机制:从机接收数据后需回复 ACK(应答),主机可通过 ACK 判断通信是否正常。
3.1 传输速度等级及应用场景
| 速度模式 | 速率 | 应用场景 |
|---|---|---|
| 标准模式(Standard) | 100 Kbps | 低速传感器(如温湿度传感器 SHT30) |
| 快速模式(Fast) | 400 Kbps | 中速设备(如 OLED 显示屏、EEPROM) |
| 高速模式(High-Speed) | 3.4 Mbps | 高速传输(如高速 ADC、摄像头模块) |
| 超快速模式(Ultra-Fast) | 5 Mbps | 极高速需求(如高性能处理器间通信) |
3.2 主从设备数量限制
主机数量:无 – 主机数量:无硬件限制(但需依赖仲裁机制避免冲突,实际开发中通常仅用 1 个主机)。从机数量:理论上 7 位地址支持 127 个从机(地址范围 0x00~0x7F,其中 0x00 为广播地址,不分配给单个从机);10 位地址支持 1024 个从机,但实际受总线负载(上拉电阻、电容)限制,通常建议不超过 10 个。
4. I2C 的高阻态:漏极开路与上拉电阻
I2C 的 SDA 和 SCL 均采用漏极开路(Open Drain) 设计,这是避免总线冲突的核心机制。
4.1 漏极开路(Open Drain)原理
漏极开路是一种输出结构,其特点:
仅能主动拉低信号线(输出低电平),无法主动输出高电平。若要输出高电平,需通过外部上拉电阻将信号线拉为高电平(空闲时 SDA/SCL 默认高电平)。总线具备 “线与” 逻辑:多个设备同时输出时,只要有 1 个设备拉低信号线,总线即为低电平;所有设备释放后,总线由上拉电阻拉为高电平。
4.2 高阻态在 I2C 中的作用
当 I2C 总线空闲或设备未参与当前通信时,设备会将 SDA/SCL 置于高阻态(相当于 “断开” 总线),此时:
空闲设备不影响总线电平(由上拉电阻维持高电平)。仅参与通信的主机和从机驱动总线,避免多设备同时驱动导致的信号冲突。
5. I2C 数据传输协议详解
I2C 的数据传输以 “帧” 为单位,完整帧结构包括:开始位→地址位→读写位→应答位→数据位→停止位。
5.1 传输帧结构总览(7 位地址示例)
plaintext
S (开始) → [7位从机地址] → [R/W位] → ACK (应答) → [8位数据] → ACK → ... → P (停止)
5.2 各字段解析
(1)开始位(S)
触发条件:主机主动生成,当 SCL 为高电平时,SDA 由高电平拉为低电平(“高→低” 跳变)。作用:通知所有从机 “通信即将开始”,从机唤醒并准备接收地址。
(2)地址位(7 位 / 10 位)
功能:主机指定目标从机,从机接收地址后与自身地址对比,若匹配则参与后续通信,否则继续保持高阻态。格式:7 位地址最常用(占 1 个字节的高 7 位,最低位为读写位);10 位地址需 2 个字节传输(适用于从机数量多的场景)。
(3)读写位(R/W)
位置:紧跟地址位(7 位地址的第 8 位,10 位地址的第 16 位)。含义:
:写操作(主机→从机传输数据,如向传感器发送配置指令)。
0:读操作(从机→主机传输数据,如读取传感器测量值)。
1
(4)应答位(ACK/NACK)
I2C 的核心容错机制,占 1 个 SCL 时钟周期:
ACK(应答):从机接收数据后,将 SDA 拉低(SCL 高电平时保持低),表示 “数据已接收成功”。NACK(非应答):SDA 保持高电平,表示 “数据接收失败” 或 “通信结束”。NACK 产生场景:
总线上无主机指定地址的从机(无设备应答)。从机忙(如传感器正在测量,无法接收数据)。主机接收从机数据时,主动发送 NACK(通知从机 “数据已接收完毕,无需继续发送”)。
(5)数据位
传输规则:8 位为 1 组,高位(MSB)先传;SCL 为高电平时,SDA 电平必须稳定(数据采样),SDA 仅能在 SCL 低电平时变化(避免采样错误)。数量:可连续传输多组数据(如向 EEPROM 写入多个字节),每组数据后需跟随 1 个 ACK/NACK。
(6)停止位(P)
触发条件:主机主动生成,当 SCL 为高电平时,SDA 由低电平拉为高电平(“低→高” 跳变)。作用:通知所有从机 “通信结束”,从机可恢复空闲状态。
5.3 写寄存器标准流程(7 位地址)
以 “主机向从机写入数据到指定寄存器” 为例,流程如下:
主机发送开始位(S),唤醒所有从机。主机发送 “7 位从机地址 + 1 位写操作(0)”,等待从机 ACK。从机地址匹配,回复ACK。主机发送寄存器地址(8 位,指定要写入的从机寄存器),等待从机 ACK。从机回复ACK。主机发送要写入的数据(8 位),等待从机 ACK。从机回复ACK(若需连续写多个寄存器,重复步骤 6~7)。主机发送停止位(P),通信结束。
5.4 读寄存器标准流程(7 位地址)
以 “主机从从机指定寄存器读取数据” 为例,流程如下(需先告知从机 “要读的寄存器地址”,再切换为读操作):
主机发送开始位(S)。主机发送 “7 位从机地址 + 1 位写操作(0)”,等待从机 ACK(此时为 “伪写”,目的是传递寄存器地址)。从机回复ACK。主机发送要读取的寄存器地址(8 位),等待从机 ACK。从机回复ACK。主机再次发送开始位(S)(重复开始位,切换通信方向)。主机发送 “7 位从机地址 + 1 位读操作(1)”,等待从机 ACK。从机回复ACK,并发送寄存器数据(8 位)。主机接收数据后,发送NACK(通知从机 “数据已接收,无需继续发送”)。主机发送停止位(P),通信结束。
6. I2C 多主设备仲裁机制
当多个主机同时尝试控制 I2C 总线时,需通过仲裁机制确保仅 1 个主机成功占用总线,且数据不丢失。I2C 仲裁分为两步:SCL 同步和 SDA 仲裁。
6.1 SCL 线同步(线 “与” 逻辑)
所有主机均会生成 SCL 时钟,总线最终的 SCL 电平由 “线与” 逻辑决定:
若某主机想将 SCL 拉低,总线 SCL 立即变为低电平,其他主机检测到 SCL 低后,会调整自身时钟,等待 SCL 恢复高电平。只有所有主机都释放 SCL(想拉高),总线 SCL 才会变为高电平。最终所有主机的 SCL 节奏完全同步,避免因时钟差异导致的数据采样错误。
6.2 SDA 线仲裁(低电平优先 + 回读机制)
SDA 仲裁是判断 “哪个主机获得总线控制权” 的核心,基于两个规则:
低电平优先:总线 SDA 电平为低时,优先认可发送低电平的主机。回读机制:每个主机发送 1 位数据后,会立即读取总线 SDA 电平,若与自身发送的电平一致,则继续发送;若不一致(自身发高,总线为低),则说明有其他主机发送低电平,立即退出仲裁(释放总线)。
6.3 仲裁实例分析
假设总线上有 3 个主机 A、B、C,同时发起通信并发送数据,仲裁过程如下:
第 1 个 SCL 周期:A、B、C 均发送 SDA=1 → 总线 SDA=1,所有主机回读一致,继续发送。第 2 个 SCL 周期:A、B、C 均发送 SDA=1 → 总线 SDA=1,继续发送。第 3 个 SCL 周期:A、B 发送 SDA=1,C 发送 SDA=0 → 总线 SDA=0(线与逻辑)。回读判断:A、B 回读 SDA=0(与自身发送的 1 不一致),立即退出仲裁(释放总线);C 回读 SDA=0(与自身发送一致),继续发送。结果:主机 C 获得总线控制权,可正常与从机通信。
7. I2C 死锁问题:原因与解决方案
I2C 开发中最常见的故障是死锁,表现为:SCL 保持高电平,SDA 保持低电平,主机与从机相互等待,无法恢复通信。
7.1 死锁产生场景
场景 1:写操作时死锁
主机发送 1 字节数据后,拉低 SCL(准备接收从机的 ACK)。从机接收数据后,拉低 SDA(发送 ACK),等待主机将 SCL 拉高→低(完成 ACK 周期)。若此时主机异常复位(如程序崩溃、 watchdog 复位),会释放 SCL(SCL 变为高电平),但未完成 ACK 周期。从机仍在等待 SCL 拉低以释放 SDA,主机复位后检测到 SDA = 低,认为总线被占用,等待 SDA = 高 —— 双方相互等待,死锁。
场景 2:读操作时死锁
从机向主机发送 1 字节数据后,拉低 SCL(等待主机的 ACK)。主机接收数据后,若发送 NACK(通知从机停止发送),需拉低 SDA 并等待 SCL 周期结束。若此时主机复位,释放 SCL 为高电平,从机仍拉低 SDA(等待 SCL 周期结束)—— 同样导致死锁。
7.2 死锁解决方法
(1)软件复位 I2C 总线(推荐)
原理:模拟 SCL 时钟脉冲,让从机完成未结束的 ACK/NACK 周期,释放 SDA。步骤:
主机复位后,先将 SDA 和 SCL 配置为推挽输出(临时)。拉低 SDA,然后生成 8~10 个 SCL 高→低脉冲(覆盖从机可能的等待周期)。最后发送 1 个停止位(SCL 高时,SDA 由低→高),总线恢复空闲状态。
(2)硬件复位从机
若从机支持硬件复位引脚(如部分传感器的 RST 引脚),可通过主机 GPIO 拉低 RST 引脚,强制从机复位,释放 SDA/SCL。
(3)总线监控与自动恢复
在主机程序中加入 “总线监控” 逻辑:定期检测 SDA/SCL 电平,若发现 SDA 长期(如 100ms)为低且 SCL 为高,则触发 “软件复位总线” 流程。
8. STM32 HAL 库 I2C 实战代码
以 STM32F103 为例,基于 HAL 库实现 I2C 主设备与从机(如 AT24C02 EEPROM)的通信,包含初始化、发送、接收、死锁恢复代码。
8.1 I2C 外设初始化(HAL_I2C_Init)
首先在 CubeMX 中配置 I2C(或手动编写初始化代码),关键参数:
从机地址:AT24C02 默认地址为 0xA0(7 位地址 0x50,左移 1 位后为 0xA0,最低位为读写位)。速度模式:快速模式(400 Kbps)。上拉电阻:启用 GPIO 内部上拉(或外接 4.7kΩ 电阻)。
c
#include "stm32f1xx_hal.h"
/* 定义I2C句柄 */
I2C_HandleTypeDef hi2c1;
/* I2C1初始化函数 */
void MX_I2C1_Init(void) {
hi2c1.Instance = I2C1;
hi2c1.Init.ClockSpeed = 400000; // 400 Kbps(快速模式)
hi2c1.Init.DutyCycle = I2C_DUTYCYCLE_2; // 占空比2(SCL高电平时间=低电平时间×2)
hi2c1.Init.OwnAddress1 = 0; // 主机无需设置自身地址
hi2c1.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT; // 7位地址模式
hi2c1.Init.DualAddressMode = I2C_DUALADDRESS_DISABLE;
hi2c1.Init.OwnAddress2 = 0;
hi2c1.Init.GeneralCallMode = I2C_GENERALCALL_DISABLE;
hi2c1.Init.NoStretchMode = I2C_NOSTRETCH_DISABLE; // 启用时钟拉伸(从机忙时可拉低SCL)
if (HAL_I2C_Init(&hi2c1) != HAL_OK) {
Error_Handler(); // 初始化失败,需处理(如死循环)
}
}
/* GPIO初始化(HAL库回调函数,由HAL_I2C_Init调用) */
void HAL_I2C_MspInit(I2C_HandleTypeDef* i2cHandle) {
GPIO_InitTypeDef GPIO_InitStruct = {0};
if(i2cHandle->Instance==I2C1) {
/* 使能I2C1和GPIOA时钟 */
__HAL_RCC_I2C1_CLK_ENABLE();
__HAL_RCC_GPIOA_CLK_ENABLE();
/* 配置PA9(SCL)和PA10(SDA)为开漏输出+上拉 */
GPIO_InitStruct.Pin = GPIO_PIN_9 | GPIO_PIN_10;
GPIO_InitStruct.Mode = GPIO_MODE_AF_OD; // 复用开漏输出
GPIO_InitStruct.Pull = GPIO_PULLUP; // 上拉电阻
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; // 高速
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
}
}
8.2 主设备发送数据(HAL_I2C_Master_Transmit)详解
函数功能:主机向从机发送指定长度的数据(阻塞模式,需等待通信完成或超时)。
c
/**
* @brief I2C主设备发送数据(阻塞模式)
* @param hi2c:I2C句柄指针(如&hi2c1)
* @param DevAddress:从机地址(7位地址需左移1位,最低位为读写位)
* @param pData:待发送数据缓冲区指针
* @param Size:待发送数据长度(字节数)
* @param Timeout:超时时间(ms,超过则返回错误)
* @retval HAL_StatusTypeDef:返回状态(HAL_OK=成功,HAL_ERROR=失败,HAL_TIMEOUT=超时)
*/
HAL_StatusTypeDef HAL_I2C_Master_Transmit(I2C_HandleTypeDef *hi2c,
uint16_t DevAddress,
uint8_t *pData,
uint16_t Size,
uint32_t Timeout) {
uint32_t tickstart = 0x00U;
/* 记录超时起始时间 */
tickstart = HAL_GetTick();
/* 检查I2C是否空闲 */
if(hi2c->State == HAL_I2C_STATE_READY) {
/* 等待总线忙标志复位(避免总线冲突) */
if(I2C_WaitOnFlagUntilTimeout(hi2c, I2C_FLAG_BUSY, SET, I2C_TIMEOUT_BUSY_FLAG, tickstart) != HAL_OK) {
return HAL_BUSY; // 总线忙
}
/* 锁定I2C外设(避免多线程冲突) */
__HAL_LOCK(hi2c);
/* 启用I2C外设(若未启用) */
if((hi2c->Instance->CR1 & I2C_CR1_PE) != I2C_CR1_PE) {
__HAL_I2C_ENABLE(hi2c);
}
/* 配置I2C状态和模式 */
hi2c->State = HAL_I2C_STATE_BUSY_TX; // 发送状态
hi2c->Mode = HAL_I2C_MODE_MASTER; // 主设备模式
hi2c->ErrorCode = HAL_I2C_ERROR_NONE; // 清除错误码
/* 设置传输参数 */
hi2c->pBuffPtr = pData; // 数据缓冲区
hi2c->XferCount = Size; // 剩余传输长度
hi2c->XferSize = Size; // 总传输长度
/* 发送从机地址(写操作) */
if(I2C_MasterRequestWrite(hi2c, DevAddress, Timeout, tickstart) != HAL_OK) {
__HAL_UNLOCK(hi2c);
return (hi2c->ErrorCode == HAL_I2C_ERROR_AF) ? HAL_ERROR : HAL_TIMEOUT;
}
/* 清除地址标志(ADDR) */
__HAL_I2C_CLEAR_ADDRFLAG(hi2c);
/* 循环发送数据,直到所有数据发送完成 */
while(hi2c->XferSize > 0U) {
/* 等待发送缓冲区为空(TXE标志置1) */
if(I2C_WaitOnTXEFlagUntilTimeout(hi2c, Timeout, tickstart) != HAL_OK) {
hi2c->Instance->CR1 |= I2C_CR1_STOP; // 发送停止位
__HAL_UNLOCK(hi2c);
return (hi2c->ErrorCode == HAL_I2C_ERROR_AF) ? HAL_ERROR : HAL_TIMEOUT;
}
/* 写入1字节数据到数据寄存器(DR) */
hi2c->Instance->DR = (*hi2c->pBuffPtr++);
hi2c->XferCount--;
hi2c->XferSize--;
/* 若支持连续发送(BTF标志置1),提前写入下一字节(提升效率) */
if((__HAL_I2C_GET_FLAG(hi2c, I2C_FLAG_BTF) == SET) && (hi2c->XferSize != 0U)) {
hi2c->Instance->DR = (*hi2c->pBuffPtr++);
hi2c->XferCount--;
hi2c->XferSize--;
}
/* 等待字节传输完成(BTF标志置1) */
if(I2C_WaitOnBTFFlagUntilTimeout(hi2c, Timeout, tickstart) != HAL_OK) {
hi2c->Instance->CR1 |= I2C_CR1_STOP; // 发送停止位
__HAL_UNLOCK(hi2c);
return (hi2c->ErrorCode == HAL_I2C_ERROR_AF) ? HAL_ERROR : HAL_TIMEOUT;
}
}
/* 发送停止位,结束通信 */
hi2c->Instance->CR1 |= I2C_CR1_STOP;
/* 恢复I2C状态 */
hi2c->State = HAL_I2C_STATE_READY;
hi2c->Mode = HAL_I2C_MODE_NONE;
/* 解锁I2C外设 */
__HAL_UNLOCK(hi2c);
return HAL_OK; // 发送成功
} else {
return HAL_BUSY; // I2C未空闲
}
}
8.3 主设备接收数据(HAL_I2C_Master_Receive)
函数功能:主机从从机接收指定长度的数据(阻塞模式)。
c
/**
* @brief I2C主设备接收数据(阻塞模式)
* @param hi2c:I2C句柄指针
* @param DevAddress:从机地址(7位地址左移1位,最低位为1(读操作))
* @param pData:接收数据缓冲区指针
* @param Size:待接收数据长度(字节数)
* @param Timeout:超时时间(ms)
* @retval HAL_StatusTypeDef:返回状态
*/
HAL_StatusTypeDef HAL_I2C_Master_Receive(I2C_HandleTypeDef *hi2c,
uint16_t DevAddress,
uint8_t *pData,
uint16_t Size,
uint32_t Timeout) {
uint32_t tickstart = 0x00U;
tickstart = HAL_GetTick();
if(hi2c->State == HAL_I2C_STATE_READY) {
/* 等待总线空闲 */
if(I2C_WaitOnFlagUntilTimeout(hi2c, I2C_FLAG_BUSY, SET, I2C_TIMEOUT_BUSY_FLAG, tickstart) != HAL_OK) {
return HAL_BUSY;
}
__HAL_LOCK(hi2c);
/* 启用I2C */
if((hi2c->Instance->CR1 & I2C_CR1_PE) != I2C_CR1_PE) {
__HAL_I2C_ENABLE(hi2c);
}
/* 配置接收模式 */
hi2c->State = HAL_I2C_STATE_BUSY_RX;
hi2c->Mode = HAL_I2C_MODE_MASTER;
hi2c->ErrorCode = HAL_I2C_ERROR_NONE;
hi2c->pBuffPtr = pData;
hi2c->XferCount = Size;
hi2c->XferSize = Size;
/* 发送从机地址(读操作) */
if(I2C_MasterRequestRead(hi2c, DevAddress, Timeout, tickstart) != HAL_OK) {
__HAL_UNLOCK(hi2c);
return (hi2c->ErrorCode == HAL_I2C_ERROR_AF) ? HAL_ERROR : HAL_TIMEOUT;
}
__HAL_I2C_CLEAR_ADDRFLAG(hi2c);
/* 若仅接收1字节,提前禁用ACK */
if(hi2c->XferSize == 1U) {
__HAL_I2C_DISABLE_ACK(hi2c);
}
/* 循环接收数据 */
while(hi2c->XferSize > 0U) {
/* 等待接收缓冲区非空(RXNE标志置1) */
if(I2C_WaitOnRXNEFlagUntilTimeout(hi2c, Timeout, tickstart) != HAL_OK) {
hi2c->Instance->CR1 |= I2C_CR1_STOP;
__HAL_UNLOCK(hi2c);
return (hi2c->ErrorCode == HAL_I2C_ERROR_AF) ? HAL_ERROR : HAL_TIMEOUT;
}
/* 从数据寄存器读取1字节 */
(*hi2c->pBuffPtr++) = hi2c->Instance->DR;
hi2c->XferCount--;
hi2c->XferSize--;
/* 接收最后1字节前,禁用ACK并准备发送停止位 */
if(hi2c->XferSize == 1U) {
__HAL_I2C_DISABLE_ACK(hi2c);
hi2c->Instance->CR1 |= I2C_CR1_STOP;
}
}
/* 恢复ACK使能(为下次通信准备) */
__HAL_I2C_ENABLE_ACK(hi2c);
/* 恢复I2C状态 */
hi2c->State = HAL_I2C_STATE_READY;
hi2c->Mode = HAL_I2C_MODE_NONE;
__HAL_UNLOCK(hi2c);
return HAL_OK;
} else {
return HAL_BUSY;
}
}
8.4 软件解决死锁代码示例
c
/**
* @brief 软件复位I2C总线(解决死锁)
* @param SCL_Pin:SCL引脚(如GPIO_PIN_9)
* @param SDA_Pin:SDA引脚(如GPIO_PIN_10)
* @param GPIOx:GPIO端口(如GPIOA)
*/
void I2C_Software_Reset(GPIO_TypeDef* GPIOx, uint16_t SCL_Pin, uint16_t SDA_Pin) {
GPIO_InitTypeDef GPIO_InitStruct = {0};
/* 1. 配置SCL和SDA为推挽输出(临时) */
GPIO_InitStruct.Pin = SCL_Pin | SDA_Pin;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; // 推挽输出
GPIO_InitStruct.Pull = GPIO_PULLUP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOx, &GPIO_InitStruct);
/* 2. 拉低SDA,生成10个SCL脉冲(覆盖从机等待周期) */
HAL_GPIO_WritePin(GPIOx, SDA_Pin, GPIO_PIN_RESET); // SDA=低
for(uint8_t i=0; i<10; i++) {
HAL_GPIO_WritePin(GPIOx, SCL_Pin, GPIO_PIN_SET); // SCL=高
HAL_Delay(1);
HAL_GPIO_WritePin(GPIOx, SCL_Pin, GPIO_PIN_RESET); // SCL=低
HAL_Delay(1);
}
/* 3. 发送停止位(SCL高时,SDA由低→高) */
HAL_GPIO_WritePin(GPIOx, SCL_Pin, GPIO_PIN_SET); // SCL=高
HAL_Delay(1);
HAL_GPIO_WritePin(GPIOx, SDA_Pin, GPIO_PIN_SET); // SDA=高
HAL_Delay(1);
/* 4. 恢复SCL和SDA为I2C复用开漏模式 */
GPIO_InitStruct.Mode = GPIO_MODE_AF_OD; // 复用开漏
HAL_GPIO_Init(GPIOx, &GPIO_InitStruct);
}
// 使用示例:主机复位后,先检测总线是否死锁,若死锁则复位
if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_10) == GPIO_PIN_RESET &&
HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_9) == GPIO_PIN_SET) {
I2C_Software_Reset(GPIOA, GPIO_PIN_9, GPIO_PIN_10); // 解决死锁
}
9. 常见问题排查(FAQ)
Q:I2C 通信无 ACK,从机无响应怎么办?A:① 检查从机地址是否正确(7 位地址需左移 1 位);② 测量 SCL/SDA 是否接反;③ 检查上拉电阻是否焊接(建议 4.7kΩ);④ 用示波器观察 SCL 是否有正常时钟脉冲。
Q:数据传输错误(接收数据与发送数据不一致)?A:① 检查 SDA 在 SCL 高电平时是否稳定(避免 SDA 跳变);② 降低传输速度(如从 400Kbps 改为 100Kbps);③ 检查总线负载(从机数量不超过 10 个,避免电容过大)。
Q:I2C 偶尔通信失败,报 HAL_TIMEOUT?A:① 增大 Timeout 参数(尤其低速外设,建议设置为 100ms 以上);② 检查从机是否存在 “忙状态”(如传感器测量时无法响应,需等待从机就绪);③ 排查电源稳定性(I2C 设备供电不足会导致应答延迟)。
总结
I2C 协议是嵌入式开发中不可或缺的通信技术,其核心优势在于 “极简硬件、灵活的主从架构、完善的容错机制”。掌握本文的协议原理(传输帧结构、仲裁机制)、死锁解决方法及 HAL 库代码,可轻松应对绝大多数 I2C 外设的开发需求(如传感器、存储芯片、显示屏)。实际开发中,建议结合示波器观察 SCL/SDA 波形,快速定位通信问题,提升调试效率。
















暂无评论内容