DS18B20第一次读数可能是85°C,超声波测距偶尔会出现异常值,光敏电阻的ADC读数会随机跳动。传感器数据不稳定是常态,滤波算法就是用来对付这些问题的。

项目 Driver/filtering.c 里实现了两种最常用的滤波算法,这一篇完整解析它们的原理和代码实现。


什么时候需要滤波?

噪声类型 来源 典型表现
高频随机噪声 电磁干扰、电源纹波 读数在小范围内随机跳动
偶发脉冲干扰 静电、接触不良 偶尔出现一个极端异常值
环境渐变 温度漂移 读数有缓慢的趋势性变化

中值滤波擅长处理脉冲干扰(比如超声波偶尔读到的异常大值),均值滤波擅长平滑高频噪声。它们各有所长,不能互相替代。


中值滤波:抗脉冲干扰的利器

原理

取最近N次采样值,排序,取中间的那个作为输出。

原始数据:[25.2, 25.1, 85.0, 25.3, 25.2]  ← 85.0是异常值
排序后:  [25.1, 25.2, 25.2, 25.3, 85.0]
取中值:  25.2  ← 异常值被完美过滤!

无论异常值有多大,只要它不占多数,中值滤波都能把它过滤掉。这是均值滤波做不到的。

完整代码(项目实际代码)

#define N 5  // 滤波窗口大小(必须为奇数)

pdata float median_data_array[5] = {0};      // 环形缓冲区
pdata unsigned char median_data_index = 0;     // 写入指针
pdata unsigned char median_data_count = 0;     // 有效数据计数

float Median_Filter(float New_Data) {
    unsigned char i, j;
    float Median_Filter_Temp_Array[N] = {0};   // 排序用临时数组
    float temp;

    /* 第一步:新数据入队 */
    median_data_array[median_data_index++] = New_Data;
    median_data_index %= N;   // 循环覆盖最旧数据

    /* 第二步:更新有效数据计数 */
    if(median_data_count < N)
        median_data_count++;

    /* 第三步:复制到临时数组 */
    for(i = 0; i < median_data_count; i++) {
        Median_Filter_Temp_Array[i] = median_data_array[i];
    }

    /* 第四步:冒泡排序 */
    for(i = 0; i < median_data_count; i++) {
        for(j = i + 1; j < median_data_count; j++) {
            if(Median_Filter_Temp_Array[i] > Median_Filter_Temp_Array[j]) {
                temp = Median_Filter_Temp_Array[i];
                Median_Filter_Temp_Array[i] = Median_Filter_Temp_Array[j];
                Median_Filter_Temp_Array[j] = temp;
            }
        }
    }

    /* 第五步:返回中值 */
    return Median_Filter_Temp_Array[median_data_count / 2];
}

环形缓冲区原理

N = 5, 初始状态:

写入25.2:  [25.2,  0.0,  0.0,  0.0,  0.0]  index=1, count=1
写入25.1:  [25.2, 25.1,  0.0,  0.0,  0.0]  index=2, count=2
写入25.5:  [25.2, 25.1, 25.5,  0.0,  0.0]  index=3, count=3
写入25.2:  [25.2, 25.1, 25.5, 25.2,  0.0]  index=4, count=4
写入85.0:  [25.2, 25.1, 25.5, 25.2, 85.0]  index=0, count=5  ← 队列满了

再写入25.4:  [25.4, 25.1, 25.5, 25.2, 85.0]  index=1
              ↑ 覆盖了最旧的25.2
排序后:      [25.1, 25.2, 25.4, 25.5, 85.0]
中值:        25.4  ← 85.0被排到末尾,不影响结果

median_data_index %= N 这个取模操作是实现环形缓冲的关键——指针到末尾后自动回到开头。


均值滤波:平滑高频噪声

原理

取最近N次采样值的平均值。代码中使用了滑动窗口均值,只需要O(1)的时间复杂度:

