学PLC这件事很多人一开始觉得无非就是 ladder、ST、FBD 来回切换变量声明随便写写就能跑。可真跑到第二十篇之后你会发现程序报错、数据跳变、在线修改失败十个里有八个跟数据类型有关系。这一篇我专门把 CoDeSys 里的数据类型做个系统总结分成两大部分一个是怎么选型选得聪明才能少踩坑另一个是怎么省内存尤其是当你的程序跑到几万行、控制器资源吃紧的时候这些细节能帮你把占用硬生生抠下来。1. 先看清楚 CoDeSys 的数据类型家族1.1 标准类型其实比你想的要多CoDeSys 基于 IEC 61131-3 标准标准类型基本信息如下表所示里面很多类型在初学者阶段不会接触但后面的通信、运动控制里会反复出现。类型位数范围/说明BOOL8位1位有效TRUE / FALSEBYTE8位0 ~ 255WORD16位0 ~ 65535DWORD32位0 ~ 4294967295SINT8位-128 ~ 127USINT8位0 ~ 255INT16位-32768 ~ 32767UINT16位0 ~ 65535DINT32位-2147483648 ~ 2147483647UDINT32位0 ~ 4294967295LINT64位约 ±9.2×10^18REAL32位浮点约7位有效数字LREAL64位浮点约15位有效数字STRING可变默认 STRING(80)按字节存储WSTRING可变默认 WSTRING(80)按双字节存储TIME32位毫秒级时间常见范围约0~49天LTIME64位纳秒级时间TOD32位时刻如 12:30:45.123DATE32位日期如 D#2026-01-15DT32位日期时刻初学者最直观的感受是类型不匹配直接报错不像某些上位机脚本那样自动转换。这个严格性其实是好事它逼着你在写程序之前就想清楚这个变量到底是什么。1.2 自定义类型 UDT把数据结构化除了标准类型CoDeSys 里还允许自定义类型UDT常见两种结构体 STRUCT 和枚举 ENUM。结构体的价值在于把一组关联变量打包成一个整体。比如一个伺服轴它有当前位置、目标位置、速度、报警标志、使能标志你当然可以声明五个独立变量但更好的做法是定义一个“轴状态”结构体TYPE ST_Axis : STRUCT bEnable : BOOL; bAlarm : BOOL; diPosition : DINT; diTarget : DINT; rSpeed : REAL; END_STRUCT END_TYPE以后每一个轴就声明成一个 ST_Axis 实例程序里用点号访问内部字段代码长度和可读性都会改善。需要注意UDT 只是数据类型本身不占用内存声明成变量之后才占用内存。枚举类型适合替代魔法数字。比如设备状态用 0、1、2、3 表示时间久了没人记得清用枚举能让别人一眼看懂TYPE E_State : ( State_Init : 0, State_Running : 1, State_Error : 2, State_Stop : 3 ) : State_Init; END_TYPE我建议凡是状态机、模式切换、报警等级这类有明确枚举含义的变量优先用枚举凡是需要和外部设备、HMI、上位机通信的数据尽量用标准整数类型避免不同系统对枚举字节解释不一致导致乱码。2. 数据类型的实战选择技巧2.1 开关量与位操作BOOL 不是随便用按钮、限位、接触器反馈、报警标志这些“是/否”类信号用 BOOL 是标准做法。BOOL 在内存里虽然占了一个字节的空间但连续声明的 BOOL 会被编译器打包到一个字节的多个位上这点后面内存优化还会细说。选 BOOL 的几个实操原则设备启停、状态标志、互锁条件全部用 BOOL不要为了“省事”用 INT 的 0/1否则后面位运算和通信字节处理都会变得别扭。如果只是取某个字节里的某一位可以用 BYTE 或 WORD 接收后再用 AND 和位移操作提取但 PLC 里通常直接访问符号变量更直观。注意 BOOL 没有“未赋值”状态声明后默认是 FALSE逻辑上一定要清楚初始值是什么。有个坑是把 BOOL 引入到数学表达式中它会被隐式当作整数 0 或 1虽然 CoDeSys 会警告甚至报错但低版本或特定上下文可能直接编译通过这会导致程序逻辑上出现非预期结果。2.2 整数类型别只看“够用”整数类型是整个数据类型选择里最需要动脑筋的部分因为控制器里整数运算最快很多指令、库函数默认参数就是 INT。具体场景怎么选我给出几条个人经验。计数类变量比如产量累加、药丸数、测试次数如果循环时间长、统计量大建议直接上 DINT。别以为 INT 够用一条产线连续运转一天十万个产品很正常十六位整数到 32767 就爆了。一旦溢出累计值会从负数开始排查起来非常头疼。轴坐标、定位目标、脉冲数这种数据源可能是 32 位甚至 64 位的必须配 DINT 或 LINT。尤其是伺服驱动器回的绝对位置经常是几百万脉冲起步用 INT 接收那必然溢出。字节流解析比如 Modbus 通信报文、自定义协议帧用 BYTE、WORD、USINT 这类无符号类型更合适因为它们和十六进制数据、位掩码的配合天然顺畅。比如报文里的一段长度是 0~255你收到后用 USINT 保存后续做大小端拼接和掩码操作都比用 INT 省心。仅用于循环计数、数组下标、临时计算结果的中间变量用 INT 足够比如 FOR 循环的 i。这里选 INT 不是因为省内存而是三个原因对齐友好、库函数兼容性最好、在线监视显示也直观。如果数组容量超过 32767那就得换 DINT。2.3 浮点数REAL 的精度红线浮点类型主要就是 REAL 和 LREALREAL 在绝大多数运动控制和过程控制里都够用但它的精度只有大约 7 位有效数字这个常识值得反复强调。举个例子你想累计一个温度传感器的平均值或者做一个长期的积分累加VAR rSum : REAL : 0.0; i : INT; END_VAR FOR i : 1 TO 10000 DO rSum : rSum 0.1; END_FOR理论上结果应该是 1000.0但 REAL 累加一万次之后结果很可能是 999.9829 或 1000.0007误差就出来了。原因很简单0.1 在二进制浮点里本身就是无限循环小数REAL 只能保存到 7 位有效数字每加一次都会舍入一次误差会累积。所以浮点选择的实操建议普通的 PID 反馈、速度给定、温度显示用 REAL性能和精度足够。做长时间积分、能量累计、金额统计、位置累加优先用 LREAL或者干脆用 DINT 配合定点数思想来计数。比较两个 REAL 是否相等是高风险操作0.1 0.2 ! 0.3 在 PLC 里同样成立。正确的做法是比较两个浮点数的差值是否小于允许误差。2.4 时间类型TIME 与 TOD 的使用边界TIME 在 CoDeSys 里单位是毫秒常用于定时器指令的 PT 参数也用于测量某段程序的执行时间。它的量程大约是 49 天多超过这个上限要用 LTIME。如果你的设备连续半年不关机并且你需要记录总运行时长TIME 就不够用了。TIME 的一些使用细节ton1(IN : bStart, PT : T#10s);T#10s 是时间字面量也可以写 T#500ms、T#2h30m。使用时要特别注意PT 虽然接受 TIME但实际定时精度受 PLC 任务周期影响如果任务周期是 10ms那么一个 T#15ms 的定时器实际触发时间会在 10ms 到 20ms 之间波动这一点做高速测量时一定要清楚。时刻类型 TOD 适合记录一天内的时间比如班次开始时间、日报表时间戳。日常调试时我更喜欢把 TOD 转成字符串在 HMI 上显示这里就要用到时间转字符串的函数后面类型转换部分会提。日期类型 DATE 和 DT 在配方管理、批次追溯里经常用到但跨年、跨月、闰年这些坑在 PLC 里同样存在做日期计算时建议直接调库函数而不是自己写。3. 内存优化的几个关键战场3.1 连续 BOOL 的打包与边界问题很多入门开发者会认为 BOOL 就是 1 个 bit不占内存这是错误的。BOOL 在 PLC 内存中按字节编址单独一个 BOOL 变量实际上整占一个字节。但 CoDeSys 编译器有个优化机制连续声明的一组 BOOL 变量会被打包到同一个字节的不同位上8 个 BOOL 合起来只占 1 个字节。什么情况下打包会失效中间插入非 BOOL 类型就可能导致边界跨位。比如VAR bA : BOOL; iB : INT; bC : BOOL; bD : BOOL; END_VARbA 单独占一个字节bC 和 bD 可能再打成一个字节中间的 iB 按 2 字节对齐。这样整体占用反而变多。想优化就把 BOOL 集中声明在一起VAR bA : BOOL; bC : BOOL; bD : BOOL; iB : INT; END_VAR这样三个 BOOL 打包进一个字节iB 占两个字节整体空间明显减少。这个习惯在 UDT 和大型功能块里尤其重要因为工程里的标志位数量动辄几十上百个分布越散空隙越多。3.2 UDT 字段排序与内存对齐内存对齐是嵌入式开发和 PLC 开发共通的话题。CoDeSys 编译器为了访问效率会把多字节变量按它们的长度对齐到特定地址边界。这里有一个真实的、让我印象深刻的案例。假设你定义了一个 UDTTYPE UDT_Demo : STRUCT bFlag : BOOL; diCount : DINT; bOk : BOOL; rValue : REAL; iTemp : INT; END_STRUCT END_TYPE按数据个数算BOOL(1) DINT(4) BOOL(1) REAL(4) INT(2) 12 字节。但是DINT 和 REAL 需要 4 字节对齐因此 bFlag 后面会被编译器塞入 3 个填充字节bOk 后面又塞 3 个填充字节iTemp 后面再补 2 个字节对齐最终这个 UDT 的占用可能是 16 字节甚至更多白白浪费了 4 个字节。如果字段顺序按从大到小排TYPE UDT_Demo : STRUCT diCount : DINT; rValue : REAL; iTemp : INT; bFlag : BOOL; bOk : BOOL; END_STRUCT END_TYPE占用就变成 4421112 字节按最终对齐规则可能 12 字节直接落地内存省了四分之一访问效率还更高。这个原则叫“从大到小排列字段”在结构体字段超过六七个时效果非常明显。对齐问题不会导致程序出错但会影响内存占用。如果你的控制器内存非常紧张检查一下所有 UDT 的字段声明顺序是成本最低的优化方式。另外如果你对某些结构体做 MEMCPY 或者通过通信发送原始字节填充字节会被一并发送出去接收端如果不清楚结构体布局就会解析出莫名其妙的数据。这也是 UDT 通信要注意的一个细节。3.3 STRING 长度声明默认 80 不一定够STRING 是 PLC 开发里内存浪费的重灾区。默认 STRING 等于 STRING(80)一个字符串变量直接占用 81 字节80 字符加 1 个结束符。很多人没意识到这点随手声明了一堆 STRING内存就悄悄没了。实操建议根据实际用途确定长度。显示一个设备名STRING(20) 足够拼接报警文本可能 STRING(100)通信帧中的字符串字段严格按协议长度来。在联网通信、配方管理等场景字符串长度要和上位机约定好不要用默认 80 去赌。拼接字符串时注意目标长度比如 CONCAT、INSERT 这类函数如果超长会直接报错或者截断提前预留长度比临时加大结构体要稳妥得多。WSTRING 更耗内存每个字符占 2 字节而且很多库函数对 WSTRING 支持不如 STRING 好除非你明确要处理 Unicode 文本否则我建议优先用 STRING。3.4 数组与 FB 实例的占用控制数组在 PLC 里的表现和上位机完全不同越界不会抛异常而是直接覆盖相邻内存这个危险行为会导致变量莫名其妙变化。所以数组声明时先把维度算清楚尽量用常量定义上限不要用魔术数字。多维数组的占用是乘法关系比如一个 100×100 的 INT 数组就是 20000 字节差不多 20KB对于主流中大型 PLC 可能还好但对小型控制器就很吃紧了。尽量用一维数组加索引换算的方式代替二维数组。功能块FB实例的内存占用也要心里有数。每个 FB 实例拥有自己独立的输入、输出、内部变量副本同一个 FB 被声明 100 个实例它的内部变量空间就会复制 100 份。因此不要滥用 FB 实例能用数组下标逻辑处理的就不必给每个设备都建一个功能块实例。如果某个 FB 只需要在单个程序组织单元里用可以把它声明成局部变量不要全部塞进全局变量列表这样方便占用管理也让变量表更清爽。保持型变量VAR RETAIN的数据在掉电时要保存到非易失区大多数控制器对保持区容量有硬限制而且频繁写入会影响存储寿命。不要在保持区里放大数组、大字符串只放必要的小体积关键数据比如模式、配方编号、累计值。4. 类型转换看似简单实则容易翻车4.1 隐式转换与告警处理CoDeSys 是强类型语言不同类型之间不能直接赋值。但有些情况下它会做隐式转换比如把 INT 赋值给 DINT 是拓宽操作一般不报错反过来把 DINT 赋值给 INT 是收窄操作通常会告警甚至报错。这里有一个容易翻车的点INT 转 DINT 是安全拓宽但 DINT 转 INT 时如果 DINT 的值超过 32767超出部分会被截断结果变成一个看起来毫无规律的数字。比如 40000 转成 INT 得到 -25536程序里莫名其妙出现负数排查时根本想不到是类型转换导致的。所以我的习惯是所有可能产生截断的赋值都先做范围判断再转换。IF diValue -32768 AND diValue 32767 THEN iValue : DINT_TO_INT(diValue); ELSE iValue : 0; // 记录报警或做边界处理 END_IF另外不要对转换函数的告警视而不见。CoDeSys 的编译日志里如果提示隐式转换风险一定点开看它是在提醒你这里可能丢精度、截断、符号位翻转。4.2 模拟量缩放的正确换算姿势模拟量输入最常见的处理就是把原始值 0~27648或 0~4095、0~65535换算成工程量比如 0~100℃。很多人的错误写法是rTemp : INT_TO_REAL(iRaw) / 27648 * 100;这个写法运气好时正常但一旦 iRaw 很小或边界条件特殊结果就会不对原因在于整数除法或者 27648 被当成 INT 参与运算导致整除后精度丢失。正确写法是把分母写成浮点字面量rTemp : INT_TO_REAL(iRaw) / 27648.0 * 100.0;再强调一次27648.0 和 100.0 后面的 .0 必须写这个细节我见过太多现场翻车。这里还可以扩展一下偏移和量程同时存在时先归一化再缩放。比如压力变送器量程是 -50~150℃原始值 0~27648 对应 -50~150换算公式是rPressure : (INT_TO_REAL(iRaw) / 27648.0) * (150.0 - (-50.0)) (-50.0);先归一化到 0~1再乘量程跨度最后加偏移这个套路在模拟量处理里通用。4.3 字符串与数值互转的实用写法数值转字符串在报警文本、HMI 显示、报表生成里很常见CoDeSys 提供 INT_TO_STRING、DINT_TO_STRING、REAL_TO_STRING 等函数。使用时注意转换结果默认格式可能带前导空格比如 DINT_TO_STRING(123) 的结果是 123前导空格在拼接字符串时会带来视觉问题。如果你想精确控制小数位建议用格式化函数比如 FORMAT 或类似库函数而不是直接用 REAL_TO_STRING否则会得到一堆科学计数法或长尾小数。字符串转数值则要注意非法输入。上位机传来 abc你用 STRING_TO_INT 转换结果是 0程序却还继续往下跑容易出问题。所以转换前最好做合法性正则或长度检查或者用带成功状态的转换函数。通信解析里我吃过不少亏现在凡是涉及字符串转数值都会先判断内容再转换。5. 常见问题排查与小经验分享5.1 高频问题速查表表现常见原因对策变量变成负数大整数超出 INT 范围截断换 DINT 或加范围判断浮点累计值偏差大使用 REAL 做长周期累加换 LREAL 或用 DINT 定点在线修改失败改变量类型导致结构内存变化停机后再下载或保持类型一致字符串显示乱码STRING 长度不足截断字符集不匹配明确字符串长度和编码UDT 通信数据对不上内存对齐填充字节被发送调整字段顺序或按字节流组包周期定时不准定时精度受任务周期影响精度要求高时缩短任务周期或使用独立测量方式这些问题的共性是数据类型没有被当成第一等公民对待。程序能跑不代表类型是对的编译通过也不代表运行时不会炸。5.2 几条来自项目现场的经验写到这里我把这些年做自动化项目时总结出来的经验分享几条。变量命名前缀是目前工业界最通用的防错手段之一。bStart、diCount、rTemp、sName看到名字就知道类型写转换时就不会随手乱写。尤其面对几千个变量时前缀能帮你从命名阶段就拦截大部分类型不匹配问题。新工程开工前先用二十分钟把全工程的 UDT、枚举、全局变量类型梳理一遍。这一点花的时间不多但会在后面的编程、调试、维护阶段成倍地赚回来。结构体封装设备状态、数组定义配方表、枚举限定模式是提升程序可维护性的三件套。关于在线修改我要特别提醒改变量类型比如 INT 改成 DINT属于结构性修改在线模式下经常无法生效。程序运行越久越要珍惜在线修改的机会能改逻辑不改类型能加变量不改结构。真要改类型找个停机窗口一次性改完再下载别把现场搞得人仰马翻。控制器资源紧张时打开 CoDeSys 的内存统计窗口看一下你会惊讶地发现大量内存被字符串和大数组吃掉了。这时候优先做三件事把不必要的 STRING(80) 改小把多维数组压成一维把分散的 BOOL 重新分组打包。这三招往往能回收 10%~20% 的内存占用。最后再分享一个小技巧做通信协议解析时尽量用无符号类型 BYTE、USINT、WORD 去接收原始字节然后用移位和掩码组合成 DINT 或 REAL这样避免了很多有符号数的负数问题。我在处理 Modbus 寄存器时习惯先把两个 WORD 组合成 DWORD再用 DWORD_TO_REAL 转换比直接拆分 INT 符号位要简单可靠得多。数据类型这个话题看起来是基础其实越往深处走越能发现它跟实时性、内存、通信可靠性都直接挂钩。希望这篇总结能帮你少走一些弯路。