Siemens S7 OB86 DATE_AND_TIME: SCL Timestamp Capture Fix

David Krause15 min read
S7-300SiemensTroubleshooting
Licensed PE Working through this on a live machine? A Maine-licensed engineer can take it from here — included with IMD hardware, by the hour for everything else. Book an engineer

1. Problem Overview

The SIMATIC S7 organization block OB 86 is the rack/DP-slave failure OB. Engineers routinely copy its start information into a data block so that an HMI, SCADA, or historian can later display the exact moment a PROFIBUS slave, PROFINET device, or central expansion rack dropped off the network. In SCL, two common patterns fail silently:

  1. A BLKMOV (or its SCL synonym MOVE_BLK / MOVE) with source #OB86_DATE_TIME and destination of type DATE_AND_TIME in a DB produces a move of the start-info bytes, but the DB field still reads DT#00-00-00 00:00:00 or all zeroes when later read.
  2. A direct assignment in SCL
    "Trap_DB".TRAP[1].OB86_DATE_TIME := OB86_DATE_TIME;
    compiles without error, runs without a compiler diagnostic, yet at runtime the destination stays at its initial value or shows garbage in the year / month fields.

The fault is not the OB itself. The OB is called correctly by the operating system and OB86_DATE_TIME is a valid temporary of type DATE_AND_TIME. The fault is the way SCL and the older BLKMOV block treat the 8-byte BCD structure in specific firmware/compiler combinations.

2. When the CPU Calls OB 86

Per the official STEP 7 / TIA Portal reference for organization blocks, the operating system invokes OB 86 in the following situations:

  • Failure of a central expansion unit is detected (not applicable on S7-300 — applies to S7-400 only, both incoming and outgoing events).
  • Failure of a distributed I/O node on PROFIBUS-DP or PROFINET IO is detected — again both at the incoming (fault appears) and outgoing (fault clears) edges.
  • Failure of a PROFINET IO submodule is reported through the system diagnostics and propagates to OB 86 only on CPUs that have PROFINET IO system-level diagnostics enabled.

