STM32 I2C 通讯原理与三种实现模式详解

一、I2C 通讯基础原理

1.1 物理层特性

  • 总线结构:两根双向线 SDA (串行数据线) 和 SCL (串行时钟线)
  • 电气特性:开漏输出 + 外部上拉电阻 (通常 4.7kΩ),线与逻辑
  • 传输速率:标准模式 (100kbps)、快速模式 (400kbps)、高速模式 (3.4Mbps)
  • 多主架构:多个设备可同时作为主机,通过总线仲裁避免冲突

1.2 协议层核心时序

  • 起始条件 (S):SCL 高电平时,SDA 由高变低
  • 停止条件 (P):SCL 高电平时,SDA 由低变高
  • 数据传输:SCL 低电平时改变 SDA,SCL 高电平时采样 SDA
  • 应答信号 (ACK/NACK):第 9 个时钟周期,接收方拉低 SDA 为 ACK,保持高为 NACK
  • 寻址格式:7 位从地址 + 1 位读写位 (0 = 写,1 = 读)

二、STM32 I2C 外设架构与核心寄存器

2.1 外设核心模块

STM32 I2C 外设包含一个硬件状态机,自动处理大部分协议时序,无需软件模拟每个时钟周期。

2.2 关键寄存器详解

寄存器 主要位域 功能说明
CR1 START, STOP, ACK, PE, SWRST 控制 I2C 的启动、停止、应答使能、外设使能和软件复位
CR2 ITEVTEN, ITERREN, DMAEN, FREQ 中断使能、DMA 使能和时钟频率配置
SR1 SB, ADDR, TXE, RXNE, BTF, STOPF, AF 事件状态标志 (起始位检测、地址匹配、发送空、接收非空等)
SR2 MSL, BUSY, TRA 主模式标志、总线忙标志、传输方向标志
DR DR7:0 数据寄存器,用于发送和接收数据
CCR CCR11:0, F/S 时钟控制寄存器,配置 SCL 时钟频率
OAR1 ADD7:1, ADDMODE 自身从地址寄存器

三、阻塞模式 (轮询模式)

核心思想:CPU 不断查询状态寄存器标志位,等待事件发生后再执行下一步操作。

3.1 主发送流程 (写操作)

步骤 操作 寄存器变化 等待的状态标志 状态说明
1 使能 I2C 外设 CR1.PE=1 - 外设进入待机状态
2 产生起始条件 CR1.START=1 SR1.SB=1 起始条件已发送,总线被占用
3 发送从地址 + 写位 DR= (slave_addr<<1) 0 地址字节写入数据寄存器
4 等待地址匹配 - SR1.ADDR=1 从设备应答地址
5 清除 ADDR 标志 读 SR1,读 SR2 - 进入主发送数据状态
6 发送第一个数据字节 DR=data0 SR1.TXE=1 数据寄存器空,可写入下一个字节
7 循环发送剩余数据 DR=datai SR1.TXE=1 直到所有数据发送完毕
8 等待最后一个字节传输完成 - SR1.BTF=1 字节传输完成,数据寄存器和移位寄存器都为空
9 产生停止条件 CR1.STOP=1 - 停止条件发送,总线释放

3.2 主接收流程 (读操作)

步骤 操作 寄存器变化 等待的状态标志 状态说明
1 使能 I2C 外设 CR1.PE=1 - 外设进入待机状态
2 产生起始条件 CR1.START=1 SR1.SB=1 起始条件已发送,总线被占用
3 发送从地址 + 读位 DR= (slave_addr<<1) 1 地址字节写入数据寄存器
4 等待地址匹配 - SR1.ADDR=1 从设备应答地址
5 清除 ADDR 标志 读 SR1,读 SR2 - 进入主接收数据状态
6 接收前 N-1 个字节 - SR1.RXNE=1 数据寄存器非空,读取 DR
7 关闭 ACK 应答 CR1.ACK=0 - 准备接收最后一个字节
8 产生停止条件 CR1.STOP=1 - 提前发送停止条件
9 读取最后一个字节 dataN-1=DR SR1.RXNE=1 最后一个字节接收完成

3.3 优缺点

  • 优点:逻辑简单直观,易于理解和调试,代码量小
  • 缺点:CPU 利用率极低,长时间阻塞等待,不适合实时性要求高的系统

四、中断模式

核心思想:当 I2C 外设发生特定事件时,触发 CPU 中断,在中断服务函数中处理后续操作。

4.1 所有相关中断详解

STM32 I2C 中断分为事件中断错误中断两大类,通过 CR2 寄存器的对应位使能:

