S7 FB Absolute Addressing: Accessing DATE_AND_TIME and DTL Bytes

David Krause14 min read
SiemensTechnical ReferenceTIA Portal
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

Overview

When an existing S7 program is migrated from FC (Function) to FB (Function Block), the storage model changes: an FB owns a working Instance DB that is generated automatically by the compiler. All STAT (static) variables declared in the FB interface are laid out at fixed byte offsets inside this instance DB. Once you move an old DB into the FB's static area, any logic that previously poked bytes out of the old global DB must be re-expressed against the instance DB.

The challenge discussed in the field appears when a static variable is a structured date/time type and the logic only needs a subset of its bytes. Examples:

  • Reading only the hour and minute for a shift comparison.
  • Storing the second byte in a histogram tag.
  • Copying the milliseconds field into a counter.
  • Moving the BCD-encoded year into a display word.

Symbolic names give you the whole variable; the question is how to reach a single byte, word, or bit without abandoning the FB encapsulation. Siemens offers two distinct mechanisms, depending on the controller family and engineering tool:

  • TIA Portal / S7-1200 / S7-1500: DTL structure subcomponent access and slice access (.B0, .W0, .X0) — pure symbolic.
  • STEP 7 Classic / S7-300 / S7-400: 8-byte DATE_AND_TIME with STL indirect addressing via address registers AR1/AR2.

This reference consolidates both paths, with byte-level layout tables, working STL and LAD/SCL examples, and a verification matrix.

Prerequisites

  • STEP 7 V5.x (for the Classic path) or TIA Portal V15.1 or later (V17 / V18 recommended for S7-1200 G2).
  • CPU firmware compatible with the date/time type you intend to use. DATE_AND_TIME is supported on every S7-300/400 CPU. DTL requires S7-1200 firmware V4.0+ or any S7-1500 CPU.
  • An FB block (not FC) with at least one static DATE_AND_TIME or DTL tag in its interface.
  • For STL examples: a network in STL view inside the FB. For LAD view: a MOVE box with a slice suffix on the input.

Date/Time Data Type Layout

DATE_AND_TIME (STEP 7 Classic, 8 bytes)

DATE_AND_TIME (DT) occupies exactly 8 bytes starting at the STAT offset. The encoding is BCD; the most-significant byte is byte 0 of the structure. This mapping is the single source of truth for every STL offset calculation that follows.

Byte Offset Field Range (BCD) Encoding
0 YEAR 90..99, 00..89 BCD byte (add 1900 if 90..99, 2000 if 00..89)
1 MONTH 01..12 BCD byte
2 DAY 01..31 BCD byte
3 HOUR 00..23 BCD byte
4 MINUTE 00..59 BCD byte
5 SECOND 00..59 BCD byte
6 + 7 MILLISEC + WEEKDAY 000..999, 1..7 3-digit BCD ms in upper 12 bits, weekday (1=Sun) in low nibble of byte 7

For the FB discussed in the source, a STAT named PLC_TIME declared as DATE_AND_TIME will sit at instance DB offset 26.0. Byte 3 (HOUR) of the timestamp is therefore at DBXx.DBBy29.0 (26 + 3) — but only after the FB has been compiled and the instance DB offset is confirmed from the symbol table.

DTL (TIA Portal, 12 bytes)

On S7-1200 and S7-1500, the DTL type is a 12-byte structured tag. Each field is a separate subcomponent with its own data type, so no offset arithmetic is needed at runtime — you reference the field by name.

Byte Offset Subcomponent Type Range
0..1 YEAR INT 1970..2554
2 MONTH USINT 1..12
3 DAY USINT 1..31
4 WEEKDAY USINT 1..7 (1 = Sunday)
5 HOUR USINT 0..23
6 MINUTE USINT 0..59
7 SECOND USINT 0..59
8..11 NANOSECOND DINT 0..999_999_999

