# 引言 在 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 位缓冲区,当半满时触发中断,打印数据。 ```c #include // 缓冲区大小(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_DESC` 和 `DMAC_WRB` 需要定义为全局数组,例如: ```c __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 的协同工作,是嵌入式开发进阶的重要一步。