📚 I2C 技术系列 · 持续更新中
本系列从 OD 开漏物理层一路打到 Linux i2c子系统,把"两根线"背后那套电气/协议/仲裁/恢复/驱动全拧成一根线。
👉 想系统追更,戳这里查看完整目录:
本篇定位 :【I2C 技术系列 08】I2C 控制器寄存器与 IP 架构讲了控制器寄存器,本篇讲软件怎么用这些寄存器完成一次传输。三种模式------轮询(PIO)、中断、DMA------各有取舍。本篇给三种模式的完整代码骨架和选择决策,你写裸机 I2C 驱动能直接套。
模式选择是 I2C 驱动的第一决策你写 I2C 驱动,先定模式:轮询(简单慢)、中断(通用)、DMA(大块快)。选错要么 CPU 占满,要么代码复杂过度。这不是"哪个高级用哪个",是场景匹配。
目录
- 一、三种模式总览
-
- [1.1 三种模式的工作方式](#1.1 三种模式的工作方式)
- [1.2 选择决策树](#1.2 选择决策树)
- 二、轮询(PIO)模式
-
- [2.1 原理](#2.1 原理)
- [2.2 完整写传输(DesignWare 风格)](#2.2 完整写传输(DesignWare 风格))
- [2.3 轮询的优缺点](#2.3 轮询的优缺点)
- 三、中断驱动模式
-
- [3.1 原理](#3.1 原理)
- [3.2 中断驱动写传输(状态机式)](#3.2 中断驱动写传输(状态机式))
- [3.3 中断驱动读传输](#3.3 中断驱动读传输)
- [3.4 中断模式优缺点](#3.4 中断模式优缺点)
- [3.5 RTOS 下中断 + 信号量](#3.5 RTOS 下中断 + 信号量)
- [四、DMA 模式](#四、DMA 模式)
-
- [4.1 原理](#4.1 原理)
- [4.2 DMA 配置](#4.2 DMA 配置)
- [4.3 DMA 的命令位难题](#4.3 DMA 的命令位难题)
- [4.4 DMA 读](#4.4 DMA 读)
- [4.5 DMA 模式优缺点](#4.5 DMA 模式优缺点)
- [4.6 缓存一致性](#4.6 缓存一致性)
- 五、三种模式取舍
-
- [5.1 数据量维度](#5.1 数据量维度)
- [5.2 实时性维度](#5.2 实时性维度)
- [5.3 复杂度维度](#5.3 复杂度维度)
- 六、混合模式
-
- [6.1 轮询 + 中断](#6.1 轮询 + 中断)
- [6.2 中断 + DMA](#6.2 中断 + DMA)
- [6.3 Linux 的做法](#6.3 Linux 的做法)
- [七、关键问题 Q&A](#七、关键问题 Q&A)
-
- [Q1:字节少时 DMA 反而慢,为什么?](#Q1:字节少时 DMA 反而慢,为什么?)
- [Q2:中断模式 ISR 里能阻塞吗?](#Q2:中断模式 ISR 里能阻塞吗?)
- [Q3:DMA buffer 对齐有要求吗?](#Q3:DMA buffer 对齐有要求吗?)
- Q4:轮询模式怎么加超时?
- Q5:同一控制器多个从机,怎么切换?
- [Q6:DMA 模式下中断还用吗?](#Q6:DMA 模式下中断还用吗?)
- 八、实战案例
-
- [案例 1:轮询模式无超时,死机](#案例 1:轮询模式无超时,死机)
- [案例 2:中断模式 ISR 没清标志,反复进](#案例 2:中断模式 ISR 没清标志,反复进)
- [案例 3:DMA 写 EEPROM,数据错位](#案例 3:DMA 写 EEPROM,数据错位)
- [案例 4:DMA 读命令位丢失](#案例 4:DMA 读命令位丢失)
- [案例 5:小数据用 DMA,反而比中断慢](#案例 5:小数据用 DMA,反而比中断慢)
- 九、本篇小结与速查
- 延伸阅读
- 参考资料
一、三种模式总览
| 模式 | CPU 占用 | 实时性 | 代码复杂度 | 适用 |
|---|---|---|---|---|
| 轮询(PIO) | 高(全程占) | 中 | 低 | 初始化/小数据/调试 |
| 中断 | 低(事件进) | 高 | 中 | 通用,中小数据 |
| DMA | 极低(完成才进) | 高 | 高 | 大块数据(EEPROM/图像) |
1.1 三种模式的工作方式
轮询:CPU 写 DAT → 死循环读 STAT 等 TX_EMPTY → 写下一字节 → ...
中断:CPU 写 DAT → 干别的 → 中断通知 TX_EMPTY → ISR 写下一字节 → ...
DMA:CPU 配 DMA+I2C → 干别的 → DMA 自动搬 → 完成中断
1.2 选择决策树
数据量?
├─ < 4 字节 / 初始化配置 → 轮询(够用,简单)
├─ 4~64 字节 / 寄存器读写 → 中断(通用)
└─ > 64 字节 / EEPROM/图像 → DMA(省 CPU)
二、轮询(PIO)模式
2.1 原理
CPU 全程参与,死循环查状态位:
c
// 写一字节
i2c_write_byte(uint8_t b) {
I2C->DAT = b; // 写数据寄存器
while (!(I2C->STAT & TX_EMPTY)); // 死等发送完成
I2C->STAT_CLR = TX_EMPTY; // 清标志
}
// 读一字节
uint8_t i2c_read_byte(void) {
I2C->CMD = READ_CMD; // 发读请求
while (!(I2C->STAT & RX_FULL)); // 死等接收满
I2C->STAT_CLR = RX_FULL;
return I2C->DAT;
}
2.2 完整写传输(DesignWare 风格)
c
// 主机写 N 字节到从机 addr
int i2c_master_write(uint8_t addr, uint8_t *data, int len) {
// 1. 等总线空闲
while (I2C->STATUS & ACTIVITY);
// 2. 设目标地址
I2C->TAR = addr;
// 3. 使能控制器
I2C->ENABLE = 1;
// 4. 逐字节写
for (int i = 0; i < len; i++) {
// 等 TX FIFO 不满
while (!(I2C->STATUS & TFNF)); // TX FIFO Not Full
// 写数据 + 命令(写,最后一字节带 STOP)
uint32_t cmd = data[i] | (i == len-1 ? BIT(9) : 0); // bit9=STOP
I2C->DATA_CMD = cmd;
}
// 5. 等 STOP 发出
while (!(I2C->RAW_INTR & STOP_DET));
I2C->CLR_STOP_DET;
// 6. 检查 NACK
if (I2C->TX_ABRT_SOURCE & ABRT_7B_ADDR_NOACK) {
I2C->CLR_TX_ABRT;
return -ENXIO; // 无此设备
}
return len;
}
2.3 轮询的优缺点
优点:
- 代码简单,无中断/DMA 依赖
- 调试容易(顺序执行)
- 适合初始化/小配置
缺点:
- CPU 全程占用,不能干别的
- 长传输(EEPROM 写 5ms)CPU 死等
- RTOS 下阻塞任务,影响调度
!note 嵌入式视角:轮询是"调试模式",生产慎用
你调 I2C 第一步用轮询(简单,能确认时序对),调通后改中断/DMA。生产代码别用轮询(浪费 CPU),除非数据极小且不频繁(如开机读一次配置)。
三、中断驱动模式
3.1 原理
配置中断,事件发生时进 ISR,ISR 推进状态机:
软件:启动传输 → 干别的
硬件:事件(TX_EMPTY/RX_FULL/STOP)→ 触发中断
ISR:查事件 → 写下一字节/读数据/完成通知
3.2 中断驱动写传输(状态机式)
c
typedef enum { ST_IDLE, ST_WRITE, ST_READ, ST_WAIT_STOP } i2c_state_t;
typedef struct {
uint8_t addr;
uint8_t *buf;
int len;
int idx;
i2c_state_t state;
void (*complete)(int status);
} i2c_xfer_t;
static i2c_xfer_t xfer;
// 启动写
int i2c_write_async(uint8_t addr, uint8_t *data, int len, void (*cb)(int)) {
xfer.addr = addr;
xfer.buf = data;
xfer.len = len;
xfer.idx = 0;
xfer.state = ST_WRITE;
xfer.complete = cb;
I2C->TAR = addr;
I2C->ENABLE = 1;
I2C->INTR_MASK = TX_EMPTY | STOP_DET | TX_ABRT; // 使能中断
// 第一字节触发(也可等 TX_EMPTY 中断)
I2C->DATA_CMD = data[0] | (len==1 ? BIT(9) : 0);
xfer.idx = 1;
return 0;
}
// ISR
void i2c_isr(void) {
uint32_t stat = I2C->INTR_STAT;
if (stat & TX_ABRT) {
I2C->CLR_TX_ABRT;
xfer.state = ST_IDLE;
xfer.complete(-EIO);
return;
}
if (stat & TX_EMPTY) {
if (xfer.idx < xfer.len) {
uint32_t cmd = xfer.buf[xfer.idx] | (xfer.idx==xfer.len-1 ? BIT(9):0);
I2C->DATA_CMD = cmd;
xfer.idx++;
} else {
I2C->INTR_MASK &= ~TX_EMPTY; // 关 TX_EMPTY
}
}
if (stat & STOP_DET) {
I2C->CLR_STOP_DET;
xfer.state = ST_IDLE;
xfer.complete(xfer.len);
}
}
3.3 中断驱动读传输
c
int i2c_read_async(uint8_t addr, uint8_t *buf, int len, void (*cb)(int)) {
xfer.addr = addr;
xfer.buf = buf;
xfer.len = len;
xfer.idx = 0;
xfer.state = ST_READ;
xfer.complete = cb;
I2C->TAR = addr;
I2C->ENABLE = 1;
I2C->INTR_MASK = RX_FULL | STOP_DET | TX_ABRT;
// 发 N 个读请求(bit8=1 表示读)
for (int i = 0; i < len; i++) {
while (!(I2C->STATUS & TFNF));
uint32_t cmd = BIT(8) | (i==len-1 ? BIT(9):0); // 读 + 末字节 STOP
I2C->DATA_CMD = cmd;
}
return 0;
}
// ISR 中 RX_FULL 处理
if (stat & RX_FULL) {
xfer.buf[xfer.idx++] = I2C->DATA_CMD & 0xFF;
if (xfer.idx >= xfer.len) {
I2C->INTR_MASK &= ~RX_FULL;
}
}
3.4 中断模式优缺点
优点:
- CPU 不死等,事件进 ISR
- 实时性好(事件即响应)
- 通用性强
缺点:
- 每字节/每事件中断,大数据量中断频繁
- ISR 逻辑要小心(状态机)
- 高速(1M)大数据下中断开销仍大
3.5 RTOS 下中断 + 信号量
c
// FreeRTOS 风格
SemaphoreHandle_t i2c_done_sem;
int i2c_write_rtos(uint8_t addr, uint8_t *data, int len) {
xfer.complete = isr_give_sem; // ISR 给信号量
i2c_write_async(addr, data, len, ...);
xSemaphoreTake(i2c_done_sem, portMAX_DELAY); // 任务阻塞等
return xfer.ret;
}
void isr_give_sem(int status) {
xfer.ret = status;
BaseType_t hpw = pdFALSE;
xSemaphoreGiveFromISR(i2c_done_sem, &hpw);
portYIELD_FROM_ISR(hpw);
}
- 任务层阻塞等信号量,ISR 完成后给
- 不死循环,RTOS 调度其他任务
四、DMA 模式
4.1 原理
DMA 控制器接管数据搬运,I2C FIFO 与内存之间自动传输:
内存 buffer ←→ DMA ←→ I2C FIFO ←→ 移位寄存器 ←→ SDA
(CPU 不参与)
4.2 DMA 配置
c
// 写:内存 → I2C TX FIFO
int i2c_write_dma(uint8_t addr, uint8_t *data, int len, void (*cb)(int)) {
xfer.complete = cb;
// 配 I2C
I2C->TAR = addr;
I2C->ENABLE = 1;
I2C->DMA_CR = DMA_TX_ENABLE | TX_FIFO_THRESH_HALF;
// 配 DMA(内存→I2C TX FIFO)
DMA->SRC = (uint32_t)data;
DMA->DST = (uint32_t)&I2C->DATA_CMD;
DMA->LEN = len;
// 问题:DATA_CMD 要带命令位(bit8=0 写,bit9=STOP)
// 多数 DMA 只搬数据,命令位要预生成
// 方案:预处理 buffer,每字节拼命令位
// 启动 DMA
DMA->CTRL = DMA_ENABLE | SRC_INC | DST_NO_INC;
// 使能 I2C DMA 中断
I2C->INTR_MASK = STOP_DET | TX_ABRT;
return 0;
}
4.3 DMA 的命令位难题
I2C 的 DATA_CMD 寄存器是 [cmd(8:9)] [data(7:0)],DMA 搬 8 位数据时命令位丢失。解决:
方案 1:32 位 DMA,预处理命令字
c
// 预处理:把 buffer 扩成 32 位,拼命令位
uint32_t cmd_buf[len];
for (int i = 0; i < len; i++) {
cmd_buf[i] = data[i] | (i == len-1 ? BIT(9) : 0); // 末字节 STOP
}
// DMA 搬 32 位
DMA->WIDTH = 32;
DMA->SRC = cmd_buf;
方案 2:控制器支持"DMA 模式自动加命令位"
- 部分 IP(DW)支持 DMA 模式下自动认为是写,末字节自动 STOP
- 配 IC_DMA_CR 寄存器
方案 3:不用 DMA,中断足够
- 数据量不大时(< 64 字节),DMA 配置开销 > 收益
4.4 DMA 读
c
int i2c_read_dma(uint8_t addr, uint8_t *buf, int len) {
// 先发 N 个读请求(命令位 bit8=1)
// 这步用 PIO 或中断发(命令位要拼)
for (int i = 0; i < len; i++) {
while (!(I2C->STATUS & TFNF));
I2C->DATA_CMD = BIT(8) | (i==len-1 ? BIT(9):0);
}
// 配 DMA:I2C RX FIFO → 内存
I2C->DMA_CR = DMA_RX_ENABLE | RX_FIFO_THRESH_HALF;
DMA->SRC = &I2C->DATA_CMD;
DMA->DST = buf;
DMA->LEN = len;
DMA->CTRL = DMA_ENABLE | DST_INC | SRC_NO_INC;
I2C->INTR_MASK = STOP_DET | TX_ABRT;
return 0;
}
4.5 DMA 模式优缺点
优点:
- CPU 几乎不参与(完成才中断)
- 大块传输高效
- 适合 EEPROM 整页读写、图像缓冲
缺点:
- 配置复杂(命令位、FIFO 阈值、缓存一致性)
- 小数据反而慢(配置开销)
- 缓存一致性问题(DMA buffer 要 cache flush/invalidate)
4.6 缓存一致性
c
// DMA 写前:flush cache(数据从 cache 写回内存)
flush_dcache_range((uint32_t)buf, len);
// DMA 读后:invalidate cache(丢弃 cache,从内存重读)
invalidate_dcache_range((uint32_t)buf, len);
- 不 flush:DMA 读到旧内存数据(数据未写回)
- 不 invalidate:CPU 读到旧 cache(DMA 新数据在内存)
- Cortex-M7/RISC-V with cache 必须处理
五、三种模式取舍
5.1 数据量维度
| 数据量 | 推荐模式 | 理由 |
|---|---|---|
| 1~4 字节 | 轮询 | 配置开销最小 |
| 4~64 字节 | 中断 | 实时 + 代码清晰 |
| 64~256 字节 | 中断/DMA | 临界,DMA 略优 |
| > 256 字节 | DMA | 省 CPU 明显 |
5.2 实时性维度
| 场景 | 推荐 |
|---|---|
| 实时控制(不能阻塞) | 中断/DMA |
| 初始化(顺序执行) | 轮询 |
| RTOS 多任务 | 中断 + 信号量 |
5.3 复杂度维度
| 模式 | 代码量 | 调试难度 |
|---|---|---|
| 轮询 | 少 | 易 |
| 中断 | 中 | 中(状态机) |
| DMA | 多 | 难(缓存/命令位) |
六、混合模式
6.1 轮询 + 中断
- 初始化用轮询(简单)
- 正式传输用中断(高效)
- 多数 RTOS 驱动这么做
6.2 中断 + DMA
- 小数据用中断
- 大数据用 DMA
- 驱动按 len 动态选
c
int i2c_xfer(uint8_t addr, uint8_t *buf, int len, int is_write) {
if (len <= 8) {
return i2c_xfer_irq(addr, buf, len, is_write); // 小数据中断
} else {
return i2c_xfer_dma(addr, buf, len, is_write); // 大数据 DMA
}
}
6.3 Linux 的做法
Linux i2c 框架:控制器驱动实现 master_xfer,框架按 msg 长度/类型调。底层用中断或 DMA,驱动透明。详见 \[11-I2C Linux驱动框架]。
七、关键问题 Q&A
Q1:字节少时 DMA 反而慢,为什么?
A :DMA 启动有开销:配源/目的/长度/命令位预处理/cache flush,启动后还要等 FIFO 阈值触发。这几 μs 的开销,大数据能摊薄,小数据(几字节)反而比中断慢(中断几条指令就发完)。DMA 阈值常设 64 或 128 字节。
Q2:中断模式 ISR 里能阻塞吗?
A :不能。ISR 要快进快出,不能等(不能 sleep/不能忙等)。数据搬运在 ISR 里做(快),耗时操作(回调处理/协议解析)推迟到任务/下半部。RTOS 下 ISR 只给信号量,任务层处理。
Q3:DMA buffer 对齐有要求吗?
A :多数 DMA 要求源/目的地址对齐(4 字节或 burst 大小)。buffer 要 __attribute__((aligned(4)))。cache line 对齐(32/64 字节)更优(cache 操作整行)。
Q4:轮询模式怎么加超时?
A:
c
uint32_t timeout = 1000000; // 超时计数
while (!(I2C->STAT & TX_EMPTY) && --timeout);
if (!timeout) return -ETIMEDOUT;
必须加超时,否则器件异常(拉伸/死机)CPU 死循环。功能安全必查。
Q5:同一控制器多个从机,怎么切换?
A :每次传输前重设 IC_TAR(目标地址)。DesignWare:发 STOP 后等总线空闲,写新 TAR,启动新传输。别在一次传输中途换 TAR(协议不允许)。
Q6:DMA 模式下中断还用吗?
A :用。DMA 完成或 I2C STOP/ABRT 仍要中断通知 CPU。DMA 替代的是"数据搬运中断"(TX_EMPTY/RX_FULL),不是"完成中断"(STOP_DET)。
八、实战案例
案例 1:轮询模式无超时,死机
现象:裸机 I2C 轮询读传感器,传感器掉电后 CPU 死循环。
分析 :while(!RX_FULL) 无超时,传感器不响应,CPU 永远等。
解决:加超时计数,超时报错。功能安全必加。
案例 2:中断模式 ISR 没清标志,反复进
现象:I2C 中断 ISR 进不去(或反复进),系统卡。
分析:ISR 没清中断标志(读 IC_CLR_*),标志一直有效,中断反复触发。
解决:ISR 末尾清标志(DesignWare 读 IC_CLR_* 自动清)。
案例 3:DMA 写 EEPROM,数据错位
现象:DMA 写 24C04 16 字节,读回发现首字节错。
分析:DMA buffer 没 flush cache,Cortex-M7 cache 里数据没写回内存,DMA 读到旧数据。
解决 :DMA 启动前 flush_dcache_range。
案例 4:DMA 读命令位丢失
现象:DMA 读 BMP280,只读到 0。
分析:DMA 模式下没先发读请求命令位,DMA 搬的是数据但 I2C 没发读请求。
解决:DMA 启动前先 PIO 发 N 个读请求命令(bit8=1),再启动 DMA 收数据。
案例 5:小数据用 DMA,反而比中断慢
现象:把所有 I2C 都改 DMA,4 字节寄存器读写变慢。
分析:DMA 配置开销 ~10μs,4 字节中断只要 90μs(400k × 9 × 4)的 1/10,但 DMA 配置占了大头。
解决:按长度动态选(< 8 字节用中断,> 64 用 DMA)。
九、本篇小结与速查
核心要点
- 三种模式:轮询(PIO,简单)/中断(通用)/DMA(大块)
- 轮询:CPU 全程占,适合小数据/调试,必加超时
- 中断:事件进 ISR,ISR 推状态机,RTOS 配信号量,通用
- DMA:FIFO↔内存自动搬,省 CPU,要处理命令位 + cache 一致性
- 选型:小数据轮询/中断,大数据 DMA;按 len 动态选最优
- DMA 命令位难题:32 位预处理 buffer 或 IP 自动加命令位
- cache 一致性:DMA 写前 flush,读后 invalidate
- ISR 不阻塞,耗时推迟到任务
选型决策表
| 场景 | 模式 | 理由 |
|---|---|---|
| 开机读配置(1次) | 轮询 | 简单,够用 |
| 寄存器读写(<8B) | 中断 | 通用,实时 |
| EEPROM 页写(64B) | 中断/DMA | 临界 |
| EEPROM 整片读(4KB) | DMA | 省 CPU |
| 图像缓冲(几KB) | DMA | 必须 |
| 实时控制(不能阻塞) | 中断+信号量 | RTOS 友好 |
| 调试阶段 | 轮询 | 易定位 |
速查表
| 想干啥 | 做啥 |
|---|---|
| 写一字节(调试) | DAT = b; while(!TX_EMPTY) |
| 中断 ISR | 查 INTR_STAT → 分支处理 → 清标志 |
| DMA 写 | flush cache → 配 DMA → 启动 → 等 STOP |
| DMA 读 | 先发读请求 → invalidate cache → 配 DMA → 启动 |
| 超时 | while(cond && --timeout) |
| RTOS 等待 | ISR 给信号量,任务 Take |
| 切换从机 | STOP → 空闲 → 写新 TAR |
| DMA 阈值 | len > 64 用 DMA |
延伸阅读
- \[08-I2C控制器寄存器与IP架构] ------ 寄存器细节
- \[10-I2C错误处理与总线恢复] ------ 中断里的错误处理
- \[11-I2C Linux驱动框架] ------ Linux 下 master_xfer 实现
- \[12-I2C设备驱动实战] ------ 实战代码
- \[FreeRTOS_设计思想与功能模块讲解] ------ RTOS 信号量/中断
参考资料
- Synopsys DesignWare I2C Databook(DMA 章节)
- STM32 HAL I2C(中断/DMA 实现)
- Linux drivers/i2c/busses/i2c-designware.c
- FreeRTOS API Reference(Semaphore)
\[00-I2C技术系列总目录] · 上一篇:\[08-I2C控制器寄存器与IP架构] · 下一篇:\[10-I2C错误处理与总线恢复]
✨ 感谢你的阅读 ✨
技术之路漫漫,分享是为了更好地交流。
如果本文的内容对你有启发,希望能得到你的支持:
👍 点赞 | ⭐ 收藏 | 👀 关注
如果你在调试过程中遇到了其他问题,
欢迎在 评论区 💬 留言,我们一起探讨。
关注我,一起交流底层开发的那些事儿!🚀