PLC ST语言定时器实战:TON/TOF指令原理与工程应用 做PLC项目调试最头疼的往往不是逻辑本身多复杂而是设备动作的时序对不上。拿ST语言写定时器控制稍微有一点经验的人都绕不开TON和TOF这两个指令。TON是接通延时定时器IN端有信号了并不马上输出而是等计时到设定值才把Q置1TOF正好反过来IN端从1变0之后输出还会保持一段时间再断开。这两条指令能覆盖实际项目里八成以上的延时、防抖、顺序启动和断电保持场景。这篇文章我把ST语言里TON/TOF的实战用法拆开讲从指令原理、实例化写法到电机控制、报警防抖、气缸到位检测这些典型场景全部过一遍顺便把我在西门子TIA Portal和Codesys环境里踩过的坑也整理出来。适合刚开始用ST语言替代梯形图的电气工程师也适合准备用结构化文本做设备控制的自动化专业学生参考。1. 定时器家族和ST语言实战思路1.1 四条定时器指令的本质区别IEC 61131-3标准下PLC定时器核心就是TON、TOF、TP这三条再加上部分厂家扩展的TOFR断电延时保持型或TONR构成了完整的定时器家族。很多人学的时候只记住了名字没有真正理解它们的时间轴行为结果一上项目就搞混。我通常这样给同事解释TON是“信号来了我不动等时间到才动”TOF是“信号没了我不动等时间到才停”TP是“不管信号怎么变我只输出一个固定宽度的脉冲”。这三条指令的输入引脚都有IN和PT输出引脚都有Q和ET。IN是触发条件PT是预设时间Q是输出结果ET是已计时间。为了看清楚差异我把它们在同一个扫描周期内的行为做成了一张对照表指令IN变1瞬间IN保持期间IN变0瞬间IN保持0期间TONQ保持0ET开始计时Q保持0直到ETPT后Q变1Q立即变0ET清零Q为0ET为0TOFQ立即变1ET归0Q保持1ET为0Q保持1ET开始计时直到ETPT后Q变0TPQ立即变1ET开始计时Q保持1直到ETPT后Q变0不影响Q计时继续Q保持0这张表初看枯燥但调试时特别有用。我见过不止一次有人把TOF当成TON用结果停机信号一消失设备立刻停止完全没起到延时保持的作用。本质就是没搞懂“输出跟随输入”的关系。1.2 为什么用ST语言写定时器而不是梯形图很多老工程师仍然习惯用梯形图拖一个TON功能块这没有问题。但当你需要在同一台设备里管理几十个定时器或者要根据配方参数动态修改定时时间梯形图会变得非常臃肿。ST语言的优势在于能用数组、FOR循环和函数封装来批量处理定时器实例。举个实际例子一条包装线上有16个气缸每个气缸的动作延时都要单独可调。梯形图里你要拖16个TON块每一个都要单独连线改一个时间就要去触摸屏或程序里翻找。用ST语言的话声明一个TON数组写一个FOR循环所有气缸的定时器统一处理代码量直接少一半以上。而且ST语言的注释和命名规范可以让别人接手时快速看懂逻辑。另外ST语言在结构化封装方面几乎是碾压梯形图的。你可以把一套“启动延时停止延时故障报警”的逻辑封装成一个函数块内部用TON和TOF组合外部只需输入启动命令和参数。需要3台设备就实例化3次互不干扰。这种复用方式在大项目中简直是效率神器。1.3 不同平台下TON/TOF的调用差异在真正写代码之前有一个必须搞清楚的问题不同品牌PLC的ST语言对定时器调用语法不一样。我在西门子博途TIA Portal、Codesys汇川、信捷、施耐德等、三菱GX Works3里都写过语法上最大的区别是“是否需要手动实例化”。西门子博途中TON是以函数块的形式存在你在ST语言里写TON_Instance(IN : ..., PT : ...)之前必须先在接口区或全局DB里声明一个TON的实例比如TON_Instance : TON;。如果你忘了声明编译直接报错。Codesys系的做法类似需要在变量表里声明FB_TON_1 : TON;然后才能调用。一个容易踩的坑是同一个TON实例不能同时被两个不同的控制逻辑复用否则计时状态会互相干扰。三菱GX Works3的ST语法有点特殊它允许在ST语言里直接调用TON但要在变量声明中指定类型为TON_1 : TON;调用格式一般写成TON_1(IN : bStart, PT : tDelay, Q bDone, ET tElapsed);。和西门子一样实例必须先声明。1.4 别把单片机定时器和PLC定时器混为一谈写ST语言的人经常来自嵌入式背景容易把PLC的TON/TOF和STM32的定时器、51单片机的定时器搞混。我在带新人时反复强调单片机的定时器本质是硬件计数器靠中断和标志位实现精确延时PLC的TON是软件功能块它的计时依赖于扫描周期底层由系统任务调度完成。这不是说PLC定时器不精确而是它的最小计时单位受扫描周期限制。普通PLC的扫描周期一般在1ms到20ms之间TON的计时精度就是这个量级。如果你需要微秒级甚至纳秒级的延时PLC本来就不是干这活的你应该去找硬件定时器或者专用运动控制模块。反过来如果你拿单片机定时器做PT为秒级的延时又会占用中断资源浪费CPU。各自有各自的战场不能跨界乱用。2. 五个高频实战场景的代码与思路2.1 电机星三角启动的延时切换星三角降压启动是电机控制里最经典的应用之一。控制要求是按下启动按钮后电机先以星形接法运行6秒等电流降下来再自动切换到三角形接法这个延时就该用TON来实现。ST语言里我会这样写VAR bStartButton : BOOL; bStarContact : BOOL; bDeltaContact : BOOL; bStarToDeltaTimer : TON; tStarTime : TIME : T#6S; END_VAR // 接触器主逻辑 bStarContact : bStartButton AND NOT bDeltaContact; bStarToDeltaTimer(IN : bStarContact, PT : tStarTime); IF bStarToDeltaTimer.Q THEN bStarContact : FALSE; bDeltaContact : TRUE; END_IF;这里有个关键细节我把星形接触器的输出作为TON的输入这样只有当星形接触器真实吸合且没有切到角形时才开始计时而不是直接用启动按钮计时。两者在正常状态下结果相同但如果星形接触器因为故障没有吸合按钮一直接通用按钮计时就会在6秒后强行切角形造成严重故障。这个是星三角控制里最容易忽略的安全细节。实际项目中还要考虑接触器互锁和切换时间通常会在星形断开和角形吸合之间留一个几十毫秒的间隔避免电弧短路。这个间隔可以用另一个TON或固定延时实现。2.2 传送带缺料报警的防抖处理传感器信号抖动在现场太常见了。光电传感器检测传送带上的物料时如果物料边缘不规则或者传感器响应抖动输出信号会在短时间内反复跳动。直接拿这个信号去触发报警操作面板上会产生无数条假报警记录操作员很快就对报警失去信任了。解决办法就是用TON做一个延时确认VAR bSensorSignal : BOOL; bAlarmOutput : BOOL; fbMaterialMiss : TON; tConfirmTime : TIME : T#2S; END_VAR // 检测到无料状态持续2秒才报缺料 fbMaterialMiss(IN : NOT bSensorSignal, PT : tConfirmTime); bAlarmOutput : fbMaterialMiss.Q;这段逻辑的解释是传感器信号为FALSE即无物料后TON开始计时如果在2秒内传感器又变回TRUE物料来了则TON的计时被清零报警不输出如果2秒内物料始终没来Q变TRUE这时候才触发缺料报警。这个2秒的“防抖窗口”不是拍脑袋定的它要大于传感器最大的单次抖动时间同时要小于允许的最长缺料停机时间。我一般会把PT做成HMI可设置参数不同物料、不同皮带速度时现场调起来方便很多。防抖的本质是一种滤波TON在这里等效于低通滤波器的底层实现。2.3 风机停机后延时停止的断电保持有些工艺要求设备主机关停后冷却风机还要继续运行30秒带走残余热量。这种场景用TOF比用TON舒服得多。控制逻辑是主机运行信号从1变为0时TOF开始计时输出继续保持30秒计时结束后风机才停。VAR bMachineRun : BOOL; bFanRun : BOOL; fbCoolingFan : TOF; tCoolTime : TIME : T#30S; END_VAR fbCoolingFan(IN : bMachineRun, PT : tCoolTime); bFanRun : fbCoolingFan.Q;注意看这段逻辑的精妙之处当bMachineRun为TRUE时TOF的输出Q也是TRUE风机跟随运行当bMachineRun变为FALSE时TOF不马上断开输出而是让风机再保持30秒。整个过程完全不需要自锁电路或额外的保持继电器。这个模式在液压站控制中同样适用。液压泵停止后冷却风扇还要延时关闭操作方法一模一样。我接触的设备中很多厂家对这一块的进线逻辑写得很啰嗦用了自锁、辅助继电器、定时器组合其实核心就是一条TOF的事。2.4 气缸到位传感器的信号过滤气缸到位检测在自动化产线里极高频。磁性开关或者接近开关检测气缸活塞到位时可能会出现几毫秒到几十毫秒的误动作信号比如气缸刚启动瞬间的抖动、到位冲击产生的机械振动。如果直接把这些信号送进PLC做下一步动作触发可能导致整个工位程序错乱。解决方案很简单用一个TON对到位信号做延时确认VAR bCylinderInPlace : BOOL; bConfirmed : BOOL; fbInPlaceConfirm : TON; tFilterTime : TIME : T#50MS; END_VAR fbInPlaceConfirm(IN : bCylinderInPlace, PT : tFilterTime); bConfirmed : fbInPlaceConfirm.Q;这里的PT我通常设置为20ms到80ms。设置太大会影响节拍太小又过滤不掉机械冲击产生的杂波。但这里有一个反向陷阱气缸到位信号是持续保持的比如磁性开关在活塞到位后一直导通所以用TON做延时确认是合理的。但如果你的传感器输出的是脉冲信号本身只有一个扫描周期的脉宽那就不该用TON去过滤那会把正常脉冲也滤掉。这种情况应该用TP脉冲定时器或者直接沿检测。判断方法其实很简单信号是要保持还是只跳一下保持用TON/TOF滤波跳变用沿检测。2.5 多台泵顺序启动的错峰控制现场有三台水泵如果同时启动启动电流叠加可能造成电压跌落甚至跳闸。常规做法是让它们依次启动每台之间间隔5秒。用ST写这个逻辑比梯形图简洁得多。我直接用三个TON实例串联每个TON的输出触发下一台的启动VAR bStartCmd : BOOL; bPump1, bPump2, bPump3 : BOOL; fbPumpDelay1, fbPumpDelay2 : TON; tInterval : TIME : T#5S; END_VAR // 第一台泵立即启动 bPump1 : bStartCmd; // 第二台延迟5秒启动 fbPumpDelay1(IN : bStartCmd, PT : tInterval); bPump2 : fbPumpDelay1.Q; // 第三台延迟10秒启动复用第二台串联计时 fbPumpDelay2(IN : fbPumpDelay1.Q, PT : tInterval); bPump3 : fbPumpDelay2.Q;这里有个值得注意的设计选择第二台的计时条件是第一台的启动完成第三台又等第二台启动完成。如果某台泵有启动反馈信号用反馈信号作为下一台计时的起点更可靠比死板地用固定时间间隔更智能。实际我往往用“启动命令接触器反馈”作为上一台启动成功的判断条件失败则停止后续启动并报警。这个模式再延伸出去就是32台变频器分时启动的控制逻辑。我做过一个项目上位机同时给32台变频器发运行指令如果所有变频器同时上电启动瞬时电流冲击非常大。后来我给每个变频器的启动命令串了一个5秒间隔的TON总共32台就是155秒的启动错峰周期问题彻底解决。这里的核心原则是用定时器把同一时刻的大电流需求摊到不同的时间片上。3. 核心细节解析和实现要点3.1 IN、Q、ET的时间轴理解把TON的IN、Q、ET三条曲线画在同一张时间轴上能很直观地看出它们的关系。很多初学ST的人搞不清楚什么时候该用ET什么时候该用Q。Q是布尔量适合做逻辑判断和输出控制ET是时间量TIME类型适合做HMI显示和进度监控。TON的ET从IN为TRUE且Q为FALSE时开始累计到Q为TRUE时停在PT值。TOF的ET从IN变FALSE后开始累计到Q变FALSE时停在PT值。理解这个细节非常关键因为如果你在逻辑里判断ET是否大于某个值需要清楚这个条件只在特定阶段才满足。我在做配方切换时经常这样用用ET监测定时器的进度通过总线传给触摸屏显示“剩余时间”或者“当前计时进度”。把一个BOOL型的定时任务变成可视化进度条操作工看起来直观很多。注意把ET读到HMI时要做类型转换比如TIME_TO_DINT(fbTimer.ET) / 1000转成秒否则显示出来是一串毫秒数看着头晕。3.2 PT的数据类型和动态赋值TON的PT引脚类型是TIME但在项目中我经常需要根据工位类型或配方动态修改延时时间。如果直接把HMI传来的数值当作PT使用数据类型经常对不上。常见的做法是用TIME_TO_DINT或DINT_TO_TIME做转换。比如HMI传来一个整数变量iDelaySeconds范围是0到600赋给TON的PT时这样写fbDelay(IN : bTrigger, PT : DINT_TO_TIME(iDelaySeconds * 1000));这里乘以1000是因为TIME类型的最小单位是毫秒而HMI传过来的是秒。还有更隐晦的坑有些PLC的TIME类型底层是带符号的32位整数最大只能表示约24天多。如果你的任务需要超长延时比如按天计的过程定时就不该直接用TIME做运算需要自己用累计变量方式实现。3.3 定时器实例化时的常见错误实例化是ST语言定时器用得对不对的分水岭。很多人第一次在Codesys或博途里写TON会犯两个典型错误要么忘了声明实例直接用TON当指令调用要么在一个函数块里重复使用同一个TON实例。第一种错误编译器会直接报错很好解决。第二种错误特别隐蔽比如你在急停处理程序里用了一个TON实例做延时又在正常运行逻辑里用了同一个实例虽然两个逻辑互斥不会同时执行但定时器的内部状态并没有真正复位可能导致启动延时莫名其妙加速或失效。解决方法是每个设备、每个功能块都有自己独立的定时器实例千万别为了省变量空间而共用一个实例。我个人的命名习惯是“FB前缀设备名功能”比如fbConveyorStartDelay、fbPumpStopDelay一眼就能看出是哪个设备用什么功能。ST语言代码的可维护性在很大程度上靠命名规范支撑项目大了之后尤为明显。3.4 扫描周期对定时器的影响前面说过PLC的TON是软件定时器和扫描周期强相关。所以有一个现象在仿真环境里跑到毫秒级定时结果和真机上不完全一致。真机扫描周期如果波动大比如某个扫描周期内通信任务很多定时器的累计时间就会在这个周期内多走一点。对绝大多数工业控制场景扫描周期波动的影响可以忽略不计。但对要求高时序精度的场景我的建议是不要依赖普通TON改用运动控制模块或独立的高速计数模块配合外部时钟源。如果必须在PLC内做高精度可以选择中断任务或者将定时代码放在独立的高速任务中保证这个任务的扫描周期固定。还有一个容易被忽视的现象TON的Q变化不会在它所在的扫描周期中间生效。假定扫描周期10ms定时器在第3ms达成计时条件PLC会等到当前扫描周期结束、进入下一个扫描周期时才把Q对应的输出刷新为TRUE。这意味着输出动作最大可能有接近一个扫描周期的延迟。在设备联调时如果你的两个PLC之间靠定时器输出做硬接线握手这种延迟一定要心里有数。4. 常见问题与排查技巧实录4.1 定时器不启动的排查顺序定时器“不动”是我被问得最多的问题。排查时我习惯按下面的思路来先看PT有没有异常值如果PT是0或者负数TON会直接表现为Q一直为TRUE或不稳定很多现场问题其实是从HMI误操作把PT写成了0开始的。再看IN端是否为TRUE这一步用在线监控一眼能看出来。如果IN为TRUE但ET还是0多半是实例化问题——程序里调用的定时器和声明的定时器不是同一个或者调用了两次但参数冲突。最后看扫描周期是否异常如果程序中某个任务运行时间超长定时器的累计节奏会被拉长。4.2 输出闪烁的三大原因定时器输出Q反复跳动通常有三种原因。一是PT值设得太小导致延时确认窗口太短响应时间不够输入信号抖动一旦产生就传到了输出端。二是TON和TOF用反了逻辑造成了“开-关-开”的死循环。三是多个定时器共用实例比如相同的TON实例在不同分支中被重复调用最后一个分支的执行结果覆盖了前面的状态。遇到输出闪烁我通常先打开在线监控观察IN、ET、Q三者的曲线是否连续。如果ET在计时过程中被清零说明IN发生了瞬间翻转先解决输入信号问题如果ET正常增加但Q还是乱跳就要查看实例是否被多重赋值。4.3 常见问题速查表故障现象可能原因排查方法定时器始终不输出PT为0或负值在线监控PT值检查HMI变量定时器输出提前IN端信号提前变FALSE后又变TRUE看ET是否被清零查找输入抖动源定时器输出滞后扫描周期过长查看任务周期和循环时间监控两个定时任务互相干扰同一实例被复用检查程序中TON实例出现次数编译报“实例未声明”忘记了实例声明在VAR区补上FB实例声明Q不变但ET一直累加程序逻辑中没有给Q赋值使用检查输出端是否被其他逻辑覆盖TOF按成TON使用指令理解错误对照1.1节时间轴表重新确认4.4 三菱ST语句的IF格式坑如果你用的是三菱GX Works3写ST时有个容易被坑的地方三菱的ST定时器调用和IF语句配合时格式要求和西门子有些差异。常见的问题是把定时器调用写在IF分支里然后复用了局部声明的实例导致编译异常。三菱的ST基本格式是IF bStart THEN StTimer(IN : bStart, PT : T#3S); IF StTimer.Q THEN bOutput : TRUE; END_IF; END_IF;注意ST定时器实例StTimer需要在变量区声明类型为TON如果你在局部作用域重复声明同名实例编译时会提示重复定义。三菱的ST还要求每条语句以分号结尾漏了分号在编译时会定位到下一行经常让人误以为是下一行出了问题。另外三菱FX5U系列和Q系列对TON的支持程度不同FX5U支持IEC指令较全Q系列部分早期CPU可能不支持ST直接调用标准的TON需要查一下CPU的具体指令集。遇到这类兼容性问题最直接的办法是看程序里编译的指令数变化或者查该CPU的指令列表手册。4.5 定时器与通讯交互时的优先级问题当PLC通过Modbus或以太网与变频器、触摸屏通讯时定时器的表现可能会被通讯干扰影响。举个例子触摸屏写入PT的通讯指令如果是一个DWORD类型而PLC内部用WORD解析可能写入后PT值变成一个大得离谱的数字定时器表现为“永远不动作”。我排查过这样一个案例一台汇川PLC用Modbus RTU和上位机通讯上位机里写入延时时间地址是400014x保持寄存器PLC端用MOVE指令读入后赋给TON的PT结果定时器一直不触发。后来发现是因为上位机写入的是毫秒单位而PLC端程序用的是秒单位读入后直接使用导致PT被放大1000倍。解决办法是在PLC端做一次单位换算再赋给定时器。这个案例说明定时器的PT值必须经过严格的数据类型和单位验证尤其是跨设备通讯时不要相信远端传来的任何数值。5. 从单个定时器到完整控制系统的设计思路5.1 定时器组合构成的时序状态机单个定时器只能做“延时”这一个动作但把多个TON/TOF组合起来就能构建出复杂的时序状态机。设备节拍控制就是一个典型例子上料延时、夹紧延时、加工延时、检测延时、复位延时全部串在一起。我通常会给每个设备工位画一个时序图横轴是时间纵轴是各个执行元件的状态。即使你只用ST语言写代码画图这一步仍然建议要保留。上了时序图之后每个延时点用什么定时器、PT设为多少、输出怎么衔接一目了然。很多新人喜欢直接上手敲代码结果逻辑漏了一个环节后面调试花的时间比画图多得多。一个很实用的模式是“运行到下一工位的进度”用TON而“保持在当前状态的余量”用TOF两者反复交替就能搭出完整工序流。定时器在这里不是孤立的指令而是构成控制时序的基本时间单位。5.2 何时不用TON/TOF虽然TON/TOF覆盖了绝大多数情况但项目中依然存在不适合用定时器的场景。比如需要统计设备累计运行时长应该用RTC时钟或者累计加1的方式实现需要生成周期性方波建议用TP定时器或者在每个周期翻转一个BOOL需要在某个绝对时刻触发动作建议用RTC比较而不是从程序启动那一刻开始计时。另外一个容易误用的场景是“长延时”。如果需要延时1小时以上直接用TON的PT当然可以但在调试和测试时不方便。我会把这种场景拆成分钟级或秒级的小定时器用计数器累计这样既方便现场测试也能在HMI上显示剩余时间。5.3 批量处理定时器的ST技巧项目大了之后比如控制多台工艺参数相同的设备不希望每一台设备都复制粘贴一份定时器逻辑。ST语言里可以把定时器实例放进数组用FOR循环统一处理。下面是一个简单的示例思路以Codesys风格为例VAR i : INT; fbDelays : ARRAY[1..10] OF TON; bTriggers : ARRAY[1..10] OF BOOL; tDelayTime : TIME : T#3S; END_VAR FOR i : 1 TO 10 DO fbDelays[i](IN : bTriggers[i], PT : tDelayTime); IF fbDelays[i].Q THEN // 执行该设备对应的动作 END_IF; END_FOR;这种方式的好处是一段代码控制十台设备修改延时时间只改一个变量。缺点也很明显难以对单台设备做差异化处理。如果每台设备的延时时间不同可以把PT也改成数组比如tDelayTimes : ARRAY[1..10] OF TIME;初始化时逐项赋值。这个数组思路再扩展就是32台变频器通讯控制那种项目的定时器管理方案。虽然每台变频器的启动命令是独立的但是错峰延时逻辑完全可以用一个FOR循环跑完。代码量从几百行压缩到几十行维护起来舒服得多。5.4 仿真能解决什么问题解决不了什么问题几乎所有主流PLC开发环境都支持仿真。仿真对定时器调试的帮助很大因为你可以用电脑就验证TON/TOF的时序逻辑还能用示波器式的曲线视图观察信号变化不用去现场反复下载程序。但仿真也有明显的边界。仿真的扫描周期和真实PLC不一定一致仿真环境通常不模拟真实IO的抖动也不模拟通讯延迟。所以我的习惯是逻辑问题用仿真解决硬件电气问题必须到现场调试。尤其是定时器与传感器信号交互的防抖逻辑仿真时感觉完美无缺接上真机之后可能因为传感器响应时间超出预期导致逻辑判断结果完全不同。5.5 从定时器看ST语言的工程化思维写到最后想聊一个更深层的感受。TON/TOF只是ST语言很小的一部分但它们体现的工程化思维是通用的先明确输入和输出再选择合适的基本单元最后用可复用的结构组织起来。我在项目评审时经常看定时器部分就能大致判断一个程序员的水平。水平一般的会把延时逻辑散落在各个功能块里到处是乱起的变量名甚至直接使用全局变量操作定时器实例。经验丰富的会把定时器封装成带参数的功能块配合注释和状态枚举让后来人可以快速理解整台设备的时序逻辑。在ST语言的生态里定时器虽然是基础指令但要写得漂亮、可靠依然需要大量现场经验的积累。这篇内容把我的实战过程完整复盘了一遍希望能帮你少踩几个坑。