S7-300 BLKMOV ANY Pointer for DATE_AND_TIME Storage

David Krause17 min read
S7-300SiemensTutorial / How-to
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

Capturing the PLC clock into a structured timestamp is a recurring requirement in lubrication, dosing, batch, and shift-logging applications. On a Siemens S7-300/S7-400 CPU programmed in STEP 7 (classic), the DATE_AND_TIME data type occupies 8 bytes of BCD-encoded fields: year/century, month, day, hour, minute, second, and millisecond/weekday. Copying that block into one of several destination offsets inside a data block requires indirect addressing.

This reference demonstrates three techniques — direct symbolic copy, ANY-pointer-driven BLKMOV (SFC 20), and ring/FIFO buffering — and shows exactly how each byte of the ANY descriptor is loaded for a DATE_AND_TIME move. The final code samples are written in STL and target the CPU 315-2 PN/DP and CPU 416-3 with firmware V3.3.x, the same pattern that ships unchanged on S7-400, WinAC RTX, and ET 200S IM 151-8 PN/DP CPUs. The original problem — "if Z20=0 save to slot 0; if Z20=1 save to slot 1" — maps directly to a base-offset plus slot×8 arithmetic addressed below.

Prerequisites

  • STEP 7 V5.5 SP2 (or STEP 7 Professional 2010 SR4) installed on the engineering station
  • CPU 31x or CPU 41x with firmware ≥ V3.3 that supports SFC 20 BLKMOV (effectively the entire S7-300/400 catalog)
  • Standard library "Standard Library → System Function Blocks" available in the project; both SFC 1 READ_CLK and SFC 20 BLKMOV are inside it
  • Data block DB_Guide_Lubric_Dates sized for the timestamp slots — 8 bytes per slot × number of slots, plus the live Actual_date_time field and a slot counter
  • Temporary (TEMP) area inside the calling FB or OB1 for the ANY pointer, the SFC 20 return value, and the computed byte offset
  • Understanding of BCD encoding, the area-cross bit at byte 6 of the ANY, and the rule that DB numbers occupy bytes 4/5 of the ANY in little-endian WORD format
S7-1500 compatibility: The ANY-pointer / SFC 20 pattern is legacy STEP 7. On S7-1500 with TIA Portal, use the symbolic MOVE_BLK instruction or array-of-DATE_AND_TIME indexed with a tag, which avoids the 10-byte ANY descriptor entirely. The patterns in this article apply to S7-300/400/WinAC only.

DATE_AND_TIME and ANY Pointer Format

DATE_AND_TIME byte layout (8 bytes, BCD)

Byte Content Range (BCD) Example
0 Year (century in upper nibble, year in lower nibble) 19 90 .. 20 89 B#16#21, B#16#26 = 2026
1 Month 01 .. 12 B#16#11 = November
2 Day 01 .. 31 B#16#15 = 15th
3 Hour 00 .. 23 B#16#14 = 14:00
4 Minute 00 .. 59 B#16#30 = :30
5 Second 00 .. 59 B#16#45 = :45
6 Milliseconds (low byte) 00 .. 99 B#16#30 = 300 ms
7 Milliseconds (high nibble) + weekday (low nibble) 0..9 + 1..7 B#16#23 = Mon, 230 ms

Because the length is fixed at 8 bytes, a single BLKMOV with length 8 captures the entire timestamp atomically — no need to copy individual fields. The base offset for the first slot, plus slot×8, yields the destination byte offset.

ANY pointer (10 bytes)

Byte Mnemonic Content Example: DT in DB 50 at byte 0
0 Syntax-ID Data type / area code B#16#10 (byte/word/dword/DT family)
1 Transport size Element width code B#16#08 (1 byte; BLKMOV uses the count instead)
2 Count (high) Length high byte B#16#00
3 Count (low) Length low byte B#16#08 (8 bytes for DATE_AND_TIME)
4 DB number (high) DB high byte or 0 B#16#00
5 DB number (low) DB low byte W#16#0032 = DB 50
6 Area code Memory area + area-cross bit B#16#84 (DB, area-cross)
7 Byte offset (high) Offset bits 16..23 B#16#00
8 / 9 Byte offset (low) Offset bits 0..15 W#16#0000 (byte 0) or W#16#002A (byte 42)

