别让杂波毁了你的项目!十大滤波算法「小白友好版」,看完就能用

做电子项目时,你有没有过这种崩溃时刻?

测温度时,数值突然从25℃蹦到99℃,以为传感器烧了,拆下来检查半天没毛病;

测水位时,数据忽高忽低像坐过山车,明明水箱没动,屏幕上的数字却跳个不停;

甚至有时候,明明代码逻辑没问题,设备却因为“假数据”乱动作

其实啊,不是传感器坏了,也不是代码写错了,多半是你忘了给数据“除杂”,

少了个关键步骤:滤波

今天就给大家扒一扒10种最常用的滤波算法,不用啃晦涩的公式,不用懂复杂的理论,像聊家常一样给你讲明白,看完不管是做Arduino项目,还是搞工业检测,都能直接上手用!

1. 限幅滤波法:给数据设个“跳崖警戒线”

(1)原理:别让数据“瞎蹦”

简单说,就是给数据定个“最大允许波动值A”——比如上次采集到的温度是25℃,这次突然变成35℃,如果A设为5,那35-25=10>5,就当这个35℃是“杂波捣蛋”,不算数,还沿用上次的25℃;要是这次只变到28℃,没超A,就用新的28℃。

(2)特点:能打“突袭杂波”,却怕“连环骚扰”

  • 优点:对付那种突然冒出来的“脉冲杂波”特别管用,比如电路偶尔受到干扰,数据跳一下,它能立刻按住。
  • 缺点:要是杂波是“周期性的”(比如每隔1秒就跳一次),它就没辙了;而且滤出来的数据不够平滑,像走台阶一样,一顿一顿的。

(3)例程:现成代码拿走用

直接复制到项目里,把FILTER_A改成你需要的“警戒线”就行:

int Filter_Value;
int Value;
// 限幅滤波法(又称程序判断滤波法)
#define FILTER_A 1  // 这里的1就是“警戒线”,根据你的数据改
int Filter() {
  int NewValue;
  NewValue = Get_AD();  // 获取新的传感器数据
  // 判断是否超过“警戒线”
  if(((NewValue - Value) > FILTER_A) || ((Value - NewValue) > FILTER_A))
    return Value;  // 超了就用上次的数
  else
    return NewValue;  // 没超就用新数
}

2. 算术平均滤波法:给数据“算平均分”

(1)原理:多抓几个数据,取个平均值

就像老师算班级平均分一样:连续采集N个数据,把它们加起来,再除以N,结果就是滤完波的数据。比如测压力,连续抓4个数据:101、102、99、100,加起来402,除以4得100.5,就用100.5当有效数据。

(2)说明:N选得好,滤波效果才好

选N有讲究,不是越大越好:

  • N大一点(比如12):数据会很平滑,但反应变慢,比如温度变了,要等12次采样才会显示变化,灵敏度低。
  • N小一点(比如4):数据反应快,但平滑度差,偶尔还是会有杂波。
  • 常见搭配:测流量(变动稍快)选N=12;测压力(相对稳)选N=4。

(3)特点:适合“稳一点”的杂波,不适合“急活”

  • 优点:对付那种“随机乱晃”的杂波很合适,比如数据在100左右跳来跳去,算完平均就稳了。
  • 缺点:一是慢,要是你需要实时控制(比如机器人避障,要立刻反应),它跟不上;二是费内存,要存N个数据,像囤太多快递占地方。

(4)例程:代码里的“平均分计算器”

int Filter_Value;
// 算术平均滤波法
#define FILTER_N 12  // 采集12个数据算平均,可改
int Filter() {
  int i;
  int filter_sum = 0;  // 用来存数据总和
  for(i = 0; i < FILTER_N; i++) {
    filter_sum += Get_AD();  // 逐个加数据
    delay(1);  // 等1毫秒再采下一个,避免太密集
  }
  return (int)(filter_sum / FILTER_N);  // 算平均并返回
}

3. 中位值滤波法:挑“中间派”当代表

(1)原理:数据排排队,中间那个最靠谱

先连续采集N个数据(N必须是奇数,比如5、7),把它们按大小排好队,然后挑正中间的那个当有效数据。比如采了5个温度:23、25、24、26、22,排好队是22、23、24、25、26,中间的24就是最终结果。

(2)特点:专治“突发极端值”,怕“急性子数据”

  • 优点:能把那种突然冒出来的“极端杂波”直接排除,比如测温度突然蹦到50℃(实际是24℃),排队后50℃在最右边,中间的24℃照样能用;尤其适合温度、湿度这种变很慢的参数。
  • 缺点:要是数据变得快(比如测水流速度,每秒都在变),等你采完N个数据排好队,实际数据早就变了,相当于“刻舟求剑”。

