|
3个FB变量陷阱+4个实战技巧,R系列入门先避坑 你写了一个电机控制FB,第一次运行完美,第二次调用电机突然不动了。排查三小时,发现是静态变量“记住”了上次的中间状态。90%的R系列初学者在FB上栽过这个跟头。FB的本质是“带记忆的函数”,但记忆在哪里、怎么管理、如何清零?这3个变量陷阱,今天一次讲透。 一、FB变量类型:不是所有变量都“活”在同一个世界 1.1 四类变量,各自为政 MELSEC iQ-R的FB支持四种变量声明区,每个区的生命周期和作用域不同。一张表秒懂: | 变量类型 | 关键字 | 数据流向 | 外部访问 | 保持特性 | 典型用途 | | 输入参数 | VAR_INPUT | 外部→内部 | 只读 | 每次调用刷新 | 设定值、使能信号 | | 输出参数 | VAR_OUTPUT | 内部→外部 | 只写 | 每次调用刷新 | 结果、状态位 | | 双向参数 | VAR_IN_OUT | 双向传递 | 读写 | 与外部变量绑定 | 累加器、状态机 | | 内部变量 | VAR | 仅内部 | 不可见 | 保持(不掉电) | 中间变量、计数器 | 核心差异: 只有 VAR区的变量会“记住”上次调用的值, VAR_INPUT和 VAR_OUTPUT每次调用时根据实参刷新。 VAR_IN_OUT则直接操作外部变量本身。 1.2 代码示例:温度监控FB ( FB定义:温度监控功能块 )FUNCTION_BLOCK FB_TempMonitor VAR_INPUT Temp_Actual : REAL; ( 实际温度,单位℃ ) Temp_Set : REAL := 100.0; ( 目标温度,默认100℃ ) END_VAR VAR_OUTPUT Heater_On : BOOL; ( 加热器控制信号 ) Alarm : BOOL; ( 超限报警 ) END_VAR VAR Counter : INT := 0; ( 内部计数器,保持型 ) Previous_Temp : REAL; ( 上一次温度值,用于判断变化率 ) END_VAR ( 逻辑:偏差大于5℃开启加热,计数器记录连续超限次数 ) IF (Temp_Actual < Temp_Set - 5.0) THEN Heater_On := TRUE; Counter := Counter + 1; ELSE Heater_On := FALSE; Counter := 0; END_IF; IF Counter > 10 THEN ( 连续11次检测到超差 ) Alarm := TRUE; END_IF; Previous_Temp := Temp_Actual; ( 保存当前值用于下一周期 ) END_FUNCTION_BLOCK 1.3 调用时别踩坑 ( 主程序调用:实例化两个温度监控块 )PROGRAM Main VAR Mon1 : FB_TempMonitor; ( 实例1 ) Mon2 : FB_TempMonitor; ( 实例2 ) ActualTemp1 : REAL := 98.0; ActualTemp2 : REAL := 95.0; Output1 : BOOL; Output2 : BOOL; Alarm1 : BOOL; Alarm2 : BOOL; END_VAR Mon1(Temp_Actual := ActualTemp1, ( 输入赋值 ) Temp_Set := 100.0, Heater_On => Output1, ( 输出赋值 ) Alarm => Alarm1); Mon2(Temp_Actual := ActualTemp2, Temp_Set := 100.0, Heater_On => Output2, Alarm => Alarm2); END_PROGRAM 注意: Mon1和 Mon2各自拥有独立的内部变量 Counter。即使 Mon1的计数器到10报警, Mon2的计数器仍然从0开始。这就是FB的封装性——也是“记忆”的来源。 悬念: 既然内部变量会保持,那如果我想让FB每次调用都从初始状态开始,是不是给变量赋初值就行了?答案会让你吃惊——往下看。 二、静态变量的保持陷阱:初值不等于每次复位 2.1 你以为的初值,只在第一次起作用 大多数工程师在FB内部声明变量时写Counter : INT := 0;,然后默认每次调用都会把Counter重置为0。错! R系列的FB内部变量初值只在第一次实例化时赋值,后续调用都不会重新初始化。也就是说: ●第一次调用:Counter = 0 → 执行逻辑,可能变成1。 ●第二次调用:Counter = 1 → 不会变回0,直接从上一次结果继续。 这个特性在累计计数、状态保持等场景很有用,但在“每次独立计算”的场景就是灾难。 2.2 实战案例:为啥我的脉冲计数器不准? 需求: 一个FB统计外部脉冲上升沿次数,每调用一次输出当前计数。 错误写法:FUNCTION_BLOCK FB_PulseCounter_ERRVAR_INPUT Pulse : BOOL; ( 脉冲信号 ) END_VAR VAR_OUTPUT Count : INT := 0; ( 输出计数 ) END_VAR VAR Edge_Detect : BOOL; ( 上升沿检测变量 ) Temp : INT := 0; END_VAR ( 上升沿检测 ) IF (Pulse AND NOT Edge_Detect) THEN Temp := Temp + 1; END_IF; Edge_Detect := Pulse; Count := Temp; ( 输出内部临时变量 ) END_FUNCTION_BLOCK 调用结果为: 每次扫描周期, Count持续累加,不会清零。如果你希望每次调用独立累计(比如在多个不同任务中分别计数),则每个实例的计数器独立,但若在一个循环中连续调用多次,则 Count会不断增长,输出值不是预期。 正确做法: 把 Count放到 VAR_OUTPUT中,让外部变量决定是否重置。 FUNCTION_BLOCK FB_PulseCounter_OKVAR_INPUT Pulse : BOOL; Reset : BOOL; ( 复位信号 ) END_VAR VAR_OUTPUT Count : INT; ( 每次调用输出当前累计值 ) END_VAR VAR Edge_Detect : BOOL; InternalCount : INT := 0; END_VAR IF Reset THEN InternalCount := 0; ELSIF (Pulse AND NOT Edge_Detect) THEN InternalCount := InternalCount + 1; END_IF; Edge_Detect := Pulse; Count := InternalCount; END_FUNCTION_BLOCK 关键点: 只有 VAR区的变量保持, VAR_OUTPUT每次调用根据逻辑刷新。如果希望外部能随时清零,就用显式复位引脚。 2.3 数据对比:保持型 vs 非保持型 | 场景 | 使用VAR | 使用VAR_OUTPUT | 结果 | | 状态机 | 保持状态 | 每次输出瞬时值 | 正确 | | 累计计数器 | 保持累计 | 输出当前累计 | 正确 | | 一次性数学运算 | 错误保持中间值 | 正确输出结果 | 运算错误 | 悬念: 内部变量保持特性已经让很多人头疼,但更隐蔽的是 VAR_IN_OUT——它直接把外部变量的“命”交到FB手里,操作不当会导致整个程序逻辑混乱。下一章揭露它的致命陷阱。 三、VAR_IN_OUT的秘密:你不能传入常数的真正原因 3.1 双向传递,而非只读/只写 VAR_IN_OUT是三菱FB中最容易被误解的类型。它不是 VAR_INPUT的加强版,也不是 VAR_OUTPUT的复制品。它的本质是 引用(Reference):FB内部直接操作外部变量的内存地址。这意味着: ●你不能传入常数(如123或TRUE),因为常数没有内存地址。 ●外部变量的任何修改都会立刻反馈到内部,反之亦然。 ●一个VAR_IN_OUT变量在调用时只能映射到一个全局标签或另一个实例的输出,不能是临时变量。 3.2 代码对比:用VAR_IN_OUT实现数据交换 需求: 两个电机速度互相取最大值,且一个电机速度变化时另一个自动跟踪。 FUNCTION_BLOCK FB_SpeedSyncVAR_IN_OUT Speed_A : INT; ( 电机A速度,双向传递 ) Speed_B : INT; ( 电机B速度 ) END_VAR VAR MaxSpeed : INT; END_VAR MaxSpeed := MAX(Speed_A, Speed_B); Speed_A := MaxSpeed; ( 修改外部变量A ) Speed_B := MaxSpeed; ( 修改外部变量B ) END_FUNCTION_BLOCK 调用:PROGRAM MainVAR M1_Speed : INT := 1000; M2_Speed : INT := 1200; Sync1 : FB_SpeedSync; END_VAR Sync1(Speed_A := M1_Speed, ( 必须传变量,不能传1000 ) Speed_B := M2_Speed); ( 执行后,M1_Speed和M2_Speed都变为1200 ) END_PROGRAM 3.3 常见错误:误用VAR_INPUT导致数据不同步 如果改用VAR_INPUT传入两个速度,FB内部只能读取,无法改写外部变量。要实现相同功能,必须增加两个VAR_OUTPUT,然后外部再赋值回去。代码膨胀一倍,效率降低。 性能对比: 使用 VAR_IN_OUT时,CPU只需一次内存地址传递;使用 VAR_INPUT+ VAR_OUTPUT需要两次数据拷贝和一次外部赋值。对于高速控制周期(如0.5ms),这种浪费可能引发任务超时。 悬念: 变量陷阱解决了,如何利用FB的“记忆”特性设计高级功能?接下来用3段代码教你用FB实现状态机——这是入门到进阶的分水岭。 四、实战技巧:用FB静态变量实现状态机(认知缺口) 4.1 状态机的本质:依赖“记忆” 状态机需要记住当前处于哪个状态,并根据输入和当前状态决定下一状态和输出。这个“记忆”正是FB内部变量VAR的强项。 4.2 三段代码:状态机FB Step 1: 定义状态类型和FB( 枚举状态类型 )TYPE Enum_State : (IDLE, RUNNING, ERROR) := IDLE; END_TYPE FUNCTION_BLOCK FB_StateMachine VAR_INPUT Start : BOOL; Stop : BOOL; Fault : BOOL; Reset : BOOL; END_VAR VAR_OUTPUT Ready : BOOL; Running : BOOL; Error : BOOL; END_VAR VAR CurrentState : Enum_State := IDLE; ( 保持当前状态 ) Timer : UINT := 0; ( 状态延时计数器 ) END_VAR Step 2: 实现状态转移逻辑CASE CurrentState OFIDLE: Ready := TRUE; Running := FALSE; Error := FALSE; IF Start AND NOT Fault THEN CurrentState := RUNNING; Timer := 0; END_IF; RUNNING: Ready := FALSE; Running := TRUE; Error := FALSE; Timer := Timer + 1; IF Stop OR Fault THEN CurrentState := ERROR; ELSIF Timer > 1000 THEN ( 运行超时保护 ) CurrentState := ERROR; END_IF; ERROR: Ready := FALSE; Running := FALSE; Error := TRUE; IF Reset THEN CurrentState := IDLE; Timer := 0; END_IF; END_CASE; END_FUNCTION_BLOCK Step 3: 主程序调用并观察结果PROGRAM MainVAR FSM1 : FB_StateMachine; Cmd_Start : BOOL := TRUE; Cmd_Stop : BOOL := FALSE; Fault_Signal : BOOL := FALSE; Reset_Signal : BOOL := FALSE; Out_Ready : BOOL; Out_Running : BOOL; Out_Error : BOOL; END_VAR FSM1(Start := Cmd_Start, Stop := Cmd_Stop, Fault := Fault_Signal, Reset := Reset_Signal, Ready => Out_Ready, Running => Out_Running, Error => Out_Error); END_PROGRAM 关键点: CurrentState是 VAR区变量,它在每个扫描周期都保持当前状态,不会因为调用结束而丢失。这正是FB实现状态机的核心——用一个变量跨越多个扫描周期。 4.3 参数验证表 | 调用周期 | Start | Stop | Fault | Reset | 当前状态 | 输出 | | 1 | TRUE | FALSE | FALSE | FALSE | IDLE→RUNNING | Running=TRUE | | 2 | FALSE | TRUE | FALSE | FALSE | RUNNING→ERROR | Error=TRUE | | 3 | FALSE | FALSE | FALSE | TRUE | ERROR→IDLE | Ready=TRUE | 认知深化: 许多人以为状态机必须用全局变量或专门的库,但其实一个FB内部声明一个枚举变量就够了。三菱R系列的FB天然支持这种“跨周期记忆”,无需额外成本。 悬念: 既然FB内部变量一直保持,那如果CPU断电会怎样?答案:背景数据块会由电池备份保存,除非你手动清除。想知道如何安全地初始化所有FB背景数据?下回分解。 总结:5个关键点 + 互动问题 你必须记住的5条铁律:1.VAR_INPUT和VAR_OUTPUT是“一次性”的:每次调用根据实参刷新,不保存历史。 2.VAR是“永久记事本”:只有它能在不同扫描周期之间保持值,初值只在实例化时有效。 3.VAR_IN_OUT是“共享内存”:直接操作外部变量地址,不能传入常量,修改会实时反馈。 4.FB实例化自动分配背景数据块:每个实例独立持有内部变量,相互隔离。 5.状态机=VAR+枚举+CASE:不要写全局变量,一个FB内部搞定。 互动问题:你的FB背景数据块占了多少内存?有没有算过最大实例化数量?你认为R系列200万个FB实例的限制是瓶颈还是冗余?留言区说出你的项目规模。 推荐阅读 摘要:今天深入学习静态代码分析技术,这是安全审计的核心技能。从 Python AST 模块到检测模式设计,收获满满! 发布于 202603 01-Python 环境搭建与第一个脚本 发布于 202603 【优化】Python代码优化与调试技巧 发布于 202603 KEYWORDS MELSEC, iQ-R, IL, 三菱, 函数 如果你觉得这篇文章有帮助,请点个在看,分享给更多需要的人! 关注我,获取更多实用干货~ 有问题欢迎评论区留言交流! 免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |