I2C中断与DMA协同:在AURIX™ TC3xx上打造高效通信的实战之道
你有没有遇到过这样的场景?系统里接了七八个I2C传感器,温度、光照、加速度……每毫秒都要轮询一次。结果CPU 80%的时间都在处理I2C中断,连主循环都跑不顺,更别提实时任务响应了。
这不是夸张——在汽车电子或工业控制这类高实时性要求的系统中,这种“中断风暴”是常态。而解决它的关键,往往就藏在一个组合拳里:I2C中断 + DMA协同工作。
今天我们就以英飞凌AURIX™ TC3xx系列为核心,深入拆解这套机制背后的原理与工程实现。不讲空话,只聊你能用得上的硬核内容。
为什么传统I2C方案扛不住复杂系统?
先来直面问题。大多数初学者甚至部分工程师还在用“中断+逐字节读写”的方式操作I2C:
void I2C_IRQHandler() { if (INTSTAT.B.RXFF) { // 接收到一字节 buffer[index++] = RXDATA; // CPU亲自搬数据 if (index == len) complete = 1; } }这看似没问题,但当你传输128字节的数据时,意味着要触发128次中断,每次都要保存上下文、跳转ISR、恢复现场……这些开销累积起来可能比数据传输本身还耗时。
更要命的是,在多核MCU如TC3xx上,频繁中断会打乱高优先级任务(比如电机控制、安全监控)的执行节奏,造成抖动甚至失控。
那怎么办?答案很明确:让硬件干它该干的事。
核心思想:
- 中断只负责“通知事件”——开始、结束、出错;
- 数据搬运交给DMA,全程零CPU干预;
- CPU腾出手来做更重要的事。
这才是现代嵌入式系统的正确打开方式。
TC3平台上的I2C与DMA协同:不只是理论
AURIX™ TC3xx 并不是普通的单片机。它是为汽车功能安全(ASIL-D)设计的多核实时处理器,内置强大的外设协同能力。其中,I2C模块、DMA控制器、中断管理单元(ICU)之间的联动机制,正是我们优化通信效率的关键抓手。
先看几个硬指标(来自TRM)
| 特性 | 参数 |
|---|---|
| I2C支持速率 | 最高400kbps(标准/快速模式),部分型号支持HSM 3.4Mbps |
| DMA通道数 | 最多32通道,可配置优先级与仲裁策略 |
| 中断延迟 | 典型值 < 5μs(取决于优先级) |
| I2C FIFO深度 | 1~4字节(影响DMA触发频率) |
这些参数告诉我们一件事:硬件已经准备好做高效传输,缺的只是正确的配置方法。
深度解析:I2C与DMA如何真正“握手”?
很多人以为“DMA能自动传数据”是个黑盒,其实不然。要想让它稳定工作,必须搞清楚三个核心环节:请求源、传输路径、完成通知。
1. 谁发起DMA请求?—— I2C状态机说了算
在TC3架构中,I2C模块内部有一个状态机,能够自动识别当前处于发送还是接收阶段,并在适当时机发出硬件请求信号(Hardware Request Line)。
例如:
- 当I2C准备发送下一个字节时,检测到Tx Buffer为空 → 触发TX_BUFFER_EMPTY请求;
- 当接收到一个完整字节后,Rx Buffer满 → 触发RX_BUFFER_FULL请求;
这个请求不会直接进CPU,而是通过ERU(Event Router Unit)或DMA硬件线直接连接到DMA控制器,作为DMA通道的启动源。
✅ 关键点:
不是靠软件轮询或中断唤醒DMA,而是由I2C外设主动推信号给DMA,实现真正的异步联动。
2. 数据怎么走?—— 建立一条“内存 ↔ 外设寄存器”的高速公路
假设我们要从传感器读取64字节数据。传统做法是每个字节进一次ISR;而现在,我们建立这样一条通路:
[ I2C_RxBuf ] ←→ [ DMA Channel ] ←→ [ RAM Buffer ] ↑ ↑ ↑ 外设数据到达 硬件自动搬运 应用层可访问整个过程无需CPU插手,直到最后一个字节落下,I2C模块才发出一个“Transfer Complete”中断,告诉CPU:“活干完了。”
3. 怎么知道结束了?—— 中断只用来“收尾”
所以你看,中断的角色变了:
-不再用于搬运数据;
-仅用于处理边界事件:启动前初始化、结束后清理、错误异常处理。
这就把原本密集的中断降成了“稀疏事件”,CPU负载直线下降。
实战代码:手把手教你配通I2C+DMA链路
下面这段代码是在TC3xx上配置I2C主发送 + DMA搬运的真实范例。我们将使用底层寄存器操作(便于理解机制),实际项目中可用iLLD库封装。
配置DMA通道用于I2C发送
#include "IfxI2c_reg.h" #include "IfxDma_reg.h" void configure_dma_i2c_tx(uint8 channel, uint8* src_buf, uint16 length) { // Step 1: 暂停通道以便配置 DMACH[channel].CHCR.B.HDWREN = 1; // 启用软件写权限 DMACH[channel].CHCR.B.ENSTAT = 0; // 停止通道 // Step 2: 设置源地址(内存缓冲区) DMACH[channel].ADRCR.U = (uint32)src_buf; DMACH[channel].ADRCR.B.ADDMODSRC = 0x1; // 源地址自增 // Step 3: 设置目标地址(I2C Tx数据寄存器) DMACH[channel].DADR.U = (uint32)&MODULE_I2C0.TXD; // 写入I2C0的TXD寄存器 // Step 4: 设置传输长度 DMACH[channel].TSR.B.TREL = length; // 传输字节数 // Step 5: 配置传输模式:内存→外设,8位宽度 DMACH[channel].CHCFGR.B.SIZE = 0; // 8-bit DMACH[channel].CHCFGR.B.SGINP = 1; // 使用服务请求输入1(需查表映射到I2C_TX_REQ) DMACH[channel].CHCFGR.B.DRM = 1; // 外设请求模式(由I2C触发) DMACH[channel].CHCFGR.B.BLKM = 0; // 单块传输 DMACH[channel].CHCFGR.B.CHDMA = 1; // 使能通道间链接(可选) // Step 6: 清除旧状态并启用通道 DMACH[channel].CHCSR.U = 0; DMACH[channel].CHCR.B.ENSTAT = 1; // 启动DMA通道 }🔍 注:
SGINP = 1表示选择哪个硬件请求源,具体对应关系需查阅芯片手册中的“DMA Source Select Table”。例如,I2C0的Tx Ready请求可能是SRID = 19。
启动I2C传输并交由DMA接管
void start_i2c_dma_write(uint8 slave_addr, uint8* data, uint16 len) { // 1. 配置DMA configure_dma_i2c_tx(DMA_CHANNEL_I2C0_TX, data, len); // 2. 配置I2C为主模式,设置从机地址 MODULE_I2C0.CTRL.B.MS = 1; // 主模式 MODULE_I2C0.ADDR.B.ADDR = slave_addr << 1; // 7-bit地址左移 MODULE_I2C0.CMD.B.START = 1; // 发送Start + Addr(W) // 3. 使能I2C的DMA请求功能(关键!) MODULE_I2C0.FCTRL.B.TDEM = 1; // 当Tx Buffer空时产生DMA请求 }注意这一句:
MODULE_I2C0.FCTRL.B.TDEM = 1;它打开了I2C模块对外的“请求门”,告诉DMA:“我需要数据了,快来喂!”
收尾工作:传输完成中断处理
最后,等所有数据发完,I2C会产生一个Transfer Complete(TC)中断,这时再让CPU介入即可。
IFX_INTERRUPT(i2cTcISR, 0, ISR_PRIORITY_I2C_TC); void i2cTcISR(void) { // 清除中断标志 MODULE_I2C0.INTSTAT.B.TC = 1; // 可选:发送Stop条件 MODULE_I2C0.CMD.B.STOP = 1; // 通知应用层 g_i2cTxComplete = TRUE; }看到没?整个过程中,CPU只参与了开头一帧和结尾一帧的操作,中间64个字节全由DMA默默搞定。
接收也一样高效:反过来配就行
接收流程逻辑对称,只需调整DMA方向:
// DMA配置片段(接收) DMACH[channel].ADRCR.U = (uint32)&MODULE_I2C0.RXD; // 源地址:I2C Rx寄存器 DMACH[channel].DADR.U = (uint32)rx_buffer; // 目标地址:RAM缓冲区 DMACH[channel].CHCFGR.B.DRM = 1; // 外设请求模式 DMACH[channel].CHCFGR.B.SGINP = 2; // 映射到I2C_RX_REQ同时开启I2C的Rx DMA使能:
MODULE_I2C0.FCTRL.B.RDFM = 1; // Rx Data Full → 触发DMA请求唯一需要注意的是:最后一个字节要提前告知I2C不要发ACK,否则总线会继续等待后续数据。
通常做法是在倒数第二个字节时设置NACK,并在收到最后一个字节后立即发STOP。
工程实践中必须注意的5个坑
再好的机制,落地时也会踩坑。以下是我在多个车载项目中总结的经验教训:
❌ 坑1:DMA地址未对齐导致传输失败
- 现象:DMA启动后无反应,或只传几个字节就卡住。
- 原因:TC3的DMA要求32位传输必须4字节对齐,16位需2字节对齐。
- 解决方案:
c __attribute__((aligned(4))) uint8 tx_buf[64];
❌ 坑2:I2C FIFO太浅,DMA请求过于频繁
- 现象:虽然用了DMA,但中断仍然很多。
- 原因:某些型号I2C只有1级FIFO,每发一字节就请求一次DMA。
- 对策:尽量选择带更深FIFO的型号,或启用突发传输(burst)减少请求次数。
❌ 坑3:DMA通道冲突或优先级设置不当
- 现象:高优先级任务被DMA阻塞。
- 建议:将I2C相关DMA通道设为中低优先级,避免抢占ADC、PWM等关键路径。
❌ 坑4:忘记清除中断标志,导致重复进入ISR
- 经典错误:
c if (INTSTAT.B.TC) { // 处理完没清标志 → 下次又进来 } - 正确做法:
c MODULE_I2C0.INTSTAT.B.TC = 1; // 写1清零
❌ 坑5:未处理NACK或总线错误
- 建议:在ISR中检查
INTSTAT.B.NACK,ARBLOS等标志,必要时重试或上报故障。
这套机制适合哪些场景?
别盲目上DMA。以下是推荐使用的典型应用场景:
| 场景 | 是否推荐 |
|---|---|
| 批量读写EEPROM/Flash | ✅ 强烈推荐(>32字节) |
| 多传感器周期采集(>1kHz) | ✅ 必须用DMA降低延迟 |
| 寄存器配置类小包通信(<8字节) | ⚠️ 可不用DMA,直接中断处理更简单 |
| 低功耗待机监听 | ✅ 可结合DMA唤醒CPU,节省能耗 |
一句话总结:数据越多、频率越高、实时性越强,越值得上DMA。
写在最后:让硬件真正为你打工
回到最初的问题:怎么让I2C不拖累系统性能?
答案不在更快的CPU,也不在更高的主频,而在于是否善用了硬件协同机制。
在AURIX™ TC3xx平台上,I2C中断与DMA的配合,本质上是一种“事件驱动 + 数据流自动化”的设计理念。它让我们可以把CPU从“搬运工”变成“指挥官”。
下次当你面对复杂的外设通信需求时,不妨问自己三个问题:
1. 这个传输能不能交给DMA?
2. 中断是不是只保留最关键的事件?
3. CPU能不能在这段时间去做更有价值的事?
如果答案都是肯定的,那你离写出真正高效的嵌入式代码,就不远了。
如果你正在开发基于TC3的项目,欢迎留言交流具体应用场景,我们可以一起探讨最优架构设计。