ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

PLC编程ST语言循环选型:REPEAT与WHILE的三大差异及工程实践

PLC编程ST语言循环选型:REPEAT与WHILE的三大差异及工程实践 做 PLC 控制器编程的人十有八九都碰到过这个问题ST 语言里写循环WHILE 和 REPEAT 看着都能用到底选哪个才不出事早些年我在现场调试抱闸控制逻辑为了少写两行把一段 WHILE 改成了 REPEAT结果条件变量的初值刚好满足退出条件循环体还是先执行了一次电机输出当场抖了一下。从那以后我意识到这两个循环并不是“长得像”那么简单。理解它们之间的三处关键差异才能真正在项目里把 REPEAT vs WHILE 这个 ST 循环选型题目做对。这篇把这三处差异逐一拆开再用实际场景给出选型建议适合刚从 C 语言转到 ST、以及被循环卡死过的同行。1. 先把语法摆到一起ST 里两个循环的骨架和速查表1.1 两条语法只剩“条件位置”不同在 IEC 61131-3 标准里结构化文本ST的这两种循环写法非常固定// WHILE 循环 WHILE 条件 DO 循环体 END_WHILE; // REPEAT 循环 REPEAT 循环体 UNTIL 条件 END_REPEAT;从语法骨架上看WHILE 把条件放在循环体前面REPEAT 把条件放在循环体后面。于是很多人会形成一个直觉把 WHILE 的条件整体取反然后搬到 END_REPEAT 前面不就是一个 REPEAT 循环吗这个直觉对了一半。REPEAT 确实可以写成“WHILE 的取反版本”但它有一个绕不开的前提循环体至少会执行一轮。这个“第一轮”在某些场景下是甜头在某些场景下却是毒药。我在后文会反复围绕这一点展开。另外写惯了 C 的同事要注意ST 里的 WHILE 和 C 的 while 基本一致条件为真就继续循环但 REPEAT 后面的 UNTIL 是“条件为真就停循环”其实更接近 C 里 do-while 的反向条件。如果把 do-while 的条件原封不动抄到 UNTIL逻辑当场就反了。这个坑几乎所有从 C 转 ST 的人都踩过。1.2 一张速查表把差异先摆在台面上下表是我给团队培训时用的“循环选型速查表”你可以把它贴在工位旁边对比项WHILE ... DO ... END_WHILEREPEAT ... UNTIL ... END_REPEAT条件检查位置进入循环体之前执行完一轮循环体之后最少执行次数0 次1 次条件为 TRUE 时继续执行循环体结束循环条件为 FALSE 时结束循环继续执行循环体空数据/空缓冲区保护天然安全可直接跳过至少访问一次容易越界等价改写基准写法相当于 WHILE 条件取反但必须考虑“多执行一次”的副作用适合场景握手、轮询、前置门槛检查初始化、滤波采样、至少处理一次的数据流这张表里最关键的信息是第三行WHILE 的条件为真就是“接着干”REPEAT 的条件为真就是“马上停”。很多逻辑错误不是因为代码本身写错而是把“继续条件”和“终止条件”混在了一起。2. 差异之一条件在入口还是出口直接决定循环最少跑几次2.1 WHILE 是“先查岗再开干”REPEAT 是“先开干再收手”先看一个最简单但很典型的例子求 0 到 9 的整数和。用 WHILE 写是这样i : 0; sum : 0; WHILE i 10 DO sum : sum i; i : i 1; END_WHILE;用 REPEAT 写是这样i : 0; sum : 0; REPEAT sum : sum i; i : i 1; UNTIL i 10 END_REPEAT;如果 i 的初始值是 0两个循环最终得到的 sum 都是 45结果完全一样。差别藏在“如果 i 的初始值不是 0而是 10”的时候WHILE一进来看到 i 10 不成立直接跳过循环体sum 保持 0。REPEAT先执行一次sum : sum i此时会把 10 加进去sum 变成 10然后才检查i 10退出。这就是“最少执行次数”的差异WHILE 可以一轮都不跑REPEAT 无论如何都会跑一轮。这个差异在求和这种简单例子里看不出什么危害但放到设备和数据访问场景里很容易变成事故。2.2 空缓冲区REPEAT 到底会不会踩出数组越界我当年真正被 REPEAT 坑到是在读取一段可能为空的缓冲区时。假设控制器从总线上收到一组数据存进数组 arr数据长度是 len。我需要求这组数据的总和。如果 len 是 0表示这次没有数据WHILE 和 REPEAT 的行为完全不同// WHILE安全len0 时直接跳过 idx : 0; sum : 0; WHILE idx len DO sum : sum arr[idx]; idx : idx 1; END_WHILE; // REPEAT危险len0 时仍然会执行 arr[0] idx : 0; sum : 0; REPEAT sum : sum arr[idx]; // len0 时 arr[0] 可能是无效访问 idx : idx 1; UNTIL idx len END_REPEAT;有些 ST 编译器对数组下标越界不报错直接读出一块地址上的垃圾值有些编译器会触发异常让任务进入停机状态。无论哪种都不是你想要的“空数据处理”结果。更现实的一个场景是通讯缓存区。Modbus、CANopen、EtherCAT 这类总线上经常出现“本次周期没有任何新数据”。你用 WHILE 去读判断条件不成立循环体不执行程序干干净净往下走你用 REPEAT 去读哪怕缓冲区是空的也会先把最后一个残留数据读一遍或者读到一个已经失效的报文。这种错误不会每次必现但一旦出现查起来非常痛苦因为它和总线上数据的时序强弱相关。所以我的第一个选型建议是如果循环体访问的是外部数据、缓冲区、数组、指针先想清楚“空数据时要不要执行一次”。需要可跳过就用 WHILE必须保底执行一次才用 REPEAT。3. 差异之二WHILE 是“继续跑”条件REPEAT 是“停跑”条件别只加个 NOT3.1 只看语法会写反bRun 到底该配 TRUE 还是 FALSE很多初学者会犯一个错误改造循环时只把 WHILE 的条件机械地搬进 REPEAT 里。举例原始逻辑用 WHILE 写bRunning : TRUE; WHILE bRunning DO DoStep(); END_WHILE;这个循环的意思很清楚只要 bRunning 为 TRUE就一直执行 DoStep。如果想把这段改成 REPEAT正确的近似写法是什么很多人会这么写bRunning : TRUE; REPEAT DoStep(); UNTIL bRunning END_REPEAT; // 错bRunning 为 TRUE 时反而退出循环这段 REPEAT 的执行逻辑变成先执行一次 DoStep然后检查 bRunning如果 bRunning 为 TRUE 就退出。这和原始 WHILE 的意思完全相反。除非 DoStep 内部自己把 bRunning 改成了 FALSE否则 REPEAT 只会跑一轮就退出。正确的改法是在 UNTIL 条件里对原条件取反bRunning : TRUE; REPEAT DoStep(); UNTIL NOT bRunning END_REPEAT;但这依然有“至少执行一次”的差异如果进入循环前 bRunning 已经是 FALSEWHILE 会直接跳过而这个 REPEAT 版本仍然会调用一次 DoStep。所以严格的结论应该是REPEAT 想等价于 WHILE既要对条件取反还要保证循环体执行一次不会产生副作用。3.2 多条件组合别硬搬要用“终止条件”重新整理现场逻辑很少只有一个布尔变量更多是多个条件组合比如“设备运行中并且没有故障并且速度大于下限才继续处理”。用 WHILE 写很直接WHILE bRunning AND NOT bFault AND nSpeed 10 DO ProcessData(); END_WHILE;如果直接用 REPEAT 重写需要把整个进入条件取反也就是REPEAT ProcessData(); UNTIL NOT (bRunning AND NOT bFault AND nSpeed 10) END_REPEAT;展开后就是REPEAT ProcessData(); UNTIL (NOT bRunning) OR bFault OR (nSpeed 10) END_REPEAT;这里必须用德摩根定律处理不是简单地在每个子条件前面加个 NOT 就行。很多人会把NOT (bRunning AND NOT bFault)错写成NOT bRunning AND bFault结果整个循环的退出逻辑完全扭曲。我一般不会直接在代码里写这种复杂的 UNTIL 表达式。如果多个终止条件都要判断我会把它们拆成两行bExit : (NOT bRunning) OR bFault OR (nSpeed 10); REPEAT ProcessData(); UNTIL bExit END_REPEAT;这样可读性更好也方便在 bExit 里追加其他退出条件。注意REPEAT 在执行循环体之前不会先检查 bExit所以如果循环体执行本身有副作用上面这种做法还是不能帮你规避“多跑一次”的问题。要想完全规避只能把条件检查放到 WHILE 里或者在 REPEAT 外面先包一层 IF 判断。4. 差异之三条件初值成立时一个跳过一个照跑一轮——这是最容易埋雷的地方4.1 初值为 TRUE 的故障信号REPEAT 居然还先干了一次活我用一个抱闸控制的简化例子来说明第三个差异也是现场最容易炸的地方。假设程序里有这么一段逻辑当故障信号 bFault 为 TRUE 时绝不允许给执行机构输出。用 WHILE 写bFault : TRUE; // 故障信号在进入循环前已经成立 WHILE NOT bFault DO Actuator : TRUE; // WHILE 不会执行到这一行 END_WHILE;bFault 为 TRUENOT bFault为 FALSE循环体直接跳过执行机构不会动作。如果换成 REPEAT看会发生什么bFault : TRUE; // 故障信号在进入循环前已经成立 REPEAT Actuator : TRUE; // REPEAT 不管三七二十一先执行一次 UNTIL bFault END_REPEAT;REPEAT 会先把Actuator : TRUE执行一次然后才看到 bFault 为 TRUE退出循环。虽然最后退出了但执行机构已经在某个扫描周期里抖了一下。在安全相关逻辑里这种“多余的一次执行”是不可接受的。这就引出了 REPEAT 的另一个工程特性循环体里的副作用在第一次条件检查之前就已经发生。WHILE 可以通过前置条件把副作用挡在门外REPEAT 没有这个门。如果你的循环体会修改输出、写总线、改全局变量就要非常谨慎地使用 REPEAT尤其是在外部条件已经成立的情况下。我在选型时有一个习惯凡是循环体内写输出或者改变物理设备状态的默认用 WHILE。只有当循环体内只是纯计算、纯数据读取并且必须保底执行一次时才会用 REPEAT。4.2 循环内更新同一个变量WHILE 看头部REPEAT 看尾部第三个差异还有一个很实际的表现循环体内更新条件变量时两种循环对“最新值”的感知时间点不同。WHILE 每轮循环开始前读一次条件如果循环体内最后一行刚把条件变量改写掉那 WHILE 要等到下一轮顶部才能感知到REPEAT 在每轮循环结束后立刻读条件循环体内最后一行对条件变量的修改马上就能影响退出判断。举一个常见的队列处理例子// WHILE 写法 i : 0; bMore : TRUE; WHILE bMore DO ReadOneRecord(bMore); i : i 1; END_WHILE; // REPEAT 写法 i : 0; bMore : TRUE; REPEAT ReadOneRecord(bMore); i : i 1; UNTIL NOT bMore END_REPEAT;两种写法里ReadOneRecord 内部会更新 bMore。如果第一次调用后 bMore 就变 FALSEREPEAT 会立刻退出WHILE 则会在下一轮顶部检查时才退出。对于独立循环两者最终结果一致但如果这两段代码后面还跟着别的逻辑并且别的逻辑依赖“循环结束时 bMore 的状态”那么时序差异就会体现出来。这个特点还有一个更微妙的用途如果循环体内没有及时更新条件变量WHILE 会一直沿用进入循环前读到的旧值只要旧值满足条件它就继续跑REPEAT 则会一直沿用循环体末尾写下的最新值。所以对于“循环体内一定要先干活再判断”的任务REPEAT 天然比 WHILE 少一次头部条件判断代码结构上也更顺。5. 工程选型实例缓冲区、握手信号、初始化逻辑分别怎么选5.1 缓冲区和队列清空首选 WHILE通讯缓冲区、FIFO 队列、打印缓冲这类数据结构最怕“空读”和“越界”。你在清空队列时如果队列本来就是空的最好的结果是“什么都不做”最坏的结果是“读了一个不存在的元素还把空标志清掉了”。这种需求下WHILE 几乎是无脑正确的选择// 清空接收队列 WHILE NOT Queue.IsEmpty() DO item : Queue.Dequeue(); Process(item); END_WHILE;如果队列为空整个循环体一次都不会执行Process 不会被调用逻辑干净利落。你要做的只是保证在循环体里把队列向前推进防止死循环。5.2 初始化与固定轮次算法REPEAT 写起来最顺REPEAT 适合的场景通常有两个特征第一任务本身必须至少执行一次第二条件判断依赖循环体内部产生的结果。最典型的是传感器数据的多次采样求平均以及通讯端口的“读一帧读到合法帧为止”// 至少尝试一次读取读到合法数据才退出最多读10次 i : 0; bValid : FALSE; REPEAT ReadFrame(byData, bValid); i : i 1; UNTIL bValid OR (i 10) END_REPEAT;用 WHILE 写也不是不行但需要在 WHILE 之前先把 bValid 初始化成 FALSE并且把第一个读取动作重复写一遍或者借助标志变量让第一次强制进入。代码会啰嗦不少。在初始化逻辑里REPEAT 也非常好用。比如给一组轴做使能顺序要求“至少执行一次使能动作直到所有轴就绪”REPEAT EnableNextAxis(); CheckAllReady(bReady); UNTIL bReady END_REPEAT;这里的逻辑是先发出使能动作再去检查是否就绪。如果用 WHILE第一轮循环前 bReady 大概率是 FALSE也能进入循环但总让人觉得多了一个没有意义的前置判断。5.3 我在项目中的默认选型习惯讲了这么多给大家一个可以直接抄的选型流程先问自己一句话这个循环体在条件不满足时能不能跑一次如果跑一次会改变输出、写总线、破坏数据那就不能用 REPEAT。再看条件语义你的条件表达的是“继续做”还是“停下来不做”WHILE 后接继续条件REPEAT 的 UNTIL 后接终止条件别把方向搞反。最后看空数据处理外部数组、缓冲区、指针变量空的时候能不能安全跳过能用 WHILE必须在空时也执行一次兜底逻辑才考虑 REPEAT。这三个问题问完十有八九的循环选型都能定下来。剩下的就是防死循环了。6. 给循环加熔断器防死循环的计数器和条件快照6.1 循环为什么会在扫描周期里卡死ST 循环跑在 PLC 的扫描周期里不像上位机程序那样有独立的操作系统调度。如果 WHILE 或 REPEAT 的条件变量在循环体里一直得不到更新或者更新的值始终无法让退出条件成立循环就会一直占用 CPU直到扫描超时控制器触发看门狗复位。我见过最典型的卡死原因有三个条件变量是另一个通讯任务写入的但这轮扫描里通讯数据没更新循环体一直拿到旧值。循环体内忘记给计数器、指针加一导致退出条件永远无法满足。在 REPEAT 里把退出条件写反了条件一开始就为 FALSE 且永远不变化于是一直循环。无论哪个原因最终表现都一样控制器重启、设备停机、现场电话被打爆。所以循环必须带“熔断器”。6.2 常用兜底写法最大循环次数 状态退出我用得最多的写法是在退出条件里强制加一个最大循环次数。WHILE 版本i : 0; bDone : FALSE; WHILE (NOT bDone) AND (i 1000) DO DoBusWork(bDone); i : i 1; END_WHILE;REPEAT 版本i : 0; bDone : FALSE; REPEAT DoBusWork(bDone); i : i 1; UNTIL bDone OR (i 1000) END_REPEAT;这里的 1000 只是一个例子。实际取值要根据控制器扫描周期、循环体复杂度和实时性要求来定原则是“正常情况下绝对跑不到这个上限异常时能快速退出”。如果编译器支持 EXIT 语句也可以在循环体内主动跳出去WHILE TRUE DO DoStep(); IF i 100 THEN EXIT; // 超出保护次数强制跳出 END_IF; i : i 1; END_WHILE;注意 EXIT 在标准 IEC 61131-3 中是存在的但部分国产编程环境可能不支持或行为有差异。用之前先查一下目标 IDE 的手册别好不容易写完最后在某个兼容性环境里编译不过。6.3 条件快照避免同一个变量在循环里反复跳动最后一个经验是关于条件变量的“快照”。ST 程序是多任务并行执行的。如果一个循环条件变量在循环期间被别的任务修改你的循环可能上一轮检查还满足下一轮就不满足了甚至可能在 WHILE 和 REPEAT 的某个分支里看到不一致的数值。我常用的做法是在循环开始前把条件变量复制到一个局部变量里循环体内只用这个局部快照bCondCopy : bCond; // 进入循环前先拍照 i : 0; WHILE bCondCopy AND (i 100) DO RefreshFromSystem(bCondCopy); // 循环体内部主动刷新快照 i : i 1; END_WHILE;这样做的好处是循环对外部状态的变化是“可预测”的要么直到循环体主动更新快照要么直到下一轮循环前重新读取。虽然多写了一个变量但能少排查很多莫名其妙的时序问题。真正到了现场你会发现大部分内存相关的 bug 都不是循环语法写错而是循环条件变量来源太杂、在扫描周期里被多个任务同时改。给条件变量做快照等于给整个循环加了一个稳定的基准也让你在复盘代码时更容易说清楚“这个循环到底依赖谁的状态”。这种习惯养成了REPEAT 和 WHILE 的选型就不再是玄学而是有明确判断依据的工程决策了。
返回列表