中断类型 中断标志 触发条件 清除方法
事件中断 SB 起始条件已发送 读 SR1,然后写 DR
ADDR 地址已发送并被应答 读 SR1,然后读 SR2
TXE 数据寄存器空 写 DR
RXNE 数据寄存器非空 读 DR
BTF 字节传输完成 读 SR1,然后读 / 写 DR
STOPF 停止条件已检测到 读 SR1,然后写 CR1
错误中断 AF 应答失败 写 0 清除 SR1.AF
OVR 溢出 / 下溢错误 读 SR1,然后读 / 写 DR
ARLO 仲裁丢失 写 0 清除 SR1.ARLO
BERR 总线错误 写 0 清除 SR1.BERR
PECERR PEC 校验错误 写 0 清除 SR1.PECERR
TIMEOUT 超时错误 写 0 清除 SR1.TIMEOUT

4.2 主发送中断流程

  1. 初始化阶段

    • 配置 I2C 外设参数 (时钟频率、地址等)
    • 使能 I2C 事件中断和错误中断 (CR2.ITEVTEN=1, CR2.ITERREN=1)
    • 配置 NVIC 中断优先级,使能 I2C 全局中断
  2. 启动传输

    • 设置传输状态为 "发送中"
    • 产生起始条件 (CR1.START=1)
  3. 中断处理流程

    • SB 中断:发送从地址 + 写位 (DR=slave_addr<<1)
    • ADDR 中断:读 SR1 和 SR2 清除标志,发送第一个数据字节
    • TXE 中断:如果还有数据未发送,发送下一个字节;否则关闭 TXE 中断,等待 BTF 中断
    • BTF 中断:所有数据发送完毕,产生停止条件 (CR1.STOP=1),设置传输状态为 "完成"
    • 错误中断:处理各种错误,产生停止条件,设置传输状态为 "失败"

4.3 主接收中断流程

  1. 初始化阶段:同主发送

  2. 启动传输

    • 设置传输状态为 "接收中"
    • 产生起始条件 (CR1.START=1)
  3. 中断处理流程

    • SB 中断:发送从地址 + 读位 (DR=(slave_addr<<1)|1)
    • ADDR 中断
      • 读 SR1 和 SR2 清除标志
      • 如果只接收 1 个字节:立即关闭 ACK (CR1.ACK=0),产生停止条件
      • 如果接收多个字节:保持 ACK 开启
    • RXNE 中断
      • 读取 DR 寄存器,存入缓冲区
      • 如果是倒数第二个字节:关闭 ACK (CR1.ACK=0)
      • 如果是最后一个字节:产生停止条件,设置传输状态为 "完成"
    • 错误中断:同主发送

4.4 优缺点

  • 优点:CPU 利用率高,非阻塞,适合多任务系统
  • 缺点:中断处理函数逻辑复杂,需要维护状态机,容易出现时序问题

五、DMA 模式

核心思想:利用 DMA 控制器自动完成 I2C 数据寄存器与内存之间的数据传输,CPU 仅在传输开始和结束时参与。

5.1 DMA 通道映射 (STM32F1 系列)

I2C 外设 发送通道 接收通道
I2C1 DMA1 Channel6 DMA1 Channel7
I2C2 DMA1 Channel4 DMA1 Channel5

5.2 主发送 DMA 流程

步骤 操作 寄存器变化 状态说明
1 初始化 I2C 和 DMA I2C.CR2.DMAEN=1DMA.CCR.MINC=1DMA.CCR.DIR=1 配置 DMA 为内存到外设模式,内存地址自增
2 配置 DMA 发送参数 DMA.CMAR=buffer_addrDMA.CPAR=&I2C.DRDMA.CNDTR=data_len 设置源地址、目的地址和传输长度
3 使能 DMA 传输完成中断 DMA.CCR.TCIE=1 传输完成时触发 DMA 中断
4 使能 DMA 通道 DMA.CCR.EN=1 DMA 准备就绪,等待 I2C 请求
5 产生起始条件 I2C.CR1.START=1 启动 I2C 传输
6 等待 SB 事件 轮询 SR1.SB=1 起始条件已发送
7 发送从地址 + 写位 I2C.DR=(slave_addr<<1) 地址字节写入数据寄存器
8 等待 ADDR 事件 轮询 SR1.ADDR=1 从设备应答地址
9 清除 ADDR 标志 读 SR1,读 SR2 进入主发送数据状态
10 等待 DMA 传输完成 等待 DMA 中断 DMA 自动完成所有数据传输
11 等待 BTF 事件 轮询 SR1.BTF=1 最后一个字节传输完成
12 产生停止条件 I2C.CR1.STOP=1 停止条件发送,总线释放

