☰
三菱FX3U用ST语言搭建设备控制框架,告别梯形图迷宫
2026/9/28 16:42:14 网站建设 项目流程

搞PLC编程的兄弟,尤其是常年跟三菱FX3U这种小型机打交道的,应该都有个共同感受:梯形图写设备逻辑,写到最后基本不是在搞工艺,而是在机械地重复“按钮启、接触器吸、反馈回来亮灯、故障跳闸锁存”这种同质化代码。一个稍微复杂的设备,动辄几百上千步梯形图,改一个连锁条件要找半天,加一个新功能又不敢乱动旧逻辑,调试和售后都痛苦。

去年我接手了一台老设备改造,用的就是三菱FX3U,甲方要求程序结构清晰、后续要自己加几个点位。我干脆把整个程序用ST语言重写了一遍,重点是把电机、气缸、变频器这些典型执行机构全部封装成FB功能块。整套架构跑下来,最大的感受是:设备控制逻辑从“一坨梯形图”变成“输入映射、FB调用、顺序逻辑、输出映射”四个层次,新设备调试时间明显缩短,改工艺只动一个状态机,查故障直接看FB输出和报警代码。这篇文章就把这套从零搭起来的框架完整拆开讲,包括FX3U上ST的能做什么不能做什么、FB功能块到底怎么设计、实测中踩过的坑,一条一条写清楚。

1. 为什么要在FX3U上用ST搭框架

1.1 FX3U的ST到底能干什么

先说结论,三菱FX3U的ST语言是IEC 61131-3标准ST的“裁剪版”,和FX5U、博途、Codesys里的ST相比,它砍掉了很多东西:结构体、联合体、枚举类型基本别想,TON、TOF这类标准功能块也没影,指针更是不可能。这意味着你没法完全照搬PC端或大型PLC的ST写法。

那FX3U的ST还能干什么?基础数据类型BOOL、INT、DINT、REAL、WORD这些都有,算术运算、比较、逻辑运算、选择语句IF/ELSIF/CASE、循环FOR/WHILE(注意循环里不能用很多功能)、函数和功能块都支持。对我个人来说,这个子集已经足够搭一个设备控制框架了。设备控制的本质就是“条件判断+状态转移+输出通断”,这些恰恰是ST最擅长表达的。

GX Works2软件里,FX3U插入ST程序的方式很简单:工程树里右键“程序”,新建数据,语言选“结构化文本(ST)”。注意FX3U的ST程序不能单独作为主程序跑,你还是要有一个MAIN程序或扫描程序,通过调用ST程序或把ST程序设置成执行类型来运行。我在实际工程中是建了一个主程序MAIN(梯形图),里面用CALL调用ST程序块,或者直接把ST程序设成扫描执行,这样最省事。

1.2 设备控制框架解决的本质问题:改得快、查得快

很多人觉得“PLC程序能跑就行”,这话在只有十来个点位的小设备上没错,但设备一旦有几十个点位、几台电机、几个气缸、一两台变频器,程序的可维护性就变成头等大事了。

我用梯形图时代最痛苦的三件事:

  • 电机类重复逻辑多。一个设备里五六台电机,每台电机的启停、反馈、热继、故障锁存逻辑几乎一样,你明明知道这是复制粘贴,但改的时候还得一台一台改。改漏一台,现场就等着烧接触器吧。

  • 连锁条件散落各处。急停、模式切换、故障复位这些全局信号,分散在不同程序段里,改一个信号的逻辑要翻遍整个工程。

  • 查故障靠猜。故障灯一亮,不知道是哪个环节出问题,只能拿万用表加软件监控一点点捋,售后效率极低。

搭框架的目的就是要把这三点全干掉。设备控制框架说白了就是一套固定的程序组织方式:哪一层负责接收输入信号,哪一层负责任务执行,哪一层负责输出,全都规定好。上层工艺逻辑只和“FB实例”打交道,不直接碰软元件。这样改一台电机的控制逻辑,只需要改FB内部一处,所有实例同步生效;查故障时看FB输出的运行状态和故障标志,配合报警代码,基本能定位到具体设备。

1.3 从梯形图思维切换过来,必须先理解三个转变

