ARTICLE DETAIL

资讯详情

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

从 FDAtool 到 C:IIR 滤波器 SOS 系数导出与定点实现

从 FDAtool 到 C:IIR 滤波器 SOS 系数导出与定点实现 简介这份 PDF 资料面向数字信号处理、嵌入式开发与语音通信方向的工程师及学生围绕 MATLAB FDAtool 设计 IIR 滤波器并把参数导出为 C 语言文件这一实际问题展开。内容从角频率与采样频率的换算关系切入说明通带、阻带截止频率边沿频率的设定方法并对比 FIR 与 IIR 在阶数、计算量和相位特性上的差异随后以一个采样率 8kHz、通带 80–3200Hz、用于滤除 50Hz 工频干扰的带通滤波器为例演示在 FDAtool 中设定指标、由工具自动计算并得到 36 阶18 个二阶节滤波器的完整过程还涉及自定义阶数、增益项对精度与稳定性的作用以及把系数转换为单段形式、导出可直接在 C 代码中复用的头文件等关键环节。资源为单个 PDF 文件压缩包约 1.08MB结构紧凑、便于随查随用。目前已有 1423 人学习适合希望把 MATLAB 滤波器设计成果迁移到嵌入式或其他编程环境的读者参考。1. FDAtool 生成的系数为什么不能直接手敲进 C 文件做嵌入式音频、电机电流环或者传感器抗混叠的人多半踩过同一个坑在 MATLAB 里画出来的频响漂漂亮亮系数抄进 C 代码一跑实测曲线要么通带塌下去一块要么高频直接自激。问题通常不在双线性变换推错了而在两件事上。第一高阶 IIR 的传递函数直接用 b、a 多项式实现极点对系数的敏感度随阶数指数级上升浮点截断一下极点就跑到单位圆外第二FDAtool 导出的系数是按 a0 归一化过的手抄时忘了把 a0 除掉整个滤波器的增益和极点位置都会偏。FDAtool较新版本里入口叫 Filter Designer真正的价值不是画曲线而是它能把设计好的滤波器拆成级联二阶节导出成 C 头文件让 MATLAB 里的仿真和 DSP 上跑的代码尽量对齐。这条链路适合两类人一类是要把 MATLAB 做离散时间系统的结果搬到单片机上的人另一类是需要在定点 DSP 上控制阶数的工程实现者。下面按定参、选结构、导文件、写 C、复核的顺序把这条路走一遍。2. 用 FDAtool 设计 IIR原型怎么选、参数怎么填2.1 命令行先定参GUI 后复核GUI 适合调参不适合复现。同一组参数今天点出来一个结果明天换个版本可能默认值就变了所以我一般先用命令行把阶数和系数算出来再打开filterDesigner用fvtool复核曲线。这样可以保证参数落在版本库里随时能重跑。五个必填量是采样率fs、通带边缘fpass、阻带边缘fstop、通带波纹rp、阻带衰减rs。其中fpass/(fs/2)必须严格落在 0 到 1 之间写成 1 或者大于 1FDAtool 会直接提示归一化频率非法。fs 48000; % 采样率 Hz fpass 3000; % 通带边缘 Hz fstop 4000; % 阻带边缘 Hz rp 0.5; % 通带波纹 dB rs 60; % 阻带衰减 dB Wp fpass/(fs/2); % 归一化到 Nyquist 频率 Ws fstop/(fs/2); [N, Wn] ellipord(Wp, Ws, rp, rs); % 求满足指标的最小阶数 [b, a] ellip(N, rp, rs, Wn); % 直接型传递函数系数 [sos, g] tf2sos(b, a); % 拆成二阶节g 为总增益 fvtool(b, a, Fs, fs); % 看幅频、相频、群延迟 disp([所需最小阶数 N , num2str(N)]);这段代码的逻辑是ellipord先根据四个边界指标反推最小阶数避免手工试阶数ellip按这个阶数生成分子分母多项式tf2sos再把它拆成若干二阶节并抽出总增益g。参数上最需要留意的是fpass和fstop的间距两者越靠近ellipord返回的N越大乘加次数和定点溢出风险一起上涨。一般过渡带宽度小于通带边缘的 20% 时就该认真考虑降采样或者换结构了。2.2 四种 IIR 原型的取舍同样一组频带指标换原型得到的阶数和相位特性差别很大。椭圆滤波器能把阶数压到最低但代价是通带和阻带同时等波纹相位非线性最严重巴特沃斯通带最平坦阶数却常常高出一截。选型时先看你的约束是算力、延迟还是相位线性度。原型通带特性阻带特性过渡带陡度相位非线性典型场景Butterworth最平坦单调下降最缓中等生物信号、抗混叠Chebyshev I等波纹单调下降较陡较差窄过渡带音频均衡Chebyshev II单调下降等波纹较陡较好阻带抑制要求高Elliptic等波纹等波纹最陡最差阶数受限的定点 DSP实战里的经验值是MCU 上没有硬件浮点、又必须把阶数压到 6 阶以内优先椭圆对相位失真敏感、能接受十几阶的用巴特沃斯。Chebyshev II 常被忽略它在阻带必须干净、通带允许一点起伏的场合其实很划算。选型不要只看曲线好不好看要看阶数带来的乘加次数能不能塞进你的采样周期。2.3 为什么要把高阶传递函数拆成 SOSIIR 的极点在 z 平面上越靠近单位圆多项式系数的一点点扰动就越容易被放大。八阶直接型的 a 系数里某一位的尾数误差可能让一对共轭极点在量化后跑到单位圆外滤波器直接从能滤变成会炸。级联二阶节把一个大多项式拆成若干个二阶子系统的乘积每个子系统的极点只由它自己的两三个系数决定敏感度被摊薄定点化后稳定性明显好过直接型。拆成 SOS 之后还有两个附带好处一是每个二阶节可以单独做饱和和舍入策略二是可以用流水线或者 SIMD 并行处理多个节。需要注意tf2sos的默认配对策略并不总是最优它在节顺序上只保证数值上的合理性不保证定点实现里最优。如果定点噪声偏大可以把tf2sos的第二个输出参数换成down或up调整配对顺序再实测噪声底。3. 把 FDAtool 的系数导出成可编译的 C 语言文件3.1 Export to Workspace 与 Generate C header 的差别FDAtool 的 File 菜单里有几条不同的出口选错了后面要么格式对不上要么没法进版本管理。Export to Workspace 是把设计对象、系数、SOS、增益导出成 MATLAB 变量适合脚本继续处理Generate C header 直接吐一个单精度浮点数组适合临时验证但它和 GUI 当前设置强绑定别人拿到头文件也还原不出你当初的设计条件。导出项输出内容适用阶段需要留意Export to Workspace对象、b/a、SOS、g脚本继续加工必须选 SOS 形式别选 b/aGenerate C headerfloat 数组头文件快速粘贴验证无法追溯设计参数Generate MATLAB code可重跑的 M 脚本进版本库不是 C需二次转换自己 fopen/fprintf 生成自定义 .h/.c量产工程自由度最高要自己定格式我几乎不直接用 Generate C header因为它给出的数组是单节的 b/a 形式不是一个完整的二阶节矩阵落到代码里还要自己重组。更稳妥的做法是把sos和g拿到手自己用文件读写生成需要的 .h 和 .c格式完全按目标工程来。3.2 用 fopen 和 fprintf 自动生成 .h 与 .cMATLAB 写 C 文件本质上就是一段普通的 C 语言文件读写操作只是这段代码写在 MATLAB 里。把下面这个函数放到你的设计脚本后面调用每次改参数重跑头文件和源文件一起刷新不会出现代码里的系数和最新曲线对不上这种事。function export_sos_c(sos, g, outdir) % 把 SOS 矩阵和总增益写成 C 可编译的 .h / .c % sos: N x 6每行 [b0 b1 b2 a0 a1 a2] nsec size(sos, 1); sos(:, 4) 1; % a0 统一归一化为 1 hf fopen(fullfile(outdir, iir_sos.h), w); fprintf(hf, #ifndef IIR_SOS_H\n#define IIR_SOS_H\n\n); fprintf(hf, #define IIR_SOS_SECTIONS %d\n\n, nsec); fprintf(hf, extern const float iir_gain;\n); fprintf(hf, extern const float iir_sos[IIR_SOS_SECTIONS][6];\n\n#endif\n); fclose(hf); cf fopen(fullfile(outdir, iir_sos.c), w); fprintf(cf, #include iir_sos.h\n\n); fprintf(cf, const float iir_gain %.9ef;\n\n, g); fprintf(cf, const float iir_sos[IIR_SOS_SECTIONS][6] {\n); for k 1:nsec if k nsec, sep ,; else, sep ; end fprintf(cf, { %.9ef, %.9ef, %.9ef, 1.0f, %.9ef, %.9ef }%s\n, ... sos(k,1), sos(k,2), sos(k,3), sos(k,5), sos(k,6), sep); end fprintf(cf, };\n); fclose(cf); end逻辑上分两步先写头文件把节数和外部声明固定下来再写源文件把总增益和每个二阶节的六个数按行展开。%.9e保证单精度浮点能完整还原位数少了会导致 C 里的系数和 MATLAB 不一致位数多了又白占空间。sos(:,4)1这一步别省tf2sos返回的 a0 本来就该是 1但如果你从别处拿到系数不归一化就会出现整体增益偏差。调用时直接export_sos_c(sos, g, pwd)即可。3.3 系数排列、a0 归一化与定点化约定生成文件之前要把格式约定死否则 C 侧一不小心就把 b1 当成 a1 用了。我习惯的排列是每行[b0 b1 b2 a0 a1 a2]a0 恒为 1处理时直接跳过第 4 列。如果目标平台是定点 DSP还要决定 Q 格式Q15 表示范围是 [-1,1)精度 2^-15适合 int16Q31 精度 2^-31适合 int32带 FPU 的 MCU 直接用 float32 最省事。格式表示范围量化精度适用位宽Q15[-1, 1)2^-15int16Q31[-1, 1)2^-31int32float32约 ±3.4e3824 bit 有效带 FPU 的 MCU定点化之前先看系数绝对值如果某个 b0 已经接近或超过 1直接按 Q15 放会溢出这时要么把这一节的增益挪到前面的iir_gain里要么整条链用 Q31。量化后一定要把系数读回 MATLAB 重画频响量化误差对高 Q 值节的极点影响最大。4. C 侧级联二阶节的实现与验证4.1 转置直接型 II 的 C 实现二阶节有几种实现形式转置直接型 IIDF2T状态量少、数值特性好是定点实现里最常见的选择。它只需要两个状态量w[0]、w[1]而且状态量的动态范围比直接型小。#include iir_sos.h typedef struct { float w[IIR_SOS_SECTIONS][2]; /* 每个二阶节两个状态量 */ } iir_state_t; /* 转置直接型 II 单节处理c [b0 b1 b2 a0 a1 a2] */ static inline float biquad_df2t(const float c[6], float *w, float x) { float y c[0] * x w[0]; w[0] c[1] * x - c[4] * y w[1]; w[1] c[2] * x - c[5] * y; return y; } /* 整条级联链增益在入口统一缩放一次 */ float iir_process(iir_state_t *s, float x) { int k; x * iir_gain; for (k 0; k IIR_SOS_SECTIONS; k) { x biquad_df2t(iir_sos[k], s-w[k], x); } return x; }biquad_df2t里w[0]承担的是上一节输出的历史分量w[1]是更早一个样本的延迟项。c[4]、c[5]对应 a1、a2因为 a0 恒为 1不用再做除法。iir_gain放在循环外做一次乘法比在每个节里各自乘一遍少很多乘加也避免中途某一节增益过冲。状态量w必须每个通道各有一份左右声道共用一份会串音。4.2 用阶跃与扫频对齐 MATLAB 和 C 的输出代码写完不代表参数接对了。我一般用两组信号交叉验证一段阶跃看瞬态响应和超调一段线性扫频看幅频有没有出现不该有的凹陷。阶跃信号最能暴露极点量化误差扫频则能暴露系数排列错位。MATLAB 侧用filter(sos, ...)得到参考输出C 侧把同样的输入喂进iir_process两条曲线叠在一起看。% 阶跃输入对比 MATLAB 参考输出 n 4000; x [ones(200,1); zeros(n-200,1)]; y_ref filter(sos, 1, x); % sos 已在 2.1 求出 % C 侧把 iir_process 的输出存成 out.csv 后读回 y_c readmatrix(out.csv); plot(1:n, y_ref, b, 1:n, y_c, r--); legend(MATLAB, C); xlabel(sample); ylabel(amplitude); grid on;误差允许一个很小的量化底噪但不能有系统性偏移。如果 C 侧整体放大或缩小了一个常数多半是iir_gain丢了如果只在某些频点偏差大往高 Q 值那一节去查量化精度。阶跃响应检查完再用扫频看通带边缘有没有提前滚降那通常意味着过渡带参数填窄了。4.3 Q 格式、溢出与二阶节顺序定点实现里最常见的故障是中间结果溢出而它往往不会立刻让程序崩溃只是让输出偶尔冒出刺耳的爆音。DF2T 的状态量w[0]动态范围比输出大用 Q15 时最容易被忽略。保险做法是给每节加饱和判断或者干脆把状态量用 Q31 存、系数用 Q15 存乘加后再移位。二阶节的顺序也影响噪声。一般把极点离单位圆最远的那一节放最前面让信号先衰减再经过高 Q 值节能减少中间溢出的概率。每节的增益分配也有讲究如果某一节 b0 特别大把它挪到整链的入口增益里避免状态量被撑满。改完顺序后记得重跑一次阶跃对比顺序变了输出在数值上应该几乎一致只有瞬态底噪略有不同。5. 量化后频响复核与系数重排技巧定点化或者降到单精度之后最值得做的事不是继续调参而是把 C 里实际使用的系数读回 MATLAB重画一条频响和理想曲线叠在一起。这一步用freqz做几行代码就能看出量化到底伤了多少。% 把 C 里实际使用的系数按 Q15 量化后回灌验证 sos_q sos; sos_q(:,1:3) round(sos(:,1:3) * 2^15) / 2^15; sos_q(:,5:6) round(sos(:,5:6) * 2^15) / 2^15; [h1, f] freqz(sos, 4096, fs); [h2, ~] freqz(sos_q, 4096, fs); plot(f, 20*log10(abs(h1)), b, f, 20*log10(abs(h2)), r--); legend(理想系数, Q15 量化后); xlabel(frequency (Hz)); ylabel(magnitude (dB)); grid on;判断标准很直接通带内的两条曲线偏差应该在 0.1 dB 以内阻带内的底噪抬高不超过几个 dB 就算合格。如果量化后通带边缘明显塌陷说明某一节的 Q 值太高Q15 撑不住这时候要么把该节换成 Q31要么降低rp放宽通带指标。反过来如果阻带底噪抬高很多但通带几乎没变多半是零点位置的量化误差可以考虑把相邻两节的零点重新配对。另一个实用技巧是二阶节的系数重排。tf2sos默认按数值稳定性配对但定点实现里还有一层哪一节的增益该被抽出去的问题。把每节的 b0 单独看一遍找一个最接近 1 的作为增益基准其余的按比例缩放能明显降低一节的动态范围压力。重排之后必须重新跑 4.2 的阶跃对比确认输出和重排前在数值上一致只有底噪级别的差别才算对。频响复核这件事不要只做一次每次改量化位宽、改节顺序、改编译器浮点选项都值得再画一次那条红色虚线。本文还有配套的精品资源点击获取
返回列表