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 // 定时中断间隔 10ms(1~255ms,1ms 分辨率) 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 一次。这个"重新武装"的时机要选好——太早了会在条件还没稳定时就被再次触发,太晚了会漏掉真实事件。我的习惯是放在状态机回到初始态的那个转换点上,和其他复位动作一起执行,逻辑上更清晰。
这套模式配合前面说的标志位解耦,能覆盖绝大多数中断场景。真正的难点从来不是指令怎么写,而是想清楚"哪件事必须立刻做、哪件事可以等一个扫描周期"。想清楚了这个分工,中断代码自然就短了,短的代码不容易出问题,出了问题也好查。
我个人在这些年的调试里体会最深的一点是:中断代码写得越少,系统反而越稳。刚接触的时候总想在中断里把事情一次做完,结果就是各种莫名其妙的时序问题;后来慢慢养成习惯,中断里只留三五条指令,其余的交给主循环,问题反而少了。这套思路放在别的控制平台上同样成立——中断是稀缺资源,用在刀刃上,别拿它当加速通道用。