☰
CoDeSys实战:手写通用滤波功能块,根治PLC模拟量信号抖动
2026/10/1 4:29:49 网站建设 项目流程

开头先聊一个场景。有一次我在调试一台水处理设备的PLC,触摸屏上那块 pH 值显示总是像呼吸灯一样来回飘,业主站在身后问了一句“这表到底准不准”。变送器送回来信号是稳的,接地也没问题,可我打开趋势一看,原始模拟量里叠了一堆几赫兹到几十赫兹的干扰,全是旁边那台变频搅拌电机贡献的。从那天起我就意识到,现场信号处理这件事,光靠硬件端去扛是扛不完的,最后还是得在 CoDeSys 里自己动手写滤波功能块。

这一期是《CoDeSys入门实战一起学习》的第十五篇。标题里写了“通滤波功能块”,说白了就是“通用滤波功能块”——一个能根据现场噪声类型切换算法、可以重复调用、参数可调的滤波处理模块。这篇文章我会把从算法选型、ST语言代码实现,到仿真调试、参数整定、现场踩坑的整套过程讲透。适合三类人看:一是正被模拟量抖动折磨的 PLC 工程师,二是刚把 ST 语言跑通、想学功能块封装思路的 CoDeSys 新手,三是想把现场仪表数据“救回来”但不知道从哪下手的工控人。

1. 为什么 PLC 里的滤波功能块值得自己写——工业现场的痛点

1.1 模拟量信号抖动的三个主要来源

传感器信号在进入 PLC 的 AI 模块之前,要经过变送器、线缆、端子排、屏蔽层,这一路下来,真正进到 ADC 采样端口的信号早就不是纯净的物理量了。我在现场遇到的信号抖动,绝大多数来源可以分成三类。

第一类是电磁耦合干扰。变频器输出侧的 PWM 波形、接触器吸合瞬间的拉弧、电机的启停冲击,都会通过空间辐射和线缆传导的方式叠加到模拟量信号上。这类干扰频率通常不低,从几十赫兹到几百赫兹都有,体现到 PLC 里就是数值高频跳动,幅度可能达到量程的 1% 到 3%。

第二类是过程本身的波动。泵的柱塞往复、搅拌桨的叶片通过、管道里的气液两相流,都会让压力、流量信号产生周期性脉动。这类干扰和电磁干扰的区别在于,它是真实存在的物理波动,只是从控制系统角度看,这种波动对调节没有意义,反而会让执行机构频繁动作。

第三类是采样和量化引入的误差。AI 模块的分辨率、共模抑制比、线缆阻抗不匹配,都会让同一时刻的采样值产生小幅偏差。很多廉价变送器输出的并不是真正的 4-20mA 电流源,而是带负载能力的电压信号,线一长,压降和感应电动势就全来了。

这三类干扰混在一起,如果不对原始信号做处理,后面的 PID 运算、报警判断、物料累计全是建立在“假象”上的。我见过一个项目,因为液位信号毛刺太多,液位计的虚假波动触发了两次高位报警,产线直接停机。这就是滤波没做好的代价。

1.2 为什么硬件滤波和变送器阻尼解决不了问题

面对信号抖动,很多人的第一反应是调变送器上的阻尼时间。这个方法不是不行,但问题很多。变送器的阻尼档位通常是离散的,有的只有固定的几个档位,你找不到恰好合适的值。阻尼调大了信号是稳了,但真实的过程变化也被“抹平”了,工艺报警会迟滞好几秒,遇到真正需要快速响应的场合根本不敢用。

硬件滤波也一样。在 AI 输入端加 RC 低通滤波器,参数定了之后想改就得换元件、改接线,现场根本不现实。而且 RC 滤波器只能对付高频干扰,对泵脉动这种低频周期性干扰几乎无效。更麻烦的是,加了电容之后信号上升沿会变缓,遇到需要高速采样的场合反而弄巧成拙。

