1. 一个除零异常如何拖垮整台PLC
1.1 从一次现场宕机说起
前阵子帮朋友处理一个产线故障,现象很典型:一台中型PLC在运行了大概十几个小时后突然停机,所有输出冻结在最后一次状态,HMI画面上所有数据不再刷新,但PLC的电源灯、运行灯看起来都正常。现场操作工的第一反应是断电重启,重启之后又能跑,但过几个小时又挂。这种"周期性假死"最折磨人,因为你到现场的时候它往往是好的。
我拿到程序之后,第一件事不是看逻辑,而是翻异常处理相关的代码。果然,在一个模拟量换算的功能块里,有这么一段ST代码:
rFlow := rRawValue / rScaleFactor;问题就出在这里。rScaleFactor这个变量来自HMI上的一个配方参数,正常情况下是100.0,但配方切换的瞬间,这个变量会被先清零再写入新值。如果扫描周期刚好卡在这个"清零窗口"里,rScaleFactor就是0.0,除法运算直接触发除零异常。
关键在于,这段代码没有任何保护。在IEC 61131-3的ST语言里,整数除零和浮点除零的行为是不一样的,而很多PLC运行时对浮点除零的处理方式,直接决定了它是"给个错误值继续跑"还是"整个任务崩掉"。
1.2 为什么一个异常能拖死整台PLC
这里要讲清楚一个很多人忽略的点:PLC的运行时(Runtime)和普通PC上的应用程序,在异常处理模型上完全不同。PC上你写个除零,操作系统会抛一个异常,进程崩了顶多是个弹窗,其他进程照跑。但PLC的运行时通常是一个实时任务调度器,所有任务共享同一个运行时上下文。
当ST代码里出现未捕获的异常时,不同平台的表现差异很大:
| 平台类型 | 除零表现 | 后果 |
|---|---|---|
| 部分软PLC(基于Windows) | 抛出运行时异常,任务终止 | 该任务停止,其他任务可能继续 |
| 部分嵌入式PLC | 硬件异常,进入故障模式 | 整个运行时挂起,所有任务冻结 |
| 部分PLC | 返回饱和值或NaN | 逻辑继续跑,但数据全错 |
| 部分PLC | 静默返回0 | 最危险,错误被掩盖 |
我遇到的这台属于第二种。除零触发了运行时的未处理异常,调度器直接进入故障状态,所有周期任务停止执行。这就是"一个除零拖死整台PLC"的完整链路:配方切换 → 变量清零 → 除法运算 → 异常抛出 → 运行时故障 → 全任务冻结。
1.3 这篇文章适合谁看
如果你正在做PLC程序开发,尤其是用ST语言写逻辑的,这篇文章值得花时间看完。不管你是刚入行的新手,还是写了几年程序的老手,异常处理这块都是最容易翻车的地方。新手容易忽略,老手容易自信过头。
我会从异常处理的底层机制讲起,然后给出具体的防护方案、代码模板、排查方法,最后分享几个我踩过的坑。内容覆盖西门子、汇川、倍福、三菱这些常见平台,但核心思路是通用的。
2. 异常处理的底层逻辑与平台差异
2.1 PLC运行时到底怎么处理异常
要理解为什么除零能拖死PLC,得先搞清楚PLC运行时的工作模型。大多数PLC的运行时可以简化为三层:
- 硬件抽象层:负责和CPU、内存、IO打交道
- 实时调度层:管理周期任务、事件任务、中断
- 应用执行层:跑你写的ST、LD、FBD代码
异常发生在应用执行层,但它的影响会向上传播。当应用层抛出一个运行时异常,调度层需要决定怎么处理。这里有两种设计哲学:
哲学一:快速失败(Fail-Fast)。检测到异常立即停止整个运行时,进入安全状态。这种设计在安全PLC里很常见,好处是不会带着错误继续跑,坏处是一个小错误就能让整条线停下来。
哲学二:隔离失败(Fail-Isolate)。异常只影响当前任务,其他任务继续跑。这种设计在软PLC里更常见,但实现复杂度高,而且如果异常发生在共享资源上,隔离也救不了。
我遇到的那台PLC属于第一种。它的运行时在检测到未处理异常后,直接调用了Shutdown(),所有任务被强制终止。从安全角度这没错,但从可用性角度就是灾难。
2.2 ST语言里除零的几种表现
ST语言基于IEC 61131-3标准,但标准对除零的定义其实很模糊。标准只说"除零是一个错误",但没规定运行时必须怎么处理。这就导致不同厂商的实现千差万别。
我实测过几个常见平台:
西门子S7-1200/1500(TIA Portal):整数除零会触发OB121(编程错误组织块),如果你没写OB121,CPU会进入STOP模式。浮点除零稍微好一点,会返回一个特殊值,但如果你在OB121里没处理,照样停机。
汇川AM系列(基于CODESYS):CODESYS的运行时对除零的处理取决于编译选项。默认情况下,浮点除零返回Inf或NaN,不会抛异常。但如果你开了"运行时检查",就会触发异常,任务终止。
倍福TwinCAT 3:默认情况下除零会触发异常,但你可以通过__TRY/__CATCH块捕获。如果不捕获,任务会进入错误状态,但其他任务继续跑。
三菱FX系列:老式PLC对除零基本不检查,直接返回0或者一个随机值。这种"静默失败"最危险,因为你的逻辑会带着错误数据继续跑。
注意:不要假设你的PLC会"友好地"处理除零。同一个品牌的不同系列,行为可能完全不同。S7-1200和S7-1500在除零处理上就有细微差别。
2.3 为什么浮点除零比整数除零更隐蔽
整数除零通常会被运行时直接检测到,因为整数除法在CPU层面就是一条指令,除零会触发硬件异常。但浮点除零不一样,IEEE 754标准定义了浮点除零的结果是Inf(无穷大)或NaN(非数),而不是异常。
这意味着,如果你的PLC遵循IEEE 754,浮点除零不会抛异常,而是返回一个特殊值。这个特殊值会继续参与后续运算,然后像病毒一样扩散。我见过一个案例:一个温度值因为除零变成了NaN,然后这个NaN被写入PID控制器的输入,PID输出也变成NaN,最后阀门控制信号变成NaN,阀门直接开到最大。
所以浮点除零的危险在于:它不一定会让PLC停机,但会让你的控制逻辑彻底失控。相比之下,整数除零虽然会导致停机,但至少你知道出问题了。
2.4 异常传播的三种路径
理解了除零的表现,还要理解异常在PLC程序里的传播路径。我总结为三种:
路径一:任务级传播。异常发生在某个周期任务里,该任务终止,但其他任务继续。这种最"温和",但如果你没监控任务状态,可能根本不知道有任务挂了。
路径二:运行时级传播。异常触发运行时的全局错误处理,整个运行时进入故障状态。这种最严重,就是标题说的"拖死整台PLC"。
路径三:数据级传播。异常没有抛出,但产生了错误数据(如NaN、Inf、0),错误数据在程序里扩散,最终导致逻辑错误。这种最隐蔽,排查难度最大。
我遇到的那台PLC走的是路径二,但很多软PLC走的是路径三。路径三的排查成本远高于路径二,因为你看不到任何错误标志,只能通过数据异常反推。
3. 除零防护的完整方案与代码模板
3.1 最基础的防护:除法前判零
最直接的办法,就是在每次除法之前判断除数是否为零。这是所有防护方案的基础,但很多人写得不够严谨。
IF rScaleFactor <> 0.0 THEN rFlow := rRawValue / rScaleFactor; ELSE rFlow := 0.0; // 或者保持上一次的值 END_IF;这段代码看起来没问题,但有几个细节要注意:
第一,浮点数的比较不能用=或<>。因为浮点数的精度问题,rScaleFactor可能不是精确的0.0,而是一个极小的值(如1e-20)。这种情况下,除法结果会是一个巨大的数,同样会导致后续逻辑出错。
正确的做法是判断绝对值是否小于一个阈值:
IF ABS(rScaleFactor) > 1.0E-6 THEN rFlow := rRawValue / rScaleFactor; ELSE rFlow := 0.0; END_IF;第二,ELSE分支里赋什么值很关键。赋0.0可能让后续逻辑误以为流量真的是0,赋上一次的值可能掩盖问题。我的建议是赋一个"安全值",同时置一个错误标志位,让上层逻辑知道这里出过问题。
3.2 进阶防护:封装安全除法功能块
如果程序里除法很多,每个地方都写IF判断会很啰嗦。更好的做法是封装一个安全除法功能块(FB)。
FUNCTION_BLOCK FB_SafeDivide VAR_INPUT rNumerator : REAL; // 被除数 rDenominator : REAL; // 除数 rDefault : REAL := 0.0; // 除零时的默认值 rEpsilon : REAL := 1.0E-6; // 零判断阈值 END_VAR VAR_OUTPUT rResult : REAL; bError : BOOL; // 除零标志 END_VAR VAR bDenomZero : BOOL; END_VAR bDenomZero := ABS(rDenominator) <= rEpsilon; IF bDenomZero THEN rResult := rDefault; bError := TRUE; ELSE rResult := rNumerator / rDenominator; bError := FALSE; END_IF;这个FB的好处是:
- 所有除法统一走这个FB,防护逻辑只写一次
bError输出可以让上层逻辑知道是否发生了除零rEpsilon可以按需调整,适应不同精度要求
调用的时候:
fbSafeDivide( rNumerator := rRawValue, rDenominator := rScaleFactor, rDefault := 0.0, rResult => rFlow, bError => bFlowError );实操心得:
rEpsilon的取值很讲究。取太大,正常的除法会被误判为除零;取太小,极小的除数会导致结果溢出。我的经验是取1.0E-6到1.0E-9之间,具体看你的数据范围。如果被除数最大是10000,除数最小是0.001,那结果最大是1e7,不会溢出REAL的范围。
3.3 平台级防护:异常捕获机制
除了代码级的防护,很多PLC平台还提供了异常捕获机制。这些机制可以作为最后一道防线。
CODESYS平台(汇川、倍福等):
CODESYS提供了__TRY/__CATCH/__FINALLY块,可以捕获运行时异常:
__TRY rFlow := rRawValue / rScaleFactor; __CATCH // 异常处理 rFlow := 0.0; bExceptionCaught := TRUE; __FINALLY // 清理代码 __ENDTRY但要注意,__TRY/__CATCH有性能开销,不建议在高频任务里大量使用。而且它只能捕获"被抛出"的异常,如果运行时配置为"静默返回NaN",__CATCH根本不会触发。
西门子S7-1200/1500:
西门子通过组织块(OB)来处理异常。除零会触发OB121(编程错误),你需要在OB121里写处理逻辑:
// OB121 内容 // 读取错误信息 // 记录日志 // 决定是否继续运行如果你不写OB121,CPU会直接进入STOP模式。写了OB121之后,CPU会继续运行,但你需要自己处理错误。
倍福TwinCAT 3:
TwinCAT 3支持__TRY/__CATCH,也支持通过Tc2_System库里的函数来查询和清除错误状态。
3.4 数据级防护:NaN和Inf的检测
如果你的PLC对浮点除零返回NaN或Inf,那代码级防护之外,还需要加一层数据检测。
// 检测NaN IF rValue <> rValue THEN // rValue是NaN END_IF; // 检测Inf IF ABS(rValue) > 1.0E38 THEN // rValue可能是Inf END_IF;NaN的特性是它不等于任何值,包括它自己。所以rValue <> rValue是检测NaN的经典方法。Inf的检测则通过判断绝对值是否超过REAL的最大值。
我建议在每个关键数据流的入口和出口都加这种检测,尤其是来自HMI、通讯、模拟量采集的数据。这些数据源最容易产生异常值。
3.5 防护方案的选择矩阵
不同场景下,防护方案的选择不一样。我整理了一个选择矩阵:
| 场景 | 推荐方案 | 理由 |
|---|---|---|
| 单次除法,低频调用 | 除法前判零 | 简单直接,开销小 |
| 多处除法,高频调用 | 封装安全除法FB | 统一管理,减少重复代码 |
| 安全相关逻辑 | 判零 + 异常捕获 + 数据检测 | 多重防护,确保安全 |
| 性能敏感任务 | 判零 + 数据检测 | 避免异常捕获的开销 |
| 老式PLC(无异常机制) | 判零 + 数据检测 | 只能靠代码级防护 |
4. 完整实操:从故障复现到防护落地
4.1 故障复现:构造一个除零场景
要验证防护方案是否有效,先得能复现故障。我写了一个简单的测试程序,模拟配方切换时的除零场景:
PROGRAM PLC_PRG VAR rRawValue : REAL := 1000.0; rScaleFactor : REAL := 100.0; rFlow : REAL; nCounter : INT; bSimulateFault : BOOL := TRUE; END_VAR // 模拟配方切换:每100个周期,rScaleFactor清零一次 nCounter := nCounter + 1; IF nCounter >= 100 THEN nCounter := 0; IF bSimulateFault THEN rScaleFactor := 0.0; // 模拟清零窗口 END_IF; ELSE rScaleFactor := 100.0; END_IF; // 危险代码:无防护的除法 rFlow := rRawValue / rScaleFactor;这段代码在大多数PLC上跑,100个周期内就会触发除零。你可以用它来验证你的PLC在除零时的行为:是停机、是返回NaN、还是静默返回0。
4.2 防护落地:改造为安全版本
复现故障之后,把危险代码替换为安全版本:
PROGRAM PLC_PRG VAR rRawValue : REAL := 1000.0; rScaleFactor : REAL := 100.0; rFlow : REAL; nCounter : INT; bSimulateFault : BOOL := TRUE; fbSafeDivide : FB_SafeDivide; bFlowError : BOOL; END_VAR // 模拟配方切换 nCounter := nCounter + 1; IF nCounter >= 100 THEN nCounter := 0; IF bSimulateFault THEN rScaleFactor := 0.0; END_IF; ELSE rScaleFactor := 100.0; END_IF; // 安全除法 fbSafeDivide( rNumerator := rRawValue, rDenominator := rScaleFactor, rDefault := 0.0, rResult => rFlow, bError => bFlowError ); // 错误处理:记录错误次数 IF bFlowError THEN nErrorCount := nErrorCount + 1; END_IF;改造之后,即使rScaleFactor清零,程序也不会崩溃,rFlow会输出0.0,bFlowError会置位,你可以用这个标志位触发报警或记录日志。
4.3 参数计算:阈值和默认值怎么定
rEpsilon和rDefault这两个参数不是随便定的,需要根据实际工况计算。
rEpsilon的计算:
假设你的被除数范围是0到10000,除数正常范围是0.1到1000。你希望除数小于0.01时判定为异常。那rEpsilon就取0.01。
但还要考虑浮点精度。REAL是32位浮点,有效精度约7位十进制。如果被除数是10000,除数是0.01,结果是1e6,在REAL范围内没问题。但如果除数是1e-10,结果就是1e14,接近REAL的精度极限,结果会不准确。
所以rEpsilon的下限应该保证:最大被除数 / rEpsilon < REAL最大值。REAL最大值约3.4e38,所以rEpsilon > 最大被除数 / 3.4e38。对于10000的被除数,rEpsilon > 3e-35。这个下限很低,实际取1e-6到1e-9就够了。
rDefault的选择:
- 如果除零代表"无数据",
rDefault取0.0 - 如果除零代表"保持上次值",
rDefault取上一次的有效值 - 如果除零代表"错误状态",
rDefault取一个明显的异常值(如-9999.0),让上层逻辑能识别
我一般推荐第三种,因为异常值更容易被发现。但要注意,异常值不能参与后续的数学运算,否则会污染数据。
4.4 现场部署:从测试到上线
防护方案在测试环境验证通过后,部署到现场还需要注意几点:
第一,不要一次性替换所有除法。先替换最关键的几处,观察一段时间,确认没问题再逐步推广。我见过有人一次性改了上百处除法,结果引入新bug,排查了一周。
第二,加日志。每次除零被捕获,都记录一条日志,包括时间戳、变量名、被除数和除数的值。这些日志是后续优化的依据。如果某个除零点频繁触发,说明上游逻辑有问题,需要从根源解决。
第三,监控错误标志。把bError标志位接到HMI上,让操作工能看到。如果频繁报警,说明配方管理或数据采集有问题。
第四,定期review。除零防护是治标,治本是从源头保证除数不为零。定期检查配方管理逻辑、通讯数据校验、模拟量采集滤波,减少除零的发生概率。
4.5 性能影响实测
有人担心加防护会影响性能。我实测过,在汇川AM401上,一个安全除法FB的调用开销大约是0.5微秒。如果一个周期任务里有100处除法,额外开销是50微秒。对于1毫秒的周期任务,这个开销占5%,可以接受。
但如果你的周期任务是100微秒,50微秒的开销就太大了。这种情况下,可以考虑:
- 只在关键路径上加防护
- 用内联的判零代替FB调用
- 把除零检测移到低频任务里
实操心得:性能优化不要凭感觉,要实测。我见过有人为了"性能"去掉了所有防护,结果现场宕机,损失远大于那点性能开销。
5. 常见问题排查与避坑指南
5.1 除零问题速查表
| 现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| PLC突然停机,无报警 | 整数除零触发硬件异常 | 查看CPU诊断缓冲区 | 加判零防护,写OB121 |
| PLC运行但数据不刷新 | 任务因异常终止 | 查看任务状态 | 加异常捕获,监控任务 |
| 数据出现NaN或Inf | 浮点除零未检测 | 在线监控变量值 | 加NaN/Inf检测 |
| 数据突然变成0 | 除零静默返回0 | 对比历史数据 | 加判零防护,记录日志 |
| 周期性宕机 | 特定条件下触发除零 | 记录触发时的变量值 | 定位触发条件,加防护 |
| HMI显示异常值 | 除零结果传到HMI | 检查HMI数据源 | 在PLC侧过滤异常值 |
5.2 排查思路:从现象到根因
排查除零问题,我一般按这个顺序走:
第一步,确认是不是除零。查看CPU诊断缓冲区,如果有"除零"相关的错误码,基本可以确定。如果没有,看变量值是否有NaN或Inf。
第二步,定位除零位置。如果PLC支持在线监控,逐个检查除法运算的除数。如果不支持,用二分法:注释掉一半代码,看问题是否复现,逐步缩小范围。
第三步,分析触发条件。除零通常不是随机发生的,而是特定条件下触发的。记录触发时的时间、变量值、操作步骤,找出规律。
第四步,从源头解决。加防护只是治标,治本是从源头保证除数不为零。比如配方切换时,先写新值再清零旧值,或者用双缓冲机制。
5.3 避坑指南:我踩过的五个坑
坑一:以为浮点除零会抛异常。实际上大多数PLC对浮点除零返回NaN,不抛异常。结果错误数据在程序里扩散,排查了两天才找到源头。
坑二:用=比较浮点数。IF rValue = 0.0 THEN这种写法在浮点运算里不可靠。应该用ABS(rValue) < epsilon。
坑三:在ELSE分支里赋0.0。赋0.0可能让后续逻辑误以为数据真的是0。更好的做法是赋一个异常值,或者保持上次值,同时置错误标志。
坑四:忽略整数除零。整数除零比浮点除零更危险,因为它会直接触发硬件异常。但很多人只关注浮点,忽略了整数。
坑五:防护加在错误的位置。有人在HMI侧加防护,但PLC内部的除法照样会崩。防护必须加在PLC侧,而且要在除法运算之前。
5.4 工具推荐:辅助排查的实用工具
在线监控:TIA Portal、CODESYS、TwinCAT都支持在线监控变量值。这是排查除零最直接的工具。
诊断缓冲区:西门子的诊断缓冲区、倍福的事件日志,都会记录异常信息。养成定期查看的习惯。
变量记录:如果PLC支持数据记录功能,把关键变量记录下来,事后分析。我一般会记录除法运算的被除数、除数、结果,以及时间戳。
仿真环境:在仿真环境里复现故障,比在现场排查方便得多。CODESYS、TIA Portal都有仿真功能。
代码静态分析:有些工具可以静态分析ST代码,找出潜在的除零风险。比如CODESYS的静态分析功能,可以标记出所有除法运算。
5.5 预防措施:从设计阶段就规避
最好的除零防护,是在设计阶段就避免除零的发生。几个设计原则:
原则一:除数不用HMI直接写入的变量。HMI写入的变量可能在任何时刻变成0。如果必须用,加一层缓冲或校验。
原则二:配方切换用双缓冲。不要直接修改正在使用的配方变量,而是先写到缓冲区,切换时原子性地替换。
原则三:模拟量采集加滤波和范围检查。模拟量采集的原始值可能因为干扰变成0,加滤波和范围检查可以过滤掉异常值。
原则四:通讯数据加校验。来自通讯的除数变量,加校验和超时检测,确保数据有效。
原则五:关键除法加冗余。对于安全相关的除法,用两个独立的计算路径,对比结果,不一致就报警。
6. 跨平台适配与代码移植
6.1 西门子TIA Portal的适配要点
西门子的ST语言和标准IEC 61131-3有一些差异。移植安全除法FB时要注意:
- 西门子的
REAL和LREAL要区分,LREAL是64位浮点 ABS函数在西门子里是ABS(),但要注意参数类型- 西门子不支持
__TRY/__CATCH,异常处理靠OB - OB121里可以读取
OB121_ERR结构体,获取错误详情
西门子的OB121处理模板:
// OB121: 编程错误 // 读取错误信息 #tempErrorCode := OB121_ERR; // 记录日志 // 决定是否继续 // 如果错误严重,调用STOP指令6.2 汇川AM系列的适配要点
汇川AM系列基于CODESYS,支持__TRY/__CATCH,但要注意:
- CODESYS的异常捕获有性能开销,高频任务慎用
- 默认配置下,浮点除零返回NaN,不抛异常
- 可以在编译选项里开启"运行时检查",让除零抛异常
- 汇川的库函数和标准CODESYS略有差异,用之前查手册
6.3 倍福TwinCAT 3的适配要点
TwinCAT 3的异常处理比较完善:
- 支持
__TRY/__CATCH/__FINALLY - 支持
Tc2_System库里的错误处理函数 - 可以在任务配置里设置异常处理策略
- 支持在线修改代码,调试方便
6.4 三菱FX系列的适配要点
老式三菱FX系列没有异常捕获机制,只能靠代码级防护:
- 所有除法前必须判零
- 没有NaN/Inf检测,因为FX系列可能不支持这些特殊值
- 建议用整数运算代替浮点运算,减少精度问题
- 如果必须用浮点,加范围检查
6.5 代码移植的通用原则
跨平台移植安全除法代码,遵循几个原则:
原则一:用标准IEC 61131-3语法。避免使用平台特有的扩展语法,除非必要。
原则二:抽象平台差异。把平台特有的异常处理封装成统一的接口,上层逻辑不关心底层实现。
原则三:测试覆盖。移植到新平台后,用故障复现程序测试,确认防护有效。
原则四:文档记录。记录每个平台的适配要点和差异,方便后续维护。
7. 从除零看PLC程序的健壮性设计
7.1 健壮性设计的三个层次
除零只是PLC程序健壮性问题的一个缩影。我把健壮性设计分为三个层次:
层次一:不出错。通过良好的设计,避免错误发生。比如除数不用HMI直接写入的变量,配方切换用双缓冲。
层次二:出错能发现。如果错误发生了,能及时检测到。比如加判零防护、NaN检测、错误标志。
层次三:出错能恢复。检测到错误后,能自动恢复或安全停机。比如异常捕获、故障切换、安全状态。
大多数PLC程序只做到了层次一,少数做到了层次二,做到层次三的很少。但真正健壮的程序,三个层次都要有。
7.2 防御性编程的实践
防御性编程的核心思想是:假设所有输入都是不可信的。具体到PLC程序:
- 所有来自HMI的输入,加范围检查和类型检查
- 所有来自通讯的数据,加校验和超时检测
- 所有来自模拟量采集的数据,加滤波和范围检查
- 所有除法运算,加判零防护
- 所有数组访问,加索引范围检查
- 所有指针操作,加空指针检查
这些检查会增加代码量,但能大幅降低现场故障率。我算过一笔账:加防护的代码量增加约20%,但现场故障率降低约80%。这个投入产出比很划算。
7.3 故障安全设计
对于安全相关的PLC程序,故障安全设计是必须的。核心原则是:故障时进入安全状态。
除零场景下的故障安全设计:
- 检测到除零,立即停止相关输出
- 切换到备用控制逻辑
- 触发报警,通知操作工
- 记录故障信息,便于事后分析
安全状态的定义取决于具体应用。比如阀门控制,安全状态可能是关闭阀门;电机控制,安全状态可能是停止电机。
7.4 代码review的检查清单
我团队里做代码review时,除零防护是必查项。检查清单:
- [ ] 所有除法运算是否有判零防护
- [ ] 浮点数比较是否用了阈值而不是等号
- [ ] 除零后的默认值是否合理
- [ ] 是否有错误标志和日志
- [ ] 关键除法是否有冗余计算
- [ ] 异常处理是否覆盖了所有平台
- [ ] 是否有故障安全逻辑
- [ ] 是否在仿真环境验证过
这个清单看起来简单,但能拦住大部分除零相关的bug。
7.5 持续改进:从故障中学习
每次现场故障都是一次学习机会。我习惯在故障解决后做三件事:
第一,写故障报告。记录故障现象、排查过程、根因、解决方案、预防措施。这份报告是团队的财富。
第二,更新检查清单。如果故障暴露了检查清单的盲区,及时补充。
第三,分享经验。在团队内部分享故障案例,让其他人避免同样的坑。
除零这个问题,我就是从一次现场故障中学到的。那次故障之后,我把所有项目的除法都加了防护,之后再没出现过类似问题。
8. 写在最后:几个实用建议
关于除零防护,最后再分享几个实用建议。
建议一:把安全除法FB加入你的标准库。每个项目都从标准库开始,不用每次重新写。标准库里的FB经过多次验证,可靠性更高。
建议二:在项目模板里预置异常处理框架。新项目创建时,自动包含OB121、异常捕获、错误日志等基础框架,减少遗漏。
建议三:定期做故障注入测试。故意在程序里注入除零、数组越界、空指针等故障,验证防护是否有效。这种测试能发现很多潜在问题。
建议四:关注PLC的固件更新。不同固件版本对异常的处理可能不同,更新固件后要重新验证防护方案。
建议五:不要过度防护。防护是为了提高可靠性,不是为了炫技。如果一个除法永远不会除零(比如除以常数),就不用加防护。过度防护会让代码臃肿,反而增加维护成本。
我在实际项目中的体会是,除零防护这件事,投入很小,收益很大。一个简单的判零,可能就避免了一次产线停机。而一次产线停机的损失,可能够你写一年的防护代码。这笔账,怎么算都划算。