5.3 主接收 DMA 流程

步骤 操作 寄存器变化 状态说明
1 初始化 I2C 和 DMA I2C.CR2.DMAEN=1DMA.CCR.MINC=1DMA.CCR.DIR=0 配置 DMA 为外设到内存模式,内存地址自增
2 配置 DMA 接收参数 DMA.CMAR=buffer_addrDMA.CPAR=&I2C.DRDMA.CNDTR=data_len 设置目的地址、源地址和传输长度
3 使能 DMA 传输完成中断 DMA.CCR.TCIE=1 传输完成时触发 DMA 中断
4 使能 DMA 通道 DMA.CCR.EN=1 DMA 准备就绪,等待 I2C 请求
5 产生起始条件 I2C.CR1.START=1 启动 I2C 传输
6 等待 SB 事件 轮询 SR1.SB=1 起始条件已发送
7 发送从地址 + 读位 I2C.DR=(slave_addr<<1) 1 地址字节写入数据寄存器
8 等待 ADDR 事件 轮询 SR1.ADDR=1 从设备应答地址
9 清除 ADDR 标志 读 SR1,读 SR2 进入主接收数据状态
10 等待 DMA 传输完成 等待 DMA 中断 DMA 自动完成前 N-1 个字节传输
11 关闭 ACK 应答 I2C.CR1.ACK=0 准备接收最后一个字节
12 产生停止条件 I2C.CR1.STOP=1 提前发送停止条件
13 读取最后一个字节 bufferdata_len-1=I2C.DR 最后一个字节手动读取

5.4 优缺点

  • 优点:CPU 利用率最高,适合大数据量传输,传输过程中 CPU 可执行其他任务
  • 缺点:配置最复杂,需要处理 DMA 和 I2C 的同步问题,最后一个字节需要特殊处理

六、三种模式对比与选型建议

对比项 阻塞模式 中断模式 DMA 模式
CPU 利用率 极低 最高
代码复杂度 中高
调试难度 中高
适合数据量 小 (<10 字节) 中 (10-100 字节) 大 (>100 字节)
实时性 最好
适用场景 简单应用,初始化配置 一般应用,多任务系统 高速数据采集,传感器阵列

七、常见问题与解决方案

  1. 总线死锁:SDA 或 SCL 被持续拉低

    • 解决方案:软件复位 I2C 外设 (CR1.SWRST=1),然后重新初始化
    • 终极方案:模拟 I2C 时序发送 9 个时钟脉冲,强制释放总线
  2. 应答失败 (AF 错误)

    • 检查从设备地址是否正确
    • 检查硬件连接和上拉电阻
    • 确认从设备是否正常工作
  3. DMA 接收最后一个字节丢失

    • 原因:DMA 传输完成后,最后一个字节还在 I2C 移位寄存器中
    • 解决方案:在 DMA 传输完成中断中,手动读取最后一个字节
  4. 仲裁丢失 (ARLO 错误)

    • 原因:多个主机同时访问总线
    • 解决方案:实现总线仲裁机制,或使用单主架构

STM32 HAL 库 I2C 三种模式完整代码

一、通用头文件与宏定义

cpp 复制代码
#include "stm32f1xx_hal.h"

// I2C外设选择
#define I2Cx                I2C1
#define I2Cx_SCL_PIN        GPIO_PIN_6
#define I2Cx_SDA_PIN        GPIO_PIN_7
#define I2Cx_GPIO_PORT      GPIOB
#define I2Cx_GPIO_CLK_ENABLE() __HAL_RCC_GPIOB_CLK_ENABLE()
#define I2Cx_CLK_ENABLE()   __HAL_RCC_I2C1_CLK_ENABLE()

// DMA通道映射(I2C1)
#define I2Cx_TX_DMA_CHANNEL DMA1_Channel6
#define I2Cx_RX_DMA_CHANNEL DMA1_Channel7
#define I2Cx_TX_DMA_IRQ     DMA1_Channel6_IRQn
#define I2Cx_RX_DMA_IRQ     DMA1_Channel7_IRQn
#define DMAx_CLK_ENABLE()   __HAL_RCC_DMA1_CLK_ENABLE()

// 通用配置
#define I2C_SPEED           100000  // 100kHz标准模式
#define I2C_OWN_ADDRESS     0x00    // 主机模式下自身地址无意义
#define I2C_TIMEOUT         100     // 超时时间(ms)