所以最后你会发现,最灵活的做法是在 PLC 程序里做数字滤波。改参数就是改一个变量,换算法就是切换一个功能块实例,完全不影响硬件。这就像手机拍照的“美颜”功能,你可以在软件层面调整平滑力度,而不是换一个镜头。

1.3 自己写滤波块,能多得到什么

CoDeSys 的库管理器里并不是没有滤波相关的库,OSCAT 基础库里也有滤波功能块,但直接用库有个问题:你不知道里面的算法细节,出了问题不好排查;而且 OSCAT 这类第三方库的授权和版本管理在一个正式项目里是额外负担。

自己使用 ST 语言写滤波功能块,无非是几行数学公式的事,换来的是完全的掌控力。我可以自己决定接口参数,把时间常数、采样周期、窗口长度做成输入参数,在触摸屏上就能调。我可以让多个通道复用同一个块,每个通道一个实例,参数互不干扰。我还可以把滤波结果和报警逻辑、PID 逻辑做成联动,内部共享“半成品数据”,避免多份代码各算各的。

另一个实际好处是项目移交的时候。把过滤波功能块的工程交给维护人员,他们打开代码就能看到一阶低通是怎么算的、窗口平均是取几个点,出了毛病知道从哪里下手查。这比面对一个黑盒模块要踏实得多。我在后面给出的三个功能块,参数命名和逻辑都按工业习惯来,抄过去稍微改改就能直接用。

2. 三种滤波算法怎么选:“通滤波”不是只有一阶低通

2.1 一阶低通:平滑趋势信号的“保温桶”

一阶低通是最经典也最好理解的数字滤波方式。它的迭代公式是:

y[k] = α * x[k] + (1 - α) * y[k-1]

其中 α = Δt / (τ + Δt),Δt 是采样周期,τ 是时间常数。这个公式的含义是:当前输出等于“最新采样值”和“上一次输出值”的加权平均。τ 越大,上一次输出的权重越大,输出越平滑;τ 越小,新鲜采样值占比越高,输出越贴近原始信号。

你可以把它想象成一个保温桶。往桶里倒热水,桶内温度不会瞬间升到热水温度,而是慢慢趋近。保温层越厚,升温越慢。PLC 里的一阶低通就是数字世界里的保温桶,它用过去一段时间内的历史信息来“消化”噪声,让输出的变化趋势保持平稳。

一阶低通适合处理温度、液位、pH 值这类变化缓慢的物理量。这类信号本身没有太多高频成分,加上干扰之后,一条本应平滑的曲线变得毛刺丛生,用一阶低通很容易就拉回正常形态。它的计算量极小,每个扫描周期只做一次乘法和一次加法,内存占用只有一个历史值,CPU 资源几乎可以忽略不计。

2.2 移动平均:对付周期脉动的“快门”

移动平均的思路更直白:取最近 N 个采样值求平均,把 N 作为滑动窗口。每来一个新采样,窗口整体前移一格,输出的就是窗口内所有样本的算术平均。如果噪声是一个固定周期的脉动,你把窗口长度设为噪声周期的整数倍,脉动就会被“抵消”掉。

移动平均的典型应用是压力、流量这类受泵脉动影响的信号。比如一台三缸柱塞泵,出口压力信号会以泵的转频为基频脉动,你在 PLC 里看到的值一上一下地跳。只要窗口覆盖一到两个完整脉动周期,平均出来的值就非常稳定。

但移动平均有一个代价:延迟。窗口长度 N 越大,输出对真实变化的跟随越慢,延迟大约在 (N-1) 倍的采样周期的一半。这个延迟在 PID 回路里是致命的。PID 控制器的微分项本身对延迟就敏感,滤波延迟再叠上去,整个回路很容易振荡。所以移动平均适合对实时性要求不高的显示、趋势记录、物料累计,不适合直接串在快速 PID 回路前面。

2.3 中值滤波:处理脉冲尖峰的“剔牙”