The CPU calls OB 86 in two distinct events: OB 86 starts as the failure appears and OB 86 starts as the failure clears. The temporary OB86_EV_CLASS tells you which edge you are handling (B#16#38 / 39 incoming, B#16#38 / 39 / 3A / 3B outgoing depending on firmware). This distinction matters when you build a circular log, because you may want one row for "incoming" and one for "outgoing" or update an existing row in place.

Reference: Rack failure organization block (OB 86) — STEP 7 / TIA Portal V21 documentation.

3. OB 86 Local Data Layout (Temp Area)

The OB 86 start information occupies 32 bytes of the L stack. The exact byte map is shown below. Sizes are fixed and identical for S7-300, S7-400, and the S7-1500 PROFINET variant.

Byte Name Type Meaning
0 OB86_EV_CLASS BYTE Event class — B#16#38 incoming failure, B#16#39 outgoing recovery (PROFIBUS), B#16#3A / 3B PROFINET variants
1 OB86_FLT_ID BYTE Fault ID — B#16#01 DP master failure, B#16#02 DP slave failure, B#16#03 PROFINET IO failure, B#16#04 rack failure (S7-400)
2-3 OB86_PRIORITY BYTE Priority class of the OB
4-5 OB86_OB_NUMBR BYTE OB number 86
6-7 OB86_RESERVED_1 BYTE Reserved
8-9 OB86_MDL_ADDR WORD Logical address of the affected module / slot (PROFIBUS: slave diagnostic address; PROFINET: device number in low byte)
10-11 OB86_Z1 / Z2 WORD / DWORD Slot or subslot identifier depending on fault ID
12-15 OB86_Z3 / Z4 DWORD Module / channel diagnostic data (PROFINET extended channel status)
16-23 OB86_DATE_TIME DATE_AND_TIME Time stamp of the event — eight BCD-encoded bytes
24-31 OB86_Z5 / Z6 / reserved Additional fault-specific information; layout depends on fault ID

The eight bytes of OB86_DATE_TIME encode year, month, day, hour, minute, second, and two "weekday + reserved" nibbles in BCD (binary-coded decimal), the legacy S7 format. SCL and most modern libraries prefer the new DTL type (introduced with S7-1500). On a classic S7-300/400 you must therefore use DATE_AND_TIME as the DB type, or convert explicitly to DTL with a hand-rolled BCD-to-INT conversion.

4. Root Cause — Why BLKMOV and SCL Direct Assignment Can Fail

Three real-world root causes are responsible for the symptom. Engineers should diagnose which one applies before changing code.

4.1 Cause A — Local data has already been overwritten by an FB call

When you call an FB from inside OB 86 and the FB uses DATE_AND_TIME as an IN_OUT parameter, the FB's own temp area is initialised in the L stack at the FB's own offset. On STEP 7 V5.5 and on early TIA Portal V13-V15 firmware, the FB's TEMP initialisation can collide with the OB's TEMP area and the OB86_DATE_TIME bytes you intended to capture end up zero or all-16#FF. Symptom: OB86_DATE_TIME is valid inside the OB (you can move it before calling the FB) but empty after the FB returns. Workaround: BLKMOV / MOVE the value into a global DB before calling any FB.

4.2 Cause B — Wrong interpretation of the second byte of DATE_AND_TIME

The legacy DATE_AND_TIME type uses 8 bytes. The year is stored in the 7th byte (offset 6 of the 8-byte structure) and the low nibble of the 8th byte contains a "weekday" 1 = Sunday … 7 = Saturday. Some SCL compilers on S7-300 with firmware < V3.3 do not support direct assignment between two DATE_AND_TIME symbols and silently promote the source to POINTER. The move completes on the pointer, not on the bytes, so the destination DB field reads an interpreted pointer value. Symptom: OB86_DATE_TIME in the DB shows a non-zero pattern that is not a valid date (year > 99, month > 12). Workaround: use a byte-array AT view, which forces a raw 8-byte copy.

4.3 Cause C — Destination DB field initial value blocks the move

If the DB field is part of a retain block and was previously written by a different OB, SCL's BLKMOV on some firmware versions refuses to write the source value over a non-zero initial value and the destination retains the old timestamp. Symptom: every OB 86 event shows the timestamp of the first event. Workaround: explicitly zero the destination with FILL_BLK (SCL FILL) before the move, or mark the DB as non-retain.

5. Solution 1 — AT-View Byte Copy (SCL)

Most reliable method for S7-300/400 with SCL: declare an AT overlay on both source and destination so the compiler is forced to treat the data as 8 raw bytes. This works on every STEP 7 version from V5.4 SP5 onward and on every TIA Portal version from V13 onward.

FUNCTION_BLOCK FB_Trap86
VAR_TEMP
  info : DATE_AND_TIME;
  info_bytes AT info : ARRAY[0..7] OF BYTE;
END_VAR
VAR
  Trap_DB : "TRAP"; // global DB with structure TRAP_ARRAY[0..63] OF TRAP_REC
END_VAR

BEGIN
  // Capture time stamp of OB 86 invocation
  info := OB86_DATE_TIME;

  // Now do a raw byte copy into the next free entry
  Trap_DB.TRAP[DB_INDEX].OB86_DATE_TIME := info;
  Trap_DB.TRAP[DB_INDEX].OB86_EV_CLASS := OB86_EV_CLASS;
  Trap_DB.TRAP[DB_INDEX].OB86_FLT_ID   := OB86_FLT_ID;
  Trap_DB.TRAP[DB_INDEX].OB86_MDL_ADDR := OB86_MDL_ADDR;
  Trap_DB.TRAP[DB_INDEX].OB86_Z1       := OB86_Z1;
  Trap_DB.TRAP[DB_INDEX].OB86_Z2       := OB86_Z2;
END_FUNCTION_BLOCK

The trick is that info_bytes AT info overlays the 8 BCD bytes on the same storage as info. If a compiler is suspicious of a direct DATE_AND_TIME assignment, you can replace the single line with a manual MOVE_BLK:

MOVE_BLK(VARIANT := info_bytes, COUNT := 8, DEST_VARIANT := Trap_DB.TRAP[DB_INDEX].OB86_BYTES);

where OB86_BYTES is an ARRAY[0..7] OF BYTE exposed under OB86_DATE_TIME via another AT overlay in the global DB.

6. Solution 2 — Explicit BCD-to-Integer Conversion

If you want the timestamp in plain integers (so you can compare day, month, year inside SCL), extract each nibble explicitly. This is the only method that gives you a usable date in SCL on S7-300 firmware < V3.3.

FUNCTION FC_BCD_DT_TO_DTL : VOID
VAR_INPUT
  src : DATE_AND_TIME;
END_VAR
VAR_OUTPUT
  year  : INT;
  month : INT;
  day   : INT;
  hour  : INT;
  min   : INT;
  sec   : INT;
  ms    : INT;  // 0 on S7-300, OB does not deliver milliseconds
END_VAR
VAR_TEMP
  b AT src : ARRAY[0..7] OF BYTE;
  lo : BYTE;
  hi : BYTE;
END_VAR
BEGIN
  // Bytes 0..6 are BCD; byte 7 is weekday+reserved
  lo := b[0] AND 16#0F;
  hi := b[0] SHR 4;
  year  := 2000 + WORD#16#100 * hi + lo;

  lo := b[1] AND 16#0F;
  hi := b[1] SHR 4;
  month := WORD#16#100 * hi + lo;

  lo := b[2] AND 16#0F;
  hi := b[2] SHR 4;
  day := WORD#16#100 * hi + lo;

  lo := b[3] AND 16#0F;
  hi := b[3] SHR 4;
  hour := WORD#16#100 * hi + lo;

  lo := b[4] AND 16#0F;
  hi := b[4] SHR 4;
  min := WORD#16#100 * hi + lo;

  lo := b[5] AND 16#0F;
  hi := b[5] SHR 4;
  sec := WORD#16#100 * hi + lo;

  ms := 0;
END_FUNCTION

You then call FC_BCD_DT_TO_DTL from inside OB 86 and store the result as INT fields in the global DB. This eliminates the BCD-encoding ambiguity entirely and is forward-compatible with the S7-1500 DTL structure once you map each INT to a DTL element.

7. Solution 3 — Use RD_SYS_T as a Cross-Check

RD_SYS_T reads the CPU's time-of-day in DTL format on the S7-1500 and in DATE_AND_TIME on the S7-300/400 (SFC 1). Many engineers call it from inside OB 86 to confirm the OB's OB86_DATE_TIME is consistent with the system clock. The two should agree within a few scan cycles:

VAR_TEMP
  sys_dt : DATE_AND_TIME;
END_VAR
BEGIN
  SFC1(RET_VAL := #ret, CDT := sys_dt);
  // Compare sys_dt vs OB86_DATE_TIME - the gap is the OB dispatch latency
  IF sys_dt <> OB86_DATE_TIME THEN
    Trap_DB.TRAP[idx].DELTA_MS := 1; // see Section 8 for delta calculation
  END_IF;
END

This is also a useful verification trick: if OB86_DATE_TIME is stuck at the CPU cold-start date and RD_SYS_T returns the correct current time, you have confirmed that the failure is at the OB-to-DB write path (Cause A, B, or C above) and not at the CPU clock.

8. Building a Multi-Event Trap DB

Capture every event (incoming and outgoing) into a ring buffer of TRAP_REC entries. The recommended DB layout is:

Symbol Type Initial Comment
TRAP ARRAY[0..63] OF TRAP_REC 64-entry ring; overwriting is acceptable if the HMI polls faster than the failure rate
HEAD DINT 0 Index of next free slot, 0..63
COUNT DINT 0 Monotonically increasing event counter — HMI can use COUNT % 64 to align with TRAP
WARN BOOL FALSE Latched fault presence for HMI annunciator

TRAP_REC structure:

TYPE TRAP_REC
STRUCT
  OB86_DATE_TIME : DATE_AND_TIME;
  OB86_EV_CLASS  : BYTE;
  OB86_FLT_ID    : BYTE;
  OB86_MDL_ADDR  : WORD;
  OB86_Z1        : WORD;
  OB86_Z2        : WORD;
  OB86_Z3        : DWORD;
  OB86_Z4        : DWORD;
  OB86_Z5        : DWORD;
  OB86_Z6        : DWORD;
  OB86_BYTES AT OB86_DATE_TIME : ARRAY[0..7] OF BYTE;
END_STRUCT
END_TYPE

The AT overlay on the record itself gives every HMI script or WinCC tag access to the raw 8 bytes for direct BCD parsing, while structured access to OB86_FLT_ID etc. remains type-safe in SCL.

9. Step-by-Step Implementation in OB 86

  1. Insert OB 86 from the right-hand "Standard library > System blocks" tree in TIA Portal / STEP 7.
  2. Open the OB and switch the language to SCL if you are using SCL; leave in LAD/FBD if you prefer BLKMOV ladder segments.
  3. Insert a call to FB_Trap86 (SCL) or to a wrapper FB that contains a BLKMOV network (LAD).
  4. Inside the FB, first move OB86_DATE_TIME into a local DATE_AND_TIME temp, then assign the temp to Trap_DB.TRAP[HEAD].OB86_DATE_TIME.
  5. Increment HEAD := (HEAD + 1) MOD 64 and COUNT := COUNT + 1 after the writes, not before. This way, if OB 86 is re-entered because of a burst of diagnostics, the previous slot is still consistent.
  6. Set WARN := TRUE for incoming (B#16#38 / 39), clear it on outgoing only if no other events are pending in the ring buffer.

10. Verification Checklist

After the implementation, perform these checks in order. Each check has an explicit pass criterion.

# Check Pass criterion
1 Trigger an OB 86 event by pulling a PROFIBUS connector or simulating with STEP 7's "PLC > Diagnostic/Process > Simulate
The text above was cut off due to length, but the pattern continues. Below is the remainder of the verification table in compressed form for the engineer to apply.
# Check Pass criterion
1 Trigger OB 86 by unplugging a PROFIBUS DP slave diagnostic connector or by using TIA Portal "Online & diagnostics > Simulate module failure". OB 86 executes, Trap_DB.HEAD advances by 1, Trap_DB.COUNT increments.
2 Read Trap_DB.TRAP[HEAD-1].OB86_DATE_TIME in the watch table in the format DT#YY-MM-DD HH:MM:SS. Value matches the time in the diagnostic buffer (drift < 1 s is acceptable; large drift means the L stack is being clobbered).
3 Replug the connector and confirm the outgoing event is logged with the same module address and a later time stamp. OB86_EV_CLASS = B#16#39 / 3B (outgoing), OB86_MDL_ADDR unchanged, WARN clears if the buffer is empty.
4 Cross-check OB86_DATE_TIME with SFC 1 / RD_SYS_T called at the same moment. Both values are within 1 second of each other.
5 Trigger a burst: pull two slaves within the same scan. The ring buffer must contain two distinct entries. COUNT advances by 2, no TRAP[i].OB86_DATE_TIME is duplicated.
6 Force a power cycle with the buffer full. After restart, the HMI reads retained COUNT and unwinds correctly. WARN state matches the actual present fault.

11. Common Errors and Their Fixes

Symptom in DB Likely cause Fix
DT#90-12-31 24:63:63 or all bytes = 16#FF Cause B: SCL treated DATE_AND_TIME as pointer / array Use AT overlay; copy 8 raw bytes with MOVE_BLK
Always the time of first event Cause C: retain-overwrite behaviour Set DB attribute "Non-retain" or call FILL_BLK with 0 first
Empty until the FB returns, then OK Cause A: FB TEMP clobbered the OB TEMP Move into the global DB before calling the FB
Random year > 89 BCD interpretation wrong: you read byte 6 as offset 5 Remember year is at byte offset 6, not 5
No event at all OB 86 was deleted; CPU goes into stop on failure Reinsert OB 86; verify OB priority in CPU properties
Event captured but no slot allocated FB calls itself recursively on OUT event Distinguish incoming / outgoing with OB86_EV_CLASS and only log one of them

12. Comparison of Capture Methods

Method CPU requirement Compiler Reliability Read-out ease Notes
SCL direct := OB86_DATE_TIME S7-300 >= V3.3, S7-1500 STEP 7 V5.5 SP4+, TIA V13+ Medium High Quickest, but fails on Cause A/B
SCL with AT overlay All All High Medium Recommended default
LAD/FBD BLKMOV All All High on S7-400, medium on S7-300 High Watch the enable input — must be TRUE
BCD-to-INT conversion FC All All Very high Medium Most portable, also works on S7-1500 with DTL
RD_SYS_T + delta check S7-300 (SFC 1), S7-1500 (native) All High High Use for cross-check, not as primary capture

13. Field-Commissioning Notes

  • Always keep a default OB 86 with a single BE in the project. If you only have an FB version, the CPU will still call the default OB and your FB through the priority class — but only if the default OB exists. Missing default OB stops the CPU on the first PROFIBUS disturbance.
  • For PROFINET IO, fault IDs are 16#03, 16#04, 16#05, 16#06, 16#07, 16#08 and the OB86_MDL_ADDR interpretation changes: low byte = device number, high byte = 0. Confirm in the hardware catalog of the device.
  • If you migrate the program from S7-300 to S7-1500, switch the DB type from DATE_AND_TIME to DTL. The 8-byte BCD form is still accessible through the AT overlay so your old FCs can keep working during cutover.
  • Always store the event count modulo the ring size on the HMI. Use the formula index = COUNT MOD 64 to keep the display aligned with the controller's writer.

14. Diagnostic Buffer Cross-Reference

For every entry in the TRAP ring, look up the corresponding entry in the CPU's diagnostic buffer. The buffer ID for OB 86 events is "Event ID 16#38E1 — Rack failure" and "Event ID 16#38E2 — Distributed I/O failure". Comparing the two confirms that you are capturing the same event the operator sees. If they disagree, the FB that wrote to the DB is writing to a wrong address — re-check the DB index calculation.

Why does my SCL direct assignment OB86_DATE_TIME := Trap_DB.TRAP[i].OB86_DATE_TIME leave the destination empty?

On S7-300 firmware < V3.3 and on STEP 7 V5.5 compilers earlier than SP4, the SCL compiler treats DATE_AND_TIME as a structured type and may emit code that re-initialises the destination pointer instead of writing 8 raw bytes. Use an AT-view overlay or a MOVE_BLK of 8 bytes; both force a byte-level copy and are reliable on every firmware from V3.3 onward.

Is BLKMOV the same as the SCL MOVE instruction for an 8-byte DATE_AND_TIME?

At the bit level yes — both copy 8 raw bytes. In the SCL editor the source line is simply "Trap_DB.TRAP[i].OB86_DATE_TIME := OB86_DATE_TIME;" without explicit BLKMOV. In LAD/FBD the BLKMOV block must be enabled permanently and the IN/OUT pointer must reference the OB temp; the OB temp is valid only for the duration of the OB, so call the BLKMOV network before exiting OB 86.

Does OB86_DATE_TIME carry milliseconds on S7-300?

No. The DATE_AND_TIME type on S7-300/400 always stores 0 milliseconds; only S7-1500 DTL (and SFC 1 RD_SYS_T) can deliver milliseconds. If you need ms resolution, read the CPU clock with SFC 1 in OB 86 and treat that value as the OB 86 timestamp; the resolution will be the resolution of the CPU's tick.

Can I rely on the same DATE_AND_TIME value for incoming and outgoing OB 86 events?

No — the operating system updates the temp on every call, so an outgoing OB 86 will have a time stamp close to the recovery moment, not the original failure. If you want to log both edges into one row, update an existing row whose address matches OB86_MDL_ADDR on the outgoing call and add a new row on the incoming call.

What is the correct way to convert DATE_AND_TIME to DTL when migrating an S7-300 trap block to S7-1500?

On the S7-1500 read the OB 86 temp into a DTL tag directly (DTL is supported in start information since firmware V4.0 of S7-1500). On a classic S7-300 program that you want to forward-port, declare an AT view over the 8 BCD bytes, extract year/month/day/hour/minute/second as INTs as shown in Section 6, and assign them to the corresponding DTL elements: .YEAR, .MONTH, .DAY, .HOUR, .MINUTE, .SECOND, .NANOSECOND (set to 0). The conversion is exact — the BCD format and the DTL BCD-style low-level encoding agree for the 6 datetime words.

Back to blog