news 2026/10/13 5:37:39

ST功能块800行拆不拆?PLC代码拆分逻辑与判断标准

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
ST功能块800行拆不拆?PLC代码拆分逻辑与判断标准

朋友发来一张截图,问我:一段ST功能块里写了800行,看着就头皮发麻,是不是该拆成20行一段?我回他:你这不是在问行数,你是在问这程序还想不想让人维护。

ST(Structured Text,结构化文本)是IEC 61131-3标准里应用最广的文本编程语言,西门子平台里叫SCL,TwinCAT、Codesys、罗克韦尔里叫ST,语法细节略有不同,核心逻辑都是同一套。很多PLC工程师写ST的习惯是把它当成"能写IF的脚本",一个功能块里从头写到尾,五六个气缸、十几个传感器、一堆报警全部平铺在一个FB里,最后代码行数一路冲到七八百。年轻工程师可能觉得"能跑就行",但等设备到了现场调试、客户提了个新需求、或者半夜报警却定位不到原因,你就会发现800行一段的代价远超想象。

这篇文章不是教条地告诉你"必须20行",而是把大段ST和小段ST的真实差异摊开来讲。适合正在写PLC程序、SCL程序,或者在团队里做代码规范的人参考。我会把拆分逻辑、实操步骤、踩过的坑都写清楚,也会给出我这些年验证过的判断标准。

1. 800行一段的ST程序,问题从来不只在"行数多"

1.1 人的工作记忆装不下这么多分支

人的大脑在阅读代码时,本质上是在维护一个"状态栈":每看到一个IF,你就要记住这个条件成立时改变了什么;每看到一个变量,你就要追溯它上一次在哪里被赋值;每看到一个定时器,你还要记住它触发之后流程会跳到哪个分支。

一个800行的ST块,假设它里面有40多个IF分支、100多个变量、若干定时器和计数器,读代码的人即便拿着放大镜,在电脑前坐一个小时,也很难把这个逻辑完整装进脑子里。认知科学里有个著名的"7±2"法则,说的是人在某一瞬间能记住的组块数量大约只有5到9个。800行代码里的信息量,早就远远超出了这个上限。大多数时候你读完前200行,再看后600行时已经完全忘了前面的变量被谁改过。

这不是工程师水平问题,是人类的生理限制。

1.2 真正致命的是"改一处、崩三处"

行数多本身不致命,更致命的是大段代码几乎必然伴随高耦合。所有变量都在同一个作用域里,所有分支都共享同一个状态空间,任何一个局部参数都有可能被多个不相干的动作引用。

我说一个很常见的场景:某个设备块里有一个定时器,用来控制气缸推进超时报警。某天工艺调整,你觉得2.5秒太短了,把PT改成3秒。结果第二天发现,另一个本来互不相关的吹气动作也跟着变了时序。为什么?因为当时写代码的人图省事,在这个大块里复用了同一个定时器的输出信号作为吹气动作的触发条件。你改的是气缸的定时,连带的却是吹气的动作。

这种"改一处、崩三处"的事故,在大块代码里几乎每周都在发生。因为耦合关系藏在几十个IF分支和一百多个变量里,没有任何工具能帮你一眼看清"这个变量到底被谁依赖"。

1.3 我见过的大段代码长什么样

有一次我接手一个物流设备项目,某个设备控制块整整850行。功能上它包含了5个气缸的顺序控制、3个光电传感器的到位判断、2路真空压力监控、一个手动自动切换逻辑,外加所有报警的输出。这些功能被平铺在一个FB里,用二十多层IF嵌套串联起来,局部变量多达七八十个,命名也从v1、v2一直到v30。

最折磨人的是报警排查。有一次现场报"气缸未到位",我的第一反应是去查气缸超时报警那段逻辑,结果发现超时报警的状态位被后续一个IF又覆盖了,翻了三层才找到真正的原因。那次排查花了我将近两个半小时,其中真正的问题只花了五分钟就能解决,剩下的时间全部浪费在"这行代码到底会被谁执行到"的推理上。

从那天之后我就彻底明白了:800行一段的ST不是技术问题,是项目管理隐患。

