第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 如何选择传输类型

嗯,这里我总结一下我的选择原则:

  1. 数据量大、不需要处理 → 内存到内存DMA
  2. CPU产生数据,外设消费 → 内存到外设DMA
  3. 外设产生数据,CPU消费 → 外设到内存DMA
  4. 外设之间直接传输 → 外设到外设DMA(慎用)

我的建议:刚开始做DMA驱动时,先从内存到外设和外设到内存入手。这两种最常用,也最容易调试。等熟悉了再碰外设到外设。

最后说一句:设备树里配置DMA时,一定要仔细看芯片手册。每个DMA控制器的dma-cells含义都不一样,有的用2个参数,有的用3个,甚至4个。搞错了,驱动根本跑不起来。

好了,这四种传输类型就聊到这儿。下一章我们讲讲DMA描述符链,那个才是真正体现DMA威力的地方。