嵌入式实战:Arduino 上利用 DMA 实现 ADC 连续采样而不阻塞主循环的完整配置
👁 1 阅读 · 2026-08-27 · 嵌入式
在嵌入式开发中,ADC 连续采样常因阻塞主循环而降低系统实时性。本文以 Arduino(基于 SAMD21 或 RP2040)为例,深入讲解如何利用 DMA(直接内存访问)将 ADC 数据自动搬运到内存缓冲区,实现后台采样、前台处理。文章涵盖原理、寄存器配置、完整代码示例及注意事项,帮助开发者摆脱轮询等待,构建高效非阻塞的数据采集系统。
# 引言
在 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 的协同工作,是嵌入式开发进阶的重要一步。