2. 20行一段为什么被推崇:耦合、内聚、接口与可测试性

2.1 单一职责——一段ST只做一件完整的事

20行一段的背后,真正的核心不是"行数"这个数字,而是"职责边界"。

如果一段ST代码超过20行,大概率意味着你在这个功能块里塞了不止一件事。举个例子:一个功能块里既写了气缸推进的顺序逻辑,又写了真空压力监控,还写了报警灯闪烁控制——这三件事虽然属于同一台设备,但它们的生命周期、变更频率和测试方式完全不同。气缸顺序逻辑跟着工艺走,压力监控跟着气路系统走,报警灯则跟着人机交互走。硬把它们绑在一个块里,将来任何一个需求变化都会导致整个块被推倒重来。

拆成小功能块之后,每个块只干一件事,而且这一件事可以完整地被描述清楚。单一职责听起来像是软件工程里的老生常谈,但在PLC工程里同样适用。IEC 61131-3标准本身就给了一套完美的表达工具——FB和FC。很多人不是不会用,而是没把API设计思维带到PLC程序里。

2.2 接口即契约:把数据流动变显式

20行一段的另一个隐藏价值,在于它逼迫你设计接口。

大块代码靠共享变量通信,数据流是隐性的,藏在几百行代码里。小块代码靠FB的输入输出参数通信,数据流是显性的,一眼看过去就知道谁在给谁传数据。这就是接口即契约:FB的VAR_INPUT和VAR_OUTPUT就是这个块跟外部世界打交道的边界,边界清晰,耦合自然降低。

我在拆分改造之后发现一个额外的红利:Code review轻松多了。看一对FB之间的调用关系,只需要检查接口参数是否匹配,而不用把两个块的全部代码从头捋一遍。这相当于给程序加了一圈"围墙",围墙上开了几个门,每个门走什么人一清二楚。

2.3 状态机天然是20行的好载体

我拆过很多设备控制逻辑,发现一个规律:设备控制类的ST代码,60%以上都可以改写成状态机。而状态机本身的结构特性,决定了它天然适合20行左右的粒度。

经典的状态机有4到6个状态,每个状态的处理逻辑3到5行,加上状态转移条件、定时器触发、报警分支,一个CASE语句写下来通常就是二十多行。外层套一个判断,这个功能块就是一个完整、独立、可测试的状态机。相比之下,用二十层IF嵌套表达同一个流程,代码量会膨胀到一百行以上,而且可读性差很多。

这不是说所有ST都要改成状态机,但当你发现一个块里充满了"某条件成立且某条件不成立……"这样的嵌套时,就该反问自己:为什么不把它拆成几个状态?状态机把时间维度的复杂性显式化了,IF嵌套则把复杂性隐藏在条件组合里。

3. 实操拆解:把一个850行ST设备控制块拆成7个小块

3.1 第一步:画职责地图,别急着动手改

接到一个800行的ST块,最忌讳的就是打开编辑器直接剪切粘贴。正确的做法是先画一张"职责地图":把这个FB里所有输入输出变量、内部变量、定时器、计数器全部列出来,然后给它们归堆。

我一般用表格来做这件事:

变量/逻辑属于哪个功能域建议去向
气缸1动作+到位判断气缸顺序控制FB_CylinderSeq
气缸2动作+到位判断气缸顺序控制FB_CylinderSeq
真空压力上限报警真空监控FB_VacuumCheck
吹气延时和吹气阀吹气动作FB_BlowOff
报警汇总输出报警处理FB_AlarmCollect
手动/自动切换模式管理FB_ModeSwitch
复位逻辑模式管理FB_ModeSwitch

这张表画完之后你会吃惊地发现,很多变量的归属是重叠的。比如同一个压力信号既被真空监控用到,又被报警等级判断用到。这时候不要急着决定"它属于谁",而是先想清楚"它到底表达什么"。在拆分过程中,归属不清的信号通常就是耦合的根源。

3.2 第二步:按"动作+保护+状态"切分功能域

做PLC设备控制,我习惯把逻辑分成三层:执行动作、设备保护、生产状态。执行动作管阀和电机怎么动;设备保护管压力、温度、超时这些安全边界;生产状态管这个设备当前处于什么模式、什么阶段。