中值滤波的做法是把最近 N 个采样值排序,取中间值作为输出。它对付的干扰类型和前两种完全不同——不是持续性的噪声,而是偶发的脉冲尖峰。比如电磁阀动作瞬间耦合进来的浪涌、接触器拉弧产生的尖刺、称重传感器受到的瞬时冲击,原始信号里会出现一个明显偏离正常范围的“毛刺”。

中值滤波的输出不受单个极端值影响,只要窗口内正常值占多数,尖峰就会被直接丢弃。它的效果非常直观:一组数据里有几个异常大的点,排完序之后,中间值仍然是正常水平。窗口长度通常取奇数,3、5、7 比较常见。窗口太小了滤不干净,窗口太大了排序计算量大,而且会损失真实阶跃信息。

中值滤波在工业上用最多的场景是称重系统。料斗秤的信号偶尔会被振动冲击污染,用一阶低通或移动平均都赶不上,要拖很多个周期才能恢复,而中值滤波一个周期就能把尖峰剔除,同时保留真实的重量变化趋势。

2.4 选型速查:先看噪声类型,再定算法

我在实际项目中,遇到信号抖动从来不急着写代码,而是先打开趋势记录看波形,判断噪声的类型和频率,再选算法。下面这个表是多年的经验总结,可以直接作为参考:

干扰特征推荐算法关键参数典型场景
高频电磁干扰、白噪声一阶低通τ 取干扰周期的 5~10 倍温度、液位、pH 值
周期性脉动(泵、压缩机)移动平均窗口覆盖 1.5~2 个脉动周期压力、流量
偶发脉冲尖峰中值滤波窗口 3、5、7称重、振动监测
需要提取变化率、滤除趋势高通滤波视截止频率而定一般不直接用于输出平滑

至于“通滤波”这个叫法,我理解就是“通用滤波”的意思。把这三种算法做成一个可切换的通用包装,就是一个能应对大部分现场噪声的滤波功能块。下一章我直接给出完整的 ST 代码。

3. 在 CoDeSys 里编码:三个功能块加一个通用包装

3.1 功能块而不是函数:滤波必须“记得上一次”

在 CoDeSys 的 POU 类型里,功能块(FUNCTION_BLOCK)和函数(FUNCTION)最本质的区别是:功能块内部有存储区,可以在多次调用之间保留数据;函数则每次调用都从零开始。

滤波恰好是一个必须有“记忆”的操作。一阶低通要用上一次的输出值,移动平均要用历史窗口数据,中值滤波要保留最近 N 个采样。如果写成函数,上一次算出来的结果无法保存,每次调用都是空白的,滤波效果无从谈起。所以滤波一定做成功能块,每个现场的模拟量通道分配一个独立实例,各实例各存各的历史数据,互不干扰。

接口设计上,我统一了三个功能块的输入输出规范:原始信号 rX 输入,滤波结果 rY 输出,复位信号 bReset 负责初始化内部状态,nWindow 和 rTau、rDt 分别控制算法参数。这样的好处是调用方不用关心内部实现,换算法只是改一个实例的事,后面封装通用功能块的时候就非常顺手。

3.2 一阶低通功能块完整代码与实现说明

下面是完整的一阶低通滤波功能块。放在 CoDeSys 里新建一个功能块 POU,语言选 ST,命名建议用 FB_LowPassFilter。

FUNCTION_BLOCK FB_LowPassFilter VAR_INPUT rX : REAL := 0.0; // 原始采样值 rTau : REAL := 1.0; // 时间常数,单位秒 rDt : REAL := 0.05; // 采样周期(任务周期),单位秒 bReset : BOOL := FALSE; // 复位信号 END_VAR VAR_OUTPUT rY : REAL; // 滤波输出值 END_VAR VAR rAlpha : REAL; // 平滑系数 rLast : REAL; // 上一次输出 END_VAR
IF bReset THEN rLast := rX; rY := rX; ELSE // 防呆处理:采样周期和时间常数必须大于 0 IF (rDt > 0.0) AND (rTau > 0.0) THEN rAlpha := rDt / (rTau + rDt); ELSE rAlpha := 1.0; END_IF; rY := rAlpha * rX + (1.0 - rAlpha) * rLast; rLast := rY; END_IF;

