# 引言 在 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 传输长度和数据处理算法,以优化系统性能。