4、数字滤波器设计:从原理到实战

好,咱们进入第四章。这一章内容比较多,但都是硬核干货。数字滤波器,说白了就是给电流环信号“洗澡”——把噪声洗掉,把有用的信号留下来。我在FOC项目里踩过最多的坑,就是滤波器没选对,导致系统要么响应慢得像蜗牛,要么高频振荡停不下来。

今天我把常用的几种滤波器都捋一遍。你不需要全部精通,但得知道它们各自适合什么场景。

4.1 FIR滤波器:稳定但“费资源”

FIR(有限脉冲响应)滤波器,最大的优点就是线性相位。什么意思?就是信号经过它之后,波形形状不变,只是整体延迟了一下。这对电流环很重要——你不想让电流波形畸变吧?

FIR的设计思路很简单:用过去N个采样值的加权平均来得到当前输出。

// 一个简单的16阶FIR低通滤波器
#define FIR_TAPS 16
float fir_coeffs[FIR_TAPS] = {
    0.0156, 0.0312, 0.0469, 0.0625,
    0.0781, 0.0938, 0.1094, 0.1250,
    0.1250, 0.1094, 0.0938, 0.0781,
    0.0625, 0.0469, 0.0312, 0.0156
};
float fir_buffer[FIR_TAPS] = {0};
int fir_index = 0;

float fir_filter(float input) {
    fir_buffer[fir_index] = input;
    fir_index = (fir_index + 1) % FIR_TAPS;
    
    float output = 0;
    int buf_idx = fir_index;
    for(int i = 0; i < FIR_TAPS; i++) {
        buf_idx = (buf_idx + 1) % FIR_TAPS;
        output += fir_coeffs[i] * fir_buffer[buf_idx];
    }
    return output;
}

你看,代码不复杂。但有个问题——阶数越高,延迟越大。我做过一个项目,用64阶FIR滤电流采样,结果延迟了将近1ms,电流环带宽直接掉到200Hz以下。后来我换成了IIR,才把带宽拉回来。

我的建议:FIR适合对相位要求苛刻的场景,比如编码器信号滤波。电流环里,除非你的MCU算力过剩,否则尽量少用高阶FIR。

4.2 IIR滤波器:高效但“脾气大”

IIR(无限脉冲响应)滤波器,说白了就是带反馈的滤波器。它用更少的阶数就能达到和FIR一样的滤波效果,但代价是可能不稳定。

最常用的IIR就是二阶巴特沃斯低通滤波器。我一般直接用双线性变换法从模拟滤波器推导数字系数。

// 二阶IIR低通滤波器,截止频率500Hz,采样率10kHz
typedef struct {
    float b0, b1, b2;  // 前馈系数
    float a1, a2;      // 反馈系数
    float x1, x2;      // 历史输入
    float y1, y2;      // 历史输出
} iir_biquad_t;

void iir_init(iir_biquad_t *f, float fc, float fs) {
    float w0 = 2 * M_PI * fc / fs;
    float alpha = sin(w0) / (2 * 0.707);  // Q=0.707,巴特沃斯
    float cos_w0 = cos(w0);
    
    float b0 = (1 - cos_w0) / 2;
    float b1 = 1 - cos_w0;
    float b2 = (1 - cos_w0) / 2;
    float a0 = 1 + alpha;
    float a1 = -2 * cos_w0;
    float a2 = 1 - alpha;
    
    // 归一化
    f->b0 = b0 / a0;
    f->b1 = b1 / a0;
    f->b2 = b2 / a0;
    f->a1 = a1 / a0;
    f->a2 = a2 / a0;
    
    f->x1 = f->x2 = 0;
    f->y1 = f->y2 = 0;
}

