4. ADC采集与校准:从初始化到稳定数据的完整链路

ADC 这玩意儿,说简单也简单,说坑也真不少。我最早用 CH32V203 做电池电压监测时,觉得不就是读个寄存器嘛,结果数据跳得跟心电图似的。后来才明白,ADC 的稳定度不光是代码问题,硬件布局、参考源、采样时序,哪个环节松一点,数据就飘给你看。

这一章,咱们就把 ADC 从初始化到多通道采集,再到校准和滤波,整个链路捋一遍。我会把项目中踩过的坑、试过的方案都摊开来讲。

4.1 ADC初始化与多通道采集

CH32 系列的 ADC 是 12 位逐次逼近型,最高 1Msps。初始化其实不复杂,但有几个细节容易忽略。

4.1.1 基础初始化流程

我个人习惯先把时钟和 GPIO 配好,再配 ADC 核心寄存器。下面是一个典型初始化代码:

void ADC_Init(void)
{
    // 1. 开启ADC时钟和GPIO时钟
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);

    // 2. 配置GPIO为模拟输入
    GPIO_InitTypeDef GPIO_InitStructure;
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
    GPIO_Init(GPIOA, &GPIO_InitStructure);

    // 3. ADC基本配置
    ADC_InitTypeDef ADC_InitStructure;
    ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;
    ADC_InitStructure.ADC_ScanConvMode = ENABLE;      // 扫描模式
    ADC_InitStructure.ADC_ContinuousConvMode = ENABLE; // 连续转换
    ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;
    ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;
    ADC_InitStructure.ADC_NbrOfChannel = 3;            // 3个通道
    ADC_Init(ADC1, &ADC_InitStructure);

    // 4. 配置各通道采样时间
    ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_239Cycles5);
    ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 2, ADC_SampleTime_239Cycles5);
    ADC_RegularChannelConfig(ADC1, ADC_Channel_2, 3, ADC_SampleTime_239Cycles5);

    // 5. 校准
    ADC_ResetCalibration(ADC1);
    while(ADC_GetResetCalibrationStatus(ADC1));
    ADC_StartCalibration(ADC1);
    while(ADC_GetCalibrationStatus(ADC1));

    // 6. 启动
    ADC_Cmd(ADC1, ENABLE);
}
我的习惯:每次初始化都做一次硬件校准。虽然手册说上电后会自动校准一次,但我在项目中遇到过复位后校准不完整的情况。手动校准多花 10μs,换来的是数据一致性。

4.1.2 多通道采集的两种模式

多通道采集,说白了就是 ADC 在多个输入引脚之间切换。CH32 支持两种方式:

模式 原理 适用场景 注意点
扫描模式 + 连续转换 自动轮询所有配置通道 需要持续监测多个信号 通道切换有稳定时间,采样周期要留够
单次转换 + 软件切换 每次手动选择通道再启动 通道数少、采样率要求不高 灵活,但CPU占用高

我建议:如果通道数不超过 4 个,直接用扫描模式。超过 4 个,要考虑通道间的串扰问题——这个后面会讲。

4.2 采样率与精度权衡

ADC 的采样率和精度,本质上是一对矛盾。你想想看,采样时间越长,电容充电越充分,精度越高,但能采的样本数就越少。

4.2.1 采样时间的影响

CH32 的 ADC 采样时间可配置,从 1.5 个周期到 239.5 个周期。我做过一组对比测试:

采样周期 理论采样率(1MHz ADC时钟) 实测有效位数(ENOB) 适用场景
1.5 Cycles ~400 ksps 9.2 bit 高频信号、快速过采样
13.5 Cycles ~68 ksps 10.8 bit 一般传感器
239.5 Cycles ~4 ksps 11.5 bit 精密测量、电池电压
关键结论:采样周期从 1.5 增加到 239.5,有效位数只提升了 2.3 bit,但采样率掉了两个数量级。所以不要盲目追求长采样时间,够用就行。

4.2.2 实际项目中的选择策略

我在做温湿度传感器项目时,用了这样的策略:

  • 温度通道:采样时间 239.5 Cycles,因为温度变化慢,需要精度
  • 湿度通道:采样时间 71.5 Cycles,折中方案
  • 按键检测:采样时间 7.5 Cycles,只判断高低电平,不需要精度

每个通道单独配采样时间,这是 CH32 的一个实用特性。别偷懒用统一配置。

4.3 参考电压漂移问题

参考电压漂移,是 ADC 精度的大敌。我吃过一次大亏:一个户外设备,白天温度 40℃,晚上 10℃,ADC 采集的电压值漂了 30mV。查了两天才发现是内部参考电压随温度变了。

4.3.1 参考源的选择

CH32 系列提供三种参考源:

参考源 精度 温漂 成本
内部参考(1.2V/2.5V) ±1% ~50 ppm/℃ 免费
VDD供电 取决于电源 取决于电源 免费
外部参考(如REF3125) ±0.2% ~15 ppm/℃ ~2元
避坑指南:我曾经用 VDD 做参考源,结果电池从 3.7V 降到 3.3V,ADC 读数直接变了 5%。后来换成外部参考,问题解决。如果你的设备是电池供电,强烈建议用外部参考或内部参考。

