02 基础滤波算法:限幅、平均、滞后与消抖
这一章的算法大多很短,但工程价值极高。它们是 MCU 测量程序里的螺丝刀和扳手——形式简单,不代表可以随便用。
先看选型表
遇到测量问题时,先判断问题类型,再选工具:
| 问题 | 优先考虑 | 不适合 |
|---|---|---|
| 偶发尖峰,幅度远超正常波动 | 限幅、中值 | 平均(会被拉偏) |
| 随机抖动,没有明显尖峰 | 平均、滑动平均、一阶滞后 | 中值(对连续抖动效果差) |
| 显示稳定但不能太慢 | 一阶滞后、短窗口滑动平均 | 长窗口平均(延迟太大) |
| 按键/开关信号跳变 | 消抖 | 任何连续信号滤波器 |
| 阈值附近反复开关 | 迟滞 | 单阈值判断 |
这个表不是绝对规则,而是一个起点。读完本章后,你应该能理解为什么这些搭配成立、什么时候会失效。
线性与非线性的区分
本章算法要分成两类,这决定了你用什么工具分析它们:
线性时不变(LTI)滤波器:平均滤波、滑动平均、一阶滞后。它们满足叠加性——输入放大多少,输出也放大多少;两个输入相加,输出也相加。这类滤波器可以写出频率响应、截止频率、群延迟。
非线性规则:限幅、中值、消抖、迟滞。它们不满足叠加性,不能用固定截止频率描述。分析它们要看阈值逻辑、状态机、异常值处理能力。
这一区分很重要。专业不是把所有算法都套同一套公式,而是知道什么场景该用什么分析工具。
限幅滤波
ADC 偶尔会输出一个离谱的值——可能是电源开关噪声、接触不良、或者 ADC 内部错误。这种尖峰通常只持续 1~3 个采样点,但幅度可能是正常值的几倍甚至几十倍。限幅滤波的思路是:真实物理量不可能在一个采样周期内变化太多。 如果新值和上一次有效值相差超过合理范围,就认为新值可疑。
两种策略
限幅有两种做法,不要混为一谈:
策略一:拒绝(reject)。超过阈值就丢弃新值,继续用上一次的输出。
策略二:夹住(clamp)。超过阈值就限制变化幅度,也叫速率限制(rate limiter)。
两种策略的区别:拒绝策略会让输出在尖峰期间保持不动;夹住策略会让输出以最大允许速率跟踪输入。后者更适合需要连续跟踪的场景,前者更适合输入可以短暂缺失的场景。
MCU C 实现
#include <stdint.h>
/* 夹住策略:限制单次最大变化幅度 */
float limit_filter_clamp(float x, float last, float max_step)
{
if (max_step < 0.0f) max_step = 0.0f; /* 负步长无意义 */
if (x > last + max_step) return last + max_step;
if (x < last - max_step) return last - max_step;
return x;
}
/* 拒绝策略:超过阈值就保持上次输出 */
float limit_filter_reject(float x, float last, float max_step)
{
if (max_step < 0.0f) max_step = 0.0f; /* 负步长无意义 */
if (x > last + max_step) return last;
if (x < last - max_step) return last;
return x;
}
这段代码要注意:两个函数的区别只有一行——夹住返回 last ± max_step,拒绝返回 last。选择哪种取决于你的应用场景。
参数选择。 D(单次最大变化量)要根据物理变化速度设置。例如温度测量:传感器每秒最多变化 2°C,采样周期 0.1 秒,则单次合理变化约 0.2°C,再留一些余量可以设 D = 0.3°C。常见误区是把 D 设得太小,这样真实快速变化也会被当成干扰,输出像被绑住一样跟不上。
代价和边界。 限幅不能处理连续多个异常值——如果尖峰持续多个采样点,夹住策略会以最大速率偏离真实值。D 的选择依赖对物理变化速度的了解,如果不确定,宁可设大一些,先保证不卡住正常信号。限幅是非线性的,没有频率响应可言。
验证方法。 在正常数据中人为插入几个尖峰,观察限幅后输出是否被夹住或拒绝。同时检查正常快速变化时输出是否跟得上。
什么时候不该用。 噪声是连续小幅抖动(不是偶发尖峰)时,限幅无能为力。信号本身可能快速变化时(比如电机启动时的电流冲击),限幅会把它当成异常。
中值滤波
中值滤波专门对付孤立尖峰。它取一组数据排序后的中间值,而不是平均值。少数极端值不会影响中间位置。
直觉解释
以 3 点中值为例,三个数排序后为:
此时中值就是 b。如果其中一个点是尖峰,例如 {100, 101, 800},排序后中间仍接近正常值 101,不会像平均值那样被 800 拉偏。
边界条件
中值滤波不是万能的。它的有效性依赖一个条件:窗口内异常值数量不能超过窗口长度的一半。
以 3 点中值为例,如果 3 个点里有 2 个是尖峰,中值就是尖峰之一。5 点中值能容忍 2 个异常值,7 点能容忍 3 个。
所以中值滤波适合"偶尔出现"的尖峰,不适合"密集出现"的异常。
MCU C 实现
#include <stdint.h>
/* 3 点中值:通过三次比较交换排序 */
float median3(float a, float b, float c)
{
if (a > b) { float t = a; a = b; b = t; }
if (b > c) { float t = b; b = c; c = t; }
if (a > b) { float t = a; a = b; b = t; }
return b;
}
这段代码要注意:三次比较交换是 3 点排序的最少比较次数。窗口更大时(5 点、7 点),排序逻辑会更复杂,但原理相同。
参数选择。 窗口长度通常取奇数,例如 3、5、7。窗口越大,抗尖峰能力越强,但延迟也越大。3 点窗口最多容忍 1 个异常值,5 点最多容忍 2 个,7 点最多容忍 3 个。如果尖峰通常只持续 1 个采样点,3 点中值就够了;持续 2 个采样点需要 5 点;持续 3 个采样点时 5 点可能失效,应选 7 点或更大。
代价和边界。 中值滤波是非线性的,它没有像滑动平均那样简单固定的频率响应,也不能严谨地说"截止频率是多少"。它不适合只含普通小幅白噪声的场景——它能去尖峰,但平滑连续随机抖动不如平均类算法自然。窗口越大,排序计算量越大。
验证方法。 在正常数据中插入单个尖峰(幅度为正常值的 5~10 倍),观察中值输出是否不受影响。然后插入多个连续尖峰,观察窗口多大时开始失效。
什么时候不该用。 噪声是连续小幅抖动时,中值不如平均类算法。异常值出现频率很高、超过窗口内一半位置时,中值也会被污染。
算术平均与滑动平均
先记住: 平均滤波用窗口长度 \(N\) 换噪声降低,但代价是延迟和更低截止频率。平均 4 次,随机噪声标准差约减半;平均 16 次,标准差约变成四分之一。
这两个算法本质相同,都是 N 点矩形窗 FIR 低通滤波器。区别只在实现方式:算术平均每次重新求和,滑动平均用递推方式更新。
噪声为什么会降低
设真实值为 s,噪声为 e[i],采样值为:
N 点平均为:
如果噪声相互独立、方差为 \(\sigma^2\),那么 N 点平均后的噪声方差变为:
标准差变为:
平均 4 次,随机噪声标准差约减半;平均 16 次,标准差约变成四分之一。 它不是无限免费变好,因为窗口越大,延迟越大,截止频率越低。
平均滤波为什么是 FIR
对连续运行的 N 点平均,可以写成:
它的冲激响应是:
因为冲激响应有限,所以它天然稳定;因为系数左右对称,所以它具有线性相位。
N 点平均的幅频响应为:
先抓住三件事:
- 低频处 \(|H|\) 接近 1,所以慢变化能通过。
- 高频处通常被衰减,所以随机抖动会变小。
- 当 \(\sin(N\omega/2) = 0\) 时,幅度为 0,这些位置叫零点。
零点对应频率为:
第一个零点是 \(f_s/N\)。如果采样率 \(f_s = 1000\;\text{Hz}\),窗口 \(N = 20\),第一个零点就是 \(50\;\text{Hz}\)。这意味着 50 Hz 正弦干扰在理想情况下会被完全压掉。这不是玄学,是矩形窗平均的零点决定的。
-3 dB 截止频率没有初等函数形式的精确闭式解。它需要求解 \(\text{sinc}(N\omega_c/2) = 1/\sqrt{2}\),其中 \(\text{sinc}(x) = \sin(x)/x\)。数值求解可得 \(N\omega_c/2 \approx 1.393\),即 \(\sin(1.393)/1.393 \approx 1/\sqrt{2}\),因此:
工程上当 \(N \ge 4\) 时这个近似足够准确。
群延迟为:
例如 \(f_s = 1000\;\text{Hz}\):
| N | 近似 -3 dB 截止频率 | 第一零点 | 群延迟 |
|---|---|---|---|
| 4 | 110.8 Hz | 250 Hz | 1.5 ms |
| 8 | 55.4 Hz | 125 Hz | 3.5 ms |
| 16 | 27.7 Hz | 62.5 Hz | 7.5 ms |
| 32 | 13.8 Hz | 31.25 Hz | 15.5 ms |
所以平均滤波的核心取舍不是"平均几次更稳",而是:
窗口长度、截止频率和延迟
如果你希望 N 点平均的 -3 dB 截止频率大约是 \(f_c\),采样率为 \(f_s\),可以反推:
例如希望在 \(f_s = 200\;\text{Hz}\) 采样时得到约 \(5\;\text{Hz}\) 截止频率:
可以选 \(N = 18\)。如果为了环形缓冲区优化选 \(N = 16\),要知道此时截止频率会变成:
这就是参数设计,而不是凭感觉写一个 for 循环平均 10 次。
MCU 上怎么实现
算术平均:每次重新求和,适合窗口较小或不连续运行的场景。
#include <stdint.h>
float mean_filter(const float *buf, uint16_t n)
{
if (n == 0) return 0.0f; /* 无样本时返回 0,调用方应避免此情况 */
float sum = 0.0f;
for (uint16_t i = 0; i < n; i++) {
sum += buf[i];
}
return sum / (float)n;
}
这段代码要注意:计算量和 N 成正比,窗口大时每次调用都要遍历整个数组。
滑动平均:用"加新值、减旧值"的方法递推更新,计算量从 \(O(N)\) 降到 \(O(1)\)。
注意:在窗口尚未填满的启动阶段,如果分母使用当前已有样本数,滤波器是随时间变化的,严格说还不是固定的 LTI 系统。频率响应、截止频率、群延迟这些结论,是在窗口填满并使用固定 N 后成立的。
#include <stdint.h>
#define MOVING_AVG_N 16
typedef struct {
float buf[MOVING_AVG_N]; /* 环形缓冲区 */
uint8_t idx; /* 下一次要覆盖的位置 */
uint16_t count; /* 启动阶段已填入的样本数 */
float sum; /* 当前窗口内样本总和 */
uint32_t tick; /* 调用计数器,用于定期修正浮点漂移 */
} MovingAvg;
/* 初始化:必须在首次调用前执行 */
void moving_avg_init(MovingAvg *f)
{
for (uint8_t i = 0; i < MOVING_AVG_N; i++) {
f->buf[i] = 0.0f;
}
f->idx = 0;
f->count = 0;
f->sum = 0.0f;
f->tick = 0;
}
float moving_avg_update(MovingAvg *f, float x)
{
/* 先减掉即将被覆盖的旧样本 */
f->sum -= f->buf[f->idx];
/* 写入新样本,并把新样本加入总和 */
f->buf[f->idx] = x;
f->sum += x;
/* 环形缓冲区索引回绕 */
f->idx++;
if (f->idx >= MOVING_AVG_N) f->idx = 0;
/* 启动阶段还没填满窗口时,分母用实际样本数 */
if (f->count < MOVING_AVG_N) f->count++;
/* 每 1024 次调用重新求和一次,修正浮点累积漂移 */
f->tick++;
if ((f->tick & 0x3FF) == 0) {
f->sum = 0.0f;
for (uint16_t i = 0; i < f->count; i++) {
f->sum += f->buf[i];
}
}
return f->sum / f->count;
}
这段代码要注意两点:
moving_avg_init必须在首次调用前执行,否则环形缓冲区和 sum 是随机值。- 启动阶段
count逐渐增加,分母不是固定 N,此时频率响应特性尚未建立。
代价和边界。 平均滤波对突发尖峰无能为力——一个极端值会把平均值拉偏,恢复需要 N 个采样周期。窗口越大,RAM 占用越多。算术平均计算量和 N 成正比,滑动平均是常数。
验证方法。 测稳态抖动(滤波后输出在稳态时的最大最小值之差)、测响应时间(输入阶跃后输出达到 95% 所需的时间)、测尖峰恢复(插入一个 10 倍尖峰,观察输出偏离多少、多久恢复)。
什么时候不该用。 信号中有明显尖峰干扰时,应先限幅或中值处理。需要快速响应阶跃变化时,长窗口平均延迟太大。RAM 非常紧张时,连几个 float 都放不下。
一阶滞后滤波
先记住: 一阶滞后是 MCU 里最划算的低通滤波器。它只保存上一次输出,不需要缓冲区,代码最短,计算量最小。这里的 \(\alpha\) 读作 alpha,是一阶滞后的平滑系数,取值通常在 0 到 1 之间。它表示每次更新时,新输入参与修正输出的比例——\(\alpha\) 越小越平滑但越慢,\(\alpha\) 越大越快但噪声越明显。
直觉解释
新输出等于"旧输出的一部分"加"新输入的一部分":
把它整理一下:
第二种写法更适合写代码。\(x[n] - y[n-1]\) 是当前输入和旧输出之间的误差,\(\alpha\) 决定这次修正误差的多少。
作为一阶 IIR
令 \(a = 1 - \alpha\),公式可写成:
它的系统函数是:
因为输出 \(y[n]\) 依赖上一时刻输出 \(y[n-1]\),所以它是 IIR,也就是无限冲激响应滤波器。它的冲激响应不是有限长度,而是按指数衰减:
这也解释了"指数平滑"这个名字。
一阶滞后的频率特性
把 \(z\) 换成 \(e^{j\omega}\),一阶滞后的幅频响应为:
其中 \(a = 1 - \alpha\),\(\omega = 2\pi f / f_s\)。
它的 -3 dB 截止频率满足:
因此:
注意:这个精确截止点只在衰减能达到 -3 dB 时存在。当 \(\alpha\) 较大时,Nyquist 频率处的衰减可能不到 -3 dB,此时不存在严格意义上的截止频率。具体来说,Nyquist 频率处(\(\omega = \pi\))的幅度为 \(|H(e^{j\pi})| = \alpha / (2 - \alpha)\),令其等于 \(1/\sqrt{2}\) 解得 \(\alpha \approx 0.828\)。所以当 \(\alpha > 0.828\) 时严格 -3 dB 截止点不存在。工程常用 \(\alpha < 0.5\) 基本不会遇到这个问题。
当 \(f_c\) 远小于 \(f_s\) 时,可以用更简单的近似:
这个近似在 \(\alpha < 0.3\) 时误差约 2% 以内;\(\alpha\) 再大误差会显著增加。如果 \(\alpha\) 较大,应使用上面的精确公式或下面的指数映射公式。
反过来,如果已知目标截止频率 \(f_c\),可以用线性近似:
但这个线性近似只在 \(f_c / f_s\) 很小时(比如 < 0.01)误差才小。当 \(f_c / f_s = 0.05\) 时,线性近似得 \(\alpha \approx 0.314\),而指数映射得 \(\alpha \approx 0.270\),差了约 16%。实际参数设计时优先用下面的指数映射公式。
更稳妥的工程设计常用连续一阶低通在零阶保持输入下的离散化(也叫 step response matching 或 matched-z):
这不是双线性变换(bilinear transform),双线性变换会有频率压缩效应(warping),映射公式不同。当 \(f_c/f_s\) 很小时两者结果接近;\(f_c/f_s\) 较大时指数映射更准确。
延迟与阶跃响应
一阶滞后没有滑动平均那样固定的线性相位群延迟,它的延迟随频率变化。低频附近(远低于截止频率)的等效群延迟约为:
例如 \(\alpha = 0.1\),低频等效延迟约 9 个采样周期。在截止频率附近及更高频率,群延迟会明显减小,不能直接用这个近似。
对单位阶跃输入(假设初始输出为零,阶跃从 \(n=0\) 开始),输出为:
所以 \(\alpha\) 越小,指数项衰减越慢,输出越稳也越慢。注意:代码中初始化为第一个输入值(而非零),所以实际阶跃响应会从初始值开始收敛,公式形式略有不同,但收敛速度和时间常数的规律不变。
工程上常用"达到最终值 90% 或 95% 所需的时间"来衡量响应速度。对于一阶系统,时间常数 \(\tau\) 和系数 \(\alpha\) 的关系是:
例如采样率 100 Hz(\(T_s = 10\;\text{ms}\)),\(\alpha = 0.1\):
参数怎么选
\(\alpha\) 越大,截止频率越高,响应越快,抖动越明显;\(\alpha\) 越小,截止频率越低,越平滑,延迟越大。常用范围是 0.01 到 0.5,但专业设计时应优先从采样率和目标截止频率反推,而不是凭感觉选。
反推步骤:
- 确定采样率 \(f_s\) 和目标截止频率 \(f_c\)。
- 计算 \(\alpha = 1 - \exp(-2\pi f_c / f_s)\)。
- 验证响应时间是否满足要求(\(3\tau\) 是否在允许范围内)。
MCU C 实现
#include <stdint.h>
typedef struct {
float y; /* 上一次输出 */
uint8_t init; /* 是否完成初始化 */
} Lowpass1st;
void lowpass_1st_init(Lowpass1st *f)
{
f->y = 0.0f;
f->init = 0;
}
float lowpass_1st_update(Lowpass1st *f, float x, float alpha)
{
if (!f->init) {
/* 第一次调用,用输入值初始化,避免启动瞬态 */
f->y = x;
f->init = 1;
return x;
}
/* 钳位 alpha 到合法范围 */
if (alpha < 0.0f) alpha = 0.0f;
if (alpha > 1.0f) alpha = 1.0f;
/* x - y 是当前输入和上次输出的差值 */
/* alpha 只取其中一部分来更新输出 */
f->y = f->y + alpha * (x - f->y);
return f->y;
}
这段代码要注意三点:
init用来避免启动瞬态——第一次调用时直接用输入值初始化,而不是从 0 开始。alpha应限制在 0 到 1 之间。超出范围时滤波器行为不可预测。- 无 FPU 的 MCU 可以改成定点实现:把
alpha放大为 2 的幂次倍再移位。
代价和边界。 一阶滞后对尖峰干扰没有特殊抵抗力——一个极端值会通过 \(\alpha\) 的比例进入输出,恢复时间约 \(1/\alpha\) 个采样周期。它只有 -20 dB/十倍频的衰减速度,对高频噪声的抑制能力有限。\(\alpha\) 太大(接近 1)时,滤波器几乎不起作用;\(\alpha\) 太小时,响应极慢。
验证方法。 测截止频率(用正弦波扫频,观察输出幅度降到 0.707 时的频率)、测阶跃响应(输入阶跃后记录输出达到 63%、95% 的时间)、和滑动平均对比(相同截止频率下,一阶滞后延迟更小但高频衰减更慢)。
什么时候不该用。 需要快速响应阶跃变化且不允许过冲时(一阶滞后没有过冲,但响应慢)。信号中有明显尖峰时,应先限幅或中值处理。需要更陡峭的截止特性时,考虑二阶 IIR 或 FIR。
消抖滤波
机械触点(按键、继电器、干簧管)闭合时不会一步到位,而是会在几毫秒内反复跳变。一个按键的典型抖动时间是 5~20 ms,抖动次数 3~10 次。如果直接读取触点状态,一次按键可能被误判为多次。消抖本质上是一个"连续计数"问题:输入状态必须连续稳定一段时间,才承认状态改变。
状态规则
设当前原始输入为 raw,上一次原始输入为 last_raw:
- 如果两次相同,稳定计数
cnt加 1。 - 如果两次不同,说明状态又变了,计数清零。
- 只有
cnt >= need时,才更新稳定输出。
如果 raw == last_raw:
cnt = min(cnt + 1, need)
否则:
last_raw = raw
cnt = 0
如果 cnt >= need:
stable = raw
MCU C 实现
#include <stdint.h>
typedef struct {
uint8_t stable; /* 已确认的稳定状态 */
uint8_t last_raw; /* 上一次读到的原始状态 */
uint16_t cnt; /* 原始状态连续不变的次数 */
} Debounce;
/* 初始化:必须在首次调用前执行,否则结构体成员是随机值 */
void debounce_init(Debounce *d, uint8_t initial_state)
{
d->stable = initial_state;
d->last_raw = initial_state;
d->cnt = 0;
}
uint8_t debounce_update(Debounce *d, uint8_t raw, uint16_t need)
{
if (raw == d->last_raw) {
/* 原始输入没有变化,稳定计数增加 */
if (d->cnt < need) d->cnt++;
} else {
/* 原始输入发生变化,重新开始计数 */
d->last_raw = raw;
d->cnt = 0;
}
/* 连续稳定时间达到要求后,才承认新状态 */
if (d->cnt >= need) d->stable = raw;
return d->stable;
}
这段代码要注意:debounce_init 必须在首次调用前执行,否则 stable、last_raw、cnt 是随机值,输出不可预测。initial_state 通常设为按键的初始状态(未按下时为 0 或 1)。
参数选择。 need 的值取决于采样周期和抖动时间。如果每 1 ms 调用一次,need = 20 表示需要稳定约 20 ms。典型按键抖动 5~20 ms,采样周期 1 ms 时,need 可以设 10~30。
代价和边界。 消抖引入延迟——状态变化至少需要 need 个采样周期才能被确认。消抖是非线性的,不能用频率响应分析。它只适用于数字量(0/1),不适用于连续模拟信号。
验证方法。 用示波器观察原始触点波形和消抖后输出,确认抖动被消除、真实状态切换的延迟在可接受范围内。
什么时候不该用。 输入是连续变化的模拟信号时。抖动时间很短、不需要消抖时(比如数字传感器输出)。
软件迟滞滤波
如果只有一个阈值,输入在阈值附近抖动时,输出会反复开关。这在液位报警、温控开关、电池欠压保护等场景中很常见。迟滞用两个阈值解决这个问题:上升时用高阈值,下降时用低阈值。
状态规则
普通单阈值判断只有一个边界:
迟滞判断有两个边界:
中间区域不是"不知道",而是"保持原状态"。
MCU C 实现
#include <stdint.h>
uint8_t hysteresis_update(float x, uint8_t state,
float low_th, float high_th)
{
/* 保证阈值顺序正确 */
if (low_th > high_th) {
float tmp = low_th;
low_th = high_th;
high_th = tmp;
}
/* 原来是关闭状态,必须越过高阈值才打开 */
if (!state && x >= high_th) state = 1;
/* 原来是打开状态,必须跌破低阈值才关闭 */
if ( state && x <= low_th) state = 0;
/* 在 low_th 和 high_th 之间时,状态不变 */
return state;
}
这段代码要注意:high_th 必须大于 low_th,否则回差带为负,逻辑会出错。两个阈值之间的区域叫回差带,输入在此范围内时输出保持原状态。
参数选择。 回差带宽度(high_th - low_th)要大于输入在阈值附近的正常抖动幅度。例如温度控制,传感器在阈值附近抖动 ±0.5°C,回差带可以设 2°C(high_th = 72,low_th = 70)。回差带太小,起不到防抖作用;回差带太大,控制精度下降。
代价和边界。 迟滞是非线性的,不能用频率响应分析。回差带内的真实变化会被忽略——如果需要精确控制,迟滞可能不合适。它只适用于数字量输出(开/关),不适用于连续控制。
验证方法。 让输入在阈值附近缓慢变化,观察输出是否稳定。如果输出仍然反复开关,说明回差带太小。
什么时候不该用。 需要连续输出而不是开关控制时。输入抖动幅度很小、单阈值就够用时。
小结:基础算法怎么选
| 场景 | 优先算法 | 主要代价 |
|---|---|---|
| 偶发尖峰 | 限幅、中值 | 可能误伤真实快速变化(限幅);窗口内异常过半时失效(中值) |
| 随机抖动 | 平均、滑动平均、一阶滞后 | 延迟增加;窗口越大/α 越小,响应越慢 |
| 按键/开关抖动 | 消抖 | 状态确认延迟(need 个采样周期) |
| 阈值附近抖动 | 迟滞 | 回差带内不响应 |
这些基础算法不是互相替代的关系,而是互相配合。实际项目中,常见做法是先限幅去尖峰,再平均或一阶滞后平滑抖动。组合使用时,顺序很重要——先剔除异常值,再做平滑。
学完第一阶段,试着不翻书独立回答下面这些问题:
- ADC 数值偶尔跳很大,先试什么?
- 数值随机抖动但没有明显尖峰,用什么?
- 按键为什么不能直接读一次就判断?
- 阈值控制为什么要有回差?
- 平滑越强,为什么响应越慢?
回答不上来也没关系,回到对应章节再看一遍。算法理解不是一次到位的,学到后面再回头看,往往会有新的理解。