做PLC调试这么多年,最让我有成就感的不是项目一次通过,而是手里攒下来一张张能反复用的功能块。今天这个“每周一个编程小例子”,想聊一个几乎所有工控人都绕不过去的坑——输入信号抖动。无论是按钮、继电器触点,还是接近开关,现场信号就没几个是干干净净的,稍微有点毛刺就能让程序误动作:计数器凭空多跳几位,电机该停不停,报警乱闪。折腾一圈下来,说不准又是哪个触点接触不良。
在这个例子里,我会把消抖功能块从原理到实现完整拆一遍,给出结构化文本和梯形图两种写法,再结合实际调试中容易踩的坑一起聊。不管你是刚入门看PLC编程基础知识,还是在非标项目里被现场干扰折磨得头大,这篇都能直接用得上。中秋在即,先祝各位工控同仁阖家团圆,吃口热乎的月饼;看完这篇,回工位上做“每周一个编程小例子”也更有底气。
1. 消抖到底消的是什么:从现场噪声到信号毛刺
1.1 常见的抖动来源与现场表现
先说说我见过比较典型的几类抖动。最普遍的是机械触点类。常规继电器、接触器、按钮,在动作瞬间金属触点会弹跳,电压波形上会出现一连串几十微秒到十几毫秒的通断,这在示波器上叫“弹跳毛刺”。第二个大户是传感器,尤其是电感式和电容式接近开关,在检测到铁板边缘、齿轮齿牙的时候,输出会反复跳变,有时候位置没完全到位,信号就贴着阈值来回翻。还有一个隐蔽的来源是变频器、伺服驱动器、接触器线圈通断产生的电磁干扰,会耦合到信号线里,让输入点出现瞬时的尖峰或电平翻转,这种尤其难查,因为它可能只在电机起停瞬间出现。
现场表现也很直观。用监控软件在线看,CPU扫描周期一般是几毫秒到几十毫秒,你肉眼在监控表里常看到某个BOOL量闪烁,或者某个计数器的数值连续加了好几下,本质上就是同一个物理动作触发了多次有效边沿。我见过最夸张的一次,是某设备上一个行程开关,按一次按钮计数器从0跳到57,根本没法用。
1.2 软硬件消抖的取舍
对付抖动,现场常用的手段分两层。
硬件层:给输入点并联RC滤波器,或者用带滤波器的输入端子模块。像西门子SMATIC一些数字量输入模块本身就有一个几个毫秒级的数字滤波参数可配,可以滤掉一部分毛刺。硬件滤波的好处是不过多占程序逻辑,缺点是需要改接线、换模块,而且RC参数固定之后不灵活。尤其点位数多、改造量大的时候,回来改电路板非常痛苦。
软件层:在PLC程序里写一个延迟判断逻辑,用一个定时器或计数器,只有信号持续维持一个设定时间后才认为它是有效状态。这就是我们常说的消抖功能块。它的好处是灵活性极高,同一个功能块可以给不同点位配置不同的消抖时间,不用动一根线,直接改参数就行。而且不太受硬件型号限制,三菱、西门子、Codesys、汇川这些主流平台都能用同一种思路实现。
从我个人的经验看,硬件滤波适合做“第一道粗筛”,软件消抖负责“精确认定”。真正核心的快逻辑、安全逻辑,还是得靠程序里的功能块兜底。硬件能滤掉一部分高频干扰,但低速弹跳、阈值反复穿越这类情况,软件定时器更可靠。
1.3 消抖时间参数的选择逻辑
功能块能不能用,关键看参数设多少。消抖时间太短,滤不干净;太长,会丢掉真实信号,尤其是高频计数、快速取反的场景,还会让响应滞后变得明显。
一般可以参考这些经验值:
- 普通按钮、旋钮、继电器触点:20ms到50ms比较合适。人在按按钮时,触点弹跳一般不超过10ms,留两到三倍余量已经足够,同时不会让人觉得响应“肉”。
- 接近开关、光电开关,常规检测点位:10ms到30ms。看现场干扰情况,如果线缆长、旁边有变频器,可以提到50ms。
- 计数和位置检测类:尽量设置在2ms到5ms,或者干脆不用软件消抖,靠高速计数器模块处理。因为计数信号一旦滤波时间太长,轮盘转快了脉冲就直接被吞掉,误差很感人。
要注意,这儿说的“时间”最终是几倍扫描周期。如果扫描周期是10ms,你设了20ms消抖,实际判断一次最快也要两个扫描周期,也就是20ms以上。这点在做时间敏感逻辑时要想清楚。
2. 功能块设计:把消抖做成一个标准封装
2.1 输入输出接口定义
要写功能块,第一步不是码代码,而是想清楚接口。消抖功能块对外至少需要这些端口:
- 输入信号(bIn):原始、未经过滤的信号。
- 消抖时间(tTime):单位用毫秒(TIME或TON类型)。
- 输出信号(bOut):经过消抖后判定的稳定信号。
- 上升沿输出(bRising):检测到消抖后信号的上升沿。
- 下降沿输出(bFalling):检测到下降沿。
此外,内部需要一个定时器,以及一个标记“当前暂态”的中间变量。有人会问,为什么输出信号之外还要上升沿/下降沿?因为在实际项目里,很多动作并不需要持续电平,而是用一个瞬时脉冲去触发。比如按钮启动,按下一次只需要一个上升沿脉冲,如果直接用稳定信号去置位线圈,按钮一直按住时线圈就不会复位,有时候正是我们想要的自锁效果,但有时候又会引起误触发。所以把沿信号一起留出来,调用端按需取用更方便。
端口命名建议统一,比如:
- inSignal
- filterTime
- outSignal
- outRising
- outFalling
这套接口可以跨平台映射到任意PLC环境里,换平台时只改内部实现。
2.2 结构化文本实现:用TON把它自然写出来
我一般用结构化文本(ST)写,逻辑思路最清晰。下面的代码以Codesys风格的ST为基础,三菱、博途、汇川改改类型声明就能用。
FUNCTION_BLOCK FB_Debounce VAR bTimerStart : BOOL := FALSE; tonDebounce : TON; END_VAR VAR_INPUT bIn : BOOL; tFilterTime : TIME := T#20MS; END_VAR VAR_OUTPUT bOut : BOOL := FALSE; bRising : BOOL := FALSE; bFalling : BOOL := FALSE; END_VAR // 核心逻辑:只有输入信号持续达到设定时间才更新输出 IF bIn <> bOut THEN bTimerStart := TRUE; // 启动定时器,持续检测,直到时间到达 tonDebounce(IN := bTimerStart AND bIn, PT := tFilterTime); IF tonDebounce.Q THEN bOut := bIn; bRising := bOut AND NOT bPrevOut; bFalling := NOT bOut AND bPrevOut; bPrevOut := bOut; END_IF ELSE bTimerStart := FALSE; tonDebounce(IN := FALSE); bRising := FALSE; bFalling := FALSE; bPrevOut := bOut; END_IF这里用到了TON定时器:输入bIn发生变化时,开始计时;若在计时期间bIn又变回去了,定时器会自动清零,不会误判。只有当bIn持续保持新状态超过tFilterTime后,才把bOut更新为新值。而bRising/bFalling通过比较上一次稳定输出bPrevOut来生成。
初次接触这个逻辑的PLC编程入门学习者,容易陷入“为什么不直接用TON定时器,非要把bTimerStart变量接进来”的疑问。原因很简单:不带使能端的TON,在输入掉回旧状态时Q输出保持一段时间,不严格。这里用“与上bIn”来确保,如果输入已经反转,定时器立刻被禁止复位,避免在半途定格。
说句实在话,像三菱的定时器OUT指令,在功能块里做循环调用容易出各种肚子的坑;用ST写TON是更稳妥的方案。如果只能用梯形图,请参考下一节。
2.3 梯形图版本与扫描周期原理
很多老师傅还是习惯用梯形图写逻辑。梯形图版本的消抖功能块在思路上通常有两种做法。
一种是“定时器+保持”法:输入信号x0,先把它和定时器线圈串联,定时器设定值设为消抖时间。输入有高电平后,定时器开始计时,计时到达后,定时器触点接通,再把一个内部中间继电器M50置位。同时,M50的常开触点并联在定时器线圈两端,起到保持作用,除非输入重新变为OFF,定时器线圈断开,M50复位。这个逻辑把“信号持续为ON超过设定时间”转化成“定时器计时完成后M50一直保持”。
另一种是“计数器+窗口”法:利用PLC的扫描周期计数,每扫描一次做一次累加,当输入为OFF时清零,累计达到设定的“次”数后输出为ON。这种适合没有定时器指令的老PLC,但分辨率不稳定,因为扫描周期会随程序长短变化。所以我还是偏向于TON。
无论哪种,都要理解扫描周期的影响。PLC从输入采集到程序扫描再到刷新的周期本身不是绝对固定,功能块里的定时器依靠PLC内部定时器中断或系统时钟计时,时间基准是准的,但判断边沿、刷新输出都发生在扫描周期的末尾。所以整个消抖链路的实际延时大约是消抖时间加上一个到两个扫描周期。这在涉及安全回路的场合必须留足余量,不能一套参数全世界通吃。
3. 典型场景实战:按钮启动停止与传感器计数
3.1 场景一:普通按钮的启动停止
最经典的“启动停止”按钮,直接用消抖后的信号和沿信号就非常舒服。
按钮X0接启动,X1接停止,分别接两个功能块实例。启动按钮的bRising沿用来置位电机线圈M0,停止按钮的bRising沿用来复位M0。这样做的好处显而易见:
- 不用担心按钮触点抖出的多次变化,不会出现按一下起两个操作。
- 利用沿而不是电平,即使操作员按住按钮不松手,也只会触发一次置位,不会反复启动。
- 停止按钮如果误按一下不小心抖出一下,也会被认为是一次有效停止,但这本来就是操作员动作,物理弹跳造成的问题已经滤掉了。
有人会问,停止按钮要不要用常闭触点接线?我的建议是,需要急停的回路用常闭,普通启停用常开。功能块只管软件抖动,硬件安全仍要遵循对应标准,不要混在一起。
3.2 场景二:接近开关与高速计数
接近开关检测齿轮齿数,或者输送带上工件计数,这种场景最考验消抖块的参数取舍。
我之前有个项目,检测旋转轴上六个齿形凸台,转速最高3600转/分,齿轮每转产生6个脉冲,算下来最高频率约360Hz,脉冲周期约2.8ms。如果我把消抖时间设成20ms,那一个信号周期都撑不过去,输出来不及翻转就被判定成无效,计数值会少得可怜。这时候必须放弃通用消抖逻辑,改用高速计数模块,或者把消抖时间压到1ms以内。但1ms在普通PLC扫描周期内又几乎不可实现。
另一个常见问题是接近开关在临界位置时反复ON/OFF,但因为运动速度慢,持续时间长,消抖时间设成50ms也不影响采样率,反而能滤掉抖动。所以做现场应用时,要先估算信号额定频率,再反推消抖时间上限。规律很简单:
- 最高信号频率下,信号ON最短时间或OFF最短时间应大于消抖时间,否则该状态永远不会被认定。
- 对于计数信号,消抖时间尽量不超过信号周期的三分之一,否则边沿会严重滞后。
表格里我给个参考:
| 应用类型 | 典型信号频率 | 推荐消抖时间 |
|---|---|---|
| 普通按钮 | <1Hz | 20ms~50ms |
| 输送带光电计数 | 10Hz~50Hz | 5ms~10ms |
| 齿轮凸台检测 | 200Hz~500Hz | 不用普通功能块,改用高速计数 |
| 电机正反转反馈 | 几Hz | 30ms~100ms(防误动) |
这表是我拍脑袋定的吗?不是,是从项目调试记录里归纳的。你只要记住,参数没有万能值,只有现场调出来的值。
3.3 场景三:在博途/Codesys/三菱中的调用方式
写好的功能块,在不同平台里调用的方式大同小异。
在西门子博途(TIA Portal)里,把功能块放到程序库的“函数块”文件夹下,然后在OB1里拖出来,为每个输入点建一个背景数据块。比如按钮A对应DB_A,接近开关B对应DB_B。这样每个实例都能单独设置过滤时间,互不干扰。
在Codesys系列平台里更简单,直接把功能块当成一个类型,在程序区声明局部变量,比如:
VAR db1 : FB_Debounce; db2 : FB_Debounce; END_VAR然后赋值调用:
db1(bIn := X1, tFilterTime := T#30MS); Q1 := db1.bOut;三菱GX Works里不支持POU复用同一个ST功能块那么自由,一般是用子程序或FB块实现,三菱的FB也有类似结构。如果你习惯用梯形图,就把功能块整体复制到主程序里,然后用不同的M点代替内部变量。
调用次数多了以后,我建议做一个“素材库”文件夹,专门存放这类基础功能块,旁边加上注释说明版本和更换日期,这样后面维护起来会比翻旧程序舒服得多。
4. 调试排障实录:消抖功能块不工作的几个坑
4.1 信号一直被过滤:时间参数过大问题
踩过最多的坑是把消抖时间设得过大,尤其从别处复制来的程序,没看参数就直接用。原本按钮只要20ms,结果功能块里留着200ms,按下去半天才反应。你在线看输出信号一直是0,监控原始输入已经变成1了,但输出还在0上,往往就是过滤时间太长。
还有更隐蔽的,信号是脉宽本身只有100ms的短信号,消抖时间也设了100ms,这样哪怕信号没有抖动,也几乎无法被识别,因为“有效”条件要求100ms内必须持续为ON,但实际上只ON了99ms,定时器刚要到点就断电复位了。这种问题在自动化计数、气缸到位检测里很常见。
排查思路:把过滤时间临时改成0或1ms,看输出是否能正常变化。如果能,就说明是时间参数过大或者是信号本身脉宽不足,然后根据实际脉宽重新设值。
4.2 信号仍然抖动:时间参数太小与硬件残留
设置10ms消抖时间,但用在线监控看,输出偶尔还是会跳一下。这里头有两个可能。
一个是物理弹跳持续时间超过10ms,尤其在老旧接触器上,触点弹跳最长可以到几十ms。这种不仅要把参数调大,最好在硬件侧也处理一下,比如在端子排上并联RC吸收回路。
另一个是信号线上叠加了干扰,在输入模块内部形成了几十毫秒的低电平毛刺。这种毛刺是不是“抖动”?严格说不是触点弹跳,而是电气噪声,但软件功能块一样可以滤掉一部分。问题是如果噪声持续时间长、反复出现,消抖时间要设到几百毫秒才压得住,但这样正常工作也会受影响。
我后来学到的办法是,先用示波器挂到PLC输入端子处看波形,确认毛刺形态和持续时长。如果是尖峰干扰,优先查线缆屏蔽层、接地、走线路径;如果单纯是触点弹跳,加消抖时间就行。不要一上来就只改功能块参数,否则治标不治本。
4.3 定时器复位与扫描周期不一致:常见逻辑错误
自己手写消抖逻辑时,最容易出现“输出沿疯狂重复触发”的问题。原因多出在沿信号生成的地方。
比如用TON定时器,当输入保持稳定很长一段时间后,你希望比较器只在状态变化那一刻输出一个扫描周期的脉冲。但有人会把沿输出这么写:
bRising := bIn AND NOT bPrevIn;这里bPrevIn是原始输入,原始输入抖动导致bPrevIn和bIn在你更新前又相等了,会出现沿丢失或重复。正确的做法是把bRising放在消抖后的输出与上一个消抖输出比较,也就是我代码里用bPrevOut的那个位置。
还有,在一个扫描周期里,如果你在功能块内部既改变了bOut又在最后一行比较bOut,有可能因为扫描顺序问题,导致bRising在同一个周期里连续触发。解决办法是,把bRising、bFalling的赋值放在功能块末尾的同一段,并且保证每次扫描只按一次逻辑计算;实在不行,就在主程序里用上升沿指令对bOut再做一次边沿提取。
碰到这种疑难杂症,最好的排查方法不是看代码段想,而是把在线监控里所有相关变量都拉在一起,加个趋势图,按时间轴看bIn、bTimerStart、tonDebounce.Q、bOut、bRising,一眼就能看出定时器状态和输出是否匹配。
5. 从消抖块到更通用的数字量处理组件
5.1 上升沿/下降沿分离扩展
一个消抖功能块并不只是用来“过滤”,它完全可以扩展成边缘检测功能块。在上面代码里,输出已经包含bRising和bFalling,这就是一个带消抖的沿检测器。
实际应用中,我们可以基于它再封装一个“延时断开”功能块,或者在输出后并联一个TOF定时器做“掉线保持”。比如设备紧急信号来了,虽然现场环境干扰很大,但只要检测到上升沿,就要锁定报警,这样就需要在功能块里加一个锁存输出。
这种模块化思路我用得很顺手。基础的功能块只负责最基本的功能,上面再一层一层包逻辑,类似盖房子时先做好地基框架再填墙,出问题时分开排查比较快。
5.2 与触摸屏、PID调节等场景联动
别以为消抖功能块只跟数字量输入有关。现场很多PID回路,比如温度PID波动温差大的问题,有一部分就是从PV侧的测温信号引入干扰引起的。热电偶信号在PLC中经过模拟量转换后变成数字量,如果这组数字量变化频繁,FB的反向计算就会一直抖。虽然模拟量不能用BOOL消抖,但可以把算法思路迁移过来:设一个小的变化死区,只有变化量超过死区时才更新PV值,等于是“模拟量消抖”。
这个思路跟PLC数字量消抖一样,都需要做“时间窗口+幅度窗口”判断。以前我调试一条生产线上的恒温炉,就是因为在程序里加了个死区功能块,PV波动幅度小了一半以上,温控曲线才真正稳下来。
触摸屏上,也可以在画面里加一个滚动显示当前消抖时间的功能,让现场调试人员可以临时微调,不用每次连电脑编译下载。做非标设备时,这种“参数可调”的体验特别好,客户会觉得自己项目是“可配置”的,而不是写死的。
5.3 我的经验分享:功能块库维护与复用
最后分享一点我个人的习惯。我电脑里有个文件夹叫“功能块库”,按年份分类,文件名带上项目编号。每写一个新功能块,我会在同一文件里放三部分:功能说明、代码或梯形图、测试数据记录。测试数据记录里写清楚在哪个PLC型号上跑过、扫描周期大致多少、设了多少消抖时间、实际效果如何。
这样做的好处是,以后再遇到类似项目,直接调出来复制,改参数就能用,不用重新推导演算。而且遇到稀奇古怪的故障时,翻旧记录往往会发现,原来两年前就碰到过一模一样的现象,当时怎么解决的,直接找出来就少走弯路。
像消抖功能块这种基础组件,确实不显眼,但它决定了整套程序的上限。有一次我在活动项目头天晚上把所有输入信号都加了消抖,第二天调试人员说,很奇怪,现场电机动作变得非常有条理,以前那些莫名奇妙的报警全没了。我笑了笑,心想,这大概就是消抖功能块的隐形价值吧。
我自己的体会是,写程序最怕的不是功能有多复杂,而是忽略了那些最基础的工程细节。每次把这个小功能块加到自己的“每周一例”清单里,其实是在提醒自己:处理现场问题,先让信号干净了,后面的逻辑才谈得上准确。