☰
STM32实现五种DSP滤波器:原理、代码与性能对比实战
2026/9/25 1:58:52 网站建设 项目流程

1. 项目整体设计与实现思路

1.1 我为什么要在一颗MCU上做DSP滤波

先说个真实场景。我在一个传感器采集项目里,用STM32F407板子上的ADC以1kHz采样率采集信号。信号本身是一个1kHz左右的周期波形,但传感器调理电路里串进了50Hz工频干扰,现场偶尔还有尖峰脉冲,波形图一放大全是毛刺。这种问题在工业现场太常见了。

很多人第一反应是“上DSP芯片”,但项目成本、开发周期和硬件改动都不允许。实际上STM32这类带FPU和DSP扩展指令的Cortex-M4/M7处理器,跑常用信号处理算法已经绰绰有余。CMSIS-DSP库更是把FIR、IIR、LMS、FFT这些常用模块封装好了,底层用SIMD指令和浮点单元做加速,我只需要写应用层逻辑。

这个项目的目的很直接:在同一块STM32F407平台上,把FIR、IIR、中值、自适应、样条这5种滤波器全部跑一遍,每种都做成可以实际投入工程的代码模块,再用同一组带噪数据做横向对比,看看各自的耗时、资源占用和滤波效果,顺便把能踩的坑都踩一遍。

如果你正在纠结“MCU上到底该用哪种滤波算法”,或者想知道怎么把MATLAB里设计好的滤波器系数搬进STM32工程,那这篇内容应该能帮你省掉不少排查时间。

1.2 硬件平台与软件环境选型

先说平台选型。STM32家族里,我推荐从带FPU的型号入手,比如F4、F7系列或者H7系列。FPU对浮点滤波器的加速效果非常明显,纯软件模拟浮点会导致一次32阶FIR滤波耗时翻好几倍。我在F407上做测试,168MHz主频配合单精度FPU,跑一个128点的32阶FIR滤波,基准测试大概在20微秒级别,这个量级对大多数1kHz以内的采样系统完全够用。

软件工具链用这几样:

  • STM32CubeMX生成基础工程,配置时钟、ADC和定时器触发采样;
  • Keil MDK或者IAR作为IDE,必须确保勾选了“使用FPU”和“优化级别O2以上”;
  • CMSIS-DSP库,Keil的Pack安装器里可以直接搞定;
  • MATLAB或Octave的FDATool用来设计滤波器、导出系数;
  • 串口或JTAG/SWD接口用于数据回传和在线调试。

有人说Keil装STM32芯片包经常失败,这里给一个稳妥做法:去Pack Installer里选对应系列,如果下载失败,手动下载DFP芯片包文件后双击安装,比在线装靠谱得多。

1.3 整体测试框架怎么搭

我不想单独为每个滤波器写一个main文件,那样对比口径不统一。我的做法是设计一个固定长度的浮点输入缓冲区和输出缓冲区,测试信号在PC端生成好,通过串口一次性灌进板子里,滤波完成后再把输出回传。这样所有滤波器处理的是完全相同的输入数据,性能测试和效果对比才有意义。

测试信号我设计成三段混合:1kHz正弦波作为有用信号,叠加上50Hz工频干扰,再随机混入几个脉冲尖峰。这样一组数据同时考验滤波器的通带保持能力、工频抑制能力和抗脉冲能力,很直观。

每次滤波前都用DWT计数器测耗时。DWT的CYCCNT是Cortex-M内部的周期计数器,精度比SysTick高,测微秒级别的函数非常合适。初始化代码也简单。

CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; DWT->CYCCNT = 0; DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;

测耗时时就三行:

uint32_t t0 = DWT->CYCCNT; arm_fir_f32(&firS, input, output, blockSize); uint32_t dt = DWT->CYCCNT - t0; float time_us = dt / 168.0f;

这个框架搭好之后,后面每个滤波器进来都只用替换处理函数,测试效率非常高。

2. 五种滤波器原理与代码实现

2.1 FIR滤波器:线性相位是它最大的价值

