2. DMA传输模式:单次、块、链式
聊DMA传输,第一步得搞清楚它怎么把数据搬来搬去。不同的搬法,对应不同的硬件开销、实时性和灵活性。我根据自己调通过的几个项目,把最常见的三种模式拆出来讲讲:单次传输、块传输、链式传输。
说白了,它们就像快递的三种配送方式:单次就是一单送一件、送完就收工;块传输是把一堆货打包成一个大包一次性送;链式传输呢,更像快递员手里拿着一张任务清单,送完一单自动跳到下一单。你会用哪种?取决于你的场景。
2.1 单次传输(Single Transfer)
这是最基础的DMA模式。CPU配好源地址、目的地址、传输长度,然后DMA控制器把数据从A搬到B,搬完就停,发个中断告诉CPU“活干完了”。
我在早期的项目中用过它,比如从ADC读取单次采样结果。那时候系统负载低,来一次数据搬一次,简单粗暴。但你想想看,如果每秒要搬几千次,CPU就得频繁处理中断,效率反而打折扣。
// 伪代码:单次传输配置
DMA_Channel->SourceAddr = &ADC_DataReg;
DMA_Channel->DestAddr = &Buffer[0];
DMA_Channel->Length = 1; // 只传输1个字
DMA_Channel->Mode = SINGLE;
DMA_Start(DMA_Channel);
// 等待传输完成中断
while(!DMA_IsComplete());
| 参数 | 说明 | 坑 |
|---|---|---|
| 源/目的地址 | 必须对齐(多数DMA要求字对齐) | 我曾经用未对齐地址导致DMA死机,查了半天 |
| 传输长度 | 通常为1~65535个字/字节 | 小心长度0,有些控制器会误触发中断 |
| 中断 | 传输完成或错误时触发 | 不要在中断里做重配置,容易丢数据 |
2.2 块传输(Block Transfer)
块传输,顾名思义,是一次搬运一大块连续数据。比如从内存搬运一帧图像到LCD显存,或者把一批传感器数据搬进缓存。
它的优势很明显:一次配置,DMA自己连续地按字/字节搬完整个块,CPU全程不参与。搬完后才发中断。效率比单次高一个数量级,因为减少了中断次数和配置开销。
我记得在给一个高速数据采集系统做优化时,原来代码每来一个数据就触发一次中断,CPU占用率飙到70%。换成块传输后,攒够512个点再触发一次中断,CPU占用直接降到3%。
// 伪代码:块传输配置 (以STM32为例)
DMA_Channel->SourceAddr = (uint32_t)&ADC_Buffer;
DMA_Channel->DestAddr = (uint32_t)&SRAM_Buffer;
DMA_Channel->Length = 512; // 一次搬512个半字
DMA_Channel->Mode = BLOCK; // 块模式
DMA_Channel->BurstSize = 4; // 突发传输,提升总线效率
DMA_Start(DMA_Channel);
// DMA自动搬完,然后触发完成中断
| 对比项 | 单次传输 | 块传输 |
|---|---|---|
| CPU参与度 | 每次传输后中断 | 整块完成后中断一次 |
| 适合数据量 | 1~几十个字 | 几十~几千个字 |
| 中断频率 | 高,易造成抖动 | 低,系统响应更平滑 |
| 总线占用 | 分散、短 | 集中、长(可能影响其他主设备) |
2.3 链式传输(Linked List / Scatter-Gather)
这才是真正的“自动挡”。链式传输允许你提前准备一个描述符链表,每个描述符包含一次传输的参数:源地址、目的地址、长度、下一个描述符指针。DMA会按链表顺序自动执行所有传输,直到遇到链表尾。
为什么需要它?举个例子:你要从三个不连续的物理缓冲区(比如A、B、C)搬数据到连续的内存区域。如果用块传输,你必须先手动把A、B、C拷贝到一起再搬,效率很低。链式传输可以逐段描述,DMA自己跳转地址,把三次搬整合成一条“流水线”。
// 伪代码:链式传输描述符(以TI的EDMA3为例)
struct DMA_Descriptor desc1, desc2, desc3;
desc1.src = &Buffer_A;
desc1.dst = &Dest_Start;
desc1.len = 128;
desc1.next = &desc2; // 指向下一个描述符
desc2.src = &Buffer_B;
desc2.dst = &Dest_Start + 128;
desc2.len = 64;
desc2.next = &desc3;
desc3.src = &Buffer_C;
desc3.dst = &Dest_Start + 192;
desc3.len = 256;
desc3.next = NULL; // 链表结束
DMA_SubmitChain(&desc1); // 从第一个描述符开始
DMA_Start(); // DMA自动遍历链表
链式传输的另一个好处是零CPU干预:CPU只需要一开始提交链表,之后可以做其他任务。链表还可以动态更新(比如加一个描述符),实现“热插拔”式的传输任务。
我个人的经验是:在需要频繁更换DMA传输参数(比如音频播放、网络数据包收发)的场景下,链式传输几乎是标配。比如一个I2S音频流,你可能要连续播放多个不同的音频段,每段起始地址、长度都不一样,用链式传输配置好链表后,DMA自己就能无缝切换,连中断都省了。
2.4 三种模式对比 & 知识结构图
下面这张SVG图帮你理清这三种模式的本质差别:
看完这张图,你应该能快速判断:如果你的数据是一锤子买卖,用单次;如果是连续的大块数据,用块传输;如果数据来源零散、需要批量处理且不想频繁唤醒CPU,果断上链式。