3、低通滤波基础:一阶低通滤波器的原理、参数选择与代码实现

做轨迹优化的朋友,十有八九都跟「抖动」打过交道。电机嘎嘎响、末端抖成筛子、轨迹跟踪误差忽大忽小……这些问题的根源,往往就是高频噪声窜进了控制环路。

怎么治?最直接的办法就是低通滤波。今天咱们就聊聊最基础、也最常用的一阶低通滤波器。别看它简单,用好了能解决大问题,用不好反而会引入滞后,让系统变「肉」。

3.1 一阶低通滤波器的数学原理

先看公式。一阶低通滤波器在连续域里的传递函数长这样:

G(s) = 1 / (τ·s + 1)

其中 τ 是时间常数,单位是秒。说白了,这个滤波器就像一个「惯性环节」——输入突然变了,输出不会立刻跟上,而是慢慢爬过去。

离散化之后,我们常用的是一阶后向差分形式:

y[k] = α · x[k] + (1 - α) · y[k-1]

这里:

  • x[k] — 当前采样值(含噪声)
  • y[k] — 当前滤波输出
  • y[k-1] — 上一拍滤波输出
  • α — 滤波系数,范围 (0, 1]

α 和 τ 的关系是:

α = ΔT / (τ + ΔT)

ΔT 是采样周期。你看,α 越小,滤波越「重」,输出越平滑;α 越大,滤波越「轻」,响应越快。

核心理解:一阶低通滤波的本质,就是「当前输出 = 当前输入和上一拍输出的加权平均」。权重 α 决定了你是更相信新数据,还是更相信历史数据。

3.2 参数选择:α 到底取多少?

这是我在项目里被问得最多的问题。说实话,没有万能答案,但有几条经验法则。

3.2.1 从截止频率反推 α

一阶低通的截止频率 f_c(-3dB 点)和 α 的关系:

f_c = α / (2π · ΔT)   (当 α 较小时近似成立)

更精确的公式是:

α = 1 - exp(-2π · f_c · ΔT)

举个例子。你的系统采样周期 ΔT = 0.001s(1kHz),想滤掉 50Hz 以上的噪声:

α = 1 - exp(-2π · 50 · 0.001) ≈ 0.269

嗯,大概取 0.27 就行。

采样周期 ΔT 目标截止频率 f_c 计算 α 推荐 α(取整)
1 ms 10 Hz 0.061 0.06
1 ms 50 Hz 0.269 0.27
1 ms 100 Hz 0.465 0.47
10 ms 5 Hz 0.269 0.27
10 ms 20 Hz 0.714 0.71

3.2.2 经验取值法

如果你懒得算,或者系统噪声特性不明确,我个人的习惯是:

  • α = 0.1 ~ 0.3 — 通用范围,适合大多数轨迹平滑场景
  • α = 0.01 ~ 0.05 — 强滤波,适合传感器噪声特别大的情况(比如廉价 IMU)
  • α = 0.5 ~ 0.8 — 弱滤波,适合只需要轻微平滑的场景

我的经验:在机器人关节速度环里,我通常从 α = 0.2 开始试。如果轨迹跟踪滞后明显,就增大 α;如果抖动没压住,就减小 α。调两三次基本就能找到平衡点。

3.3 代码实现:C 语言与 Python 示例

理论说完了,上代码。先看 C 语言版本,适合嵌入式实时系统:

// 一阶低通滤波器结构体
typedef struct {
    float alpha;    // 滤波系数
    float y_prev;   // 上一拍输出
    uint8_t first;  // 首次运行标志
} LowPassFilter;

// 初始化
void lpf_init(LowPassFilter* lpf, float alpha) {
    lpf->alpha = alpha;
    lpf->y_prev = 0.0f;
    lpf->first = 1;
}

// 滤波计算
float lpf_process(LowPassFilter* lpf, float x) {
    float y;
    if (lpf->first) {
        // 首次运行,直接输出当前值
        y = x;
        lpf->first = 0;
    } else {
        y = lpf->alpha * x + (1.0f - lpf->alpha) * lpf->y_prev;
    }
    lpf->y_prev = y;
    return y;
}