Memory area codes for ANY byte 6

Area Code (no area-cross) Code (area-cross, bit 7 set)
Bit memory (M) B#16#83 B#16#83
Process image inputs (I) B#16#81 B#16#81
Process image outputs (Q) B#16#82 B#16#82
Data block (DB) B#16#84 B#16#84 (recommended for offset > 8191)
Instance DB (DI) B#16#85 B#16#85
Local data (L / TEMP) B#16#86 B#16#86
Bit 7 of byte 6 is the area-cross bit. Always set it (0x80 OR 0x04 = 0x84 for DB) when the offset exceeds 8191 bytes on a CPU 315 or 16383 bytes on a CPU 416. For offsets ≤ 8191 on a CPU 315, both forms work identically, but using 0x84 is the safest default.

SFC 20 BLKMOV Fundamentals

SFC 20 BLKMOV copies a contiguous block of bytes from SRCBLK to DSTBLK. Both parameters are ANY descriptors. SFC 20 is in the "Standard Library → System Function Blocks" catalog of every STEP 7 V5.5 install.

Parameter Declaration Type Description
SRCBLK INPUT ANY Source area (must be fully inside one memory area)
RET_VAL OUTPUT INT Error code; W#16#0000 = OK
DSTBLK OUTPUT ANY Destination area (must be fully inside one memory area)

RET_VAL error codes

Code Meaning
W#16#0000 No error
W#16#8091 Source area exceeds the destination DB or area
W#16#8092 Source ANY syntax error (length 0, bad area code)
W#16#80A1 Destination area exceeds the destination DB or area
W#16#80A2 Destination ANY syntax error
W#16#80B1 SRCBLK and DSTBLK areas overlap (in-place copy rejected)
W#16#80C0 Source is write-protected (e.g., load memory)
W#16#80C2 Source or destination is load memory (FM)

See the Siemens SFC 20 BLKMOV online help for the full error list and a worked example of the area-cross mechanism.

Step-by-Step STL Implementation

Step 1 — Build the destination DB

Create DB_Guide_Lubric_Dates with the structure below. Slot 0 is the live clock; slots 1..n are historical.

DATA_BLOCK "DB_Guide_Lubric_Dates"
TITLE = Lubrication timestamp log
VERSION : 1.1
  STRUCT
    Actual_date_time : DATE_AND_TIME;     // 8 bytes, byte offset 0
    Slot_01          : DATE_AND_TIME;     // byte offset 8
    Slot_02          : DATE_AND_TIME;     // byte offset 16
    Slot_03          : DATE_AND_TIME;     // byte offset 24
    Slot_04          : DATE_AND_TIME;     // byte offset 32
    Slot_05          : DATE_AND_TIME;     // byte offset 40
    Slot_06          : DATE_AND_TIME;     // byte offset 48 (Z20 = 0 case)
    Slot_07          : DATE_AND_TIME;     // byte offset 56 (Z20 = 1 case)
    SlotCount        : INT;               // 0..n, ring index
  END_STRUCT;
END_DATA_BLOCK

Step 2 — Declare TEMP variables in the calling FB

Open FB 100 Lubrication_Log and add the following TEMP interface. Static (VAR) is used for the edge bit; TEMP is used for the ANY scratch and the SFC 20 return value.

VAR
  EdgeMem : BOOL;            // static edge memory (survives the call)
END_VAR
VAR_TEMP
  DB_ANY       : ANY;        // 10-byte scratch ANY pointer
  Aux1         : INT;        // SFC 20 RET_VAL
  DST_Offset_W : WORD;       // computed destination byte offset
END_VAR

Step 3 — Compute the destination byte offset

The slot index lives in Z20 (an INT marker or DBW). For the original problem the destination is at a base offset of 42 plus slot×8. The full formula is:

L     "Z20"                // ACCU1 = slot index
L     8                    // ACCU2 = 8 (DATE_AND_TIME length)
*I                        // ACCU1 = slot * 8
L     42                   // ACCU2 = 42 (base offset for Slot_06)
+I                        // ACCU1 = slot * 8 + 42
T     #DST_Offset_W        // store final offset for ANY assembly

