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 本章知识体系
下面这张图帮你理清一阶低通滤波的核心脉络:
3.6 避坑指南
最后分享几个我踩过的坑:
- 采样周期不稳定:如果你的系统不是固定周期采样,α 需要根据实际 ΔT 动态计算。固定 α 会导致滤波效果时重时轻。
- 浮点运算精度:在低端 MCU 上,α 非常小(比如 0.01)时,y[k] 的更新可能被浮点精度吃掉。我建议用定点数或提高 α 下限。
- 不要对位置直接滤波:位置信号本身是低频的,滤波后滞后明显。我更推荐对速度或加速度做滤波。
- 级联使用要小心:两个一阶低通级联相当于二阶低通,但相位滞后翻倍。如果你需要更强的滤波效果,不如直接用二阶滤波器。
我的习惯:在调试阶段,我会把 α 做成可在线调整的参数,通过上位机实时修改。这样不用重新编译就能找到最佳值,效率高很多。
一阶低通滤波是轨迹抖动优化的「入门刀」。它简单、可靠、计算量小,适合大多数实时系统。但别指望一把刀解决所有问题——后面我们会聊更高级的滤波方法,比如二阶低通、卡尔曼滤波、以及基于优化的轨迹平滑。先把一阶玩明白,后面的路就好走了。