FB_DiagOB · 诊断 OB 三件套

把 CPU 的诊断组织块(OB80 / 82 / 83 / 86 / 121 / 122)统一收进一个全局 DB: OB 里只记账,OB1 里做判定,不再让十几个 OB 各自为政。
S7-1200 / S7-1500 · TIA Portal SCL · 三个 .scl 文件一套 · 无需额外指令库
✅ 实机导入通过:2026-10-09 已导入 TIA 工程并运行;配套 Python 离散仿真(1:1 复刻递推逻辑) 8 个场景、79 项断言全部通过。站上的规矩没变 —— 没跑通的不放上来。
下载完整包(zip · 9 个 SCL + 仿真脚本 + 导入指南)
TIA Portal SCL · 免费,可直接用于工程,出问题欢迎反馈

一句话: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事件S7-1200S7-1500有配对「离去」
OB80循环时间超限✅✅❌ 没有
OB82模块诊断中断(断线 / 短路 / 超量程)✅✅✅ 有
OB83分布式模块拔插✅ FW V4 起✅✅ 有
OB86PN IO 设备 / DP 从站故障✅ FW V4 起✅✅ 有
OB121编程错误❌ 不支持✅❌ 没有
OB122I/O 访问错误❌ 不支持✅❌ 没有

二、文件清单与导入顺序

顺序文件内容说明
1DiagOB_Types.sclUDT_DiagEvent + UDT_DiagState + DB_DiagOB必须最先导,后两个都依赖它
2FC_DiagOBReport.sclFC_DiagOBReport每个诊断 OB 里调一次
3FB_DiagOB.sclFB_DiagOBOB1 里每个周期调一次

导入路径:项目树 → 外部源文件 → 添加新的外部源文件 → 选中 .scl → 从源生成块。 不要先手动新建同名块,否则会冲突。

三个编码版本怎么选

后缀编码用途
(无)UTF-8 + BOM阅读与留档,不推荐直接导入
_ENUTF-8 + BOM,纯 ASCII首推导入版,任何 TIA 语言环境都不会出编码问题
_GB2312GBK 无 BOM想要中文注释时用这个导入
DB_DiagOB 的「优化的块访问」保持 TIA 默认(开启)。这套走符号访问,不开反而容易出岔子。 (注意:这和 Modbus 写缓存那套的要求相反,别混。)

不想下整个压缩包,单文件直接点(右键另存也行):


三、六个 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/CBPN 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 输入

变量类型默认说明
bEnableBOOLTRUEFALSE 时不判定不清零,输出保持
bResetBOOLFALSE上升沿清所有故障位、计数、首件留样
tDedupTIMET#2s去重窗口。同一来源在窗口内反复触发只算一次新事件,其余进 RepeatTotal
bAutoClearHwBOOLTRUE硬件故障离去时是否自动配平,见第六节 —— 这条要读
bLocalErrBOOLFALSE本地错误通道入口(1200 抓程序类错误靠它)
wLocalErrIdWORD16#0000本地错误码,随 bLocalErr 上升沿被锁存

FB_DiagOB 输出

变量类型说明
bAnyFaultBOOL硬件 / 程序 / 循环 任一故障(不含 bLocalFault)
bHwFaultBOOL硬件类故障未确认(82/83/86),稳态量,每周期重算
bProgFaultBOOL程序类故障未确认(121/122)
bCycFaultBOOL循环时间类故障未确认(OB80)
bLocalFaultBOOL本地错误通道上报过未确认
bNewEventBOOL单周期脉冲:本周期收到了新事件
bNewProgErrBOOL单周期脉冲:本周期收到了新程序类错误
bRepeatBurstBOOL同一来源正在刷屏,直到安静一个窗口才落
bParaFaultBOOL参数自检没过(tDedup 越界)
bClockValidBOOLCPU 时钟可信(年份 2020…2099)
byVerBYTE版本指纹,在线监视到 16#01 即本版
wLastCodeWORD本批最新事件的故障码,读法见第八节
wLastLocalErrWORD最近一次本地错误码
dwEvtTotalDWORD事件总数(到达 + 离去都算)
dwRepeatTotalDWORD去重窗口内被吞掉的重复次数
uHistCountUINT历史已存条数(最多 16)

脉冲 vs 稳态:bNewEvent / bNewProgErr 是脉冲,只在事发那一周期为 TRUE,用来触发报警记录; bHwFault / bParaFault / bClockValid 是稳态量,每周期重算,用来驱动画面指示灯。别混用。

FC_DiagOBReport 输入

变量类型说明
wOBNrWORDOB 号:80/82/83/86/121/122
byEvClassBYTE事件类别,83/86 用(16#38 / 16#39)
byFaultIdBYTE故障 ID。OB80/83/86/121/122 都有 Fault_ID,务必接;OB82 没有,填 16#00
wIOstateWORDIO 状态字,82 用
wLaddrUINT硬件标识符。OB 侧是 HW_ANY,基本类型是 Uint,声明成 WORD 编译不过
wChannelUINT通道号
bLeavingBOOLTRUE = 这是「故障消除」事件
stInOut"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#5200OB82(0x52=82),无细节码
16#5639OB86,Event_Class = 39 → 设备故障到达
16#56CBOB86,Fault_ID = CB → 设备故障恢复(离去)
16#5351OB83,Fault_ID = 51 → 模块拔掉
16#7A42OB122(0x7A=122),Fault_ID = 42 → 读 I/O 地址出错
16#79xxOB121(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 周期,无误报、无计数漂移
2OB82 到达 → 离去,计数配平、故障位熄灭
3OB121 同步错误,无配对离去,必须人工复位
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 条区域长度错误 —— 现场的体验是「看着出了三千次故障,其实就一件事」。这类行为在正常运行条件下看不见,只能靠建模造出来。

十一、导入报错排查(两条硬坑,都是真踩过的)

  1. 报「语法错误 + 意外的文件结束」→ 文件末尾缺换行符。下载后别用某些编辑器改末尾,本包九个文件均已检查过末尾换行齐全。
  2. 报「该函数返回了一个值」→ RD_LOC_T 的 RET_VAL 没接。必须写成 iTimeRc := RD_LOC_T(OUT => ...), 那个 iTimeRc 别当无用变量清掉。
  3. TIA 报的行号 ≠ 文件里的行号,别按行号去找,按报错关键字找。
  4. 如果 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 —— 记账是全局的,判定是每实例各自的。

使用与免责:源码按「现状」免费提供,仅供学习交流与合法用途。已在 TIA 工程实机导入运行, 并通过 Python 1:1 离散仿真验证(79 项断言),但用到项目上请自行验证,关键工况务必先在测试台上跑一遍。 用着有问题、或者想要什么功能,欢迎来信。
← 程序源码板块 FB_ModbusRtuSlave 自由口 Modbus RTU 从站 电气自控工具箱 娱乐板块 关于我