4. DMA与ADC数据采集:连续采样与双缓冲实战

ADC连续采样配合DMA循环传输,这几乎是每个做数据采集的嵌入式工程师都会碰到的组合。我记得刚入行那会儿,总觉得ADC中断读数据就够用了,直到有一次做音频采集,发现CPU根本忙不过来——采样率一上去,系统就卡死。从那以后,我才真正开始认真研究DMA搬运ADC数据这套方案。

说白了,ADC只管采样,DMA只管搬数据,CPU只管处理。三者各干各的,效率才能拉满。今天咱们就把这套组合拳拆开揉碎了讲清楚。

4.1 ADC连续采样模式配置

连续采样模式,就是ADC启动后自己不停地采,不需要CPU一次次触发。配置起来其实不复杂,但有几个坑我得先给你提个醒。

注意:连续采样模式下,ADC的功耗会比单次采样高不少。如果你的项目对功耗敏感,记得在不需要采样时及时关闭ADC时钟,或者切换到低功耗模式。

我一般这样配置ADC连续采样(以STM32为例):

// ADC连续采样模式配置
ADC_InitTypeDef ADC_InitStruct = {0};
ADC_InitStruct.ADC_ContinuousConvMode = ENABLE;  // 开启连续转换
ADC_InitStruct.ADC_DataAlign = ADC_DATAALIGN_RIGHT;
ADC_InitStruct.ADC_ExternalTrigConv = ADC_SOFTWARE_START; // 软件触发
ADC_InitStruct.ADC_NbrOfConversion = 1;
ADC_InitStruct.ADC_ScanConvMode = DISABLE;       // 单通道,不扫描
HAL_ADC_Init(&hadc);
HAL_ADC_ConfigChannel(&hadc, &sConfig);
HAL_ADC_Start(&hadc);  // 启动后自动连续采样

这里有个细节:ADC_ContinuousConvMode置为ENABLE后,ADC会一直采下去,直到你调用HAL_ADC_Stop()。我见过有人忘了关这个模式,结果电池半天就耗光了。

4.2 DMA循环传输搬运ADC数据

ADC采出来的数据总得有个地方放。用CPU去读?太浪费了。DMA循环传输就是干这个的——它自动把ADC的数据寄存器内容搬到内存里,搬完一轮再从头开始,循环往复。

配置DMA循环模式时,我习惯这样写:

// DMA循环传输配置
DMA_InitTypeDef DMA_InitStruct = {0};
DMA_InitStruct.DMA_Mode = DMA_CIRCULAR;          // 循环模式
DMA_InitStruct.DMA_PeripheralBaseAddr = (uint32_t)&(ADC1->DR);
DMA_InitStruct.DMA_MemoryBaseAddr = (uint32_t)adc_buffer;
DMA_InitStruct.DMA_BufferSize = BUFFER_SIZE;
DMA_InitStruct.DMA_PeripheralInc = DMA_PINC_DISABLE;
DMA_InitStruct.DMA_MemoryInc = DMA_MINC_ENABLE;
DMA_InitStruct.DMA_PeripheralDataSize = DMA_PDATAALIGN_HALFWORD;
DMA_InitStruct.DMA_MemoryDataSize = DMA_MDATAALIGN_HALFWORD;
HAL_DMA_Init(&hdma_adc);

循环模式的好处是:你不用操心DMA搬完数据后怎么办,它会自动回卷。但问题也来了——如果CPU处理速度跟不上DMA的搬运速度,数据就会被覆盖。这就是咱们接下来要解决的核心问题。

我的经验:DMA缓冲区大小要结合采样率和CPU处理时间来算。比如采样率10kHz,CPU处理一个数据包需要200μs,那缓冲区至少得能存2个以上的数据包,否则肯定丢数据。

4.3 双缓冲机制防止数据覆盖

双缓冲,说白了就是准备两个缓冲区。DMA往一个缓冲区写数据时,CPU从另一个缓冲区读数据。写满了就交换角色。这样两边各干各的,谁也不抢谁的。

