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 → DMA | DMA_REQ_TX / RX | 发送/接收数据请求 |
| DMA → I2C | DMA_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外设之间的信号交互。注意,在实际芯片里,这些信号是在一个时钟周期内完成的,别把它当成“步骤式”的握手。
4.5 硬件接口的寄存器配置要点
要真的让DMA和I2C“连起来”,你得在软件里干三件事:
- 使能I2C的DMA请求: 在I2C_CR2寄存器中,设置DMAEN位(发送和接收可以分开)。
- 配置DMA通道: 选择正确的通道号、外设地址(I2C_DR)、内存地址、数据宽度(通常是8位)、地址增量模式(内存增量,外设固定)。
- 设置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访问内存的带宽,导致实时任务调度抖动。