PLC模块化编程实战:UDT+FB重构泵控程序,效率提升看得见

发布时间:2026/10/3 4:18:46
PLC模块化编程实战:UDT+FB重构泵控程序,效率提升看得见 接了个水泵站项目四个泵两个变频两个工频现场控制逻辑大同小异。我刚入行那会儿图省事每个泵都单独写一段梯形图一个子程序能堆到三四百行。改个延时时间得翻半天程序甲方说再加一台泵我整个人是崩溃的。后来第二期项目我咬着牙把所有泵的逻辑重构成结构体UDT加功能块FB的写法PLC编程效率提升是肉眼可见的后期维护更是省心不止一点。这篇文章就围绕“UDTFB”这套模块化做法展开聊聊为什么PLC程序会“越写越乱”、UDT和FB到底解决什么问题、两者怎么配合落地以及我在实际项目中踩过的坑。适合那些已经能独立写梯形图或SCL但程序还停留在“一份代码复制N遍”阶段的工程师也适合刚转行做PLC编程、想从一开始就建立良好编程习惯的朋友。目录逻辑我会按“为什么要模块化、UDT怎么用、FB怎么用、UDTFB组合实战、常见问题排查”这样走尽量把原理、代码、排错都串起来让你看完能直接上手改自己的程序。1. 为什么PLC程序需要模块化很多初学者会有个错觉PLC程序嘛变量多、逻辑杂能跑起来就行。确实单机设备、几十个I/O点的情况下怎么写都行。但一旦进入多工位、多设备的产线级项目程序的可读性、可维护性、可扩展性就会决定项目成败。1.1 复制粘贴式编程的灾难我见过最夸张的一个项目12台相同的风机工程师把风机启停程序复制了12份每个程序段里变量名就是FAN1_START、FAN2_START、FAN3_START……一直到FAN12_START。改一个保护逻辑就得同时修改12份程序漏改一个就会出问题。这种“复制、粘贴、改变量”的编程方式短期开发看似快后期维护成本是成倍增长的。更麻烦的是这种程序占用的存储空间很大扫描周期也会被拉长。在S7-1200这类中低端CPU上程序段多了在线监控都会卡顿。而且团队成员之间协作时别人拿了你的程序光搞清楚FAN7和FAN8之间的细微差别就要花大力气。1.2 模块化的核心收益模块化的本质是把“数据”和“逻辑”都封装成可复用的单元。数据上用结构体UDT统一格式逻辑上用功能块FB统一行为。这样带来三个直接好处复用性一个泵控FB写好100台泵就是100个实例不用重复写逻辑。可维护性修改FB内部逻辑所有实例同步更新不用逐个程序段去改。可协作性程序结构清楚团队分工明确你写电机FB、我写阀门FB最后拼装即可。这里要区分一下PLC的模块化与高级语言里的“函数库”思想是一致的。你写C语言不会把同一个函数复制100份而是定义函数再调用。PLC里的UDT和FB就是PLC世界的“结构体定义”和“类方法”。2. 结构体UDT把松散的数据绑在一起UDTUser Defined Type在西门子TIA Portal里叫“PLC数据类型”在三菱GX Works3里叫“结构体”在欧姆龙Sysmac Studio里也叫“结构体”。名字不同意思一样把一组不同类型但逻辑相关的数据打包成一个整体。2.1 UDT到底是什么打个比方你要管理一个仓库最原始的方式是拿一个本子把所有货物的名称、数量、位置、入库日期都记在流水账里。找一样东西要翻半天。UDT相当于你把仓库划分成若干货架每个货架上有固定的格子每种货物放在自己专属的位置上。你只要知道货架编号就能直接找到所有相关信息。在PLC里定义一个电机相关的UDT大概包含这些成员Motor_UDT ├── CMD命令区 │ ├── StartCmd : Bool │ ├── StopCmd : Bool │ ├── ResetCmd : Bool ├── ST状态区 │ ├── Running : Bool │ ├── Fault : Bool │ ├── Ready : Bool ├── PARAM参数区 │ ├── StartDelay : Time │ ├── RatedCurrent : Real │ └── OverloadThreshold : Real └── STAT统计区 └── TotalRunTime : Real有了这样一个UDT你在创建电机变量时不需要再单独建立十几个独立的Bool、Real变量而是直接声明一个Motor1 : Motor_UDT。电机1的所有信息都通过Motor1.CMD.StartCmd、Motor1.ST.Running这种层级方式访问清爽又直观。2.2 在TIA Portal里定义UDT以西门子S7-1200/1500为例三菱、欧姆龙的操作思路完全一致定义步骤是在项目树中找到“PLC数据类型”右键→“添加新数据类型”取个名字比如Pump_UDT。然后在打开的编辑表中输入成员名称选择数据类型填初始值和注释。这里有个关键点成员顺序会直接影响数据的存储布局。虽然对运行逻辑影响不大但对在线监控时的可读性影响很大建议按“命令→状态→参数→预留”的顺序排列和现场端子排布保持一致的对应关系。2.3 UDT的进阶玩法嵌套与数组UDT之间可以互相嵌套。比如你定义了一个Motor_UDT然后要做一个多电机的泵站可以再定义PumpStation_UDT ├── Motor1 : Motor_UDT ├── Motor2 : Motor_UDT ├── Motor3 : Motor_UDT └── InletValve : Valve_UDT这样整个泵站就是一个变量所有子设备的数据都在其中。HMI组态时变量表结构也能保持同样的层级关系触摸屏那边的变量连接效率也更高。UDT数组也极其实用。比如有4台泵你直接定义Pump : Array[1..4] of Pump_UDT然后程序里可以用FOR循环遍历查找当前运行状态统计运行中泵数量 FOR i : 1 TO 4 DO IF Pump[i].ST.Running THEN RunningCount : RunningCount 1; END_IF; END_FOR;用数组的思路配合后文要讲的FB是实现设备轮换、组启动、组停止的利器。2.4 UDT定义的注意事项UDT看似简单但实际使用中很容易踩坑。我总结几条经验预留扩展区在UDT末尾加一段Reserve : Array[0..15] of Byte。项目后期要增加通道、增加参数时直接使用预留区可以避免修改UDT定义导致CPU停机或背景DB结构变化。命名要有区分度“CMD”和“ST”这两个区组名很好用一看就知道是命令还是状态。不要取“DATA1、DATA2、DATA3”这种序号式命名维护时猜猜猜太痛苦。注释写单位Real类型成员必须写上单位比如RatedCurrent : Real注释写成“额定电流单位A”。否则几个月后你自己看着这个变量都不知道该填什么数。3. 功能块FB有记忆的程序块FB是Function Block翻译为“函数块”或“功能块”。它和FCFunction最大的区别在于FB拥有自己独立的数据存储区也就是背景数据块Instance DB。简单说FB有“记忆”FC没有“记忆”。3.1 FB和FC怎么选入门工程师最容易搞混的就是FB和FC。我习惯用一个“储物柜”的比喻FC就像一个没有储物柜的临时工你给他一个输入他现场算完给你一个输出干完就走什么都不留。FB则自己带一个专属储物柜干完活会把中间变量、状态值工整地放进柜子里存好下次再进来能接着上次的状态继续干活。实际项目中设备级控制逻辑——如电机启停、阀门动作、伺服定位——一律用FB因为设备有“当前状态”数学计算、数据类型转换、简单比较——用FC就够因为这类功能不依赖历史状态。三菱PLC里也有类似的区分FB还是叫FB和西门子的FB一个概念但三菱没有严格对应FC的“无状态”函数概念早期多用子程序和宏欧姆龙则既支持功能块也支持无记忆的功能。各大品牌思路一致只是叫法略有不同。3.2 FB的接口设计先想好“外立面”FB的接口就是它的“外立面”是别人调用时能看到的窗口。设计FB接口时分这么几类VAR_INPUT输入由外部传入如启动命令、停止命令、使能信号。VAR_OUTPUT输出由FB内部计算后输出给外部如运行指示、故障指示、输出到接触器的指令。VAR_IN_OUT输入输出兼有适合传结构体。这个很关键——你可以把整个Pump_UDT作为IN_OUT传给FBFB内部可以直接读写它的成员。VAR_STATFB内部的静态变量保存在背景DB中掉电保持或扫描间保持。VAR_TEMP临时变量执行完本周期就清零不保存。接口设计的核心原则是FB要尽量“自包含”。也就是说外部只负责给命令和读状态具体内部怎么判断、怎么延时、怎么互锁都封装在FB里。一个标准的泵控FB接口大概长这样FUNCTION_BLOCK FB_PumpControl VAR_INPUT Enable : Bool; // 总使能 StartCmd : Bool; // 启动命令 StopCmd : Bool; // 停止命令 AutoMode : Bool; // 自动模式 RunFb : Bool; // 运行反馈 FaultSig : Bool; // 故障信号 END_VAR VAR_OUTPUT RunCmd : Bool; // 运行指令输出 FaultOut : Bool; // 故障输出 END_VAR VAR_IN_OUT PumpIO : Pump_UDT; // 泵的数据结构 END_VAR VAR_STAT step : Int; // 状态机当前步 tDelay : TON; // 延时定时器 END_VAR3.3 FB内部逻辑状态机写法让逻辑彻底清晰FB内部逻辑组织我非常推荐“状态机”写法这也是搜索结果中“PLC编程状态机写法”这个热词对应的核心内容。状态机的思想其实很简单设备任何时刻都处在有限个状态之一每个状态只处理自己的事情条件满足就切换状态。用泵控来举例状态可以分成空闲IDLE→ 启动延时START_DELAY→ 运行RUNNING→ 停止延时STOP_DELAY→ 故障FAULT。梯形图写这种状态机会很啰嗦SCL会优雅很多核心就是CASE语句CASE step OF 0: // IDLE IF StartCmd AND NOT RunFb AND NOT FaultSig THEN step : 1; tDelay(IN : FALSE); END_IF; 1: // START_DELAY tDelay(IN : TRUE, PT : PumpIO.PARAM.StartDelay); IF tDelay.Q THEN RunCmd : TRUE; step : 2; END_IF; IF StopCmd OR FaultSig THEN step : 0; END_IF; 2: // RUNNING IF FaultSig THEN RunCmd : FALSE; step : 3; ELSIF StopCmd OR NOT RunFb THEN RunCmd : FALSE; step : 0; END_IF; 3: // FAULT IF PumpIO.CMD.ResetCmd THEN step : 0; END_IF; END_CASE;特别提醒状态机的变量只在状态切换的瞬间赋值其他情况下不要反复写否则一个扫描周期内可能出现“状态还没启动延时延时定时器就被Clear”的古怪问题。这也是为什么静态变量step要单独声明——它代表了FB的“记忆”。4. 实战UDTFB组合完成一台泵的模块化控制理论讲完下面用一套完整的三泵联动项目案例把UDT和FB的组合应用串起来。这台泵的需求不完全一样两用一备手动/自动切换故障自动切泵运行时间累计。4.1 第一步定义泵的数据结构在TIA Portal的“PLC数据类型”里新建Pump_UDT。包含以下成员Pump_UDT ├── CMD命令区 │ ├── StartRemote : Bool; // 远程启动 │ ├── StopRemote : Bool; // 远程停止 │ ├── ResetFault : Bool; // 复位故障 │ ├── AutoMode : Bool; // 自动模式1自动/0手动 ├── ST状态区 │ ├── RunFb : Bool; // 运行反馈 │ ├── FaultSig : Bool; // 故障信号 │ ├── Running : Bool; // 运行状态 │ ├── Fault : Bool; // 故障状态 ├── PARAM参数区 │ ├── StartDelay : Time; // 启动延时 │ ├── StopDelay : Time; // 停止延时 │ ├── MinRunTime : Time; // 最小运行时间 ├── STAT统计区 │ └── TotalRunTime : Real; // 累计运行时间小时 └── Reserve预留区 └── Reserve : Array[0..7] of Byte;注意这里我把“状态区”和“统计区”分开了。为什么因为状态区是实时刷新、掉电重置的统计区则需要保持后续编程时对掉电保持区域的规划会更清晰。4.2 第二步封装泵控FB新建FB命名FB_PumpControl。接口按3.2节设计内部逻辑用状态机。下面补充两个关键环节。手动/自动切换AutoMode为TRUE时FB只接收外部联锁逻辑送来的StartRemote/StopRemoteAutoMode为FALSE时FB只接收就地按钮信号。这里不建议手动信号和自动信号混用否则容易出现“自动状态下手动按钮也能启泵”的安全隐患。累计运行时间在RUNNING状态下每个扫描周期做一次累加。如果直接每次加1单位是扫描周期非常不直观。更稳的做法是调用系统时钟计算两次扫描之间的时间差// 使用系统时间差累计 nowTime : DINT_TO_LREAL( TIME_TO_DINT( T_PLC_MS() ) ); IF step 2 THEN PumpIO.STAT.TotalRunTime : PumpIO.STAT.TotalRunTime (nowTime - lastTime) / 3600000.0; END_IF; lastTime : nowTime;T_PLC_MS()是读取CPU运行毫秒数的系统函数不同的PLC品牌有各自的系统时钟读取方式但原理一样。这个写法比每周期加常数要精确得多也不会因为CPU扫描周期波动导致累计时间偏差。4.3 第三步实例化FB并建立泵组调度在主程序OB1中创建3台泵的数据结构和FB实例。以下是我在FC调用逻辑里的写法VAR PumpData : Array[1..3] of Pump_UDT; fbPump1 : FB_PumpControl; fbPump2 : FB_PumpControl; fbPump3 : FB_PumpControl; END_VAR然后分别在网络段里调用// 泵1 fbPump1( Enable : TRUE, StartCmd : PumpData[1].CMD.StartRemote, StopCmd : PumpData[1].CMD.StopRemote, AutoMode : PumpData[1].CMD.AutoMode, RunFb : PumpData[1].ST.RunFb, FaultSig : PumpData[1].ST.FaultSig, RunCmd PumpData[1].ST.Running, FaultOut PumpData[1].ST.Fault, PumpIO : PumpData[1] );3台泵就是3个FB实例程序段几乎一模一样。后续如果要加第4台泵复制一个调用加上对应数据和变量5分钟搞定。轮换逻辑放在另一个FC里做。核心思路是自动模式下如果当前运行泵故障在泵组中查找“累计运行时间最短”且“无故障”的泵作为下一台启动对象。这个过程本质上就是个数组遍历加比较几行SCL就搞定。模块化的好处在这里体现得淋漓尽致轮换调度只关心泵的UDT.ST和UDT.STAT根本不用管每一台泵具体是怎么启动的。4.4 与HMI联动变量表也模块化用了UDT之后HMI组态的变量连接也会轻松很多。比如触摸屏要显示1号泵的运行状态变量地址就是PumpData[1].ST.Running。在WinCC或第三方HMI中可以直接导入PLC变量层级结构保持和UDT一致。这意味着HMI画面的开发也可以模板化建一个“泵操作面板”的画面模板绑定PumpData[i]即可复制3份对应3台泵。HMI上最常见的操作——“复位故障”在FB内部是这么处理的按下复位按钮ResetFault置位FB在FAULT状态下检测到之后把故障状态清掉、回到IDLE。这里要多说一句复位按钮最好用“上升沿触发”而不是电平触发否则按钮一直按着故障一出现就被瞬间复掉可能掩盖真实故障。5. 常见问题与排查技巧实录模块化实践不像书上写的那么顺风顺水实际项目中总会遇到各种幺蛾子。下面这几个问题是我和身边同行交流后总结出的高频坑。5.1 背景数据块太多项目树爆炸每调用一个FB就会生成一个背景DB。如果一个设备有几十个FB实例项目树里就会出现几十个背景DB看着就脑仁疼。这也是很多工程师嫌弃FB的原因——“变量不好找嘛”。其实解决思路有两个。一是使用多重背景在某个FB的静态变量区声明其他FB的实例例如在FB_PumpSystem的静态区中声明fbPump1 : FB_PumpControl这样所有泵控FB的背景DB只有一份作为FB_PumpSystem的背景DB的子区域。二是把某些内部状态不重要的FB改用FC减少背景DB数量。但后者会牺牲“记忆”能力很多设备逻辑复用不了。我的建议是设备级FB可以考虑多重背景过程级调度逻辑用FC这种“FB管设备、FC管调度”的分工在实际项目中很顺手。5.2 在线修改UDT后CPU直接STOP这个问题最容易让人冷汗直冒。项目调试中你觉得UDT里缺一个参数于是在线修改了UDT定义然后点击“下载”结果CPU从RUN切换到STOP现场设备全部停机。原因是UDT结构变化会导致所有引用它的数据块、背景DB的存储布局全部变化CPU无法在不停止的情况下重新分配内存。处理方式很明确项目上线前把UDT定义冻结禁止追加或删除成员只允许改初始值或注释。UDT尾部一定要加预留字节就是前面说的Reserve : Array[0..7] of Byte项目后期需要加数据时用完这8个字节日后再加能避免绝大多数的停机风险。如果要修改的UDT已经在生产线上运行务无比对好影响范围停机窗口内快速下载不熟练的情况下可以用“复制UDT为另一个新类型”的方式做增量逻辑验证验证通过再切换。5.3 状态机执行异常为什么设备偶尔卡在一个状态状态机的坑多半出在“边沿处理”和“多周期竞争”上。比如START_DELAY状态里tDelay.Q条件满足了但同时StopCmd也来了这时候到底进RUNNING还是回IDLECASE语句的判断顺序决定了结果。如果IF StopCmd OR FaultSig写在IF tDelay.Q前面那就会优先退回IDLE不会进入RUNNING。这不是bug是逻辑顺序问题但很容易让后来接手的工程师困惑。排查方法很简单把step变量加到监控表里在线观察设备卡在哪个状态值。如果一直卡在某个状态不跳多半是跳出条件不满足——比如反馈信号一直没到位如果状态在反复横跳多半是多个条件同时满足、而CASE的优先级和你预期的不一致。5.4 在线监控看不到UDT数组里的成员S7-1200/1500默认使用“优化的块访问”在这种模式下监控表里虽然能展开UDT数组但某些版本的TIA Portal在监控时会显示为“下标不可用”。我见过不少工程师以为程序写错了急得团团转。解决办法一是把该数据块或FB的“优化的块访问”属性取消改为非优化访问二是直接把要监控的成员单独建一个监控DB用AT覆盖或赋值的方式把关键状态引出来三是充分利用TIA Portal的“监控表格”功能把表达式替换成PumpData[1].ST.Running这种完整路径。不同品牌PLC三菱、欧姆龙的在线监控方式各有差异但核心思路一致——监控路径要完整、访问属性要匹配。5.5 命名规范一人一个风格等于没有规范模块化程序最怕命名混乱。UDT叫MotorInfoFB叫FB_MOTOR变量名一会儿Pump1_State一会儿boPumpState这种程序即使结构清晰读起来依然费劲。推荐的做法是建立一份项目级命名规范包含以下规则UDT命名用名词Pump_UDT、Valve_UDT、Analog_UDT。FB命名用动词名词FB_PumpControl、FB_ValveControl。变量名采用“设备名_信号含义”的格式比如Pump1_StartCmd、Pump1_FaultSig。注释必须写明功能状态位还要标注有效电平高电平有效/低电平有效模拟量要标注工程量和单位。我见过一个泵站项目由于前人不写注释后面人接手后只能对着地址表猜变量含义平均每次故障排查要花三倍时间。程序是写给后来的工程师包括三个月后的你自己看的不只是写给PLC执行的。结尾模块化实践这件事确实不是一蹴而就的。我个人的经验是不要等到项目做大了才想起来重构而是从下一个项目开始找一个最常出现的设备类型泵、阀门、电机都行先定义UDT再封装FB哪怕一开始只有一两个实例也要用调用的方式写。你会发现前几次封装可能比直接写梯形图还慢因为要思考接口、状态划分、异常处理但一旦封装完成后面就是复制-改参-实例化效率完全是另一回事。最后再分享一个小技巧把封装好的UDT和FB放进TIA Portal的“全局库”中下次项目直接以“库元素”的方式拖拽复用。这样不仅能跨项目保持程序风格一致还能把你自己沉淀下来的时间滞后逻辑、保护互锁规则这些“项目魂”带过去。用得越多库越厚代码质量自然水涨船高。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询