STM32 采集 NTC 温度抖动:RC 硬件滤波与软件滑动平均怎么选

上周给实验室的小恒温箱换主控,方案很朴素:STM32F103C8T6 加一颗 10K 的 NTC(B 值 3950),和 10K 电阻分压后接 ADC1 的 PA0,12 位采样、1Hz 上报温度。硬件焊完,串口助手一看就傻眼了------室温明明稳定在 24 度出头,显示值却在 23.6 到 25.2 之间来回跳,小数点后一位完全没法看,加热继电器一吸合,跳得更凶。

第一反应是电源不干净,于是把 5V 换成一颗低压差 LDO,又在探头两端并了 100nF,没本质改善;接着怀疑 ADC 本身,换了块板子,抖动照旧。最后老老实实把 ADC 原始码值和换算后的温度一起打出来,才看清楚问题不是"ADC 不准",而是我把三类完全不同的东西混在一起了:量化误差、随机噪声,还有一个来自继电器和工频的周期性干扰。这篇就把当时的判断顺序、计算过程和最后的取舍写清楚。

一、先把"抖"拆开,再谈滤波

很多教程一上来就教你写滑动平均,但滤波只能处理带内噪声,处理不了已经混叠的成分。所以第一步是分清抖动从哪来,我的做法是打印原始码值,而不是温度。

先算一笔账,看看 12 位 ADC 本身能有多细。参考电压 3.3V,一个 LSB 是:

text 复制代码
LSB = 3.3 V / 4096 ≈ 0.806 mV

再看分压的灵敏度。25 度时 NTC 和上拉电阻都约 10K,分压点约 1.65V;NTC 的阻值温度系数大约每个摄氏度变化 4% 左右,换算到分压点,每摄氏度带来几十毫伏的变化(按 10K 分压估算约 36mV/℃)。也就是说,一个 LSB 大约对应 0.02 度。

那么显示抖动 ±0.8 度,对应几十个 LSB,绝不是量化误差造成的。这一步很关键,因为它直接排除了"ADC 位数不够、换 16 位 ADC"这个看起来最顺手的错误方向。

接着看抖动的时间特征。我在 1kHz 下连续采样,把数据丢到串口:

  • 有一条固定周期约 20ms 的低幅波动,这是 50Hz 工频串进来的;
  • 继电器吸合瞬间有几个码值的尖峰,是触点动作和线圈反向电动势引起的;
  • 其余是围绕均值上下的小幅随机跳动。

这三种东西要用的手段不一样:工频靠硬件 RC 和屏蔽,尖峰靠中值滤波,随机噪声靠平均。如果只写一个滑动平均,尖峰会被"摊"成一个缓坡,工频则可能因为 1Hz 上报而混叠成慢漂移,看起来像温度真的在慢慢变,实际是采样率太低造成的假信号。

二、RC 低通放在 ADC 前面:算截止频率,也算建立时间

硬件滤波的价值有两个:压掉高频干扰,以及抗混叠。软件滤波再厉害,也没法把已经混叠进低频的成分分出来,所以只要条件允许,第一道滤波应该放在 ADC 之前。

一阶 RC 低通的截止频率是:

text 复制代码
fc = 1 / (2πRC)

我实际试过的两组参数:

R C 截止频率 fc 说明
1kΩ 100nF ≈ 1.59 kHz 只压高频,对 50Hz 几乎无作用
1kΩ 1µF ≈ 159 Hz 保留温度信号,工频衰减有限
10kΩ 1µF ≈ 15.9 Hz 工频压得下去,但建立时间变长

温度信号本身是慢变量,恒温箱的热时间常数是几十秒,带宽根本不需要超过几赫兹。理论上 15.9Hz 这组更合适。但我最后选的是 1kΩ + 1µF,串一个 100nF 到地做高频旁路,原因不在滤波效果,而在ADC 的源阻抗。

