4、ADC接口驱动开发:ADC工作原理、采样率设置、多通道采集、数据滤波
ADC,模数转换器,说白了就是把模拟电压变成数字值。做消防报警系统,传感器出来的信号全是模拟量——烟雾浓度、温度、气体浓度,都得靠ADC来“翻译”成单片机能读懂的0和1。
我刚开始做嵌入式那会儿,觉得ADC不就是读个电压嘛,有啥难的?后来踩了不少坑才明白,采样率设不对、通道切换没处理好、数据抖动不滤波,系统根本没法稳定工作。今天咱们就把ADC驱动开发这件事彻底聊透。
4.1 ADC工作原理——你得知道它怎么“翻译”的
ADC的核心工作,就是比较。它拿输入的模拟电压跟内部参考电压做比较,输出一个对应的数字码。
常见的ADC类型有这么几种:
- 逐次逼近型(SAR ADC):用得最多。速度适中,精度不错,功耗低。消防报警里大部分MCU内置的都是这种。
- Σ-Δ型(Sigma-Delta):精度极高,但速度慢。适合做称重、精密测量,消防里偶尔用在气体检测上。
- 双积分型:抗干扰能力强,但慢得离谱。老式万用表里常见,嵌入式里基本不用了。
我个人的习惯是,先看MCU数据手册里的ADC章节。重点关注三个参数:
| 参数 | 说明 | 消防场景要求 |
|---|---|---|
| 分辨率 | 比如12位,就是0~4095 | 10位以上够用,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