4. DMA描述符与链表:描述符结构、链表模式、循环模式、硬件自动跳转
好,咱们今天来聊聊DMA描述符和链表。说实话,这块内容在嵌入式开发里特别重要,但也是很多人容易踩坑的地方。我当年刚接触DMA时,就被描述符的配置折腾了好几个通宵。
4.1 描述符结构:DMA的大脑
描述符是什么?说白了,它就是DMA控制器的一个“任务单”。每个描述符告诉DMA:你要从哪里搬数据,搬到哪去,搬多少,搬完了怎么办。
一个典型的DMA描述符结构大概长这样:
struct dma_descriptor {
uint32_t src_addr; // 源地址
uint32_t dst_addr; // 目标地址
uint32_t transfer_size; // 传输大小(字节)
uint32_t ctrl; // 控制字段
uint32_t next_desc; // 下一个描述符指针
uint32_t status; // 状态字段
};
每个字段都有讲究。源地址和目标地址不用多说,但要注意对齐问题。我在项目中遇到过,如果源地址不是4字节对齐,某些DMA控制器会直接罢工。
控制字段(ctrl)是个大杂烩,里面包含了:
- 传输方向(内存到外设,还是外设到内存)
- 数据宽度(8位、16位、32位)
- 中断使能(传输完要不要通知CPU)
- 链式使能(要不要跳转到下一个描述符)
关键点:描述符本身必须放在内存中,而且通常要求地址对齐到缓存行大小(一般是32字节或64字节)。否则,DMA从内存读取描述符时可能会读到脏数据。
4.2 链表模式:串起来的任务
单个描述符只能做一次传输。但实际项目中,我们往往需要连续传输多个数据块。这时候,链表模式就派上用场了。
链表模式的核心思想是:每个描述符里都有一个next_desc字段,指向下一个描述符的地址。DMA完成当前传输后,自动去读取下一个描述符,继续干活。
我习惯把描述符链表画成这样的结构:
描述符1 --> 描述符2 --> 描述符3 --> ... --> 描述符N --> NULL
最后一个描述符的next_desc设为NULL,表示传输结束。DMA看到NULL后,就会停止,并触发一个完成中断。
链表模式的好处很明显:
- 可以处理不连续的内存块
- CPU只需要配置一次,DMA自己跑完整个链表
- 适合大数据量传输,比如网络数据包、音频流
我的经验:配置链表时,一定要确保所有描述符都在DMA可访问的内存区域。我曾经把描述符放在DDR里,但DMA只能访问SRAM,结果DMA读到的全是0xDEADBEEF,排查了好久才发现是内存区域的问题。
4.3 循环模式:永不停歇的搬运工
有些场景下,我们希望DMA一直工作,比如音频播放、视频采集。这时候,循环模式就比链表模式更合适。
循环模式其实就是把链表的最后一个描述符的next_desc指回第一个描述符,形成一个环:
描述符1 --> 描述符2 --> 描述符3 --> ... --> 描述符N
^ |
|________________________________________________|
DMA跑完一圈后,自动回到开头,继续下一轮传输。CPU只需要在中断里更新描述符里的数据缓冲区,就能实现不间断的数据流。
循环模式有几个关键点要注意:
- 描述符数量要足够,避免CPU来不及更新缓冲区
- 每个描述符的缓冲区大小要合理,太小会导致中断频繁,太大则延迟增加
- 要处理好“生产者-消费者”关系,CPU和DMA不能同时操作同一个描述符
避坑指南:我曾经在音频项目中用过循环模式,结果声音断断续续。后来发现是描述符数量太少(只有2个),CPU更新缓冲区的速度跟不上DMA的消耗速度。解决方案很简单:把描述符增加到8个,问题就解决了。你想想看,缓冲区数量至少要是CPU处理延迟的2倍以上,才能保证不丢数据。
4.4 硬件自动跳转:DMA自己找活干
硬件自动跳转,是DMA控制器最酷的特性之一。它让DMA不需要CPU干预,就能自动从链表或循环中获取下一个描述符。
实现原理其实不复杂:
- DMA完成当前描述符的传输
- 检查当前描述符的next_desc字段
- 如果next_desc非空,DMA自动去内存读取下一个描述符
- 加载新描述符到内部寄存器
- 开始新的传输
整个过程完全由硬件完成,CPU可以安心睡觉或者干别的活。这也是DMA被称为“后台搬运工”的原因。
不同厂家的DMA控制器,自动跳转的实现略有差异:
| 厂家 | 自动跳转方式 | 特点 |
|---|---|---|
| TI(德州仪器) | EDMA3 | 支持三维传输,描述符可嵌套 |
| NXP(恩智浦) | eDMA | 支持TCD(传输控制描述符),可配置优先级 |
| Xilinx(赛灵思) | AXI DMA | 支持分散-聚集(Scatter-Gather)模式 |
| ST(意法半导体) | DMA2D | 专门用于图形加速,支持像素格式转换 |
嗯,这里要注意:硬件自动跳转虽然方便,但也有一些限制。比如,描述符更新后,DMA可能不会立即感知到。我记得在某个项目中,CPU更新了描述符的缓冲区地址,但DMA还在用旧的地址,导致数据写到了错误的位置。
解决办法是:在更新描述符后,执行一个内存屏障指令(如DSB),确保CPU的写操作已经到达内存,然后再通知DMA重新加载描述符。
总结一下:描述符是DMA的任务单,链表模式适合一次性传输多个数据块,循环模式适合持续的数据流,硬件自动跳转让DMA能自己找活干。理解这些概念,你就能写出高效、可靠的DMA驱动。
我个人建议,刚开始学习时,先从单描述符开始,跑通后再尝试链表模式,最后再上循环模式。一步一个脚印,别想着一步登天。我在带新人时,经常看到他们一上来就搞复杂的循环模式,结果出了问题根本不知道从哪排查。
好了,这一章的内容就到这里。下一章我们会聊聊DMA中断处理与性能优化,到时候我会分享一些实际项目中的调优经验。