4、DMA控制器基本原理归纳总结

说实话,我见过不少工程师把DMA当成一个“黑盒子”——配置好寄存器,数据就自动搬完了。但一旦CAN FD的报文频率上来,或者数据长度超过64字节,那个黑盒子就会突然失效。我个人觉得,理解DMA控制器内部的工作原理,是做好CAN FD传输的第一步。下面我把核心逻辑拆开讲,结合我这些年踩过的坑。

4.1 DMA控制器的核心骨架

一个标准DMA控制器,说白了包含四个部件:请求仲裁器、传输控制逻辑、地址发生器和传输计数器。它们之间通过内部总线连接,再通过AHB/APB桥接到系统总线。像我最早用STM32F4时,它的DMA1有8个流,每个流对应一个通道,其实就是一套完整的独立“搬运工”。

核心思想:DMA就是把CPU从重复的数据搬移中解放出来。CPU只管下发任务(配置DMA),然后去干别的,DMA自己完成一遍又一遍的搬运。

DMA控制器内部结构框图 请求仲裁器 优先级裁决 传输控制逻辑 状态机 + 配置 地址发生器 源/目的地址 传输计数器 CNDTR 递减 状态寄存器 TC/HT/ERR 外设请求 CAN RX / Timer 系统总线 (AHB / APB)

我的经验:一开始我总搞混“流(stream)”和“通道(channel)”。其实一个流可以响应对应的通道请求,而通道仅仅是外设与DMA之间的“接口编号”。记住:配置时选对通道,否则DMA根本收不到CAN的请求信号。

4.2 DMA请求与仲裁——谁先上车?

DMA请求来源主要有两种:硬件请求(外设触发,比如CAN RX FIFO非空)和软件请求(写寄存器手动启动)。当多个请求同时到达,仲裁器按照优先级裁决。优先级可以是固定的(通道0最高),也可以是可编程的(循环优先、每轮固定)。

你想想看,CAN FD接收中断频率可能很高,如果DMA被其他低优先级外设占着通道,RX FIFO就会溢出。我建议你在项目里给CAN分配最高硬件优先级,除非有更紧急的实时任务(比如定时器捕获)。

触发方式典型场景优先级设置建议
硬件请求CAN RX、ADC完成、定时器更新高(0或1)
软件请求测试、初始化搬运低(由仲裁器分配)
链式请求一个DMA完成启动另一个DMA取决于链式配置

4.3 传输模式:单次、突发与循环

DMA通常支持三种传输模式,我分别说说我的看法:

  • 单次传输(Single Transfer):每次请求只搬1个数据单元(按配置的宽度)。简单,但总线利用率低。适合外设速率慢的场景,比如GPIO电平读取。
  • 突发传输(Burst Transfer):一次请求连续搬多个数据单元(如4次、8次)。总线占用时间长,但吞吐量高。CAN FD的64字节数据负载推荐用突发模式,否则每收4字节就得重新仲裁一次,浪费带宽。
  • 循环传输(Circular / Auto‑reload):传输完成后地址和计数器自动回到初始值,无限循环。常用于周期性采样或双缓冲。我有个同事在CAN FD接收用了循环模式,结果忘了清标志,数据覆盖了——切记检查溢出中断。

⛔ 避坑提醒:我曾经在NXP S32K上配置CAN FD DMA时,突发长度选了16,结果DMA始终不触发。后来发现该型号DMA突发最大支持8拍,超出就静默不工作。一定要查手册中的突发上限。

4.4 关键参数配置技巧

DMA配置的核心寄存器就那么几个,但参数之间互相制约。我把常见的配置参数罗列出来,你配置时可以对照检查:

  • 源地址与目的地址:必须指向数据寄存器的地址(CAN FD的DLC字段后紧接数据)。建议用指针强制转换,不要硬编码。
  • 传输宽度:字节(8bit)、半字(16bit)、字(32bit)。CAN FD数据只能按字节存取,所以宽度选8bit。如果选了32bit,一次搬4字节,接收缓冲区字节序会错乱。
  • 地址递增模式:外设侧通常固定(只访问一个数据寄存器),内存侧递增。配置错误会导致所有数据写进同一个地址——那就是灾难。
  • 数据块大小:即传输次数。对于CAN FD,次数 = 报文数据长度(0~64)。例如接收8字节报文,设置NDTR = 8。

这里有一个我常用的初始化代码片段(基于STM32 HAL):

// CAN FD接收DMA配置(常规模式)
DMA_HandleTypeDef hdma_can_rx;
hdma_can_rx.Instance = DMA1_Stream0;
hdma_can_rx.Init.Request = DMA_REQUEST_CAN1_RX;
hdma_can_rx.Init.Direction = DMA_PERIPH_TO_MEMORY;
hdma_can_rx.Init.PeriphInc = DMA_PINC_DISABLE;
hdma_can_rx.Init.MemInc = DMA_MINC_ENABLE;
hdma_can_rx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
hdma_can_rx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
hdma_can_rx.Init.Mode = DMA_NORMAL;   // 接收完停止
hdma_can_rx.Init.Priority = DMA_PRIORITY_HIGH;
hdma_can_rx.Init.FIFOMode = DMA_FIFOMODE_ENABLE;
hdma_can_rx.Init.FIFOThreshold = DMA_FIFO_THRESHOLD_FULL;
// 别忘了使能传输完成中断
HAL_DMA_Init(&hdma_can_rx);

嗯,这里要注意FIFO模式:如果开启FIFO且阈值设得太低,DMA会频繁触发总线传输,反而增加延迟。对于CAN FD这种64字节一次收完的,建议把FIFO阈值设为Full(即满才搬),这样一次性完成。

4.5 中断与完成通知

DMA传输完成后,硬件会置位一个标志位(如TCIF),如果使能了对应的中断,就会进入回调。在CAN FD应用中,你通常需要在传输完成中断里做:

  • 更新接收缓冲区的索引
  • 重新配置下一次DMA接收(如果需要连续接收)
  • 检查传输计数器是否为零,防止漏数据

我记得有一次调试,DMA中断进了两次,但CAN只发了一帧。后来发现是半传输中断(HTIF)和传输完成中断(TCIF)都使能了,而我在半传输中断里也做了完整的处理——导致数据被处理了两遍。建议只使能TCIF,或者明确区分半/全逻辑。

总结:DMA控制器的基本原理其实不复杂——请求-仲裁-地址产生-计数-完成标志。但每个环节都有“陷阱”,特别是与CAN FD这种突发性高、数据量可变的场景结合时。建议你在调试新平台时,先用一个固定长度的报文(比如8字节)验证DMA通道是否通,再上64字节突发。这样能快速隔离问题。

4.6 不同MCU的DMA差异速查

不同厂家的DMA实现略有差异,下面是我整理的一个小表格,方便快速参考:

特性STM32 (DMA1/2)NXP S32K (eDMA)Infineon TC3xx (DMA)
通道/流结构流 + 通道通道 + TCD传输引擎 + 通道
最大突发长度16(字节模式)816
循环模式支持支持(TCD.citer)支持
链式DMA支持(流级)支持(TCD链接)支持(触发链)
CAN FD支持情况需手动配置FIFO阈值需配合eDMA分散/收集内置CAN DMA通道(部分型号)

实用建议:无论用哪家的芯片,先把CAN FD的FIFO深度和DMA的FIFO阈值对齐。我习惯在初始化时打印DMA的当前配置寄存器(读回影子寄存器),确认硬件真的接受了我设置的参数。别问我为什么——因为手册有时候也写错。