FIR滤波器的本质是卷积运算,当前输出由前N个输入和N个系数的乘积累加得到。它没有反馈路径,所以结构上天生稳定,而且可以做成严格的线性相位,也就是说信号通过滤波器后虽然会有延迟,但波形形状不会失真。这对于需要保留波形细节、做同步检测的场合非常重要。

在设计低通FIR时,我用FDATool生成一个32阶、截止频率150Hz的滤波器。输出的是浮点系数,直接以C数组形式导出。需要强调的是,FDATool导出时选择“单精度浮点”模式,如果选成定点数,量化误差可能让高频段阻带特性明显变差。

CMSIS-DSP库里的FIR调用分三步:定义滤波器实例、初始化、执行滤波。

#include "arm_math.h" #define FIR_NUM_TAPS 32 #define BLOCK_SIZE 128 float32_t firCoeffs[FIR_NUM_TAPS] = { /* FDATool导出的32个系数 */ }; float32_t firState[FIR_NUM_TAPS + BLOCK_SIZE]; float32_t firOutput[BLOCK_SIZE]; arm_fir_instance_f32 firS; void fir_init(void) { arm_fir_init_f32(&firS, FIR_NUM_TAPS, (float32_t *)&firCoeffs[0], &firState[0], BLOCK_SIZE); } void fir_process(float32_t *input) { arm_fir_f32(&firS, input, firOutput, BLOCK_SIZE); }

这里有一个新手容易忽略的点:arm_fir_init_f32不会清零状态缓冲区。如果工程在运行中反复初始化,或者刚上电时状态缓冲里有随机值,输出的前几十个点会有一段“冒泡”过程。所以每次初始化之后,建议手动把状态缓冲区清零。

memset(firState, 0, sizeof(firState));

FIR的缺点是资源占用偏高。32阶系数只有128字节,但状态缓冲区需要(阶数+块大小)*4字节,200字节以下的RAM增量对现代MCU不算事,但如果你要做一个512阶的高性能滤波器,开销就会明显上升。在MCU上做FIR,建议阶数控制在200以内,块大小选择64或128,这样CMSIS-DSP里的优化能够充分发挥。

2.2 IIR滤波器:巴特沃斯与50Hz双T陷波实战

IIR和FIR最大的区别在于它有反馈,用较低的阶数就能达到很高的阻带衰减和陡峭的过渡带。代价是相位是非线性的,信号通过后波形会发生畸变,而且设计或实现不小心会不稳定。

我在项目里用IIR做了两个用途:一个是用8阶巴特沃斯低通滤波器,把50Hz工频和更高频毛刺一起压掉,只看1kHz以内的趋势波形;另一个是专门针对50Hz工频设计的双T型陷波器,只挖掉50Hz附近一个窄带,尽量不影响旁边频段的有用信号。

IIR设计我强烈建议不要手算系数,直接用FDATool。选择一个IIR低通,阶数设为8,巴特沃斯类型,采样率1000Hz,截止150Hz。导出时FDATool会给出SOS矩阵和增益值。SOS矩阵每行代表一个二阶节,格式是 [b0 b1 b2 a0 a1 a2]。CMSIS-DSP的biquad接口要求每个二阶节的系数格式是 [b0 b1 b2 a1 a2],也就是要先把a0归一化到1,再把它从数组里去掉。

这个转换是项目中最容易出问题的地方。我专门写了一个小工具函数,把FDATool的SOS转成CMSIS能用的系数数组:

// 输入: sos[6] = [b0 b1 b2 a0 a1 a2] // 输出: coeffs[5] = [b0/a0 b1/a0 b2/a0 a1/a0 a2/a0] void sos_to_cmsis(const float32_t *sos, float32_t *coeffs) { float32_t a0 = sos[3]; coeffs[0] = sos[0] / a0; coeffs[1] = sos[1] / a0; coeffs[2] = sos[2] / a0; coeffs[3] = sos[4] / a0; coeffs[4] = sos[5] / a0; }

四段biquad级联的I2R处理代码:

#define IIR_NUM_STAGES 4 float32_t iirCoeffs[IIR_NUM_STAGES * 5]; float32_t iirState[IIR_NUM_STAGES * 4]; arm_biquad_cascade_df1_instance_f32 iirS; void iir_init(void) { // 依次对每段SOS调用sos_to_cmsis,填入iirCoeffs arm_biquad_cascade_df1_init_f32(&iirS, IIR_NUM_STAGES, iirCoeffs, iirState); memset(iirState, 0, sizeof(iirState)); } void iir_process(float32_t *input) { arm_biquad_cascade_df1_f32(&iirS, input, firOutput, BLOCK_SIZE); }

陷波器的设计思路更值得展开。50Hz双T型陷波在模拟电路里很常见,用两个T型RC网络并联形成陷波。数字实现时,直接让FDATool设计一个50Hz陷波器,带宽选窄一点,比如Q值设为20。注意采样率很低时,50Hz对应数字频率大概0.1pi,离零频很近,系数会非常靠近单位圆。这种时候如果单片机内部精度不够,或者系数计算有误差,很容易出现极点漂移到单位圆外,滤波器直接自激振荡。

我的经验是:陷波器在浮点STM32上运行,系数用单精度够用,但必须保证FDATool导出时采样频率设置和你工程实际采样率完全一致。如果采样率因为定时器分频计算失误差了2%,陷波中心会偏到49Hz,然后你会发现50Hz纹波没有被压下去。

2.3 中值滤波:去脉冲毛刺的“重剑无锋”

中值滤波是这5种里最没有“DSP高级感”的一个,原理一句话:取一个窗口内的数据,排序后取中间值作为输出。

它的适用场景非常明确:去除脉冲型噪声和传感器偶发毛刺。我在测试数据里混入几个随机尖峰脉冲,其他滤波器多少都会把脉冲扩散成一个隆起,只有中值滤波能把这个尖峰彻底“消灭”掉。

代码实现也简单。窗口大小选5,对每个输出点取当前点前后各2个点,排序后取中间值。这里有个关键决策:排序算法选什么。窗口只有5时,插排比快排更合适,因为数据量小、分支少,最坏情况也就10次比较,足够快。

#define MED_WINDOW 5 #define MED_HALF 2 float32_t medFilter5(float32_t *buffer, uint16_t len, float32_t *out) { float32_t win[MED_WINDOW]; for (uint16_t i = 0; i < len; i++) { for (int j = -MED_HALF; j <= MED_HALF; j++) { int idx = (int)i + j; if (idx < 0) idx = 0; if (idx >= len) idx = len - 1; win[j + MED_HALF] = buffer[idx]; } // 5个元素排序,直接冒泡/插入 for (int m = 1; m < MED_WINDOW; m++) { float32_t key = win[m]; int n = m - 1; while (n >= 0 && win[n] > key) { win[n + 1] = win[n]; n--; } win[n + 1] = key; } out[i] = win[MED_HALF]; } }

边界处理是容易忽略的坑。窗口在数据起始和结尾部分会越界,我的写法是越界时直接复制边缘值,相当于把窗口向外填满。这样处理会让边界点输出略微失真,但至少不会产生随机值导致的异常突跳。

实际使用中,中值滤波的窗口大小直接影响“毛刺压制能力”和“有效信号保留度”。窗口越大,毛刺压制越彻底,但对快速变化的波形削峰效应也越明显。5点窗口适合采样率1kHz左右的脉冲噪声去除,如果你想压制更宽的尖峰,可以试7点或9点,但要注意延迟同步增加。

延迟方面,中值滤波属于非线性滤波器,严格地说没有“群延迟”的概念,但窗口中心点的输出可以粗略理解为固定延迟(MED_HALF)个采样周期,也就是2个点。这个延迟比同样去毛刺能力的FIR小得多,这是它的一大优势。

2.4 自适应滤波:LMS起步,真正处理“未知干扰”

