嵌入式进阶: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_sectionwrite_back_section,以及descriptor结构体,可参考Atmel START生成或手动声明。此处为简化,实际使用时需补充完整DMA描述符定义。

六、注意事项与常见陷阱

  • 缓冲区大小与DMA传输长度:确保缓冲区足够大,避免溢出。DMA循环模式会自动回绕,但ISR处理需快于一轮采样时间。
  • 同步性:自由运行模式下,各通道是顺序转换的,并非绝对同步。若需严格同步,需使用ADC的同步采样保持功能(部分MCU支持),或采用外部触发。
  • 中断优先级:DMA中断应设为较高优先级,避免丢失数据。
  • 内存对齐:DMA目的地址建议按2字节对齐,避免总线错误。
  • 调试技巧:先用较小缓冲区验证中断触发,再逐步增大。

七、总结

通过DMA中断驱动ADC采样,嵌入式系统实现了多路数据采集的零阻塞,CPU得以专注于核心逻辑。本文以Arduino Due为例,展示了从原理到代码的完整流程。掌握此技术后,您可轻松扩展至更高采样率或更多通道,为复杂实时系统奠定基础。

延伸思考:若需更高性能,可考虑双缓冲机制(Ping-Pong),在DMA搬运当前缓冲区时,CPU处理上一缓冲区,进一步减少数据延迟。