
写ST逻辑写多了你会发现一个很有意思的现象REPEAT和WHILE这两条循环语句新手纠结选哪个老手其实也在纠结。尤其做设备控制、数据处理和通信时序的时候选错一次轻则逻辑少跑一趟重则设备直接冲顶或者程序卡死。ST循环选型这件事真不是随便挑一个顺眼就能用的。今天我就REPEAT和WHILE的选型问题把底层逻辑、三处核心差异、实际场景和踩坑记录一次性讲透内容基于IEC 61131-3结构化文本的常见实现适合刚入门ST的电气工程师也适合正在优化老程序、想搞清楚循环行为的自动化从业者。1. 先把两个循环的底层逻辑对齐1.1 语法对照条件的位置决定行为在结构化文本ST里这两个循环的语法长得非常像但执行逻辑完全相反。先看标准写法// WHILE先判断条件为TRUE才进入循环体 WHILE (condition) DO statement1; statement2; END_WHILE; // REPEAT先执行一次循环体再判断条件 REPEAT statement1; statement2; UNTIL (condition) END_REPEAT;关键点就在这里WHILE后面跟的是“继续执行的条件”REPEAT后面跟的是“退出循环的条件”。很多初学者第一次看到REPEAT都会懵REPEAT不是“重复”的意思吗为什么UNILT条件满足反而跳出去我刚开始也这样后来把一个真实例子套进去就通了——REPEAT翻译成人话是“干这件事一直干到某个条件成立为止”。所以它的条件本质是终止条件不是继续条件。WHILE则完全不同它更像是“只要某个条件还成立就继续干”。条件在循环体执行前判断一次不成立就一次都不干直接跳过去。这个区别看似小实际工程里影响极大。后面我会专门说三处核心差异这里先把语法层面的“位置关系”刻在脑子里WHILE的条件在开头REPEAT的条件在末尾条件性质也不一样。1.2 两种思维方式先检查再执行 vs 先执行再检查用一个生活化的例子说明。WHILE像你早上出门前先看天气预报如果显示“有雨”你就带伞如果显示“晴天”伞这个动作一次都不用执行。REPEAT像你已经走出了门然后抬头看天发现下雨了才转身回家拿伞。也就是说REPEAT一定至少会“出一次门”WHILE可能连门都不出。这种思维模式直接对应到工程上就是某些逻辑需要“先确保现场状态安全再决定要不要操作”那就更适合WHILE某些逻辑需要“先让设备动一下再看反馈决定下一步”REPEAT更贴合。比如设备启动时你要先检查急停信号急停按下了就不该发启动指令这种场景用WHILE非常自然反过来通信握手时你要先把请求帧发出去再等待应答这种情况用REPEAT从语义上就比别人明白。说实话很多选型问题并不是语法不会而是业务逻辑的“执行起点”没想清楚。想清楚了选哪个循环其实就一句话的事。2. 三处躲不开的核心差异2.1 差异一最少执行次数0和1差别很大第一个差异也是最容易被忽略的WHILE最少执行0次REPEAT最少执行1次。WHILE如果一开始条件就是FALSE循环体整个被跳过。REPEAT不管条件是什么先跑一次循环体再说跑完才去看UNILT条件。这就带来一个非常直接的问题如果你用REPEAT去扫描一个空数组第一次循环体就会访问下标0。数组长度是0下标0根本不存在轻则读到脏数据重则直接越界报错。我早些年在一条包装线上就吃过这个亏。当时用REPEAT扫描一个产品数据队列正常情况队列里至少有一条数据所以从来没出过问题。有一天上游传感器故障队列是空的程序跑到循环里第一行就访问了空指针。在线监控一看任务卡死整条线停机。后来我把这段逻辑改成WHILE先判断数组长度大于0再进入循环扫描问题再也没有出现过。所以选型时你要先问自己如果数据源是空的、状态是FALSE、条件一开始就不满足我的循环体还能不能安全地从第一次开始执行如果不能老老实实用WHILE。如果业务里这个循环本来就要求“必须先执行一次”REPEAT反而是更准确的选择。2.2 差异二条件判断的时机不同第二个差异是判断时机。WHILE在循环体执行之前判断条件REPEAT在循环体执行之后判断条件。看起来只是顺序不同但放到实时控制系统里这个时机会直接影响设备行为。举个例子你要控制一个伺服轴点动找原点逻辑是“按下按钮轴开始运动直到原点开关信号到达”。用WHILE写WHILE (NOT b_HomeSwitch) DO b_MoveCmd : TRUE; END_WHILE;如果程序扫描到这一行时原点开关信号已经是TRUE那么WHILE条件不成立循环体一次都不会执行轴压根不会动。可用户明明按了按钮你希望的是轴先动起来再检测原点开关。这种情况用REPEAT就顺了REPEAT b_MoveCmd : TRUE; UNTIL (b_HomeSwitch) END_REPEAT;REPEAT不管怎样都会先给一次运动指令然后再循环检测到位信号。哪怕原点开关在上电初始状态就是TRUE它也会先输出一次运动指令再退出。运动控制里这种“先动作、再判断”的需求比比皆是这就是判断时机带来的行为差异。需要注意上面的代码里没有超时保护工程上绝对不能直接这么用后面我会专门讲。这里只看循环时机REPEAT确实更贴近“先执行、后观察”的设备语义。2.3 差异三条件逻辑要“翻译”着写第三个差异是条件方向。WHILE条件为TRUE时继续循环REPEAT条件为TRUE时退出循环。把一段WHILE循环改成REPEAT时条件必须取反否则逻辑完全变味。看一个简单的计数循环// WHILE版本i从0增加到9循环10次 i : 0; WHILE (i 10) DO i : i 1; END_WHILE; // REPEAT版本条件取反i 10 变成 i 10 i : 0; REPEAT i : i 1; UNTIL (i 10) END_REPEAT;表面上看这两个循环最终i都等于10。但别忘了REPEAT在判断前已经执行了一次i : i 1。如果i初始值是100WHILE一次也不跑REPEAT还会执行一次让i变成101。所以“条件取反”只是翻译的第一步还要考虑“是否允许至少执行一次”这个前提。实际调试中我见过很多次这样的案例有人把WHILE循环改成REPEAT只把条件从改成一边是继续条件一边是退出条件结果设备状态判断完全反了。要么一上来就退出要么永远退不出去。遇到这种问题先别查变量先检查条件方向是不是写反了。2.4 一张表把差异说清楚对比维度WHILEREPEAT条件性质继续条件TRUE才执行退出条件TRUE就退出最少执行次数0次1次判断时机循环体之前循环体之后空数据/空队列场景安全天然跳过有越界风险典型语义只要条件满足就做先做一次做到条件成立为止条件翻译原样使用条件需要取反理解典型场景数据扫描、安全联锁、条件任务状态刷新、通信发送、运动启动这张表是选型时最直接的参考。但记住工程上不是“REPEAT更高级”或者“WHILE更安全”而是要看逻辑本身是先检查还是先执行。3. ST循环选型结合场景看怎么用3.1 场景A扫描数组或队列先保护再处理在PLC里最常用的循环场景就是批量处理数组、队列、配方数据。这种情况下我强烈建议优先考虑WHILE因为WHILE天然支持“空集合保护”。假设要从一个数组里找第一个大于100的数值并把位置记下来nIndex : 0; nFindPos : -1; WHILE (nIndex ARRAY_LENGTH) DO IF (data[nIndex] 100) THEN nFindPos : nIndex; nIndex : ARRAY_LENGTH; // 找到后强制退出 ELSE nIndex : nIndex 1; END_IF; END_WHILE;WHILE循环在一开始就检查nIndex是否小于数组长度。如果数组长度是0条件直接不成立完美跳过。你不需要为“空数组”单独写IF判断因为WHILE本身就把这个保护做进去了。如果这里换成REPEAT即使你在循环体里写了很完善的判断第一次进入循环体时nIndex0访问data[0]这个动作已经发生了。空数组情况下data[0]本身就不存在这是结构性问题不是加几个IF能救回来的。所以我做集合扫描、队列读取这类逻辑时闭着眼睛选WHILE除非能百分之百保证集合至少有一个元素。3.2 场景B状态机里“至少动作一次”状态机是ST程序里的常客。很多状态机要求状态切换之后必须先执行一次输出刷新再根据反馈跳转。这种情况下REPEAT就非常顺手。举个例子设备有三个气缸要让它们在启动时全部缩回然后等待全部到位信号。到位信号可能上电瞬间就是TRUE也可能不是。我们希望的是“让缩回动作至少输出一次再检查到位结果”REPEAT bCylinder1Retract : TRUE; bCylinder2Retract : TRUE; bCylinder3Retract : TRUE; UpdateFeedback(); // 刷新到位信号 UNTIL (bCyl1Back AND bCyl2Back AND bCyl3Back) END_REPEAT;这段代码用REPEAT的好处就是无论到位信号初始状态是什么三路缩回输出都先建立起来然后才开始判断。如果用WHILE万一三个到位信号在程序扫描到循环那一刻恰好全是TRUE整个循环体一次都不执行气缸一根都不会动。设备维修时最怕这种“看起来逻辑没问题但就是不动作”的玄学故障。当然REPEAT里依然要加超时保护这点我后面会强调。先动作再检查这就是REPEAT相比WHILE不可替代的优势。3.3 场景C运动控制中的“先指令再反馈”运动控制里启动、停止、归零这类指令都有一个共同特点你得先把指令发出去才能等待反馈。如果你用WHILE去“等待”一个反馈很可能连指令都发不出去。看这一段简化的轴启动逻辑// 不推荐WHILE可能一次都不发指令 WHILE (NOT bAxisInPosition) DO bAxisEnable : TRUE; bAxisStart : TRUE; END_WHILE; // 推荐REPEAT强制先发指令再查到位 REPEAT bAxisEnable : TRUE; bAxisStart : TRUE; RefreshAxisStatus(); UNTIL (bAxisInPosition) END_REPEAT;WHILE版本在“轴已经处于到位状态”时不会执行循环体启动指令永远不会输出。这种问题在静态调试时很难发现因为你会觉得“轴都到位了启动不启动无所谓啊”。但实际设备上轴可能因为急停、报警或者抱闸反馈信号是假的到位你希望每一次启动都重新输出命令。REPEAT因为至少执行一次保证了“每次进入这段逻辑指令都会被刷新一遍”。这正是动作控制里非常需要的“可靠性”。设备动作不稳定时很多老工程师会刻意用REPEAT来做输出刷新道理就在这里。3.4 场景D通信等待与超时保护通信指令比如Modbus读写、自由协议收发也有典型的循环等待场景先发送请求再等待响应响应没来就继续等待直到收到响应或超时。这种逻辑REPEAT和WHILE都能写但REPEAT更符合“先发请求”的语义nTimeout : 0; bResponseReceived : FALSE; REPEAT SendRequest(); // 发送请求帧 ReceiveResponse(); // 尝试接收响应 nTimeout : nTimeout 1; UNTIL (bResponseReceived OR (nTimeout 100)) END_REPEAT;当然写进PLC里的循环不能靠CPU空转等待这只是一个逻辑示意。真正工程上建议用定时中断或状态机去管理通信超时而不是在同一个任务里用循环“死等”。但如果你确实需要在某个方法里短等待REPEAT比WHILE直观得多因为它的执行顺序和通信的业务流程完全一致先发再看不行就再发。4. 常见问题与排查实录4.1 REPEAT条件写反启动即退出或退不出去REPEAT最常见的坑就是把条件方向搞反。REPEAT是UNILT条件为TRUE时退出不是为TRUE时继续。我见过一个故障设备启动后程序应该循环执行注胶动作直到胶量到达设定值。有人写成REPEAT bInject : TRUE; UNTIL (nCurrent nTarget) END_REPEAT;本意是“胶量没到就继续注胶”。但REPEAT的逻辑是“胶量没到TRUE就退出”结果程序一进来只要当前胶量小于目标值立即退出了。设备根本不动。修的时候只要把条件改成UNTIL (nCurrent nTarget)就正常了。排查技巧很简单遇到REPEAT先在旁边写一行注释“条件为TRUE时退出”。然后对照业务业务说“当胶量达到设定值时停止”那么条件就是nCurrent nTarget和业务描述一致才不会错。4.2 死循环的三大原因死循环是ST程序里最让人头疼的问题之一。WHILE和REPEAT都可能死循环我总结三个最常见的根源第一循环体内没有更新条件变量。比如条件依赖nIncrease但循环体里忘记写nIncrease : nIncrease 1nIncrease永远不变循环永远出不去。第二比较方向写反。把WHILE nIndex nMax写成WHILE nIndex nMax如果nIndex初始为0nMax为100那条件一开始就是FALSE循环跳过看似不卡死但如果nIndex初始为200就成了死循环。第三用REAL浮点数做精确相等判断。ST里REAL是浮点数WHILE position target DO这种写法极不可靠因为浮点运算会有精度误差。正确做法是给一个死区区间WHILE (ABS(position - target) 0.001) DO position : position step; END_WHILE;用绝对值差判断接近程度好用又不会卡死。4.3 循环体里放延时和阻塞通信扫描周期失控很多新手会把延时功能块直接塞进WHILE或REPEAT里比如WHILE (bWait) DO TON_INSTANCE(IN : TRUE, PT : T#1000MS); END_WHILE;在PLC里这种写法非常危险。WHILE循环会阻塞当前任务扫描周期被拉长逻辑看门狗很容易超时CPU甚至会直接报故障停机。更麻烦的是有些老款PLC在循环里调用延时功能块循环外的定时中断已经跑了但主任务还在循环里出不来整个程序节奏全部乱掉。我的经验是PLC编程里尽量少用“长时间循环等待”。真正的等待应该用状态机加定时器去实现把“等待”拆成“每隔一个周期检查一次”。如果非要在某个块里用循环一定要设最大迭代次数一旦超限马上退出不要赌条件一定会满足。4.4 调试REPEAT时容易懵的位置在线调试的时候WHILE的断点通常停在DO所在行条件不满足时能很清楚地看到跳过。REPEAT的断点经常停在UNTIL那行或END_REPEAT的位置新手第一次找会一脸懵循环体执行完了断点怎么停在末尾这是因为REPEAT的判断发生在循环体末尾。你需要在UNTIL行看条件结果如果条件为FALSE程序会跳回REPEAT关键字后面的第一条语句继续执行如果条件为TRUE则跳出循环到END_REPEAT之后。在线监控时注意观察UNTIL那一行的布尔变量值FALSE表示还会继续TRUE表示这次循环是最后一次。另外循环变量监控也有技巧。REPEAT循环体的变量值在第一次进入时和WHILE一样都会在循环内部实时刷新。如果你在循环结束后才看变量看到的是退出后的值容易误判。建议把循环变量加到监视窗口单步执行时观察每一步变化。4.5 排查速查表症状可能原因解决方向循环一次都不执行WHILE条件初始为FALSEREPEAT条件写反检查条件方向确认业务是否需要“至少执行一次”循环停不下来循环变量没更新条件永远为TRUE浮点比较无死区在循环体内更新条件变量加最大迭代次数访问数组越界用REPEAT扫描空数组边界判断缺失换成WHILE或先判断数组长度程序扫描周期变长循环体内放延时、通信等待、大量计算避免循环内阻塞拆分状态机断点位置奇怪REPEAT判断在末尾到UNTIL行查看条件不要只看循环开头设备没有输出动作WHILE条件初始为TRUE循环体被跳过改用REPEAT强制输出一次或调整条件逻辑5. 选型之外容易被忽略的工程细节5.1 可读性WHILE优先但别勉强代码写出来是给人看的。团队维护的时候WHILE的“满足条件就执行”比REPEAT的“执行到条件成立为止”更容易被大多数人理解。所以在两种循环都能用、业务也允许的情况下我一般优先选WHILE因为后续接手的人翻代码的成本更低。但这不代表所有地方都硬凑WHILE。如果一个操作本质上就是“先执行一次再看结果”你用WHILE反而要加很多前置初始化逻辑还得小心翼翼保证它不会跳过关键动作。这种时候大大方方用REPEAT但一定要在注释里写清楚“这里强制先执行一次是业务需求”避免后人看不懂给你乱改成WHILE。5.2 不同控制器的ST实现不完全相同虽然都叫ST但不同品牌的PLC在循环实现上有一点差异。比如CODESYS、TwinCAT、Siemens SCL、三菱结构化文本对循环的支持大体一致但编译器的严格程度、是否允许在循环内部跳出、是否支持RETURN细节上是有差别的。举几个我在项目里实际遇到过的点有些控制器支持在循环体里用EXIT跳出有些则不支持只能用条件赋值让循环自然结束有些控制器对循环内部变量类型检查很严格REAL和INT混用会编译告警有些则很宽松。更隐蔽的是个别老款PLC的ST编译器对REPEAT的UNILT条件优化有问题在线监控时变量值已经为TRUE但程序还要多跑一次循环才退出。遇到这种问题没有别的办法只有一条原则移植到新平台前先写一段最小测试程序验证循环行为再放心用。这个习惯帮我避过好几次坑。5.3 给循环加安全护栏循环的安全护栏我分两道。第一道是迭代次数上限nCount : 0; WHILE (bNeedProcess AND (nCount 10000)) DO // 处理逻辑 nCount : nCount 1; END_WHILE;不管业务条件是否满足循环最大只会跑10000次从结构上杜绝死循环拖死CPU的可能。REPEAT也是一样在UNILT条件里加上OR (nCount 10000)或者用EXIT跳出。第二道是功能块或Task级别的看门狗。有些PLC支持循环块执行时间监测循环时间超限会报错。我在维护老设备时见过太多“程序运行几个月突然卡死”的案例最后原因都是某个循环在极端工况下退不出去。加迭代上限虽然治标不治本但至少能让CPU保持可控故障能早点暴露出来。5.4 性能上的一点经验循环性能往往被自动化工程师忽略。PLC的扫描周期是固定的你在循环里写得越多任务周期越长。我总结三个实用的小习惯第一把不依赖循环变量的计算挪到循环外。比如某个增益系数是固定的提前算好再进循环不要在循环体里反复做浮点乘法。第二循环体尽量短小。如果循环体超过二十行我就会怀疑是不是该把内部逻辑封装成一个功能块或函数。循环里只放核心处理其他放外面。第三条件里避免调用带副作用的函数。比如WHILE (GetStatus() 1) DO如果GetStatus函数内部会改变全局变量循环的每次判断都会产生额外影响排查起来特别困难。最好把状态先存入局部变量再进入循环判断。这些细节平时看不出差距但在轴控、高速数据采集这种对时序敏感的场景循环体内的几毫秒差异都会被放大。6. 一个例子把 REPEAT 和 WHILE 串起来6.1 问题定义等待多轴全部到位并带超时假设现场有3个伺服轴每个轴有一个到位信号。程序要完成这样一个动作按下启动按钮后给所有轴发送启动命令然后等待三个轴全部到位。如果5秒内没到位就报超时并停止。这个需求里最关键的一点是启动命令必须先建立然后才能等待到位。如果轴已经在到位状态也必须重新输出一次启动命令防止假信号。6.2 第一眼反应WHILE很合理但有个坑很多人第一反应会写bStartCmd : TRUE; nTimer : 0; WHILE (NOT (bAxis1Done AND bAxis2Done AND bAxis3Done) AND (nTimer 5000)) DO nTimer : nTimer 1; END_WHILE;这段代码的问题是如果执行到WHILE这一行时三个轴的到位信号恰好全部为TRUE循环体一次都不执行。虽然不会死循环但后续的超时判断、报警标志都可能漏掉。而且你看这段代码的第一行bStartCmd : TRUE是在循环外如果轴未到位循环体里没有刷新这个命令启动命令只建立了一次。更合理的做法是把启动命令刷新的动作放在循环体内让它至少执行一次。这正好是REPEAT的用武之地。6.3 用REPEAT重置这段逻辑bStartCmd : TRUE; nTimer : 0; REPEAT bStartCmd : TRUE; // 每个循环周期都刷新启动命令 RefreshAxisStatus(); // 刷新到位信号 nTimer : nTimer 1; UNTIL (bAxis1Done AND bAxis2Done AND bAxis3Done) OR (nTimer 5000) END_REPEAT; IF (nTimer 5000) THEN bAlarmTimeout : TRUE; END_IF;这段逻辑用REPEAT就非常合适不管轴在什么初始状态启动命令都至少被刷新一次。到位信号如果没满足继续刷新、继续计时。一旦三个轴都到位或者超时立刻退出。如果你非要用WHILE也不是不行但要确保循环体会先执行一次。可以改造成bStartCmd : TRUE; nTimer : 0; RefreshAxisStatus(); WHILE (NOT (bAxis1Done AND bAxis2Done AND bAxis3Done) AND (nTimer 5000)) DO bStartCmd : TRUE; RefreshAxisStatus(); nTimer : nTimer 1; END_WHILE;注意我在WHILE之前手动强制调用了一次RefreshAxisStatus目的是模仿REPEAT的“先执行一次”。但这样写很容易被后来维护的人误删因为他们会觉得这行多余。所以如果业务语义本来就是“先动作再等待”REPEAT会更自解释。6.4 怎么选型才不纠结到了这里选型标准其实已经浮出来了。你只需要问自己三个问题第一这个循环体在条件不成立时是否也需要执行一次是选REPEAT否选WHILE。第二业务描述里是“当XX时继续做”还是“一直做到XX为止”前者对应WHILE后者对应REPEAT。第三如果循环体执行次数为0会不会造成设备不动作或数据不处理会考虑REPEAT不会WHILE更安全。我自己在实际维护设备时的习惯是凡是要先输出命令再检测反馈的逻辑默认写REPEAT凡是扫描集合或条件性处理的逻辑默认写WHILE。这样定下来之后写循环基本不再纠结出问题的概率也低很多。最后再分享一个小技巧。写REPEAT的时候我会在UNILT条件的同一行加注释// 条件为TRUE时退出循环。这个注释看起来多余但对后来接手代码的人非常有帮助——因为REPEAT的条件方向和WHILE正好相反这是最容易看错的地方。如果你也经常在两个循环之间来回切换不妨试试这个习惯能省不少排查时间。