嵌入式高精度低频信号采集:Arduino ADC 采样窗口与 DMA 协同实战
👁 2 阅读 · 2026-08-27 · 嵌入式
在嵌入式系统中,低频信号(如生物电、振动、温度)的采集往往对精度和时序有严苛要求。本文深入探讨如何在 Arduino(基于 AVR 或 SAMD 内核)上,利用 ADC 的采样窗口(Sample and Hold)与 DMA(直接内存访问)结合,实现高精度、低 CPU 占用的低频信号采集。通过原理剖析、寄存器级配置、完整代码示例及常见陷阱规避,帮助开发者突破 Arduino 高层 API 的限制,掌握底层嵌入式优化技巧。
# 引言
在 Arduino 生态中,`analogRead()` 虽简单,但无法满足高精度低频信号(如 0.1Hz~100Hz)的采集需求:其采样时序不可控、CPU 全程等待、且无法实现多通道同步。本文以 AVR(如 ATmega328P)和 SAMD21(如 Arduino Zero)为例,讲解如何利用 ADC 的采样窗口(Sample Duration)与 DMA 传输,实现低噪声、高吞吐的采集系统。
# 原理剖析
## 1. ADC 采样窗口(Sample and Hold)
ADC 转换分为采样阶段和转换阶段。采样阶段中,采样电容需充电至输入电压,其时间常数由源阻抗和电容决定。若采样时间不足,则采样值偏差(增益误差)。对于低频信号,源阻抗可能较高,因此需要延长采样窗口。
- **AVR**:通过 ADCSRA 寄存器的 ADPS 位设置预分频,但采样时间固定为 1.5 个 ADC 时钟周期(默认)。可通过 ADCSRB 的 ADLAR 或使用自动触发模式调整,但更精细的控制需修改寄存器。
- **SAMD21**:提供 SAMPCTRL 寄存器(8 位),可设置采样周期(0~1023 个 ADC 时钟),极大灵活。
## 2. DMA 的作用
DMA 允许 ADC 转换结果直接传输至内存,无需 CPU 干预。这解决了两个问题:
- 降低 CPU 负载,使其可处理其他任务(如滤波、存储)。
- 保证采样时序的确定性,避免中断延迟导致的抖动。
# 配置步骤
## 硬件准备
- Arduino Uno(AVR)或 Arduino Zero(SAMD21)
- 低频信号源(如函数发生器输出 10Hz 正弦波)
- 示波器(可选)
## 步骤 1:ADC 初始化(以 SAMD21 为例)
SAMD21 的 ADC 配置需操作 PAC、ADC 和 DMAC 模块。以下代码配置 ADC 为 12 位分辨率,采样窗口设置为最大(1023 周期),并启用自由运行模式。
```c
#include
// 寄存器定义(SAMD21)
#define ADC_BASE 0x42004000
#define ADC_CTRLA (*(volatile uint32_t *)(ADC_BASE + 0x00))
#define ADC_CTRLB (*(volatile uint32_t *)(ADC_BASE + 0x04))
#define ADC_SAMPCTRL (*(volatile uint8_t *)(ADC_BASE + 0x08))
#define ADC_INPUTCTRL (*(volatile uint32_t *)(ADC_BASE + 0x0C))
#define ADC_EVCTRL (*(volatile uint32_t *)(ADC_BASE + 0x10))
#define ADC_INTENCLR (*(volatile uint32_t *)(ADC_BASE + 0x14))
#define ADC_INTENSET (*(volatile uint32_t *)(ADC_BASE + 0x16))
#define ADC_SWTRIG (*(volatile uint8_t *)(ADC_BASE + 0x18))
#define ADC_RESULT (*(volatile uint32_t *)(ADC_BASE + 0x1C))
void adc_init() {
// 使能 APB 时钟(ADC 时钟)
PM->APBCMASK.reg |= PM_APBCMASK_ADC;
// 配置 GCLK 为 48MHz/16 = 3MHz
GCLK->CLKCTRL.reg = GCLK_CLKCTRL_CLKEN | GCLK_CLKCTRL_GEN_GCLK0 | GCLK_CLKCTRL_ID_ADC;
while (GCLK->STATUS.bit.SYNCBUSY);
// 复位 ADC
ADC_CTRLA.bit.SWRST = 1;
while (ADC_CTRLA.bit.SWRST);
// 设置 ADC 控制 A:启用,自由运行模式
ADC_CTRLA.reg = ADC_CTRLA_ENABLE | ADC_CTRLA_RUNSTDBY;
// 控制 B:12 位分辨率,采样保持时间由 SAMPCTRL 决定
ADC_CTRLB.reg = ADC_CTRLB_PRESCALER_DIV16 | ADC_CTRLB_RESSEL_12BIT;
// 设置采样窗口:最大 1023 周期(约 341us @3MHz)
ADC_SAMPCTRL = 0xFF; // 实际最大 255,但寄存器为 8 位,此处用 255
// 输入配置:使用 AIN0(PA02)单端,参考电压 VDD
ADC_INPUTCTRL.reg = ADC_INPUTCTRL_MUXPOS_PIN0 | ADC_INPUTCTRL_GAIN_1X | ADC_INPUTCTRL_REFSEL_INTVCC0;
// 禁用中断(使用 DMA)
ADC_INTENCLR.reg = 0xFF;
// 启动 ADC 转换(自由运行)
ADC_SWTRIG.bit.START = 1;
}
```
## 步骤 2:配置 DMA 传输
SAMD21 的 DMAC 需要设置描述符和触发源。以下代码配置 DMA 通道 0,触发源为 ADC 结果就绪(RESRDY),每次传输 16 位(12 位数据),循环模式。
```c
#define DMAC_BASE 0x4100A000
#define DMAC_CTRL (*(volatile uint32_t *)(DMAC_BASE + 0x00))
#define DMAC_CHCTRLB (*(volatile uint32_t *)(DMAC_BASE + 0x04)) // 通道控制
#define DMAC_CHINTENCLR (*(volatile uint32_t *)(DMAC_BASE + 0x08))
#define DMAC_CHINTENSET (*(volatile uint32_t *)(DMAC_BASE + 0x0C))
#define DMAC_CHSTATUS (*(volatile uint32_t *)(DMAC_BASE + 0x10))
#define DMAC_CHBASE (*(volatile uint32_t *)(DMAC_BASE + 0x14)) // 描述符基址
// 描述符结构(需 4 字对齐)
struct dma_descriptor {
uint32_t btctrl;
uint32_t btcnt;
uint32_t srcaddr;
uint32_t dstaddr;
} __attribute__((aligned(16)));
volatile struct dma_descriptor desc;
volatile uint16_t adc_buffer[256]; // 存储 256 个采样值
void dma_init() {
// 使能 DMAC 时钟
PM->AHBMASK.reg |= PM_AHBMASK_DMAC;
PM->APBBMASK.reg |= PM_APBBMASK_DMAC;
// 复位 DMAC
DMAC_CTRL.bit.SWRST = 1;
while (DMAC_CTRL.bit.SWRST);
// 配置描述符:块传输 256 次,每次 16 位,源地址为 ADC 结果寄存器,目标为缓冲区,循环模式
desc.btctrl = DMAC_BTCTRL_BEAT_SIZE_HWORD | DMAC_BTCTRL_SRCINC_OFF | DMAC_BTCTRL_DSTINC_1 | DMAC_BTCTRL_VALID | DMAC_BTCTRL_BLOCKACT_INT;
desc.btcnt = 256;
desc.srcaddr = (uint32_t)&ADC_RESULT;
desc.dstaddr = (uint32_t)adc_buffer;
// 设置通道 0 描述符地址
DMAC_CHBASE = (uint32_t)&desc;
// 配置通道控制:触发源为 ADC,优先级高,使能
DMAC_CHCTRLB.reg = DMAC_CHCTRLB_TRIGSRC_ADC_RESRDY | DMAC_CHCTRLB_LVL_LVL2 | DMAC_CHCTRLB_ENABLE;
// 使能通道中断(可选)
DMAC_CHINTENSET.reg = DMAC_CHINTENSET_TC;
}
```
## 步骤 3:主程序与数据处理
在 `setup()` 中初始化串口、ADC 和 DMA,然后进入循环。DMA 自动填充缓冲区,当缓冲区满时触发中断,我们可在中断中处理数据(如计算 RMS 或滤波)。
```c
volatile bool buffer_ready = false;
void DMAC_Handler() {
if (DMAC_CHINTFLAG.bit.TC) {
DMAC_CHINTFLAG.bit.TC = 1; // 清除标志
buffer_ready = true;
}
}
void setup() {
Serial.begin(115200);
adc_init();
dma_init();
// 使能 DMA 中断
NVIC_EnableIRQ(DMAC_IRQn);
}
void loop() {
if (buffer_ready) {
buffer_ready = false;
// 计算平均值和峰峰值(示例)
uint32_t sum = 0;
uint16_t min_val = 4095, max_val = 0;
for (int i = 0; i < 256; i++) {
sum += adc_buffer[i];
if (adc_buffer[i] < min_val) min_val = adc_buffer[i];
if (adc_buffer[i] > max_val) max_val = adc_buffer[i];
}
float avg = (float)sum / 256.0;
float vref = 3.3; // 参考电压
float voltage = avg * vref / 4095.0;
Serial.print("Avg Voltage: "); Serial.println(voltage, 4);
Serial.print("Peak-Peak: "); Serial.println((max_val - min_val) * vref / 4095.0, 4);
// 重新启动 DMA(循环模式已自动重载,但需重新触发)
DMAC_CHCTRLB.bit.CMD = DMAC_CHCTRLB_CMD_UPDATE; // 更新描述符
}
}
```
# 注意事项
- **采样窗口与源阻抗**:若信号源阻抗高(>10kΩ),务必延长采样窗口,否则精度下降。SAMD21 的 SAMPCTRL 最大 255,但实际有效值可能受时钟限制,需实测。
- **DMA 描述符对齐**:描述符必须 16 字节对齐,否则 DMA 传输错误。使用 `__attribute__((aligned(16)))` 确保。
- **缓冲区大小**:低频信号采样率低,缓冲区可较小,但需考虑 DMA 循环模式下的覆盖问题。若处理不及时,数据会被覆盖,建议使用双缓冲或中断标志。
- **参考电压稳定性**:使用内部参考电压(如 INT1V)时,需确保电源稳定,否则引入噪声。可外接高精度基准。
- **AVR 平台差异**:ATmega328P 的 ADC 采样时间调整需通过 ADCSRA 的 ADPS 和 ADCSRB 的 ADTS 组合,但无法像 SAMD21 那样精细控制。若需高精度,建议使用 SAMD 或 STM32。
- **中断优先级**:DMA 中断应设置为高优先级,避免被其他中断延迟,导致缓冲区溢出。
# 总结
通过 ADC 采样窗口与 DMA 的结合,Arduino 平台也能实现高精度低频信号采集,且 CPU 占用极低。本文以 SAMD21 为例展示了寄存器级配置,但原理同样适用于其他嵌入式平台。开发者可根据实际需求调整采样窗口、DMA 传输长度和数据处理算法,以优化系统性能。