(3)例程:排序挑中间,代码给你写好了

int Filter_Value;
// 中位值滤波法
#define FILTER_N 101  // 101是奇数,可改(建议不要太小)
int Filter() {
  int filter_buf[FILTER_N];  // 存N个数据的数组
  int i, j;
  int filter_temp;  // 临时变量,用来排序交换
  // 先采N个数据存起来
  for(i = 0; i < FILTER_N; i++) {
    filter_buf[i] = Get_AD();
    delay(1);
  }
  // 冒泡排序:把数据从小到大排好队
  for(j = 0; j < FILTER_N - 1; j++) {
    for(i = 0; i < FILTER_N - 1 - j; i++) {
      if(filter_buf[i] > filter_buf[i + 1]) {
        filter_temp = filter_buf[i];
        filter_buf[i] = filter_buf[i + 1];
        filter_buf[i + 1] = filter_temp;
      }
    }
  }
  // 返回中间的那个数
  return filter_buf[(FILTER_N - 1) / 2];
}

4. 递推平均滤波法:给数据搞个“滚动队列”

(1)原理:新数据进队,老数据出局

想象有个固定长度的“数据队列”(比如能装12个数据),每次采集新数据,就把它放进队尾,同时把队首最老的数据扔掉,然后算现在队列里12个数据的平均值。比如队列里原来有1-12,新数据13进来,就扔掉1,队列变成2-13,再算这12个的平均。

(2)特点:能治“周期性杂波”,怕“脉冲突袭”

  • 优点:对付那种“周期性反复出现”的杂波(比如每秒都跳一次的干扰)特别厉害,滤出来的数据平滑得像刚熨过的衣服;适合电机振动、高频振荡这种场景。
  • 缺点:要是遇到偶尔的“脉冲杂波”(比如突然跳一次大的),它会把这个杂波算进平均里,相当于“一颗老鼠屎坏了一锅粥”;而且不适合开关电源电路,容易被电源干扰带偏。

(3)例程:滚动队列的代码实现

#define N 12  // 队列长度,可改
char value_buf[N];  // 存数据的队列
char i = 0;  // 记录队尾位置

char filter(void) {
    char count = 0;
    int sum = 0;  // 存队列数据总和

    value_buf[i++] = get_ad();  // 新数据进队尾
    if(i == N) {
        i = 0;    // 队列满了,下次从队首开始(先进先出)
    }
    // 算队列里所有数据的和
    for(count = 0; count < N; count++) {
        sum += value_buf[count];
    }
    return (char)(sum / N);  // 返回平均值
}

5. 中位值平均滤波法:“先筛再算”,双保险

(1)原理:先去掉极端值,再算平均

相当于“中位值滤波”+“算术平均滤波”的组合:先采N个数据,排好队,去掉最大的和最小的两个极端值,再把剩下的N-2个数据算平均。比如采10个数据:1、2、3、4、5、6、7、8、9、10,去掉1和10,剩下8个算平均。

(2)特点:杂波的“全能杀手”,就是有点慢

  • 优点:既继承了中位值的优点(能去掉极端脉冲杂波),又有平均的优点(能治周期性杂波),相当于“双buff加持”;平滑度高,适合高频振荡的系统。
  • 缺点:步骤多,要采集、排序、删极值、算平均,比单独的滤波法慢很多,要是你急着要数据(比如实时控制),它就有点跟不上。

(3)例程:先筛后算的代码

#define N 12  // 采集12个数据,可改
char filter(void) {
    char i = 0, j = 0, temp = 0;
    char value_buf[N];  // 存数据的数组
    int sum = 0;  // 存剩下数据的总和

    // 先采N个数据
    for(i = 0; i < N; i++){
        value_buf[i] = get_ad();
        delay();  // 等一会儿再采下一个
    }

    // 冒泡排序,把数据排好队
    for(j = 0; j < N - 1; j++) {
        for(i = 0; i < N - j; i++) {
            if(value_buf[i] > value_buf[i + 1]) {
                temp = value_buf[i];
                value_buf[i] = value_buf[i + 1];
                value_buf[i + 1] = temp;
            }
        }
    }

    // 去掉最大(最后一个)和最小(第一个),算中间的和
    for(i = 1; i < N - 1; i++) {
        sum += value_buf[i];
    }
    // 除以剩下的数据个数(N-2)
    return (char)(sum / (N - 2));
}

6. 限幅平均滤波法:“先拦后算”,稳上加稳

(1)原理:先设警戒线,再算平均

就是“限幅滤波”+“算术平均滤波”的组合:先给每个新采集的数据做“限幅检查”(没超A就留着,超了就用上次的数),等收集够N个“合格数据”后,再算它们的平均值。

