ARTICLE DETAIL

资讯详情

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

CoDeSys实战:从零开发可复用的一阶低通滤波功能块

CoDeSys实战:从零开发可复用的一阶低通滤波功能块 1. 项目概述与需求拆解1.1 为什么要在CoDeSys里写滤波功能块做PLC编程的朋友尤其是接触过模拟量采集、传感器信号处理、运动控制反馈的十有八九都遇到过同一个问题现场信号太“脏”了。我这里说的“脏”不是灰尘那种脏而是说直接采上来的信号经常带噪声。比如一个压力变送器输出的4-20mA信号因为现场有变频器、电磁阀、大功率电机这些干扰源采集到的数值会上下跳动。又比如编码器反馈的速度值在低速段或者启停瞬间数值波动非常明显。如果你把这路信号直接拿去参与PID运算或者比较判断控制效果基本没法看——阀门开度忽大忽小电机忽快忽慢严重的时候还会触发报警误动作。这时候就需要滤波了。滤波的本质很简单在保留有用信号特征的同时把不需要的噪声分量压下去。在PLC里做滤波最常见的实现方式有两种一是在硬件上加RC滤波电路二是在程序里写软件滤波功能块。硬件滤波响应快、原理直观但是改参数要动电路板调试麻烦软件滤波灵活、参数可调、不用改硬件而且能在线修改参数实时观察效果适合在项目调试阶段反复试。我之前遇到过挺多看网上教程的朋友一上来就搜“低通滤波电路”“RC低通滤波电路”其实那都是硬件工程师的思路。做PLC的哥们儿更应该关注的是软件滤波——也就是用代码把采样值做算法处理。这篇博文要讲的就是在CoDeSys环境里从零开发一个可复用的通滤波功能块以低通滤波为核心顺带把高通滤波也带出来并且把它封装成标准功能块方便在项目里拖出来直接用。1.2 这个实战案例适合谁如果你已经在用CoDeSys做了几个项目对ST语言有一定基础但又觉得自己写的程序总是“土办法”——比如用一大段IF判断去处理信号抖动那这篇文章非常适合你。我讲的不光是个滤波功能块的代码更是一整套功能块开发的方法论从需求分析、数学原理、接口设计、代码实现、封装测试到现场调参每一步该干什么、为什么要这么干我都会讲清楚。如果你是完全的新手刚接触CoDeSys不久ST语言还不太熟那这篇文章你也能看。我尽量把逻辑拆细遇到关键语法和概念会停下来解释。比如说REAL类型是什么VAR_IN_OUT和VAR_OUTPUT有什么区别功能块和函数有什么差别——这些基础概念我都会带一笔。如果有不懂的地方建议先看看我之前写的CoDeSys入门系列前面的文章再把这篇作为进阶实战来读。2. 滤波算法选型与设计思路2.1 低通、高通、通滤波到底差在哪先把这个基础概念捋清楚。滤波器的分类核心看频率。信号可以拆成不同频率分量的叠加低频分量对应变化缓慢的趋势高频分量对应快速波动的噪声。低频部分的信息通常是我们真正关心的比如液位慢慢上升的趋势、温度缓慢升高的过程、速度从静止加到目标值的变化规律。而高频噪声往往是干扰比如电磁耦合造成的毛刺、变频器脉冲干扰、传感器自身抖动。低通滤波器LPF让低频通过、削弱高频这是最常用的。高通滤波器HPF反过来让高频通过、削弱低频常用于提取信号中的突变信息比如检测压力骤升、振动冲击、速度的瞬态变化。而“通滤波”这个词行业里一般不这么叫但从功能上讲就是带通BPF即让某一频段的信号通过低于和高于这个频段的都衰减。带通可以简单理解为低通和高通的串联组合或者叫“保留指定频段”的滤波。咱们这次做的主要是低通滤波功能块但我会把接口设计成可以扩展的这样你学会了低通的做法高通和带通也就是改个公式的事情。2.2 一阶低通滤波的数学原理软件低通滤波的算法很多有滑动平均滤波、中值滤波、加权平均滤波、一阶惯性滤波、二阶滤波还有更高级的卡尔曼滤波不过那玩意在PLC里一般不用计算量偏大。对于PLC项目来说用的最多、最稳定的其实是一阶惯性滤波也叫一阶低通滤波。它的算式极其简洁Y(n) Y(n-1) α × (X(n) - Y(n-1))拆开说Y(n)是本次滤波输出值Y(n-1)是上一次的滤波输出值X(n)是本次的采样输入值α是滤波系数取值在0到1之间。这个公式的意思是本次输出 上次输出 采样偏差的一小部分。α越小输出变化越缓慢滤波作用越强但是信号的响应就越迟钝α越大输出追随输入越快噪声抑制效果就越差。你可以把它想象成一个水池——输入是水管里的流量输出是水池里的水位α就是出水口的阀门开度。阀门开得大水位变化快但也容易被波动影响阀门开得小水位平稳但要等好一会儿才能反映上游流量的变化。现场调试滤波效果本质就是在“滤得多干净”和“响应得多快”之间找平衡。2.3 α系数的计算真正的核心难点纯讲公式没意思大部分人真正不懂的是这个α到底取多少这个问题的关键点在于α并不是凭空拍的它跟两个东西有关采样周期也就是程序的扫描周期TS以及你希望设定的截止频率fC。截止频率是滤波器的一个重要界限。低于这个频率的信号分量能保留高于它的分量会被明显衰减。计算公式是α 1 - exp(-2π × fC × TS)举例假设程序扫描周期是10ms0.01秒你希望截止频率是1Hz那么α 1 - exp(-2π × 1 × 0.01) 1 - exp(-0.0628) ≈ 1 - 0.9391 0.0609也就是说每次扫描新采样值只有大约6%的权重被叠加到输出上滤波很“重”。再看另一种情况扫描周期还是10ms但希望截止频率提高到10Hzα 1 - exp(-2π × 10 × 0.01) 1 - exp(-0.628) ≈ 1 - 0.5336 0.4664差别很明显α一下变到将近0.47信号响应快多了。这里特别要提醒一个新手容易踩的坑α的取值跟扫描周期是绑定的。如果你的PLC扫描周期是20ms你却在某个1ms触发的中断任务里调用这个滤波功能块那截止频率就完全不对了。这个滤波功能块的参数和实际调用周期必须匹配否则现场调试时怎么调都不对。所以设计功能块的时候我建议把“截止频率fC”做成输入参数而扫描周期TS从上层任务配置里传进来或者干脆作为另一个输入参数调用时填PLCSYSTEM信息即可。为了方便我们直接做成输入参数调用者在哪个任务里调用就填对应的周期。3. 功能块接口设计与ST语言实现3.1 功能块整体架构在CoDeSys里做功能块本质就是定义一个带有内部状态和方法的程序单元。功能块和函数的区别在于函数FUN没有记忆同样的输入永远得到同样的输出功能块FB有内部变量能记住上一次的状态。滤波这种运算天然需要记忆——因为要保存Y(n-1)所以用功能块是正确选择函数做不了这件事除非你用全局变量存上次输出但那不利于复用。我设计的这个滤波功能块名字就用FB_FirstOrderFilter。它的接口分这么几块输入参数采样值X、截止频率fC或者直接给α也行但给截止频率更直观、采样周期TS。输出参数滤波结果Y还有一个状态量比如输出是否已经稳定方便上层判断是否把滤波后的值用于关键控制。内部变量上一次的输出Y_Prev、初始化标志bInit还有自动计算的α值。为了做带通扩展我会把算法逻辑拆成低通核心部分和高通核心部分都写在同一个功能块里通过一个枚举类型参数来选择工作模式。这样相当于一个功能块解耦出三种滤波方式用起来很方便。3.2 完整ST代码逐行解析下面我给出完整的功能块ST实现。你直接在CoDeSys里新建一个功能块选择ST语言结构化文本把代码复制进去就能用。FUNCTION_BLOCK FB_FirstOrderFilter VAR_INPUT bEnable : BOOL; // 功能块使能TRUE时启动滤波FALSE时输出跟随输入 rInput : REAL; // 待滤波的原始输入信号 rCutFreq : REAL; // 截止频率单位Hz低通模式有效 rSampleTime : REAL; // 采样周期单位秒填写实际调用周期 eFilterType : BYTE; // 滤波模式0低通 1高通 2带通 rBandFreqLow : REAL; // 带通模式下带通下限截止频率 rBandFreqHigh : REAL; // 带通模式下带通上限截止频率 END_VAR VAR_OUTPUT rOutput : REAL; // 滤波后的输出信号 bInitialized : BOOL; // 内部状态是否已初始化 END_VAR VAR rPrevOutput : REAL; // 上一次的输出值 rAlphaLow : REAL; // 低通滤波系数 rAlphaHigh : REAL; // 高通滤波系数 rPrevInput : REAL; // 上一次的输入值用于高通计算 rBandPrev : REAL; // 带通中间变量寄存 bFirstRun : BOOL : TRUE; // 首次运行标志 END_VAR然后是功能块的主体部分也就是动作Action里面写逻辑。在CoDeSys里功能块的逻辑建议放到一个Action里这样主程序结构更清晰。// 主执行逻辑可以放到Action: Main中 IF NOT bEnable THEN // 未使能时直通信号并重置内部状态 rOutput : rInput; rPrevOutput : rInput; rPrevInput : rInput; bFirstRun : TRUE; bInitialized : FALSE; RETURN; END_IF // 首次运行做状态初始化防止滤波器从0开始在输出端产生巨大的跳变 IF bFirstRun THEN rPrevOutput : rInput; rPrevInput : rInput; bFirstRun : FALSE; bInitialized : TRUE; END_IF // 防止截止频率或采样周期传参无效做保护处理 IF rSampleTime 0.0 OR rSampleTime 10.0 THEN rSampleTime : 0.01; // 默认按10ms周期处理 END_IF IF rCutFreq 0.0 THEN rCutFreq : 1.0; END_IF IF rBandFreqLow 0.0 THEN rBandFreqLow : 0.1; END_IF IF rBandFreqHigh 0.0 THEN rBandFreqHigh : 1.0; END_IF IF rBandFreqHigh rBandFreqLow THEN rBandFreqHigh : rBandFreqLow 1.0; END_IF // 按模式分别计算滤波系数并执行滤波运算 CASE eFilterType OF 0: // 低通模式 rAlphaLow : 1.0 - EXP(-2.0 * 3.14159265 * rCutFreq * rSampleTime); // 一阶低通Y(n) Y(n-1) α*(X(n) - Y(n-1)) rOutput : rPrevOutput rAlphaLow * (rInput - rPrevOutput); 1: // 高通模式 rAlphaHigh : 1.0 / (1.0 2.0 * 3.14159265 * rCutFreq * rSampleTime); // 一阶高通Y(n) β * (Y(n-1) X(n) - X(n-1))β 1/(1τ/T) rAlphaHigh : 1.0 / (1.0 (2.0 * 3.14159265 * rCutFreq * rSampleTime)); rOutput : rAlphaHigh * (rPrevOutput rInput - rPrevInput); 2: // 带通模式先低通再高通串联 // 先按上限频率做低通 rAlphaLow : 1.0 - EXP(-2.0 * 3.14159265 * rBandFreqHigh * rSampleTime); rBandPrev : rPrevOutput rAlphaLow * (rInput - rPrevOutput); // 在按下限频率做高通 rAlphaHigh : 1.0 / (1.0 (2.0 * 3.14159265 * rBandFreqLow * rSampleTime)); rOutput : rAlphaHigh * (rPrevOutput rBandPrev - rPrevInput); // 这里用rPrevInput保存的是中间量需要单独处理简化实现中直接存中间变量 rPrevInput : rBandPrev; rPrevOutput : rOutput; RETURN; // 注意带通模式的rPrevOutput更新逻辑独立需在RETURN前已有更新 END_CASE // 更新内部状态变量 rPrevOutput : rOutput; rPrevInput : rInput;上面这段代码是我浓缩出来的核心逻辑几个重要细节要单独说明。第一个细节是首次运行的初始化。如果不做这一步滤波器刚上电的瞬间Y(n-1)是0输入信号如果是100那么输出会从0慢慢爬到100那速度比真实信号慢太多甚至可能触发误判。初始化让输出直接从输入值起步回避了这个问题。第二个细节是禁用分支的处理。项目里有时候需要临时把滤波旁路直接看原始信号。我在接口上设计了bEnable禁用时直接输出原始值并且重置内部状态。第三个细节是带通模式。低通和高通的串联会导致状态变量管理比较复杂。我在上面的代码里用了一个简化的中间量存取方式实际做的时候建议更规范一点把低通输出的中间值单独存到一个VAR内部变量里。我给的代码是示意性的具体实现时我会拆成两个独立的小步骤来写避免混淆。3.3 CoDeSys里创建功能块的完整操作如果你从来没在CoDeSys里建过自定义功能块那我把操作路径说一下。打开CoDeSys开发环境在左侧的PLC_PRG或者你的主程序节点上右键选择“添加对象”-“功能块”输入名称FB_FirstOrderFilter语言选“结构化文本ST”确定。然后在功能块的声明区上半部分把VAR_INPUT、VAR_OUTPUT、VAR的这些声明全部复制进去。再把逻辑代码放到功能块的Body部分或者新建Action然后把代码写进去。主程序调用方式很简单示例PROGRAM PLC_PRG VAR fbFilter : FB_FirstOrderFilter; rawValue : REAL; // 从模拟量通道采集的原始值 filteredValue : REAL; END_VAR // 每个扫描周期调用一次注意填采样周期 fbFilter(bEnable : TRUE, rInput : rawValue, rCutFreq : 2.0, rSampleTime : 0.01, eFilterType : 0); filteredValue : fbFilter.rOutput;这样调用每个扫描周期滤波功能块执行一次截止频率2Hz采样周期10ms。你可以把fbFilter放在任务里调用任务周期配置成10ms即可。写完代码我一般直接做仿真测试CoDeSys自带的在线仿真模式挺好用。把功能块拖进一个测试程序给它一个正弦波和叠加方波或者白噪声的输入信号在线观测输出波形验证滤波性能。4. 参数整定与实战场景调试4.1 截止频率怎么定从信号特征反推实际项目里最纠结的问题往往不是代码而是参数怎么设。我一般按这样一个思路来操作。先分析信号特征。把信号在Trace工具里采集下来观察波形的波动频率。比如一个压力信号正常是缓慢变化的。你在Trace里如果看到噪声毛刺的波动周期大概在0.1到0.2秒也就是5到10Hz的频率成分而有用信号的变化周期是几秒到几十秒0.1到1Hz那截止频率就可以设在1到2Hz之间。低于这个频率的趋势保得住高于这个频率的毛刺会被削掉。具体实施公式我们前面已经有了α 1 - exp(-2π × fC × TS)。以扫描周期10ms为例截止频率取1.5Hz算出来是α ≈ 1 - exp(-0.0942) ≈ 0.09。再比如运动控制里的速度反馈。伺服电机的速度环扫描周期通常是1ms或更短这个任务里的滤波截止频率可以放到50到100Hz因为运动控制需要快速响应不能滤得太重。拿1ms周期、100Hz截止频率代入公式α 1 - exp(-2π × 100 × 0.001) 1 - exp(-0.628) ≈ 0.4664这个α接近0.47说明响应非常快基本跟手。这里要单独强调一下“工作频率”和“截止频率”的关系。滤波不是变速器它不能凭空产生信号中不存在的频率分量。如果你希望控制器能响应1Hz的控制指令变化那截止频率不能低于1Hz否则指令已经变了输出还没跟上那麻烦就大了。4.2 现场调试的黄金三板斧我调试滤波参数有三步几乎没失手过。第一步滤波前把原始波形记录下来。不管你的客户说“信号有点乱”还是“波动比较大”都要先看原始波形。没有基线对比就调滤波参数等于闭着眼睛开车。在CoDeSys里做这事很方便把原始值直接转成模拟量输出或者用Trace工具在线监视。第二步先把截止频率压低保守调。比如预估2Hz合适我一般先按0.5到1Hz调让输出变得非常平滑。然后逐步抬高截止频率直到输出在“平滑”和“跟手”之间达到客户能接受的那个平衡点。从保守到激进的方向调比反过来调不容易被现场噪声误导。第三步制造一个阶跃扰动观察恢复时间。在现场可以手动给一个阀门开度变化、或者让电机做一个小的速度阶跃看滤波输出从开始变化到跟随到位需要多久。记录这个响应时间如果太慢然后把截止频率调高一档再试。反复几次就能确定一个对系统最合适的参数组合。顺便说一个我踩过的坑有次在现场调一个温度信号温度变化本来就慢我把截止频率压到0.1Hz滤波倒是干净得很但后来操作员说有一步物料切换阀门后温度反馈半天不动PID一直在加输出都快超限了。后来发现是滤波太狠把真实的温度变化速度也给压没了。从那以后我养成了一个习惯滤波参数一定要在系统闭环的情况下验证绝不只看滤波效果本身。4.3 多路信号的统一滤波管理如果现场有几十路模拟量信号都要滤波每路建一个功能块实例就完事了吗可以但是不够聪明。我会封装一个多路管理功能块内部用一个ARRAY来存储多个滤波实例输入也用数组一次调用处理N路信号。FUNCTION_BLOCK FB_MultiChannelFilter VAR_INPUT rInputArray : ARRAY[0..15] OF REAL; rCutFreq : REAL; rSampleTime : REAL; bFilterEn : BOOL; END_VAR VAR_OUTPUT rOutputArray : ARRAY[0..15] OF REAL; END_VAR VAR fbFilters : ARRAY[0..15] OF FB_FirstOrderFilter; nIdx : INT; END_VAR // 按通道循环执行滤波 FOR nIdx : 0 TO 15 DO fbFilters[nIdx](bEnable : bFilterEn, rInput : rInputArray[nIdx], rCutFreq : rCutFreq, rSampleTime : rSampleTime, eFilterType : 0); rOutputArray[nIdx] : fbFilters[nIdx].rOutput; END_FOR这个做法的好处是扫描周期只需要调用一次这个多路功能块整体结构干净很多而且在组态上位机、画面绑定时只需要绑定数组变量省事很多。如果不同通道需要不同的截止频率加一个参数数组rCutFreqArray[]就行代码改动很小。5. 常见问题与排查技巧实录5.1 滤波输出启动瞬间跳变到输入值附近正常吗正常这是我前面讲的bFirstRun初始化逻辑在起作用。如果你希望输出从0慢慢爬升可以把初始化代码去掉但那样启动过程会有很大的过渡偏移。实际项目中传感器信号上电初期本来就不真实推荐的做法是功能块使能之前先让输出跟随输入bEnable:FALSE时输出直通输入等信号稳定建立之后再给bEnable置TRUE开始滤波这样输出不会出现任何突变。5.2 输出还是有明显波动是滤波不管用吗先别急着加滤波深度你得先看波动频率。如果波动频率很低跟有用信号接近低通滤波没法滤掉——因为你把它压掉的同时信息也没了只能从信号源上找干扰原因。比如传感器屏蔽层没接地、电缆跟动力线走同一个线槽、模拟量模块地线没接好这些都会导致低频干扰。软件滤波是最后的防线不是第一选择屏蔽、布线、接地永远是最先要解决的。如果波动频率确实很高加滤波没效果多半是α没变化。检查一下你调用的任务周期和功能块里填的rSampleTime是不是一致。我曾经见过一个案例任务周期是10ms但rSampleTime填了1秒算出来α趋近于0输出基本上是一条横线还在那儿调滤波调了半天。5.3 用EXP函数在REAL类型运算精度够吗CoDeSys里EXP函数支持LREAL和REAL两种重载你传REAL参数时它返回REAL。对于滤波系数的计算来说REAL的32位浮点精度完全够用。但需要注意一个细节当rSampleTime非常小比如1ms且截止频率很低0.1Hz时指数里的参数是2π×0.1×0.001 0.000628非常接近0EXP(-0.000628) ≈ 0.999371.0 - 0.99937 0.00063。这个结果在REAL精度范围内还能表达但如果你做减法顺序不当很容易出现有效数字丢失。建议用我代码里的写法alpha : 1.0 - EXP(-2.0 * PI * fC * TS)不要先算指数再改写成别的形式。5.4 功能块在多个任务里被调用有没有问题一个功能块实例不要同时在多个任务里调用尤其不要一个在快速任务一个在慢速任务里同时调用。因为功能块的内部变量只保存一份两次调用会互相覆盖状态输出会乱。要么每个任务建独立实例要么只在固定的任务里调用。这是我做多轴运动控制时一个真实的教训两个任务里调用了同一个滤波实例电机转速反馈忽快忽慢排查好久才发现是这个原因。5.5 滤波后数值有相位滞后能用什么办法缓解一阶低通滤波天然带来相位滞后滞后的大小跟截止频率和信号频率有关。如果系统对相位敏感比如闭环控制可以通过两个手段缓解一是适当提高截止频率增大α让滤波器更“通透”当然毛刺会多回来一点二是改用二阶滤波或者带前馈的滤波结构计算量增加不多但相位特性好一些。这个如果大家感兴趣我可以下一篇单独展开讲。6. 功能块封装技巧与扩展方向6.1 做好参数检查与保护一个成熟的功能块输入参数必须做合法性检查。在我的代码里已经示范了rSampleTime、rCutFreq的默认值保护。实际项目里可能还有更复杂的场景比如上位机在线修改截止频率的时候一下子传了一个特别离谱的值进来。如果你不做保护轻则波形异常重则计算溢出。建议所有输入参数都要在入口做范围约束超范围就取默认值同时可以加一个报警输出。6.2 用可视化和仿真辅助调试CoDeSys的可视化页面可以直接把功能块输入输出绑到控件上比如Slider控件绑截止频率Trend控件绑输入输出波形。调试的时候人机交互页面做好边拖动滑块边看波形找参数的过程体验非常好。这也是CoDeSys和传统PLC编程环境相比的一个显著优势调试工具链很完整。6.3 扩展方向移动平均、中值滤波、二阶滤波这次讲的一阶低通是基础后续扩展路径我给你画一下移动平均滤波维护一个固定长度的数据窗口每次输出窗口内所有采样值的平均值。它的优点是线性相位特性好缺点是要占用内存存窗口数据而且对突变信号的反应比较迟钝。中值滤波取窗口内数据的中位数作为输出对脉冲噪声比如雷击干扰、电磁尖峰抑制效果极好但会损失一些细节信息。二阶低通滤波用二阶微分方程滤波截止频率附近的衰减陡峭度更高噪声抑制更干净但有超调风险参数整定也复杂得多。卡尔曼滤波如果你做的是高精度运动控制或者惯性导航卡尔曼滤波是最优选择但它在PLC上计算量大、调参门槛高一般不轻易用。实战中我通常的组合拳是先做一次中值滤波干掉脉冲尖峰再串一级一阶低通压制高频噪声中间加一级移动平均做平滑最后输出给控制逻辑用。这个组合在大多数工业场景下效果都很硬但代价是滞后大了一点需要配合合适的截止频率设计来控制。6.4 最后再分享一个小技巧在CoDeSys里建议把滤波功能块放进你自己的库工程里设置成可发布库。这样以后再开新项目直接从库管理器里拖出来用不用每次复制代码。库版本管理还能保证所有项目的滤波算法逻辑完全一致出问题也不用担心项目之间代码有细微差别。另外给功能块写注释文档这一点我劝你千万不要省。过半年你再看到这段代码的时候会感谢当初写了注释的自己。我自己的真实感受是滤波功能块这种“小东西”看起来不起眼但它是所有模拟量信号处理的地基。把这个地基打好后面的PID、运动控制、设备保护逻辑都会稳很多。这篇实战内容到这里就算完整走了一遍。如果你们在CoDeSys项目里也遇到过信号抖动的坑或者用了我这个功能块以后遇到什么问题随时交流我可以把后续的移动平均和二阶滤波也整理出来。
返回列表