按照这个思路,850行的块可以切成这样:

  • FB_CylinderSeq:5个气缸的时序动作,每个气缸一个状态机;
  • FB_VacuumCheck:真空压力监控与报警;
  • FB_ModeSwitch:手动/自动/复位/急停状态的统一管理;
  • FB_AlarmCollect:报警汇总、报警分级、报警输出的集合。

每个FB的正文控制在20到40行。你可能注意到,我并没有严格卡在20行以内,这是因为有些功能块天生需要多一点上下文。这个后面会说,别急。

3.3 第三步:用FB接口约束数据流动

拆分之后最关键的一步,是设计接口。

我定了一条规则:拆出来的新FB,一律不允许通过全局变量跟外部交换数据。要数据,就从VAR_INPUT进;要结果,就从VAR_OUTPUT出。这样做的直接好处是,我可以在不打开其他代码的情况下,单测一个FB。

接口的参数数量也要克制。一个功能块的输入输出加在一起超过10个,就要反思是不是粒度又粗了。输入参数超过5个,内部逻辑大概率在同时处理多个业务维度。

我给一个实际例子,原来的850行块里有一个气缸推进逻辑,接口参数我最终设计了6个:

FUNCTION_BLOCK FB_CylinderAdvance VAR_INPUT bStart : BOOL; (* 推进启动信号 *) bHomeSensor : BOOL; (* 原位传感器 *) bWorkSensor : BOOL; (* 工作位传感器 *) rTimeoutTime : TIME := T#2.5S; (* 超时时间 *) bEnable : BOOL; (* 使能信号 *) END_VAR VAR_OUTPUT bAdvancing : BOOL; (* 正在推进 *) bAtWorkPos : BOOL; (* 已到工作位 *) bTimeoutAlarm : BOOL; (* 超时报警 *) END_VAR VAR eStep : INT := 0; (* 状态机步号 *) tonTimeout : TON; (* 超时定时器 *) END_VAR

3.4 第四步:一个20行级别的ST功能块长什么样

接着上面这个接口,写出来的功能块正文大概是这样的:

CASE eStep OF 0: (* 待机 *) IF bEnable AND bStart AND bHomeSensor THEN eStep := 1; bAdvancing := TRUE; END_IF; 1: (* 推进 *) tonTimeout( IN := TRUE, PT := rTimeoutTime); IF bWorkSensor THEN bAdvancing := FALSE; bAtWorkPos := TRUE; eStep := 2; ELSIF tonTimeout.Q THEN bAdvancing := FALSE; bTimeoutAlarm := TRUE; eStep := 3; END_IF; 2: (* 到位保持 *) IF NOT bWorkSensor THEN bAtWorkPos := FALSE; eStep := 0; END_IF; 3: (* 报警等待复位 *) IF NOT bEnable THEN bTimeoutAlarm := FALSE; eStep := 0; END_IF; END_CASE;

这段代码连声明带正文,加起来大概30行。它没有严格卡在20行以内,但它的结构非常清晰:4个状态,每个状态做的事情一眼能看明白,超时报警和到位判断互不干扰。这就是我所说的"20行一段"的真实含义——不是数字硬指标,而是结构上要让一段代码能被一次性读懂。

这个FB最大的好处是可独立测试:我在测试环境里只需要把传感器信号模拟好,就能完整验证推进、到位、超时、复位四个路径,完全不用管其他设备动作。

3.5 第五步:拆完之后的实测效果

改造完那个850行的块之后,我做了一个简单对比。改造前扫描周期略有波动,改造后单个设备块的扫描时间从1.2毫秒降到了0.8毫秒——其实这个数字在PLC场景里不算什么关键收益,真正的关键收益是报警定位时间。

原来的定位时间是一两个小时,改造之后变成几分钟。原因很简单:报警被"关"在了FB_VacuumCheck或者FB_CylinderAdvance里面,哪一路报警触发了,直接看功能块的输出状态位,同时打开该功能块看它的内部状态走到哪一步,问题基本一目了然。

