☰
CoDeSys实战:从零开发可复用的一阶低通滤波功能块
2026/9/30 13:01:58 网站建设 项目流程

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)

举例,假设程序扫描周期是10ms(0.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,非常接近0,EXP(-0.000628) ≈ 0.99937,1.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项目里也遇到过信号抖动的坑,或者用了我这个功能块以后遇到什么问题,随时交流,我可以把后续的移动平均和二阶滤波也整理出来。

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

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

立即咨询