这里有两个细节值得说。第一,复位时我把 rLast 和 rY 都赋成了当前采样值 rX,而不是赋成 0。这个做法的用意是避免功能块刚投入运行时,输出从 0 猛地跳到真实值造成“台阶”。如果你看过波形就会知道,从 0 起步的输出曲线会有一个明显的爬升过程,很容易被上位机误判为真实的过程变化。复位置成当前值,启动即平稳。

第二,rAlpha 的取值完全依赖 rDt 和 rTau。rDt 应当等于功能块所在任务的扫描周期。这个参数加权不当,滤波效果就会偏差,后面我会专门讲。

3.3 移动平均功能块完整代码与实现说明

移动平均我用了一个环形缓冲区来实现。环形缓冲的好处是每个扫描周期只做一次“减去旧值、加上新值”的运算,计算量恒定,不会随着窗口长度增加而变慢。窗口 nWindow 可以在外部动态调整,但不能超过数组容量。

FUNCTION_BLOCK FB_MovingAverage VAR_INPUT rX : REAL := 0.0; // 输入采样 nWindow : INT := 16; // 窗口长度 bReset : BOOL := FALSE; END_VAR VAR_OUTPUT rY : REAL; // 平均结果 nValid : INT; // 当前有效样本个数 END_VAR VAR_CONSTANT cMaxLen : INT := 64; // 环形缓冲区容量上限 END_VAR VAR arrBuf : ARRAY[0..63] OF REAL; nIdx : INT := 0; nCnt : INT := 0; rSum : REAL := 0.0; i : DINT; END_VAR
IF bReset THEN FOR i := 0 TO 63 DO arrBuf[i] := 0.0; END_FOR; nIdx := 0; nCnt := 0; rSum := 0.0; nValid := 0; rY := rX; ELSE // 窗口长度限制 IF nWindow <= 0 THEN nWindow := 1; END_IF; IF nWindow > cMaxLen THEN nWindow := cMaxLen; END_IF; // 环形缓冲:先减掉即将被覆盖的旧值,再写入新值 rSum := rSum - arrBuf[nIdx]; arrBuf[nIdx] := rX; rSum := rSum + rX; nIdx := nIdx + 1; IF nIdx >= nWindow THEN nIdx := 0; END_IF; IF nCnt < nWindow THEN nCnt := nCnt + 1; END_IF; nValid := nCnt; IF nCnt > 0 THEN rY := rSum / INT_TO_REAL(nCnt); END_IF; END_IF;

这段代码里有个容易被忽视的点:窗口启动阶段,nCnt 还没达到 nWindow 时,输出是“已累计样本的平均值”,而不是“窗口填满后的平均值”。这意味着功能块刚启动时输出会偏小,需要等 nWindow 个扫描周期后才是完整窗口语义。如果你的系统要求启动瞬间就必须有正确的窗口平均值,可以把冷启动策略改成“启动阶段直接输出 rX 或填充当前值”,这个完全可以按项目需求调整。

还有一个细节:环形缓冲区里 arrBuf 的索引是 nIdx,它始终在 0 到 nWindow-1 之间循环。窗口长度一旦在运行中被调小,缓冲区里残留的旧数据虽然不在计算范围内,但不影响正确性。窗口长度调大时要小心,如果调后的长度超过 cMaxLen,代码里做了限制,会直接钳位到上限。

3.4 中值滤波功能块完整代码与实现说明

中值滤波实现的核心是对窗口内样本排序。窗口通常不超过 15 个点,我直接用两层冒泡排序,代码短、易读,执行时间也在可控范围内。如果你要处理 32 点以上的大窗口,建议换成插入排序或快速排序,但对现场滤波而言,7 个点以内的冒泡排序已经绰绰有余。

