ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

STM32实现五种DSP滤波器:原理、代码与性能对比实战

STM32实现五种DSP滤波器:原理、代码与性能对比实战 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(n1) 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的步长μ是整个算法最敏感的参数。μ太大收敛快但稳态误差大甚至直接发散μ太小收敛慢可能干扰已经结束了还没跟上。工程上通常用输入信号功率做一个归一化LMSNLMS来规避这个选择难题CMSIS-DSP库里没有现成的NLMS需要自己写其实就是在μ前面除一个参考输入的能量估计值。另一个注意点是LMS只有在参考输入与噪声高度相关、与有用信号弱相关时效果才好。如果参考输入里混入了大量有用信号滤波器会连有用信号一起抵消输出反而变差。这个滤波器我在项目里的实际效果是经过约500个点的收敛过程后50Hz工频分量确实被压低得很明显比固定陷阱IIR更“聪明”的地方在于如果工频干扰漂移到49.8HzLMS能跟踪过去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阶BLOCK128约20(32-1)/215.5状态缓冲约640B系数128BIIR低通4段biquad8阶约4.5频率相关通常很小状态系数约120BIIR陷波2段biquad约2.5频率相关通常很小状态系数约60B中值窗口5约28约2仅窗口临时数组LMS自适应32阶约32收敛过程不稳定状态系数约256BB样条平滑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配合DMACortex-M4上最直观的做法是声明成全局数组并加对齐属性。第二个是编译器优化级别。Keil里默认-O0时我的32阶FIR耗时比-O2多出3倍以上。开发调试阶段用-O0验证完成后发布版本用-O2或-O3注意-O3可能引入浮点重排导致细微差异最好用-O2。第三个是循环展开。CMSIS-DSP内部已经做了大量循环展开优化所以不要在应用层再去手写卷积直接用库函数比自己写快得多。我见过有人不用CMSIS-DSP自己写for循环做FIR耗时多了五六倍还沾沾自喜完全没有必要。第四个是浮点类型选择。明确用float32_t而不是float64_tF4系列只有单精度FPUdouble类型回归软浮点速度惨不忍睹。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和自适应滤波结合起来做一个频谱自适应的噪声抑制器。那又是一个新世界了。
返回列表