简介这份资源为嵌入式开发与单片机信号处理场景提供了一套完整的滤波算法参考实现适合需要处理采样数据抖动、脉冲干扰或周期性噪声的工程师和学习者。压缩包内共3个文件整体198KB涵盖C语言源码文件、Word说明文档和PDF格式的滤波算法研究论文兼顾代码实操与理论对照。内容围绕10种常用滤波方法展开包括限幅滤波、中位值滤波、算术平均滤波、递推平均滤波、中位值平均滤波、限幅平均滤波、一阶滞后滤波、加权递推平均滤波、消抖滤波及限幅消抖滤波。每种算法均给出原理说明、适用场景与优缺点分析C源码可直接移植到主流单片机平台便于按实际需求快速筛选和验证合适的滤波策略。已有3189人学习下载对于从事传感器数据采集、仪表显示或自动控制开发的读者这份资源可作为日常编码中的速查手册也可通过PDF论文加深对数字滤波原理的理解。1. 滤波算法这碗饭为什么值得你用 C 语言亲手做一遍嵌入式采集、工业传感器、电机控制、音频前端……只要是 ADC 采出来的原始数据几乎没人敢直接拿去做控制或显示。噪声、毛刺、脉冲干扰混在信号里轻则曲线抖动重则触发误动作。滤波算法 C 代码实现是每个做底层开发的工程师绕不开的基本功甚至可以说你对数据的理解深度就体现在这一层处理上。很多人一开始图省事直接调库、用 MATLAB 仿真一把就完事。但到了单片机上内存只有几 KB浮点运算慢得感人库函数根本搬不进来这时候手写一套可移植的 C 代码反而是最可靠的路。这篇文章要讲的 10 种常用滤波算法从最简单的限幅到相对复杂的滑动平均、一阶低通每一种我都给出可直接抄进工程的结构体定义、函数实现和参数经验值。读过之后你能根据信号特征选型、改参数也能预判它会在什么场景下翻车。2. 先分清这 10 种算法各治什么病选型才是第一步很多人一上来就调参其实第一步错在选型。滤波算法没有绝对的优劣只有适不适合当前信号的形态。选错了算法参数调得再漂亮也是白费力气。2.1 限幅滤波专治“离谱的跳变”不碰正常波动限幅滤波的逻辑极其朴素本次采样值和上次有效值的差超过阈值就丢弃本次值沿用上次值。它治的是那种明显不合理的跳变比如传感器偶尔受雷击、电磁干扰产生的尖峰。思路只有两条一是限幅阈值要大于真实信号的最大变化率二是判断失败时保留旧值意味着输出会有一拍延迟。以温度采集为例正常升温每秒最多变化 0.1℃如果采样周期是 1 秒相邻两次的差值几乎不会超过 0.1℃。突然来一个 10℃ 的跳变几乎可以断定是干扰直接丢掉。这种滤波对高频随机噪声完全无效因为随机噪声每拍都可能小幅偏离真值限幅阈值设小了会把正常信号也削掉设大了又滤不掉噪声。2.2 中位值滤波对付尖峰脉冲的“老实人”中位值滤波的思想很直白连续采样 N 次排序后取中间值作为有效值。它对偶然的脉冲干扰有着天然的免疫力——因为少数极端值不会改变排序后的中间位置。但它有两个硬伤。一来采样 N 次就要存 N 个值排序还要耗时实时性很差。二来对快速变化的信号这种滤波会引入严重的滞后因为它输出的所谓“当前值”实际上来自 N 次采样之后。所以它的典型应用场景是温度、液位这类变化缓慢的过程量而不是电机电流、振动加速度这类高速信号。2.3 算术平均滤波与滑动平均滤波压随机噪声但有一个代价算术平均滤波的做法是连续采样 N 次求算术平均作为本次有效值。它对周期性噪声和随机噪声的抑制效果都很好但 N 越大信号的实时性就越差。比如 N100 对 ADC 数据取平均相当于输出天然滞后了 100 个采样周期。滑动平均滤波则更聪明一点维护一个长度固定的队列每次采样进来一个新值挤掉一个旧值对队列里的 N 个值求平均。它的输出滞后是恒定的大约(N-1)/2 个采样周期而且计算量可以通过“滑窗和”来优化不必每次重新累加全部 N 个数。这两兄弟在工程上出场率极高滑动平均用于示波器显示、平滑曲线算术平均用于直流信号、弱信号测量。2.4 一阶低通滤波代价最小的“万金油”但相位滞后明显一阶低通滤波的离散形式是Y(n) a*X(n) (1-a)*Y(n-1)其中系数 a 在 0 到 1 之间。它的优点是只需要一个浮点变量存上次输出内存开销极小代码只有三行是单片机上性价比最高的滤波手段。缺点同样突出。a 越小滤波越强滞后越严重a 越大跟上真实信号的速度越快但噪声残留也就越多。这是一种对“平滑度”和“跟随性”的妥协参数整定全靠对信号的先验认知。它对付高频随机噪声效果很好但对付低频漂移无能为力对付脉冲干扰则会把尖峰“拉宽”成一个缓变的小山丘——波峰还在只是变钝了。2.5 加权递推平均滤波给滑动平均加上“偏心眼”滑动平均滤波对队列里的 N 个值一视同仁而加权递推平均滤波给每个值分配不同的权重。离当前时刻越近的数据权重越大越旧的数据权重越小。这样做的目的是在平滑噪声的同时减轻历史数据拖累导致滞后。实现上可以先定义一个权重数组比如 N5权重分别为 1、2、3、4、5求和后再除以总权重。它比普通滑动平均的计算量略大但换来了更好的动态响应。要注意的是权重系数并没有“最优解”完全依赖你对信号变化速度的经验判断。如果给最近的数据权重太高滤波效果会退化噪声又冒出来了。2.6 防脉冲干扰平均滤波和限幅平均滤波处理工业现场的“组合拳”这两种都属于复合滤波思路是“先剔除坏值再取平均”。防脉冲干扰平均滤波的做法是连续采样 N 次去掉一个最大值和一个最小值对剩下的 N-2 个值求平均。它在平均滤波的基础上把脉冲干扰对结果的影响降到最低。问题在于去掉的极值未必都是干扰如果信号本身真的达到过最大值也会被误杀。限幅平均滤波则是先用限幅判断把本次采样值和上次有效值差值过大的样本扔掉然后再对剩余样本求平均。它在防脉冲干扰平均滤波的基础上更进一步通过把“异常跳变”在采样阶段就拦截掉避免了“把正常峰值误当干扰”的问题。这两种组合滤波是工业仪表、变送器里最常见的手段因为它们能同时对抗脉冲噪声和随机噪声只是实现上稍微费一点代码量和内存。3. 用 C 代码实现这 10 种滤波器结构体定义、函数实现、逻辑说明选型思路定了接下来就是把每个算法落成能直接编译运行的 C 代码。我会先给出每个滤波器用到的结构体和过滤函数最后给一段 main 函数把它们串起来做演示。代码风格的取向是“可移植、占用少、不依赖外部库”。3.1 准备一个公共的结构体集合为了不让每个滤波函数都扛着一堆全局变量常见做法是把滤波器的状态封装在结构体里。这样同一个滤波函数可以被实例化成多路分别处理多个通道的数据。typedef struct { float last_valid_value; // 上一次有效值 float limit_threshold; // 限幅阈值 int initialized; // 首次调用标记 } limit_filter_t; typedef struct { float buffer[10]; // 采样值缓冲区 int count; // 当前已采样的个数 } median_filter_t; typedef struct { float buffer[16]; // 滑动窗口 float sum; // 滑窗内值的和 int index; // 当前写入位置 int count; // 窗口内有效数据个数 } sliding_average_filter_t; typedef struct { float last_output; // 上一次输出值 float alpha; // 滤波系数0.01~0.5 之间常见 } lowpass_filter_t;代码逻辑上做了几件事每个滤波器通过结构体保存自己的状态避免全局变量冲突initialized标记用于首采时直接赋值避免把未经滤波的 0 当成有效数据缓冲区大小先写死成固定数组方便在单片机上按需修改。这里有一个值得注意的点结构体里的缓冲区大小是魔术数字。在正式工程里我通常会用宏定义或者 enum 把窗口大小抽出来不然换一个场景就改一次结构体很容易出低级错误。3.2 限幅滤波与中位值滤波的核心函数限幅滤波的代码很直接float limit_filter(limit_filter_t* flt, float raw_value) { if (!flt-initialized) { flt-last_valid_value raw_value; flt-initialized 1; return raw_value; } if (fabsf(raw_value - flt-last_valid_value) flt-limit_threshold) { return flt-last_valid_value; // 丢弃异常值 } flt-last_valid_value raw_value; return raw_value; }逻辑说明首次调用直接把采样值当作有效值避免 0 值污染。后续每一次采样计算与上次有效值的差的绝对值如果超过阈值就返回旧值否则正常更新。参数limit_threshold完全取决于真实信号的物理变化率并没有通用数值。中位值滤波实现时要处理缓冲区排序和取中间值两步float median_filter(median_filter_t* flt, float raw_value, int n) { if (flt-count n) { flt-buffer[flt-count] raw_value; } if (flt-count n) { return raw_value; // 样本不足时先直通 } // 拷贝一份做排序避免破坏写入顺序 float temp[10]; memcpy(temp, flt-buffer, n * sizeof(float)); // 简单插入排序n 很小不用上快排 for (int i 1; i n; i) { float key temp[i]; int j i - 1; while (j 0 temp[j] key) { temp[j1] temp[j]; j--; } temp[j1] key; } flt-count 0; // 消费完本轮 N 个样本 return temp[n/2]; }参数说明n是排序窗口宽度通常取 3 到 7工程上 5 最常用。窗口过小抗脉冲能力不足过大内存和计算量都会上去且滞后明显。排序部分拷贝了一份临时数组是为了保持写入顺序的连续性因为中位值滤波要求的是连续 N 次的原始采样而不是窗口滑动后的混叠数据。3.3 滑动平均与一阶低通的核心函数滑动平均用滑窗和来降低计算量float sliding_average_filter(sliding_average_filter_t* flt, float raw_value, int n) { if (flt-count n) { flt-count; } else { flt-sum - flt-buffer[flt-index]; // 挤掉最旧的值 } flt-buffer[flt-index] raw_value; flt-sum raw_value; flt-index (flt-index 1) % n; // 环形索引 return flt-sum / flt-count; }这段代码最关键的优化在于用环形索引和累计和避免了每来一个数据就重新累加整个窗口。sum保存当前窗口内所有值的和写入新值时先减掉被覆盖的旧值再加上新值。这样的时间复杂度是 O(1)而朴素实现是 O(n)在中断里高频调用时差异会被放大。一阶低通滤波的代码只有三行float lowpass_filter(lowpass_filter_t* flt, float raw_value) { flt-last_output flt-alpha * raw_value (1.0f - flt-alpha) * flt-last_output; return flt-last_output; }参数alpha的语义要理解透alpha 越大新采样值占的权重越大滤波越弱、响应越快alpha 越小历史输出占的权重大平滑效果好但滞后严重。如果说一个经验值采样周期 10ms 的温控系统里alpha 取 0.05 到 0.2 之间比较常见而采样周期 1ms 的电流环里alpha 可能需要到 0.5 左右才跟得上参考值。3.4 加权递推、防脉冲干扰和限幅平均的复合实现加权递推平均滤波需要预定义一组权重代码里我用静态数组保存float weighted_average_filter(float* buffer, int n, const float* weights) { float sum 0.0f; float weight_sum 0.0f; for (int i 0; i n; i) { sum buffer[i] * weights[i]; weight_sum weights[i]; } return sum / weight_sum; }调用前需要维护一个长度为 n 的缓冲区每来一个新值覆盖最旧的值。权重数组的构造常见做法是取 1 到 n 的等差数列如 weights[] {1, 2, 3, 4, 5}也可按指数衰减构造但那样小数乘法多在定点 MCU 上未必划算。防脉冲干扰平均滤波核心是先找最大值和最小值然后剔除求和float anti_pulse_average_filter(float* buffer, int n) { float max buffer[0], min buffer[0]; float sum 0.0f; for (int i 0; i n; i) { if (buffer[i] max) max buffer[i]; if (buffer[i] min) min buffer[i]; sum buffer[i]; } return (sum - max - min) / (n - 2); }这里默认只剔除一个最大值和一个最小值。如果现场脉冲干扰非常密集光剔一对极值不够需要先对数据排序再掐头去尾那实现就会和中位值滤波的思想重合。限幅平均滤波则是在这个函数之前先对每个新采样值做一次限幅判断不合格的直接用上一次有效值补位再做平均。这样做的优势是提前拦截了异常跳变平均过程不容易被污染。4. 参数调优采样频率、窗口大小、滤波系数的协同整定代码写完了并不是直接跑就行。每个滤波器都有参数要定而参数定得不合理算法效果会远差于预期。这一章讲透参数调节的方法论。4.1 采样频率决定滤波器的“视野”滤波器看到的信号质量一半取决于采样频率。采样频率太低信号的真实变化会被扭曲任何滤波算法都救不回来采样频率太高则噪声的频带会被展宽需要更强烈的滤波才能压住。工程上有一个朴素的判断标准采样频率至少要达到信号最高频率的 10 倍以上再谈滤波。以 50Hz 工频干扰为例采样频率至少要 500Hz滑动平均的窗口 N 才有意义。如果采样频率只有 100Hz窗口 N5 只能覆盖 0.05 秒对 50Hz 工频干扰几乎无能为力。4.2 滑动窗口 N 的选取一个基于截止频率的经验公式滑动平均本质上是一个 FIR 低通滤波器它的等效截止频率大约可以用这个经验公式估算f_cutoff ≈ 0.443 * f_sample / N这里 0.443 是滑动平均窗的主瓣带宽系数可以直接拿来用。如果你希望滤除 100Hz 以上的噪声采样频率是 1000Hz那么 N 可以按0.443 * 1000 / 100 ≈ 4.43取整为 5 左右。这个公式定给你的是一阶近似不要指望精确。但它的价值在于给你一个从“振动的频率特征”出发反向推导窗口大小的路径而不是靠感觉拍脑袋。注意N 越大滞后越明显如果滞后导致控制环震荡就说明 N 过了要减小。4.3 一阶低通 alpha 的确定先看阶跃响应再微调alpha 的整定比 N 还要“玄学”。我一般会先给系统一个阶跃输入观察输出从 10% 升到 90% 所需的上升时间。一阶低通的上升时间大约满足t_rise ≈ 2.2 / f_cutoff ≈ 2.2 * T_sample * (1 - alpha) / alpha这个式子可以做粗略反推想要上升时间 100ms采样周期 10ms那么 (1-alpha)/alpha ≈ 4.5解出来 alpha ≈ 0.18。跑一次看看实际曲线如果噪声压不住alpha 调小如果信号跟得太慢alpha 调大。每次只动 20% 以内的幅度多试几轮就能找到合适的点。值得警惕的是alpha 整定没有数学最优解因为它本质上是平滑度和滞后之间的权衡。做控制的同事调 PID 的时候常说“参数是试出来的”滤波器和它一模一样你只需要带着上述的估算值入场再用观察法收敛。4.4 限幅阈值的确定用“最大物理变化率”说话限幅阈值不产生相位滞后也不牺牲高频细节它唯一要禁掉的是超出物理可能的变化。所以它的设定依据应该是对象本身的最大变化速率。比如测压力系统加压最快 2kPa/s采样周期 10ms那么相邻两次采样的最大差值是 0.02kPa。阈值取 0.03~0.05kPa留出一点余量就够用。不要根据噪声幅度来定阈值因为噪声分布在不同工况下会变而物理变化率不会。如果发现限幅阈值太小导致信号变化被“削平”先检查是不是采样频率太低而不是一味放宽阈值。各滤波器的参数依赖关系我整理成一个简洁的对照表方便查阅滤波器主要参数依赖信号特征滞后表现限幅滤波限幅阈值最大物理变化率无滞后但会丢数据中位值滤波窗口 N脉冲干扰宽度固定 (N-1)/2 拍算术平均窗口 N噪声频率分布固定 (N-1)/2 拍滑动平均窗口 N目标截止频率固定 (N-1)/2 拍一阶低通alpha期望上升时间可调与 alpha 成反比加权递推权重数组信号变化速度比滑动平均小防脉冲平均窗口 N脉冲干扰宽度固定 (N-1)/2 拍5. 避坑指南滤波算法在工程落地上最容易踩的 5 个坑滤波算法的数学原理都不难代码也就几十行真正的血泪经验全在工程细节里。这一章把我见过、踩过的坑一条条拆开每条按“现象 → 原因 → 解决”的方式记录。5.1 第一个坑首次调用输出 0 导致启动瞬间飞车现象系统上电后滤波器前几拍输出 0如果输出直接送给执行机构电机会猛地蹿一下阀门会瞬间全开。很多人 debug 半天最后发现是滤波器的初始状态没设置。原因结构体是全局变量时C 语言会默认清零但如果定义在函数内部且未初始化里面的值是随机的。滤波函数在第一次调用时拿 0 当作历史值去算输出自然异常。解决每个滤波函数都要加initialized标记或直接在初始化函数里把缓冲区和历史值赋成当前采样值。我的习惯是在创建滤波器后先调用一次滤波函数把当前 ADC 值“灌”进去再做正常滤波。这段代码不值得省它能避免大多数启动阶段的诡异行为。5.2 第二个坑浮点运算在定点 MCU 上把性能拖垮现象滤波算法一开系统中断响应变慢或者主循环掉帧。用示波器量发现单次滤波调用耗时几十微秒频率一高直接爆。原因很多廉价 MCU 没有硬件浮点单元float运算要编译器模拟开销极高。再加上fabsf、memcpy这类库函数调用耗时比想象中大得多。解决在 Cortex-M4 以上内核直接用 float 没问题在 8 位、16 位 MCU 上能转成定点就别用 float。比如一阶低通的alpha可以放大 256 倍用整数表示Y (alpha_q * X (256 - alpha_q) * Y) 8效果基本一致速度提升明显。还要注意不是所有编译器都自带fabsf有的需要显式链接libm否则编译报错。5.3 第三个坑滑动平均的环形缓冲区索引越界现象程序运行一段时间后偶尔出现滤波输出跳变或者内存被踩坏。表面上看起来完全没有规律重启后又能跑很久。原因环形索引index在自增时没有正确处理回绕。假如n不是 2 的幂用(index 1) % n没问题但有些人图省事写成index然后if (index n) index 0。逻辑上没错但一旦index被外部修改或者类型是无符号数且n不匹配就有隐患。解决严格使用取模运算或者把窗口大小定为 2 的幂比如 8、16、32用按位与(index 1) (n-1)替代取模。后者的性能在中断里优势明显也能杜绝边界问题。5.4 第四个坑滤波后的数据直接用于 PID 控制导致相位滞后振荡现象控制系统的反馈值经过强滤波后肉眼看起来曲线很漂亮但系统就是稳不住轻微振荡甚至发散。把滤波撤掉振荡反而消失。原因滤波带来的相位滞后在控制环里相当于增加了延迟。控制理论对延迟非常敏感滞后 2~3 拍就足以让 PID 的相位裕度耗尽系统趋于不稳定。解决控制回路的滤波和显示用的滤波应该分开。显示通道可以为了平滑加大窗口控制回路则应该用轻微滤波甚至不滤波或者补偿滞后。一阶低通在控制回路里alpha 经验值不要低于 0.3否则系统几乎必然振荡。这也是“滤波不是越强越好”的最典型场景。5.5 第五个坑参数是“调出来的”但不要用同一组参数套所有工况现象设备在实验室跑得好好的上了现场数据就烂掉。现场工况一变之前的参数全部失效滤波效果还不如不滤波。原因滤波参数天然依赖信号特征。实验室的纯正弦叠加白噪声和现场的变频器干扰、继电器火花、电机换向尖峰频谱特征完全不一样。解决如果工况有数档参数应该做成分档切换。比如转速高时用强滤波转速低时用弱滤波或直接直通用查表方式切换滤波器参数。这样写起来不复杂但能避免设备在不同工况下出现“水土不服”的尴尬。6. 验证与进阶构造测试信号、量化滤波效果、再决定要不要学卡尔曼最后一章回到一个关键问题你怎么知道滤波器真的“工作正常”靠眼睛看曲线当然可以但更稳的做法是构造输入信号、量化评价指标再根据结果决定下一步要不要上更高级的算法。我在验证滤波器时会先在 PC 上用 C 语言写一个测试程序用三段信号模拟不同工况然后把滤波前后的数据导出做对比。第一段是阶跃信号用来测跟随性和上升时间第二段是正弦信号叠加白噪声用来测平滑效果和相位滞后第三段是在稳定值上注入几个脉冲尖峰用来测抗脉冲能力。量化的指标有两个一个是均方根误差RMSE反映滤波后与真值的偏差程度另一个是滤波前后的方差比方差降得越多说明噪声压制越强。但不要只看方差如果信号本身快速变化方差降得漂亮真实信号也被滤平了这时候要结合阶跃响应的上升时间一起看。// 量化滤波效果计算滤波输出的均方根误差和方差 float calc_rmse(float* output, float* reference, int len) { float sum 0.0f; for (int i 0; i len; i) { float err output[i] - reference[i]; sum err * err; } return sqrtf(sum / len); }对于普通的嵌入式应用10 种经典滤波器已经能覆盖 90% 以上的场景。如果做完上述验证发现噪声频带和信号频带严重重叠或者需要在线自适应改变滤波强度这时候才值得认真考虑卡尔曼滤波。卡尔曼滤波需要建立运动模型和噪声协方差矩阵代码量更大调参难度也不是一个量级。我的看法是先用经典滤波把问题定位清楚再用卡尔曼去替换那些确实无法满足的部分不盲目追新。滤波器的代码看起来简单真正决定成败的往往不在算法本身而在调用时机、数据结构、参数整定这些细节里。有段时间我迷信“高级算法”把一阶低通换成卡尔曼结果因为 Q 和 R 矩阵没调好输出比滤波前还抖。后来老老实实把经典滤波的参数吃透再回头看复合滤波才发现大部分现场噪声根本用不着上卡尔曼。希望这篇文章能帮你把这条路走稳少踩那些我已经替你踩过的坑。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?