FUNCTION_BLOCK FB_MedianFilter VAR_INPUT rX : REAL := 0.0; nWindow : INT := 5; bReset : BOOL := FALSE; END_VAR VAR_OUTPUT rY : REAL; END_VAR VAR_CONSTANT cMaxWin : INT := 15; END_VAR VAR arrBuf : ARRAY[0..14] OF REAL; arrTmp : ARRAY[0..14] OF REAL; nIdx : INT := 0; nCnt : INT := 0; i : DINT; j : DINT; rHold : REAL; nMid : INT; END_VAR
IF bReset THEN FOR i := 0 TO 14 DO arrBuf[i] := rX; END_FOR; nIdx := 0; nCnt := 0; rY := rX; ELSE // 参数保护 IF nWindow <= 0 THEN nWindow := 3; END_IF; IF nWindow > cMaxWin THEN nWindow := cMaxWin; END_IF; arrBuf[nIdx] := rX; nIdx := nIdx + 1; IF nIdx >= nWindow THEN nIdx := 0; END_IF; IF nCnt < nWindow THEN nCnt := nCnt + 1; END_IF; // 复制当前窗口样本到临时数组 FOR i := 0 TO (nWindow - 1) DO arrTmp[i] := arrBuf[i]; END_FOR; // 冒泡排序,窗口小,够用 FOR i := 0 TO (nWindow - 2) DO FOR j := (i + 1) TO (nWindow - 1) DO IF arrTmp[j] < arrTmp[i] THEN rHold := arrTmp[i]; arrTmp[i] := arrTmp[j]; arrTmp[j] := rHold; END_IF; END_FOR; END_FOR; // 取中值:偶数窗口取中间两个的平均 IF (nWindow MOD 2) = 0 THEN nMid := nWindow / 2; rY := (arrTmp[nMid - 1] + arrTmp[nMid]) / 2.0; ELSE rY := arrTmp[nWindow / 2]; END_IF; END_IF;

这段代码在复位时用 rX 填充了整个缓冲区,目的和低通滤波一样,避免启动阶段输出跳变。如果你不这么做,数组初值是 0,第一个输出就会是历史 0 和真实采样值混在一起的中值,波形会出现一个“假爬升”。

中值滤波对窗口内样本的个数很敏感,我建议现场直接固定用奇数窗口。奇数窗口的中值永远是窗口内的真实样本,不会凭空造出一个不存在的值;偶数窗口虽然可以取中间两数的平均,但会让输出出现“半整数台阶”,有些工艺画面显示起来会感觉不自然。

3.5 组合起来:一个可切换算法的通用滤波功能块

把上面三个功能块封装到一个外壳里,就得到了标题里说的“通滤波功能块”。我这里用 nMode 参数选择算法:0 表示一阶低通,1 表示移动平均,2 表示中值滤波。外部只暴露统一接口,内部实例分别保存各自的状态。

FUNCTION_BLOCK FB_UniversalFilter VAR_INPUT rX : REAL := 0.0; nMode : INT := 0; // 0=低通,1=移动平均,2=中值 rTau : REAL := 1.0; // 低通时间常数 rDt : REAL := 0.05; // 采样周期 nWindow : INT := 5; // 移动平均/中值窗口 bReset : BOOL := FALSE; END_VAR VAR_OUTPUT rY : REAL; nModeOut : INT; END_VAR VAR fbLP : FB_LowPassFilter; fbMA : FB_MovingAverage; fbMD : FB_MedianFilter; END_VAR
nModeOut := nMode; CASE nMode OF 0: fbLP(rX := rX, rTau := rTau, rDt := rDt, bReset := bReset); rY := fbLP.rY; 1: fbMA(rX := rX, nWindow := nWindow, bReset := bReset); rY := fbMA.rY; 2: fbMD(rX := rX, nWindow := nWindow, bReset := bReset); rY := fbMD.rY; ELSE rY := rX; END_CASE;

