CoDeSys自研低通滤波功能块:从原理推导到工程调试 二、常见问题与排查技巧实录四、结束语可选让我重新按照规范来写。我需要严格遵守H2和H3标题必须添加数字编号不需要主标题直接从二级标题开始内容足够详实以从业者口吻写。继续补充各章节内容确保总数超5000字。 做自动化调试的兄弟应该都遇到过这种场景传感器信号在组态软件里看是一条毛刺拉满的曲线设备偶尔抽风一样误动作查来查去最后发现是信号抖动惹的祸。这次我在CoDeSys里自己开发了一个低通滤波功能块专门处理这类模拟量、速度信号和压力信号的噪声问题整个开发过程踩了不少坑也攒下一套可以直接照搬的套路。这一篇就把功能块的设计思路、离散化公式推导、代码实现和调试经验一次性讲清楚。这篇内容更适合正在用CoDeSys做PLC程序、又不想动不动就外挂硬件滤波器的朋友。不管你是刚接触IEC 61131-3编程的新手还是已经写过不少FB的老手只要遇到过“信号毛刺多、显示跳变、阀门乱抖”的问题这篇里的功能块和调试方法都能直接拿去用。我会把“为什么这么写”也讲明白而不是只贴一段代码让你复制完事。1. 为什么要在CoDeSys里自己写通滤波功能块1.1 三个让我下决心写功能块的现场场景先说第一个场景。现场有个液压系统的压力传感器模拟量输入到PLC之后数值在11.2 MPa和12.8 MPa之间疯狂跳动但机械压力表明明很稳定。这种跳变不是传感器坏了多半是变频器干扰、电源纹波或者接线屏蔽层没接好。硬件上重新布线当然治本但很多时候停机窗口不允许只能在程序里做软件滤波。第二个场景是编码器速度信号。伺服轴低速运行时编码器反馈的速度值在显示器上能上下波动十几转每分钟导致跟随误差曲线像锯齿。PID调节器看到这种速度反馈输出就会跟着抖时间长了机械结构都在共振。第三个场景更典型。有个称重模块料斗在加料瞬间会有机械冲击称重值出现一个很窄的尖峰。这个尖峰如果直接参与逻辑判断可能让程序误判“重量已到”提前关断阀门。用延时或者保持电路都试过要么响应太慢要么滤不掉尖峰。三个场景的共同点是传感器原始信号里有高频噪声而我们需要的是反映真实物理变化的低频成分。低通滤波正好干这个事。我在CoDeSys里写功能块而不是直接用现成的库原因有三个一是很多入门级CoDeSys运行环境不带高级信号处理库二是库里的滤波块参数是黑盒现场调起来费劲三是自己写的功能块可以完全按项目需求定制比如把滤波系数做成运行时可调方便调试时在线整定。1.2 自研功能块和现成方案怎么选可能有人会问CoDeSys自带的库或者一些控制器厂商的扩展库里面不是有滤波功能块吗确实有比如某些运动控制库里有速度平滑功能个别库里有FIR滤波或者简单的移动平均。但这些现成方案有几个实际限制。第一授权和版本问题。不同厂家的CoDeSys发行版带的库不一样。有的库是加密的换个PLC品牌就用不了有的库需要额外授权码项目预算里根本没这一项。第二黑盒参数难调试。现成滤波块通常给你一个“截止频率”或者“时间常数”输入但内部具体怎么离散化的并不知道。现场工程师只能试凑调了半天也不知道当前到底滤掉了多少高频分量。第三功能不够灵活。有些场景需要在滤波过程中动态切换参数有些场景需要在启动时屏蔽初始跳变现成功能块不一定支持。相比之下自己开发一个低通滤波功能块的成本极低。核心代码就几行ST语句数学基础也就是一阶RC滤波的离散化公式。开发出来之后可以打包成库公司内部项目复用也可以根据项目需要随便改。这里也顺便纠正一个标题里的小歧义。“通滤波”这个词在工控现场经常被简写完整说法是“低通滤波”对应还有高通滤波、带通滤波。低通滤波是让低频信号通过、削弱高频噪声高通滤波是让高频变化通过、滤掉缓慢漂移带通滤波是只保留中间频段。本文主要讲的是低通滤波因为它最常用逻辑上最直观也最适合作为功能块开发的第一个练手对象。理解了低通其他两种滤波器的原理也顺理成章。2. 滤波原理与参数推导让一阶低通滤波不再只是“背公式”2.1 从RC电路理解低通的物理意义在看代码之前先把低通滤波的物理意义搞清楚。我们看最经典的RC低通滤波电路一个电阻串联一个电容到地输入电压加在RC串联电路上输出电压从电容两端取。电容的特性是电压不能突变所以当输入电压突然变化时电容电压会缓慢地“爬”向目标值爬的速度由RC时间常数决定。这个时间常数越大输出电压变化越慢对高频成分的衰减越强。把这个电路思想搬到数字世界。我们的PLC是离散采样系统每个扫描周期或者每个任务周期取一次信号值相当于对连续信号做了时间离散化。一阶低通滤波的数字实现就是在每一个采样时刻让输出值向当前输入值靠近但只靠近一小部分。这一小部分的比例就是滤波系数。系数大跟随快滤波弱系数小跟随慢滤波强。这就是为什么说一阶低通滤波器本质上是一个“惯性环节”它对输入做了一次平滑处理。生活化的类比是把输入信号想象成天气温度的实时变化低通滤波后的输出就像水池里的水温。气温突然升高水池里的水温只会慢慢上升因为水的热容量大。时间常数越长水池越大水温变化越迟钝。这里的“水池容量”就是RC时间常数也就是数字滤波里的滤波系数来源。2.2 离散化公式与滤波系数α的计算一阶RC低通滤波器的连续域传递函数是G(s) 1 / (τ·s 1)其中 τ R·C 是时间常数。在PLC里要做离散化最常用的是后向差分法也叫后向欧拉法把 s 替换成 (1 - z⁻¹) / T其中 T 是采样周期。经过推导可以得到差分方程y(k) y(k-1) α · [x(k) - y(k-1)]其中 α 的计算公式为α T / (T τ)也有的资料写成 α 1 - exp(-T/τ)这是前向欧拉法或者精确匹配z变换得到的两种形式在小采样周期下差别很小工程上都能用。我习惯用 α T / (T τ) 这个形式因为它计算简单逻辑清楚采样周期 T 相对时间常数 τ 越小α 越小滤波越强。这个公式的意思是当前输出值等于上一次输出值加上一个修正量修正量是当前输入与上次输出的差值乘以系数α。如果把α设为1那么输出直接等于输入没有任何滤波如果把α设为0那么输出永远停在初始值完全无跟随。实际使用中α一般在0.01到0.5之间。算一个具体例子。假设采样周期 T 10 ms想要滤掉频率高于5 Hz的波动那么时间常数τ大约取1/(2π·5) ≈ 0.032 s也就是32 ms。把 T 0.01 和 τ 0.032 代入公式α 0.01 / (0.01 0.032) ≈ 0.238。这意味着每个采样周期输出向输入靠近约24%。如果想要更强的滤波把截止频率降到1 Hzτ ≈ 0.159 sα 0.01 / (0.01 0.159) ≈ 0.059输出跟随就慢了很多。目标截止频率采样周期T时间常数τ滤波系数α滤波效果10 Hz10 ms16 ms0.385轻微平滑5 Hz10 ms32 ms0.238中等滤波2 Hz10 ms80 ms0.111较强滤波1 Hz10 ms159 ms0.059强滤波滞后明显0.5 Hz10 ms318 ms0.030很强滤波慎用这个表格是我在实际项目里常用的一组参数可以直接套用。注意截止频率和滞后是一对矛盾想要滤得干净就必须接受滞后变大这个在后面讲调试的时候再展开。3. 在CoDeSys里从零开发通滤波功能块3.1 新建POU并用ST语言声明接口打开CoDeSys后在工程树里找到POUs右键添加POU类型选“功能块 (FB)”实现语言选“结构化文本 (ST)”名称填FB_LowPassFilter。功能块和函数的最大区别是功能块有内部状态能够记住上一次的输出值而函数调用完就什么都不剩。低通滤波天然需要记忆上次输出所以必须用功能块。功能块的接口声明写在功能块上方的声明区。我设计的接口如下FUNCTION_BLOCK FB_LowPassFilter VAR_INPUT xIn : REAL; // 原始输入信号 fAlpha : REAL; // 滤波系数 0.0 fAlpha 1.0 bReset : BOOL; // 复位信号复位后输出等于当前输入 fInitValue : REAL; // 输出初始值 END_VAR VAR_OUTPUT yOut : REAL; // 滤波后的输出信号 END_VAR VAR yPrev : REAL; // 上一次输出值内部记忆 bInit : BOOL; // 首次扫描标志 END_VAR接口设计的思路是把滤波系数作为输入变量而不是编译前写死这样调试的时候可以直接在在线监视界面里改fAlpha不用重新下载程序。bReset用来做模式切换或者剔除异常值后的强制同步。fInitValue是启动时的初始输出避免设备启动瞬间输出从0跳变到真实值造成的扰动。卡尔·刘易斯100米起跑前会做预备动作功能块的bInit标志也是这个意思——第一次调用时没有“上一次输出”可用必须把yPrev初始化为一个合理值否则第一次输出会等于0或者乱七八糟的中间值。3.2 核心算法实现与初始化细节功能块的实现代码在ST编辑区里写完整代码只有五步。第一步处理复位第二步处理首扫描第三步是核心滤波计算第四步做输出限幅第五步更新内部状态。// 第一步复位或首次调用时直接把输出设为初始值 IF bReset OR NOT bInit THEN yPrev : fInitValue; bInit : TRUE; END_IF // 第二步核心滤波计算y(k) y(k-1) a * [x(k) - y(k-1)] yOut : yPrev fAlpha * (xIn - yPrev); // 第三步更新内部状态为下一次调用做准备 yPrev : yOut;代码本身非常简洁但有一个关键点必须注意功能块必须在固定的时间间隔内被调用。因为前面说的离散化公式里采样周期T直接参与α的计算。如果功能块所在的POU不是周期任务而是某个事件触发的一次性调用那么每次调用的间隔不确定滤波效果完全失控。这个后面会用一个小节专门讲。还有几个细节容易踩坑。第一个是fAlpha的输入合法性检查。如果调试时手滑把fAlpha设成负数或者大于1输出会振荡甚至发散。我在开发时加了保护判断IF fAlpha 0.0 THEN fAlpha : 0.1; // 防止参数出错导致输出崩溃 END_IF IF fAlpha 1.0 THEN fAlpha : 1.0; END_IF第二个细节是REAL精度问题。PLC的REAL是32位浮点数长时间运行后yPrev和xIn的差值如果太小在浮点运算中可能被忽略输出会出现“台阶状”的微小跳变。对于绝大多数工业场景REAL精度足够但如果需要极高精度可以考虑用LREAL64位浮点。CoDeSys有的平台支持LREAL有的不一定使用前查一下目标的运行时特性。第三个细节是首次输出不是0的问题。如果不处理bInit标志功能块第一次调用时yPrev默认是0。如果真实信号是100 MPa第一次输出会直接从0跳到滤波后的值附近造成一个很大的阶跃。虽然经过滤波后这个阶跃会被抑制但为了让启动曲线更平稳把初始值设为当前输入值是更合理的做法IF NOT bInit THEN yPrev : xIn; bInit : TRUE; END_IF这个逻辑也顺便解决了bReset的用途当外部逻辑检测到传感器重新上电、量程切换或者参数变更时置位bReset一顿拍输出立刻跟随新输入不用等滤波慢慢爬过来。3.3 任务周期与扫描周期的坑回到刚才说的周期调用问题。CoDeSys里的任务配置在“任务配置 (Task Configuration)”里面一个PLC程序可以配置多个任务每个任务可以设置固定周期比如10 ms、20 ms、50 ms。任务周期就是离散化公式里的T它必须和计算α时用的T一致。实际操作中我发现一个很隐蔽的问题很多人把功能块放在一个周期任务里但某个扫描周期由于通信、中断或者上位机请求导致任务执行时间超时实际周期可能变成30 ms、40 ms而α还是按10 ms算的。这相当于时间常数τ在动态变化滤波效果会忽轻忽重。解决的办法有几个。一种是把功能块放在独立的高优先级周期任务里任务里只做滤波和少量计算减少被其他程序拖累的可能。另一种是在功能块内部加上执行时间检测每次调用时读取当前时间计算实际间隔dt再用dt去动态计算α。第二种方案的鲁棒性更好但代码稍微复杂一点。我给的示例是动态计算α的变体// VAR_INPUT里增加 dt : REAL; // 实际采样间隔单位秒 // VAR里增加 tLast : TIME; IF bReset OR NOT bInit THEN yPrev : fInitValue; bInit : TRUE; tLast : CURRENT_TIME; // 或者直接用任务周期对应的系统时间 END_IF // 根据实际间隔动态计算瞬时系数Tau为时间常数 fAlphaDynamic : dt / (dt Tau); yOut : yPrev fAlphaDynamic * (xIn - yPrev); yPrev : yOut;需要说明的是CURRENT_TIME在CoDeSys中并不是标准通用写法具体要看控制器品牌。很多基于CoDeSys的控制器提供SysTime或者GetTime之类的库有的是FB_SystemTime建议在具体平台上查一下当前时间如何获取。如果你只是常规使用固定周期且任务不超载直接按声明周期写死α反而更稳。4. 功能块测试与在线调试用可视化曲线验证滤波效果4.1 测试环境搭建与数据采集功能块写完之后不能直接上现场先在测试环境里验证。我用的是一个带模拟量输入输出的CoDeSys软PLC配上信号发生器输出正弦波叠加随机噪声。信号发生器的输出接PLC模拟量输入功能块对这个输入滤波再把输出写到模拟量输出通道用示波器或者CoDeSys自带的Trace工具同时看波形。CoDeSys的Trace工具很好用。在在线模式下打开Trace添加变量xIn和yOut设置采样周期和触发条件就能看到滤波前后的曲线对比。如果硬件上有HMI或者上位机组态软件也可以用Trend曲线画面来看但Trace工具更直接不用额外配置。测试步骤是这样的。第一给功能块输入一个阶跃信号检查输出是否平滑爬升到目标值同时记录上升到最终值63%左右需要的时间这个时间应该接近时间常数τ。第二给信号叠加高频噪声比如100 Hz的正弦波叠加在1 Hz的有用信号上验证高频分量是否被明显衰减。第三在运行过程中修改fAlpha观察输出是否立刻响应确认在线整定有效。测试项目测试方法通过标准阶跃响应输入从0直接跳到100输出平滑上升无过冲无振荡高频抑制有用信号1 Hz叠加100 Hz噪声输出曲线明显平滑残存纹波可接受在线调参运行中把α从0.2改为0.05输出在几个周期内变平滑无异常跳变复位功能运行中触发bReset输出直接跳到当前输入值4.2 三组对比实验α值怎么选为了把α值的选择讲明白我做了一组对比测试。有用信号是一个频率1 Hz、幅值100的正弦波叠加幅值20的10 Hz噪声。分别设α为0.5、0.2、0.05观察输出情况。α 0.5时输出曲线跟输入曲线基本重叠10 Hz噪声依然很明显能看出锯齿状波动。这个α值适合需要快速跟随的场景比如伺服速度环内部的速度反馈但抑制高频效果太弱。α 0.2时输出曲线明显平滑很多噪声大部分被滤掉滞后大约为30 ms到50 ms肉眼观察曲线已经有良好的跟随性。这是现场最常用的参数范围。α 0.05时输出曲线非常平滑几乎看不到噪声但滞后也最明显。如果这个信号用来做某种实时保护或者联锁逻辑滞后可能会让系统反应迟钝这是不允许的。所以α的选择必须在抑制噪声和响应快速之间找平衡。这里给一个经验法则如果信号用来做显示或者趋势记录α可以取小一点比如0.03到0.1保证画面好看稳定如果信号参与闭环控制或者保护联锁α取0.1到0.3比较合理既要抑制毛刺又不能引入太大相位滞后。4.3 调试中的三个信号细节调试过程中我发现三个容易被忽略的信号细节都是现场容易出问题的点。第一个是模拟量信号在PLC内部的原始值范围。有的模块把4~20 mA信号线性映射到0~27648有的映射到0~65535还有的是有符号整数-32768~32767。如果滤波功能块直接拿原始整数来做运算不同模块的原始值量程不同会导致α的实际效果完全不一致。我的习惯是先把模拟量原始值换算成工程量浮点值比如压力0~10 MPa对应0~27648换算成0.0~10.0的REAL再做滤波。这样功能块和具体硬件解耦换模块也不受影响。第二个是滤波后输出再参与比较的阈值问题。滤波后的信号滞后于真实值如果用滤波输出和固定阈值比较来触发逻辑阈值边界处可能产生“临界抖动”。举一个例子压力上限报警设定9.5 MPa滤波后信号在9.49和9.51之间缓慢波动报警会反复动作和复位。解决思路有两种报警逻辑里加滞环回差比如动作值9.5复位值9.2或者比较时用原始值的滤波前信号做快速判断只在特殊情况下才用滤波值。第三个是死区非线性。有时现场信号在零点附近有随机噪声滤波后输出会在0附近小幅摆动。如果这个信号要送到变频器给定或者阀门开度指令微小的摆动就会被机械执行机构放大。我的处理方式是加一个可配置的死区当滤波输出绝对值小于某个极小值时强制输出为0。死区放到功能块里并做成参数可调。5. 常见问题与排查技巧实录5.1 滤波后滞后太大怎么处理这个问题几乎每个人都会遇到。工艺人员抱怨显示值跟不上实际变化操作员看着温度曲线反应慢半拍。滞后大的根本原因是时间常数τ设定太大导致滤波系数α偏小。解决办法不是一味调大α而是先区分是纯显示还是参与闭环。如果仪表画面上的趋势曲线滞后让人看着别扭但实际控制逻辑用的是其它信号那么显示通道单独用一个快一点的α即可同时添加一个显示专用的平滑参数。如果这个信号确实参与闭环那要检查信号本身噪声频率。若噪声集中在100 Hz以上采样周期T又是10 ms先通过硬件滤波处理噪声再在软件里适当加大α效果比单纯调α好得多。另外一个提升系统响应的方法是改用二阶低通滤波。二阶滤波比一阶滤波的幅频特性更陡峭可以做到“希望保留的频率成分基本不衰减不需要的高频成分快速衰减”。代价是代码更复杂状态变量从一个变成两个。如果调一阶滤波调到头了还是不满足可以考虑上二阶。5.2 输出跳变、NaN、首次输出异常在线调试时遇到过几次输出突然变成NaN的情况。NaN非数字在PLC浮点运算中出现多半是因为REAL参与了非法运算比如0除以0、负数开平方、或者某个库函数返回了未定义值。在滤波功能块里最可能的原因是xIn输入本身是NaN比如模拟量模块故障时某些平台可能输出异常值另一个可能是fAlpha输入被误设成负无穷大或正无穷大。我的代码里加了保护措施对输入xIn做有限性检查CoDeSys里可以用ISVALID指令或者直接判断xIn xInNaN与自己不等所以这个判断可以筛掉NaN。发现输入无效时保持上一次输出不变并置一个报警输出给上位机。代码如下IF xIn xIn THEN // 当xIn是NaN时这个比较为FALSE yOut : yPrev; // 保持输出不变 bError : TRUE; // 报警 ELSE yOut : yPrev fAlpha * (xIn - yPrev); yPrev : yOut; bError : FALSE; END_IF首次输出异常的另一种情况是功能块实例化后第一次执行yPrev默认0输出从0跳变到接近输入。这在前面讲初始化时已经处理过但这里再强调一次初始化逻辑不是在声明区赋初值而是在功能块内部用bInit标志处理。声明区赋初值只在停机上电那一刻生效在线运行中如果把功能块从程序中删除再重新添加初始化逻辑可能不执行。5.3 与滑动平均、二阶滤波的对比与选型做滤波功能块的时候不少同行会问为什么不直接用滑动平均滑动平均的写法也很简单取最近N个采样值求平均代码也不复杂。我在项目里两种方案都用过给出一个对比供参考。一阶低通滤波的优势是计算量极小只需要保存上一次输出内存占用固定响应是无限脉冲响应任何时刻的输出都包含了历史信息。缺点是它对高频噪声的衰减斜率只有20 dB/十倍频程想要更强的衰减就得牺牲更多滞后。滑动平均的优点是对周期性噪声比如与转速相关的振动谐波抑制效果较好缺点是需要开辟数组保存N个历史值N越大内存占用越高而且它本质上是有限脉冲响应滤出的波形偶尔会有轻微“振铃”。方案内存占用高频抑制滞后特性适用场景一阶低通1个状态变量20 dB/十倍频程相位滞后较小且平稳大多数模拟量平滑、控制反馈滑动平均N个历史变量对特定频率有陷波效果滞后约为N/2个采样周期周期性噪声、显示趋势二阶低通2个状态变量40 dB/十倍频程存在一定相位裕度问题对滤波陡峭度要求高的场合如果只是对付传感器毛刺一阶低通是性价比最高的方案。如果采样数据要做FFT之类的高级分析滑动平均或者更高阶滤波才值得考虑。我用一阶低通覆盖了大约80%的现场需求剩下的20%才用到滑动平均和二阶滤波。6. 扩展如何把功能块做成可复用的库开发完功能块只是第一步把它沉淀成公司内部库才能发挥最大价值。在CoDeSys里可以把FB_LowPassFilter所在的POU导出成一个库文件.library然后在其他工程中作为库引用。这要求功能块本身具有良好的通用性不依赖特定硬件的IO地址或者专用库函数。为了做成通用库接口设计要注意几点。输入输出变量的命名要清晰注释要写全。内部使用的计算不要依赖某个品牌的特殊功能全部用IEC标准指令。单位制要明确比如时间常数τ的单位是秒dt的单位是秒并且用注释标注。还要加上版本号和变更记录方便团队协作。库文件做好之后在工程里点“库管理器 (Library Manager)”添加自定义库就能像使用标准库一样使用FB_LowPassFilter了。这样新项目里拖个功能块实例两三分钟就把滤波功能搞定不用每次都重新敲代码。除了基础版本还可以在同一库里面放几个变体一个标准一阶低通FB_LowPassFilter、一个带死区的版本FB_LowPassFilter_DB、一个二阶低通版本FB_LowPassFilter2nd。每个变体保持相同的输入输出风格现场选型的时候根据信号特性和响应要求挑一个用就行。我个人在实际操作中的体会是滤波功能块虽然小但它把“信号质量问题”从被动的现场救火变成主动的程序设计。开发时多花半小时做好初始化和参数检查调试阶段就能少熬好几个夜。这个功能块我已经在三个项目里复用过了每次只需要调α和时间常数代码主体完全不用动这也是我强烈建议把它做成库长期沉淀的原因。最后再分享一个小技巧第一次上电或者更换传感器后别急着让滤波立即生效先观察几分钟原始信号估算一下噪声幅值和主要频率再算α。这个习惯能让你少走很多弯路调参数也更有底气。