4. DMA传输模式分类:单次、突发、循环、链式

好,咱们进入第4章。前面聊了DMA的基本概念和配置流程,现在该聊聊传输模式了。这四种模式——单次、突发、循环、链式,说实话是我在实际项目中每天都要打交道的。选对了,整个系统跑得飞快;选错了,等着你的就是各种奇怪问题。

我先把它们做个快速对比,心里有个数,再一个个掰开讲。

模式 核心特点 典型场景 我遇到过的问题
单次传输 一次请求传输一个数据单位 低速外设(如UART) 频繁打断CPU,吞吐上不去
突发传输 一次请求传输一批数据 高速外设(如ADC、高速SPI) 总线被长时间占用,其他任务饿死
循环传输 传输完成后自动从头开始 音频流、连续采样 溢出后数据错位,很难查
链式传输 多个描述符接力执行 多通道、复杂数据流 链表指针写错,系统直接挂掉

好,我们从最基础的单次传输开始。

4.1 单次传输(Single Transfer)

说白了,就是每次DMA请求只搬运一个数据单位。你想想看,外设说“我需要一个数据”,DMA就去搬一个。搬完,收工。

它的特点很明显:

  • 简单粗暴:配置量最少,我一般调试新外设时先用这个模式。
  • 对系统总线友好:每次只占一个总线周期,不会长时间锁定总线。
  • 传输效率低:每次请求都有开销,搬大量数据时比较吃亏。

我曾在一个项目中用单次模式给UART做DMA接收。串口速度才115200波特率,单次模式完全够用。但后来换成高速SPI(10Mbps以上),单次模式就扛不住了,CPU频繁响应DMA中断,效率直线下降。

提示:我刚入行时总喜欢用单次传输,觉得简单。但后来发现,对于大量数据传输的场景,单次模式会频繁触发传输完成中断,而这个中断的开销有时比CPU直接搬运还要大。所以,如果发现CPU负载莫名其妙升高,先查查DMA是不是用了单次模式。

代码上怎么体现呢?

// 单次传输配置示例(伪代码)
dma_channel_config_t config;
config.transfer_mode = DMA_SINGLE_TRANSFER;  // 单次传输
config.src_addr = &periph_data_register;
config.dst_addr = &ram_buffer;
config.data_size = 1;                        // 每次1个数据单位
config.block_size = 100;                     // 总共100个

dma_channel_init(DMA_CH0, &config);

嗯,这里要注意data_size和block_size的区别。data_size是每次传输的单位大小,block_size是总共要传多少个单位。单次模式下,这两者分开配置,灵活性很高。

4.2 突发传输(Burst Transfer)

好,接下来是突发传输。这个模式跟单次传输正好相反——每次DMA请求,它一口气搬运多个数据单位。我习惯称之为“批量搬运”。

你想想看,如果外设的FIFO深度是16个word,那用突发传输一次搬16个,是不是比单次搬16次要爽快得多?

突发传输的核心参数:

  • 突发大小(Burst Size):每次搬运多少个数据单位,通常为4、8、16等。
  • 突发长度(Burst Length):一次突发传输中,连续传输的总字节数。
  • 总线占用时间:突发传输期间,总线被DMA独占。

我在做高速ADC采集时遇到了一个典型问题。ADC采样率高达1Msps,用单次传输,CPU几乎被DMA中断淹没了。后来改成突发传输,每次突发64个采样值,CPU负载一下就降下来了。但别高兴太早——突发传输有个大坑。

警告:我曾经在一个实时性要求很高的系统中吃过亏。突发传输一旦开始,会连续占用总线多个周期。这时如果CPU有紧急中断要处理(比如外部触发信号),可能因为拿不到总线而延迟响应。所以,如果系统中有多个高优先级中断,突发大小别设得太大。我一般建议不超过16个数据单位。

突发传输配置示例:

// 突发传输配置
dma_channel_config_t config;
config.transfer_mode = DMA_BURST_TRANSFER;
config.burst_size = 16;                      // 每次突发16个数据单位
config.src_addr = &adc_fifo_register;
config.dst_addr = &adc_buffer;
config.data_size = 2;                        // 每个数据2字节(16位ADC)
config.block_size = 1024;                    // 总共1024个采样值

dma_channel_init(DMA_CH1, &config);

4.3 循环传输(Circular / Auto-reload Transfer)

这个模式很有意思。传输完成后,它会自动从起始地址重新开始。说白了,就是一个环形缓冲区。

为什么需要循环传输?

做音频输出或连续数据采集时,你希望DMA一直跑,不要停。每次传输完了又重新从头开始,形成一个永不结束的循环。这样CPU只需要在缓冲区满了之后去取数据,不用每次都参与DMA的启动和停止。