// 错误码定义
#define I2C_OK              0
#define I2C_ERROR_TIMEOUT   1
#define I2C_ERROR_ACK       2
#define I2C_ERROR_BUSY      3
#define I2C_ERROR_ARLO      4
#define I2C_ERROR_BERR      5
#define I2C_ERROR_OVR       6

二、I2C 句柄与全局变量

cpp 复制代码
I2C_HandleTypeDef hi2c1;
DMA_HandleTypeDef hdma_i2c1_tx;
DMA_HandleTypeDef hdma_i2c1_rx;

// 中断和DMA模式传输完成标志
static uint8_t i2c_tx_done = 0;
static uint8_t i2c_rx_done = 0;
static uint8_t i2c_error = 0;

三、I2C 通用初始化函数

所有模式都需要先初始化 I2C 外设和 GPIO 引脚:

cpp 复制代码
/**
 * @brief  I2C GPIO和外设基础初始化
 * @param  None
 * @retval None
 */
void I2C_CommonInit(void)
{
    GPIO_InitTypeDef GPIO_InitStruct = {0};
    
    // 使能时钟
    I2Cx_GPIO_CLK_ENABLE();
    I2Cx_CLK_ENABLE();
    
    // 配置SCL和SDA为开漏复用输出
    GPIO_InitStruct.Pin = I2Cx_SCL_PIN | I2Cx_SDA_PIN;
    GPIO_InitStruct.Mode = GPIO_MODE_AF_OD;
    GPIO_InitStruct.Pull = GPIO_PULLUP;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
    HAL_GPIO_Init(I2Cx_GPIO_PORT, &GPIO_InitStruct);
    
    // 配置I2C外设
    hi2c1.Instance = I2Cx;
    hi2c1.Init.ClockSpeed = I2C_SPEED;
    hi2c1.Init.DutyCycle = I2C_DUTYCYCLE_2;
    hi2c1.Init.OwnAddress1 = I2C_OWN_ADDRESS;
    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();
    }
}

/**
 * @brief  错误处理函数
 * @param  None
 * @retval None
 */
void Error_Handler(void)
{
    // 在这里添加错误处理代码,例如点亮LED
    while (1) {
        // 闪烁LED指示错误
        HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13);
        HAL_Delay(500);
    }
}

四、阻塞模式 (轮询模式)

4.1 工作步骤

  1. 初始化 :调用I2C_CommonInit()配置 GPIO 和 I2C 外设
  2. 发送流程
    • 调用HAL_I2C_Master_Transmit()函数
    • 函数内部自动处理:产生起始条件→发送从地址→发送数据→产生停止条件
    • 等待函数返回,检查返回值判断是否成功
  3. 接收流程
    • 调用HAL_I2C_Master_Receive()函数
    • 函数内部自动处理:产生起始条件→发送从地址→接收数据→产生停止条件
    • 等待函数返回,检查返回值判断是否成功

4.2 完整代码

cpp 复制代码
/**
 * @brief  I2C阻塞模式发送数据
 * @param  slave_addr: 从设备7位地址
 * @param  data: 发送数据缓冲区
 * @param  len: 发送数据长度
 * @retval 错误码
 */
uint8_t I2C_Blocking_Write(uint8_t slave_addr, uint8_t* data, uint16_t len)
{
    HAL_StatusTypeDef status;
    
    // 检查总线是否忙
    if (HAL_I2C_GetState(&hi2c1) != HAL_I2C_STATE_READY) {
        return I2C_ERROR_BUSY;
    }
    
    // 发送数据
    status = HAL_I2C_Master_Transmit(&hi2c1, slave_addr << 1, data, len, I2C_TIMEOUT);
    
    // 转换HAL错误码为自定义错误码
    switch (status) {
        case HAL_OK:
            return I2C_OK;
        case HAL_TIMEOUT:
            return I2C_ERROR_TIMEOUT;
        case HAL_ERROR:
            if (hi2c1.ErrorCode & HAL_I2C_ERROR_AF) {
                return I2C_ERROR_ACK;
            } else if (hi2c1.ErrorCode & HAL_I2C_ERROR_ARLO) {
                return I2C_ERROR_ARLO;
            } else if (hi2c1.ErrorCode & HAL_I2C_ERROR_BERR) {
                return I2C_ERROR_BERR;
            } else if (hi2c1.ErrorCode & HAL_I2C_ERROR_OVR) {
                return I2C_ERROR_OVR;
            } else {
                return I2C_ERROR_BUSY;
            }
        default:
            return I2C_ERROR_BUSY;
    }
}

/**
 * @brief  I2C阻塞模式接收数据
 * @param  slave_addr: 从设备7位地址
 * @param  data: 接收数据缓冲区
 * @param  len: 接收数据长度
 * @retval 错误码
 */