前几种滤波器都是系数固定不变的,一旦设计好,无论输入怎么变,频响都固定。自适应滤波则不同,它能在运行过程中根据误差信号自动调整权重,适合处理那些频率或幅度缓慢变化的干扰。

LMS是最简单也最常用的自适应算法,更新公式不复杂:

y(n) = w(n) · x(n),误差e(n) = d(n) - y(n),然后w(n+1) = w(n) + 2μ·e(n)·x(n)。

其中x(n)是参考输入,d(n)是期望信号(通常是混入干扰的实际信号),y(n)是滤波器对干扰的估计,e(n)就是去除干扰后的“干净”信号。

这里最关键的工程问题是:参考输入x(n)从哪来。如果没有一路只含噪声、不含目标信号的参考输入,LMS就没法工作。我在项目里是这样处理的:针对周期性的工频干扰,把原始输入信号延时一定数量的采样点后作为参考输入。因为工频干扰是强周期的,而有用信号本身不是严格周期与延迟完全相关,这样LMS可以“学会”预测并消除工频成分。

CMSIS-DSP库提供了现成的LMS函数:

#define LMS_NUM_TAPS 32 float32_t lmsState[LMS_NUM_TAPS + BLOCK_SIZE]; float32_t lmsCoeffs[LMS_NUM_TAPS]; arm_lms_instance_f32 lmsS; void lms_init(void) { // 步长mu要按工程实测调整,这里先给一个保守值 arm_lms_init_f32(&lmsS, LMS_NUM_TAPS, lmsState, lmsCoeffs, 0.001f, BLOCK_SIZE); } void lms_process(float32_t *input, float32_t *desired, float32_t *out) { arm_lms_f32(&lmsS, input, desired, out, lmsError, BLOCK_SIZE); }

LMS的步长μ是整个算法最敏感的参数。μ太大,收敛快但稳态误差大,甚至直接发散;μ太小,收敛慢,可能干扰已经结束了还没跟上。工程上通常用输入信号功率做一个归一化LMS(NLMS)来规避这个选择难题,CMSIS-DSP库里没有现成的NLMS,需要自己写,其实就是在μ前面除一个参考输入的能量估计值。

另一个注意点是:LMS只有在参考输入与噪声高度相关、与有用信号弱相关时效果才好。如果参考输入里混入了大量有用信号,滤波器会连有用信号一起抵消,输出反而变差。

这个滤波器我在项目里的实际效果是:经过约500个点的收敛过程后,50Hz工频分量确实被压低得很明显,比固定陷阱IIR更“聪明”的地方在于,如果工频干扰漂移到49.8Hz,LMS能跟踪过去,IIR陷波器则无能为力。

2.5 样条滤波:嵌入式里被低估的平滑手段

样条滤波器在这个列表里最冷门,但它解决的是另一类问题:数据平滑和曲线重建。经典样条滤波通常指三次样条插值,把数据点用分段三次多项式连接起来,保证在每段连接点处值、一阶导数、二阶导数连续。这样得到的曲线非常平滑,不会出现多项式拟合常见的龙格现象。

但三次样条在嵌入式实时系统里很少直接使用,因为求解自然样条需要解一个三对角方程组,计算量随数据点数上升非常快,不适合按块实时处理。我在项目里做的是两件事:

一是离线场景:传感器标定数据、历史曲线重建,点数几百个以内,直接在PC端或者后期上传到电脑上处理,用科学计算库做样条拟合,效果好且不占用MCU算力。

二是实时场景:用一个简易的B样条滑动平均方案,本质上是一种加权窗口平滑,取5个点,分别乘上[1,4,6,4,1]再除以16,作为当前点的平滑值。这个系数组合对应的是三次B样条的离散卷积形式,能获得比算术平均更平滑、对高频更不敏感的曲线。

float32_t bSpline5(float32_t *buffer, uint16_t len, float32_t *out) { for (uint16_t i = 0; i < len; i++) { int idx[5]; for (int j = -2; j <= 2; j++) { int x = (int)i + j; if (x < 0) x = 0; if (x >= (int)len) x = len - 1; idx[j + 2] = x; } out[i] = (buffer[idx[0]] + 4.0f * buffer[idx[1]] + 6.0f * buffer[idx[2]] + 4.0f * buffer[idx[3]] + buffer[idx[4]]) / 16.0f; } }