通用功能块适合用在项目前期不确定现场噪声类型的阶段,先在调试模式下切换 nMode 试出最优算法,再针对性地换成专用功能块。这样做的代价是整个通用块会同时占用三份内部状态内存,一个实例大概多出几百字节,PLC 内存紧张时要注意,每个模拟量通道都挂一个通用块,几百个通道下来内存占用就不小了。

如果是大批量上点位的项目,我建议直接复制专用功能块代码,按通道创建实例,只保留实际用到的算法,能省不少内存。

4. 从建工程到仿真验证:实操流程一步不差

4.1 在 CoDeSys 里新建 POU,把代码放进去

打开 CoDeSys 新建工程,标准工程,设备选择 CoDeSys Control Win V3 x64(接力仿真就选这个),PLC 语言按需选择。工程建好之后,在左侧树形结构里右键“应用程序”,选择“添加对象”→“POU”,类型选“功能块”,实现语言选“结构化文本(ST)”,名称填 FB_LowPassFilter。

把上一个章节的代码分别粘贴到对应功能块中,编译一次。如果编译报错,优先检查变量声明和常量名是否一致。注意功能块名不要和 CoDeSys 库自带的名称冲突,比如 FB_MedianFilter 这个命名在大部分环境里不会撞车,但如果你引入过 OSCAT 等库,里面可能已经有同名对象,建议加项目前缀,比如 FB_MY_MedianFilter。

编译通过后,再新建 FB_MovingAverage、FB_MedianFilter 和 FB_UniversalFilter,依次粘贴代码。每次粘贴后都编译一下,避免错误集中在最后一起爆发,反而不好定位。

4.2 主程序实例化调用,别拜错主任务

功能块写好了只是“造了个工具”,要在主程序里实例化并调用它。我习惯在主程序 PLC_PRG 的变量区里声明功能块实例:

VAR fbFilter : FB_UniversalFilter; rAnalogRaw : REAL; // 模拟量原始值,工程中通常来自AI模块 rFiltered : REAL; // 滤波后的值 bResetCmd : BOOL; uiModeSel : INT := 0; // 0=低通,1=移动平均,2=中值 END_VAR

在主程序循环里调用:

fbFilter( rX := rAnalogRaw, nMode := uiModeSel, rTau := 2.0, rDt := 0.05, nWindow := 5, bReset := bResetCmd ); rFiltered := fbFilter.rY;

CoDeSys 允许实例调用后通过实例名.输出变量名读取输出值,上面的写法就是标准做法。这里最要紧的是 rDt 必须和任务周期匹配。如果主任务是一个周期为 50ms 的循环任务,那 rDt 就填 0.05,不能填 0.1 也不能填 0.02。这个参数错了,滤波效果和你预期会差得很远。

任务配置的位置在设备树的“应用程序”→“任务配置”里。打开任务配置,双击你的循环任务,能看到周期设置。PLC_PRG 里调用功能块的代码运行在这个任务中,所以任务周期就是功能块的采样周期。

4.3 用模拟量信号源与趋势图验证滤波效果

功能块写完先别急着接现场信号,先用模拟信号验证。我的做法是在主程序里临时加一段信号发生器逻辑,生成一个带噪声的模拟量,效果比手动强制变量方便得多,能看到滤波的动态响应。

VAR tSimTime : TIME; rSimRaw : REAL; rSimNoise : REAL; bSimOn : BOOL := TRUE; END_VAR
IF bSimOn THEN tSimTime := TIME(); // 基波:50 Hz 的 0.1 倍幅值正弦,模拟缓慢变化 rSimRaw := 100.0 + 10.0 * SIN(TIME_TO_REAL(tSimTime) / 1000.0 * 0.3); // 叠加一个人工脉冲毛刺,模拟现场尖峰干扰 IF (TIME_TO_REAL(tSimTime) MOD 2000.0) < 50.0 THEN rSimNoise := 6.0; ELSE rSimNoise := 0.0; END_IF; rAnalogRaw := rSimRaw + rSimNoise; ELSE // 实际运行时接入真实的AI模拟量地址 rAnalogRaw := 0.0; // 接实际输入通道,例如 AI 模块的原始值 END_IF;