uint8_t I2C_Blocking_Read(uint8_t slave_addr, uint8_t* data, uint16_t len)
{
    HAL_StatusTypeDef status;
    
    // 检查总线是否忙
    if (HAL_I2C_GetState(&hi2c1) != HAL_I2C_STATE_READY) {
        return I2C_ERROR_BUSY;
    }
    
    // 接收数据
    status = HAL_I2C_Master_Receive(&hi2c1, slave_addr << 1, data, len, I2C_TIMEOUT);
    
    // 转换HAL错误码为自定义错误码
    switch (status) {
        case HAL_OK:
            return I2C_OK;
        case HAL_TIMEOUT:
            return I2C_ERROR_TIMEOUT;
        case HAL_ERROR:
            if (hi2c1.ErrorCode & HAL_I2C_ERROR_AF) {
                return I2C_ERROR_ACK;
            } else if (hi2c1.ErrorCode & HAL_I2C_ERROR_ARLO) {
                return I2C_ERROR_ARLO;
            } else if (hi2c1.ErrorCode & HAL_I2C_ERROR_BERR) {
                return I2C_ERROR_BERR;
            } else if (hi2c1.ErrorCode & HAL_I2C_ERROR_OVR) {
                return I2C_ERROR_OVR;
            } else {
                return I2C_ERROR_BUSY;
            }
        default:
            return I2C_ERROR_BUSY;
    }
}

五、中断模式

5.1 工作步骤

  1. 初始化
    • 调用I2C_CommonInit()配置 GPIO 和 I2C 外设
    • 配置 NVIC 中断优先级,使能 I2C 事件中断和错误中断
  2. 启动传输
    • 重置传输完成标志和错误标志
    • 调用HAL_I2C_Master_Transmit_IT()HAL_I2C_Master_Receive_IT()函数
  3. 中断处理
    • HAL 库自动处理 I2C 中断,无需手动编写中断服务函数
    • 传输完成后自动调用HAL_I2C_TxCpltCallback()HAL_I2C_RxCpltCallback()
    • 发生错误时自动调用HAL_I2C_ErrorCallback()
  4. 主程序:轮询传输完成标志,等待传输结束

5.2 完整代码

cpp 复制代码
/**
 * @brief  I2C中断模式初始化
 * @param  None
 * @retval None
 */
void I2C_Interrupt_Init(void)
{
    // 通用初始化
    I2C_CommonInit();
    
    // 配置I2C中断优先级
    HAL_NVIC_SetPriority(I2C1_EV_IRQn, 1, 0);
    HAL_NVIC_EnableIRQ(I2C1_EV_IRQn);
    
    HAL_NVIC_SetPriority(I2C1_ER_IRQn, 0, 0); // 错误中断优先级更高
    HAL_NVIC_EnableIRQ(I2C1_ER_IRQn);
}

/**
 * @brief  启动I2C中断模式发送
 * @param  slave_addr: 从设备7位地址
 * @param  data: 发送数据缓冲区
 * @param  len: 发送数据长度
 * @retval 错误码
 */
uint8_t I2C_Interrupt_StartWrite(uint8_t slave_addr, uint8_t* data, uint16_t len)
{
    HAL_StatusTypeDef status;
    
    // 检查总线是否忙
    if (HAL_I2C_GetState(&hi2c1) != HAL_I2C_STATE_READY) {
        return I2C_ERROR_BUSY;
    }
    
    // 重置标志
    i2c_tx_done = 0;
    i2c_error = I2C_OK;
    
    // 启动中断发送
    status = HAL_I2C_Master_Transmit_IT(&hi2c1, slave_addr << 1, data, len);
    
    if (status != HAL_OK) {
        return I2C_ERROR_BUSY;
    }
    
    return I2C_OK;
}

/**
 * @brief  启动I2C中断模式接收
 * @param  slave_addr: 从设备7位地址
 * @param  data: 接收数据缓冲区
 * @param  len: 接收数据长度
 * @retval 错误码
 */
uint8_t I2C_Interrupt_StartRead(uint8_t slave_addr, uint8_t* data, uint16_t len)
{
    HAL_StatusTypeDef status;
    
    // 检查总线是否忙
    if (HAL_I2C_GetState(&hi2c1) != HAL_I2C_STATE_READY) {
        return I2C_ERROR_BUSY;
    }
    
    // 重置标志
    i2c_rx_done = 0;
    i2c_error = I2C_OK;
    
    // 启动中断接收
    status = HAL_I2C_Master_Receive_IT(&hi2c1, slave_addr << 1, data, len);
    
    if (status != HAL_OK) {
        return I2C_ERROR_BUSY;
    }
    
    return I2C_OK;
}

