1. 一阶数字高通滤波器基础概念第一次接触数字滤波器时我也被那些复杂的公式搞得头晕眼花。后来在实际项目中才发现一阶数字高通滤波器其实就像是我们生活中的筛子——只不过它筛的是信号中的低频成分。想象一下你在淘金高通滤波器就是那个能让细小的金沙通过却把大石块挡在外面的筛网。在嵌入式系统中我们经常需要处理传感器采集的信号。比如用加速度计测量振动时往往会有低频的基线漂移干扰。这时候一阶高通滤波器就能大显身手它能保留我们关心的振动信号高频同时滤除那些讨厌的漂移低频。我曾在无人机飞控项目中用它处理陀螺仪数据效果相当不错。一阶高通滤波器的核心参数是截止频率(fc)。这个频率点决定了什么样的信号能通过。低于fc的成分会被衰减高于fc的则能顺利通过。在实际应用中我们需要根据信号特性来选择合适的fc。比如要滤除50Hz工频干扰就可以把fc设在稍高于50Hz的位置。2. 从模拟到数字的转换方法2.1 双线性变换原理要把模拟滤波器转换为数字滤波器双线性变换是最常用的方法之一。我第一次学习这个变换时总觉得它像变魔术一样神奇。简单来说它通过一种巧妙的数学映射把连续的s域模拟域转换到离散的z域数字域。具体实现时我们会用tan()函数来完成这个转换。这里有个小技巧为了避免频率畸变需要进行预畸变校正。我在STM32项目中发现如果忽略这一步实际截止频率会和设计值有显著偏差。下面这个公式就是关键halfdigiW tan(π * fc * Ts)其中fc是截止频率Ts是采样周期。记得当时调试时我因为忘记把角度转换为弧度制结果滤波器行为完全不对排查了好久才发现这个低级错误。2.2 Q15定点数运算技巧在资源有限的MCU上浮点运算往往代价高昂。这时Q格式定点数就派上用场了。Q15表示用1位符号位和15位小数位范围在[-1,1)之间。我习惯把系数放大32768倍即左移15位转换为Q15格式计算完成后再右移还原。但要注意溢出问题有一次我的滤波器输出莫名其妙地振荡最后发现是中间结果超过了16位范围。解决方法是在关键计算步骤加入饱和处理#define Q15M(a,b) (((int32_t)a*b)15)这个宏定义实现了Q15乘法并保证了正确的舍入。在Cortex-M0这类没有硬件除法器的芯片上定点数运算能显著提升性能。实测下来Q15实现比浮点版本快3-5倍非常适合实时性要求高的应用。3. 嵌入式C语言实现详解3.1 滤波器结构体设计好的代码结构能让后续维护轻松很多。我习惯把滤波器设计成自包含的对象这样在同一个系统中可以方便地创建多个实例。下面是我常用的结构体设计struct MHpf1W_F { /* 初始化接口 */ struct { void (*Set)(struct MHpf1W_F* self, float cutFreq, int samFreq); void (*VaryCutFreq)(struct MHpf1W_F* self, float cutFreq); float cutFreq; // 截止频率 float samFreq; // 采样频率 } Init; /* 运行时接口 */ struct { int (*In)(struct MHpf1W_F* self, int Xn); int out_y; // 输出值 } Prd; /* 私有变量 */ struct { float Ts; // 采样周期 int a0, b0, b1; // 差分系数(Q15) int Xn_1, Yn_1; // 历史数据 } pri; };这种设计把接口和实现分离外部只需关心Init和Prd两个接口模块。我在四轴飞行器项目中就用这种方式管理了6个并行的滤波器实例代码依然保持清晰。3.2 核心算法实现滤波器的核心就是那个差分方程Y(n) b0X(n) b1X(n-1) a0*Y(n-1)。看起来简单但实现时有不少坑要注意。比如初始状态处理不当会导致输出瞬态冲击我通常会把历史值Xn_1和Yn_1初始化为0。动态调整截止频率是个很实用的功能。当信号特性变化时我们可以实时更新滤波器参数static void _Update(struct MHpf1W_F* self) { float halfdigiW PI * self-Init.cutFreq * self-pri.Ts; float tgAnaWT tan(halfdigiW); self-pri.b0 (1/(tgAnaWT1)) * 32768; // Q15 self-pri.b1 -self-pri.b0; self-pri.a0 ((1-tgAnaWT)/(tgAnaWT1)) * 32768; }注意这里用32768进行Q15转换时要确保系数范围在[-1,1)之间。我在一次项目中因为截止频率设得过高导致系数溢出滤波器完全失效。后来加了参数范围检查才解决self-Init.cutFreq MID(cutFreq, 0.0f, samFreq*0.5f);4. 实际应用与波形验证4.1 Python仿真对比在真正烧写到MCU前我强烈建议先用Python做个快速验证。用Matplotlib可以直观看到滤波效果import numpy as np import matplotlib.pyplot as plt Wc 2*np.pi*30 # 截止频率30Hz Tsw 0.00314 # 采样时间 halfdigiW np.tan(Wc/2*Tsw) # 计算系数 b10 1/(halfdigiW1) b11 -b10 a10 (1-halfdigiW)/(halfdigiW1) # 生成测试信号 x np.linspace(-np.pi, np.pi, 2000) data np.sin(2*np.pi*x) 0.5*np.sin(30*2*np.pi*x) # 1Hz 30Hz # 滤波处理 y1 np.zeros_like(x) for i in range(1, len(x)): y1[i] b10*data[i] b11*data[i-1] a10*y1[i-1]通过调整Wc值可以观察到不同截止频率下的滤波效果。当fc1Hz时30Hz信号几乎无衰减fc30Hz时1Hz信号被明显抑制。这种可视化验证能帮我们快速确认算法正确性。4.2 匿名科创地面站实测把代码移植到STM32后我用匿名科创地面站观察实际波形。设置DAC输出混合信号1Hz30Hz通过滤波器后用ADC采集结果。这里有个小技巧合理设置采样率。根据奈奎斯特定理采样频率至少要是信号最高频率的2倍。我通常取5-10倍以保证质量。实测数据与Python仿真高度一致验证了算法的正确性。当动态调整截止频率时能清晰看到滤波效果的实时变化。这个功能在需要自适应滤波的场景特别有用比如我在电机控制系统中就用它来抑制不同转速下的特定振动频率。5. 性能优化与常见问题5.1 资源占用分析在STM32F10372MHz上实测单个滤波器实例约占用RAM20字节结构体Flash约200字节代码计算时间5μsQ15实现对于M0内核的芯片可以考虑以下优化用查表法替代tan()计算将系数改为Q7或Q8格式降低计算量使用汇编优化关键循环5.2 典型问题排查遇到过最棘手的问题是滤波器自激振荡。现象是输入为0时输出却不断增大。原因可能有系数计算错误导致极点超出单位圆定点数运算溢出采样频率设置不当解决方法包括检查tan()函数输入范围添加系数范围限制用Python先验证系数正确性逐步打印中间变量值另一个常见问题是相位失真。高通滤波器会引入非线性相位这在某些需要保持波形形状的应用中可能有问题。如果对相位要求严格可以考虑使用零相位滤波技术或者改用FIR滤波器。