Key difference vs DATE_AND_TIME: DTL is binary (not BCD), and the YEAR is a full INT (so it covers 1970..2554 without the 1900/2000 wraparound that bites Classic DT). The two types are not interchangeable; converting requires T_CONV / DTL_TO_DT or hand-coded FCs.

Instance DB Offset Discovery

The FB-internal offset of any STAT is determined by the compiler, not by the programmer. Two ways to read it back:

  1. In the FB source (STEP 7 Classic): Open the FB, right-click the variable in the interface table, choose Properties. The Address column shows the offset within the instance DB.
  2. In the Instance DB (STEP 7 Classic): Open the instance DB generated for the FB call. Each STAT shows its absolute address (for example DB100.DBD26 or DB100.DBB29).
  3. In TIA Portal: Open the FB, switch to the interface section. The Offset in the instance DB column shows the same value symbolically. There is no need to know it numerically if you only use symbolic access — but the slice syntax .B0 is resolved at compile time using exactly this offset.
Important: Adding a new STAT above PLC_TIME in the interface shifts all subsequent offsets. Always re-verify after a refactor; do not hard-code byte addresses elsewhere in the project.

Method 1 — TIA Portal: Symbolic Slice Access

TIA Portal allows you to take a slice (a sub-word, sub-byte, or single bit) out of any elementary or structured tag without leaving symbolic programming. The suffixes are:

Suffix Width Example Description
.Xn 1 bit #PLC_TIME.X0 Bit 0 of the first byte of the DTL tag.
.Bn 1 byte #PLC_TIME.B0 Byte 0 (YEAR high byte on DTL).
.Wn 1 word #PLC_TIME.W0 Word 0 (YEAR on DTL, 2 bytes).
.Dn 1 double word #PLC_TIME.D0 Double word 0 (YEAR..HOUR on DTL).

LAD Example — Extract the 4th Byte

The original question asked specifically for "the 4th byte of the 8 bytes" of PLC_TIME. With a DTL tag, byte 4 is the WEEKDAY field; with a DATE_AND_TIME tag (Classic), byte 4 is MINUTE. The slice syntax is identical regardless of controller:

// In LAD, drop a MOVE box.
// IN  : "#PLC_TIME.B3"        // byte 3 of DTL = HOUR (0..23, USINT)
// OUT : "#HourBuffer"
//
// For the 4th byte (offset index 3), use .B3.
// For the very first byte (offset index 0), use .B0.

For byte-indexed access inside a structured DTL tag, TIA Portal also accepts the named subcomponent directly:

// Equivalent, fully typed access:
// IN  : "#PLC_TIME.HOUR"
// OUT : "#HourBuffer"

Use the named form whenever the meaning of the byte is known (HOUR, MINUTE, SECOND, NANOSECOND, WEEKDAY, etc.) because it survives interface refactors automatically.

LAD Example — Two-Byte WORD Slice

If you need the calendar date as a single 16-bit word (for example to push into a panel tag), use the .W0 slice on a DTL YEAR field, or combine the two bytes you want:

// Variant A: read YEAR as INT
// IN  : "#PLC_TIME.YEAR"       // 16-bit INT
// OUT : "#DisplayYearWord"
//
// Variant B: take a raw word from the start of the structure
// IN  : "#PLC_TIME.W0"         // raw 2 bytes at offset 0
// OUT : "#RawYearWord"

LAD Example — Single Bit Inside the Tag

The .X<n> suffix addresses a single bit of the first byte of the tag:

// Read bit 0 of byte 0 of the timestamp
// IN  : "#PLC_TIME.X0"
// OUT : "#TimeBit0"

To address a bit that is not in the first byte, address a byte slice first and then take a bit on a temporary copy, or use a contact/coil directly on a struct-member boolean field such as PLC_TIME.WEEKDAY compared against a constant.

Method 2 — TIA Portal: SCL Slice on DTL

SCL is fully type-safe and resolves slice access at compile time:

// SCL inside an FB
#HourBuffer    := #PLC_TIME.B3;          // USINT, raw byte
#HourField     := #PLC_TIME.HOUR;        // USINT, semantically the hour
#YearAsWord    := WORD_TO_INT(#PLC_TIME.W0); // 16-bit word at offset 0
#WeekdayBit0   := #PLC_TIME.B4.%X0;      // first bit of weekday byte

// Example: copy the minute and second into a packed DWORD for logging
#PackedLog     := DWORD_FROM_BYTES(#PLC_TIME.B4, #PLC_TIME.B5, B#16#0, B#16#0);
SCL slice .Bn, .Wn, .Dn and .Xn are equivalent to the LAD/FBD slice syntax. Prefer the named subcomponent (.HOUR) when the meaning is known — it survives any future reordering of the FB interface.

Method 3 — STEP 7 Classic: STL Indirect Addressing

On S7-300/S7-400 with STEP 7 V5.x, you cannot take a slice out of a structured DATE_AND_TIME symbolically. The standard technique is pointer-based load using the AR1 or AR2 address register. The example code from the field discussion is reproduced and annotated below.

Pattern: Read System Time and Decode One Field

// FB network (STL view)
// Read CPU system clock into local temp "hd1" of type DATE_AND_TIME
CALL  RD_SYS_T
RET_VAL := #hd2              // return code, INT
OUT     := #hd1              // 8 bytes DATE_AND_TIME

// Load the address of the local 8-byte temp into AR1
L     P##hd1                 // pointer to hd1
LAR1                         // AR1 = &hd1