#define N 5

pdata unsigned char mean_data_array[N] = {0};  // 滑动窗口
pdata unsigned char mean_sum = 0;               // 窗口内数据和
pdata unsigned char mean_data_index = 0;        // 写入指针
pdata unsigned char mean_data_count = 0;        // 有效数据计数

unsigned char Mean_Filter(unsigned char New_Data) {
    /* 第一步:减去即将被覆盖的旧值 */
    mean_sum -= mean_data_array[mean_data_index];

    /* 第二步:写入新值 */
    mean_data_array[mean_data_index] = New_Data;

    /* 第三步:加上新值 */
    mean_sum += mean_data_array[mean_data_index];

    /* 第四步:移动指针 */
    mean_data_index = (++mean_data_index) % N;

    /* 第五步:更新计数 */
    if(mean_data_count < N)
        mean_data_count++;

    /* 第六步:返回均值 */
    return mean_sum / mean_data_count;
}

滑动窗口为什么高效?

传统均值滤波每次都要遍历N个数据求和,时间复杂度O(N)。滑动窗口利用上一次的和:

sum_new = sum_old - data_old + data_new

示例(N=5):
  当前窗口:[10, 12, 11, 50, 13]  sum=96
  新数据:25
  减去最旧的10,加上25:
  新窗口:[12, 11, 50, 13, 25]  sum = 96-10+25 = 111
  均值 = 111/5 = 22.2

每个新数据只需要一次减法、一次加法和一次除法,不需要遍历数组。虽然均值滤波对异常值敏感(上面例子中85.0会拉高均值),但它计算量小,适合对实时性要求高的场景。


两种算法的对比

特性 中值滤波 均值滤波
数据类型 float unsigned char
内存占用 约22字节 约8字节
计算复杂度 O(N²)(排序) O(1)(滑动窗口)
抗脉冲干扰 优秀
平滑效果 一般 优秀
适用场景 超声波、温度 ADC电压、光强

实际效果对比(输入数据流:25.2, 25.1, 85.0, 25.3, 25.2, 25.4, 25.1):

中值滤波(N=5):
  25.2 → 25.15 → 25.2 → 25.3 → 25.2
  (85.0被完全过滤!)

均值滤波(N=5):
  25.2 → 25.15 → 37.12 → 32.92 → 25.2
  (85.0的影响持续了2次!)

调度器本身就是天然滤波

有时候不需要显式的滤波算法。看一下调度器的配置:

{Read_Tem,    100,  0},     // 每100ms读一次温度
{Get_Distance, 999,  0},    // 每999ms读一次距离

采样周期本身就构成了一种低通滤波——采样间隔越远,滤除的高频噪声越多。对于大多数蓝桥杯题目来说,调度器的采样间隔已经够用了。

但如果题目明确要求"显示稳定"或者传感器噪声特别大(比如超声波),那就需要加滤波。建议:

  • DS18B20温度:加中值滤波(N=5),防止85°C初始值和偶发跳变
  • 超声波距离:加中值滤波(N=5),防止偶发异常值
  • PCF8591光敏/电位器:均值滤波(N=3~5)就够了

补充:其他实用滤波技巧

限幅滤波

// 新值与上次值的差超过阈值就丢弃
unsigned char Limit_Filter(unsigned char new_val, unsigned char last_val,
                           unsigned char threshold) {
    if(abs(new_val - last_val) > threshold)
        return last_val;    // 超限,保持旧值
    return new_val;
}

一阶滞后滤波(最简单)

// output = alpha * new + (1-alpha) * last
float Low_Pass_Filter(float new_val, float last_val, float alpha) {
    return alpha * new_val + (1 - alpha) * last_val;
}
// alpha = 0.1~0.3,越小滤波越强但响应越慢

一阶滞后滤波只需要保存上一次的输出值,内存占用极小(4字节),适合RAM紧张的场合。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