不要小看这个简单的B样条平滑。它的好处是:不会像中值滤波那样产生平坦台阶,也不会像普通移动平均那样对数据变化反应过慢。在陀螺仪角度平滑、温湿度曲线平滑这类对实时性要求不苛刻的场景,效果很自然。

如果确实需要完整的样条插值又必须在板端跑,我建议分两步:先采完一整段数据,然后在无实时要求的空闲时间(比如RTOS的Idle任务)里去求解样条系数,输出到DAC或绘图。这样既能享受样条的平滑效果,又不会拖累实时采样循环。

3. 五种滤波器性能对比实测

3.1 测试条件说明

为了让对比结果有意义,我统一了测试条件:

  • 平台:STM32F407VGT6,主频168MHz,开启单精度FPU;
  • 编译:Keil MDK AC5,优化级别-O2;
  • 输入数据:128个浮点采样点,1kHz正弦波叠加50Hz工频,随机尖峰;
  • 每次滤波处理一个完整的128点数据块;
  • 耗时用DWT周期计数器测量,重复1000次取平均;
  • 所有滤波器均为浮点实现,CMSIS-DSP库版本为1.9.0。

需要说明的是,这个耗时会随编译器版本、优化选项、数据对齐方式略有浮动,但同平台横向对比的参考价值是够的。

3.2 性能数据表与初步结论

下面是5类滤波器在同一输入上的实测参考数据:

滤波器类型配置参数平均耗时(us)固定延迟(采样点)典型RAM占用(约)
FIR32阶,BLOCK=128约20(32-1)/2=15.5状态缓冲约640B+系数128B
IIR低通4段biquad,8阶约4.5频率相关,通常很小状态+系数约120B
IIR陷波2段biquad约2.5频率相关,通常很小状态+系数约60B
中值窗口5约28约2仅窗口临时数组
LMS自适应32阶约32收敛过程不稳定状态+系数约256B
B样条平滑5点窗口约7约2几乎为零

看到这个表,几个结论立刻浮现出来:

IIR在计算效率上碾压其他方案,8阶低通跑一个128点数据块只要4.5微秒,实时性要求高的场合它基本是首选。但代价是相位非线性,如果你后续要做波形还原或同步解调,就必须评估相位失真会不会影响系统指标。

FIR虽然耗时是IIR的4倍多,但换来严格的线性相位。在128点块处理中20微秒仍然非常短,1kHz采样率下单个采样周期是1000微秒,算力余量非常大。因此,除非你用的是频率极低的低端MCU,否则不必为了省一点算力牺牲FIR的相位优势。

中值滤波耗时28微秒看起来不低,因为每次都要对窗口内5个元素排序。这个耗时和窗口大小强相关,9点窗口会比5点窗口多一倍以上。如果你只需要去脉冲尖峰,用5点窗口足够。

LMS最慢,32阶跑128点要32微秒,同时还有收敛过程,不是上来就能用。它适合那些干扰特性“会变”的场景,如果干扰频率固定,固定系数滤波器更划算。

B样条平滑计算量很小,7微秒就能跑完,但它本质是低通平滑,对特定频带干扰没有针对性,适合做数据预处理而不是主滤波。

3.3 按业务场景怎么选型

选滤波器不能只看耗时表,要结合信号特性和系统约束。

如果你的目标是保留波形形状、做相位敏感分析,优先FIR。比如振动信号分析、电机相电流采样、心电信号预处理,这些场景线性相位非常重要,宁可多花点算力也不能让波形变形。

如果你的目标是实时性强、资源紧张,比如1kHz以上的高频采样、中断里要做滤波,IIR是首选。巴特沃斯低通做平滑,陷波器做工频抑制,都能在几微秒内完成。但要注意IIR的相位失真,以及高频段的稳定性。

