拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

CoDeSys自研低通滤波功能块:从原理推导到工程调试

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精度足够,但如果需要极高精度,可以考虑用LREAL(64位浮点)。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 = xIn(NaN与自己不等,所以这个判断可以筛掉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)。每个变体保持相同的输入输出风格,现场选型的时候根据信号特性和响应要求挑一个用就行。

我个人在实际操作中的体会是,滤波功能块虽然小,但它把“信号质量问题”从被动的现场救火变成主动的程序设计。开发时多花半小时做好初始化和参数检查,调试阶段就能少熬好几个夜。这个功能块我已经在三个项目里复用过了,每次只需要调α和时间常数,代码主体完全不用动,这也是我强烈建议把它做成库长期沉淀的原因。

最后再分享一个小技巧:第一次上电或者更换传感器后,别急着让滤波立即生效,先观察几分钟原始信号,估算一下噪声幅值和主要频率,再算α。这个习惯能让你少走很多弯路,调参数也更有底气。

返回列表