/**
 * @brief  检查I2C中断传输是否完成
 * @param  None
 * @retval 1=完成,0=进行中
 */
uint8_t I2C_Interrupt_IsTxDone(void)
{
    return i2c_tx_done;
}

uint8_t I2C_Interrupt_IsRxDone(void)
{
    return i2c_rx_done;
}

/**
 * @brief  获取I2C中断传输错误码
 * @param  None
 * @retval 错误码
 */
uint8_t I2C_Interrupt_GetError(void)
{
    return i2c_error;
}

/**
 * @brief  I2C发送完成回调函数
 */
void HAL_I2C_MasterTxCpltCallback(I2C_HandleTypeDef *hi2c)
{
    if (hi2c->Instance == I2Cx) {
        i2c_tx_done = 1;
    }
}

/**
 * @brief  I2C接收完成回调函数
 */
void HAL_I2C_MasterRxCpltCallback(I2C_HandleTypeDef *hi2c)
{
    if (hi2c->Instance == I2Cx) {
        i2c_rx_done = 1;
    }
}

/**
 * @brief  I2C错误回调函数
 */
void HAL_I2C_ErrorCallback(I2C_HandleTypeDef *hi2c)
{
    if (hi2c->Instance == I2Cx) {
        if (hi2c->ErrorCode & HAL_I2C_ERROR_AF) {
            i2c_error = I2C_ERROR_ACK;
        } else if (hi2c->ErrorCode & HAL_I2C_ERROR_ARLO) {
            i2c_error = I2C_ERROR_ARLO;
        } else if (hi2c->ErrorCode & HAL_I2C_ERROR_BERR) {
            i2c_error = I2C_ERROR_BERR;
        } else if (hi2c->ErrorCode & HAL_I2C_ERROR_OVR) {
            i2c_error = I2C_ERROR_OVR;
        } else if (hi2c->ErrorCode & HAL_I2C_ERROR_TIMEOUT) {
            i2c_error = I2C_ERROR_TIMEOUT;
        } else {
            i2c_error = I2C_ERROR_BUSY;
        }
        
        // 标记传输完成(失败)
        i2c_tx_done = 1;
        i2c_rx_done = 1;
    }
}

// I2C事件中断服务函数(HAL库已实现,只需声明)
void I2C1_EV_IRQHandler(void)
{
    HAL_I2C_EV_IRQHandler(&hi2c1);
}

// I2C错误中断服务函数(HAL库已实现,只需声明)
void I2C1_ER_IRQHandler(void)
{
    HAL_I2C_ER_IRQHandler(&hi2c1);
}

六、DMA 模式

6.1 工作步骤

  1. 初始化
    • 调用I2C_CommonInit()配置 GPIO 和 I2C 外设
    • 配置 DMA 通道和中断
    • 将 DMA 句柄关联到 I2C 句柄
  2. 启动传输
    • 重置传输完成标志和错误标志
    • 调用HAL_I2C_Master_Transmit_DMA()HAL_I2C_Master_Receive_DMA()函数
  3. DMA 传输
    • DMA 控制器自动完成数据寄存器与内存之间的数据传输
    • 传输完成后触发 DMA 中断
    • HAL 库自动处理 DMA 中断,调用相应的回调函数
  4. 主程序:轮询传输完成标志,等待传输结束

6.2 完整代码

cpp 复制代码
/**
 * @brief  I2C DMA模式初始化
 * @param  None
 * @retval None
 */