如果信号被脉冲毛刺污染,其他滤波器都救不了你,上中值。注意中值滤波不能单独解决所有问题,最好与低通或IIR串联使用。

如果干扰源是移动的、时变的,比如电机转速变化导致振动频率变化,或者目标信号被宽带噪声覆盖,LMS自适应滤波会给你意外的惊喜,但要做好参考输入设计和收敛时间管理。

如果做的是平滑曲线、温漂数据拟合这类相对“温和”的任务,B样条平滑和分段样条插值是最舒服的选择,效果好,调试量小。

在实际工程里,我经常把一个IIR陷波器放在前级去工频,中间串一个5点中值去脉冲毛刺,最后再用FIR做一次带通提取目标频带。这种组合方式能发挥每种滤波器的优势,比单一高级算法更可靠。

4. 工程落地中的常见问题与排查心得

4.1 FDATool导出的系数“不对”怎么办

这是我在多个项目里见过最高频的坑,占比能到一半。现象是:用FDATool生成滤波器系数后,抄进STM32代码里,运行结果完全不是预期的波形,甚至出现振荡、发散。

第一个排查点是格式。FDATool导出时有多种格式可选,有的导出是16进制定点格式,有的是浮点格式。STM32工程如果用了float32_t数组,却把导出格式选成了定点数,那翻译过来就是一堆毫无意义的整数,波形必然错。

第二个排查点是SOS矩阵的ESCOZ。如果选择“导出SOS”而不是“导出单个系数”,CMSIS的arm_biquad_cascade_df1接口无法直接使用SOS矩阵。前面给出了sos_to_cmsis转换函数,这是必须做的归一化处理。直接拿SOS当系数用,等于把a0也当成了反馈系数,滤波特性会完全错乱。

第三个问题是量化。如果在MATLAB里用双精度设计,直接在板子上用单精度实现,一般场景下误差可控。但如果滤波器阶数高、过渡带特别窄,单精度舍入误差可能让极点位置发生偏移。这时有两个办法:一是改用零极点更分散的多个二阶节串联,二是把系数扩展到float64_t,但STM32F4的DP-FPU是软件模拟的,处理速度会掉一个量级,不到万不得已不用。

4.2 Keil环境、下载器和芯片包相关坑

这个项目的调试过程里,我遇到了几个和开发环境相关的经典问题,顺手记录一下。

第一个是Keil提示找不到STM32芯片包。绝大多数情况是Pack Installer网络问题,在线装不上。解决办法是去官网下载对应系列的DFP离线包,双击安装后重启Keil,在“Manage Run-Time Environment”里勾选CMSIS-DSP。安装成功后在工程里能看到arm_math.h头文件,链接时能找到对应的库。

第二个是ST-Link连接失败、无法识别USB设备。先别急着怀疑板子坏了,先查驱动。ST-Link的驱动和STM32的USB驱动不是一回事,用STM32CubeProgrammer自带的驱动更新工具最省事。还有一点,如果之前用J-Link或者别的调试器连过这个芯片,SWD引脚可能被配置成GPIO了,这时候要按住板子复位键同时点击下载,在芯片复位瞬间擦除选项字节,能救回来。

第三个是经典的“禁用了JTAG导致无法下载”。默认情况下STM32的JTAG/SWD功能是使能的,但如果初始化代码里调用了类似GPIO_PinRemapConfig(GPIO_Remap_SWJ_Disable, ENABLE)之类的函数,会把SWD引脚释放成普通GPIO。之后程序一旦跑起来,调试器就再也连不上了。项目里建议使用GPIO_Remap_SWJ_JTAGDisable,只关JTAG、保留SWD,这样既释放3个引脚,又保留调试口。万一真把SWD也关了,就只能用串口ISP或者ST-Link Utility的“Connect under reset”模式救回来。

4.3 滑动窗口滤波器的延迟到底是怎么来的

很多人在用FIR和中值滤波时抱怨“波形变慢了”,这其实是延迟在作祟。