STM32 的 ADC 采样阶段,内部采样电容要在规定的采样时间内充电到足够精度。充电时间常数由源阻抗和采样电容共同决定,源电阻越大,需要的采样时间越长。10K 分压本身已经不算小,再串 10K,等效源阻抗就到 20K 量级;如果采样时间还按默认的最短档配,读数会系统性偏低。与其去改采样时间,不如把串联电阻压到 1K,让充电这件事变得无所谓,剩下的平滑交给软件------软件滤波不花钱、可调、还能顺便做异常值剔除。

这里有个容易被忽略的点:RC 的时间常数也决定了探头断线、接触不良时电压建立的速度。电容越大,"坏"的迹象被拖得越久。

三、软件侧三种滤波:原理、代价和固定点代码

软件滤波我用过三种,都在 100Hz 的采样节奏下跑,单片机是 Cortex-M3,有硬件除法,开销不是问题。

滑动平均最适合随机白噪声。N 点平均能把标准差压到原来的约 1/√N,代价是群延迟大约 (N-1)/2 个采样周期。也就是说,8 点平均在 100Hz 下有 35ms 的滞后。

c 复制代码
#define WIN 8

static uint16_t buf[WIN];
static uint8_t  idx;
static uint32_t sum;          /* 必须用 32 位,见后面的坑 */

uint16_t moving_avg_push(uint16_t x)
{
    sum -= buf[idx];
    buf[idx] = x;
    sum += x;
    idx = (idx + 1) % WIN;
    return (uint16_t)(sum / WIN);
}

这段的关键是"进一个、出一个",不用每次重新累加整个窗口,计算量和窗口大小无关。

中值滤波对付单点尖峰最有效。5 点窗口能干掉最多两个连续的异常值,而且不会像平均那样把尖峰摊开:

c 复制代码
static uint16_t median5(const uint16_t *v)
{
    uint16_t s[5];
    for (int i = 0; i < 5; i++) s[i] = v[i];
    for (int i = 1; i < 5; i++) {
        uint16_t t = s[i];
        int j = i - 1;
        while (j >= 0 && s[j] > t) { s[j + 1] = s[j]; j--; }
        s[j + 1] = t;
    }
    return s[2];
}

**一阶 IIR(指数滤波)**内存最小,两个字节就够,适合 RAM 紧张或者需要随时调平滑程度的场合:

c 复制代码
static uint16_t iir_update(uint16_t x)
{
    static int32_t y;
    /* y += (x - y) / 8,加 4 做四舍五入 */
    y += ((int32_t)x - y + 4) / 8;
    return (uint16_t)y;
}

注意这里必须先把 x 转成有符号再减,否则 x 小于 y 时无符号相减会回绕成一个巨大的正数,输出直接飞掉。

四、怎么选:把延迟预算摆到桌面上

三种方案没有绝对优劣,关键看你的系统能容忍多少延迟。我把它们放在一张表里对比,选型时按行对号入座:

方案 群延迟 抗单点尖峰 RAM 计算量 参数整定 适合场景
RC 一阶低通 约 1/(2πfc) 差 0 0 R、C 高频干扰、需要抗混叠
滑动平均 N 点 (N-1)/2 × Ts 一般,尖峰被摊平 2N 字节 每点两次加减 窗口长度 随机白噪声
中值 5 点 2 × Ts 强 10 字节 小数组排序 窗口长度 偶发毛刺、触点干扰
一阶 IIR 约 Ts/α 差 2 字节 两次运算 α 内存紧、需连续平滑

套到两个具体场景里,结论完全不同:

  • 恒温箱:热时间常数几十秒,几十毫秒的延迟毫无影响,可以放心用 8 点甚至 16 点平均,换更小的抖动;
  • 电机电流环:1kHz 采样,8 点平均就是 3.5ms 延迟,直接吃掉相位裕度,宁可只用 2 点平均或者一阶 IIR,甚至不滤。

