第3章:DMA传输类型详解
DMA传输类型这个话题,说实话刚入行时我也觉得挺绕的。四种类型:内存到内存、内存到外设、外设到内存、外设到外设。每种都有它的脾气,用错了地方,轻则性能下降,重则系统崩溃。我踩过的坑不少,今天跟你好好聊聊。
3.1 内存到内存(Memory-to-Memory)
这种模式说白了就是搬数据。从内存A搬到内存B,CPU不用插手。听起来简单,但坑也不少。
核心特点:
- 源和目标都是系统内存
- 不需要外设参与
- 常用于数据拷贝、缓冲区整理
我在项目中遇到过这么个事:用DMA做内存拷贝,结果发现比CPU memcpy还慢。为什么?因为DMA需要总线仲裁,小数据量时开销反而大。一般建议数据量超过64字节才用DMA。
我的经验:内存到内存DMA适合大块数据搬运,比如视频帧、音频缓冲区。小数据就别折腾了,CPU直接拷贝更快。
设备树里怎么配?看个例子:
// 内存到内存DMA通道配置
dma_chan: dma@ff000000 {
compatible = "myvendor,dma-controller";
reg = <0x0 0xff000000 0x0 0x1000>;
interrupts = <0 56 4>;
#dma-cells = <3>;
dma-channels = <8>;
// 重点:mem2mem通道需要特殊标记
channel0 {
dma-type = "mem-to-mem";
max-burst-size = <64>;
};
};
3.2 内存到外设(Memory-to-Peripheral)
这个场景最常见。CPU把数据准备好,DMA负责送到外设。比如往UART发数据、往SPI写数据、往LCD送显示内容。
你想想看,如果每次发一个字节都要CPU去写寄存器,那CPU啥也别干了。DMA就是干这个的。
| 外设类型 | 典型应用 | 注意事项 |
|---|---|---|
| UART | 串口发送数据 | 注意FIFO深度,避免溢出 |
| SPI | 写Flash、写DAC | CS信号管理要小心 |
| I2S | 音频播放 | 需要环形缓冲区 |
| LCD | 显示刷新 | 带宽要求高,注意优先级 |
我曾经踩过的坑:做SPI Flash写入时,DMA传输完数据但CS信号没及时拉高,导致数据错位。后来加了DMA传输完成中断,在中断里处理CS信号才解决。
设备树配置示例:
// UART的DMA发送通道
uart0: serial@ff010000 {
compatible = "myvendor,uart";
reg = <0x0 0xff010000 0x0 0x1000>;
interrupts = <0 52 4>;
dmas = <&dma_chan 0 0x10 0x1>;
dma-names = "tx";
// 0x10表示外设地址,0x1表示内存到外设方向
};
3.3 外设到内存(Peripheral-to-Memory)
这个方向跟上面相反。外设产生数据,DMA负责搬到内存。比如ADC采样、摄像头采集、网络数据接收。
我个人习惯把这种模式叫做「数据收割机」。外设就像庄稼,DMA就是收割机,CPU只管最后处理粮食就行。
关键点:
- 外设产生数据的速度要匹配DMA带宽
- 内存缓冲区要足够大,防止数据丢失
- 中断处理要及时,避免覆盖
我记得有个项目做高速ADC采集,采样率10Msps,每次采样16位。算下来每秒20MB数据。如果不用DMA,CPU根本扛不住。用了DMA后,CPU只需要每毫秒处理一次缓冲区数据就行。
避坑指南:外设到内存DMA,一定要用环形缓冲区(ring buffer)。我曾经用线性缓冲区,结果DMA写满了还没处理,新数据直接覆盖旧数据,查了半天才发现。
设备树配置:
// ADC的DMA接收通道
adc0: adc@ff020000 {
compatible = "myvendor,adc";
reg = <0x0 0xff020000 0x0 0x1000>;
interrupts = <0 60 4>;
dmas = <&dma_chan 1 0x20 0x2>;
dma-names = "rx";
// 0x20表示外设地址,0x2表示外设到内存方向
dma-buffer-size = <0x10000>; // 64KB环形缓冲区
};
3.4 外设到外设(Peripheral-to-Peripheral)
这个模式用得最少,但某些场景下特别有用。比如从ADC直接送到DAC,或者从摄像头直接送到编码器。
说白了就是外设之间直接对话,CPU和内存都不参与。数据流就像水管一样,从源头直接流到目的地。
注意:不是所有DMA控制器都支持外设到外设模式。我遇到过一款芯片,手册上写着支持,实际调试时发现总线仲裁有问题,数据总是丢。最后只能改成外设到内存再到外设的两步走方案。
适用场景:
- 音频直通(ADC→DAC)
- 视频格式转换(摄像头→编码器)
- 数据协议转换(SPI→UART)
设备树配置示例:
// 外设到外设DMA配置
dma_chan: dma@ff000000 {
// 需要两个外设地址
channel2 {
dma-type = "per-to-per";
src-periph = <0x20>; // ADC地址
dst-periph = <0x30>; // DAC地址
max-burst-size = <32>;
// 注意:两个外设的时钟要同步
};
};
3.5 如何选择传输类型
嗯,这里我总结一下我的选择原则:
- 数据量大、不需要处理 → 内存到内存DMA
- CPU产生数据,外设消费 → 内存到外设DMA
- 外设产生数据,CPU消费 → 外设到内存DMA
- 外设之间直接传输 → 外设到外设DMA(慎用)
我的建议:刚开始做DMA驱动时,先从内存到外设和外设到内存入手。这两种最常用,也最容易调试。等熟悉了再碰外设到外设。
最后说一句:设备树里配置DMA时,一定要仔细看芯片手册。每个DMA控制器的dma-cells含义都不一样,有的用2个参数,有的用3个,甚至4个。搞错了,驱动根本跑不起来。
好了,这四种传输类型就聊到这儿。下一章我们讲讲DMA描述符链,那个才是真正体现DMA威力的地方。