Verification of the two original cases: when Z20 = 0 the offset is 42 (start of Slot_06); when Z20 = 1 the offset is 50 (start of Slot_07). Both fit inside the 64-byte DB shown above.

Step 4 — Build the destination ANY pointer in TEMP

Load the 10 bytes of the descriptor into #DB_ANY using AR1 (Address Register 1). The DST_Offset_W from Step 3 is placed in bytes 8/9.

LAR1  P##DB_ANY            // AR1 -> start of TEMP DB_ANY
L     B#16#10; T B [AR1, P#0.0];   // Syntax-ID
L     B#16#08; T B [AR1, P#1.0];   // Transport size
L     B#16#00; T B [AR1, P#2.0];   // Length high
L     B#16#08; T B [AR1, P#3.0];   // Length low = 8 bytes
L     B#16#00; T B [AR1, P#4.0];   // DB number high
L     W#16#0032; T W [AR1, P#5.0]; // DB number low = DB 50
L     B#16#84; T B [AR1, P#6.0];   // Area code = DB, area-cross bit
L     B#16#00; T B [AR1, P#7.0];   // Byte offset high
L     #DST_Offset_W; T W [AR1, P#8.0]; // Byte offset low = slot*8+42
DB number encoding: Bytes 4 and 5 hold the DB number as a little-endian WORD. The high byte goes to byte 4, the low byte to byte 5. Use W#16#0032 for DB 50, W#16#000A for DB 10, W#16#0064 for DB 100, and so on. For instance DBs use area code B#16#85 and the instance-DB number.

Step 5 — Detect the rising edge of the trigger

The timestamp should be captured only once per event. Edge-detect the contactor feedback KM31B70:

A     "KM31B70"            // contactor auxiliary contact
FP    #EdgeMem             // static edge memory bit
=     #Trigger             // local one-cycle flag

Using FP against a static bit (not a TEMP) guarantees the edge re-arms correctly across multiple calls. Using a TEMP would freeze the bit at the value left by the previous scan.

Step 6 — Call SFC 20 BLKMOV on the rising edge

The complete FB body for the lubrication log write is shown below. Place it in a single FB (multi-instance capable) and call it from OB 1 unconditionally.

FUNCTION_BLOCK FB 100
TITLE = Lubrication timestamp capture
VERSION : 1.1
VAR
  EdgeMem : BOOL;
END_VAR
VAR_TEMP
  DB_ANY       : ANY;
  Aux1         : INT;
  DST_Offset_W : WORD;
END_VAR
BEGIN
NETWORK 1   // Rising-edge detect on KM31B70
A     "KM31B70";
FP    #EdgeMem;
=     #Trigger;