第一,从“软元件思维”转向“变量思维”。梯形图里你会经常写“X0”“M100”“D20”,但在ST框架里,这些软元件最好只出现在最底层的映射程序里,其他逻辑层一律使用有名字的标签变量,比如bStartButton、mConveyorRun。这样程序的可读性和可搜索性会天差地别。

第二,从“扫描顺序决定逻辑”转向“调用关系决定逻辑”。梯形图的执行顺序是从上到下,很多人靠程序段摆放位置来实现优先级。ST里你更需要注意的是FB实例之间的调用关系和数据流:谁先执行、谁提供数据给谁,是显式调用得出的,不容易出现“把这段梯形图挪个位置逻辑就变了”的坑。

第三,从“每个功能单独写一遍”转向“封装一次,复用多次”。这就是FB功能块的价值。你写的不是一个“电机控制程序”,而是一个“电机控制模板”,每个电机只是模板的一个实例。这个转变是这套框架的核心。

2. 框架设计:四层结构怎么搭

2.1 一个典型设备例子的整体分层

我拿一台小型贴标机来举例,这是典型的FX3U应用场景。设备大概有:两个启动/停止按钮、一个急停、一个手动/自动切换旋钮;三四个传感器用来检测物料到位;一台输送电机、一台贴标电机(都是变频器驱动);一个贴标气缸;一个三色灯。整体I/O规模不超过32点。

这套设备用四层框架来组织,每层职责单一:

  • 第一层,输入处理层:把X输入、D寄存器里的通讯数据,统一映射到有意义的标签变量上。
  • 第二层,设备控制层:每个电机、气缸、变频器都是一个FB实例,接收指令,输出状态和故障。
  • 第三层,工艺逻辑层:自动运行的状态机、手动/自动切换、联锁保护都写在这里。
  • 第四层,输出映射层:把内部标签(比如mConveyorRun)最终映射回Y输出和通讯写寄存器。

这个分层思路从上位机开发里借鉴来的,PLC完全可以用。它的优势是:每层只干一件事,改I/O点位只动第一层和第四层,改工艺只动第三层,设备控制逻辑基本不动。

2.2 第一层与第四层:I/O映射的写法

在GX Works2的ST程序里,输入映射我习惯用一个单独的ST程序文件“IO_Map”,放在扫描执行的最前面。代码长这样:

(* 输入映射 *) bStartBtn := X0; (* 启动按钮,常开 *) bStopBtn := X1; (* 停止按钮,常开 *) bEStop := X2; (* 急停,硬接线常闭,正常为ON *) bModeSw := X3; (* 模式切换,OFF=手动 ON=自动 *) bMcConvey := X4; (* 输送电机接触器反馈 *) bMcLabel := X5; (* 贴标电机接触器反馈 *) bSensor1 := X6; (* 物料到位传感器 *) bSensor2 := X7; (* 贴标完成传感器 *)

输出映射:

(* 输出映射 *) Y0 := bConvRunOut; (* 输送电机变频器启动 --> Y1 := bLabelRunOut; (* 贴标电机变频器启动 *) Y2 := bCylinderOut; (* 贴标气缸电磁阀 *)

注意几个细节:

  • 急停信号的处理。如果急停按钮常闭点接入X2,正常工作时X2为ON,急停按下为OFF。所以安全逻辑里判断的是“bEStop = FALSE”时输出全部禁止。这个信号不要只映射一次就完,要让它参与所有FB实例的允许条件。

  • 标签变量命名规范。我用的前缀体系,b开头表示BOOL位,i开头表示INT整数,r开头表示REAL浮点数。比如bConvRunOut是BOOL型输出位,iFreqSet是INT型频率设置。这个规范坚持下来,程序读起来会非常舒服。

  • GX Works2里,ST程序可以直接使用X0、Y0这种软元件描述,也可以使用全局标签。我推荐的方式是:外部I/O用软元件直接映射到标签,内部逻辑使用全局标签。这样在线监控时看标签名,比看X0Y0直观得多。

2.3 第二层与第三层:FB调用区和状态机区

第二层我把所有FB实例的调用集中放在一个ST程序段里“Device_Control”。比如设备里有两台电机和一个气缸,就在VAR区声明三个FB实例,然后在同一扫描周期内依次调用。这样谁在运行、谁在故障、谁是停止状态,一眼就能扫出来。

第三层工艺状态机单独一个ST文件“Auto_Machine”,里面只写CASE状态机,不碰具体软元件。它给第二层“下达指令”——是启动输送带还是停贴标电机,全部通过标签变量实现。

我个人的经验是:IO映射程序、FB调用程序、状态机程序、输出映射程序这四个ST文件,在工程里按顺序排列,扫描顺序固定。即便某天换人接手,也能很快搞清楚程序脉络。

3. FB功能块实战:电机控制和报警管理

3.1 FB在GX Works2里怎么建、怎么存

FB功能块在GX Works2工程树里是独立的一类对象。右键“功能块”文件夹,新建数据,语言选“结构化文本(ST)”,名字取FB_Motor这样的格式。FB定义界面分几个区域,最上面是VAR_INPUT、VAR_OUTPUT、VAR_IN_OUT、VAR这些变量声明区,下面是逻辑代码区。

关键点:FB里声明变量时,要搞清楚每个变量的作用。

  • VAR_INPUT:外部传入的参数,可以理解成“旋钮和按钮”,只读。
  • VAR_OUTPUT:FB算完结果后输出去的量,外部可以读。
  • VAR_IN_OUT:既能传入也能改写的变量,相当于“接口兼内部寄存器”。FX3U上这个类型能用,但限制比较多,我基本不用,需要“回写”的场景直接用OUTPUT加一个中间变量搞定。
  • VAR:FB内部的“私有变量”,外部不关心,多实例时各自独立。

写FB时有一个黄金规则:FB内部的逻辑代码,绝对不要直接使用X、Y、M、D、T这些软元件。用了之后,同一个FB实例化两次,两个实例就会抢同一个软元件,程序跑起来逻辑必乱。正确做法是全部用VAR内部的标签变量,具体软元件分配交给编译器做。这也是“封装”的意义所在。

3.2 第一个FB:电机控制功能块完整代码

这是框架里最有价值的FB。我把电机的启停、故障锁存、反馈检测、手动/自动模式选择全部封装进去。代码如下:

FUNCTION_BLOCK FB_Motor VAR_INPUT bRunCmd : BOOL; (* 自动运行指令 *) bStopCmd : BOOL; (* 停止指令 *) bManualCmd : BOOL; (* 手动点动指令 *) bManualMode : BOOL; (* 手动模式标志 *) bResetCmd : BOOL; (* 故障复位指令 *) bFeedback : BOOL; (* 运行反馈信号 *) bThermo : BOOL; (* 热继/变频器故障信号 *) bSafetyOn : BOOL; (* 安全允许,急停未触发 *) iFbDelayCnt : INT; (* 反馈断开确认计数 *) END_VAR VAR_OUTPUT bRunOut : BOOL; (* 运行输出 *) bRunning : BOOL; (* 运行反馈状态 *) bFault : BOOL; (* 故障锁存 *) bReady : BOOL; (* 允许运行 *) END_VAR VAR bOn : BOOL; (* 内部运行锁存 *) bFaultLatch : BOOL; (* 内部故障锁存 *) bFbOld : BOOL; (* 反馈沿检测用 *) iCnt : INT; (* 反馈断开计数 *) END_VAR (* 故障复位,优先于故障置位 *) IF bResetCmd THEN bFaultLatch := FALSE; END_IF; (* 故障锁存 *) IF bThermo THEN bFaultLatch := TRUE; END_IF; (* 允许条件 *) bReady := (NOT bFaultLatch) AND bSafetyOn; (* 手动/自动启停逻辑 *) IF bManualMode THEN bOn := bManualCmd; (* 手动模式下点动跟随 *) ELSE IF bRunCmd OR (bOn AND NOT bStopCmd) THEN bOn := TRUE; ELSE bOn := FALSE; END_IF; IF bFaultLatch THEN bOn := FALSE; END_IF; END_IF; (* 输出 *) bRunOut := bOn AND bReady; (* 反馈状态处理:用沿检测和断开计数消除反馈抖动 *) bFbOld := bFeedback; IF bFeedback AND NOT bFbOld THEN bRunning := TRUE; END_IF; IF NOT bFeedback THEN iCnt := iCnt + 1; IF iCnt >= iFbDelayCnt THEN bRunning := FALSE; END_IF; ELSE iCnt := 0; END_IF;

这段代码的核心逻辑几句话能说清:故障状态锁存后必须复位才能清除;手动模式直接点动;自动模式收到运行指令并且没故障、允许条件满足,才输出运行;反馈消失后不是立即断开bRunning,而是等一小段时间去抖,防止接触器抖动或反馈线干扰造成误判。

关于去抖,我用了一个“扫描周期计数”的替代方案。严格来说它不等于定时器,时间精度取决于扫描周期,但对接触器反馈这种毫秒级抖动的场景完全够用。如果对时间精度有要求,就用FX3U的定时器T200,下面状态机部分会讲。

3.3 第二个FB:报警采集功能块完整代码

电机FB只管自己的故障,但设备层面需要把所有故障汇总起来,还要输出报警代码给触摸屏显示。这个报警FB也非常通用:

FUNCTION_BLOCK FB_Alarm VAR_INPUT bTrigger : BOOL; (* 报警源触发 *) bReset : BOOL; (* 报警复位 *) iCode : INT; (* 报警代码 *) END_VAR VAR_OUTPUT bAlarm : BOOL; (* 报警输出 *) iActCode : INT; (* 当前报警代码 *) END_VAR VAR bLatch : BOOL; (* 锁存位 *) END_VAR IF bReset THEN bLatch := FALSE; END_IF; IF bTrigger THEN bLatch := TRUE; END_IF; bAlarm := bLatch; IF bLatch THEN iActCode := iCode; END_IF;

这个FB的逻辑很简单:报警触发后立即锁存,输出报警标志和代码;收到复位信号才清除。报警代码在锁存期间保持不变,HMI读取iActCode就能显示对应的中文报警信息。

3.4 FB的实例化和调用必须注意的写法

FB定义好以后,在ST主程序里要实例化。VAR区里这样写:

VAR fbM1 : FB_Motor; fbM2 : FB_Motor; fbAlm1 : FB_Alarm; fbAlm2 : FB_Alarm; END_VAR

然后调用:

fbM1(bRunCmd := mConvAutoRun, bStopCmd := mConvStop, bManualCmd := mConvJog, bManualMode := bManualMode, bResetCmd := bResetAll, bFeedback := bMcConvey, bThermo := bConvFault, bSafetyOn := bEStop, iFbDelayCnt := 10, bRunOut => mConvRunOut, bRunning => mConvRunning, bFault => mConvFault, bReady => mConvReady); fbM2(bRunCmd := mLabelAutoRun, bStopCmd := mLabelStop, bManualCmd := mLabelJog, bManualMode := bManualMode, bResetCmd := bResetAll, bFeedback := bMcLabel, bThermo := bLabelFault, bSafetyOn := bEStop, iFbDelayCnt := 10, bRunOut => mLabelRunOut, bRunning => mLabelRunning, bFault => mLabelFault, bReady => mLabelReady);

这里有几个ST的调用语法细节:输入参数用“:=”赋值,输出参数用“=>”接收,参数之间用逗号分隔。三菱ST的FB调用必须把每个参数都写上,不像有些语言支持“省略参数用默认值”。这看起来很啰嗦,但也逼着每个实例的接线关系一目了然。

调用之后,主程序里直接用mConvRunOut、mLabelFault这些变量就行。比如输出映射层,直接把mConvRunOut赋值给Y0。再比如工艺逻辑里,如果自动状态机要判断“输送电机是否故障”,直接用IF mConvFault THEN……这就是FB的输出参与上层逻辑。

强烈建议每定义一个实例,就给它命一个和设备实际结构对应的名字。fbM1、fbM2这种名称只适合示例,现场设备最好用fbConvMotor、fbLabelMotor、fbPumpMotor这类名字,不然程序大了自己都会忘。

4. ST状态机编程:设备的核心控制逻辑

4.1 CASE状态机的ST写法

设备控制框架里,自动运行逻辑我几乎不用梯形图那种“步进指令STL”,而是在ST里直接写CASE状态机。三菱的ST语法里CASE语句是标准写法,分支条件可以是整数变量,非常适合做顺序控制。

下面是一个贴标设备的自动流程状态机,状态定义如下:

  • 0:待机,等待启动信号
  • 10:输送带运行,物料进给
  • 20:物料到位,气缸伸出贴标
  • 30:贴标保持,等待贴标完成传感器
  • 40:气缸缩回,循环结束

ST代码:

CASE autoState OF 0: (* 待机状态 *) mConvAutoRun := FALSE; mLabelAutoRun := FALSE; bCylinderOut := FALSE; IF bStartBtn AND bModeAuto THEN autoState := 10; END_IF; 10: (* 输送带运行,等待物料到位 *) mConvAutoRun := TRUE; IF bSensor1 THEN mConvAutoRun := FALSE; autoState := 20; END_IF; 20: (* 气缸伸出贴标 *) bCylinderOut := TRUE; autoState := 30; 30: (* 等待贴标完成传感器 *) IF bSensor2 THEN autoState := 40; END_IF; 40: (* 气缸缩回,回到待机 *) bCylinderOut := FALSE; autoState := 0; ELSE autoState := 0; END_CASE;

这段代码的逻辑非常直白。每个状态里只做两件事:置位本状态需要的输出,然后判断是否满足跳转到下一个状态的条件。所有的“步进感”都是靠“状态号变化”实现的,而不是靠梯形图的SET/RST一堆M去绕。

注意一个容易踩的坑:CASE分支里给输出赋值的操作,在状态跳转的瞬间,新状态的赋值会覆盖旧状态。比如状态10里mConvAutoRun := TRUE,检测到bSensor1后立刻把它置FALSE再跳20,这是允许的。但实时性敏感的场合,要注意状态跳转发生在扫描周期的中段,一次扫描内旧状态和新状态的逻辑都会执行。这通常不是问题,因为旧状态在前的赋值会被新状态的赋值覆盖,以新状态为准。

4.2 ST状态机里怎么用FX3U的定时器

在ST状态机里经常要用到“在这个状态待多久”的需求,比如气缸伸出后延时2秒再判断有没有到位。FX3U的梯形图里定时器用OUT T0 K20,ST里则是用带条件的TMR指令调用,这是很多从梯形图转过来的人会卡住的地方。

我给你一个典型用法。假设在状态20要气缸保持2秒:

CASE autoState OF 20: bCylinderOut := TRUE; TMR(T200, K200); (* T200 是10ms定时器,K200 = 2000ms *) IF T200 THEN autoState := 30; END_IF; END_CASE;

关键点:TMR指令必须放在条件内才会“条件计时”,否则每个扫描周期都会持续计时。当状态从20跳走之后,T200会因为没有继续调用而自动复位(TMR指令被“跳过”,相当于线圈断电)。这正是梯形图里定时器线圈的行为逻辑。

FX3U定时器编号规则我列在下面,方便对照:

定时器编号单位类型典型用途
T0~T199100ms普通定时器长时间延时
T200~T24510ms普通定时器短延时、去抖
T246~T2491ms累计累计定时器高频计测
T250~T255100ms累计累计定时器断电保持计时

强烈建议在ST程序里给定时器编号建一个“分配表”,哪个定时器给哪个逻辑用,写清楚。FX3U的定时器是全局的,虽然ST里不会像软元件那样冲突,但如果你在多个地方反复使用同一个T编号,逻辑互相干扰,排查起来比梯形图更痛苦。

4.3 急停、手动自动切换这类全局信号接入方式

框架最后要解决一个现实问题:急停、模式切换这种“全局信号”,怎么接入所有FB和状态机,又不至于在每个FB里都复制一堆条件。

我的做法是用一个专门的“安全链”程序段,在扫描最前面做全局计算:

bSafetyOn := bEStop AND NOT bFaultAllMinus;

其实更严谨的是:急停硬接线已经串在接触器回路里了,程序里的bSafetyOn只是做逻辑联锁和状态复位。然后每个FB的bSafetyOn输入都接这一个全局变量,状态机里也用它作为“允许进入自动”的前提。

手动和自动切换我建议做成“互斥信号”,用一个旋钮开关,X3为OFF手动,X3为ON自动:

bManualMode := NOT X3; bModeAuto := X3;

这俩信号进到FB里,手动模式下FB直接响应点动指令;自动模式下FB只响应状态机给的运行指令。切换的时候要注意:从手动切到自动那一刻,状态机必须强制回到待机状态0,并且所有输出先回到安全状态。我通常在模式切换沿上做状态复位:

IF bModeAuto AND NOT bModeAutoOld THEN autoState := 0; mConvAutoRun := FALSE; mLabelAutoRun := FALSE; bCylinderOut := FALSE; END_IF; bModeAutoOld := bModeAuto;

这里用了一个上升沿检测的标准写法:当前值真且上一扫描周期假,就认为“刚刚从OFF变成ON”。这也是ST里做沿检测最常用的方式。

5. 实测中的坑与排查技巧

5.1 编译报错和逻辑陷阱

这套框架我前后调试过好几轮,编译层面的坑主要集中在几个地方。

一是语法细节。ST对空格、换行、中英文标点敏感。GX Works2的ST编辑器支持中文注释,但括号、分号、冒号必须是半角英文符号。在中文输入法下写代码,经常整出“全角分号”,编译报错还不提示具体位置,非常恼人。我的办法是写完代码立刻“全选→格式化”,然后再编译。

二是数据类型不匹配。FX3U的ST对类型检查很严格。INT不能直接赋值给BOOL,BOOL也不能直接和INT做比较。有人习惯把“M0”这种位软元件当“0或1”的整数用,在ST里就要注意,该转换的显式转换。比如iTemp := INT_TO_INT(D100)这种,D100本身是16位数值,直接给INT变量是合法的,但如果是从BOOL转数值、或从DINT转INT,要显式用转换函数。

三是FB调用次数。一个FB实例在一个扫描周期里只能调用一次。如果你在状态机和手动逻辑里各调用了一次同一个实例,执行结果会被最后一次调用覆盖,输出来回跳,设备就会“鬼畜”。我在框架里强制规定:所有FB实例只允许在“Device_Control”这一个ST程序里调用,其他地方要用输出就用变量名,不再重复调用。

四是CASE语句漏写ELSE。如果autoState因为干扰变成了一个未定义的状态号,CASE语句会“什么都不执行”,输出保持上一个状态,设备可能卡死。所以CASE的结尾一定要有ELSE兜底,把状态强制拉回0,这是我在第一条代码里就写了ELSE的原因。

5.2 在线监控和调试ST程序的经验教训

GX Works2对FX3U的ST程序在线监控,体验只能说“能用,但别指望太好”。进入监控模式后,代码每行右边会显示当前值,基本能看清逻辑走到哪一步。但FB内部的VAR局部变量在在线监控里经常看不到实时值,或者显示延迟,这让调试FB内部逻辑变得很痛苦。

我自己的调试策略是“分层验证”。

第一步,验证IO映射。专门看全局标签的当前值,确认每个X输入、D寄存器都正确映射到了标签。连这个都没验证,后面全是白调。

第二步,验证FB外部接口。不看FB内部,直接监控FB输出的几个变量,比如mConvRunOut、mConvFault、mConvRunning,用强制按钮的方法触发各种输入条件,确认输出是否符合预期。如果输出逻辑不对,才进FB内部检查。

第三步,验证状态机。把autoState这个变量放到监控窗口里,让它实时显示当前状态号。设备跑起来,盯着状态号跳变,出问题就能立刻看出来是哪一步没跳。

关于仿真,GX Simulator对FX3U的ST程序支持比较弱,FB调用和定时器行为在仿真里不一定准。我强烈建议ST程序尽量在真机上调试。这不只是STM32那种“仿真代替实机”的思路,PLC的扫描周期、通讯时序、IO响应,仿真器很难模拟准确。

5.3 ST框架的资源开销与程序步管理

很多用户担心FX3U用ST会很占程序步,我的实测结论是:影响很小,但要注意FB实例的“放大效应”。

FX3U程序容量是64K步。一个电机FB内部逻辑大约折合200~300步,实例化3个就是不到1000步,占比很低。ST写的状态机和梯形图写的STL指令实现同样的顺序控制,程序步差异大约在10%~30%,毕竟ST生成的代码本身就是由基本指令组成的。

真正的坑是FB内部用了太多“额外变量”或“数组”。FX3U的ST支持简单数组,比如DREG[0..10],但数组运算在小型机上很消耗扫描周期,维护也麻烦。我的原则是:FB内部变量控制在10个以内,不要用多维数组,不要用REAL做大量浮点运算,FX3U的浮点运算本来就比INT慢不少。

还有一点,FX3U的标签变量和软元件之间有映射关系。在全局标签里定义的每个BOOL、INT变量,最终都会占用内部软元件资源。标签定义太多了(几百个以上),编译时会提示标签溢出。所以框架里的标签比梯形图时代的中间继电器数量多很多,但这个量级对FX3U来说完全撑得住,几百个标签没问题,真正要注意的是别把数组定义得太奢侈。

5.4 FB封装的反模式:什么都往FB里塞

最后说一个更偏设计层面的踩坑经验。

FB能提升复用性,但过度封装会让程序变得难懂。我见过有人做一个“超级设备FB”,把电机、气缸、报警、通讯全部塞进去,输入输出参数有二三十个,内部状态七拐八绕。结果一个新设备稍微一点差异,就不得不复制整个FB出来改,改完又怕影响其他实例。这个方向就反了。

我现在的设计原则是:FB只做“一类设备”的通用控制,粒度控制在“电机”“气缸”“阀门”“变频器”这个级别。工艺联锁和模式切换留在状态机里,不要让FB去理解整个设备的工艺流程。变频器FB可以包含MODBUS写频率的逻辑,但它不管“这台变频器在工艺里是收卷还是放卷”——那是状态机的事。

FB参数个数如果超过15个,就要考虑是不是拆分成两个小FB。输入输出参数太多,阅读和调用都很费劲,拆小反而更清晰。这方面我没有硬性标准,但你写FB时如果觉得“这个接口列表长得像一篇作文”,那就该拆了。

6. 扩展:通讯与多设备联动

6.1 变频器MODBUS控制在框架里的位置

前面FB_Motor处理的是接触器型电机,但很多设备里电机是变频器驱动的,频率设定、状态读取都要走通讯。FX3U常用的方案是挂FX3U-485ADP-MB模块,用MODBUS RTU协议做主站,通过ADPRW指令读写变频器。在ST框架里,我建议把整个“变频器通讯控制”也封装成FB。

这个FB和FB_Motor的区别在于,它内部需要调用ADPRW指令去读写变频器的寄存器。典型逻辑是:启动时先写运行命令寄存器,再写频率设定寄存器;运行中周期读取运行状态和电流;故障时读故障码。这些全部封装在FB内部,外部只需给指令和频率,收状态和故障。

不过这里有一个绕不开的坑:ADPRW同一时间只能执行一个站号的一条指令。如果你有3台变频器,3个FB实例同时往里挤,通讯帧会打架。解决方案是在框架里加一个通讯仲裁位,用一个专门的“通讯调度”ST程序统一管理发送队列,变频器FB只是把“请求发送”标志置位,真正调用ADPRW的只有调度程序那一处。这样程序结构是干净了,代价是通讯实时性有所下降,但对大多数设备控制逻辑足够了。

6.2 从一个框架到多个类似设备的复制方法

这套框架最大的收益是“第二次使用”。当你在这台贴标机上把框架搭好,下一个项目是做分选机或者包装线,复制框架的步骤极其机械。

第一步,复制整个工程,删掉不需要的FB实例和状态机内容。第二部,更新IO映射和输出映射,改成新设备的点位表。第三部,设备控制层里,新的电机、气缸各建实例,改参数接信号。第四部,按新工艺重写状态机。剩下的报警汇总、模式切换、安全链逻辑,基本不用动。

这里特别提醒:FB内部代码除非有bug,否则不要在新项目里去改。框架的价值在于稳定,你每次复制都改FB内部,就等于放弃的复用的意义。如果新设备的需求差异很大,优先考虑“是不是能拆成两个FB组合”,而不是“把旧FB改成两用”。

我自己在这套框架跑通后,后续项目里FB_Motor、FB_Alarm基本原封不动从旧工程拷来,新的开发量主要集中在IO映射和状态机。设备交到售后手里,售后人员反馈排查故障比以前快很多,因为报警代码和FB输出能直接告诉他“第3号电机故障”,剩下的事就是去现场查接触器和电机了。

这个内容后续还可以再扩展,比如把MODBUS通讯调度的细节点写出来,或者把手动调试模式做得更精细,方便现场不带电脑试车。以后有时间我再单独开一篇写写。先分享到这儿,各位兄弟如果在FX3U上搞ST遇到什么坑,欢迎交流。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询