4. 输入信号处理与滤波:硬件去抖与软件去抖、数字滤波算法

各位工程师朋友,咱们今天聊一个很实在的话题——输入信号处理。说实话,我在工业现场摸爬滚打这么多年,见过太多因为信号没处理好导致的“灵异事件”。机器莫名其妙跳变、传感器读数忽高忽低、按键按一下触发十次……这些问题的根源,往往就是信号处理没做到位。

工业控制环境有多恶劣?你想想看,电机启动时的电磁干扰、变频器产生的谐波、现场焊接带来的地电位波动,这些噪声都会叠加到我们的输入信号上。如果不做处理,控制器读到的数据就是“假数据”,控制逻辑再完美也没用。

4.1 硬件去抖:第一道防线

硬件去抖,说白了就是用物理手段把噪声挡在门外。我个人习惯,只要成本允许,一定优先考虑硬件去抖。为什么?因为硬件处理是实时的,不占用CPU资源,可靠性也更高。

最常见的硬件去抖方案就是RC低通滤波器。一个电阻加一个电容,简单粗暴但有效。原理其实不复杂:高频噪声通过电容旁路到地,低频信号则顺利通过。我曾在一条包装生产线上遇到过编码器信号抖动的问题,加了个RC滤波器后,问题立刻消失。

关键参数计算:

RC滤波器的截止频率 f_c = 1 / (2πRC)。对于机械按键,抖动时间通常为5-20ms,建议将截止频率设置在50-100Hz。

除了RC滤波,施密特触发器也是硬件去抖的好帮手。它利用滞回特性,给信号设定两个不同的阈值——上升沿触发阈值和下降沿触发阈值。这样信号在阈值附近来回跳动时,输出不会跟着乱跳。嗯,这里要注意,施密特触发器的滞回宽度要大于噪声幅度,否则白搭。

我的经验: 在强干扰环境下,建议RC滤波+施密特触发器组合使用。RC负责滤除高频噪声,施密特负责消除阈值附近的抖动。双保险,效果翻倍。

4.2 软件去抖:灵活的第二道防线

硬件去抖虽好,但有时候成本受限,或者电路板已经定型了,这时候就得靠软件来救场。软件去抖的核心思想很简单:不轻易相信信号的第一次变化,等它稳定了再说。

最经典的软件去抖算法就是“延时确认法”。检测到信号变化后,先等一段时间(比如10ms),再读取一次。如果两次结果一致,就认为信号有效。我在项目中遇到过一种情况:按键按下时抖动很厉害,但释放时很干净。针对这种不对称抖动,我采用了“按下延时、释放立即”的策略,效果出奇的好。

// 按键去抖 - 延时确认法
#define DEBOUNCE_TIME_MS 10

uint8_t debounce_key(uint8_t key_current) {
    static uint8_t key_last = 1;  // 假设高电平为释放状态
    static uint32_t last_change_time = 0;
    static uint8_t key_stable = 1;
    
    if (key_current != key_last) {
        last_change_time = get_tick_ms();
        key_last = key_current;
    } else if (get_tick_ms() - last_change_time > DEBOUNCE_TIME_MS) {
        key_stable = key_current;
    }
    
    return key_stable;
}

还有一种更鲁棒的方法叫“连续采样法”。连续采样N次,如果N次结果都相同,才认为信号有效。N的取值取决于抖动时间和采样周期。比如抖动时间10ms,采样周期1ms,那N至少取10。

避坑指南: 我曾经在某个项目中把去抖时间设得太长(50ms),结果操作员按键时感觉“反应迟钝”,用户体验极差。去抖时间不是越长越好,要在可靠性和响应速度之间找平衡。机械按键一般5-20ms就够了,继电器触点可能需要20-50ms。

4.3 数字滤波算法:让数据更干净

说完开关量,咱们聊聊模拟量的滤波。工业现场的模拟信号,比如温度、压力、流量,噪声来源五花八门。数字滤波算法就是我们的“数据清洗工具”。