我个人习惯在以下场景使用循环传输:

  • 音频播放:持续将内存中的音频数据送进DAC。
  • 连续ADC采集:例如电力监测,需要不间断地采集电压电流。
  • 传感器数据流:陀螺仪、加速度计的连续输出。

有一次,我在做一个音频处理项目,用循环模式给DMA跑音频数据。一开始没注意缓冲区大小的设置,设得太小了。结果数据还没处理完,新的数据就覆盖了旧的,产生了严重的爆音。你想想看,整整花了两天时间才定位到这个问题。

经验:使用循环传输时,一定要确保缓冲区足够大,给CPU留出足够的时间去处理数据。一般建议缓冲区的深度至少是CPU处理周期的2倍以上。另外,别忘了开启“半传输中断”或“阈值中断”,这样可以在数据刚写满一半时就通知CPU来处理,避免溢出。

循环传输配置:

// 循环传输配置
dma_channel_config_t config;
config.transfer_mode = DMA_CIRCULAR_TRANSFER; // 循环模式
config.src_addr = &dac_data_register;
config.dst_addr = &audio_buffer;
config.buffer_size = 4096;                   // 缓冲区大小
config.data_size = 2;                        // 16位音频数据

// 开启半传输中断
dma_enable_interrupt(DMA_CH2, DMA_INT_HALF_TRANSFER);
dma_enable_interrupt(DMA_CH2, DMA_INT_FULL_TRANSFER);

dma_channel_init(DMA_CH2, &config);

4.4 链式传输(Linked-list / Chained Transfer)

这个模式是四种模式中最复杂,也是最灵活的。它使用一组描述符(Descriptor)来定义多个传输任务,每个描述符包含了传输的源地址、目的地址、数据长度等信息。运行时,DMA先执行描述符1,完成后自动跳转到描述符2,以此类推。

说白了,就是给DMA编排了一个任务流水线。

链式传输的典型应用:

  • 多通道数据采集:每个通道对应一个描述符,自动轮询。
  • 复杂数据搬移:数据需要从多个源地址搬移到多个目的地址。
  • 双缓冲(Ping-Pong Buffer):描述符1指向缓冲A,描述符2指向缓冲B,交替使用。

我第一次用链式传输时,感觉特别爽。但配置链式传输很容易出错,我举个例子——描述符中指向下一个描述符的指针写错了。我曾经因为这个问题,让整个系统跑飞了。你想想看,DMA执行完一个描述符后,跳到了一个未知的地址,后果是什么?设备直接死机。

链式传输的关键点:描述符必须在内存中对齐,且不能被编译器优化掉或放在会被覆盖的区域。另外,最后一个描述符的Next指针必须指向NULL或第一个描述符,否则DMA会直接停下。

链式传输配置:

// 定义两个描述符,实现双缓冲
dma_descriptor_t desc1 = {
    .src_addr = &adc_data1,
    .dst_addr = &buffer_a,
    .count = 256,
    .next_desc = &desc2        // 指向下一个描述符
};

dma_descriptor_t desc2 = {
    .src_addr = &adc_data1,
    .dst_addr = &buffer_b,
    .count = 256,
    .next_desc = &desc1        // 指向第一个描述符,形成循环
};

dma_config_linked_list(DMA_CH3, &desc1);
dma_channel_enable(DMA_CH3);
DMA传输模式对比与选择 单次传输 突发传输 循环传输 链式传输 特点 • 每次1个数据单位 • 简单、对总线友好 • 低速外设适用 特点 • 批量搬运数据 • 总线占用时间长 • 高速外设适用 特点 • 自动循环重启 • 用于连续数据流 • 需防溢出 特点 • 多描述符接力 • 实现任务流水线 • 最复杂、最灵活 如何选择? 低速外设(如UART)→ 单次传输 高速外设(如ADC)→ 突发传输 连续数据流(如音频)→ 循环传输 复杂多通道场景 → 链式传输

链式传输是真正考验嵌入式工程师功力的地方。它的配置虽然复杂,但灵活性也是四个模式中最高的。我记得有一次,要做8通道的ADC采集,每个通道的采样率和数据格式都不一样。如果用循环传输,你得配置很多个DMA通道。但用链式传输,你只需要一个通道,然后定义8个描述符,每个描述符指向不同的配置。完美解决。

再次提醒:写链式传输的描述符时,务必要用volatile关键字修饰描述符结构体,并且确保描述符在内存中的物理地址是连续的。我吃过这个亏——描述符被编译器优化后,Next指针被放到了奇怪的地方,导致DMA执行到一半跳到了错误地址。排查了一整天才发现是编译器优化惹的祸。

好了,四种模式都讲完了。你会发现,单次和循环是一对“好基友”——一个最简、一个最自动化;突发和链式则更像“进阶选手”——一个拼速度、一个拼灵活。理解了它们的本质,你在实际项目中就能信手拈来,快速找到最适合的方案。