4、DMA控制器基本原理归纳总结
说实话,我见过不少工程师把DMA当成一个“黑盒子”——配置好寄存器,数据就自动搬完了。但一旦CAN FD的报文频率上来,或者数据长度超过64字节,那个黑盒子就会突然失效。我个人觉得,理解DMA控制器内部的工作原理,是做好CAN FD传输的第一步。下面我把核心逻辑拆开讲,结合我这些年踩过的坑。
4.1 DMA控制器的核心骨架
一个标准DMA控制器,说白了包含四个部件:请求仲裁器、传输控制逻辑、地址发生器和传输计数器。它们之间通过内部总线连接,再通过AHB/APB桥接到系统总线。像我最早用STM32F4时,它的DMA1有8个流,每个流对应一个通道,其实就是一套完整的独立“搬运工”。
核心思想:DMA就是把CPU从重复的数据搬移中解放出来。CPU只管下发任务(配置DMA),然后去干别的,DMA自己完成一遍又一遍的搬运。
我的经验:一开始我总搞混“流(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(字节模式) | 8 | 16 |
| 循环模式 | 支持 | 支持(TCD.citer) | 支持 |
| 链式DMA | 支持(流级) | 支持(TCD链接) | 支持(触发链) |
| CAN FD支持情况 | 需手动配置FIFO阈值 | 需配合eDMA分散/收集 | 内置CAN DMA通道(部分型号) |
实用建议:无论用哪家的芯片,先把CAN FD的FIFO深度和DMA的FIFO阈值对齐。我习惯在初始化时打印DMA的当前配置寄存器(读回影子寄存器),确认硬件真的接受了我设置的参数。别问我为什么——因为手册有时候也写错。