NETWORK 2   // Build destination ANY pointer
LAR1  P##DB_ANY;
L     B#16#10; T B [AR1, P#0.0];
L     B#16#08; T B [AR1, P#1.0];
L     B#16#00; T B [AR1, P#2.0];
L     B#16#08; T B [AR1, P#3.0];
L     B#16#00; T B [AR1, P#4.0];
L     W#16#0032; T W [AR1, P#5.0];
L     B#16#84; T B [AR1, P#6.0];
L     B#16#00; T B [AR1, P#7.0];
L     "Z20";
L     8;
*I;
L     42;
+I;
T     W [AR1, P#8.0];

NETWORK 3   // BLKMOV on rising edge only
A     #Trigger;
JCN   END1;
CALL  "BLKMOV" (
  SRCBLK   := "DB_Guide_Lubric_Dates".Actual_date_time,
  RET_VAL  := #Aux1,
  DSTBLK   := #DB_ANY);
END1: NOP 0;
END_FUNCTION_BLOCK

Step 7 — Refresh the live value Actual_date_time

The Actual_date_time field must be refreshed from the CPU clock before BLKMOV. Place the following network in OB 1 so the value updates every cycle:

NETWORK 1   // Read system clock into DB field
CALL "READ_CLK" (
  RET_VAL := #ret,
  CDT     := "DB_Guide_Lubric_Dates".Actual_date_time);

SFC 0 SET_CLK sets the clock, SFC 1 READ_CLK reads it. Both are in the standard library. SFC 1 RET_VAL is W#16#0000 on success; W#16#8080 indicates the clock has stopped (CPU 312 IFM without MMC) and the values in CDT are zero. See the STEP 7 System and Standard Functions reference manual for the complete list.

Alternatives and Variants

Variant A — Direct symbolic copy (no ANY pointer)

For a fixed number of slots with addresses known at compile time, the symbolic copy eliminates the ANY assembly and gives full online monitoring of each field. STL fragment for the original two-slot problem:

A     "KM31B70";
FP    "M86.1";
JCN   NO_SAVE;

L     "Z20";
L     0;
==I;
JCN   SLOT1;
// SLOT 0  — copy all 4 DWORDs from Actual_date_time to Slot_06
L     DBT "DB_Guide_Lubric_Dates".Actual_date_time.year_month;
T     DBT "DB_Guide_Lubric_Dates".Slot_06.year_month;
L     DBT "DB_Guide_Lubric_Dates".Actual_date_time.day_hour;
T     DBT "DB_Guide_Lubric_Dates".Slot_06.day_hour;
L     DBT "DB_Guide_Lubric_Dates".Actual_date_time.minute_second;
T     DBT "DB_Guide_Lubric_Dates".Slot_06.minute_second;
L     DBT "DB_Guide_Lubric_Dates".Actual_date_time.msec_weekday;
T     DBT "DB_Guide_Lubric_Dates".Slot_06.msec_weekday;
JU    NO_SAVE;

SLOT1: L    "Z20";
L     1;
==I;
JCN   NO_SAVE;
// SLOT 1  — copy to Slot_07
L     DBT "DB_Guide_Lubric_Dates".Actual_date_time.year_month;
T     DBT "DB_Guide_Lubric_Dates".Slot_07.year_month;
L     DBT "DB_Guide_Lubric_Dates".Actual_date_time.day_hour;
T     DBT "DB_Guide_Lubric_Dates".Slot_07.day_hour;
L     DBT "DB_Guide_Lubric_Dates".Actual_date_time.minute_second;
T     DBT "DB_Guide_Lubric_Dates".Slot_07.minute_second;
L     DBT "DB_Guide_Lubric_Dates".Actual_date_time.msec_weekday;
T     DBT "DB_Guide_Lubric_Dates".Slot_07.msec_weekday;

NO_SAVE: NOP 0;

Use this form when slot count ≤ 3 and addresses are constant. The code scales linearly with slots; the ANY-pointer form scales with constant size.

Variant B — Ring buffer for arbitrary history

For an unlimited or large history, maintain a slot counter and BLKMOV into the current slot. Increment modulo the buffer length.

L     "DB_Guide_Lubric_Dates".SlotCount;
L     10;                     // ring length (Slot_01..Slot_10)
MOD;
INC   1;                      // 1..10
T     #NewIndex;

L     #NewIndex;
L     8;
*I;
T     #DST_Offset_W;

LAR1  P##DB_ANY;
L     B#16#10; T B [AR1, P#0.0];
L     B#16#08; T B [AR1, P#1.0];
L     B#16#00; T B [AR1, P#2.0];
L     B#16#08; T B [AR1, P#3.0];
L     W#16#000A; T W [AR1, P#4.0];  // DB 10 (DB_Guide_Lubric_Dates)
L     B#16#84; T B [AR1, P#6.0];
L     B#16#00; T B [AR1, P#7.0];
L     #DST_Offset_W; T W [AR1, P#8.0];

CALL  "BLKMOV" (
  SRCBLK  := "DB_Guide_Lubric_Dates".Actual_date_time,
  RET_VAL := #Aux1,
  DSTBLK  := #DB_ANY);

L     #NewIndex;
T     "DB_Guide_Lubric_Dates".SlotCount;

For a 200-slot ring buffer, declare Slot_001..Slot_200 of type DATE_AND_TIME (1600 bytes). Mark the DB as Retain in STEP 7 → DB properties → Retain so the log survives a CPU restart.

Variant C — SCL implementation with TYPED_ANY

STEP 7 SCL (Structured Control Language) lets you build the ANY pointer in two lines using the POINTER / ANY attributes of a structured tag.

FUNCTION_BLOCK FB 110
VAR_TEMP
  retcode : INT;
  idx     : INT;
END_VAR
BEGIN
  idx := "Z20";                                // 0 or 1
  "DB_Guide_Lubric_Dates".SlotCount := ("DB_Guide_Lubric_Dates".SlotCount MOD 10) + 1;
  // The AT view exposes the slot as a DATE_AND_TIME overlaid on bytes 0..7
  retcode := BLKMOV(
    SRCBLK := "DB_Guide_Lubric_Dates".Actual_date_time,
    DSTBLK := "DB_Guide_Lubric_Dates".Slots["DB_Guide_Lubric_Dates".SlotCount]);
END_FUNCTION_BLOCK

The SCL BLKMOV call accepts array elements directly because the SCL compiler constructs the ANY descriptor internally. The AT-overlay trick ("DB_Guide_Lubric_Dates".Slots[x] is a DATE_AND_TIME) keeps the destination typed, which avoids the area-code and length bugs common in hand-assembled STL ANYs.

Verification and Commissioning

  1. Watch the live field. Open DB_Guide_Lubric_Dates in STEP 7 → Monitor/Modify and confirm Actual_date_time increments every second after SFC 1 READ_CLK runs in OB 1. The fields must change in the order year→month→day→hour→minute→second.
  2. Force the trigger. In Monitor/Modify, set KM31B70 = TRUE for one OB 1 cycle. The static edge bit EdgeMem must go TRUE for exactly one cycle. If it stays latched, you used a TEMP instead of a VAR/static.
  3. Inspect the destination slot. With Z20 = 0, the bytes at offset 42..49 in DB 50 must change within the same cycle. With Z20 = 1, the bytes at offset 50..57 must change. Both windows can be viewed in the online DB monitor.
  4. Check RET_VAL. Open a VAT and monitor #Aux1 in the calling FB. The value must be W#16#0000. Anything in the W#16#80xx range indicates a misassembled ANY (length, area code, DB number, or overflow).
  5. Repeat for 100 events. Trigger the event 100 times. Confirm 100 unique timestamps are stored. If entries are duplicated, the rising-edge detector is malfunctioning; if entries are missing, the BLKMOV RET_VAL is non-zero and being silently ignored.
  6. Check the diagnostic buffer. From STEP 7 → CPU → Diagnostic Buffer, confirm no OB 1 stop and no "Area length error writing" or "Area length error reading" entries. These messages are logged by the CPU when an ANY descriptor points outside valid memory.
  7. Test the cold-restart path. Stop the CPU, power-cycle, and bring it back to RUN. If the destination DB is not marked Retain, all timestamp slots will reset to DT#1990-01-01-00:00:00.000. Configure the retain area in STEP 7 → HW Config → CPU → Properties → Retain Memory to cover the DB length.

Troubleshooting Matrix

Symptom Likely cause Fix
RET_VAL = W#16#8092 Source ANY length is 0 or syntax ID wrong Verify byte 3 of SRCBLK = B#16#08 and byte 0 = B#16#10. The syntax ID for a complete DATE_AND_TIME BLKMOV is always 0x10.
RET_VAL = W#16#80A1 Destination ANY offset + 8 exceeds DB length Check that slot * 8 + 42 + 8 fits inside the DB. Verify *I did not overflow a 16-bit INT.
RET_VAL = W#16#80B1 Source and destination overlap inside the same DB Ensure DST_Offset_W ≥ 8 so the destination does not overlap Actual_date_time at offset 0.
Destination never changes FP edge never re-arms or trigger is sticky Confirm FP uses a static VAR (not TEMP) and that the trigger contactor is wired through a hardware debounce if mechanical.
Wrong slot written Z20 multiplied by wrong factor DATE_AND_TIME length is 8 bytes, not 4 or 10. Confirm the second L operand is exactly 8.
SF LED lit, "Area length error OB 1" DST_Offset_W points outside the destination DB or into load memory Recompute DST_Offset_W; double-check that the destination DB number at bytes 4/5 is correct.
Timestamps identical across writes SFC 1 READ_CLK is not called, or called only in OB 100 (warm restart) Place the SFC 1 call in OB 1 or in a time-of-day OB 10, not in restart OBs.
DT field shows 00.00.0001 after restart DB is not retentive and the clock was reset to 1990-01-01 Mark the DB as Retain in DB properties; ensure the CPU retain area covers its length.
BLKMOV runs every cycle, no edge filter FP was omitted or placed after the BLKMOV Place the FP network before the BLKMOV network; gate the BLKMOV call with a JCN to END1.
Online monitoring shows garbage ASCII in DT The display column is BYTE and not DATE_AND_TIME In the DB Monitor/Modify view, right-click → Display Format → DATE_AND_TIME.

Performance, Cycle Time, and Integration

Cycle time impact

SFC 20 BLKMOV on a CPU 315-2 PN/DP executes in ~25–40 µs for an 8-byte move, well inside the typical 1 ms OB 1 budget. The 10-instruction ANY assembly adds ~10 µs. For a 200-slot ring buffer the BLKMOV cycle impact remains identical because the move length is always 8 bytes. SFC 1 READ_CLK runs in ~30 µs on the same CPU; called once per OB 1 cycle it adds 0.003% of the 10 ms minimum cycle.

If multiple timestamps must be captured in a single cycle (e.g., eight outputs energize simultaneously), use SFC 81 UBLKMOV (interrupt-safe variant) or chain the BLKMOV calls in a single OB 1 network. Do not parallelize the BLKMOVs in multiple cyclic OBs — that re-orders the writes and can corrupt the ring index.

Wiring and electrical note

The timestamp logic is purely software, but the trigger source KM31B70 must reflect a real contactor auxiliary. On 24 V DC inputs, use a 1 A slow-blow fuse per 8 inputs; the SIMATIC S7-300 SM 321 manual recommends 2 A max per group. For AC 230 V inputs on SM 321-1FH00, fit RC snubbers (100 Ω + 0.1 µF X2) across the contactor coil to keep the input free of capacitive cross-talk that can mimic a rising edge. Refer to the SIMATIC S7-300 module data manual for the exact derating curves.

WinCC and HMI integration

To display the timestamps on a WinCC Flexible 2008 SP5 or WinCC Professional V13 SP1 panel, expose the DATE_AND_TIME variable as a Date/Time field. Use the format string yyyy-MM-dd HH:mm:ss.fff so the weekday and milliseconds survive the OPC UA / S7-connection round-trip. Set the field's Update property to 500 ms; the panel does not need per-second updates and a 2 Hz cycle is sufficient for operator visibility.

Endianness note: WinCC Flexible reads the DT as 8 BCD bytes in the order year→ms. The PLC stores them the same way. No byte swap is required when using a S7-connection or an OPC DA server with the SIMATIC NET PN driver. With a third-party OPC UA gateway, enable "byte order" = little-endian for the first two bytes only if the gateway exposes the DT as a 64-bit value.

FAQ

What is the byte length of DATE_AND_TIME for SFC 20 BLKMOV?

Always 8 bytes. The fields are year/century, year, month, day, hour, minute, second, and the combined millisecond/weekday byte. Set ANY bytes 2/3 to W#16#0008.

Why does RET_VAL report W#16#80A1 even though the DB is large enough?

Either the offset (bytes 8/9 of the ANY) plus the length overflows the DB, or the area-cross bit at byte 6 bit 7 is not set when the offset exceeds 8191 bytes on a CPU 315. Use B#16#84 (not B#16#04) at byte 6 of the destination ANY when the offset is high.

Can I copy directly from the system clock to the destination without the intermediate Actual_date_time field?

Yes. Use SFC 1 READ_CLK and pass its CDT output as the source of an SFC 20 BLKMOV. The CDT parameter is itself a DATE_AND_TIME; BLKMOV will copy the live 8 bytes to the destination without needing a separate "current" field.

What is the difference between area-cross (0x84) and plain DB (0x04) in ANY byte 6?

Area-cross is a CPU capability to address any byte inside a DB with a single instruction. For SFC 20 BLKMOV, set bit 7 of byte 6 only when the offset is large. For offsets < 8192 bytes on a CPU 315, both codes work identically.

How do I retain the timestamp log across a CPU restart?

Mark the destination DB as Retain in STEP 7 → DB properties → Retain. Configure the Number of memory bytes parameter to cover the entire DB. On a CPU 315-2 PN/DP, the maximum retentive area is 16384 bytes; on a CPU 416-3 it is 524288 bytes.

Back to blog