void I2C_DMA_Init(void)
{
    // 通用初始化
    I2C_CommonInit();
    
    // 使能DMA时钟
    DMAx_CLK_ENABLE();
    
    // 配置I2C TX DMA通道
    hdma_i2c1_tx.Instance = I2Cx_TX_DMA_CHANNEL;
    hdma_i2c1_tx.Init.Direction = DMA_MEMORY_TO_PERIPH;
    hdma_i2c1_tx.Init.PeriphInc = DMA_PINC_DISABLE;
    hdma_i2c1_tx.Init.MemInc = DMA_MINC_ENABLE;
    hdma_i2c1_tx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
    hdma_i2c1_tx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
    hdma_i2c1_tx.Init.Mode = DMA_NORMAL;
    hdma_i2c1_tx.Init.Priority = DMA_PRIORITY_HIGH;
    
    if (HAL_DMA_Init(&hdma_i2c1_tx) != HAL_OK) {
        Error_Handler();
    }
    
    // 关联DMA句柄到I2C句柄
    __HAL_LINKDMA(&hi2c1, hdmatx, hdma_i2c1_tx);
    
    // 配置I2C RX DMA通道
    hdma_i2c1_rx.Instance = I2Cx_RX_DMA_CHANNEL;
    hdma_i2c1_rx.Init.Direction = DMA_PERIPH_TO_MEMORY;
    hdma_i2c1_rx.Init.PeriphInc = DMA_PINC_DISABLE;
    hdma_i2c1_rx.Init.MemInc = DMA_MINC_ENABLE;
    hdma_i2c1_rx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
    hdma_i2c1_rx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
    hdma_i2c1_rx.Init.Mode = DMA_NORMAL;
    hdma_i2c1_rx.Init.Priority = DMA_PRIORITY_HIGH;
    
    if (HAL_DMA_Init(&hdma_i2c1_rx) != HAL_OK) {
        Error_Handler();
    }
    
    // 关联DMA句柄到I2C句柄
    __HAL_LINKDMA(&hi2c1, hdmarx, hdma_i2c1_rx);
    
    // 配置DMA中断优先级
    HAL_NVIC_SetPriority(I2Cx_TX_DMA_IRQ, 1, 0);
    HAL_NVIC_EnableIRQ(I2Cx_TX_DMA_IRQ);
    
    HAL_NVIC_SetPriority(I2Cx_RX_DMA_IRQ, 1, 0);
    HAL_NVIC_EnableIRQ(I2Cx_RX_DMA_IRQ);
    
    // 配置I2C中断优先级(错误中断)
    HAL_NVIC_SetPriority(I2C1_ER_IRQn, 0, 0);
    HAL_NVIC_EnableIRQ(I2C1_ER_IRQn);
}

/**
 * @brief  I2C DMA模式发送数据
 * @param  slave_addr: 从设备7位地址
 * @param  data: 发送数据缓冲区
 * @param  len: 发送数据长度
 * @retval 错误码
 */
uint8_t I2C_DMA_Write(uint8_t slave_addr, uint8_t* data, uint16_t len)
{
    HAL_StatusTypeDef status;
    uint32_t timeout;
    
    // 检查总线是否忙
    if (HAL_I2C_GetState(&hi2c1) != HAL_I2C_STATE_READY) {
        return I2C_ERROR_BUSY;
    }
    
    // 重置标志
    i2c_tx_done = 0;
    i2c_error = I2C_OK;
    
    // 启动DMA发送
    status = HAL_I2C_Master_Transmit_DMA(&hi2c1, slave_addr << 1, data, len);
    
    if (status != HAL_OK) {
        return I2C_ERROR_BUSY;
    }
    
    // 等待传输完成
    timeout = HAL_GetTick() + I2C_TIMEOUT;
    while (!i2c_tx_done) {
        if (HAL_GetTick() > timeout) {
            // 超时,中止传输
            HAL_I2C_Master_Abort_IT(&hi2c1, slave_addr << 1);
            return I2C_ERROR_TIMEOUT;
        }
    }
    
    return i2c_error;
}

/**
 * @brief  I2C DMA模式接收数据
 * @param  slave_addr: 从设备7位地址
 * @param  data: 接收数据缓冲区
 * @param  len: 接收数据长度
 * @retval 错误码
 */
uint8_t I2C_DMA_Read(uint8_t slave_addr, uint8_t* data, uint16_t len)
{
    HAL_StatusTypeDef status;
    uint32_t timeout;
    
    // 检查总线是否忙
    if (HAL_I2C_GetState(&hi2c1) != HAL_I2C_STATE_READY) {
        return I2C_ERROR_BUSY;
    }
    
    // 重置标志
    i2c_rx_done = 0;
    i2c_error = I2C_OK;
    
    // 启动DMA接收
    status = HAL_I2C_Master_Receive_DMA(&hi2c1, slave_addr << 1, data, len);
    
    if (status != HAL_OK) {
        return I2C_ERROR_BUSY;
    }
    
    // 等待传输完成
    timeout = HAL_GetTick() + I2C_TIMEOUT;
    while (!i2c_rx_done) {
        if (HAL_GetTick() > timeout) {
            // 超时,中止传输
            HAL_I2C_Master_Abort_IT(&hi2c1, slave_addr << 1);
            return I2C_ERROR_TIMEOUT;
        }
    }
    
    return i2c_error;
}

// DMA发送通道中断服务函数(HAL库已实现,只需声明)
void DMA1_Channel6_IRQHandler(void)
{
    HAL_DMA_IRQHandler(&hdma_i2c1_tx);
}

