引言

在 Arduino 平台上,传统的 analogRead() 函数是同步阻塞的,每次采样需等待转换完成,导致主循环无法及时响应其他任务。对于需要连续采样(如音频、振动监测)的应用,这种阻塞方式会严重拖累系统性能。利用 DMA(Direct Memory Access)控制器,可以让 ADC 在后台自动将采样结果存入内存,CPU 完全解放出来处理其他逻辑。本文将以 Arduino 生态中常见的 SAMD21(如 Zero、MKR 系列)和 RP2040(如 Raspberry Pi Pico)为例,详细讲解配置步骤和代码实现。

原理概述

ADC 与 DMA 协作机制

  • ADC 转换:ADC 模块将模拟电压转换为数字值,转换完成后产生一个事件或中断请求。
  • DMA 搬运:DMA 控制器在接收到 ADC 转换完成事件后,自动将 ADC 数据寄存器(RESULT)的值搬运到指定的内存地址,无需 CPU 干预。
  • 循环模式:DMA 可配置为环形缓冲区模式,当缓冲区填满后自动回绕到起始地址,实现连续不间断采样。
  • 非阻塞:CPU 只需在缓冲区半满或全满时处理数据,其余时间可执行其他任务。

为什么选择 DMA?

  • 降低 CPU 占用率,提升系统实时性。
  • 采样速率更稳定,不受主循环代码执行时间影响。
  • 适合高频连续采样场景,如音频采集、振动分析。

硬件与平台说明

本文以 Arduino Zero(SAMD21G18) 为例,其 ADC 和 DMA 配置较为典型。RP2040 的配置略有不同,但思路一致,可参考官方数据手册。

配置步骤(以 SAMD21 为例)

1. 初始化 ADC

  • 配置 ADC 时钟、分辨率(如 12 位)、采样时间、参考电压。
  • 启用 ADC 的自由运行模式(Free Running),使其连续转换。
  • 使能 ADC 转换完成事件输出,用于触发 DMA。

2. 配置 DMA 控制器

  • 选择 DMA 通道,设置源地址为 ADC 结果寄存器(ADC->RESULT.reg)。
  • 设置目的地址为内存缓冲区(如 uint16_t buffer[256])。
  • 设置传输宽度为 16 位(与 ADC 分辨率匹配)。
  • 启用循环模式(Circular Mode),并设置传输计数。
  • 使能 DMA 中断(可选),用于缓冲区半满/全满通知。

3. 启动采样

  • 启动 ADC 转换,DMA 将自动开始搬运数据。
  • 主循环中无需等待,可处理其他任务。

完整代码示例

以下代码在 Arduino Zero 上实现 1kHz 连续采样,DMA 将数据存入 256 个 16 位缓冲区,当半满时触发中断,打印数据。

#include <Arduino.h>

// 缓冲区大小(256 个采样点)
#define BUFFER_SIZE 256

// DMA 缓冲区(必须保持全局或静态)
uint16_t adc_buffer[BUFFER_SIZE];

