嵌入式进阶:Arduino DMA中断驱动多路ADC同步采样,主循环零阻塞
一、为什么需要DMA驱动的ADC采样?
传统Arduino的analogRead()是同步阻塞的:每次调用需等待转换完成(约100µs),若需采样多路(如8路),主循环将被拖慢近1ms。在实时控制或高频数据采集中,这会导致响应延迟、数据丢失。
核心痛点:
- CPU忙于等待ADC转换,无法处理其他任务
- 多路采样时,通道切换与读取需逐次进行,效率低下
- 无法实现真正的同步采样(各通道间有时间差)
解决方案:利用DMA(Direct Memory Access)外设,让ADC转换结果自动搬运至内存缓冲区,无需CPU介入。配合中断,仅在缓冲区满时通知CPU处理,实现零阻塞采样。
二、原理剖析:ADC + DMA + 中断的协作机制
2.1 ADC自由运行模式
配置ADC为自由运行模式(Free Running),即持续不断地转换指定通道序列。每次转换完成后,结果存入ADC数据寄存器(如AVR的ADCH/ADCL,或SAMD的RESULT)。
2.2 DMA的循环搬运
DMA控制器被配置为循环模式,每次ADC转换完成触发一次DMA请求,将数据寄存器内容搬运至内存数组。当搬运次数达到设定值(如缓冲区大小),DMA触发传输完成中断。
2.3 中断服务程序(ISR)
在ISR中,CPU快速处理缓冲区数据(如计算平均值、存储),然后清除标志位,继续下一轮。主循环完全不受影响。
数据流图:
ADC转换完成 → 触发DMA请求 → DMA搬运至内存数组 → 数组满 → DMA中断 → ISR处理 → 主循环继续
三、硬件与平台选择
本文以**Arduino Due(SAMD21)**为例,因其内置ADC和DMA控制器,且支持多通道序列。AVR(如Uno)的DMA能力有限,但原理类似。
- MCU:ATSAMD21G18(48MHz)
- ADC:12位,最多12通道,支持序列扫描
- DMA:支持循环传输,触发源可选ADC
四、配置步骤详解
4.1 初始化ADC为自由运行模式
需设置:
- 使能ADC时钟
- 配置参考电压、采样时间
- 设置通道序列(如ADC0~ADC3)
- 开启自由运行模式
4.2 配置DMA传输
- 选择DMA通道,关联ADC触发源
- 设置源地址为ADC数据寄存器,目的地址为内存数组
- 传输宽度为16位(12位数据)
- 循环模式,传输长度等于缓冲区大小
4.3 使能DMA中断
- 在NVIC中使能DMA中断
- 编写ISR,处理缓冲区数据
五、完整代码示例(Arduino Due)
// 多路ADC同步采样,DMA中断非阻塞
#include <Arduino.h>
#define ADC_CH_NUM 4 // 通道数
#define BUFFER_SIZE 64 // 每通道采样数(DMA循环周期)
volatile uint16_t adc_buffer[ADC_CH_NUM][BUFFER_SIZE];
volatile bool data_ready = false;
void ADC_Init() {
// 使能ADC时钟
PM->APBCMASK.reg |= PM_APBCMASK_ADC;
// 配置ADC:12位,自由运行,参考电压3.3V
ADC->CTRLA.reg = ADC_CTRLA_SWRST; // 复位
while (ADC->CTRLA.bit.SWRST);
ADC->CTRLB.reg = ADC_CTRLB_PRESCALER_DIV4 | ADC_CTRLB_RESSEL_12BIT;
ADC->REFCTRL.reg = ADC_REFCTRL_REFSEL_INTVCC1; // 内部参考
ADC->AVGCTRL.reg = ADC_AVGCTRL_SAMPLENUM_1; // 无平均
ADC->SAMPCTRL.reg = ADC_SAMPCTRL_SAMPLEN(10); // 采样时间
ADC->CTRLA.reg = ADC_CTRLA_ENABLE; // 使能ADC
while (!ADC->STATUS.bit.SYNCBUSY);
// 配置通道序列:A0~A3对应ADC0~ADC3
ADC->INPUTCTRL.reg = ADC_INPUTCTRL_MUXPOS_PIN0 | ADC_INPUTCTRL_MUXNEG_GND;
ADC->CTRLB.bit.FREERUN = 1; // 自由运行
// 使能ADC结果就绪事件(用于触发DMA)
ADC->EVCTRL.reg |= ADC_EVCTRL_RESRDYEO;
}
void DMA_Init() {
// 使能DMA时钟
PM->AHBMASK.reg |= PM_AHBMASK_DMAC;
DMAC->CTRL.reg = DMAC_CTRL_SWRST;
while (DMAC->CTRL.bit.SWRST);
DMAC->CTRL.reg = DMAC_CTRL_DMAENABLE;
// 配置DMA通道0
DMAC->BASEADDR.reg = (uint32_t)descriptor_section;
DMAC->WRBADDR.reg = (uint32_t)write_back_section;
// 设置描述符(见下方结构体)
descriptor.descaddr = 0; // 循环模式,自动回绕
descriptor.srcaddr = (uint32_t)&ADC->RESULT.reg;
descriptor.dstaddr = (uint32_t)adc_buffer;
descriptor.btcnt = ADC_CH_NUM * BUFFER_SIZE;
descriptor.btctrl = DMAC_BTCTRL_BEATSIZE_HWORD | DMAC_BTCTRL_SRCINC_OFF |
DMAC_BTCTRL_DSTINC | DMAC_BTCTRL_VALID | DMAC_BTCTRL_EVOSEL_BLOCK;
// 使能通道中断
DMAC->CHINTENSET.reg = DMAC_CHINTENSET_TCMPL;
NVIC_EnableIRQ(DMAC_IRQn);
// 关联ADC事件触发
DMAC->CHCTRLB.reg = DMAC_CHCTRLB_TRIGSRC_ADC_RESRDY | DMAC_CHCTRLB_TRIGACT_BEAT;
}
// DMA传输完成中断(每轮采样完成)
void DMAC_IRQHandler() {
if (DMAC->CHINTFLAG.bit.TCMPL0) {
DMAC->CHINTFLAG.reg = DMAC_CHINTFLAG_TCMPL0; // 清除标志
data_ready = true; // 通知主循环
}
}
void setup() {
Serial.begin(115200);
ADC_Init();
DMA_Init();
// 启动ADC转换
ADC->SWTRIG.bit.START = 1;
}
void loop() {
if (data_ready) {
data_ready = false;
// 处理缓冲区数据(例如打印)
for (int i = 0; i < BUFFER_SIZE; i++) {
Serial.print("Ch0:"); Serial.print(adc_buffer[0][i]);
Serial.print(" Ch1:"); Serial.print(adc_buffer[1][i]);
Serial.print(" Ch2:"); Serial.print(adc_buffer[2][i]);
Serial.print(" Ch3:"); Serial.println(adc_buffer[3][i]);
}
}
// 主循环可执行其他任务,无阻塞
digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN)); // 闪烁LED证明非阻塞
delay(10);
}
注意:上述代码需定义descriptor_section和write_back_section,以及descriptor结构体,可参考Atmel START生成或手动声明。此处为简化,实际使用时需补充完整DMA描述符定义。
六、注意事项与常见陷阱
- 缓冲区大小与DMA传输长度:确保缓冲区足够大,避免溢出。DMA循环模式会自动回绕,但ISR处理需快于一轮采样时间。
- 同步性:自由运行模式下,各通道是顺序转换的,并非绝对同步。若需严格同步,需使用ADC的同步采样保持功能(部分MCU支持),或采用外部触发。
- 中断优先级:DMA中断应设为较高优先级,避免丢失数据。
- 内存对齐:DMA目的地址建议按2字节对齐,避免总线错误。
- 调试技巧:先用较小缓冲区验证中断触发,再逐步增大。
七、总结
通过DMA中断驱动ADC采样,嵌入式系统实现了多路数据采集的零阻塞,CPU得以专注于核心逻辑。本文以Arduino Due为例,展示了从原理到代码的完整流程。掌握此技术后,您可轻松扩展至更高采样率或更多通道,为复杂实时系统奠定基础。
延伸思考:若需更高性能,可考虑双缓冲机制(Ping-Pong),在DMA搬运当前缓冲区时,CPU处理上一缓冲区,进一步减少数据延迟。