4、DMA与I2C的硬件连接

芯片内部,DMA控制器和I2C外设之间怎么“握手”?说白了就是两件事——谁发起传输,谁响应传输。我刚开始学这个的时候,觉得不就是两根线的事吗?等你真去调一个多主设备共享DMA通道的系统,你就知道这里面的门道有多深了。

4.1 内部总线架构:DMA与I2C怎么“见面”

先看看它们在芯片里怎么连的。典型的MCU内部结构是这样的:DMA控制器挂载在系统总线(AHB/APB)上,I2C外设作为从设备也挂在总线上。但DMA要访问I2C的寄存器,不能直接拉一根“私线”过去——它们通过总线矩阵和仲裁器通信。

关键点:DMA控制器并不“认识”I2C,它只认地址和请求通道。I2C外设通过硬件请求线(DMA request)告诉DMA:“我有数据要搬了,快来!”

我印象很深的是在某款STM32F4的板子上,I2C1的DMA请求是映射到DMA1的通道6和通道7。如果你初始化时把通道配错了,数据收发完全没反应,连错误标志都不报——因为DMA根本没收到请求。

信号方向名称说明
I2C → DMADMA_REQ_TX / RX发送/接收数据请求
DMA → I2CDMA_ACK应答信号(部分MCU需要)
DMA → 总线总线请求DMA向总线仲裁器申请控制权
总线 → DMA总线授权仲裁器把总线控制权交给DMA

4.2 DMA请求信号——谁喊“开始搬”?

I2C外设内部有一个数据寄存器(DR)。当I2C完成一个字节的接收或发送,它会把DR的状态映射到一根硬件请求线上。这根线直接连到DMA控制器的某个通道。

具体触发时机:

  • 发送方向:I2C_DR变空(TXE标志置位),I2C硬件拉高DMA请求线,告诉DMA“再给我一个字节”。
  • 接收方向:I2C_DR收到新数据(RXNE标志置位),I2C硬件拉高DMA请求线,告诉DMA“快把数据搬走”。

一个小技巧:我习惯在初始化后先读一下DMA的剩余传输计数值,确认它≠0。如果计数值没变,说明请求线根本没拉起来,十有八九是I2C的DMA使能位没开。

4.3 应答信号——怎么知道搬完了?

DMA收到请求后,会通过总线读取/写入I2C_DR。这个操作本身就会清除I2C内部的请求标志(TXE或RXNE),所以不需要额外的应答信号——这是大多数MCU的做法。但有些芯片(比如NXP的LPC系列)有专用的DMA应答引脚,DMA完成一次传输后需要拉低该引脚,I2C才会产生下一个请求。

我记得在调试某款国产MCU时,数据手册上写“支持DMA传输”,但实际I2C的DMA请求根本不会自动清除。最后发现它需要软件手动清标志——这算是设计上的“坑”。

避坑:我曾经因为没注意应答模式,让I2C连续产生了两次请求。DMA从机端收到了两个字节,但主机只发了一个,结果总线卡住,SCL被拉低超时。解决办法是:在DMA传输完成中断里,一定要等STOP条件生成后再关闭DMA请求。

4.4 硬件连接流程:一个典型的DMA+I2C传输过程

下面这张图展示了I2C从机接收时,DMA控制器与I2C外设之间的信号交互。注意,在实际芯片里,这些信号是在一个时钟周期内完成的,别把它当成“步骤式”的握手。

DMA与I2C硬件请求/应答流程 I2C外设 I2C_DR (数据寄存器) DMA控制器 DMA通道 (CH6/CH7) 系统总线 DMA请求 (TXE/RXNE) DMA应答(部分MCU) 说明: ① I2C_DR数据就绪(TXE/RXNE)→ 硬件产生DMA请求 ② DMA控制器收到请求 → 通过系统总线访问I2C_DR → 自动清除请求 (常用MCU:STM32、GD32、NXP LPC系列)

4.5 硬件接口的寄存器配置要点

要真的让DMA和I2C“连起来”,你得在软件里干三件事:

  1. 使能I2C的DMA请求: 在I2C_CR2寄存器中,设置DMAEN位(发送和接收可以分开)。
  2. 配置DMA通道: 选择正确的通道号、外设地址(I2C_DR)、内存地址、数据宽度(通常是8位)、地址增量模式(内存增量,外设固定)。
  3. 设置DMA传输模式: 一般是普通模式(Normal)或循环模式(Circular),后者用于连续I2C主从通信。

举个例子,STM32上配置I2C1发送DMA的代码片段(HAL库风格):

__HAL_RCC_DMA1_CLK_ENABLE();
hdma_tx.Instance = DMA1_Channel6;
hdma_tx.Init.Direction = DMA_MEMORY_TO_PERIPH;
hdma_tx.Init.PeriphInc = DMA_PINC_DISABLE;
hdma_tx.Init.MemInc = DMA_MINC_ENABLE;
hdma_tx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
hdma_tx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
hdma_tx.Init.Mode = DMA_NORMAL;
HAL_DMA_Init(&hdma_tx);
__HAL_LINKDMA(&hi2c1, hdmatx, hdma_tx);
HAL_I2C_Master_Transmit_DMA(&hi2c1, slave_addr, pData, size);

我个人的习惯:调试阶段先把DMA中断打开(传输完成+半传输),用调试器看剩余计数值(CNDTR寄存器)。如果它没减少,说明DMA根本没进入服务——再看I2C的DMA请求使能位有没有被总线错误自动清零。很多MCU在I2C NACK后会清除所有DMA使能位,这是个隐藏的“坑”。

4.6 多请求冲突:DMA优先级与总线仲裁

当多个外设同时请求DMA时(比如I2C和SPI同时传输),DMA控制器内部有一个优先级仲裁器。通常会分成四个等级:最高、高、中、低。I2C这种实时性要求高的场景,我建议至少配到“高”优先级,否则数据可能来不及搬,导致I2C时钟拉伸超时。

你想想看,I2C主机在拉低SCL时等着从机释放数据,如果DMA因为被其他传输阻塞,从机没法及时读写DR,SCL就会被一直拉低——整个总线就卡住了。嗯,这里要注意,DMA优先级不是越高越好,如果给得太高,会挤占CPU访问内存的带宽,导致实时任务调度抖动。