ARM PrimeCell DMA 控制器:从架构到实战

做嵌入式驱动这么多年,我接触过不少 DMA 控制器。但 ARM PrimeCell DMA(通常叫 PL080 或 PL081)是我用得最顺手的。它不花哨,但很可靠。说白了,它就是一块专门负责数据搬运的硬件,能把 CPU 从繁琐的拷贝工作中解放出来。

你想想看,没有 DMA 的时候,CPU 得亲自把数据从外设搬到内存,或者从内存搬到外设。这就像让公司 CEO 亲自去复印文件——浪费资源。DMA 控制器就是那个“专职文员”,CPU 只需要告诉它“把这份数据从 A 搬到 B”,然后就可以去处理更重要的事了。

PrimeCell DMA 的核心架构

ARM PrimeCell DMA 控制器的内部结构,我习惯把它分成三个主要部分:

  • DMA 引擎:负责实际的数据搬运工作,包含 FIFO 缓冲和地址生成逻辑
  • 通道控制器:管理多个 DMA 通道的优先级、状态和中断
  • 寄存器接口:通过 AMBA AHB 总线与 CPU 通信,接收配置指令

我在项目中遇到过一个问题:某次调试时 DMA 传输总是不完整,查了半天才发现是 FIFO 深度配置错了。嗯,这里要注意——PrimeCell DMA 的 FIFO 深度是 4 个字,如果你传输的数据量小于这个值,一定要小心边界条件。

关键点:PrimeCell DMA 控制器通过 AHB 主接口访问内存和外设,通过 AHB 从接口接收 CPU 的配置。这种双接口设计让它既能独立工作,又能被灵活控制。

DMA 通道与请求线

DMA 通道,你可以把它想象成一条专用的数据高速公路。PrimeCell 通常提供 8 个通道(PL080)或 2 个通道(PL081)。每个通道都有自己的配置寄存器,可以独立设置源地址、目的地址、传输宽度和突发大小。

请求线是什么?它是外设向 DMA 控制器发出的“我需要数据”或“我有数据要给你”的信号。每个 DMA 通道可以连接一条或多条请求线。

通道编号 默认请求线 典型外设 传输方向
通道 0 DMA_REQ0 UART0 发送 内存→外设
通道 1 DMA_REQ1 UART0 接收 外设→内存
通道 2 DMA_REQ2 SPI0 发送 内存→外设
通道 3 DMA_REQ3 SPI0 接收 外设→内存
通道 4-7 DMA_REQ4-7 通用/保留 可配置

我曾经踩过一个坑:在配置 UART 的 DMA 传输时,把发送通道和接收通道的请求线接反了。结果数据全乱了。所以我的建议是——画好硬件连接图,再动手写代码。

个人经验:我习惯在初始化时先禁用所有通道,然后逐个配置。这样能避免通道间的干扰。另外,别忘了设置通道优先级——高优先级通道会抢占低优先级通道的传输机会。

三种 DMA 传输类型

DMA 传输类型,说白了就是数据从哪搬到哪。ARM PrimeCell 支持三种基本模式:

1. 内存到内存(Memory-to-Memory)

这种模式不需要外设请求线。DMA 控制器自己决定什么时候开始传输。常用于数据拷贝、缓冲区移动等场景。

// 内存到内存传输配置示例
DMA_Channel_Config config;
config.src_addr = (uint32_t)src_buffer;    // 源地址
config.dst_addr = (uint32_t)dst_buffer;    // 目的地址
config.transfer_size = 1024;               // 传输字节数
config.src_width = DMA_WIDTH_32;           // 32位宽度
config.dst_width = DMA_WIDTH_32;
config.burst_size = DMA_BURST_4;           // 4字突发
config.flow_control = DMA_FLOW_MEM_TO_MEM; // 内存到内存

DMA_Channel_Init(0, &config);
DMA_Channel_Enable(0);

我记得第一次用内存到内存 DMA 时,发现传输速度并没有想象中快。后来才明白——突发大小很关键。如果突发太小,DMA 频繁获取总线控制权,效率反而低。

2. 内存到外设(Memory-to-Peripheral)

这种模式用于向外设发送数据。比如通过 UART 发送一串字符,或者向 DAC 输出波形数据。外设通过请求线告诉 DMA:“我准备好了,给我数据。”

为什么会这样?因为外设的处理速度通常比内存慢。如果 DMA 一股脑把数据全推过去,外设根本来不及处理。请求线机制就是用来做“握手”的。

避坑指南:我曾经在配置内存到外设传输时,忘了设置源地址递增。结果 DMA 每次都从同一个地址读数据,外设收到的全是重复内容。记住:内存地址通常要递增,外设地址通常固定。

3. 外设到内存(Peripheral-to-Memory)

这种模式用于从外设接收数据。比如从 ADC 采集数据,或者从 UART 接收数据包。外设每准备好一个数据,就拉高请求线,DMA 就把数据搬到内存里。

你想想看,如果没有 DMA,CPU 得不停地轮询外设状态,或者响应中断。有了 DMA,CPU 可以安心睡觉,等数据搬完了再被叫醒。

// 外设到内存传输配置示例
DMA_Channel_Config config;
config.src_addr = (uint32_t)&UART0->DR;     // 外设数据寄存器地址
config.dst_addr = (uint32_t)rx_buffer;       // 内存缓冲区地址
config.transfer_size = 256;                  // 接收256字节
config.src_width = DMA_WIDTH_8;              // UART是8位
config.dst_width = DMA_WIDTH_8;
config.burst_size = DMA_BURST_1;             // 单字传输
config.flow_control = DMA_FLOW_PER_TO_MEM;   // 外设到内存
config.src_inc = DMA_ADDR_FIXED;             // 外设地址固定
config.dst_inc = DMA_ADDR_INCREMENT;         // 内存地址递增

DMA_Channel_Init(1, &config);
DMA_Channel_Enable(1);

三种传输类型的对比

传输类型 是否需要请求线 源地址模式 目的地址模式 典型应用
内存→内存 否 递增 递增 数据拷贝、缓冲区移动
内存→外设 是 递增 固定 UART发送、DAC输出
外设→内存 是 固定 递增 ADC采集、UART接收

DMA 传输的完整流程

下面这张图展示了 DMA 从配置到完成的完整流程。我画了很多次,每次给团队新人讲 DMA 时都会用到它。

DMA 传输完整流程图 CPU 配置 DMA 通道 设置源/目的地址 配置传输参数 使能 DMA 通道 等待外设请求线(外设模式) DMA 执行数据传输 注意 内存到内存模式跳过 “等待请求线”步骤 直接开始传输 提示 传输完成后触发中断 通知 CPU 处理数据

从图中你能看到,内存到内存模式是最简单的——配置完就能跑。而外设相关的模式需要等待请求线,这也是最容易出问题的地方。

调试技巧:我习惯在 DMA 传输完成后检查剩余传输计数寄存器。如果计数值不为零,说明传输被中断了或者配置有误。这个寄存器是排查问题的第一站。

嗯,关于 ARM PrimeCell DMA 控制器的基本概念,我们就聊到这里。记住三个核心点:通道是传输的载体,请求线是外设的“敲门砖”,传输类型决定了数据流向。把这些搞清楚了,后面的多通道传输和驱动开发就会顺畅很多。