一、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 主发送中断流程
-
初始化阶段
- 配置 I2C 外设参数 (时钟频率、地址等)
- 使能 I2C 事件中断和错误中断 (CR2.ITEVTEN=1, CR2.ITERREN=1)
- 配置 NVIC 中断优先级,使能 I2C 全局中断
-
启动传输
- 设置传输状态为 "发送中"
- 产生起始条件 (CR1.START=1)
-
中断处理流程
- SB 中断:发送从地址 + 写位 (DR=slave_addr<<1)
- ADDR 中断:读 SR1 和 SR2 清除标志,发送第一个数据字节
- TXE 中断:如果还有数据未发送,发送下一个字节;否则关闭 TXE 中断,等待 BTF 中断
- BTF 中断:所有数据发送完毕,产生停止条件 (CR1.STOP=1),设置传输状态为 "完成"
- 错误中断:处理各种错误,产生停止条件,设置传输状态为 "失败"
4.3 主接收中断流程
-
初始化阶段:同主发送
-
启动传输
- 设置传输状态为 "接收中"
- 产生起始条件 (CR1.START=1)
-
中断处理流程
- 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 字节) |
| 实时性 | 差 | 好 | 最好 |
| 适用场景 | 简单应用,初始化配置 | 一般应用,多任务系统 | 高速数据采集,传感器阵列 |
七、常见问题与解决方案
-
总线死锁:SDA 或 SCL 被持续拉低
- 解决方案:软件复位 I2C 外设 (CR1.SWRST=1),然后重新初始化
- 终极方案:模拟 I2C 时序发送 9 个时钟脉冲,强制释放总线
-
应答失败 (AF 错误)
- 检查从设备地址是否正确
- 检查硬件连接和上拉电阻
- 确认从设备是否正常工作
-
DMA 接收最后一个字节丢失
- 原因:DMA 传输完成后,最后一个字节还在 I2C 移位寄存器中
- 解决方案:在 DMA 传输完成中断中,手动读取最后一个字节
-
仲裁丢失 (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 工作步骤
- 初始化 :调用
I2C_CommonInit()配置 GPIO 和 I2C 外设 - 发送流程 :
- 调用
HAL_I2C_Master_Transmit()函数 - 函数内部自动处理:产生起始条件→发送从地址→发送数据→产生停止条件
- 等待函数返回,检查返回值判断是否成功
- 调用
- 接收流程 :
- 调用
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 工作步骤
- 初始化 :
- 调用
I2C_CommonInit()配置 GPIO 和 I2C 外设 - 配置 NVIC 中断优先级,使能 I2C 事件中断和错误中断
- 调用
- 启动传输 :
- 重置传输完成标志和错误标志
- 调用
HAL_I2C_Master_Transmit_IT()或HAL_I2C_Master_Receive_IT()函数
- 中断处理 :
- HAL 库自动处理 I2C 中断,无需手动编写中断服务函数
- 传输完成后自动调用
HAL_I2C_TxCpltCallback()或HAL_I2C_RxCpltCallback() - 发生错误时自动调用
HAL_I2C_ErrorCallback()
- 主程序:轮询传输完成标志,等待传输结束
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 工作步骤
- 初始化 :
- 调用
I2C_CommonInit()配置 GPIO 和 I2C 外设 - 配置 DMA 通道和中断
- 将 DMA 句柄关联到 I2C 句柄
- 调用
- 启动传输 :
- 重置传输完成标志和错误标志
- 调用
HAL_I2C_Master_Transmit_DMA()或HAL_I2C_Master_Receive_DMA()函数
- DMA 传输 :
- DMA 控制器自动完成数据寄存器与内存之间的数据传输
- 传输完成后触发 DMA 中断
- HAL 库自动处理 DMA 中断,调用相应的回调函数
- 主程序:轮询传输完成标志,等待传输结束
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();
}
}
八、关键注意事项
- 地址格式 :HAL 库中 I2C 地址是左移后的 8 位地址,需要将 7 位从地址左移 1 位
- 超时处理:所有函数都加入了超时机制,避免程序死锁
- 错误处理 :通过
HAL_I2C_ErrorCallback()统一处理所有 I2C 错误 - DMA 关联 :必须使用
__HAL_LINKDMA()宏将 DMA 句柄关联到 I2C 句柄 - 中断优先级:错误中断优先级应高于事件中断和 DMA 中断
- 总线恢复 :如果总线死锁,可以调用
HAL_I2C_DeInit()然后重新初始化