(2)特点:杂波拦得住,就是费内存

  • 优点:结合了两种滤波的优点,既能拦住偶尔的脉冲杂波(限幅),又能让数据平滑(平均),相当于“先把门守好,再整理房间”。
  • 缺点:跟算术平均一样,要存N个数据,有点费内存;如果N选得大,内存占用会更明显。

(3)例程:带限幅的平均滤波代码

/**
 * 限幅平均滤波法
 * @param input 输入的原始采样数据数组
 * @param input_len 输入数据的长度
 * @param threshold 限幅阈值(“警戒线”),超过此值的变化被视为干扰
 * @param window_size 平均窗口大小(算几个数据的平均)
 * @param output 输出的滤波结果数组
 * @param output_len 输出结果的长度(通过指针返回)
 */
void filter(float *input, int input_len, float threshold,
           int window_size, float *output, int *output_len) {

    // 先申请一块内存存“合格数据”
    float *valid_data = (float *)malloc(input_len * sizeof(float));

    // 第一个数据直接算合格,不用检查
    valid_data[0] = input[0];
    float last_valid = input[0];  // 记录上一个合格数据
    int valid_count = 1;  // 合格数据的个数

    // 逐个检查数据,做限幅
    for (int i = 1; i < input_len; i++) {
        // 判断当前数据和上一个合格数据的差是否超阈值
        if (fabs(input[i] - last_valid) > threshold) {
            // 超了,用上次的合格数据
            valid_data[valid_count++] = last_valid;
        } else {
            // 没超,用当前数据,更新上一个合格数据
            last_valid = input[i];
            valid_data[valid_count++] = input[i];
        }
    }

    // 算合格数据的平均值
    *output_len = valid_count - window_size + 1;
    if (*output_len < 1) {  // 数据不够,返回空
        *output_len = 0;
        free(valid_data);
        return;
    }

    // 逐个窗口算平均
    for (int i = 0; i < *output_len; i++) {
        float sum = 0.0f;
        // 算当前窗口里数据的和
        for (int j = 0; j < window_size; j++) {
            sum += valid_data[i + j];
        }
        output[i] = sum / window_size;  // 平均结果存到输出数组
    }

    free(valid_data);  // 释放内存,别占着不用
}

7. 加权递推平均滤波法:给新数据“加权重”

(1)原理:新数据“话语权更大”

对“递推平均滤波”做了点改进:给队列里的每个数据贴个“权重”——越新的数据,权重越大(比如最新的数据权重12,前一个11,最老的1),然后用“数据×权重”的总和,除以“权重总和”,得到结果。就像老师改卷,期末考占60%,期中占40%,新数据的“分量”更重。

(2)特点:适合“慢反应设备”,不适合“慢数据”

  • 优点:适合两种场景:一是设备反应慢(比如大滞后的温度控制系统),二是采样周期短(数据更新快);新数据权重高,能更快反映实际变化。
  • 缺点:要是设备反应快、采样周期长(比如测缓慢变化的室温,10分钟采一次),这方法就像“用大炮打蚊子”——新数据和老数据差别不大,加权重也没用,滤波效果反而差。

(3)例程:带权重的递推平均代码

/* coe数组是加权系数表,越后面的数权重越大(可自己改) */
#define N 12  // 队列长度
char coe[N] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12};  // 权重数组
char sum_coe = 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 + 11 + 12;  // 权重总和

char filter(void) {
    char i = 0;
    char value_buf[N];  // 存数据的队列
    int sum = 0;  // 存“数据×权重”的总和

    // 先采N个数据存队列
    for(i = 0; i < N; i++) {
        value_buf[i] = get_ad();
        delay();
    }

    // 算“数据×权重”的总和
    for(i = 0; i < N; i++) {
        sum += value_buf[i] * coe[i];
    }

    // 除以权重总和,得到结果
    return (char)(sum / sum_coe);
}

8. 消抖滤波法:给数据“一点耐心”

(1)原理:等数据“稳定下来”再认

搞个“消抖计数器”,每次采新数据就和当前的“有效数据”比:

  • 如果一样,说明数据稳了,计数器清零;
  • 如果不一样,计数器就+1,再等下次采样;
  • 要是计数器加到上限(比如12),还不一样,就把新数据当成有效数据,计数器再清零。

就像你按开关,偶尔手抖按一下没反应,要按稳几秒才会触发——避免数据“手抖”。

(2)特点:能治“临界抖动”,怕“干扰撞上限”

  • 优点:对付慢变数据的“临界抖动”特别管用,比如测水位在“报警线”附近跳来跳去,用它能让数据稳定下来,不会频繁触发报警;也能避免显示器上的数字“跳广场舞”。
  • 缺点:不适合快变数据(比如测车速,一秒变一次);而且如果计数器刚满的时候,刚好采到的是杂波,就会把杂波当成有效数据,相当于“认错人”。

