4. DMA传输模式分类:单次、突发、循环、链式
好,咱们进入第4章。前面聊了DMA的基本概念和配置流程,现在该聊聊传输模式了。这四种模式——单次、突发、循环、链式,说实话是我在实际项目中每天都要打交道的。选对了,整个系统跑得飞快;选错了,等着你的就是各种奇怪问题。
我先把它们做个快速对比,心里有个数,再一个个掰开讲。
| 模式 | 核心特点 | 典型场景 | 我遇到过的问题 |
|---|---|---|---|
| 单次传输 | 一次请求传输一个数据单位 | 低速外设(如UART) | 频繁打断CPU,吞吐上不去 |
| 突发传输 | 一次请求传输一批数据 | 高速外设(如ADC、高速SPI) | 总线被长时间占用,其他任务饿死 |
| 循环传输 | 传输完成后自动从头开始 | 音频流、连续采样 | 溢出后数据错位,很难查 |
| 链式传输 | 多个描述符接力执行 | 多通道、复杂数据流 | 链表指针写错,系统直接挂掉 |
好,我们从最基础的单次传输开始。
4.1 单次传输(Single Transfer)
说白了,就是每次DMA请求只搬运一个数据单位。你想想看,外设说“我需要一个数据”,DMA就去搬一个。搬完,收工。
它的特点很明显:
- 简单粗暴:配置量最少,我一般调试新外设时先用这个模式。
- 对系统总线友好:每次只占一个总线周期,不会长时间锁定总线。
- 传输效率低:每次请求都有开销,搬大量数据时比较吃亏。
我曾在一个项目中用单次模式给UART做DMA接收。串口速度才115200波特率,单次模式完全够用。但后来换成高速SPI(10Mbps以上),单次模式就扛不住了,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负载一下就降下来了。但别高兴太早——突发传输有个大坑。
突发传输配置示例:
// 突发传输配置
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跑音频数据。一开始没注意缓冲区大小的设置,设得太小了。结果数据还没处理完,新的数据就覆盖了旧的,产生了严重的爆音。你想想看,整整花了两天时间才定位到这个问题。
循环传输配置:
// 循环传输配置
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执行完一个描述符后,跳到了一个未知的地址,后果是什么?设备直接死机。
链式传输配置:
// 定义两个描述符,实现双缓冲
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);
链式传输是真正考验嵌入式工程师功力的地方。它的配置虽然复杂,但灵活性也是四个模式中最高的。我记得有一次,要做8通道的ADC采集,每个通道的采样率和数据格式都不一样。如果用循环传输,你得配置很多个DMA通道。但用链式传输,你只需要一个通道,然后定义8个描述符,每个描述符指向不同的配置。完美解决。
好了,四种模式都讲完了。你会发现,单次和循环是一对“好基友”——一个最简、一个最自动化;突发和链式则更像“进阶选手”——一个拼速度、一个拼灵活。理解了它们的本质,你在实际项目中就能信手拈来,快速找到最适合的方案。