float iir_process(iir_biquad_t *f, float x) {
    float y = f->b0 * x + f->b1 * f->x1 + f->b2 * f->x2
              - f->a1 * f->y1 - f->a2 * f->y2;
    
    f->x2 = f->x1;
    f->x1 = x;
    f->y2 = f->y1;
    f->y1 = y;
    
    return y;
}
我曾经踩过的坑:IIR的系数对量化误差非常敏感。在STM32上用float没问题,但如果你用定点数或者低精度MCU,一定要做系数缩放和饱和处理。否则滤波器可能直接“炸掉”——输出发散到无穷大。

4.3 移动平均滤波器:简单粗暴但有效

移动平均滤波器,就是取最近N个采样值的平均值。它其实是一种特殊的FIR滤波器——所有系数都相等。

为什么单独拿出来说?因为它实现简单、计算量小、没有溢出风险。我经常用它来做ADC采样的预处理。

// 16点移动平均滤波器
#define MA_WINDOW 16
float ma_buffer[MA_WINDOW] = {0};
int ma_index = 0;
float ma_sum = 0;

float moving_average(float input) {
    ma_sum -= ma_buffer[ma_index];
    ma_buffer[ma_index] = input;
    ma_sum += input;
    ma_index = (ma_index + 1) % MA_WINDOW;
    return ma_sum / MA_WINDOW;
}

你看,代码就这几行。但要注意:窗口越大,延迟越大,对高频噪声的抑制效果也越好。窗口大小N对应的延迟是(N-1)/2个采样周期。

窗口大小 延迟(采样周期) 适用场景
4 1.5 高速电流环,带宽要求高
8 3.5 一般电流采样滤波
16 7.5 速度环或位置环预处理
32 15.5 离线分析或慢速系统

4.4 中值滤波器:对付“野点”的利器

中值滤波器不取平均,取中间值。它对脉冲噪声(比如偶尔出现的尖峰)有奇效。

我遇到过一种情况:电机运行时,电流传感器偶尔会受电磁干扰,产生一个比正常值大10倍的尖峰。如果用平均滤波,这个尖峰要拖好几个周期才能消掉。但中值滤波——直接把它当异常值扔掉。

// 5点中值滤波器
#define MED_WINDOW 5
float med_buffer[MED_WINDOW] = {0};
int med_index = 0;

// 简单的冒泡排序找中值(窗口小,够用)
float median_filter(float input) {
    med_buffer[med_index] = input;
    med_index = (med_index + 1) % MED_WINDOW;
    
    // 复制并排序
    float temp[MED_WINDOW];
    for(int i = 0; i < MED_WINDOW; i++) temp[i] = med_buffer[i];
    for(int i = 0; i < MED_WINDOW - 1; i++) {
        for(int j = 0; j < MED_WINDOW - 1 - i; j++) {
            if(temp[j] > temp[j+1]) {
                float t = temp[j];
                temp[j] = temp[j+1];
                temp[j+1] = t;
            }
        }
    }
    return temp[MED_WINDOW / 2];
}
小技巧:中值滤波的窗口大小建议用奇数。3点、5点、7点最常见。窗口太大,排序计算量会暴涨,而且会丢失信号的细节变化。

4.5 卡尔曼滤波器基础:最优估计

卡尔曼滤波器,听起来高大上,其实核心思想就一句话:用模型预测 + 测量修正。它能在有噪声的情况下,给出最优的状态估计。

在FOC里,我常用卡尔曼滤波来估计转子位置和速度。但说实话,电流环里直接用卡尔曼的并不多——因为计算量太大,而且电流环的采样频率通常很高(10kHz以上),卡尔曼的矩阵运算容易把MCU跑死。

不过,如果你做的是无传感器FOC,卡尔曼滤波几乎是绕不开的。这里给一个简化的一维卡尔曼滤波示例(用于平滑电流信号):

// 一维卡尔曼滤波器
typedef struct {
    float x;      // 状态估计值
    float P;      // 估计误差协方差
    float Q;      // 过程噪声协方差
    float R;      // 测量噪声协方差
} kalman_1d_t;

void kalman_init(kalman_1d_t *k, float init_x, float init_P) {
    k->x = init_x;
    k->P = init_P;
    k->Q = 0.01;   // 根据系统调参
    k->R = 0.1;    // 根据传感器噪声调参
}