(3)例程:消抖计数器的代码

#define N 12  // 计数器上限,可改(越大越稳,但越慢)
char filter(void) {
    char i = 0;
    char new_value = 0, value = 0;  // new_value:新数据;value:当前有效数据

    new_value = get_ad();  // 先采第一个新数据

    // 只要新数据和有效数据不一样,就继续等
    while(value != new_value) {
        i++;  // 计数器+1
        if(i > N) {  // 计数器超上限,认新数据
            return new_value;
        }
        delay();  // 等一会儿再采
        new_value = get_ad();  // 采新的一次数据
    }
    // 数据一样,返回有效数据
    return value;
}

9. 限幅消抖滤波法:“先拦后等”,双重保险

(1)原理:先拦瞎蹦的数据,再等它稳定

相当于“限幅滤波”+“消抖滤波”的组合:

  1. 先做限幅:新数据和上次有效数据比,超A就用上次的,没超就用新的;
  2. 再做消抖:用限幅后的“候选数据”和当前有效数据比,不一样就计数,超上限再更新有效数据。

(2)特点:杂波拦得严,就是慢一点

  • 优点:比单独的消抖滤波更靠谱——先把“跳得太狠”的杂波拦在门外,再等数据稳定,不会把明显的干扰当成有效数据;继承了限幅和消抖的所有优点。
  • 缺点:和消抖滤波一样,不适合快变数据;步骤多了一点,反应会比单独的滤波法慢。

(3)例程:限幅+消抖的组合代码

int Filter_Value;
int Value;

// 限幅消抖滤波法
#define FILTER_A 1  // 限幅阈值(警戒线)
#define FILTER_N 5  // 消抖计数器上限
int i = 0;  // 消抖计数器

int Filter() {
  int NewValue;
  int new_value;  // 限幅后的候选数据

  NewValue = Get_AD();  // 采新数据

  // 第一步:限幅
  if(((NewValue - Value) > FILTER_A) || ((Value - NewValue) > FILTER_A))
    new_value = Value;  // 超阈值,用上次的
  else
    new_value = NewValue;  // 没超,用新的

  // 第二步:消抖
  if(Value != new_value) {
    i++;  // 计数器+1
    if(i > FILTER_N) {  // 超上限,更新有效数据
      i = 0;
      Value = new_value;
    }
  }
  else
    i = 0;  // 数据一样,计数器清零

  return Value;  // 返回有效数据
}

10. 一阶滞后滤波法:给数据“一点缓冲”

(1)原理:新数据和老数据“掺着用”

设个系数α(α在0到1之间,比如0.01),滤波后的结果=(1-α)×新采样值 + α×上次滤波结果。比如上次结果是25,新采样值是26,α=0.01,那这次结果=0.99×26 + 0.01×25≈25.99,既接近新数据,又不会突然跳变。

α越小,新数据占比越高,反应越快;α越大,老数据占比越高,越平滑。

(2)特点:能治“高频杂波”,有“延迟后遗症”

  • 优点:对付周期性的高频杂波很厉害,数据平滑度高;适合波动频率高的场景(比如电机运行时的电流检测)。
  • 缺点:有“相位滞后”——实际数据变了,滤波后的结果要慢一点才会跟上,像反应慢半拍;而且α越大,滞后越明显;另外,滤不掉频率比采样频率一半还高的杂波(比如采样每秒10次,滤不掉每秒5次以上的杂波)。

(3)例程:带缓冲的一阶滞后代码

int Filter_Value;
int Value;

// 一阶滞后滤波法
#define FILTER_A 0.01  // 系数α,0<α<1,可改
int Filter() {
  int NewValue;
  NewValue = Get_AD();  // 采新数据
  // 按公式计算滤波结果
  Value = (int)((float)NewValue * (1.0 - FILTER_A) + FILTER_A * (float)Value);
  return Value;  // 返回结果
}

最后:怎么选对滤波算法?记住这3条

  1. 看杂波类型:偶尔跳一次选“限幅”,周期性跳选“递推平均”,极端值多选“中位值”;
  2. 看数据速度:慢变数据(温度、湿度)用“中位值”“消抖”,快变数据(流量、速度)用“限幅”“一阶滞后”;
  3. 看实时性:要快就用“限幅”“一阶滞后”,不着急就用“中位值平均”“限幅平均”。

其实不用死记硬背,做项目时多试两次,比如测温度先试试中位值,不行再换中位值平均,很快就能找到适合自己的那一个~

Logo

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

更多推荐