1. 一台小型 PLC 里ATCH 到底挂在哪根链条上很多人第一次看到ATCH 指令是在编程软件的指令树里点开中断文件夹发现里面躺着 ATCH、DTCH、ENI、DISI 四个东西说明书上写着中断连接中断断开允许中断禁止中断然后就没了。真正上手写程序时才发现这四个指令之间的关系远比字面意思复杂——尤其是 ATCH它决定了什么事件发生时跳去执行哪段代码一旦填错程序不报错、不报警就是不按你想的方式跑。我接触过不少现场程序中断这块出问题的比例其实不低而且往往表现为偶发计数器偶尔多计一个数、急停偶尔慢半拍、通信偶尔丢一帧。这类问题的共同点是复现困难因为中断的本质就是打断正常节奏一旦节奏被打乱现象就变成概率事件。这篇内容围绕 ATCH 这一条指令展开把它的参数含义、与事件号的对应关系、和 ENI 的配合顺序、中断程序内部的写法边界以及几个能直接抄的场景代码讲透。适合已经能写基本梯形图、但中断用得不太踏实的朋友完全没接触过 PLC 的新手也能看懂因为我会把为什么要用中断这件事从头说清楚。文中涉及的事件号、特殊存储区地址不同代际的机型会有差异我会给出查表方法而不是让你死记数字——这一点非常关键抄错一个编号比不写更麻烦。1.1 轮询方式在什么情况下就不够用了PLC 的基本工作方式是循环扫描读输入、执行程序、刷新输出一圈一圈地转。这套机制对付绝大多数逻辑控制绰绰有余因为机械动作本身就慢几十毫秒的扫描周期完全跟得上。但它有个天生的短板——它只会问不会被通知。举个具体例子。一个增量编码器接在高速计数器上要求计到 5000 个脉冲时立刻切断输出。如果用轮询写法主程序每个扫描周期读一次当前值、和 5000 比较看起来没问题。可一旦扫描周期是 10ms而这 10ms 里编码器走了 200 个脉冲那么刚好等于 5000这个条件可能永远不成立——你读到的是 4950下一圈读到的是 5150等号两边擦肩而过。这就是典型的漏判。再比如一个宽度只有 2ms 的到位信号扫描周期 10ms 的话信号来了又走了程序根本没看见。轮询在这种场景下不是慢一点而是完全失效。中断解决的就是这个问题。它把我定期去问变成你有事叫我。硬件层面检测到指定事件立即打断当前正在执行的程序跳到一段专门的代码去处理处理完再回来接着干。响应时间从一个扫描周期缩短到微秒到毫秒级而且不会漏。这里要建立一个正确的心理预期中断不是万能的加速器。它加速的是事件响应不是程序执行。中断程序本身依然要占用 CPU 时间写得越长对主程序节奏的破坏越大。这个度怎么把握后面第 3 章会详细讲。1.2 ATCH 干的事只有一件把事件和程序对上号理解了被通知这个模型ATCH 就好懂了。系统内部有一张表左边是事件谁按的门铃右边是中断程序开门后执行哪段代码。上电时这张表是空的门铃响了也没人理。ATCH 就是往这张表里填一行把某个事件指向某个中断程序。它的伙伴DTCH干的是相反的事——把这一行擦掉。擦掉之后事件照样会发生但不会再有程序被调用。所以一条完整的中断链路是这样的硬件检测到事件 → 系统查表看有没有绑定 → 有绑定且全局中断处于允许状态 → 保存现场 → 跳到中断程序执行 → 恢复现场返回。ATCH 只管第二步里的查表环节它既不负责检测事件那是硬件和配置干的也不负责打开总闸那是 ENI 干的。这三件事任何一件没做到位中断都不会跑。我在排查现场问题时见过太多只写了 ATCH 就以为万事大吉的情况。指令写进去了编译通过下载运行然后盯着设备发呆——因为 ENI 没写全局中断是关着的那张表填了也白填。这个坑后面第 5 章会专门展开。2. 两个操作数填错了后面全是白忙ATCH 是一条带两个操作数的指令看起来简单但这两个位置填什么、怎么填是错误高发区。而且它不像数学指令那样填错了会算出明显离谱的结果ATCH 填错了往往看起来在运行只是中断该来的时候不来或者来了执行了别的东西。2.1 指令格式与中断程序的命名绑定在语句表里它的形式是ATCH INT, EVNT前面那个是中断程序后面那个是事件号。在梯形图里它是一个指令盒两个输入端分别对应这两项。第一个操作数填的是中断程序的标识不是随便一个编号而是你在程序块里实际建出来的那个中断程序。编程软件里新建一个中断程序时会自动给你一个默认名字通常是 INT_0、INT_1 这样递增这个名字就是你要填进去的东西。有些人图省事直接在指令盒里敲了个数字编译能过但实际指向的是编号对应的那个程序——一旦你后来删了重建编号顺序变了指向就错位了。用名字而不是用编号是减少这类隐性错误最省事的办法。中断程序本身有几个硬性约束新手最容易踩它没有参数也没有返回值不能像子程序那样传值进去。要交换数据只能走全局变量区。它不需要在主程序里调用写了 CALL 反而是错的它只由事件触发。它不能嵌套自己同一类事件在上一个中断还没处理完时又来了只能排队或者丢弃不会重入。第三条值得多说一句。同一时刻只能有一个中断程序在执行这条规则在这类小型 PLC 上是硬约束。所以你不用担心两个中断程序同时改同一个变量的问题但要担心排队排爆的问题——中断来得太密、处理得太慢队列满了就会丢事件。系统存储区里有对应的溢出标志位具体位置查手册调试阶段值得盯一眼。还有一个细节中断程序末尾不需要手工写返回指令编译器会自动处理。有些从别的平台转过来的朋友习惯性加一句结果要么编译报错要么产生莫名其妙的跳转。这个平台的规矩就是进去写业务出来交给系统。2.2 事件号不必死记按类别查表更快事件号是 ATCH 的第二个操作数也是最容易填错的地方。我的建议很直接别背数字背分类。因为不同代际、不同型号的机型事件号表是有调整的你背的那张表换个型号就可能全错而且错得很隐蔽——程序照跑只是触发源不对。正确的做法是按类别去找。这些事件大致分四类事件类别触发条件典型用途使用前提数字量输入边沿指定输入点的上升沿或下降沿急停、限位、外部触发只有少数几个输入点支持不是全部高速计数器当前值等于预置值、方向改变、外部复位定长切断、定位、测速必须先用高速计数器配置指令完成组态定时器中断固定的时间间隔到达周期采样、软启动斜坡、看门狗间隔值写在指定的特殊存储字节里通信口事件接收字符、发送完成自由口协议、自定义报文端口必须工作在自由口模式这四类里第一类有个特别容易忽略的限制不是所有输入点都能产生边沿中断。以常见的这类小型机型为例只有最前面的四个输入点I0.0 到 I0.3支持而且上升沿和下降沿是两个独立的事件要分别绑定。我曾经见过一个程序把限位信号接在 I0.5 上然后认认真真写了 ATCH 和 ENI跑起来就是不响应——不是程序写错了是这个点压根没有边沿中断能力。这种问题不看手册根本发现不了。第二类高速计数器事件前提是配置指令已经执行过。配置指令负责设定计数模式、预置值、当前值、复位方式等等中断只是到达预置值这个瞬间的通知。配置没做中断永远不会来。而且配置指令一般放在首次扫描标志SM0.1后面只执行一次这个顺序不能乱。第三类定时器中断有个反直觉的点周期不是靠给定时器装预置值设定的。这类中断有专门的间隔寄存器单位是毫秒取值范围 1 到 255。你把间隔写进去系统自动按这个周期触发不需要你再手动启动定时器。很多教程在这里讲得含糊导致有人又是装预置值又是启动定时器反而把周期搞乱了。第四类通信事件前提是端口工作在自由口模式。如果端口还挂在系统默认的通信协议上这些事件根本不会产生。2.3 一个事件只能绑一个程序后绑的会把前面的顶掉这条规则必须单独拎出来讲因为它不报错、不警告属于纯粹的逻辑覆盖。ATCH 的语义是设置不是追加。同一个事件号被 ATCH 两次第二次会把第一次的绑定直接覆盖而不是两个程序都执行。这个特性在写复杂程序时会造成很隐蔽的 bug——比如你不小心在初始化块和主循环里各写了一次 ATCH指向了不同的中断程序那实际生效的永远是后执行的那一次。反过来的情况是允许的同一个中断程序可以被多个事件共用。比如三个限位信号都需要跳到同一段处理逻辑可以 ATCH 三次三次的第二个操作数不同、第一个操作数相同。这样三个事件任意一个发生都进同一个程序。这个技巧在多个触发源走同一套处理流程时很好用。如果确实需要在运行时切换中断程序正确做法是先用 DTCH 解除绑定再 ATCH 新的。直接 ATCH 覆盖虽然也能生效但中间那一小段时间里的行为就不确定了能规避就规避。3. 从 ENI 打开总闸到中断程序执行完中间发生了什么把 ATCH 写对只是完成了登记。真正让中断跑起来还要过一道总闸还要面对什么时候响应这个时间问题最后还要在中断程序里守住几条边界。这一整条链路上任何一环出问题表现出来的现象都是中断不触发或者触发了但结果不对很难一眼看出是哪个环节断的。3.1 ENI 与 DISI 管的是全局不是单个事件这两个指令的关系可以类比成楼道里的总闸和每户的分开关。ATCH 是把某户的分开关打开ENI 是把整栋楼的总闸推上去。总闸不推分开关全开也没用。这类小型 PLC 上电后的默认状态是全局中断禁止。也就是说即使你上电就把 ATCH 全部写好了只要没执行 ENI中断一次都不会响应。这是很多人第一次用中断时最常犯的错误而且因为程序不报错特别让人抓狂。ENI 和 DISI 的典型用法有两种一种是初始化时打开全程不关。绝大多数应用都是这个模式在首次扫描标志后面写一次 ENI之后再不碰它。另一种是临界区保护——在某段绝对不能被中断打扰的代码前后夹上 DISI 和 ENI。比如一段需要连续更新多个相关变量的赋值逻辑如果中途被中断打断中断程序读到的就是半新半旧的数据。这种问题极难复现但一旦出现就是灾难性的。加个临界区几行代码就能规避。不过要提醒一句DISI 期间的时长要严格控制。如果关中断的时间超过了一个中断周期事件就会排队甚至丢失。我见过有人把 DISI 和 ENI 之间塞进了整个初始化流程几百毫秒结果就是所有中断全断。临界区应该是几行赋值这个量级不是一个功能块。还有一个配合关系要注意ATCH 和 ENI 的先后顺序。惯例是先 ATCH 再 ENI这样总闸推上去的时候表里已经有内容了。反过来写虽然也能工作但中间存在一个总闸开着、表是空的的窗口如果恰好有事件在窗口里发生就直接丢了。虽然概率极低但没有任何理由不按正确顺序写。3.2 中断响应延迟藏在扫描周期的哪个位置中断这个词容易给人错觉好像事件一发生 CPU 立刻就跳过去了。实际不是。中断的响应需要等当前正在执行的那条指令做完而 CPU 内部还有一些固定的开销。所以真实存在的概念是中断延迟它一般在一个毫秒以内的量级但会被主程序拖长。具体会被什么拖长几类典型情况长指令。比如大块的传送、复杂的浮点运算、大循环执行到一半是不能被打断的中断得等它做完。如果一个循环要跑几十毫秒那中断延迟就是几十毫秒。通信阻塞。自由口模式下的发送有的写法会等待发送完成这段时间 CPU 基本被占住。中断程序本身太长。这是最容易被忽视的——中断服务程序执行期间其他中断是排队的。如果第一个中断跑了 5ms而中断周期是 1ms那么队列不是在排队是在爆。系统存储区里的溢出标志会被置位但很多人从来不看那个位。所以一个健康的程序结构应该是主程序负责慢逻辑中断程序负责快响应两者之间用标志位解耦。中断程序里只做记录、置位、计数这类几个指令就能完成的动作复杂的判断和输出交给主循环。这个原则听起来简单实际写的时候很容易失控因为人在中断里写代码时会不自觉地顺手把事情做完。3.3 中断程序里动手脚的三条红线关于中断程序内部能写什么我总结成三条不能碰的红线都来自实际踩坑。第一条不要在里面做耗时运算和循环。字符串处理、大数组遍历、嵌套循环这些都不合适。中断程序的时间预算应该按几微秒到几十微秒来算超过一个数量级就要重新审视设计。第二条不要在里面等待某个条件成立。比如等某个输入变成 1 才继续、等通信回应、等标志位翻转。中断是一次性的执行流在里面等等于把整个系统挂住。而且等的那个条件很可能恰恰需要主程序去满足这就死锁了。第三条不要指望在里面设置的输出能一直保持。中断程序里用立即输出指令刷新物理输出点确实能立刻生效但下一个扫描周期主程序刷新输出时会整个覆盖。如果主程序里对应的输出线圈逻辑不配合你刷的那一下只维持了一个扫描周期。正确做法是在中断里置位一个内部标志由主程序根据标志去驱动输出。除了这三条还有几个实践中的经验中断程序里尽量不要调用子程序虽然技术上允许但调用层级带来额外的现场保存开销得不偿失中断程序和主程序共享的变量尽量用单个字节或单个字操作避免多条指令才能写完的复合结构如果中断里要修改某个计数器而这个计数器主程序也在读那就得考虑用影子变量让主程序读影子、中断写本体。4. 三个能直接抄的场景写法讲完原理落到能跑的代码上。下面三个场景覆盖了这类小型 PLC 上 ATCH 最常见的三种用法每一段都可以直接拿来改成自己的逻辑。代码用语句表表示梯形图对照着画即可。4.1 高速计数器到位中断定长切断与定位这个场景的需求很典型编码器跟着送料辊转走到设定的长度立刻动作误差要求在一个脉冲以内。轮询做不到必须用中断。初始化部分只在首次扫描执行LD SM0.1 MOVB 16#F8, SMB37 // 控制字节允许计数、写当前值、写预置值等 MOVD 0, SMD38 // 当前值清零 MOVD 5000, SMD42 // 预置值 5000 个脉冲对应实际长度需换算 HDEF 0, 0 // HSC0 配置为模式 0内部方向、单相计数 ATCH INT_0, 12 // 把当前值预置值事件绑到中断程序 ENI HSC 0 // 启动高速计数器中断程序里只做最短的动作// INT_0 LD SM0.0 S M10.0, 1 // 置位到位标志主程序据此切断输出 MOVD 0, SMD38 // 当前值清零为下一段做准备注意几个细节。第一中断程序里没有直接驱动输出而是置位标志由主程序去切断——这样输出状态和主程序的逻辑是一致的不会出现下一个扫描周期又被打开的情况。第二当前值清零放在中断里做比放在主程序里做更及时避免两次中断之间计数值累积。第三配置指令的各个特殊存储地址在不同机型上是不同的上面这些地址只是示例实际使用时必须查对应型号的系统手册。如果预置值不是固定的而是每段产品长度不同那就在主程序里算好新的预置值、写进对应的寄存器中断里清零当前值就够了。不要在主程序里频繁执行高速计数器配置指令那条指令会重置整个计数器容易丢脉冲。4.2 定时中断固定周期采样与软启动斜坡需要每隔固定时间做一件事用定时中断比在主程序里数扫描周期可靠得多因为扫描周期本身是浮动的。初始化LD SM0.1 MOVB 10, SMB34 // 定时中断间隔 10ms1~255ms1ms 分辨率 ATCH INT_1, 10 // 定时中断 0 事件绑到 INT_1 ENI中断程序里做一个简单的斜坡发生// INT_1 LD SM0.0 MOVW VW100, VW102 // 当前给定值送输出映像 INCW VW100 // 每个周期加 1 LDW VW100, 5000 MOVW 5000, VW100 // 限幅每 10ms 加 1加到 5000 需要 50 秒这就是一条最简单的软启动曲线。用定时中断的好处是时间基准完全独立于扫描周期哪怕主程序里某个扫描周期特别长斜坡的斜率也不会变。相比之下如果在主循环里用计数器累加负载一变斜率就变现场调试时经常遇到空载和带载速度不一样的怪现象根源就在这。定时中断的间隔取值有个实际约束间隔必须大于中断程序本身的执行时间还要留出足够余量。10ms 间隔配一个 1ms 的中断程序理论能跑但余量太薄一旦主程序里有偶发的长指令队列就开始积累。我的经验是中断程序的执行时间不要超过间隔的三分之一。4.3 输入边沿中断急停与一次性触发急停这类安全相关的信号用中断做记录是合适的真正的安全回路必须靠硬件软件只是补充。这里用 I0.0 的下降沿。LD SM0.1 ATCH INT_2, 4 // 事件 4 I0.0 下降沿不同机型编号需查手册 ENI中断程序// INT_2 LD SM0.0 S M20.0, 1 // 置急停发生标志 DTCH 4 // 立刻解绑防止抖动重复触发这里用了DTCH 做一次性触发这是个很实用的技巧。机械触点断开瞬间会有抖动如果没有去抖中断可能连进好几次把计数或者状态机搞乱。在中断里第一件事就解绑等于把只响应第一次这个语义写死了。需要重新启用时主程序在确认条件复位后再 ATCH 一次即可。同样的思路可以推广到复位后重新武装的各种场景。相比在主程序里用上升沿检测加标志位中断加解绑的方式响应更快而且不会因为扫描周期漏掉窄脉冲。5. 指令写了但中断不触发按这个顺序往下查中断不响应是这类问题里最典型的一类而且因为没有任何报错只能靠逻辑推。下面这个排查顺序是我自己习惯用的从最外层往最里层收基本能在十分钟内定位。5.1 先确认总闸和注册顺序第一件事永远是最简单的那个ENI 写了没有执行到了没有。别笑忘了写 ENI这个错误的出现频率高得离谱。检查方法很直接——在线监控看 ENI 所在的那条分支有没有被使能。如果 ENI 写在某个条件后面而这个条件不成立那它就没执行。第二件事ATCH 执行到了没有。如果 ATCH 写在首次扫描标志后面而程序是热启动或者运行中重新下载的首次扫描标志可能没有按预期触发。稳妥的写法是把它放在一个上电初始化程序块里由主程序在每次从停止切换到运行时执行一次。第三件事中途有没有被 DTCH 掉。这个隐蔽性很强尤其是从别人手里接过来的程序。全局搜一下 DTCH看有没有哪个分支会在运行时把你要的事件解绑了。我遇到过一次某个调试用的分支里留着 DTCH条件刚好在生产模式下偶尔成立结果就是大部分时候好用偶尔失灵。第四件事有没有被重复 ATCH 覆盖。同样全局搜 ATCH看同一个事件号是不是出现了两次。两次都执行的话后执行的那次生效前面绑的程序永远进不去。5.2 输入滤波与硬件配置把事件吃掉了如果注册和总闸都没问题中断还是不来那就要怀疑事件压根没产生。对于输入边沿中断最常见的杀手是输入滤波时间。这类 PLC 的数字量输入都有可配置的滤波默认值通常在毫秒量级。滤波的作用是把抖动和干扰滤掉但它同时也把窄脉冲滤掉了。如果你的信号宽度小于滤波时间硬件层面就当这个信号不存在中断自然不会有。这种情况在现场特别容易误判成程序问题因为用万用表量电压是对的用示波器看波形才发现脉宽只有一两毫秒。解决办法是在硬件组态里把对应输入点的滤波时间调小。代价是抗干扰能力下降所以这是个权衡不是无脑调最小。对于高速计数器中断要查的是配置指令有没有执行、参数对不对。重点看计数模式是否和实际接线匹配单相/双相、内部方向/外部方向、预置值有没有真的写进去、计数器有没有被启动。这几个任何一个不对事件都不会产生。对于定时中断检查间隔寄存器有没有被写、值是否在有效范围内。写 0 或者超过上限行为是不确定的。对于通信中断检查端口是否真的工作在自由口模式。挂在系统协议上的端口不会产生这些事件。5.3 中断程序自身把 CPU 拖住还有一种情况更隐蔽中断其实触发了但表现出来像没触发。典型现象是设备响应变得一卡一卡的或者主程序的其他逻辑开始异常。原因通常是中断程序执行时间过长导致主程序被反复打断有效执行时间被压缩。极端情况下中断频率高到主程序几乎跑不完一个完整扫描看门狗就会动作。判断方法在中断程序的开头和结尾各翻转一个标志位用状态表看这个标志位的高电平占比。如果占比超过一半说明中断占用的时间已经严重挤占主程序了。优化方向有三个把中断里的非关键逻辑挪到主程序降低中断触发频率比如定时中断间隔从 5ms 改成 20ms把中断里的大块运算改成置标志 主程序处理。第三条在多数场景下是最优解代价是响应多延迟一个扫描周期但很多应用根本不在乎这一个周期。6. 中断和主程序抢同一份数据时的处理方式中断用起来之后下一个要面对的问题就是数据一致性。中断可以在任何一条指令之后插进来这意味着主程序里读-改-写这三步之间被打断的概率不为零。这类 bug 的特点是复现率极低、后果却可能很严重。6.1 为什么中断里改的变量会被覆盖设想主程序里有这么一段逻辑读一个计数变量、加一、写回去。如果中断恰好在读之后、写之前发生并且中断里也修改了这个变量那么中断改完返回后主程序接着写回去的是它自己算出来的旧值——中断的修改被彻底抹掉了。这就是经典的竞态。它在单线程的 PLC 程序里看似不该存在因为中断在逻辑上是并行的。扫描周期越长、主程序里对这个变量的操作越多撞上的概率越高。另一个方向的覆盖同样存在中断里置位了某个输出标志主程序在同一个扫描周期里因为别的原因把它复位了表现出来就是中断明明进去了输出没动作。6.2 标志位加主循环处理的解耦写法最稳妥的模式是单向数据流中断只往里写主程序只读和处理两边不交叉修改。具体做法是把数据分成两层。中断层维护一组原始状态变量只做记录从不根据业务逻辑去改。主程序层在扫描开始时把这组原始状态整体复制到工作变量里然后基于工作变量做所有判断和输出。复制这个动作本身很快被打断的概率极低即使被打断下次扫描会重新复制不会累积错误。对于必须由主程序回写的数据比如已处理标记让主程序写一个独立的标志中断层只读这个标志不写。这样每个变量永远只有一个写者竞态就没有土壤了。有个细节值得注意复制的时候要保证是一次性完成的。如果原始状态分散在多个字节里复制过程中被中断打断拿到的就是半新半旧的数据。解决办法要么是在复制前后加 DISI/ENI 做临界区要么是把相关数据放在连续的地址区间里用一条块传送指令一次搬完。6.3 一次性中断与 DTCH 的配合最后补一个实战里很常用的模式一次性中断。有些事件只关心第一次发生后续的重复触发不但没用还会干扰逻辑。比如首次到位后锁定、故障首次出现时记录时间戳。这种场景的写法是ATCH 绑定 → 事件发生进入中断 → 中断里第一件事 DTCH 解绑 → 处理 → 返回。这样无论事件之后发生多少次都只有第一次会进中断。需要重新启用时主程序在确认条件恢复后再 ATCH 一次。这个重新武装的时机要选好——太早了会在条件还没稳定时就被再次触发太晚了会漏掉真实事件。我的习惯是放在状态机回到初始态的那个转换点上和其他复位动作一起执行逻辑上更清晰。这套模式配合前面说的标志位解耦能覆盖绝大多数中断场景。真正的难点从来不是指令怎么写而是想清楚哪件事必须立刻做、哪件事可以等一个扫描周期。想清楚了这个分工中断代码自然就短了短的代码不容易出问题出了问题也好查。我个人在这些年的调试里体会最深的一点是中断代码写得越少系统反而越稳。刚接触的时候总想在中断里把事情一次做完结果就是各种莫名其妙的时序问题后来慢慢养成习惯中断里只留三五条指令其余的交给主循环问题反而少了。这套思路放在别的控制平台上同样成立——中断是稀缺资源用在刀刃上别拿它当加速通道用。