代码评审也轻松了很多。以前评审这个850行的块,三个工程师坐在一起看了半天,谁也不敢说改哪里。拆完之后,每个FB单独评审,二十分钟搞定。

4. 别把20行当教条:拆过头的副作用和判断标准

4.1 我见过最糟糕的"20行教条"

有一次我在某个项目里看到一套编码规范,规定所有功能块正文一律不得超过20行。结果一位同事为了满足这个指标,把一段本来50行、结构也算合理的逻辑硬拆成了7个互相调用的"迷你功能块",每个块5到8行。

结果读代码的时候,你需要在7个文件之间反复跳转,层层点进去才能把一个完整动作串起来。有一次为了查一个"吹气阀没启动"的问题,我从主程序跳到FB_A,FB_A里调用了FB_B,FB_B里调用了FB_C,FB_C里又调用了FB_D……等我找到最终控制吹气阀输出的那行代码时,已经过去了40分钟。改之前用普通的长块找,5分钟就能找到。

这就是典型的"为了指标而拆"。

4.2 逻辑碎片化的三个典型症状

拆过头之后,最容易出现的三个症状:

一是调用链超过三层。我从主程序往下数,如果要在代码里连续跳转三个以上的FB才能找到最终的赋值语句,那这个拆法就已经在制造新的阅读成本了。

二是对象之间互相调用对方。FB_A需要FB_B的结果,FB_B又需要FB_A的结果,两个块形成环状依赖。这种情况在PLC程序里非常棘手,因为功能块的调用顺序会直接影响功能块的执行结果,而环状依赖在扫描周期里必然有一个块读到的是上一次扫描的旧数据。

三是为了调用关系写了大段注释。如果你发现每个功能块顶部都要用二十行注释来解释"我为什么要调用下一个功能块",那说明这个拆分本身已经违背了"让代码自解释"的初衷。

4.3 三条实用的粒度判断标准

经过这些年的反复试错,我总结出三条判断拆分粒度是否合适的标准,每条标准都不依赖具体的行数。

第一条:能不能独立测试。把一个功能块单独拉出来,不管外部环境,只给定输入信号,就能验证它所有输出是否符合预期。能做到,粒度就合格。做不到,要么是这个块对外部世界依赖太深,要么是它内部塞了太多不相干的事。

第二条:能不能一屏之内讲明白。我标准的屏幕是1080P的编辑器窗口,代码加上注释,最好能在不滚动的情况下看完整个功能块。如果你需要翻页才能看懂自己在写什么,这个块该拆了。这条标准比20行更有弹性,因为有些人一个块写60行,只要结构统一、状态清晰,也能一屏看完。

第三条:调用链不超过三层。从主程序开始,到最终动作输出,中间的功能块嵌套层级控制在三层以内。超过三层,我宁可把中间层的几个小块合并一下。因为PLC世界里,深层次嵌套意味着跨扫描周期的数据同步问题会成倍增加。

4.4 量化参考:圈复杂度与接口参数数量

如果还是拿不准,我给两个比较量化的参考值。

第一个是圈复杂度。简单计算方式:一个功能块里共有多少个"判断分支"(IF、CASE分支、AND/OR组合等),加1,得到的数值就是圈复杂度。我一般要求功能块正文的圈复杂度不超过10到15。超过15,不管行数多少,这个块已经复杂到容易埋bug了。

第二个是接口参数数量。一个功能块的输入变量加输出变量之和,尽量控制在8个以内,最多不要超过12个。接口参数太多,意味着数据在块与块之间传递得过于频繁,那你要思考的是接口设计的问题,而不是再拆一层的问题。

这两个量化指标,配合前面三条定性标准,基本上就够用了。行数可以作为一个感性参考,但不是硬指标。

5. 在团队里让"20行一段"真正落地

5.1 编码规范不靠口号靠模板

回回有人说"我们要写规范的ST程序",结果规范文档写了一堆"应简洁""应清晰"的空话,工程师们看完不知道到底应该怎么做。我的经验是,规范必须配上能直接抄的模板。

我自己在团队里推行ST规范时,给了一套模板:每个设备类功能块必须包含状态机声明区、输入接口区、输出接口区、内部变量区、CASE状态区、报警处理区。每个区的作用和写法都有示例。新人拿到模板之后,不需要理解什么叫"内聚和耦合",照着模板填逻辑,写出来的代码天然就是20行一段的结构。