// Read the first byte (YEAR field) using area-crossing pointer access
L     B [AR1, P#0.0]        // byte 0 = YEAR (BCD)
BTI                          // BCD -> INT (not strictly needed for comparison)
L     13                     // example comparison constant
==I
=     #OUT1                  // OUT1 = 1 if year (BCD) equals 13

Reading the 4th Byte (Index 3) of PLC_TIME STAT

The original question asked how to address the 4th byte of a STAT named PLC_TIME. The instance-DB absolute address is known after compile (for example, byte 26 in the IDB). Two equivalent techniques:

// Technique A: open the instance DB and address the byte directly
// (assumes PLC_TIME sits at IDB offset 26, so byte 3 = IDB offset 29)
OPN   DI[#iDB_No]            // open the instance DB for this FB call
L     DIB 29                 // load 4th byte = MINUTE field (BCD)
BTI
T     #MinuteInt

// Technique B: load address of the STAT from its symbolic any-pointer
// (works regardless of where PLC_TIME sits in the IDB)
L     P##PLC_TIME            // pointer to first byte of PLC_TIME
LAR1
L     B [AR1, P#3.0]        // byte at offset +3 = MINUTE (BCD)
BTI
T     #MinuteInt

Both approaches compile and behave identically; Technique B is preferred during refactors because it survives offset shifts as long as the symbolic name PLC_TIME remains in the interface.

LAD-Equivalent for Classic: Use MOVE Boxes With Symbolic Tags

If the requirement is only to move individual DATE_AND_TIME fields out of an instance DB and the controller supports it (S7-400 from firmware V5.x upward offers limited structured-tag symbolic access via multi-instance or DB_ANY), the cleanest LAD pattern is:

  1. Declare a STAT DATE_AND_TIME in the FB interface.
  2. Compile the FB; TIA Portal / STEP 7 then shows the absolute offsets in the interface table.
  3. Add a STAT for each byte you need (BYTE_YEAR, BYTE_MONTH, ...).
  4. Insert MOVE boxes inside the FB that copy each byte of PLC_TIME into the target STATs.
// Suggested FB interface additions for Classic:
VAR
    PLC_TIME      : DATE_AND_TIME;   // 8 bytes at offset 26.0
    BYTE_YEAR     : BYTE;            // BCD year, copy of PLC_TIME.B0
    BYTE_MONTH    : BYTE;            // BCD month, copy of PLC_TIME.B1
    BYTE_DAY      : BYTE;            // BCD day, copy of PLC_TIME.B2
    BYTE_HOUR     : BYTE;            // BCD hour, copy of PLC_TIME.B3
    BYTE_MINUTE   : BYTE;            // BCD minute, copy of PLC_TIME.B4
    BYTE_SECOND   : BYTE;            // BCD second, copy of PLC_TIME.B5
    BYTE_MSD      : BYTE;            // milliseconds high, copy of PLC_TIME.B6
    BYTE_WD_MSL   : BYTE;            // weekday+msL, copy of PLC_TIME.B7
END_VAR

This avoids inline STL in a project that is otherwise Ladder, and gives the rest of the program symbolic access to date/time components.

Migrating DATE_AND_TIME ↔ DTL

If the refactor crosses the platform boundary (S7-300/400 → S7-1200/1500), the 8-byte BCD format must be converted. The field-proven approach is to byte-swap and re-encode:

// SCL: convert DT (8 BCD bytes) to DTL (12 bytes binary)
#dtl.YEAR       := BCD_TO_INT(#dt.B0);     // year 00..89 -> 2000..2089
IF #dtl.YEAR < 100 THEN #dtl.YEAR := #dtl.YEAR + 2000; END_IF;
#dtl.MONTH      := BCD_TO_INT(#dt.B1);
#dtl.DAY        := BCD_TO_INT(#dt.B2);
#dtl.WEEKDAY    := #dt.%B7.%X0      // weekday in low nibble of byte 7
                AND B#16#0F;        // mask the milliseconds low nibble
#dtl.HOUR       := BCD_TO_INT(#dt.B3);
#dtl.MINUTE     := BCD_TO_INT(#dt.B4);
#dtl.SECOND     := BCD_TO_INT(#dt.B5);
#dtl.NANOSECOND := DWORD_BCD_TO_INT(WORD_TO_DWORD(#dt.%W6)) * 1000000;
                // milliseconds upper 12 bits * 1_000_000
Confirm each conversion step against the relevant Siemens standard function manual. Year wrap-around (90..99 = 1990..1999, 00..89 = 2000..2089) is the single most common migration bug.

Memory-Area Reference for Absolute CPU Access

Beyond the FB instance DB, an S7-1200 program can read directly from the standard CPU memory areas. According to the S7-1200 programmable controller system manual, M memory is global and accessible from any OB, FC, or FB. The same section explains I, Q, and the local (L) stack and how each is addressed absolutely.

See: SIMATIC S7-1200 G2 Manual Collection — Using absolute addressing to access CPU data.

Best Practices

  1. Prefer symbolic over absolute. Use #PLC_TIME.HOUR (DTL) rather than DB100.DBBx whenever the target type supports it.
  2. Centralize the read. One FB-internal initialization network populates byte- and sub-component STATs from PLC_TIME; downstream code references only the populated STATs. This isolates the offset to a single place.
  3. Protect address registers. In Classic STL, save AR1/AR2 before any FB call that may itself use indirect addressing, and restore them afterwards. The reference code from the field omits this — for production code, wrap the body with TAR1 / LAR1 around the save/restore.
  4. Match types to controllers. Use DATE_AND_TIME only on S7-300/400. Use DTL on S7-1200 V4.0+ and S7-1500. Mixing triggers compile errors or silent truncation.
  5. Avoid slice access for alarm timestamps where nanosecond resolution matters. A DTL NANOSECOND field is 4 bytes; do not collapse it into a 16-bit word.
  6. Document offsets. When you must use absolute addressing, add a comment with the symbolic name and the date of last verification.
  7. Re-test after refactors. Adding a STAT above PLC_TIME shifts every subsequent byte offset. Run a consistency check (cross-reference, Go to > Instance) before re-downloading.

Troubleshooting Matrix

Symptom Likely Cause Verification Fix
Compile error "Slice access not supported on DATE_AND_TIME" Trying #t.B0 on an old DT tag Inspect tag type in FB interface Switch to STL indirect addressing or migrate to DTL
LAD MOVE box shows red X after pasting slice suffix Slice index out of range Hover for tooltip Use indices 0..7 for 8-byte DT, 0..11 for DTL
Wrong year read after migration (e.g. 1993 instead of 2023) BCD year wrap-around not handled Compare against DATE_AND_TIME source byte Add 1900 for values 90..99, 2000 for 00..89
AR1 overwritten by called FB Indirect addressing inside the called block Single-step watch of AR1/AR2 Save AR1 with TAR1 before call, restore with LAR1 after
Pointer access runtime error (SF LED) AR1 points outside instance DB Check STL pointer arithmetic against actual IDB offset Recompile FB; verify IDB number with DI register
Always read zero Reading byte of DTL tag in DTL context but expecting BCD Inspect raw byte vs named subcomponent Use named access (.HOUR) instead of .B5
Symbol "PLC_TIME" undefined in network Tag declared in TEMP, not STAT Open FB interface table Move declaration to STAT so it persists in IDB

Verification

  1. Compile cleanly. No warnings about overlapping memory or invalid slice.
  2. Cross-reference online. In the FB, right-click the slice expression and choose Go to > Usage. Confirm the target operand is the expected instance DB.
  3. Watch table test. Force the instance DB to a known DATE_AND_TIME or DTL value. Read each byte back and confirm the field mapping (byte 0 = YEAR for DT; bytes 0..1 = YEAR for DTL).
  4. Online monitor. With the FB instance open, switch to LAD/FBD view and confirm the slice indicator on the MOVE box matches the expected field.
  5. PLC trace. On S7-1500, record #PLC_TIME and the extracted STATs simultaneously to verify byte-level equivalence over time.

How do I read just the 4th byte of a DATE_AND_TIME inside an S7-300/400 FB?

Open the compiled instance DB and note the byte offset of PLC_TIME (for example 26). Byte 4 of the structure is at offset 29. In STL inside the FB use L P##PLC_TIME / LAR1 / L B [AR1, P#3.0] to load the MINUTE field as BCD, or call OPN DI[#iDB_No] / L DIB 29 to read it directly. In LAD, declare BYTE_MINUTE : BYTE as a STAT and add a MOVE box from DBxx.DBB29 to BYTE_MINUTE; this avoids STL while still exposing the byte symbolically to the rest of the program.

Can I use slice syntax .B3 directly on a DATE_AND_TIME tag in LAD?

No. Slice access .Bn, .Wn, .Dn, .Xn is supported on elementary types and on structured types whose subcomponents are exposed as named elements. DATE_AND_TIME on Classic controllers is treated as an opaque 8-byte BCD; you must use indirect addressing via AR1/AR2 or copy the structure into per-byte STATs through MOVE boxes.

What is the difference between DATE_AND_TIME and DTL?

DATE_AND_TIME is an 8-byte BCD-encoded timestamp used on S7-300/S7-400 and exposed as a flat block of bytes. DTL is a 12-byte structured type used on S7-1200 (firmware V4.0+) and S7-1500, with binary fields YEAR (INT), MONTH (USINT), DAY (USINT), WEEKDAY (USINT), HOUR, MINUTE, SECOND (each USINT) and NANOSECOND (DINT). DTL fields are named, so you reference them symbolically with #PLC_TIME.HOUR; DT fields require byte-offset arithmetic.

Why does my year read 1993 when I move it from a DT to a DTL?

DATE_AND_TIME stores the year as a single BCD byte: 90..99 is interpreted as 1990..1999 and 00..89 as 2000..2089. If you convert with a simple BTI + type cast you get the raw value (93) and not the calendar year. Add 1900 when the value is ≥ 90, otherwise add 2000, then store it in the DTL YEAR (INT) field.

Does the FB instance DB offset change when I add a new STAT above PLC_TIME?

Yes. The compiler assigns offsets in declaration order. Inserting a new STAT above PLC_TIME shifts PLC_TIME (and every byte inside it) downward by the size of the new tag. Re-verify all absolute addresses after every interface edit, and prefer indirect addressing through P##PLC_TIME over hard-coded DBxx.DBB<n> so that the code survives refactors.

Back to blog