// 使用示例
LowPassFilter lpf;
lpf_init(&lpf, 0.2f);

float raw_speed = read_encoder_speed();  // 读取原始速度
float filtered_speed = lpf_process(&lpf, raw_speed);

再看 Python 版本,适合仿真和快速验证:

class LowPassFilter:
    def __init__(self, alpha: float):
        self.alpha = alpha
        self.y_prev = None

    def process(self, x: float) -> float:
        if self.y_prev is None:
            self.y_prev = x
            return x
        y = self.alpha * x + (1 - self.alpha) * self.y_prev
        self.y_prev = y
        return y

    def reset(self):
        self.y_prev = None

# 使用示例
lpf = LowPassFilter(alpha=0.2)
data = [10.5, 11.2, 9.8, 10.1, 12.3, 9.5]
filtered = [lpf.process(d) for d in data]
print(filtered)

注意:首次运行时,y_prev 没有值。我见过有人直接初始化为 0,结果前几拍输出严重失真。正确的做法是首次直接输出 x,或者用 x 初始化 y_prev。

3.4 一阶低通滤波的局限性

说实话,一阶低通不是万能的。它有明显的短板:

  • 相位滞后:滤波越强,滞后越大。在轨迹跟踪里,滞后意味着「跟不上指令」,位置误差会变大。
  • 幅值衰减:不仅衰减噪声,也衰减有用信号的高频成分。如果你需要保留快速变化的轨迹细节,一阶低通可能不合适。
  • 对阶跃响应有超调?没有。一阶系统不会超调,但响应速度慢。

我在项目中遇到过一个问题:用一阶低通滤波关节速度信号,结果位置环的跟踪误差在急加减速时明显增大。后来我改用二阶低通 + 前馈补偿才解决。所以,一阶低通适合「轻度平滑」,别指望它解决所有抖动问题。

3.5 本章知识体系

下面这张图帮你理清一阶低通滤波的核心脉络:

一阶低通滤波器知识体系 一阶低通滤波器知识体系 一阶低通滤波器 数学原理 连续域:G(s) = 1 / (τ·s + 1) 离散域:y[k] = α·x[k] + (1-α)·y[k-1] α = ΔT / (τ + ΔT) 参数选择 由截止频率 f_c 反推 α 经验取值:0.1 ~ 0.3 权衡:平滑 vs 滞后 代码实现 C 语言:结构体 + 状态机 Python:类封装 首次运行处理 局限性:相位滞后、幅值衰减、不适用于强噪声 核心:一阶低通 = 加权平均。α 小则平滑但滞后,α 大则响应快但滤波弱。 实际调试时,从 α=0.2 开始,根据轨迹跟踪误差和抖动幅度双向调整。

3.6 避坑指南

最后分享几个我踩过的坑:

  • 采样周期不稳定:如果你的系统不是固定周期采样,α 需要根据实际 ΔT 动态计算。固定 α 会导致滤波效果时重时轻。
  • 浮点运算精度:在低端 MCU 上,α 非常小(比如 0.01)时,y[k] 的更新可能被浮点精度吃掉。我建议用定点数或提高 α 下限。
  • 不要对位置直接滤波:位置信号本身是低频的,滤波后滞后明显。我更推荐对速度或加速度做滤波。
  • 级联使用要小心:两个一阶低通级联相当于二阶低通,但相位滞后翻倍。如果你需要更强的滤波效果,不如直接用二阶滤波器。

我的习惯:在调试阶段,我会把 α 做成可在线调整的参数,通过上位机实时修改。这样不用重新编译就能找到最佳值,效率高很多。

一阶低通滤波是轨迹抖动优化的「入门刀」。它简单、可靠、计算量小,适合大多数实时系统。但别指望一把刀解决所有问题——后面我们会聊更高级的滤波方法,比如二阶低通、卡尔曼滤波、以及基于优化的轨迹平滑。先把一阶玩明白,后面的路就好走了。