5.2 代码审查与自动化检查

规范定了,还要有执行的抓手。我们团队的做法是两件事同时做。

第一件是Code Review清单。清单里明确列出了几项硬检查:功能块正文是否超过40行?接口参数是否超过12个?是否存在全局变量跨块通信?是否有超过三层的调用链?每项都不讲道理,只讲事实。评审会上直接对照清单打勾,哪一项不过,打回去改。

第二件是自动化检查。Twincat、Codesys这些IDE本身没有强制的代码行数检查插件,但可以借助版本控制工具做最低限度的检查:在提交代码前写一个小的脚本,扫描ST源文件,统计每个FUNCTION_BLOCK、FUNCTION的正文行数,超过阈值就阻断提交。脚本很简单,用正则匹配抓到FUNCTION_BLOCK和END_FUNCTION_BLOCK之间的代码,数一下有效代码行数就行。

5.3 老项目改造的渐进路线

最后说老项目怎么改。我强烈建议不要一次性重写一个大块。老设备程序改造最安全的路线,是"先围栏、后平移、再优化"三步走。

第一步先围栏:在旧的850行FB外面包一层新的FB接口,把原来所有输入输出映射到新接口上,让外部调用关系先稳定下来。

第二步换内部实现:把旧块里的逻辑按职责拆成新的小FB,外部看到的接口不变,内部实现逐渐替换。

第三步逐步优化:新逻辑跑稳定之后,再回头看那些接口参数能不能精简、命名是不是清晰、注释是不是准确。

我接手过的老项目里,按这个路线走,几乎没有一次因为重构导致现场停机的。反而是那些不分步骤、上来就大刀阔斧重写的人,常常被各种隐藏的耦合坑到加班。

我自己的感受是,800行一段和20行一段的争论,本质上是"写代码时图自己方便"和"读代码时为别人着想"的分岔口。PLC程序的特点是会被现场反复调试和修改,代码的阅读次数远远多于编写次数。所以在ST的世界里,多花点时间把逻辑拆成小块,把接口设计清楚,是所有投资里回报率最高的一笔。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/13 5:37:05

编译原理实操指南:词法语法分析与符号表工程落地

简介:本资源是河南大学软件学院《编译原理》课程期末复习核心考点精要总结,专为该校软件学院本科生备考设计,聚焦高频题型与易错难点,助力高效冲刺期末考试。文档为单个13KB的Word文件(.docx),内…

作者头像 李华
网站建设 2026/10/13 5:36:58

wrk-4.1.0-linux 压测实战:从编译安装到高并发调优与避坑指南

简介:本资源为wrk-4.1.0的Linux版本安装包,面向需要进行Web服务器性能压测的开发者与运维人员。wrk是一款用C语言编写的高性能HTTP基准测试工具,擅长高并发、短连接场景下的压力测试,相比JMeter、ab等工具更轻量、更易扩展。压缩包…

作者头像 李华
网站建设 2026/10/13 5:35:56

ECU休眠唤醒与上下电时序的耦合机制解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/13 5:35:34

戴尔服务器重装 ESXi 6.7 DellEMC 定制版完整指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/13 5:34:54

三菱FX3U自动门控制全解析:从I/O分配到仿真调试与现场整定

上周去现场调一套商场平移门的改造,甲方拍着胸脯说:“就电机正反转,PLC控制,带防夹,一天能搞完。”结果从IO分配到仿真验证,再到现场整定,我整整蹲了三天。自动门系统控制确实不是什么高深技术&…

作者头像 李华
网站建设 2026/10/13 5:33:22

AnyPS5:旧硬件系统重构与跨平台复用实战指南

1. 项目缘起与核心定位AnyPS5 这个名字第一次出现在我视野里的时候,我正蹲在一堆旧主机配件里翻找能用的散热风扇。当时一个做嵌入式开发的朋友甩过来一句话:“你那个吃灰的旧主机,其实还能再战三年。”这句话让我开始认真思考一个问题——那…

作者头像 李华