这就是"设计取舍"的实质:不是找最强的滤波器,而是先算清楚系统还剩多少延迟预算,再往里填。

也顺便说一下为什么没上卡尔曼。卡尔曼需要状态方程和噪声统计,恒温箱这种一阶热系统当然能建模,但参数辨识和调参的成本远高于收益;而且模型一旦和实际箱子不一致,滤波反而会给出"看起来很平滑但系统性偏差"的结果,比抖动更难发现。

五、我最终的方案,和两个真实的坑

最后落地的链路是:NTC 分压 → 1kΩ + 1µF 的 RC → 定时器 1kHz 触发 ADC → DMA 循环搬运 16 点 → 中值 3 点 → 滑动平均 8 点 → 100Hz 得到温度,1Hz 上报一次。

为什么先中值再平均?因为中值先把偶发尖峰拿掉,平均再压随机噪声;顺序反过来,尖峰已经被平均"染"进窗口,中值就无能为力了。

第一个坑是累加变量溢出。我最初把窗口开到 32 点,sum 声明成 uint16_t,12 位满量程 4095,32 点累加远超 65535,直接回绕,温度偶尔跳到负数。改成 uint32_t 就没事了。这类错误不会报错,只会让数据"偶尔疯一下",排查时优先怀疑。

第二个坑更值得说:滤波曾经帮我掩盖了硬件问题。探头引线在箱门铰链处被反复弯折,内部铜丝将断未断,表现为随机的大幅尖峰。中值滤波把它吃得干干净净,曲线看着很漂亮,但真实温度偶发偏差两三度。后来我在调试版本里额外输出窗口内的最大值和最小值,一眼就看出"极差远超正常噪声",顺着查到线。滤波是让数据好看,不是让系统变好,极差、断线检测这类诊断量必须保留。

验证方法也很简单:用手指捏住探头,看滤波前后的阶跃响应。如果输出明显滞后或者有过冲,说明窗口太大或者 IIR 的 α 太小;如果响应正常但稳态还在抖,说明噪声没压够,再回头查干扰源。

小结

这件事我自己的执行顺序是固定的:先算量化误差,判断抖动是不是 ADC 分辨率的问题;再看时间特征,分清工频、尖峰和随机噪声;然后硬件上放一阶 RC,既压高频也抗混叠,同时注意源阻抗和采样时间;最后软件上按延迟预算选窗口,中值去尖峰、平均压噪声,累加一定要用够宽的整型。滤波解决的是"读数不好看",接触不良、干扰源这类根因,还得靠硬件和诊断量去抓。

相关推荐
糖糖单片机设计11 小时前
基于 STM32 与中移 OneNet 的智慧电梯监控平台系统
stm32·单片机·嵌入式硬件·51单片机·语音识别
创界工坊工作室14 小时前
55-基于STC89C52宠物智能饲喂控制系统设计
stm32·单片机·嵌入式硬件·51单片机·proteus·宠物
极客 - L U17 小时前
卡尔曼滤波从零到精通
c语言·数据结构·单片机·嵌入式硬件·mcu
AMASENSYS艾马斯18 小时前
变频器滤波器选型与EMC合规建议
驱动开发·单片机·其他·硬件工程·精益工程
码小文19 小时前
CAM350使用笔记(6)---Gerber文件铺铜间距检查
嵌入式硬件·cam350软件·cam350使用笔记
Zw-awa20 小时前
编译器工具链:GCC、Clang、MSVC、ICC、armclang
stm32
Yiran_G20 小时前
双频 Wi-Fi 6 + 蓝牙 5.4:无声讯通 WS8712 模组的开发与集成要点
网络·嵌入式硬件·物联网·智能家居
凯基迪科技21 小时前
STM32G系列(G0/G4)芯片解密程序提取RDP读保护解析
网络·stm32·安全
光子物联单片机21 小时前
ESP32传感器模块编程实践(一)DIY智能WIFI视频控制小车
嵌入式硬件·esp32·智能硬件·硬件