4. DMA与ADC数据采集:连续采样与双缓冲实战
ADC连续采样配合DMA循环传输,这几乎是每个做数据采集的嵌入式工程师都会碰到的组合。我记得刚入行那会儿,总觉得ADC中断读数据就够用了,直到有一次做音频采集,发现CPU根本忙不过来——采样率一上去,系统就卡死。从那以后,我才真正开始认真研究DMA搬运ADC数据这套方案。
说白了,ADC只管采样,DMA只管搬数据,CPU只管处理。三者各干各的,效率才能拉满。今天咱们就把这套组合拳拆开揉碎了讲清楚。
4.1 ADC连续采样模式配置
连续采样模式,就是ADC启动后自己不停地采,不需要CPU一次次触发。配置起来其实不复杂,但有几个坑我得先给你提个醒。
我一般这样配置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的搬运速度,数据就会被覆盖。这就是咱们接下来要解决的核心问题。
4.3 双缓冲机制防止数据覆盖
双缓冲,说白了就是准备两个缓冲区。DMA往一个缓冲区写数据时,CPU从另一个缓冲区读数据。写满了就交换角色。这样两边各干各的,谁也不抢谁的。
我画了一张图,帮你理解双缓冲的工作流程:
实现双缓冲,关键是用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;
}
}
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循环传输、双缓冲切换,这三个环节环环相扣。你只要把每个环节的配置参数搞对,再把中断回调里的切换逻辑写严谨,基本就不会出大问题。