float kalman_update(kalman_1d_t *k, float z) {
    // 预测
    float x_pred = k->x;          // 状态预测(一维,假设恒定)
    float P_pred = k->P + k->Q;   // 协方差预测
    
    // 更新
    float K = P_pred / (P_pred + k->R);  // 卡尔曼增益
    k->x = x_pred + K * (z - x_pred);    // 状态更新
    k->P = (1 - K) * P_pred;             // 协方差更新
    
    return k->x;
}
关键点:卡尔曼滤波的Q和R参数决定了滤波器的“性格”。Q大表示相信测量值更多,响应快但噪声大;R大表示相信模型预测更多,响应慢但平滑。我一般先用离线数据调好参数,再固化到代码里。

4.6 陷波滤波器:精准打击特定频率

陷波滤波器,就是专门干掉某个特定频率的噪声。比如电机在某个转速下会产生500Hz的机械共振,你就可以用陷波滤波器把它滤掉,同时不影响其他频率的信号。

二阶陷波滤波器的传递函数长这样:

// 二阶陷波滤波器,中心频率500Hz,带宽50Hz,采样率10kHz
void notch_init(iir_biquad_t *f, float f0, float bw, float fs) {
    float w0 = 2 * M_PI * f0 / fs;
    float alpha = sin(w0) * sinh(log(2.0) / 2 * bw * w0 / sin(w0));
    float cos_w0 = cos(w0);
    
    float b0 = 1;
    float b1 = -2 * cos_w0;
    float b2 = 1;
    float a0 = 1 + alpha;
    float a1 = -2 * cos_w0;
    float a2 = 1 - alpha;
    
    f->b0 = b0 / a0;
    f->b1 = b1 / a0;
    f->b2 = b2 / a0;
    f->a1 = a1 / a0;
    f->a2 = a2 / a0;
}

嗯,这里要注意:带宽bw不能设得太窄,否则滤波器对参数变化非常敏感。我见过有人把带宽设成1Hz,结果温度一变,中心频率漂了2Hz,滤波器完全失效。

4.7 低通滤波器截止频率选择

这是整个滤波器设计里最“艺术”的部分。截止频率选高了,噪声滤不干净;选低了,电流环响应变慢,带宽下降。

我的经验公式是这样的:

  • 电流环采样频率:通常10kHz~20kHz
  • 电流环带宽目标:1kHz~2kHz
  • 低通滤波器截止频率:带宽的3~5倍,即3kHz~10kHz

为什么是3~5倍?因为滤波器会引入相位延迟。截止频率越低,延迟越大。如果截止频率和带宽太接近,相位裕度会被吃掉,系统容易振荡。

电流环带宽目标 推荐低通截止频率 滤波器阶数
500 Hz 1.5 ~ 2.5 kHz 二阶
1 kHz 3 ~ 5 kHz 二阶
2 kHz 6 ~ 10 kHz 一阶或二阶
3 kHz 9 ~ 15 kHz 一阶
我曾经犯过的错:在一个项目中,我把低通截止频率设成了和带宽一样(都是1kHz)。结果电流环在低速时正常,一加速就开始振荡。后来用示波器一看,相位裕度只剩15度。把截止频率提到4kHz后,问题解决。

4.8 本章知识体系总览

说了这么多,我画了一张图帮你理清思路。这张图展示了不同滤波器的定位和选择逻辑:

数字滤波器选择逻辑图 含噪声电流信号 噪声类型判断 脉冲噪声 宽带噪声 特定频率 中值滤波器 低通/平均/卡尔曼 陷波滤波器 FIR / IIR 实现 截止频率 / 阶数 / Q值调优 干净电流信号

这张图的核心逻辑是:先判断噪声类型,再选滤波器,最后用FIR或IIR实现,并调优参数。别一上来就套卡尔曼,也别所有场景都用移动平均。对症下药,才是工程师该干的事。