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);
}
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 | 精密测量、电池电压 |
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元 |
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
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 如何选择
我的决策流程是这样的:
- 先看噪声频率:如果是 50Hz 工频干扰,硬件滤波更直接
- 再看 CPU 余量:如果 CPU 空闲率 > 50%,用软件滤波;否则加硬件
- 最后看成本:量产产品,能省一个电容就省一个
4.5 本章知识结构
下面这张图把 ADC 采集与校准的核心链路画出来了,从初始化到最终输出稳定数据,每一步都有对应的技术点:
从这张图可以看得很清楚:ADC 的稳定数据不是靠某一个环节,而是从初始化到滤波的完整链路。每个环节都有对应的技术选择和权衡点。