4、AALC数据类型与变量:基本数据类型、用户自定义数据类型、变量作用域与生命周期
数据类型和变量,是写控制逻辑的基石。说白了,你连数据长什么样、能存多大、活多久都搞不清楚,那程序跑起来肯定要出乱子。我见过不少新手,上来就写逻辑,结果因为数据类型不匹配,现场设备直接罢工。今天咱们就把这块彻底捋清楚。
4.1 基本数据类型:BOOL、INT、REAL 等
AALC 的基本数据类型,跟大多数工业控制器差不多。但有几个细节,我建议你特别留意。
| 类型 | 关键字 | 位宽 | 取值范围 | 典型用途 |
|---|---|---|---|---|
| 布尔 | BOOL | 1 bit | TRUE / FALSE | 开关信号、状态标志 |
| 字节 | BYTE | 8 bit | 0 ~ 255 | 原始数据、通信字节 |
| 字 | WORD | 16 bit | 0 ~ 65535 | 寄存器值、位组合 |
| 双字 | DWORD | 32 bit | 0 ~ 4294967295 | 累加器、时间戳 |
| 短整数 | SINT | 8 bit | -128 ~ 127 | 小范围计数 |
| 整数 | INT | 16 bit | -32768 ~ 32767 | 常规计数、索引 |
| 双整数 | DINT | 32 bit | -2147483648 ~ 2147483647 | 大范围计数、位置 |
| 实数 | REAL | 32 bit | ±1.18e-38 ~ ±3.4e38 | 模拟量、PID 运算 |
嗯,这里要注意。很多工程师习惯用 INT 做所有整数运算。但我在项目中遇到过,一个计数器的值超过了 32767,结果直接溢出,设备乱跳。从那以后,凡是可能超过 1000 的计数,我统一用 DINT。你想想看,多花一个字节,换来的是安全,值不值?
核心原则:能用 DINT 就别用 INT,能用 REAL 就别用 INT 做除法。数据类型的选型,决定了你程序的鲁棒性。
4.2 用户自定义数据类型:STRUCT 与 ENUM
基本类型够用吗?说实话,写小项目还行。一旦项目大了,比如一个工位有几十个参数,你再用一堆散乱的 INT、BOOL,那代码简直没法看。这时候,STRUCT 和 ENUM 就派上用场了。
4.2.1 STRUCT(结构体)
STRUCT 就是把多个相关数据打包成一个整体。我个人习惯,把每个电机、每个阀、每个传感器的数据都定义成 STRUCT。这样不仅代码整洁,而且复用性极高。
// 定义一个电机数据结构
TYPE MotorData :
STRUCT
Speed : INT; // 当前转速
Torque : REAL; // 当前扭矩
Temperature : INT; // 绕组温度
RunHours : DINT; // 累计运行时间
IsRunning : BOOL; // 运行状态
FaultCode : BYTE; // 故障代码
END_STRUCT
END_TYPE
// 声明变量
VAR
ConveyorMotor : MotorData;
PumpMotor : MotorData;
END_VAR
// 使用
ConveyorMotor.Speed := 1500;
IF ConveyorMotor.Temperature > 85 THEN
ConveyorMotor.FaultCode := 16#A1;
END_IF
我曾经接手过一个项目,前任工程师用了几十个散落的变量来管理 8 台电机。我花了三天时间,全部重构为 STRUCT。结果呢?代码量减少了 40%,而且新同事上手快多了。这就是 STRUCT 的魅力。
4.2.2 ENUM(枚举)
ENUM 用来定义一组有名字的常量。比如设备状态、操作模式、故障类型。用 ENUM 代替魔法数字,代码可读性直接拉满。
// 定义设备状态枚举
TYPE MachineState :
(
Idle := 0,
Running := 1,
Paused := 2,
Error := 3,
EmergencyStop := 4
) BYTE;
END_TYPE
// 声明变量
VAR
CurrentState : MachineState;
END_VAR
// 使用
CASE CurrentState OF
MachineState#Idle:
// 待机逻辑
MachineState#Running:
// 运行逻辑
MachineState#Error:
// 错误处理
END_CASE
我的小技巧:ENUM 的底层类型我习惯用 BYTE 或 SINT,省内存。但如果你枚举值超过 127,记得用 INT。别问我怎么知道的——有一次我枚举了 150 个状态,用了 SINT,结果编译报错,查了半天。
4.3 变量作用域与生命周期
变量不是随便定义的。它在哪里能用,能活多久,这些都有讲究。搞不清楚作用域,程序跑飞了都不知道原因。
4.3.1 变量作用域
AALC 的作用域分三种:局部、全局、模块级。
| 作用域 | 关键字 | 可见范围 | 生命周期 |
|---|---|---|---|
| 局部变量 | VAR | 仅当前 POU(程序组织单元) | POU 执行期间 |
| 全局变量 | VAR_GLOBAL | 整个项目 | 程序运行全程 |
| 模块变量 | VAR(在 FB 内) | 当前功能块实例 | 功能块实例存活期间 |
为什么会这样设计?你想想看,如果所有变量都是全局的,那一个地方改了,所有地方都受影响。调试的时候,你根本不知道是谁动了你的数据。所以,我的原则是:能用局部,绝不用全局。
避坑指南:我曾经在一个项目中,为了图方便,把 50 多个变量都定义成了全局。结果现场调试时,一个定时器中断里改了某个全局标志,主程序完全不知道,导致设备顺序错乱。查了整整两天才找到原因。从那以后,全局变量我严格控制,每个全局变量都要写注释说明「谁写、谁读」。
4.3.2 变量生命周期
生命周期,说白了就是变量从出生到死亡的时间。
- 局部变量:每次进入 POU 时创建,退出时销毁。注意,它不会保持上次的值。如果你需要保持,要用 RETAIN 关键字。
- 全局变量:程序启动时创建,程序停止时销毁。全程有效。
- 功能块内部变量:功能块实例化时创建,实例销毁时释放。每个实例都有自己的副本。
- 保持型变量:用 RETAIN 修饰,断电后数据不丢失。常用于掉电保持的场景。
// 保持型变量示例
VAR_GLOBAL RETAIN
TotalProductionCount : DINT; // 总产量,断电不丢失
LastRecipeID : INT; // 上次使用的配方号
END_VAR
重要提醒:RETAIN 变量不是无限用的。每个控制器都有保持型内存上限。我见过有人把整个数据库都声明成 RETAIN,结果内存爆了,程序下载都下不进去。合理规划,只保持真正需要的数据。
4.4 知识体系结构图
下面这张图,把本章的知识脉络串起来了。你可以把它当作一个快速索引。
好了,数据类型和变量这块,咱们就聊到这儿。记住一句话:选对类型,程序就成功了一半。剩下的,就是逻辑本身了。
公众号:蓝海资料掘金营,微信deep3321