对所有线性相位FIR,固定延迟就是(阶数-1)/2个采样点。32阶FIR,延迟15.5个采样点,在1kHz采样率下就是15.5毫秒。如果你的采样率提高到10kHz,延迟变成1.55毫秒,但阶数通常也需要提高以维持同样的物理频率特性。

中值滤波没有群延迟的概念,但因为输出是窗口中心点的值,可以固定理解为(MED_WINDOW-1)/2个采样点的延迟。5点窗口延迟2个点,7点窗口延迟3个点。

IIR的群延迟是频率的函数,低频段延迟大,高频段延迟小。这也是IIR波形看起来“容易发生畸变”的原因之一。如果对延迟敏感,比如要做实时闭环控制,最简单的做法就是选FIR,并且尽量用较低的阶数和较高的采样率来压缩绝对延迟时间。

还有一个容易被忽视的延迟源:分块处理。如果ADC用DMA攒够128个点才触发一次滤波,那无论滤波器本身多快,系统的极限延迟都至少是128个采样周期。1kHz采样下就是128毫秒的硬延迟。这种场景下,要么把块大小改小,比如32点,要么改成逐点滤波。CMSIS-DSP做逐点滤波时块大小设为1,仍然能跑,但优化效率会稍微下降。

4.4 滤波结果异常:发散、失真、边缘跳变的排查思路

我做这5种滤波器对比时,几乎每种都遇到了输出异常,整理成速查表:

表现可能原因处理建议
输出值指数增长,很快溢出IIR系数错误或极点不稳定检查SOS归一化是否完成,验证极点模值是否小于1
前几个输出点明显异常状态缓冲区未清零初始化后memset状态数组,尤其是FIR和IIR
输出波形整体幅度偏小滤波器本身通带增益衰减FDATool导出时检查增益,必要时在代码里乘上补偿系数
边界点有明显跳变边界处理方式不合适中值和B样条等窗口类算法要明确边界复制策略
输出高频噪声更严重系数量化误差或数据类型溢出检查FDATool导出格式,确认是浮点而非Q15

我看到过有人把巴特沃斯低通和高通直接串联后,整个通带增益变成-6dB还找不到原因。用FDATool设计时,每个滤波器默认都是0dB通带增益,但级联后增益会乘积叠加。最稳妥的验证方法是用信号发生器产生一个已知频率和幅度的正弦波输入,用串口看输出幅度。

4.5 实时性优化:从数据对齐到编译器优化

如果滤波耗时还没达到要求,有几个优化方向值得优先尝试。

第一个是数据对齐。CMSIS-DSP的浮点优化代码会使用SIMD或双字加载,输入输出缓冲区如果不是4字节或8字节对齐,性能会明显下降,甚至触发hardfault。定义缓冲区时用__ALIGNED(8)修饰,或者用arm_cache相关API配合DMA,Cortex-M4上最直观的做法是声明成全局数组并加对齐属性。

第二个是编译器优化级别。Keil里默认-O0时,我的32阶FIR耗时比-O2多出3倍以上。开发调试阶段用-O0,验证完成后发布版本用-O2或-O3,注意-O3可能引入浮点重排导致细微差异,最好用-O2。

第三个是循环展开。CMSIS-DSP内部已经做了大量循环展开优化,所以不要在应用层再去手写卷积,直接用库函数比自己写快得多。我见过有人不用CMSIS-DSP,自己写for循环做FIR,耗时多了五六倍还沾沾自喜,完全没有必要。

第四个是浮点类型选择。明确用float32_t而不是float64_t,F4系列只有单精度FPU,double类型回归软浮点,速度惨不忍睹。

5. 这几种滤波器在真实项目中还能怎么扩展

5.1 从低通到带通、带阻:滤波器组合的艺术

前文的代码示例以低通和陷波为主,但实际项目里带通和带阻的需求也很多。最简单的办法是把FDATool里的滤波器类型改成Bandpass,其他流程完全一样。

另一种工程上很常用的做法是“减法”式带通:先设计一个低通滤波器,再设计一个高通滤波器,两者串联,或者用原始信号减去低通输出来实现高通效果。这种方法在实现层面简单,但要注意相位叠加问题,串联后总相位特性是两个滤波器相位相加,整体和单个带通滤波器不完全等价。

如果你处理的是频谱分析任务,常用的做法是“带通滤波器组”,把整个频段切分成若干个子带,每个子带用一个IIR或FIR滤波器。这里就涉及到CIC抽取滤波和多相滤波器组合了。CIC滤波器在数字下变频和过采样应用中特别流行,因为它不需要乘法器,只要积分器和梳状器,非常适合MCU或FPGA做抽取前的抗混叠。

5.2 ADC过采样与测频法:采样前端对滤波效果的加成

滤波效果好不好,采样前端质量占了至少一半。如果ADC采样抖动大、信噪比差,再好的滤波器也很难把信号捞回来。

我自己在项目里做了两件事:一是采样率尽量取高,然后软件抽取。比如最终输出只需要100Hz带宽,我用10kHz采样,先用简单的均值滤波做4倍抽取,再用IIR做精细滤波,这样可以有效抑制带外噪声折叠。二是用定时器触发ADC,避免软件查询造成的抖动。

“测频法”也是从热词列表里看到的常见需求。实际上,用STM32定时器的输入捕获模式数上升沿/下降沿,测频率是最经典的做法。如果只用一个通道捕获频率,加上中值滤波去毛刺,然后再用FIR做一次趋势平滑,测出来的频率曲线就会非常稳定。这个组合我在电机测速项目里试过,效果比单纯捕获后直接输出好太多。

5.3 把滤波结果可视化:串口调试和LVGL曲线

滤波算法开发最怕“盲调”,看不见波形就不知道有没有问题。我建议在调试阶段用串口把原始数据和滤波后数据一起回传到PC,用串口绘图软件或Python脚本实时画图。这样可以直观看到滤波前后差异,也能快速定位到是参数问题还是代码问题。

等产品化之后,如果带屏幕,可以接LVGL控件画曲线。但要注意,LVGL刷新不能放在和ADC采样中断同一个优先级,否则会阻塞采集,导致采样不均匀,滤波效果大打折扣。用RTOS的话,把采集和滤波放高优先级任务,把显示放低优先级任务,是最合理的分工。

5.4 这套滤波框架怎么沉淀成可复用模块

做完这个项目后,我把5种滤波器封装成了一个统一的处理接口:初始化函数、处理函数、参数配置结构体。这样在后续项目里,只需要改参数配置就能原地切换滤波器类型,不用改上层业务代码。

类型定义大致是这样:

typedef enum { FILTER_TYPE_FIR, FILTER_TYPE_IIR, FILTER_TYPE_MEDIAN, FILTER_TYPE_LMS, FILTER_TYPE_BSPLINE } filter_type_t; typedef struct { filter_type_t type; uint16_t blockSize; uint16_t order; union { struct { float32_t *coeffs; } fir; struct { uint8_t stages; float32_t *coeffs; } iir; struct { uint8_t window; } median; struct { uint8_t taps; float32_t mu; } lms; struct { uint8_t window; } bspline; } cfg; } filter_config_t;

公司内部如果有多人协作,一定要把FDATool导出的系数文件单独存放,并注明对应的工程版本。我踩过一个大坑:测试固件用了旧版系数,功能板却刷了新固件,查问题查了整整一天才发现是系数文件版本不一致。

另外,我在实际使用中最大的体会是:滤波器不是越复杂越好,而是越匹配信号越好。先搞清楚你要滤掉什么、保留什么、允许多大延迟、有多少算力和内存预算,再决定用什么方案。多数项目里,一个IIR低通加一个中值滤波组合,已经能解决80%的问题。

如果你想把这条路继续走下去,下一步建议去啃啃CMSIS-DSP里FFT相关的接口,然后尝试把FFT和自适应滤波结合起来,做一个频谱自适应的噪声抑制器。那又是一个新世界了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询