// ADC 和 DMA 配置函数
void setup_adc_dma() {
    // 1. 配置 ADC
    ADC->CTRLA.bit.ENABLE = 0;  // 先禁用 ADC
    while (ADC->STATUS.bit.SYNCBUSY);

    ADC->CTRLB.reg = ADC_CTRLB_PRESCALER_DIV32 |   // 时钟分频
                     ADC_CTRLB_RESSEL_12BIT |      // 12 位分辨率
                     ADC_CTRLB_FREERUN;            // 自由运行模式

    ADC->REFCTRL.bit.REFSEL = ADC_REFCTRL_REFSEL_INTVCC1_Val; // 内部参考
    ADC->INPUTCTRL.bit.MUXPOS = ADC_INPUTCTRL_MUXPOS_PIN0_Val; // 使用 A0 引脚
    ADC->SAMPCTRL.bit.SAMPLEN = 0x3F;  // 采样时间
    ADC->CTRLA.bit.ENABLE = 1;         // 启用 ADC
    while (ADC->STATUS.bit.SYNCBUSY);

    // 使能 ADC 转换完成事件输出
    ADC->EVCTRL.bit.RESRDY = 1;

    // 2. 配置 DMA
    // 启用 DMA 时钟
    PM->AHBMASK.bit.DMAC = 1;
    DMAC->CTRL.bit.SWRST = 1;  // 软件复位
    while (DMAC->CTRL.bit.SWRST);

    DMAC->CTRL.reg = DMAC_CTRL_DMAENABLE | DMAC_CTRL_LVLEN(0xF); // 使能 DMA

    // 配置 DMA 通道 0
    DMAC->BASEADDR.reg = (uint32_t)DMAC_DESC;  // 描述符基地址(需在内存中定义)
    DMAC->WRBADDR.reg = (uint32_t)DMAC_WRB;    // 写回区

    // 设置描述符(简化,实际需定义 DMAC_DESC 和 DMAC_WRB)
    DMAC_DESC[0].SRCADDR.reg = (uint32_t)&ADC->RESULT.reg;
    DMAC_DESC[0].DSTADDR.reg = (uint32_t)adc_buffer;
    DMAC_DESC[0].BTCNT.reg = BUFFER_SIZE;
    DMAC_DESC[0].BTCTRL.reg = DMAC_BTCTRL_BEATSIZE_HWORD |  // 16 位传输
                               DMAC_BTCTRL_SRCINC_OFF |      // 源地址不增加
                               DMAC_BTCTRL_DSTINC |          // 目的地址增加
                               DMAC_BTCTRL_BLOCKACT_INT |    // 块传输完成中断
                               DMAC_BTCTRL_VALID;            // 描述符有效

    // 启用 DMA 通道
    DMAC->CHCTRLA.reg = DMAC_CHCTRLA_ENABLE;

    // 3. 启用 DMA 中断(可选)
    NVIC_EnableIRQ(DMAC_IRQn);
}

// DMA 中断服务函数
void DMAC_IRQHandler() {
    // 检查通道 0 中断标志
    if (DMAC->INTFLAG.bit.CH0) {
        DMAC->INTFLAG.bit.CH0 = 1;  // 清除标志
        // 处理数据(例如,打印前 128 个采样点)
        for (int i = 0; i < BUFFER_SIZE/2; i++) {
            Serial.println(adc_buffer[i]);
        }
    }
}

void setup() {
    Serial.begin(115200);
    setup_adc_dma();
}

void loop() {
    // 主循环可以执行其他任务,如控制 LED、读取按键等
    delay(10);
}

注意:上述代码中 DMAC_DESCDMAC_WRB 需要定义为全局数组,例如:

__attribute__((aligned(16))) DMAC_DESCRIPTOR DMAC_DESC[1];
__attribute__((aligned(16))) DMAC_WRITEBACK DMAC_WRB[1];

注意事项

  • 缓冲区大小:DMA 缓冲区必须是 2 的幂次方(如 256、512),以便循环模式正确回绕。
  • 内存对齐:DMA 描述符和写回区需要按 16 字节对齐,否则可能导致总线错误。
  • 中断优先级:DMA 中断应设置为较高优先级,避免数据丢失。
  • 采样率计算:实际采样率取决于 ADC 时钟和采样时间,需根据需求调整分频系数。
  • 平台差异:RP2040 的 DMA 配置类似,但寄存器名称不同,需参考官方 SDK 或数据手册。
  • 调试技巧:在调试时,可先用小缓冲区并打印数据,验证 DMA 搬运是否正确。

总结

通过 DMA 实现 ADC 连续采样,可以显著提升嵌入式系统的实时性和效率。本文以 Arduino Zero 为例,详细介绍了原理和配置步骤,并提供了可直接运行的代码。开发者可根据实际需求调整缓冲区大小、采样率和数据处理逻辑,将这一技术应用到音频采集、传感器监测等场景。掌握 DMA 与 ADC 的协同工作,是嵌入式开发进阶的重要一步。