
1. 从一次数据对不上的现场说起搞STStructured Text结构化文本的人几乎都经历过这种时刻PLC程序跑得好好的HMI上显示的温度、流量、累计量却总是差那么一点点。不是差得离谱就是小数点后几位对不上或者一个本该是32767的数突然变成了-1。你盯着代码看半天逻辑没问题算法没问题最后发现问题出在一个最不起眼的地方——数据类型选错了。这个事儿的隐蔽性在于它不会报错。编译器不会告诉你你这里精度丢了运行时也不会抛异常。它就像水管上针眼大的漏点平时看不出来跑上几天、几个月累计量就偏出去了。更麻烦的是有些丢精度是静默的——你算出来的结果看起来完全合理只是它已经不是你想要的那个值了。这篇内容就是围绕ST语言里从BOOL到LREAL这一整条数据类型谱系把选型这件事讲透。哪些场景该用哪个类型类型转换时哪里会悄悄丢东西INT和DINT混用会出什么问题REAL和LREAL到底差在哪BOOL类型函数的返回值有哪些坑。适合已经能写ST程序、但对类型系统还没形成肌肉记忆的人也适合被数据对不上折磨过、想彻底搞清楚根因的人。我自己的经验是ST程序里90%的玄学bug最后都能追溯到类型上。把类型选对了很多问题根本不会发生。2. BOOL不是小整数它的语义边界比你想的严格2.1 BOOL的本质一个位不是0和1很多人下意识把BOOL当成只能存0或1的整数这个理解在ST里是危险的。BOOL在底层确实占一个bit或者在某些实现里占一个字节的对齐空间但它的语义是逻辑真值不是数值。这意味着两件事第一BOOL参与算术运算时必须先转成整数类型。有些编译器允许你写b 1有些直接报错有些虽然能过但结果依赖具体实现。这种能过但不确定的代码是移植时的定时炸弹。第二BOOL的赋值来源如果是数值转换规则各平台不一致。比如b : 2;这种写法有的平台把非零当TRUE有的只认1有的直接编译不过。我见过一个项目从A平台迁到B平台所有b : counter;的地方行为全变了因为A平台非零即真B平台只认1。实操建议BOOL变量永远只从比较表达式、逻辑表达式、或者明确的BOOL函数返回值赋值。不要从整数直接赋需要判断就写b : (n 0);意图清晰跨平台也稳。2.2 BOOL类型函数的返回值别被看起来对骗了热词里有个bool类型函数的返回值这个点值得单独说。ST里函数返回BOOL常见写法是函数内部根据条件RETURN TRUE;或RETURN FALSE;。坑在哪坑一函数没有在所有分支上显式返回。有些编译器会给返回值一个默认初值通常是FALSE有些则是未定义。你测试的时候走的是有返回的分支跑起来没问题现场某个边界条件触发了没返回的分支返回值就是随机的。这种bug极难复现。坑二把BOOL函数当条件用但函数有副作用。比如IF CheckAndReset() THEN这个函数内部可能改了某个状态。BOOL返回值本身没问题但调用时机和副作用耦合在一起逻辑就脆了。坑三返回值被隐式转换。比如函数返回BOOL你赋给一个INT变量n : CheckSomething();TRUE变成1FALSE变成0。看起来合理但如果后面有人把n当计数器用就埋雷了。我的习惯是BOOL函数名一律用Is、Has、Can开头返回值只用于条件判断绝不参与算术。这样读代码的人一眼就知道这是逻辑值不会拿去算。2.3 BOOL数组与位操作的边界ST里BOOL数组ARRAY[0..31] OF BOOL常被用来做位标志。这里有个容易忽略的点BOOL数组不保证内存连续按位排列。很多平台里每个BOOL占一个字节甚至一个字你以为是32个bit实际占了32个字节。如果你依赖内存布局去做位操作或者和外部设备映射就会错位。真要做位操作用BYTE、WORD、DWORD配合位运算AND、OR、SHL、SHR或者用平台提供的位访问语法。BOOL数组只用来做逻辑分组别拿它当位图。3. INT、DINT、SINT整数家族的容量陷阱3.1 各整数类型的取值范围与典型误用先把这张表刻在脑子里类型位宽取值范围典型场景SINT8位-128 ~ 127小范围计数、状态码USINT8位0 ~ 255字节数据、ASCIIINT16位-32768 ~ 32767传统PLC的默认整数UINT16位0 ~ 65535无符号计数DINT32位-2^31 ~ 2^31-1累计量、大计数UDINT32位0 ~ 2^32-1大无符号计数LINT64位-2^63 ~ 2^63-1超大累计、时间戳最常见的误用是用INT做累计量。一个每秒加1的计数器INT最大32767不到10小时就溢出了。溢出后从32767跳到-32768你的累计量瞬间变成负数。如果这个值再参与后续计算整个结果就废了。我踩过的坑一个流量累计程序用INT存脉冲数跑了大概9小时累计量突然变成负的操作员以为表坏了。改成DINT之后按每秒1000个脉冲算也能跑将近25天实际项目里够用了。真要长期累计直接上LINT或者用REAL/LREAL存工程量。3.2 INT转QString格式化输出的那些细节热词里有int转qstring这是HMI或者上位机通信时的常见需求。ST本身没有QString但很多平台提供了字符串转换函数。坑在于格式化和位宽。比如你要把一个INT转成4位十六进制字符串。有人写format(n, 04X)n是INT。如果n是负数十六进制表示会是补码形式比如-1变成FFFF。这可能是你要的也可能不是。如果你想要的是绝对值的4位十六进制得先取绝对值。再比如format(int(char), 04b)这种写法热词里出现的意图是把一个字符的编码转成4位二进制字符串。这里int(char)把字符转成整数04b表示4位二进制、不足补零。坑在于如果字符编码超过4位能表示的范围也就是大于1504b会截断高位你得到的是低4位不是完整编码。这种截断是静默的不报错。经验做字符串格式化时永远先确认目标位宽够不够。二进制4位只能表示0-15八进制、十六进制同理。不确定就用更宽的格式或者先判断范围。3.3 整数除法与取模的符号问题ST里整数除法a / b的结果类型和符号处理各平台有差异。有的平台整数除法直接截断小数7 / 2 3有的会四舍五入有的要求你显式用DIV。负数除法更麻烦-7 / 2有的得-3有的得-4。取模MOD同理。-7 MOD 3有的得-1有的得2。如果你用取模来做循环索引或者分组符号处理不对索引就会越界或者错位。我的做法涉及除法和取模的地方先确认操作数符号。如果可能为负要么先转成无符号处理要么显式写清楚期望的行为加注释。别指望编译器帮你兜底。4. REAL与LREAL精度丢失的重灾区4.1 浮点数的本质不是带小数的数REAL是32位浮点LREAL是64位浮点。很多人以为REAL就是能存小数的数这个理解会导致严重的精度误判。浮点数在计算机里是符号位指数位尾数位的表示。REAL有23位尾数能精确表示的整数范围大概是±2^24约1600万。超过这个范围整数就不能精确表示了只能近似。LREAL有52位尾数精确整数范围到±2^53约9×10^15大得多。这意味着什么如果你用REAL存一个累计量超过1600万之后加1可能加不上去——因为1已经小于当前值的精度间隔了。比如REAL存16777216加1还是16777216。这不是bug是浮点的固有特性。4.2 REAL vs LREAL什么时候必须上LREAL场景REAL够不够说明单次测量值温度、压力够精度通常到小数点后3-4位就够短时间累计分钟级够数值不会太大长时间累计天/月级不够数值会超过1600万丢精度大范围坐标如机械臂看情况绝对值大时REAL精度不够高精度计算如PID积分项建议LREAL积分项长期累加REAL会漂时间戳毫秒级不够毫秒时间戳很快超过1600万我遇到过一个典型案例PID控制器的积分项用REAL运行几天后积分项累加到很大再加微小增量时加不上去积分饱和失效控制精度下降。改成LREAL后问题消失。这个坑的隐蔽性在于前期完全正常跑久了才出问题。4.3 浮点数比较永远不要用等号IF r 3.14 THEN这种写法在浮点世界里基本是错的。因为3.14在浮点里存的是近似值你算出来的结果也是近似值两个近似值相等的概率极低。正确做法是比较差值IF ABS(r - 3.14) 0.0001 THEN // 认为相等 END_IF这个容差0.0001要根据你的实际精度需求定。容差太小永远不相等容差太大误判。一般取你关心的最小有效位的1/2到1/10。同理浮点数做循环条件、做数组索引、做状态判断都要小心。能用整数的地方尽量用整数浮点只用在真正需要小数的物理量上。4.4 类型转换中的隐式丢精度ST里类型转换分隐式和显式。隐式转换比如INT赋给REAL通常安全因为整数转浮点不会丢在精度范围内。但反过来REAL赋给INT小数部分直接截断而且是静默的。更危险的是混合运算。比如INT REAL编译器会把INT提升成REAL再算结果是REAL。如果你把这个结果赋回INT小数就丢了。这一连串操作里丢精度的点可能不止一个。n : 7; r : 2.0; result : n / r; // result是REAL3.5 n : n / r; // n是INT结果是30.5丢了第二行这种写法很多编译器不报错但结果和你预期的不一样。我的习惯是混合类型运算时显式写转换函数比如REAL_TO_INT()、INT_TO_REAL()让每一步的意图都清晰。虽然啰嗦但不会出意外。5. 类型选型的决策链路从需求倒推类型5.1 先问三个问题选类型之前先问自己这个值需要参与算术运算吗不需要就用BOOL或枚举。这个值的范围有多大决定用INT还是DINT还是LINT。这个值需要小数吗精度要求多少决定用REAL还是LREAL。这三个问题回答完类型基本就定了。剩下的就是转换和边界处理。5.2 一张选型对照表需求推荐类型避免开关状态、标志位BOOLINT语义不清小范围计数100SINT/USINTINT浪费一般计数、索引INT/DINTSINT易溢出累计量、大计数DINT/LINTINT必溢出物理量温度、压力REALINT丢小数高精度物理量LREALREAL长期漂移时间戳毫秒LINT/LREALINT/DINT溢出字符串长度、编码UINT/DINTINT负数无意义5.3 边界值测试选型后的必做动作类型选完不是结束必须做边界测试。具体做法把变量赋成类型的最小值和最大值跑一遍逻辑看会不会溢出、会不会符号翻转。把变量赋成0、负数、临界值看除法、取模、比较的行为。如果是累计量估算最坏情况下的增长速度算多久会到类型上限。我有个习惯每个累计量变量旁边写注释标明类型上限和预计溢出时间。比如// DINT, 每秒1000脉冲, 约24.8天溢出。这样维护的人一眼就知道风险在哪。6. 那些年我踩过的类型坑四个真实案例6.1 案例一INT溢出导致的负流量一个水流量累计程序脉冲当量是每脉冲0.1升用INT存脉冲数。跑了大概9小时累计流量突然变成负数。排查发现INT到了32767后溢出到-32768。改成DINT后解决。教训累计量永远不要用INT。6.2 案例二REAL精度不足导致的PID失效前面提过的PID积分项案例。REAL积分项累加到1600万以上后微小增量加不上去积分失效。改成LREAL后正常。教训长期累加的浮点量用LREAL。6.3 案例三BOOL函数未全分支返回一个IsReady()函数只在条件满足时RETURN TRUE其他情况没写返回。测试时条件都满足没问题。现场某个传感器故障走了没返回的分支返回值随机导致设备误动作。教训BOOL函数每个分支都要显式返回。6.4 案例四隐式转换导致的索引错位数组索引用REAL计算idx : r * 10;然后arr[idx]。r是2.5时idx是25没问题r是2.55时idx是25.5隐式转成INT变成25或26看平台索引就偏了。改成idx : REAL_TO_INT(r * 10 0.5);显式四舍五入后解决。教训数组索引必须用整数转换要显式且明确舍入规则。7. 写ST代码时的类型纪律7.1 声明即文档变量声明时把类型、范围、单位都写清楚VAR nPulseCount : DINT; // 脉冲计数, 0~2^31, 每秒最大1000 rFlowRate : REAL; // 瞬时流量 L/min, 0.0~100.0 rTotalFlow : LREAL; // 累计流量 L, 长期累加 bPumpRun : BOOL; // 泵运行状态 END_VAR这样读代码的人不用猜维护时也不会误改类型。7.2 转换函数显式化所有跨类型赋值用显式转换函数。虽然多打几个字但省下的调试时间远超这点成本。7.3 定期审查类型项目做到一定阶段回头审查一遍所有变量的类型。重点看累计量够不够大、浮点精度够不够、BOOL有没有被当整数用。这个动作花不了多少时间但能提前发现很多隐患。类型选型这件事说到底是个想清楚的功夫。想清楚这个值是什么、会变成什么、参与什么运算类型自然就选对了。选对了那些数据对不上的玄学问题大部分根本不会出现。