
开头先聊一个场景。有一次我在调试一台水处理设备的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_VARIF 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_VARIF 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_VARIF 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_VARnModeOut : 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_VARIF 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我却在功能块里填了 rDt0.01结果滤波输出几乎没效果噪声还是原样。后来排查发现工作人员复制代码时照抄了旧项目的 rDt 默认值没按新项目的任务周期改。rDt 是采样周期必须等于功能块实际被调用的周期。如果功能块在一个 100ms 的任务里被调用那 rDt0.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 里的其他实战模块到时候见。