I2C死锁解决与协议全解析

嵌入式 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 波形,快速定位通信问题,提升调试效率。

© 版权声明
THE END
如果内容对您有所帮助,就支持一下吧!
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容