4.3.2 软件补偿方法

如果硬件上已经用了内部参考,可以通过软件补偿来减小漂移。思路是:用 ADC 读一个已知精度的内部基准电压(比如 CH32 内部有 1.2V 基准),反推出当前参考电压的实际值。

// 软件补偿参考电压漂移
float ADC_GetCompensatedValue(uint16_t raw_adc)
{
    // 读取内部基准电压通道(通常为通道17)
    uint16_t vrefint_raw = ADC_GetChannelValue(ADC_CHANNEL_VREFINT);
    
    // 已知内部基准电压为1.2V(1200mV)
    // 实际参考电压 = 1200 * 4095 / vrefint_raw
    float actual_vref = 1200.0f * 4095.0f / vrefint_raw;
    
    // 补偿后的实际电压
    float voltage = (float)raw_adc * actual_vref / 4095.0f;
    
    return voltage;
}

这个方法我用了好几年,效果不错。补偿后精度可以从 ±1% 提升到 ±0.3% 左右。

4.4 软件滤波与硬件滤波选择

滤波这件事,硬件和软件各有千秋。我的原则是:能硬件解决的别用软件,能软件解决的别改硬件。

4.4.1 硬件滤波

硬件滤波是在 ADC 输入引脚前加 RC 低通滤波器。优点是实时性好,不占 CPU;缺点是增加了 BOM 成本,而且截止频率固定。

我常用的 RC 参数:

  • 一般传感器:R=1kΩ, C=0.1μF,截止频率 ~1.6kHz
  • 音频信号:R=100Ω, C=1μF,截止频率 ~1.6kHz
  • 电源监测:R=10kΩ, C=10μF,截止频率 ~1.6Hz
一个细节:RC 滤波器的 R 不能太大,否则 ADC 内部的采样电容充电时间会变长,导致采样值偏低。我一般限制 R 不超过 10kΩ。

4.4.2 软件滤波

软件滤波灵活,但会消耗 CPU 时间。常用的几种:

方法 原理 优点 缺点
均值滤波 连续采样N次取平均 简单,对随机噪声有效 对脉冲干扰敏感
中值滤波 排序后取中间值 抗脉冲干扰强 计算量大,有延迟
一阶低通滤波 Y(n) = α*X(n) + (1-α)*Y(n-1) 平滑,实时性好 有相位滞后
滑动平均 维护一个窗口,每次更新 响应快,平滑度可调 需要缓冲区

我个人最常用的是 滑动平均 + 中值滤波 的组合。先做一次 3 点中值剔除异常值,再做 8 点滑动平均。这个组合在工业项目中表现很稳。

#define FILTER_WINDOW 8

uint16_t ADC_SlidingMedianFilter(uint16_t new_sample)
{
    static uint16_t buffer[FILTER_WINDOW];
    static uint8_t index = 0;
    uint16_t temp[FILTER_WINDOW];
    uint16_t median;
    
    // 更新缓冲区
    buffer[index++] = new_sample;
    if(index >= FILTER_WINDOW) index = 0;
    
    // 复制并排序
    memcpy(temp, buffer, sizeof(temp));
    // 简单冒泡排序(窗口小,够用)
    for(uint8_t i = 0; i < FILTER_WINDOW - 1; i++) {
        for(uint8_t j = 0; j < FILTER_WINDOW - 1 - i; j++) {
            if(temp[j] > temp[j+1]) {
                uint16_t t = temp[j];
                temp[j] = temp[j+1];
                temp[j+1] = t;
            }
        }
    }
    
    // 取中值
    median = temp[FILTER_WINDOW / 2];
    
    return median;
}

4.4.3 如何选择

我的决策流程是这样的:

  1. 先看噪声频率:如果是 50Hz 工频干扰,硬件滤波更直接
  2. 再看 CPU 余量:如果 CPU 空闲率 > 50%,用软件滤波;否则加硬件
  3. 最后看成本:量产产品,能省一个电容就省一个
我的经验:大多数物联网传感器场景,用 10kΩ + 0.1μF 的硬件滤波 + 8 点滑动平均软件滤波,效果已经很好。别过度设计。

4.5 本章知识结构

下面这张图把 ADC 采集与校准的核心链路画出来了,从初始化到最终输出稳定数据,每一步都有对应的技术点:

ADC采集与校准知识结构图 ADC采集与校准核心链路 ADC初始化 GPIO配置为AIN 时钟使能 硬件校准 多通道采集配置 扫描模式 vs 单次模式 各通道独立采样时间 采样率 vs 精度权衡 采样周期选择:1.5Cycles ~ 239.5Cycles 参考电压漂移问题 内部/外部参考选择 软件补偿方法 硬件滤波(RC低通) 软件滤波(均值/中值/滑动) 组合策略选择 最终目标:稳定、准确、高效的ADC数据输出

从这张图可以看得很清楚:ADC 的稳定数据不是靠某一个环节,而是从初始化到滤波的完整链路。每个环节都有对应的技术选择和权衡点。

最后说一句:调试 ADC 时,别急着上软件滤波。先把硬件参考电压测准,再用示波器看输入信号质量。很多时候,问题出在硬件上,软件只是背锅的。