前言
传感器接入单片机后,原始数据很少是一条理想的平滑曲线。温度、距离、压力、光照、电流、IMU 等信号都会受到量化误差、电源噪声、机械振动、通信抖动和环境变化影响。更麻烦的是,数据中的“跳变”不一定都是噪声:有些是偶发的毛刺,有些是缓慢叠加的随机波动,也有些恰好是真实物理量发生了阶跃变化。
因此,滤波的目标不是让曲线看起来越平滑越好,而是在抑制无效波动与保留有效变化之间取得平衡。窗口过大、滤波过强,可能让控制系统反应变慢;滤波过弱,又无法消除误触发和错误读数。
本文以嵌入式实时采样为背景,说明如何根据跳变形态选择均值滤波、中值滤波和一阶低通滤波,并给出可直接移植到 Arduino、STM32 或 ESP32 项目的 C/C++ 示例。
1. 先判断“跳变”属于哪一种
在写滤波代码前,先连续保存一段原始采样值,至少包含正常工况、干扰出现时刻以及信号真实变化时刻。建议同时记录采样时间戳,避免把采样周期不稳定误认为数据噪声。
常见数据形态可以分成以下几类:
| 数据形态 | 常见原因 | 首选方法 | 注意事项 |
|---|---|---|---|
| 围绕真实值的小幅随机波动 | ADC 量化、热噪声、轻微机械振动 | 均值滤波或低通滤波 | 会带来一定响应滞后 |
| 偶尔出现单点极大值或极小值 | I2C/SPI 读数异常、接触不良、超声波回波误判 | 中值滤波 | 先确认不是有效事件 |
| 连续多个样本突然偏离 | 电源扰动、外界强干扰、传感器失效 | 限幅 + 故障检测 | 单靠平滑滤波不够 |
| 真实值缓慢变化,但叠加高频抖动 | 温度、光照、电位器等慢变量 | 一阶低通滤波 | 根据采样周期设置系数 |
| 真实值本身发生阶跃 | 开关状态、接近目标、碰撞等 | 少滤波或使用状态判断 | 过度滤波会延迟响应 |
一个实用原则是:先按异常形态处理离群点,再做平滑。 例如超声波测距偶尔测到 0 cm 或 500 cm,应先判断该值是否在合理物理范围内;直接做均值可能会让这个异常值污染后续多个输出样本。
2. 三种滤波器的核心差异
| 方法 | 主要处理对象 | 对单点毛刺的抑制 | 响应速度 | RAM 占用 | 典型用途 |
|---|---|---|---|---|---|
| 均值滤波 | 连续随机波动 | 一般 | 窗口越大越慢 | 需要保存窗口 | 稳态 ADC、温度、光照 |
| 中值滤波 | 孤立异常点 | 很强 | 取决于窗口大小 | 需要保存并排序窗口 | 距离、接触不良读数、脉冲干扰 |
| 一阶低通 | 高频抖动 | 较弱 | 由系数决定 | 极低 | 连续慢变量、控制输入 |
三者不是互斥关系。实际项目中常见的组合是:
原始采样 -> 合法范围检查 -> 中值滤波 -> 一阶低通 -> 业务判断
这条链路尤其适合超声波、红外测距和部分模拟量传感器:中值滤波先去掉偶发错误读数,低通滤波再减小持续的小幅抖动。
3. 均值滤波:适合连续随机噪声
均值滤波使用最近 N 个样本的算术平均值作为输出:
y[k] = (x[k] + x[k-1] + ... + x[k-N+1]) / N
当噪声近似随机、正负波动大致对称时,多个样本平均后会互相抵消,因此均值滤波适合温度、光照、电压等变化相对平缓的信号。
3.1 固定窗口均值实现
以下实现维护一个环形缓冲区与累计和,每次更新的时间复杂度为 O(1),避免反复遍历整个数组。
template <size_t N>
class MovingAverage {
public:
float update(float sample) {
sum_ -= samples_[index_];
samples_[index_] = sample;
sum_ += sample;
index_ = (index_ + 1) % N;
if (count_ < N) {
++count_;
}
return sum_ / count_;
}
private:
float samples_[N] = {};
float sum_ = 0.0f;
size_t index_ = 0;
size_t count_ = 0;
};
MovingAverage<8> voltageFilter;
float filteredVoltage(float rawVoltage) {
return voltageFilter.update(rawVoltage);
}
3.2 窗口大小怎么定
窗口不是越大越好。采样周期为 T_s 时,N 个点大致覆盖 N × T_s 的历史数据。
-
每
10 ms采样一次、使用N = 8,输出会参考约80 ms内的数据。 -
每
100 ms采样一次、使用N = 20,输出会参考约2 s内的数据。 -
用于阈值报警时,应确认这段延迟是否会造成漏检或响应过慢。
均值滤波对单个离群点并不鲁棒。假设正常值约为 100,其中一个样本误读为 500,即便 N = 5,平均值也会被抬高约 80。所以存在明显毛刺时,应先做范围检查或中值滤波。
4. 中值滤波:优先处理孤立毛刺
中值滤波取一个窗口内样本排序后的中间值。对于 N = 5 的窗口:
样本:100, 101, 99, 500, 100
均值:180
中值:100
可以看到,单个 500 的异常样本会严重影响均值,但几乎不会影响中值。中值滤波因此很适合处理偶发尖峰、通信偶发错误、回波误判和接触抖动。
4.1 使用奇数窗口
窗口长度通常取奇数,例如 3、5、7。这样排序后存在唯一中位数,不需要额外定义两个中间值如何合并。嵌入式项目中 3 点中值和 5 点中值最常用:
-
3点:延迟低,能清除单点毛刺。 -
5点:抗离群能力更强,适合错误读数较多的环境。 -
7点及以上:计算和延迟增加,只有干扰持续较多采样点时才考虑。
4.2 小窗口中值实现
以下示例使用 5 个样本,复制后进行插入排序。对于很小的固定窗口,代码简单且开销可控。
template <size_t N>
class MedianFilter {
public:
static_assert(N % 2 == 1, "Median window must be odd");
float update(float sample) {
samples_[index_] = sample;
index_ = (index_ + 1) % N;
if (count_ < N) {
++count_;
}
float sorted[N];
for (size_t i = 0; i < count_; ++i) {
sorted[i] = samples_[i];
}
for (size_t i = 1; i < count_; ++i) {
float value = sorted[i];
size_t j = i;
while (j > 0 && sorted[j - 1] > value) {
sorted[j] = sorted[j - 1];
--j;
}
sorted[j] = value;
}
return sorted[count_ / 2];
}
private:
float samples_[N] = {};
size_t index_ = 0;
size_t count_ = 0;
};
MedianFilter<5> distanceFilter;
中值滤波不是故障诊断的替代品。如果异常值连续出现,说明传感器安装、通信链路、电源或测量条件可能存在真实问题。此时应记录异常次数并上报状态,而不是无限吞掉错误数据。
5. 一阶低通滤波:用一个系数平衡平滑与响应
一阶低通滤波也常被称为指数滑动平均。它不需要保存历史窗口,只保留上一次输出:
y[k] = y[k-1] + alpha × (x[k] - y[k-1])
其中:
-
x[k]是当前原始样本。 -
y[k]是当前滤波输出。 -
alpha位于0到1之间。
alpha 越小,输出越平滑,但跟随真实变化越慢;alpha 越大,输出越灵敏,但对噪声的抑制越弱。
5.1 适合实时项目的实现
class LowPassFilter {
public:
explicit LowPassFilter(float alpha) : alpha_(alpha) {}
float update(float sample) {
if (!initialized_) {
value_ = sample;
initialized_ = true;
} else {
value_ += alpha_ * (sample - value_);
}
return value_;
}
void reset(float sample) {
value_ = sample;
initialized_ = true;
}
private:
float alpha_;
float value_ = 0.0f;
bool initialized_ = false;
};
LowPassFilter temperatureFilter(0.2f);
float filteredTemperature(float rawTemperature) {
return temperatureFilter.update(rawTemperature);
}
初始化时要直接使用第一个有效样本。若默认从 0 开始滤波,设备刚启动时会出现一段不真实的爬升过程,可能误触发低阈值告警。
5.2 根据采样周期设置 alpha
如果希望用时间常数 tau 描述平滑强度,可根据实际采样周期 T_s 计算:
alpha = T_s / (tau + T_s)
例如每 20 ms 采样一次,希望输出在约 180 ms 的尺度上平滑:
alpha = 0.02 / (0.18 + 0.02) = 0.1
这比固定写一个经验值更可靠,尤其当任务调度改变、采样周期从 10 ms 改为 100 ms 时。若采样周期存在明显抖动,应使用真实的时间差 dt 动态计算系数,或先让采样定时器保持稳定。
6. 一套实际可用的处理链路
对容易出现离群值又需要平滑输出的传感器,可以采用“有效性检查 + 中值 + 低通”的组合:
MedianFilter<5> medianFilter;
LowPassFilter lowPassFilter(0.2f);
bool readFilteredDistance(float rawDistance, float& result) {
// 范围应按具体传感器规格和安装条件配置。
if (rawDistance < 2.0f || rawDistance > 400.0f) {
return false;
}
const float median = medianFilter.update(rawDistance);
result = lowPassFilter.update(median);
return true;
}
处理顺序很重要:
-
先拒绝协议错误值、超量程值和不符合物理约束的值。
-
使用中值滤波抵抗偶发毛刺。
-
使用低通滤波减小连续抖动。
-
最后再做阈值判断、PID 控制或数据上报。
对于安全相关或闭环控制场景,不能只依赖滤波后的值。还需要保留原始值、异常计数、连续有效样本数量和超时状态,避免滤波器用旧数据掩盖传感器已经失效的事实。
7. 滤波参数的验证方法
参数应来自采样数据和业务约束,而不是“看起来顺眼”。建议在串口、CSV 文件或上位机曲线中同时输出原始值和滤波值,并完成以下测试:
-
静态测试:传感器保持不动,比较滤波前后的峰峰值和标准差。
-
阶跃测试:快速改变被测量,例如移动目标物,记录输出达到新值 90% 所需时间。
-
毛刺测试:模拟一次错误读数,确认不会导致报警、控制输出或 UI 误跳变。
-
持续异常测试:让传感器持续返回非法值,确认系统会报告故障而不是一直显示旧的平滑结果。
-
边界测试:在量程上下限、阈值附近和上电初始阶段测试,检查滤波初始化与滞回逻辑。
可以用下面的记录格式快速对比:
time_ms, raw, median, lowpass, valid
0, 102.1, 102.1, 102.1, 1
20, 101.8, 101.8, 102.0, 1
40, 486.0, 102.1, 102.0, 1
60, 102.3, 102.1, 102.0, 1
80, -1.0, -, -, 0
8. 常见误区
8.1 把滤波当成修复硬件问题
若数据跳变与电机启动、继电器吸合、线缆移动或电源切换强相关,应优先排查供电、接地、屏蔽、传感器安装和通信时序。滤波只能降低表现,不能修复错误来源。
8.2 所有传感器都套同一组参数
温度变化慢,可接受较强平滑;避障测距和姿态控制则更关注延迟。不同传感器、不同采样频率和不同业务目标不应共用同一个窗口或 alpha。
8.3 只过滤、不设置滞回
当测量值在报警阈值附近上下抖动时,即便做过滤波,仍可能频繁切换状态。对开关类业务应配合滞回:例如高于 32.0 才报警,低于 30.0 才解除报警。
8.4 忽略滤波引入的延迟
滤波输出一定会滞后于真实输入。用于电机保护、碰撞检测或高速控制前,必须以最坏情况评估延迟,并保留快速故障通道。
9. 选型结论
可以用下面的规则快速起步:
-
数据只是连续小幅抖动,且允许短暂延迟:选均值滤波。
-
数据会偶发跳到明显不合理的极值:先选中值滤波。
-
信号持续变化,需要很低的 RAM 占用和可调的平滑程度:选一阶低通滤波。
-
同时存在毛刺与高频抖动:采用范围检查 + 中值 + 低通的组合。
-
数据异常连续出现或与硬件动作相关:先解决传感器、电源、接线和安装问题,再讨论滤波。
最终应以“误触发是否减少、真实变化是否仍能及时响应、异常是否能够被识别”为验收标准。曲线平滑只是结果,可靠地保留有效信息才是滤波设计的目标。