4.3.1 中值滤波:对付脉冲噪声的利器

中值滤波的原理很简单:连续采样N次,排序后取中间值。它对脉冲噪声(比如偶尔出现的尖峰干扰)有奇效。我曾在热处理炉的温度采集中使用中值滤波,炉内偶尔会有电弧干扰,中值滤波直接把那些离谱的尖峰数据给剔除了。

// 中值滤波 - 窗口大小为5
#define MEDIAN_WINDOW 5

uint16_t median_filter(uint16_t new_sample) {
    static uint16_t buffer[MEDIAN_WINDOW] = {0};
    static uint8_t index = 0;
    uint16_t temp[MEDIAN_WINDOW];
    uint16_t i, j, tmp;
    
    // 更新环形缓冲区
    buffer[index] = new_sample;
    index = (index + 1) % MEDIAN_WINDOW;
    
    // 复制并排序
    for (i = 0; i < MEDIAN_WINDOW; i++) {
        temp[i] = buffer[i];
    }
    // 冒泡排序(窗口小,效率可接受)
    for (i = 0; i < MEDIAN_WINDOW - 1; i++) {
        for (j = 0; j < MEDIAN_WINDOW - 1 - i; j++) {
            if (temp[j] > temp[j + 1]) {
                tmp = temp[j];
                temp[j] = temp[j + 1];
                temp[j + 1] = tmp;
            }
        }
    }
    
    return temp[MEDIAN_WINDOW / 2];  // 返回中间值
}
注意: 中值滤波的窗口大小N一般取奇数,3、5、7比较常见。N越大,滤除脉冲噪声的效果越好,但响应速度也越慢。对于快速变化的信号,窗口不宜过大。

4.3.2 均值滤波:平滑噪声的经典方法

均值滤波,就是把连续N次采样的值加起来除以N。它对随机噪声(比如热噪声、量化噪声)的抑制效果很好。说白了,就是通过平均来“稀释”噪声的影响。

均值滤波有两种实现方式:一种是每次重新计算所有采样值的平均,另一种是滑动平均(只更新最新数据,去掉最老数据)。我个人更推荐滑动平均,因为它计算量小,而且数据更新更平滑。

// 滑动平均滤波 - 窗口大小为8
#define AVG_WINDOW 8

uint16_t moving_average(uint16_t new_sample) {
    static uint16_t buffer[AVG_WINDOW] = {0};
    static uint8_t index = 0;
    static uint32_t sum = 0;
    static uint8_t count = 0;
    
    // 减去最旧的数据
    sum -= buffer[index];
    // 加入新数据
    buffer[index] = new_sample;
    sum += new_sample;
    
    index = (index + 1) % AVG_WINDOW;
    if (count < AVG_WINDOW) count++;
    
    return (uint16_t)(sum / count);
}
均值滤波 vs 中值滤波:
特性均值滤波中值滤波
适用噪声类型随机噪声脉冲噪声
响应速度较慢中等
计算量小(滑动平均)中等(需排序)
边缘保持能力差(会模糊跳变)好(保留阶跃)

4.3.3 卡尔曼滤波:工业界的“智能滤波器”

卡尔曼滤波,听起来很高大上,其实它就是一个能“预测+修正”的智能算法。它不只会滤除噪声,还能根据系统模型预测下一个值,然后用实际测量值来修正预测。说白了,它比普通滤波多了一个“思考”的过程。

我在一个液位控制项目中用过卡尔曼滤波。液位传感器受液面波动影响很大,普通滤波根本压不住。卡尔曼滤波结合了液位变化的物理模型(比如液位变化是连续的,不会突变),效果出奇的好。

// 一维卡尔曼滤波 - 简化版
typedef struct {
    float Q;      // 过程噪声协方差
    float R;      // 测量噪声协方差
    float P;      // 估计误差协方差
    float K;      // 卡尔曼增益
    float X;      // 状态估计值
} KalmanFilter_t;

