3. DMA控制器架构:常见DMA控制器结构
聊完了DMA的基础概念,咱们得深入看看它的“骨架”——控制器架构。说白了,CPU怎么把数据搬移任务交出去,全靠这个控制器。我前后接触过几种主流的DMA控制器,最典型的就是ARM系的PL330和Linux内核里的DMA engine框架。这两者代表了“硬件实现”和“软件抽象”两种思路。
3.1 PL330:硬件线程的“八爪鱼”
PL330是ARM公司推出的微DMA控制器,常用于手机SoC里。我记得第一次看它的手册时,头都大了。这玩意儿内部有多个独立的“微线程”,能同时处理多条传输。
核心特点:
- 多通道并行:通常支持2-8个通道,每个通道独立工作。
- 微程序控制:通道执行一套指令集,不是简单的寄存器操作。
- 低中断负载:一次程序描述传输一整段,完成后才产生一次中断。
看个简单的PL330传输编程流程。这代码不是我写的,是芯片驱动里的片段,我改了一点注释:
// 初始化一个传输微程序
// 从外设寄存器0x40010000搬1KB数据到内存0x80000000
PL330_INSTR instr[] = {
// 读操作:源地址递增,目的地址固定
{.opcode = LOAD, .src = 0x40010000, .dst = 0x80000000, .size = 1024, .inc = SRC_INC},
// 同步信号,告诉外设数据已读取
{.opcode = SEND_DMA, .dma_sig = 1},
// 等待外设允许写入
{.opcode = WAIT_DMA, .dma_sig = 2},
// 写操作
{.opcode = STORE, .src = 0x80000000, .dst = 0x40020000, .size = 1024, .inc = DST_INC},
// 结束,触发CPU中断
{.opcode = END, .irq = 1}
};
// 写地址到PL330的通道程序指针寄存器
*(volatile uint32_t *)(PL330_BASE + CH0_PC) = (uint32_t)&instr;
// 启动
*(volatile uint32_t *)(PL330_BASE + CH0_CTRL) = 1;
嗯,这里要注意一点。PL330的指令集虽然有“取指令、执行”的通用处理器特征,但它的核心任务只有数据搬移。我在实际调优时发现,当外设速度不匹配时,微程序的WAIT指令能帮CPU省掉很多轮询开销。
3.2 DMA engine:Linux内核里的“调度器”
PL330是硬件,那DMA engine呢?它是Linux内核里的一层抽象框架。你想想看,如果每个SoC的DMA控制器驱动都自己写,那上层应用和驱动程序就乱套了。DMA engine的作用就是统一接口,让其他驱动(比如SPI、MMC)能通过标准API发起DMA传输。
| 项目 | PL330(硬件视角) | DMA engine(软件视角) |
|---|---|---|
| 体系结构 | 独立硬件模块,多通道并发 | 内核子系统,管理多个DMA设备 |
| 编程模型 | 微指令序列,直接操作寄存器 | 描述符链表,通过async_tx API提交 |
| 多通道管理 | 硬件固定通道数 | 软件虚拟通道,可复用硬件通道 |
| 中断处理 | 每通道独立中断 | 统一中断处理,回调机制 |
| 应用场景 | 嵌入式裸机/RTOS | Linux内核驱动开发 |
说白了,DMA engine把“谁先传、怎么传”的决策权从CPU手里拿走了。我曾经在一个视频采集卡驱动里调过DMA engine,那感受特别深——只要描述符链串得好,CPU几乎不参与数据搬运。
实战小窍门:用DMA engine时,描述符的预分配池一定要做大。我遇到过因为描述符申请失败导致DMA传输断流的bug,后来改成“一次申请100个描述符+复用队列”就稳定了。
3.3 架构对比:哪种结构对CPU更友好?
很多刚入行的工程师问:PL330和DMA engine哪个好?其实没法简单说。我从三个维度给你拆解一下:
3.3.1 中断频率
PL330一次微程序执行完才中断一次,但微程序本身的执行是顺次的。如果需要高频率、小尺寸传输,每个微程序也要产生中断,中断频率和传输次数成正比。
DMA engine用描述符链表,可以一个中断处理完一整串传输。我算过,在同样的数据量下,DMA engine的中断次数只有PL330的1/5到1/10。这对CPU来说,省下的上下文切换时间非常可观。
3.3.2 通道利用率
PL330的通道是硬分配的。比如SPI用通道0,I2C用通道1,如果SPI停了,通道0就闲置。这有点浪费。
DMA engine通过软件调度,可以让多个外设共享一个硬件通道。我在项目里做过实验:把4个低速外设(温湿度、光感、加速度计)复用到一个硬件通道上,DMA engine自动排队,CPU负载基本没波动。
3.3.3 可维护性
PL330的微程序是写死的,改一次传输模式就得重新烧固件。碰上产品升级,很头疼。
DMA engine全在软件层,加个新的DMA传输类型,只要注册新设备就行。这灵活性,做过大型项目的都懂。
踩过的坑:DMA engine虽然灵活,但描述符链的结构体对齐要求很高。我以前在一个ARM64平台上没注意缓存对齐,结果DMA反复取到脏数据。之后所有描述符我都用cacheline对齐分配,再没出过类似问题。
3.4 实战图例:两种架构在系统中的位置
下面这张图很直白,把PL330(硬件路线)和DMA engine(软件抽象)放在整个系统中对比。你看,CPU在两者之间的位置完全不同:
你看,左侧PL330架构里,CPU直接面对硬件通道,外设硬件和DMA控制器紧密耦合。右侧DMA engine架构里,CPU只和软件描述符链打交道,下层硬件细节被完全屏蔽。从CPU负载角度看,右侧架构显然更省心——CPU提交一次任务,后面的事都是DMA engine自己调度。
3.5 我的选择建议
搞裸机或RTOS项目,PL330那套直接操作寄存器的方式更可控、响应更快。但如果是Linux系统,强烈建议用DMA engine。我在做车载信息娱乐系统时,遇到过PL330中断太频繁导致CPU满载的极端情况——那时候真想换成DMA engine的描述符链批量处理。
一句话总结:PL330是“可靠的老黄牛”,一次干一件事;DMA engine是“聪明的调度员”,把一堆事排队干完。选哪个,看你的系统对实时性和灵活性的需求权重。