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 传输完成后检查剩余传输计数寄存器。如果计数值不为零,说明传输被中断了或者配置有误。这个寄存器是排查问题的第一站。
嗯,关于 ARM PrimeCell DMA 控制器的基本概念,我们就聊到这里。记住三个核心点:通道是传输的载体,请求线是外设的“敲门砖”,传输类型决定了数据流向。把这些搞清楚了,后面的多通道传输和驱动开发就会顺畅很多。