void kalman_init(KalmanFilter_t *kf, float init_value) {
    kf->Q = 0.01;   // 根据系统特性调整
    kf->R = 0.1;    // 根据传感器精度调整
    kf->P = 1.0;
    kf->X = init_value;
}

float kalman_update(KalmanFilter_t *kf, float measurement) {
    // 预测阶段
    kf->P = kf->P + kf->Q;
    
    // 更新阶段
    kf->K = kf->P / (kf->P + kf->R);
    kf->X = kf->X + kf->K * (measurement - kf->X);
    kf->P = (1 - kf->K) * kf->P;
    
    return kf->X;
}
卡尔曼滤波的坑: 参数Q和R的调优是关键。Q太大,滤波结果会跟着测量值剧烈波动;Q太小,又跟不上信号变化。我建议先用离线数据跑一遍,找到合适的Q和R值,再固化到代码里。另外,卡尔曼滤波对计算资源有一定要求,低端MCU慎用。

4.4 信号有效性校验:最后的把关

滤波做完了,数据看起来干净了,但还不能直接拿来用。为什么?因为传感器可能坏了、线路可能断了、AD转换可能出错了。信号有效性校验,就是给数据做一次“体检”。

常用的校验方法有几种:

  • 范围校验: 检查数据是否在合理范围内。比如4-20mA电流环,正常范围是4-20mA,超出这个范围直接报错。
  • 变化率校验: 检查数据变化是否过快。比如温度传感器,1秒内变化超过10度,基本可以判定为异常。
  • 一致性校验: 对于冗余传感器,比较多个传感器的读数是否一致。偏差超过阈值就报警。
  • CRC校验: 对于通信信号,通过循环冗余校验确保数据在传输过程中没有出错。
// 信号有效性校验示例
typedef enum {
    SIGNAL_OK = 0,
    SIGNAL_OUT_OF_RANGE,
    SIGNAL_RATE_TOO_HIGH,
    SIGNAL_STUCK
} SignalStatus_t;

SignalStatus_t validate_signal(float value, float last_value, float min, float max, float max_rate) {
    // 范围校验
    if (value < min || value > max) {
        return SIGNAL_OUT_OF_RANGE;
    }
    
    // 变化率校验
    if (fabs(value - last_value) > max_rate) {
        return SIGNAL_RATE_TOO_HIGH;
    }
    
    // 卡死校验(连续多次值不变,可能传感器卡住了)
    static uint8_t stuck_count = 0;
    if (fabs(value - last_value) < 0.001) {
        stuck_count++;
        if (stuck_count > 10) {
            return SIGNAL_STUCK;
        }
    } else {
        stuck_count = 0;
    }
    
    return SIGNAL_OK;
}
我的习惯: 信号有效性校验的结果不要只用来丢弃数据,还要记录日志。这样后期排查问题时,能清楚地知道是哪个环节出了故障。我曾经靠日志里的“信号超范围”记录,定位到一个传感器接线松动的问题。

知识体系总览

下面这张图,把本章的核心内容串起来了。从信号进入控制器开始,经过硬件去抖、软件去抖、数字滤波、有效性校验,最终得到可靠的数据。每一步都有它的作用,缺一不可。

输入信号处理与滤波 - 知识体系 原始信号 含噪声 硬件去抖 RC滤波/施密特 软件去抖 延时确认/连续采样 数字滤波 中值/均值/卡尔曼 有效性校验 范围/变化率/CRC 可靠数据 算法对比 中值滤波: 对付脉冲噪声,保留信号边缘 均值滤波: 平滑随机噪声,响应较慢 卡尔曼滤波: 预测+修正,适合动态系统 硬件去抖: 实时处理,不占CPU,成本略高 软件去抖: 灵活可调,占用CPU资源 有效性校验: 最后一道防线,不可省略

好了,关于输入信号处理与滤波,咱们就聊到这儿。记住一个原则:不要相信任何未经处理的信号。硬件去抖、软件去抖、数字滤波、有效性校验,这四步走下来,你的控制程序才算真正有了“免疫力”。

专注资料整理