一句话:CPU 出硬件故障或者程序跑飞时会调用对应的诊断 OB,但六个 OB 的接口长得都不一样, 每个 OB 里各写一遍处理逻辑很快就会互相打架。这套东西把它拆成三层:OB 里只负责「把事情记下来」, OB1 里负责「判断现在到底还在不在故障」。
诊断 OB80/82/83/86/121/122 OB1
┌──────────────────────┐ ┌──────────────────┐
│ 每个 OB 只调一次 │ │ 每个周期调一次 │
│ FC_DiagOBReport │────▶│ FB_DiagOB │──▶ 画面 / 报警
│ (赋值+计数+入队) │ 共享 │ (分类/去重/汇总) │
└──────────────────────┘ DB └──────────────────┘
│ │
└───────▶ DB_DiagOB ◀────┘
(UDT_DiagState 全局 DB)
为什么共享数据必须放全局 DB:诊断 OB 是异步打断 OB1 执行的,两边必须指向同一份数据; 背景 DB 属于 FB 实例,FB 被多次调用就会各算各的。所以这三个块不能合成一个 FB,也别为了省事把数据挪进背景 DB。
一、先记住这四条
- OB 不存在 = 该类事件到来时 CPU 直接 STOP。哪怕 OB 内容是空的,只要在工程里,CPU 就不会因这类事件停机。
- S7-1200 没有 OB121 / OB122。1200 上发生编程错误或 I/O 访问错误,CPU 直接 STOP,没有挽救机会 —— 想抓程序类错误只能走本套的本地错误通道(见第七节)。
- 同步错误(121/122、OB80)没有配对「离去事件」,所以它们的故障位只能人工复位 —— 这是刻意的,不是漏写。
- OB80 救不了已经跑飞的周期。一个周期内超时两倍循环监控时间时,即使有 OB80 也照样 STOP;OB80 救的是「偶尔超一次」。
支持矩阵
| OB | 事件 | S7-1200 | S7-1500 | 有配对「离去」 |
|---|---|---|---|---|
OB80 | 循环时间超限 | ✅ | ✅ | ❌ 没有 |
OB82 | 模块诊断中断(断线 / 短路 / 超量程) | ✅ | ✅ | ✅ 有 |
OB83 | 分布式模块拔插 | ✅ FW V4 起 | ✅ | ✅ 有 |
OB86 | PN IO 设备 / DP 从站故障 | ✅ FW V4 起 | ✅ | ✅ 有 |
OB121 | 编程错误 | ❌ 不支持 | ✅ | ❌ 没有 |
OB122 | I/O 访问错误 | ❌ 不支持 | ✅ | ❌ 没有 |
二、文件清单与导入顺序
| 顺序 | 文件 | 内容 | 说明 |
|---|---|---|---|
| 1 | DiagOB_Types.scl | UDT_DiagEvent + UDT_DiagState + DB_DiagOB | 必须最先导,后两个都依赖它 |
| 2 | FC_DiagOBReport.scl | FC_DiagOBReport | 每个诊断 OB 里调一次 |
| 3 | FB_DiagOB.scl | FB_DiagOB | OB1 里每个周期调一次 |
导入路径:项目树 → 外部源文件 → 添加新的外部源文件 → 选中 .scl → 从源生成块。 不要先手动新建同名块,否则会冲突。
三个编码版本怎么选
| 后缀 | 编码 | 用途 |
|---|---|---|
| (无) | UTF-8 + BOM | 阅读与留档,不推荐直接导入 |
_EN | UTF-8 + BOM,纯 ASCII | 首推导入版,任何 TIA 语言环境都不会出编码问题 |
_GB2312 | GBK 无 BOM | 想要中文注释时用这个导入 |
DB_DiagOB 的「优化的块访问」保持 TIA 默认(开启)。这套走符号访问,不开反而容易出岔子。
(注意:这和 Modbus 写缓存那套的要求相反,别混。)
不想下整个压缩包,单文件直接点(右键另存也行):
- DiagOB_Types.scl · 数据层(先导)
- FC_DiagOBReport.scl · OB 侧上报
- FB_DiagOB.scl · OB1 侧判定
- DiagOB_Types_GB2312.scl · 中文导入版
- FC_DiagOBReport_GB2312.scl · 中文导入版
- FB_DiagOB_GB2312.scl · 中文导入版
- DiagOB_Types_EN.scl · 纯 ASCII
- FC_DiagOBReport_EN.scl · 纯 ASCII
- FB_DiagOB_EN.scl · 纯 ASCII
- test_DiagOB_simulation.py · 仿真脚本
三、六个 OB 里怎么接线
各 OB 的启动信息变量名、类型,不同 TIA 版本差别很大。 抄之前先打开你工程里那个 OB,看它 Input 区实际列了哪些名字,按名字接 —— 变量名对不上就编译不过,这是最常遇到的一关。TIA V17+ / S7-1500 是IO_State/LADDR/Channel, 老教程里写的是IOstate/laddr/channel。
OB82 模块诊断中断(最常用)
触发前提:模块必须先勾选了诊断中断。IO_State 位4 = 有错误(断线、短路…),
位5 = 组态不正确,位7 = 发生了 I/O 访问错误。
"FC_DiagOBReport"(wOBNr := 82,
byEvClass := 16#00,
byFaultId := 16#00, (* OB82 接口里没有 Fault_ID *)
wIOstate := #IO_State,
wLaddr := #LADDR, (* HW_ANY,形参是 Uint *)
wChannel := #Channel,
bLeaving := (#IO_State AND W#16#00B0) = W#16#0000,
st := "DB_DiagOB".st);
判据:位4 / 位5 / 位7 全为 0 才算「这条是故障消除」。掩蔽字 W#16#00B0 = 位4 + 位5 + 位7。
OB86 PN IO 设备 / DP 从站故障(分布式项目必留)
"FC_DiagOBReport"(wOBNr := 86,
byEvClass := #Event_Class,
byFaultId := #Fault_ID,
wIOstate := 16#0000,
wLaddr := #LADDR,
wChannel := 0,
bLeaving := (#Event_Class = 16#38)
AND NOT (((#Fault_ID >= 16#C5) AND (#Fault_ID <= 16#C8))
OR (#Fault_ID = 16#F9)),
st := "DB_DiagOB".st);
| Event_Class + Fault_ID | 含义 | 判定 |
|---|---|---|
39 + CA/CB | PN IO 系统 / 设备故障 | 到达(故障) |
38 + CB | 设备故障恢复 | 离去(好了) |
38 + C5…C8 / F9 | 部分恢复但仍有毛病 | 仍算故障 |
32 / 33 | 激活 / 禁用 IO 设备 | 不算故障 |
OB83 分布式模块拔插
OB83 的 Event_Class 38/39 含义和 OB86 是反的,别照抄 OB86 的写法: 39 + 51/54 = 模块拔掉(故障);38 + 54 = 插上且匹配(离去);38 + 55/56/57 = 插上但不匹配,仍是故障;38 + 58 = 已更正访问错误(离去)。
bLeaving := (#Event_Class = 16#38)
AND ((#Fault_ID = 16#54) OR (#Fault_ID = 16#58));
OB80 循环时间超限
OB80 的启动信息只有 fault_id / csg_OBnr / csg_prio,没有 Event_Class,也没有 LADDR。
"FC_DiagOBReport"(wOBNr := 80,
byEvClass := 16#00,
byFaultId := #Fault_ID, (* 务必接,否则永远是 16#5000 *)
wIOstate := 16#0000,
wLaddr := 16#0000,
wChannel := 0,
bLeaving := FALSE, (* 同步错误,没有离去 *)
st := "DB_DiagOB".st);
fault_id:16#01 超出最大循环时间 · 16#02 请求的 OB 无法启动 · 16#07/16#09 中断队列溢出。
OB121 / OB122(仅 S7-1500)
这两个 OB 的优化接口里没有 Event_Class、没有 LADDR、没有 IO_State、没有 Channel,
唯一能给出「错在哪一类」的字段就是 Fault_ID —— 必须接,不接的话 wLastCode 低字节永远是 00。
"FC_DiagOBReport"(wOBNr := 121, (* 122 同理 *)
byEvClass := 16#00,
byFaultId := #Fault_ID,
wIOstate := 16#0000,
wLaddr := 16#0000, (* 这两个 OB 没有 LADDR *)
wChannel := #BlockNr, (* 借这一栏放出错块号 *)
bLeaving := FALSE,
st := "DB_DiagOB".st);
OB122 的 Fault_ID 只有两个值:16#42 = 读 I/O 出错,16#43 = 写 I/O 出错。
四、OB1 里怎么调
建一个背景 DB(如 DB_DiagOB_Inst),然后:
"FB_DiagOB"(bEnable := TRUE,
bReset := "HMI_复位按钮",
tDedup := T#2s,
bAutoClearHw := TRUE,
bLocalErr := "全局_LocalErr",
wLocalErrId := "全局_LocalErrId",
st := "DB_DiagOB".st,
bAnyFault => "HMI_总故障",
bHwFault => "HMI_硬件故障",
bProgFault => "HMI_程序故障",
bCycFault => "HMI_循环超时",
bLocalFault => "HMI_本地错误",
bNewEvent => "报警_新事件脉冲",
wLastCode => "HMI_故障码",
dwEvtTotal => "HMI_事件总数",
uHistCount => "HMI_历史条数");
st是 InOut,必须写"DB_DiagOB".st这种地址,不能拖一个临时变量过去。
五、接口速查
FB_DiagOB 输入
| 变量 | 类型 | 默认 | 说明 |
|---|---|---|---|
bEnable | BOOL | TRUE | FALSE 时不判定不清零,输出保持 |
bReset | BOOL | FALSE | 上升沿清所有故障位、计数、首件留样 |
tDedup | TIME | T#2s | 去重窗口。同一来源在窗口内反复触发只算一次新事件,其余进 RepeatTotal |
bAutoClearHw | BOOL | TRUE | 硬件故障离去时是否自动配平,见第六节 —— 这条要读 |
bLocalErr | BOOL | FALSE | 本地错误通道入口(1200 抓程序类错误靠它) |
wLocalErrId | WORD | 16#0000 | 本地错误码,随 bLocalErr 上升沿被锁存 |
FB_DiagOB 输出
| 变量 | 类型 | 说明 |
|---|---|---|
bAnyFault | BOOL | 硬件 / 程序 / 循环 任一故障(不含 bLocalFault) |
bHwFault | BOOL | 硬件类故障未确认(82/83/86),稳态量,每周期重算 |
bProgFault | BOOL | 程序类故障未确认(121/122) |
bCycFault | BOOL | 循环时间类故障未确认(OB80) |
bLocalFault | BOOL | 本地错误通道上报过未确认 |
bNewEvent | BOOL | 单周期脉冲:本周期收到了新事件 |
bNewProgErr | BOOL | 单周期脉冲:本周期收到了新程序类错误 |
bRepeatBurst | BOOL | 同一来源正在刷屏,直到安静一个窗口才落 |
bParaFault | BOOL | 参数自检没过(tDedup 越界) |
bClockValid | BOOL | CPU 时钟可信(年份 2020…2099) |
byVer | BYTE | 版本指纹,在线监视到 16#01 即本版 |
wLastCode | WORD | 本批最新事件的故障码,读法见第八节 |
wLastLocalErr | WORD | 最近一次本地错误码 |
dwEvtTotal | DWORD | 事件总数(到达 + 离去都算) |
dwRepeatTotal | DWORD | 去重窗口内被吞掉的重复次数 |
uHistCount | UINT | 历史已存条数(最多 16) |
脉冲 vs 稳态:bNewEvent / bNewProgErr 是脉冲,只在事发那一周期为 TRUE,用来触发报警记录;
bHwFault / bParaFault / bClockValid 是稳态量,每周期重算,用来驱动画面指示灯。别混用。
FC_DiagOBReport 输入
| 变量 | 类型 | 说明 |
|---|---|---|
wOBNr | WORD | OB 号:80/82/83/86/121/122 |
byEvClass | BYTE | 事件类别,83/86 用(16#38 / 16#39) |
byFaultId | BYTE | 故障 ID。OB80/83/86/121/122 都有 Fault_ID,务必接;OB82 没有,填 16#00 |
wIOstate | WORD | IO 状态字,82 用 |
wLaddr | UINT | 硬件标识符。OB 侧是 HW_ANY,基本类型是 Uint,声明成 WORD 编译不过 |
wChannel | UINT | 通道号 |
bLeaving | BOOL | TRUE = 这是「故障消除」事件 |
st | InOut | "DB_DiagOB".st |
六、bAutoClearHw:唯一需要你拍板的开关
设备掉站来一条「到达」,修好来一条「离去」。最直觉的写法是「到达置 TRUE、离去置 FALSE」—— 这个写法在多设备场合是错的:一个工程少则三五个、多则几十个分布式设备,用单一 BOOL 的话, 任何一个设备恢复就把全局清零了,剩下还在报错的那些凭空消失。现场最怕这种假「已恢复」。
本套用计数配平:uHwOpen : UInt 记录还没等到配对离去事件的「到达」条数。
到达:uHwOpen := uHwOpen + 1
离去:uHwOpen := uHwOpen - 1 (下限 0,上限 65535)
判定:HwFault := (uHwOpen > 0) 每周期重算
| bAutoClearHw | 行为 | 适用 |
|---|---|---|
| TRUE(默认) | 离去事件正常减计数,配平到 0 就熄灭 | 设备少(三五个以内),图省事 |
| FALSE | 到了就亮,人来看过才灭。离去照常记入历史,但不减计数,必须人工 bReset | 设备多、拓扑复杂 |
FALSE 不是偷懒,是诚实:计数配平认不出「这条离去配的是哪一路」,设备多了就可能漏账。 宁愿你手动确认一遍,也不给你一个可能是假的「已恢复」。
七、本地错误通道(S7-1200 必看)
1200 没有 OB121/122,程序出错直接 STOP。想在 1200 上抓程序类错误,只能在自己写的、有风险的块里主动接住:
// 在可能有错的调用后面取 ENO
"某个有风险的块"(...);
IF NOT ENO THEN
"全局_LocalErrId" := GET_ERR_ID();
"全局_LocalErr" := TRUE;
END_IF;
FB_DiagOB 侧看到 bLocalErr 的上升沿,就把 wLocalErrId 锁进 wLastLocalErr,
置 bLocalFault 并打一个 bNewProgErr 脉冲。常见场景:间接寻址数组越界、
DB_ANY 打开不存在的块、MOVE_BLK 长度超界、自己算出来的指针。
八、wLastCode 怎么读
两个字节:高字节 = OB 号,低字节 = 故障 ID(FaultId 非零就用它,为零才退用 EvClass)。
| wLastCode | 含义 |
|---|---|
16#5200 | OB82(0x52=82),无细节码 |
16#5639 | OB86,Event_Class = 39 → 设备故障到达 |
16#56CB | OB86,Fault_ID = CB → 设备故障恢复(离去) |
16#5351 | OB83,Fault_ID = 51 → 模块拔掉 |
16#7A42 | OB122(0x7A=122),Fault_ID = 42 → 读 I/O 地址出错 |
16#79xx | OB121(0x79=121),xx = Fault_ID → 编程错误 |
离去事件不覆盖wLastCode—— 它始终保留最后一次真正的故障。 设备修好后这个值不会变成 0,这是刻意的:画面上要一直显示「故障原因」,而不是修好的瞬间被抹掉。 HMI 上建议按%04X显示,再配一张按 OB 号分段的对照表。
九、历史环形缓冲
Hist[0..15] 保存最近 16 条事件,每条含 Serial / OBNr / EvClass /
FaultId / IOstate / Laddr / Channel / Leaving / Stamp(DTL 本地时间)。
此外还有三个不会被覆盖的快照:
| 字段 | 说明 |
|---|---|
Last | 最近一条事件(不管哪一类) |
LastHw | 最近一条硬件类(82/83/86 到达) |
LastProg | 最近一条程序类(121/122 + 本地通道) |
FirstSinceRst | 复位后第一条事件 |
FirstSinceRst 是现场最值钱的字段。后面那一大串往往都是它的跟班症状 ——
一眼看到「第一条是什么」,比翻 16 条历史有用得多。所以它是留样保护、不被环形覆盖的。
十、验证记录
仿真脚本 test_DiagOB_simulation.py 用 Python 1:1 复刻了 FB 的递推逻辑 ——
同样的环形 FIFO、同样的 DTL 时间戳换算、同样的 TOF 行为,逐周期对拍:
| 场景 | 验的东西 |
|---|---|
| 1 | 空跑 100 周期,无误报、无计数漂移 |
| 2 | OB82 到达 → 离去,计数配平、故障位熄灭 |
| 3 | OB121 同步错误,无配对离去,必须人工复位 |
| 4 | 同一 OB121 每周期刷屏:1000 条事件实际 1 个来源、999 条刷屏 |
| 5 | 一个扫描内 OB82 打断三次(到达+离去+另一路到达),只处理最新一条会随顺序翻车 |
| 6 | 本地错误通道(1200 上的程序类错误入口) |
| 7 | 边界与异常:未知 OB 号落 CntUnknown、tDedup=0 判非法、时钟 1990 判不可信、历史封顶 16、首件不被覆盖 |
| 8 | 多分布式设备的假「已恢复」、bAutoClearHw 两条路对照、封顶 65535 |
断言合计 79 项,失败 0 项。跑法:Python 3 直接 python test_DiagOB_simulation.py,无需第三方库。
为什么还要单独写仿真:这套块的价值全在「有些错误会每个扫描周期刷一次」这件事上。 真实案例:一台 1510SP-1 PN 上一条跑飞的数组下标,53 分钟里往诊断缓冲区灌了 3170 条区域长度错误 —— 现场的体验是「看着出了三千次故障,其实就一件事」。这类行为在正常运行条件下看不见,只能靠建模造出来。
十一、导入报错排查(两条硬坑,都是真踩过的)
- 报「语法错误 + 意外的文件结束」→ 文件末尾缺换行符。下载后别用某些编辑器改末尾,本包九个文件均已检查过末尾换行齐全。
- 报「该函数返回了一个值」→
RD_LOC_T的 RET_VAL 没接。必须写成iTimeRc := RD_LOC_T(OUT => ...), 那个iTimeRc别当无用变量清掉。 - TIA 报的行号 ≠ 文件里的行号,别按行号去找,按报错关键字找。
- 如果 FC 名字本身打了红线:多半是
DiagOB_Types没先导 / 没生成块,回第二步按顺序重来一遍。
十二、几个常见疑问
Q:为什么 OB 里只调一个 FC,不多做点事?
OB80 是已经超时才进来的,OB121 是已经出错才进来的。在这两处多加料,很可能二次触发或直接把 CPU 顶 STOP。
而且 OB121/122 继承出错 OB 的优先级,随时能打断 OB1。OB 里只做最必要的记账。
Q:SerialGen 为什么要最后更新?
OB1 侧靠盯 SerialGen 的变化判断「有没有新事件」。序号一变就保证内容已经完整落地了;
反过来写的话,OB1 可能读到「序号已变、内容还没写完」的中间态。
Q:复位会不会把历史也清了?
清一半。清的是四个故障位、RepeatTotal、环形历史写指针、FirstSinceRst 和 uHwOpen;
刻意保留的是 EvtTotal、六个 CntOBxx、三个快照,以及 SerialGen(它是流水号发生器,清零后新旧事件会重号)。
想彻底清零就把 DB_DiagOB 整个下载一遍。
Q:dwRepeatTotal 一直是 0 正常吗?
正常。平稳现场这个值应该很小;它突然飙升通常意味着某个块在跑飞。
Q:什么算「同一个来源」?
去重 key = OB 号 + 故障小类 + 硬件标识(Laddr)。Channel 刻意不进 key ——
多通道模块上同一类错会挨个通道报一轮(8 通道断线会来 8 条),那本来就该算多次。
Q:FB 能被多次调用吗?
能,每次调用有自己的背景 DB,但 st 必须都指向同一个 "DB_DiagOB".st —— 记账是全局的,判定是每实例各自的。