// DMA接收通道中断服务函数(HAL库已实现,只需声明)
void DMA1_Channel7_IRQHandler(void)
{
    HAL_DMA_IRQHandler(&hdma_i2c1_rx);
}

七、主函数使用示例

cpp 复制代码
int main(void)
{
    uint8_t tx_buf[10] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A};
    uint8_t rx_buf[10] = {0};
    uint8_t ret;
    
    // HAL库初始化
    HAL_Init();
    
    // 系统时钟初始化(根据实际硬件配置)
    SystemClock_Config();
    
    // 选择一种模式初始化
    // I2C_CommonInit(); // 仅阻塞模式需要
    // I2C_Interrupt_Init(); // 中断模式
    I2C_DMA_Init(); // DMA模式
    
    while (1) {
        // 阻塞模式示例
        // ret = I2C_Blocking_Write(0x50, tx_buf, 10);
        // if (ret == I2C_OK) {
        //     ret = I2C_Blocking_Read(0x50, rx_buf, 10);
        // }
        
        // 中断模式示例
        // ret = I2C_Interrupt_StartWrite(0x50, tx_buf, 10);
        // if (ret == I2C_OK) {
        //     while (!I2C_Interrupt_IsTxDone());
        //     ret = I2C_Interrupt_GetError();
        //     if (ret == I2C_OK) {
        //         ret = I2C_Interrupt_StartRead(0x50, rx_buf, 10);
        //         while (!I2C_Interrupt_IsRxDone());
        //         ret = I2C_Interrupt_GetError();
        //     }
        // }
        
        // DMA模式示例
        ret = I2C_DMA_Write(0x50, tx_buf, 10);
        if (ret == I2C_OK) {
            ret = I2C_DMA_Read(0x50, rx_buf, 10);
        }
        
        // 延时
        HAL_Delay(1000);
    }
}

/**
 * @brief  系统时钟配置函数(示例)
 * @param  None
 * @retval None
 */
void SystemClock_Config(void)
{
    RCC_OscInitTypeDef RCC_OscInitStruct = {0};
    RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};
    
    // 配置HSE振荡器
    RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
    RCC_OscInitStruct.HSEState = RCC_HSE_ON;
    RCC_OscInitStruct.HSEPredivValue = RCC_HSE_PREDIV_DIV1;
    RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
    RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
    RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL9;
    
    if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK) {
        Error_Handler();
    }
    
    // 配置系统时钟、AHB和APB总线时钟
    RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
                              |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
    RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
    RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
    RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2;
    RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;
    
    if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2) != HAL_OK) {
        Error_Handler();
    }
}

八、关键注意事项

  1. 地址格式 :HAL 库中 I2C 地址是左移后的 8 位地址,需要将 7 位从地址左移 1 位
  2. 超时处理:所有函数都加入了超时机制,避免程序死锁
  3. 错误处理 :通过HAL_I2C_ErrorCallback()统一处理所有 I2C 错误
  4. DMA 关联 :必须使用__HAL_LINKDMA()宏将 DMA 句柄关联到 I2C 句柄
  5. 中断优先级:错误中断优先级应高于事件中断和 DMA 中断
  6. 总线恢复 :如果总线死锁,可以调用HAL_I2C_DeInit()然后重新初始化
相关推荐
zlinear数据采集卡2 小时前
电源纹波杀手:LDO线性稳压电路的“降噪哲学”——基于ZLinear数据采集卡的深度解析
单片机·嵌入式硬件·fpga开发·硬件架构
资深流水灯工程师2 小时前
STM32 USART 通讯原理与三种模式详解
stm32·单片机·嵌入式硬件
资深流水灯工程师2 小时前
STM32 单片机 SPI 通讯原理详解
stm32·单片机·嵌入式硬件
EMTime2 小时前
玲珑GUI-工程设置
单片机·mcu·ui·用户界面
不做无法实现的梦~2 小时前
MAVLink 协议教程
linux·stm32·嵌入式硬件·算法
QiLinkOS3 小时前
【用呼吸重构创造价值关系——QiLink生态】
c语言·数据结构·c++·人工智能·单片机·嵌入式硬件·算法
sxstj3 小时前
STM32F103 串口数量 + 对应 GPIO
单片机·嵌入式硬件
嵌入式ZYXC3 小时前
第4章:MCU最小系统设计——从一颗光杆芯片到它能跑起来
stm32·单片机·嵌入式硬件·物联网
czhaii4 小时前
ABB变频器 ACS510 传动故障诊断的故障队列
嵌入式硬件·硬件工程