3、数据类型与变量:基本数据类型(BOOL, INT, REAL, TIME),用户自定义数据类型(UDT),全局变量与局部变量。
各位好,咱们今天聊聊数据类型和变量。这玩意儿,说白了就是PLC程序的“砖瓦”。砖瓦没选对,墙砌得再高也得塌。我见过太多新手,甚至一些老手,在这上面栽跟头。
你想想看,一个BOOL变量你非要用INT去存,不是不行,但浪费内存不说,别人看你的程序还得猜半天。咱们做工程,讲究的是“精准”和“规范”。
3.1 基本数据类型:PLC的“原子”
基本数据类型,就是PLC能认识的最基础的数据单元。就像化学里的元素周期表,你得先认识它们。
| 数据类型 | 关键字 | 位宽 | 取值范围 | 典型用途 |
|---|---|---|---|---|
| 布尔型 | BOOL | 1位 | TRUE / FALSE | 开关、按钮、指示灯、状态位 |
| 整型 | INT | 16位 | -32768 ~ 32767 | 计数器、小范围数值、索引 |
| 浮点型 | REAL | 32位 | ±1.18e-38 ~ ±3.40e+38 | 模拟量、PID运算、温度压力 |
| 时间型 | TIME | 32位 | 0ms ~ 24天20小时31分23秒647ms | 定时器设定值、延时时间 |
核心要点: 选型时,能用BOOL别用INT,能用INT别用REAL。REAL运算会占用更多CPU资源,而且有精度问题。我在一个高速包装机上吃过亏,用REAL做累加,结果小数点后几位累积误差导致定位不准。后来全改成DINT,问题就解决了。
3.1.1 BOOL:最基础的“是”与“否”
BOOL变量,就两个值:TRUE和FALSE。在PLC里,它对应一个“位”。
我个人习惯,把所有BOOL变量命名时都加上“b”前缀,比如 bStartButton、bMotorRunning。这样一眼就能看出这是个开关量。
// 声明一个BOOL变量
VAR
bConveyorRun : BOOL := FALSE; // 传送带运行状态,初始为停止
END_VAR
// 使用
IF bConveyorRun THEN
// 传送带正在运行,执行相关逻辑
ELSE
// 传送带停止
END_IF
3.1.2 INT:整数世界的主力军
INT是16位有符号整数。在大多数中小型项目中,它够用了。但要注意,别让它溢出。
我曾经遇到一个案例:一个计数器用INT,计数值到了32767后,再加1就变成了-32768。结果设备莫名其妙地反转。排查了半天,才发现是数据类型溢出了。从那以后,凡是可能超过10000的计数,我全用DINT(32位整数)。
VAR
iProductCount : INT := 0; // 产品计数
END_VAR
// 计数逻辑
iProductCount := iProductCount + 1;
// 安全处理:防止溢出
IF iProductCount > 32000 THEN
iProductCount := 0; // 或者报警
END_IF
3.1.3 REAL:模拟量的“翻译官”
REAL是浮点数,用来处理带小数点的数值。比如温度25.5℃,压力0.35MPa。
嗯,这里要注意:REAL不能用来做精确的等于比较。因为浮点数在计算机里是近似存储的。你写 IF MyReal = 0.1 THEN,很可能永远不成立。
避坑指南: 我曾经在PID调节时,用REAL做积分项累加。结果因为浮点精度问题,积分项始终无法归零,导致系统有静差。后来改用DINT做累加,最后再转成REAL输出,问题才解决。所以,做精确计算时,尽量用整数,最后再转换。
VAR
rTemperature : REAL := 25.0; // 温度值
END_VAR
// 正确的比较方式:用范围判断
IF rTemperature >= 24.9 AND rTemperature <= 25.1 THEN
// 认为温度在25℃附近
END_IF
3.1.4 TIME:时间管理的利器
TIME类型,专门用来存时间长度。比如定时器设定值T#5s,延时时间T#100ms。
我个人习惯,所有时间常量都用TIME类型,而不是用INT乘以1000。这样代码可读性高,别人一看就知道是时间。
VAR
tDelayTime : TIME := T#2s500ms; // 延时2.5秒
tElapsed : TIME := T#0ms; // 已过时间
END_VAR
// 使用TON定时器
TON_1(IN := bStart, PT := tDelayTime, Q => bDone, ET => tElapsed);
3.2 用户自定义数据类型(UDT):你的“乐高积木”
基本数据类型就像单个的乐高颗粒。但你要搭一个复杂的机器人,总不能一个个颗粒去拼吧?UDT就是让你把多个颗粒拼成一个“模块”。
说白了,UDT就是结构体。你可以把相关的数据打包在一起。
我的经验: 在大型项目中,我几乎每个设备都会定义一个UDT。比如一个电机,我会把它的启动按钮、停止按钮、运行反馈、故障信号、电流值、转速值全部打包成一个 Motor_DataType。这样管理起来,清爽得很。
// 定义一个电机数据类型的UDT
TYPE Motor_DataType :
STRUCT
bStartCmd : BOOL; // 启动命令
bStopCmd : BOOL; // 停止命令
bRunFeedback : BOOL; // 运行反馈
bFault : BOOL; // 故障信号
rCurrent : REAL; // 电流值
rSpeed : REAL; // 转速值
tRunTime : TIME; // 累计运行时间
END_STRUCT
END_TYPE
// 使用UDT声明变量
VAR
stMotor1 : Motor_DataType; // 1号电机
stMotor2 : Motor_DataType; // 2号电机
END_VAR
// 访问UDT成员
stMotor1.bStartCmd := TRUE;
stMotor1.rCurrent := 12.5;
3.3 全局变量与局部变量:你的“领地”意识
变量放在哪,决定了谁能访问它。这就像公司的文件柜——全局变量是公共区域,谁都能翻;局部变量是你抽屉里的私人物品。
| 变量类型 | 声明位置 | 作用域 | 生命周期 | 典型用途 |
|---|---|---|---|---|
| 全局变量 | 全局变量列表(GVL) | 整个项目所有POU | 程序运行期间一直存在 | 系统参数、设备状态、共享数据 |
| 局部变量 | POU内部(程序/功能块/函数) | 仅限当前POU | POU执行时存在,结束后释放 | 临时计算、中间结果、循环变量 |
3.3.1 全局变量:方便,但别滥用
全局变量确实方便。你在任何地方都能读写它。但方便的背后是风险。
我见过一个项目,全局变量用了上千个。结果呢?程序里到处都是 g_xxx,改一个变量,不知道会影响多少地方。调试时,一个变量莫名其妙变了,查都查不到是谁改的。
避坑指南: 我曾经接手一个项目,全局变量满天飞。一个电机状态,有3个不同的全局变量在控制。结果现场调试时,电机时转时停,查了整整两天。最后发现是不同程序段对同一个全局变量做了冲突操作。从那以后,我定了个规矩:能用局部变量,绝不用全局变量。非要用的,必须加注释说明用途和修改者。
// 全局变量列表(GVL)
VAR_GLOBAL
g_bSystemRun : BOOL := FALSE; // 系统运行标志
g_rSystemTemp : REAL := 25.0; // 系统温度
g_iAlarmCount : INT := 0; // 报警计数
END_VAR
3.3.2 局部变量:你的“私人空间”
局部变量只在你当前写的程序、功能块或函数里有效。别人看不到,也改不了。这大大降低了程序的耦合度。
我个人习惯,所有临时计算、循环变量、中间状态,全用局部变量。这样,我写的功能块拿到别的项目里,直接复制粘贴就能用,不用担心变量冲突。
// 在功能块内部声明局部变量
FUNCTION_BLOCK FB_MotorControl
VAR_INPUT
bStart : BOOL; // 输入变量
bStop : BOOL;
END_VAR
VAR_OUTPUT
bRun : BOOL; // 输出变量
END_VAR
VAR
// 局部变量:仅在此功能块内有效
iCounter : INT := 0;
bInternalFlag : BOOL;
tTimer : TIME;
END_VAR
// 逻辑处理
IF bStart THEN
bInternalFlag := TRUE;
END_IF
IF bInternalFlag THEN
iCounter := iCounter + 1;
IF iCounter >= 100 THEN
bRun := TRUE;
END_IF
END_IF
3.4 知识体系总览
说了这么多,咱们用一张图来梳理一下本章的核心逻辑。数据类型和变量,就像程序的骨架和血肉,选对了,程序就成功了一半。
好了,数据类型和变量这块,咱们就聊到这儿。记住,选型时多想想“这个数据到底需要多大范围?会被谁访问?”,养成好习惯,后面写程序会省心很多。