4、ADC接口驱动开发:ADC工作原理、采样率设置、多通道采集、数据滤波

ADC,模数转换器,说白了就是把模拟电压变成数字值。做消防报警系统,传感器出来的信号全是模拟量——烟雾浓度、温度、气体浓度,都得靠ADC来“翻译”成单片机能读懂的0和1。

我刚开始做嵌入式那会儿,觉得ADC不就是读个电压嘛,有啥难的?后来踩了不少坑才明白,采样率设不对、通道切换没处理好、数据抖动不滤波,系统根本没法稳定工作。今天咱们就把ADC驱动开发这件事彻底聊透。

4.1 ADC工作原理——你得知道它怎么“翻译”的

ADC的核心工作,就是比较。它拿输入的模拟电压跟内部参考电压做比较,输出一个对应的数字码。

常见的ADC类型有这么几种:

  • 逐次逼近型(SAR ADC):用得最多。速度适中,精度不错,功耗低。消防报警里大部分MCU内置的都是这种。
  • Σ-Δ型(Sigma-Delta):精度极高,但速度慢。适合做称重、精密测量,消防里偶尔用在气体检测上。
  • 双积分型:抗干扰能力强,但慢得离谱。老式万用表里常见,嵌入式里基本不用了。

我个人的习惯是,先看MCU数据手册里的ADC章节。重点关注三个参数:

参数说明消防场景要求
分辨率比如12位,就是0~409510位以上够用,12位更稳
转换时间一次转换需要多少微秒一般1~10μs都行
参考电压决定量程范围通常用内部2.5V或3.3V

举个例子,STM32F103的ADC是12位、逐次逼近型。它的转换过程大致是这样:

// 伪代码描述ADC转换流程
1. 启动转换(设置ADC_CR2的ADON位)
2. 等待转换完成(检查EOC标志位)
3. 读取数据寄存器(ADC_DR)
4. 返回12位数字值(0~4095)

嗯,这里要注意:参考电压的稳定性直接影响精度。我在项目中遇到过,直接用板子上的3.3V供电做参考,结果传感器一动作,电压掉到3.2V,ADC读数直接飘了3%。后来老老实实加了外部基准电压芯片,问题才解决。

4.2 采样率设置——不是越快越好

采样率,就是ADC每秒采多少次。单位是SPS(Samples Per Second)。

你想想看,消防报警里的烟雾传感器,信号变化很慢,几赫兹都算快的。温度传感器更慢,一秒采一次都嫌多。但如果你做的是电弧检测或者快速响应气体泄漏,那采样率就得往上提。

采样率怎么设?核心公式就一个:

采样率 ≥ 2 × 信号最高频率(奈奎斯特采样定理)

实际工程中,我一般取5~10倍,留足余量。

具体到代码里,采样率由两个东西决定:

  • ADC时钟频率:比如STM32的ADC最高14MHz,但精度会下降。我一般设12MHz。
  • 采样周期:每个通道采多久。采样周期越长,精度越高,但速度越慢。
// STM32 ADC采样率配置示例
// 目标:采样率 100K SPS
// ADC时钟 = 12MHz,采样周期 = 7.5个周期
// 转换时间 = (7.5 + 12.5) / 12MHz ≈ 1.67μs
// 采样率 ≈ 1 / 1.67μs ≈ 600K SPS

ADC_InitTypeDef ADC_InitStruct;
ADC_InitStruct.ADC_Resolution = ADC_Resolution_12b;
ADC_InitStruct.ADC_ScanConvMode = DISABLE;
ADC_InitStruct.ADC_ContinuousConvMode = ENABLE;
ADC_InitStruct.ADC_ExternalTrigConv = ADC_ExternalTrigConv_T1_CC1;
ADC_InitStruct.ADC_DataAlign = ADC_DataAlign_Right;
ADC_InitStruct.ADC_NbrOfChannel = 1;
HAL_ADC_Init(&hadc, &ADC_InitStruct);

我的经验:采样率不是越高越好。采样率太高,CPU被ADC中断占满,其他任务没法跑。消防报警系统里,我通常把采样率控制在10K~100K SPS,够用又不浪费资源。

4.3 多通道采集——轮着来,别打架

消防报警系统里,一个MCU要同时采集烟雾、温度、气体浓度好几个信号。这时候就得用多通道采集。

多通道采集有两种模式:

  • 顺序扫描:一个通道采完,自动切到下一个。适合通道数不多、采样率要求不高的场景。
  • 注入式采集:优先级高的通道可以“插队”。适合紧急信号需要立即响应的场景。

我个人的习惯是,消防报警里用顺序扫描就够了。每个通道分配一个固定的采样周期,轮着来。

// 多通道顺序扫描配置(STM32 HAL库)
// 通道0:烟雾传感器(采样周期7.5)
// 通道1:温度传感器(采样周期13.5)
// 通道2:气体传感器(采样周期28.5)

ADC_ChannelConfTypeDef sConfig;

// 配置通道0
sConfig.Channel = ADC_CHANNEL_0;
sConfig.Rank = 1;
sConfig.SamplingTime = ADC_SAMPLETIME_7CYCLES_5;
HAL_ADC_ConfigChannel(&hadc, &sConfig);

// 配置通道1
sConfig.Channel = ADC_CHANNEL_1;
sConfig.Rank = 2;
sConfig.SamplingTime = ADC_SAMPLETIME_13CYCLES_5;
HAL_ADC_ConfigChannel(&hadc, &sConfig);

// 配置通道2
sConfig.Channel = ADC_CHANNEL_2;
sConfig.Rank = 3;
sConfig.SamplingTime = ADC_SAMPLETIME_28CYCLES_5;
HAL_ADC_ConfigChannel(&hadc, &sConfig);

我曾经踩过的坑:多通道切换时,通道之间会互相串扰。尤其是高阻抗传感器,前一个通道的残余电荷会影响下一个通道的读数。解决办法是:在切换通道后,先丢弃第一次采样值,或者加长采样周期。

4.4 数据滤波——把毛刺去掉

ADC读回来的数据,直接拿来用?别闹。电源噪声、传感器抖动、电磁干扰,都会让数据跳来跳去。消防报警系统要是直接拿原始数据做判断,误报率能让你崩溃。

常用的滤波方法有这几种:

方法原理适用场景
均值滤波连续采N次,取平均噪声随机、信号变化慢
中值滤波采N次,排序取中间值有突发脉冲干扰
一阶低通滤波新值 = 旧值 × α + 新值 × (1-α)实时性要求高、内存小
滑动平均滤波维护一个队列,取队列平均值需要平滑且响应快

我个人最喜欢用一阶低通滤波。代码简单,不占内存,效果还凑合。消防报警里,烟雾和温度信号变化慢,用这个正合适。

// 一阶低通滤波实现
// alpha = 0.1 表示滤波强度大,响应慢
// alpha = 0.9 表示滤波强度小,响应快

#define ALPHA 0.2f

uint16_t adc_filter(uint16_t new_value, uint16_t last_value)
{
    return (uint16_t)((float)last_value * ALPHA + (float)new_value * (1.0f - ALPHA));
}

// 使用示例
uint16_t adc_raw;
uint16_t adc_filtered = 0;

while(1)
{
    adc_raw = HAL_ADC_GetValue(&hadc);
    adc_filtered = adc_filter(adc_raw, adc_filtered);
    // 用adc_filtered做后续判断
}

我的建议:滤波参数别死记硬背。实际调试时,把原始数据和滤波后的数据同时打印出来,用串口助手看波形。调alpha值,直到你觉得“嗯,这个曲线看着舒服”为止。

还有一种情况要注意——突变信号。比如火灾发生时,烟雾浓度会突然飙升。这时候滤波太强,反而会延迟报警。我的做法是:检测到连续3次采样值超过阈值,就跳过滤波,直接报警。

// 突变检测逻辑
if(adc_raw > THRESHOLD_HIGH)
{
    count_high++;
    if(count_high >= 3)
    {
        // 直接报警,跳过滤波
        alarm_on();
    }
}
else
{
    count_high = 0;
    // 正常滤波
    adc_filtered = adc_filter(adc_raw, adc_filtered);
}

好了,ADC接口驱动开发的核心内容就这些。总结一下:

  • 理解ADC工作原理,重点关注分辨率和参考电压
  • 采样率根据信号频率来设,别盲目追求高采样率
  • 多通道采集用顺序扫描,注意通道切换时的串扰
  • 数据滤波用一阶低通或滑动平均,别让噪声影响判断

下一章咱们聊PWM接口驱动开发,到时候会讲到怎么用PWM控制蜂鸣器和LED调光。那玩意儿也挺有意思的。


公众号:蓝海资料掘金营,微信deep3321