
说起来挺有意思很多人一听到“定时器”三个字第一反应是“这有什么好讲的”。可真到了现场调试TON、TOF、TP这三个最常见的标准功能块能把老手也绕晕。我见过有人把同一个定时器实例复制得到处都是结果设备上一会儿报警、一会儿不动作查了整整一天才发现是实例串扰也见过有人想用TON做“断开后延时”逻辑上直接短路KM1和KM3差点同时吸合。这些坑如果你只听“定时器很简单”这种话是永远踩不出来的。这篇文章就聚焦汇川InoProShop平台把TON、TOF、TP三种定时器的时序逻辑彻底拆开讲透再给出一套可以直接抄作业的工程落地写法。无论你是刚接触InoProShop的新手还是从三菱、西门子平台转过来的老工程师只要想把定时器用得明明白白这篇都值得花二十分钟读完。1. 为什么我觉得定时器值得专门写一篇深度实战1.1 定时器不简单的三个理由第一个理由定时器是所有逻辑控制的时间轴。设备里大半的控制逻辑都离不开“延时启动”“超时报警”“脉冲输出”而这三件事分别对应TON、TOF、TP。你要是只会其中一两个遇到具体场景就容易硬凑逻辑凑出来的程序又长又难维护。第二个理由InoProShop的定时器模型和你以前用过的平台可能完全是两套思维。三菱FX系列里定时器是T0~T255这种编号型资源梯形图里类似一个线圈而InoProShop基于CODESYS内核定时器是功能块要声明实例、要填TIME类型参数、要通过实例名访问Q和ET。这种转变对很多老工程师来说比想象中别扭。尤其是“复制粘贴功能块但忘了改实例名”这个操作直接能让程序随机乱跳。第三个理由TON、TOF、TP虽然引脚长得一样但时序行为差异极大。不知道边界条件不看复位时序写出来的程序在正常工况下可能没问题一到信号抖动、突然断电、快速启停这些边界场景就原形毕露。1.2 这篇你能得到什么我尽量把内容组织成一条完整的链路。你会看到TON、TOF、TP的逐帧时序拆解输入变化时Q和ET到底怎么动InoProShop里创建定时器实例的完整过程梯形图和结构化文本两种方式都写任务周期和定时器精度之间的关系以及为什么你的定时器会偏慢电机星三角启动、气缸超时报警、非对称闪烁、按键防抖、通讯心跳看门狗这五个拿来即用的场景代码实战中高发的几个问题以及我的排查思路。如果你是一个从来没把TOF当回事、总觉得TP只是“手册里的摆设”的工程师这篇文章就是为你准备的。2. TON/TOF/TP逐个击破时序逻辑深度拆解2.1 TON接通延时控制逻辑里的出场率之王TON的中文名一般叫“接通延时定时器”接口就四个引脚类型说明INBOOL启动计时信号PTTIME预设延时时间QBOOL计时完成输出ETTIME当前累计时间时序逻辑用文字描述大概是这样的初始状态IN为FALSEQ为FALSEET为0。t0时刻IN从FALSE变成TRUE计时开始ET从0不断增大。t1时刻ET达到PTQ变为TRUEET停在PT值上不再增长。之后只要IN一直为TRUEQ就一直是TRUE。一旦IN变回FALSEQ立刻变FALSEET立刻清零相当于整个定时器复位。这里有三个细节容易被忽略。第一TON是电平型延时不是脉冲型延时。输入断开就复位不存在“断开了还能继续计时”的说法。第二PT可以设成0如果PT0那么IN为TRUE后Q要等下一个扫描周期才变TRUE实际有“一个扫描周期”的固有延迟。第三TON没有独立的复位引脚。你想在外部强制复位只能把IN置成FALSE或者通过逻辑把IN关掉。TON最适合做什么电机延时启动、气缸超时报警、传感器信号防抖、顺序启动控制。简单说凡是“某个信号接通后等一阵子再做反应”的场景都优先考虑TON。2.2 TOF断开延时让信号多撑一会儿TOF是“断开延时定时器”接口和TON一模一样但行为正好反过来。继续用文字拆解时序初始状态IN为FALSEQ为FALSEET为0。t0时刻IN从FALSE变成TRUEQ立即变成TRUE同时ET清零。只要IN保持TRUEQ就一直为TRUEET保持0。t1时刻IN从TRUE变成FALSEQ不立即变FALSE而是继续保持TRUE同时ET从0开始增加。t2时刻ET达到PTQ才变为FALSEET停在PT值上。如果IN又变回TRUEQ立即为TRUEET清零。换句话说TOF记住的是“输入断开后已经过了多久”。这个特性让它特别适合做停机延时场景比如设备停止后散热风扇还要再转10秒、设备停止后润滑油泵继续工作5秒。有一个等价关系值得记住把TON的输入取反、输出再取反得到的行为和TOF基本一致。ST里可以这样验证(* 用TON实现TOF *) tonInternal(IN : NOT inputSignal, PT : PT_value); outputSignal : NOT tonInternal.Q;这个等价式在我实际测试中是成立的理解它对于从三菱转过来的工程师特别重要因为三菱里没有专门的TOF很多人都是拿定时器加常闭触点硬搭的。2.3 TP定时脉冲输出宽度只和PT有关TP是“定时脉冲”功能块很多人平时很少用它但它其实非常有用。时序逻辑如下初始状态IN为FALSEQ为FALSEET为0。t0时刻IN从FALSE变成TRUEQ立即变成TRUEET开始计时。t1时刻ET达到PTQ变为FALSEET停在PT值上。之后即使IN继续保持TRUEQ也不会再变TRUE。想让Q再次输出必须让IN先回到FALSE再产生一次上升沿。特别注意当Q正在输出期间如果IN先变FALSE再变TRUE这种“重触发”在标准TP上是无效的不会开启新一轮脉冲。一句话概括不管输入按多久输出永远是固定宽度的脉冲。这个特性在工控现场非常值钱。比如给扫码枪一个200ms的触发信号给相机一个固定宽度的曝光触发给蜂鸣器一个固定鸣叫时间都是TP的典型用法。但TP也有一个让很多人头疼的缺陷没有复位引脚。如果执行过程中你希望它立刻停止输出TP做不到。这时候要么用自定义功能块要么用TON来构造一个可随时复位的脉冲输出。我给出一个比较实用的TON写法(* 用TON构造可复位脉冲输出 *) tonPulse(IN : startCmd AND NOT resetCmd, PT : pulseWidth); pulseOut : startCmd AND NOT tonPulse.Q AND NOT resetCmd;当startCmd为TRUE时tonPulse开始计时pulseOut为TRUE计时到PT后tonPulse.Q为TRUEpulseOut自动变FALSE。如果在计时过程中resetCmd变TRUEtonPulse因为IN被断开而立刻复位pulseOut也立刻变FALSE。这样既保留了固定脉冲宽度又支持外部强制复位工程里更灵活。2.4 边界条件对比一个案例看三种定时器的差异为了让你直观感受三种定时器的差别用一个具体例子来对比。假设PT都是3秒输入信号在0秒时由FALSE变成TRUE1秒时变回FALSE2秒时又变成TRUE。TON0秒开始计时1秒输入断开计时器立刻复位ET清零。2秒重新开始计时一直要等到5秒Q才变TRUE。TOF0秒输入变TRUEQ立即TRUE。1秒输入断开Q保持TRUEET开始增加。2秒输入又变TRUEQ保持TRUEET清零。之后输入一直TRUEQ一直是TRUE。TP0秒输入上升沿Q立即TRUEET开始计时。1秒输入断开Q不受影响仍然TRUE。2秒输入又变TRUE因为Q还在TRUE阶段这个上升沿不触发新脉冲。3秒ET到PTQ变FALSE。之后输入降到FALSE再升TRUE才会产生第二次脉冲。这个例子建议你在InoProShop里实际跑一遍把Q和ET都拉到监控表里看比我写一万字都管用。3. InoProShop实操从拖放功能块到ST调用与任务配置3.1 在梯形图里创建定时器功能块实例InoProShop的库管理器默认加载标准库TON、TOF、TP都在里面。操作路径不复杂打开程序组织单元POU进入梯形图LD编辑界面。从右侧工具箱或功能块列表里找到TON直接拖到程序段网络中。系统会弹出实例名创建对话框输入实例名比如tonMotorStart。在引脚上连接变量IN接布尔量PT可以填T#3S这样的常量也可以接一个TIME类型变量。Q和ET建议都接上便于在线监控。这里有一条非常重要的经验实例名一定要起得有意义。ton1、timer2这种名字短期看省事等程序到了几十个定时器的规模你根本分不清哪个是哪个。我的习惯是“定时器类型缩写用途”比如tonStarDelay、tofFanDelay、tpScannerTrigger。还有一个避坑点同一个定时器实例在一个扫描周期里只能被调用一次。如果你在程序A和程序B里都写了同一个实例名InoProShop虽然不一定报错但执行顺序不同会导致结果完全不可控。这种问题特别隐蔽查起来非常浪费时间。3.2 在结构化文本ST里调用定时器ST语言的调用方式和梯形图不太一样但逻辑更紧凑。看代码VAR tonMotorStart : TON; startPB : BOOL; motorStartDelayDone : BOOL; motorStartDelayET : TIME; END_VAR (* 调用TON启动按钮按下且急停未触发延时3秒 *) tonMotorStart( IN : startPB AND NOT estop, PT : T#3S, Q motorStartDelayDone, ET motorStartDelayET );ST里调用功能块有几个规则输入参数用 : 赋值多个参数之间用逗号分隔。输出参数用 连接到变量。如果不想用某个输出比如ET可以直接不写这一行。调用后要使用Q值可以直接访问实例名点属性比如tonMotorStart.Q。如果只是想在某个IF条件里判断定时器是否完成可以这样简化tonMotorStart(IN : startPB, PT : T#3S); IF tonMotorStart.Q THEN (* 延时3秒后要执行的动作 *) END_IF每个定时器实例必须在变量表里声明过才能用。我习惯把定时器实例声明在POU的局部变量区这样只在该程序里生效不会跨POU串扰。3.3 任务周期与定时器精度的关系InoProShop里的定时器不依赖硬件定时器它是在任务周期调用时通过计算两次调用之间的系统时间差来累加时间的。所以定时器的最小精度约等于任务周期。假如任务周期设置为2ms那你的定时器理论上最多只能分辨2ms。如果任务周期被某些耗时操作拖长比如某个网络里做了大量通讯指令那定时器的实际时间会比设定值偏长。几条实操建议对时间精度要求高的逻辑放到固定周期任务里不要放到空闲任务或自由运行任务。不要在多个任务里调用同一个定时器实例否则时间基准会互相干扰。如果需要微秒级或更高精度的控制老老实实用运动控制的位置触发或硬件中断不要指望定时器。这部分很多工程师栽过跟头。我曾经遇到一个案例程序里有个1秒的延时结果实际到1.3秒才动作。查到最后是因为那个扫描周期里有一个Modbus通讯指令最长阻塞了300多毫秒。定时器本身没错错的是任务里塞了太多东西。3.4 TIME类型最常见的新手编译错误TIME类型是定时器的核心参数类型写起来有几个硬性规则。常量必须带前缀常见写法有T#3S表示3秒T#500MS表示500毫秒TIME#2S500MS表示2.5秒T#1H30M表示1小时30分直接写500或者0.5编译必报错。如果你要从HMI上读取秒数再转成TIME代码可以这样写ptTime : UDINT_TO_TIME(hmi_seconds * 1000);这里hmi_seconds是用户在触摸屏上输入的整数秒乘1000变成毫秒再用UDINT_TO_TIME转成TIME类型。如果HMI上要支持小数秒可以读取实数再转换但要注意浮点数转换后可能会有一两个毫秒的误差对大多数逻辑控制没影响。TIME类型的本质是一个32位无符号整数单位是毫秒最大值约49.7天。如果你要设一个以天为单位的延时要小心不要超过上限。4. 工程落地五个拿来即用的典型场景4.1 电机星三角启动的完美时序星三角启动是电机控制里的经典逻辑。难点不在“星形转三角”而在“星形接触器先断开、三角形接触器后吸合”之间必须有足够延时否则三相电源直接短路。代码实现(* 星三角启动控制 *) tonStar(IN : startCmd, PT : T#3S); starContact : startCmd AND NOT tonStar.Q; tonDelta(IN : NOT starContact AND startCmd, PT : T#300MS); deltaContact : tonDelta.Q AND NOT starContact;逐行分析一下startCmd为TRUE后tonStar开始计时starContact为TRUE电机星形启动。3秒后tonStar.Q变为TRUEstarContact变FALSE星形接触器断开。此时tonDelta的输入变为TRUE开始300ms延时。300ms后tonDelta.Q为TRUEdeltaContact为TRUE三角形接触器吸合。由于deltaContact强制了AND NOT starContact即使逻辑上有极端情况两个接触器也不可能同时为TRUE。现场调试时我建议把3秒和300ms都放到HMI变量里不同负载的电机启动时间差异很大这样不用改动程序就能调整。4.2 气缸超时报警与自锁复位气缸动作不到位是设备维护的高发故障。一般来说气缸伸出指令给出后如果5秒内没有收到到位传感器信号就应该报警。(* 气缸伸出超时报警 *) tonCyl(IN : cylOutCmd, PT : T#5S); cylTimeout : cylOutCmd AND NOT cylDoneSensor AND tonCyl.Q; (* 报警自锁和复位 *) alarmCyl : (cylTimeout OR alarmCyl) AND NOT resetPB;逻辑不复杂但有个细节值得注意tonCyl的IN接的是cylOutCmd所以一旦气缸伸出指令取消定时器立刻复位报警条件也随之消失。为了防止报警一瞬而过我加了alarmCyl自锁。复位时按下resetPBalarmCyl清掉。但这里有个体验问题如果复位按钮一直按住alarmCyl会一直被复位按钮松开后才可能再次报警。有些设备希望“按一下”就算复位那就要做上升沿复位IF reset_edge THEN alarmCyl : FALSE; END_IF4.3 非对称闪烁灯快闪慢闪自由配指示灯闪烁是几乎所有设备都有的需求。用两个TON可以实现非对称闪烁比如输出亮0.5秒、灭1.5秒。(* 非对称闪烁输出0.5秒亮1.5秒灭 *) tonHigh(IN : runFlag AND NOT tonLow.Q, PT : T#500MS); tonLow(IN : tonHigh.Q AND runFlag, PT : T#1500MS); lampOut : tonHigh.Q AND runFlag;循环过程是这样的runFlag为TRUE后tonHigh开始计时lampOut为TRUE持续0.5秒0.5秒后tonHigh.Q变TRUEtonLow开始计时这时注意tonHigh的输入变成了FALSE因为tonLow.Q还没有变TRUE所以tonHigh立即复位Q变FALSElampOut熄灭1.5秒后tonLow.Q变TRUEtonHigh的输入变成TRUE重新开始计时同时tonLow的输入因为tonHigh.Q为FALSE而变FALSEtonLow复位Q变FALSE于是又开始新一轮。如果你想要对称闪烁把两个PT设成相同值即可。这个结构比用特殊脉冲指令更清晰参数也直观。4.4 按键防抖与短信号整定机械按钮在按下和释放的瞬间会因为触点弹跳产生几毫秒到十几毫秒的抖动信号。如果直接采集PLC可能会误判成多次按钮动作。TON的延时特性天然具备防抖能力。tonBtn(IN : rawButton, PT : T#50MS); buttonClean : tonBtn.Q;只有按钮信号持续保持50ms以上tonBtn.Q才会为TRUE。抖动产生的短脉冲在第50ms之前就被“过滤”掉了。防抖时间的选择要按实际手感来。设太小比如5ms防抖效果可能不彻底设太大比如1秒操作人员快速按一下按钮就没反应。一般机械设备上20ms到100ms是比较合理的区间。如果是安全相关的急停信号不要依赖软件防抖请用硬件回路。4.5 通讯心跳看门狗设备与上位机、伺服或从站通讯时经常需要监控“数据是否还在更新”。做法是让通讯对端周期性地发送心跳信号PLC侧判断如果超时没有新心跳就报通讯故障。心跳看门狗用TON实现起来需要一点技巧因为TON在输入为FALSE时会复位所以不能直接把心跳信号接进TON的IN。我用的是“边沿短暂拉低”的思路(* 心跳上升沿信号hbtEdge来自心跳信号的边沿检测 *) wdtTimer(IN : NOT hbtEdge, PT : T#2S); comAlarm : wdtTimer.Q;正常通讯时每2秒内必然有一次心跳上升沿hbtEdge每次边沿把wdtTimer的IN拉低一个扫描周期ET清零重新计时。如果超过2秒没有任何边沿wdtTimer的Q变成TRUE触发通讯报警。这个写法本质上是把TON当成一个“可复位的看门狗”现场实测非常好用。需要注意心跳信号本身必须是边沿变化的不能是恒定的TRUE或FALSE否则检测不到“数据没有更新”这个问题。5. 常见问题与排查技巧实录5.1 定时器不工作从哪几个方向查现象可能原因排查方法定时器完全不触发IN没有接通、实例未正确分配在线监控IN和PT看是否为TRUE、PT是否有效Q一直为TRUEIN一直为TRUE没有复位逻辑检查IN的控制逻辑看是否需要增加断开条件ET不增加任务没有周期运行或者实例被多处调用检查任务状态搜索实例名确认没有重复调用定时时间不准任务周期太大、任务过载查看任务周期把耗时操作移出该任务程序一运行逻辑就乱多个位置共用同一个实例全局搜索实例名把重复引用的地方拆分实例5.2 实例复制粘贴引发的“鬼影”问题这是CODESYS平台上很典型的一个坑。你在梯形图里复制了一个TON粘贴到下一段时InoProShop可能不会强制让你新建实例名结果两个网络都在调用同一个实例。由于程序的执行顺序是自上而下的第一段的输出会直接影响第二段定时器的状态表现出来就是“第一个网络刚触发第二个网络的Q也变了”。预防方法很简单复制粘贴功能块后一定检查实例名需要的话手动改成新的实例名。我自己的习惯是遇到需要多个相同延时逻辑的情况直接声明实例数组用循环来处理从根上避免复制问题。示例代码VAR tonArray : ARRAY[1..8] OF TON; inputSignals : ARRAY[1..8] OF BOOL; outputSignals : ARRAY[1..8] OF BOOL; i : INT; END_VAR FOR i : 1 TO 8 DO tonArray[i](IN : inputSignals[i], PT : T#1S); outputSignals[i] : tonArray[i].Q; END_FOR这样8个定时器共用一个循环结构参数统一关联变量也很清晰。当你的设备有8个工位、8个气缸、8个报警通道时这种写法能省下一大堆重复代码。5.3 如何在HMI上动态调整定时时间现场调试时定时参数经常要反复改。如果PT每次都要改程序再下载效率太低。建议把常用定时器的时间参数做成HMI变量。ST里这样写tonMotorStart(IN : startCmd, PT : pt_motor_delay, Q motorReady);pt_motor_delay是一个TIME类型变量可以在HMI上绑定一个数值输入控件但HMI直接编辑TIME类型不太方便。我一般会让HMI输入“秒”或“毫秒”的整数然后在PLC侧转换(* HMI输入秒数动态刷新PT *) pt_motor_delay : UDINT_TO_TIME(hmi_motor_delay_sec * 1000);要注意单位换算和溢出问题。HMI上输入的数值太大换算成毫秒后超过TIME类型的上限程序会报错或者时间不正确所以HMI端要加一个合理的上下限。5.4 TON/TOF/TP的复位条件速查三者的复位条件不一样用表格记一下定时器复位方法复位后的状态TON将IN置为FALSEQFALSEET0TOF将IN置为TRUEQTRUEET0TP无外部复位引脚只能等计时结束如果输入一直不变无法强制复位如果你需要在逻辑中强制复位TP直接改输入信号不现实建议用自定义功能块封装一个带复位端的TP或者用我前面提到的TON构造法。5.5 封装一个带复位端的TON标准TON没有复位端这个设计让很多从三菱转过来的工程师不习惯。所以我习惯自己封装一个TON_RST功能块FUNCTION_BLOCK TON_RST VAR_INPUT IN : BOOL; PT : TIME; RST : BOOL; END_VAR VAR_OUTPUT Q : BOOL; ET : TIME; END_VAR VAR tonInternal : TON; END_VAR tonInternal(IN : IN AND NOT RST, PT : PT); Q : tonInternal.Q; ET : tonInternal.ET;RST为TRUE时tonInternal的IN被强制断开定时器复位Q变FALSEET清零。RST释放后如果IN仍为TRUE则重新开始计时。这个封装在需要“故障复位后重新开始延时”的场景特别有用比如油泵启动后欠压报警复位后要重新计时延时。6. 最后再说几句定时器背后的工程思维根据我这些年现场摸爬滚打的经验定时器用得好不好往往不是看你会不会背时序图而是看你能不能根据现场需求选出最合适的那一种。启动延后找TON停止延后找TOF固定脉宽找TP超时报警用TON加条件判断心跳看门狗用TON加边沿信号。这几个套路看起来简单但真到了设备满负荷运行、信号乱飞的时候选对类型比堆一堆复杂逻辑靠谱得多。还有一个小建议凡是需要现场调整时间的参数尽量做成HMI变量不要写死在程序里。设备的机械磨损、负载变化、调试阶段的不同需求都会让同一个延时参数需要反复调整。程序里留好接口现场少改代码这是很多成熟工程师的共识。最后再分享一个我的心得体会如果一段PLC程序里定时器数量超过十个我会停下来重新审视一下程序结构考虑是不是应该用状态机或者数组统一管理。定时器本身不复杂复杂的是它们之间的关系。理清关系程序才真正经得起现场考验。