上周给实验室的小恒温箱换主控,方案很朴素: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,既压高频也抗混叠,同时注意源阻抗和采样时间;最后软件上按延迟预算选窗口,中值去尖峰、平均压噪声,累加一定要用够宽的整型。滤波解决的是"读数不好看",接触不良、干扰源这类根因,还得靠硬件和诊断量去抓。