资讯动态

MATLAB设计巴特沃斯低通滤波器并转嵌入式C代码完整实践

发布时间:2026/10/24 4:44:39 来源:尧图企业网站定制
最近在整理一个信号采集前端的项目传感器出来的信号里混了明显的工频干扰和高频抖动软件上必须加一级数字低通滤波。方案本身不算复杂但真正做下来我发现一个很现实的问题不少人会用MATLAB直接调butter把滤波器系数算出来下一步却卡住了——这些系数怎么安全、准确地变成嵌入式C代码中间涉及浮点精度、离散域结构选择、状态变量更新顺序等一堆细节任何一个地方处理不好仿真波形漂亮一上板就面目全非。这篇文章我就拿这个低通滤波器作为完整例子把“MATLAB设计巴特沃斯滤波器系数 - C代码化”的整条链路拆开讲包括我实际踩过的坑和最后验证输出用的方法。适合正在做嵌入式信号处理、MCU/DSP上实现数字滤波器的朋友参考即使你对MATLAB不算熟按步骤操作也能跑通全流程。1. 为什么非要绕一圈先MATLAB算系数再转成C代码很多人第一反应是巴特沃斯滤波器不是有现成公式吗直接把传递函数写进C里不行吗理论上可以实际做起来非常痛苦。关键不在“能不能写”而在“设计过程需要反复迭代”。1.1 手算巴特沃斯系数有多麻烦巴特沃斯低通滤波器的幅度平方函数是 1/(1(ω/ωc)^(2n))要求系数就得先求极点、展开多项式、再做双线性变换。n2的时候手算还能忍n4或者更高时多项式展开后的系数已经是一长串小数稍微算错一位幅频响应就完全不对了。更麻烦的是指标一变比如采样率从1kHz改成2kHz、截止频率从50Hz改成80Hz整套推导又得重来。MATLAB做这件事就轻松很多输入采样率、截止频率、阶数输出就是一组完整系数还能直接画频率响应看效果。设计阶段本来就是反复试参数的过程没有工具辅助迭代成本太高。所以我的工作方式一直是设计阶段全部在MATLAB里完成确认指标无误后再把系数固定下来去做C代码化。这两个阶段要分开看待设计是“算得对不对”的问题代码化是“移得准不准”的问题。1.2 巴特沃斯和切比雪夫、椭圆滤波器到底差在哪设计低通滤波器时选型是第一步。巴特沃斯不是唯一选择但它在大多数通用场景里是最“省心”的。我把三种常见滤波器的特点对比一下类型通带特性过渡带相位响应典型用途巴特沃斯通带内最平坦无波纹较宽相位非线性相对平缓通用滤波、传感器信号调理切比雪夫I型通带有等波纹较窄相位非线性更明显对过渡带宽度有较高要求椭圆/考尔通带和阻带都有波纹最窄非线性最强对阶数/资源限制极其严格巴特沃斯的优势是通带平坦度最高不会在有用信号频段里引入幅度起伏。代价是同样的过渡带要求下它需要更高的阶数。但对MCU来说多一两个二阶节的计算量通常可以接受所以默认选巴特沃斯是一个稳妥的起点。另外提一句相位问题。巴特沃斯虽然相位不是线性的但在通带内的群延迟相对平滑很多场景下无需额外补偿。如果系统对相位敏感比如闭环控制那更该考虑的是Bessel滤波器甚至零相位滤波这里不展开。1.3 代码化路径也有好几种选适合自己的从MATLAB系数到C代码我见过三种主流路径路径一MATLAB Coder自动生成C代码。优点是省事设计完模型后点生成就行。缺点是生成的代码往往带有较大运行时库依赖结构偏工程化塞进裸机MCU里会很臃肿而且你不一定看得懂它内部的定点策略。路径二HDL Coder走FPGA实现。适用于硬件加速需求但和普通MCU场景基本无关周期也长。路径三手动把系数翻译成C。需要自己写滤波结构但代码完全可控体积小、可读性强也方便嵌入到现有工程里。我自己大部分场景都是走这条路。这篇文章核心也是路径三用MATLAB把滤波器系数算准然后手动实现一个可用的Biquad级联C代码。2. 先用MATLAB把设计指标变成一组靠谱的滤波器系数2.1 设计指标怎么定采样率、截止频率、阶数假设我的传感器信号采样率 fs 1000 Hz需要保留40Hz以内的有效信号50Hz工频及其谐波尽量压掉。那么截止频率我取 fc 45 Hz左右。注意截止频率不是“一按就停”的悬崖而是衰减到-3dB的位置所以设计时要给过渡带留余量。阶数n不是拍脑袋定的可以先用buttord估算fs 1000; % 采样率 fc 45; % 通带截止频率 fstop 90; % 阻带起始频率 rp 1; % 通带最大衰减 dB rst 20; % 阻带最小衰减 dB % 归一化频率 Wp fc / (fs/2); Ws fstop / (fs/2); [n, Wn] buttord(Wp, Ws, rp, rst);这里返回的 n 就是满足指标所需的最小阶数。我实测下来fc45Hz、fstop90Hz、阻带衰减要求20dB时阶数通常在2到4之间。对MCU来说4阶意味着2个二阶环节联计算量很小完全可以直接用。2.2 butter函数的归一化频率最容易搞错接着用butter生成系数[b, a] butter(n, Wn, low);这里的Wn必须是归一化到奈奎斯特频率的值即fc / (fs/2)。比如 fs1000Hz奈奎斯特频率是500Hz如果fc45Hz那么Wn 45/500 0.09。如果忘了除以 fs/2MATLAB会理解为相对奈奎斯特频率的比例设计出来的滤波器完全不同这是新手最容易踩的坑。得到b和a后先别急着导出用freqz看一眼频率响应[H, f] freqz(b, a, 4096, fs); plot(f, 20*log10(abs(H)));观察三个点通带是否平坦、-3dB点是否在45Hz附近、90Hz处衰减是否达到20dB以上。如果指标不满足调整阶数或重新设置rp、rst这是一个反复验证的过程直到响应满意再固化为最终系数。2.3 阶数不是越高越好这点要冷静有人觉得阶数越高衰减越陡、效果越好真不一定。阶数高了系数动态范围变大C代码浮点实现时对舍入误差更敏感相位延迟也会增加实时性要求高的场合会明显感觉到“滞后”。另外MCU上每个二阶节都需要独立的乘法累加运算阶数翻倍计算量也翻倍。我通常按这个原则选阶满足阻带衰减指标后再留一阶余量即可不要一味追求高指标。别忘了数字滤波器实现时还要考虑量化误差指标越好电路实现越娇气。3. 导出系数这一步藏着三个特别容易踩的坑MATLAB里butter算出来的系数看起来是一串小数直接复制到C里似乎很简单。但实际操作中至少有三个坑。3.1 显示位数不等于存下来的精度MATLAB命令行窗口默认只显示4~5位小数直接肉眼抄下来或者用默认disp输出精度严重不足。比如系数可能是1.234567890123456界面上显示成1.2346抄到C里误差已经很大了。正确做法是用fprintf指定精度或者用format long查看完整数值。我自己的习惯是直接用脚本把系数写成C数组% 把系数格式化输出为C数组形式 fprintf(float b0 %.10f;\n, b(1)); fprintf(float b1 %.10f;\n, b(2));至少保留10位有效数字。后续如果做定点化会在这个浮点系数基础上再量化精度影响可控。3.2a系数第一个值不是1C代码里必须归一化MATLAB返回的传递函数是H(z) (b0 b1·z⁻¹ b2·z⁻²) / (a0 a1·z⁻¹ a2·z⁻²)其中a0通常不等于1除非特别规范。如果用差分方程直接实现y[n] (b0·x[n] b1·x[n-1] b2·x[n-2] - a1·y[n-1] - a2·y[n-2]) / a0别忘了除以 a0。这是我在帮别人审查代码时发现频率最高的问题MATLAB里算好的滤波器拿到C里一跑幅度直接偏掉就是因为 a0 没处理。一定要把除法先做完即令 B0b0/a0, B1b1/a0, B2b2/a0, A1a1/a0, A2a2/a0后面C代码里就不需要除法了。3.3 高阶滤波器直接展开成大多项式数值上死得快如果阶数 n4butter返回的 b 和 a 各是5个系数的数组对应4阶传递函数。理论上可以直接把它写成一个5阶差分方程。但实际中这种“直接型结构”对系数误差极其敏感因为高次多项式的根对系数的微小扰动变化很大嵌入式浮点环境下误差会被放大。我建议高阶滤波器一律转换成多个二阶节级联SOS。MATLAB里sos tf2sos(b, a);sos是一个 (n/2)×6 的矩阵每一行表示一个二阶节[b0 b1 b2 a0 a1 a2]。级联结构相当于把一个大过滤器拆成几个小过滤器串联每个小过滤器的数值稳定性都远好于整体直接展开。实际经验4阶以上如果坚持用直接形式在16位定点MCU上几乎必出问题级联Biquad则稳定得多。4. 手动把系数翻译成C代码先浮点再定点4.1 浮点版Biquad级联实现拿到sos矩阵后写C代码就顺理成章了。每个二阶节的结构体存5个系数和4个状态变量typedef struct { float b0, b1, b2; float a1, a2; float x1, x2; // 输入历史 float y1, y2; // 输出历史 } biquad_t; float biquad_process(biquad_t *f, float x) { float y; // 注意这里的系数已经是除以a0之后的结果a0约定为1 y f-b0 * x f-b1 * f-x1 f-b2 * f-x2 - f-a1 * f-y1 - f-a2 * f-y2; // 状态更新注意顺序用新值覆盖旧值 f-x2 f-x1; f-x1 x; f-y2 f-y1; f-y1 y; return y; }然后用两个结构体变量串联biquad_t stage1, stage2; float filter_apply(float x) { float v biquad_process(stage1, x); v biquad_process(stage2, v); return v; }这里有一个细节容易忽略状态更新必须在输出计算完之后执行顺序不能反。因为 y[n] 依赖的是 x[n-1]、x[n-2]、y[n-1]、y[n-2]如果先更新状态再用新状态计算等于用错了数据点。4.2 状态初始化和C数据类型的选择状态变量在首次运行前必须清零否则初始结果就是垃圾void biquad_init(biquad_t *f) { f-x1 f-x2 0.0f; f-y1 f-y2 0.0f; }数据类型方面如果MCU支持硬件浮点比如Cortex-M4F以上直接float就够了计算速度也能接受。如果是不带FPU的低端MCUfloat运算会调用软件浮点库速度慢很多这时候就需要考虑定点实现。所谓“C代码化”到了具体硬件上本质上就是浮点和定点两种路线的权衡。double我一般不用。MCU上double运算比float慢得多而且滤波器系数的精度需求用float足矣。只有在PC端做基准测试时才可能用double。4.3 定点化把系数从double变成整数如果MCU没有FPU最稳妥的做法是把系数定标成Q格式整数。思路是浮点系数乘以一个缩放因子变成整数运算结果再除以缩放因子。我以Q14为例示范每个系数乘 2^14 16384 后四舍五入取整。但注意系数范围必须在[-2, 2)内才能用Q14否则会溢出。巴特沃斯低通的系数通常都在这个范围内可以先用浮点脚本检查一下最大值。#define Q 14 #define SCALE (1 Q) // 假设这些是从MATLAB导出后四舍五入得到的整数系数 static const int b0 (int)(B0 * SCALE); static const int b1 (int)(B1 * SCALE); static const int b2 (int)(B2 * SCALE); static const int a1 (int)(A1 * SCALE); static const int a2 (int)(A2 * SCALE); int biquad_process_fixed(int x) { long long acc; acc (long long)b0 * x (long long)b1 * x1 (long long)b2 * x2 - (long long)a1 * y1 - (long long)a2 * y2; // 输出需要除以SCALE同时保持截位一致 int y (int)(acc Q); x2 x1; x1 x; y2 y1; y1 y; return y; }这里几个关键点乘积累加到long long32位MCU上至少64位累加器防止中间结果溢出。输入x也需按同样的Q格式表示比如ADC采集的12位值先左移2位变成Q14格式最终输出再右移回去。定点化的舍入误差会随级联级数累积所以级联顺序和缩放分配要仔细。一般MATLAB的tf2sos已经按增益优化了顺序直接用即可。定点和浮点输出的差异简单测试下来误差通常低于0.1%FS对大多数传感器应用完全够用。如果要求更高精度可以提升到Q30中间用64位累加系数乘以130。5. 绕不开的最后一步让MATLAB和C代码对同一份数据说话5.1 构造一份能看出效果的测试激励代码写完不能不验证就上板。最稳妥的验证方法不是直接看实时波形而是先在PC上让MATLAB和C代码跑同一份输入数据然后对比输出。我先在MATLAB里构造一段测试信号3Hz的低频有效信号 50Hz工频干扰 300Hz高频噪声。这个组合很贴近真实传感器场景t (0:1999) / fs; % 2秒 x sin(2*pi*3*t) % 有效信号 0.5*sin(2*pi*50*t) % 工频干扰 0.3*sin(2*pi*300*t); % 高频噪声把这段x存成文本文件或头文件数组C程序读入同样的数组调用滤波函数得到输出再用MATLAB的filter直接对同一x滤波得到参考输出。两者对比误差一目了然。如果不想让C程序依赖文件读取也可以直接在C里生成同样的正弦信号注意正弦计算精度我测试时通常直接用预生成数组避免引入额外误差。5.2 对比输出时域波形和误差统计MATLAB端参考输出y_ref filter(b, a, x); % 或者用 sos 一节节 filterC端输出我存成一个文本文件用MATLAB读进来y_c load(output.txt); % 假设是逐行输出 e y_ref(:) - y_c(:); rms_err sqrt(mean(e.^2)); peak_err max(abs(e));正常情况峰值误差应该在10⁻⁵量级浮点版定点版在10⁻²~10⁻³量级取决于Q格式位数。如果误差明显偏大重点检查三件事系数有没有抄错、a0归一化有没有漏、状态更新顺序是否正确。另外一个有效的验证方法是算频域一致性把C输出和MATLAB参考输出的频谱画在同一张图里看主要频率成分是否完全对齐。时域波形对相位敏感频域则能直观看到带外衰减效果。5.3 实际调试中我遇到的三个经典错误第一个错误是系数顺序反了。MATLABsos矩阵的行顺序是有讲究的tf2sos默认按极点排序返回一般第一个二阶节对应离单位圆较近的极点。如果打乱顺序虽然整体响应理论不变但中间级的信号动态范围会变差定点环境下可能导致溢出。所以C代码里按sos原始顺序串联是最省心的。第二个错误是状态变量没有清零。有些平台的内存上电初值不一定是0Biquad结构体如果放在未初始化段里首次滤波会有一段不可预测的瞬态。我一开始调试时输出前几十个点是乱的排查了半天根源就是忘了初始化。建议在系统启动时就显式调用biquad_init。第三个错误是定点实现中移位方向搞反。Q14格式下输出是acc 14如果写成输出瞬间放大16000倍波形直接溢出。这个问题也是我踩过一次才真正长记性定点代码每写一处移位都要在旁边注释清楚Q值和缩放关系。5.4 把PC验证过的代码搬上MCU还要注意什么PC上验证通过只是万里长征走完前半程。移植到MCU时我一般再处理三件事采样间隔稳定性滤波器系数设计时是固定采样率的MCU的ADC采集中断如果抖动大实际截止频率会漂移。保证定时器触发精度比纠结系数本身更重要。内存对齐如果用了DMA搬运ADC数据滤波函数处理的数据buffer要注意对齐否则部分内核上会触发硬件异常。执行时间多个Biquad级联的计算量非常小但如果是中断里实时处理还是建议用IO引脚翻转或者定时器计数测一下最坏执行时间避免高优先级中断把其他任务饿死。之前我看到过一个项目用了8阶巴特沃斯定点实现中断频率1kHzMCU主频只有几十MHz最终处理时间占掉了中断周期的一半以上导致其他实时任务无法运行。后来换成4阶并调整阻带指标性能就从容多了。这说明设计指标和硬件资源必须放在一起权衡。写在最后的实操建议整个流程走下来我最深的体会是MATLAB算系数只占了一小部分工作量系数从浮点到C代码的落地才是真正决定成败的地方。很多人停留在“仿真通过就完事”的阶段结果一到嵌入式环境就出现波形畸变、噪声放大甚至系统不稳定回头排查往往就是a0归一化、状态更新顺序、定点格式这三个细节中的某一个出了问题。我自己现在的工作习惯是先写一个小的测试脚本把MATLAB滤波结果和C滤波结果做全量对比再往工程里集成。哪怕只是给MCU换了一个型号只要影响浮点行为我都重新跑一遍对比不省这一步。另外滤波器设计参数采样率、截止频率、阶数、Q格式在工程代码里一定要用宏定义集中管理方便后续调参。最后就是上面提到的定点版移位操作旁边多写注释几个星期后回看代码你会感谢当时的自己。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