我画了一张图,帮你理解双缓冲的工作流程:

双缓冲机制工作流程图 ADC 连续采样 DMA 循环搬运 写入 写入 缓冲区 A DMA 正在写入 缓冲区 B CPU 正在处理 读取 读取 CPU 处理 FFT/滤波/存储 写满后交换 DMA ↔ CPU 角色互换 关键点: 1. DMA 和 CPU 永远不会同时操作同一个缓冲区 2. 缓冲区大小必须能覆盖 CPU 的最长处理延迟 3. 交换操作由 DMA 传输完成中断触发,零开销

实现双缓冲,关键是用DMA的传输完成中断来切换缓冲区指针。代码大概长这样:

// 双缓冲初始化
#define BUFFER_SIZE 256
uint16_t buffer_a[BUFFER_SIZE];
uint16_t buffer_b[BUFFER_SIZE];
volatile uint8_t active_buffer = 0;  // 0: 使用A, 1: 使用B

// DMA传输完成中断回调
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc) {
    if (active_buffer == 0) {
        // DMA当前写的是A,CPU处理B
        process_data(buffer_b, BUFFER_SIZE);
        // 切换DMA目标到B
        HAL_ADC_Stop_DMA(hadc);
        HAL_ADC_Start_DMA(hadc, (uint32_t*)buffer_b, BUFFER_SIZE);
        active_buffer = 1;
    } else {
        // DMA当前写的是B,CPU处理A
        process_data(buffer_a, BUFFER_SIZE);
        // 切换DMA目标到A
        HAL_ADC_Stop_DMA(hadc);
        HAL_ADC_Start_DMA(hadc, (uint32_t*)buffer_a, BUFFER_SIZE);
        active_buffer = 0;
    }
}
核心要点:双缓冲的本质是用空间换时间。多花一倍的内存,换来CPU和DMA的完全并行工作。在实时性要求高的场景(比如音频、振动分析),这笔买卖绝对划算。

4.4 三种方案的对比

我把常见的三种ADC数据采集方案做了个对比,方便你选型时参考:

方案 CPU占用率 数据安全性 内存开销 适用场景
中断读取 高(每次采样都中断) 低(易丢数据) 低(单变量) 低频采样(<1kHz)
单缓冲+DMA 中(半满中断) 中(可能覆盖) 中(1个缓冲区) 中频采样(1-10kHz)
双缓冲+DMA 低(满中断) 高(无覆盖) 高(2个缓冲区) 高频采样(>10kHz)

从表里能看出来,双缓冲方案虽然多占了一倍内存,但换来了最低的CPU占用和最高的数据安全性。我在做电机电流采样项目时,采样率设到20kHz,用双缓冲方案,CPU占用率才不到5%。

4.5 避坑指南

做这个实验时,有几个坑我踩过,给你列出来:

  • 缓冲区对齐问题:DMA对内存地址对齐有要求,有些MCU要求缓冲区地址必须按4字节对齐。我遇到过因为没对齐,DMA死活不工作的怪事。
  • 中断优先级:DMA传输完成中断的优先级要设置合理。如果优先级太低,可能被其他中断打断,导致缓冲区切换不及时,数据还是会被覆盖。
  • 缓存一致性:如果你的MCU带Cache(比如STM32H7系列),DMA和CPU操作同一块内存时,记得做Cache维护。否则CPU读到的可能是旧数据。
我曾经踩过的坑:有一次做多通道ADC采集,开了扫描模式,结果DMA配置的传输长度没乘以通道数。ADC采了8个通道的数据,DMA只搬了1个通道的长度,后面的数据全丢了。排查了整整一天才找到原因。

嗯,双缓冲这套方案其实不难,关键是把流程理清楚。ADC连续采样、DMA循环传输、双缓冲切换,这三个环节环环相扣。你只要把每个环节的配置参数搞对,再把中断回调里的切换逻辑写严谨,基本就不会出大问题。