然后用 CoDeSys 内置的“调试”→“跟踪”功能,把 rAnalogRaw 和 rFiltered 两个变量加进去,启动在线仿真,跑几十秒,看趋势曲线。你应当看到:低通模式下毛刺幅度明显减小,输出曲线滞后于原始信号一段;中值模式下尖峰脉冲被直接削掉,但阶跃响应保持锐利;移动平均模式下周期性脉动被抹平,窗口越大越平滑。

没有 Trace 功能的环境,也可以直接在可视化页面放两个数值显示框和趋势控件,效果类似。关键是同时观察“滤波前”和“滤波后”两条曲线,而不是只看滤波后那一根线。没有滤波前做对照,你根本判断不了滤波力度是不是合适。

4.4 效果评判:滞后、幅值和稳定三件事

仿真阶段怎么判断参数合不合适,我总结了三个指标。

第一看幅值抑制率。滤波后的噪声幅度至少要降到滤波前的百分之五十以下才算有效。比如原始信号在 100 到 106 之间跳,滤波后能稳在 100 到 102 之间,说明把大部分高频成分滤掉了。如果滤波后还是跳得厉害,要么 rTau 太小,要么窗口长度不够覆盖干扰周期。

第二看滞后程度。低通滤波和移动平均都不可避免地引入相位滞后,但滞后不能影响工艺控制。给一个阶跃输入,观察输出从阶跃开始到稳定值的响应时间,如果这个时间明显超过了工艺允许的报警响应时间,就得减小时间常数或窗口长度。

第三看稳定性。滤波输出在高频噪声被抑制的同时,不能出现自激振荡、不能出现锯齿状爬坡。如果输出波形里有规律性的波浪,多半是采样周期 rDt 和实际任务周期不一致,或者时间常数与噪声周期形成了谐振。

仿真调参阶段,我强烈建议把 rTau、nWindow 做成可视化页面上的输入框,边看趋势边调整。这样一套流程走下来,到现场接真实信号时,你只需要微调一两轮参数就能交付。

5. 参数整定与现场踩坑:比书本多一层的东西

5.1 时间常数和任务周期对不上,滤波等于白做

我在第二个项目里用一阶低通滤波时犯过一个低级错误。任务周期实际是 100ms,我却在功能块里填了 rDt=0.01,结果滤波输出几乎没效果,噪声还是原样。后来排查发现,工作人员复制代码时照抄了旧项目的 rDt 默认值,没按新项目的任务周期改。

rDt 是采样周期,必须等于功能块实际被调用的周期。如果功能块在一个 100ms 的任务里被调用,那 rDt=0.1,这在算法上才是对的。填小了,α 变小,滤波输出几乎不跟随真实信号,通道堵得像卡住一样;填大了,α 变大,滤波趋近于直通,噪声也就原样透传了。

建议把 rDt 设计成只由工程初始化的常量,不要在触摸屏上开放修改。因为现场人员很难理解“采样周期”这个概念,他们更习惯调时间常数。时间常数 rTau 倒是可以做成 HMI 可调参数,但要加限幅,比如限制在 0.1 到 10 秒之间,防止误设成 0 或者负数。

5.2 扫描周期漂移怎么办:手动 rDt 与自动取时间

PLC 的任务周期通常由操作系统调度,偶尔会出现时间漂移,尤其是系统负载高、任务被其他中断抢占的时候。CoDeSys 自身对软实时系统有调度能力,但你如果在功能块里把 rDt 写死成 0.05,而任务实际执行间隔忽长忽短,滤波算法的α 就会在每次调用时略有偏差,导致输出出现肉眼可见的抖动。

如果你的项目中任务周期确实不稳定,可以在功能块内部自动测量两次调用之间的时间间隔,用系统时间函数替代固定 rDt。CoDeSys 里可以用F_GetSystemTime或者TIME()获取系统时间,在功能块里存下上次调用的时间,相减得到实际间隔,再作为 rDt 参与计算。

不过这个方法对新手来说会增加难度,我是建议在入门阶段先固定任务周期。大多数 CoDeSys 软 PLC 和嵌入式控制器的任务周期还是能保证微秒级精度的,只要你的主任务里没有大量重型运算导致超时,写死 rDt 完全够用。

5.3 浮点转换、无用样本和启动瞬间的“台阶”

模拟量模块输出的原始值通常是 INT 或 WORD,要先转换成 REAL 再进滤波功能块。这里有一个容易忽略的细节:如果你的模拟量模块分辨率是 12 位,工程量转换过程本身就会引入量化误差,滤波之后看起来虽然平滑,但可能已经“磨掉”了真实的小变化。建议在模拟量转换函数里保留尽量多的小数位,不要先取整再滤波。

另外一点,中值滤波和移动平均在 PLC 工程里接收的是每个扫描周期的采样值。如果某个周期因为任务被抢占而漏采样,功能块并不知道,它所认为的“连续采样”实际上是稀疏采样,滤波结果在时间上并不均匀。所以尽量把滤波功能块放到固定周期任务里,不要放到事件触发任务里。事件任务触发的时机不稳定,滤波效果几乎没谱。

启动瞬间的台阶问题,我的处理办法已经在每个功能块里用 bReset 分支解决了。但如果你在运行时通过上位机下发复位命令,复位瞬间输出会立刻跳到当前采样值,这本来是设计行为,不是错误。需要注意的是一定要把复位逻辑做成“上升沿触发”,而不是持续置位。持续置位会导致功能块永远停留在跟随输入的状态,滤波功能完全失效。

5.4 滤波输出与 PID、报警联动时,别让“平滑”害了你

最后说一个最容易栽跟头的地方。滤波是给执行机构和显示用的,但不一定适合给报警和保护逻辑用。你把液位信号滤得越平滑,报警的响应就越迟钝。真到了液位快速上升的紧急时刻,滤波造成的滞后可能让报警晚来好几秒,这在安全相关场景里是不能接受的。

我的经验是给报警逻辑使用“轻度滤波”甚至原始信号,给 PID 和显示使用“重度滤波”。同一路模拟量可以同时接两个功能块实例,一个时间常数小的用于报警判断,一个时间常数大的用于显示和累计。数据采集和处理本身就便宜,多建一个实例不会增加硬件成本,但能同时保住“稳定显示”和“灵敏报警”两个要求。

至于 PID 控制,滤波时间常数必须小于控制对象的响应时间常数,否则回路的相位裕度被滤波吃掉,调参时会出现一个很奇怪的现象:P 调大了振荡,调小了又不跟,怎么调都不对劲。遇到这种情况,先检查滤波是否串在 PID 的反馈路径里,再把滤波时间常数降下来试试。很多时候不是 PID 参数的问题,而是滤波导致反馈信号“太钝”了。

如果你把滤波功能块接到模拟量累计逻辑上,比如流量累计,也要小心。累计逻辑通常在每个扫描周期把瞬时流量乘上周期时间再累加,滤波改变了瞬时流量的波形,如果滤波器有偏置(比如中值滤波在噪声非对称时会引入偏置),累计结果就会和真实值有系统偏差。这种偏差是慢性的,跑一天下来可能差几个百分点,不对比原始值根本发现不了。

这一期从“为什么要自己写滤波块”讲到三种算法,再到 CoDeSys 完整代码、仿真验证、现场调参,算是把滤波功能块这条路完整走了一遍。最后再分享一点个人体会:滤波器不是摄像头里的美颜功能,它的本质是用“延迟”换“平滑”。你不可能既要响应快又要输出稳,现场选参数前一定先问自己,我到底要的是哪一头。想清楚这个,滤波功能块就成功了一半。下一期我再接着聊 CoDeSys 里的其他实战模块,到时候见。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询