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_Datessized for the timestamp slots — 8 bytes per slot × number of slots, plus the liveActual_date_timefield 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
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 |
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
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
-
Watch the live field. Open
DB_Guide_Lubric_Datesin STEP 7 → Monitor/Modify and confirmActual_date_timeincrements every second after SFC 1 READ_CLK runs in OB 1. The fields must change in the order year→month→day→hour→minute→second. -
Force the trigger. In Monitor/Modify, set
KM31B70= TRUE for one OB 1 cycle. The static edge bitEdgeMemmust go TRUE for exactly one cycle. If it stays latched, you used a TEMP instead of a VAR/static. -
Inspect the destination slot. With
Z20 = 0, the bytes at offset 42..49 in DB 50 must change within the same cycle. WithZ20 = 1, the bytes at offset 50..57 must change. Both windows can be viewed in the online DB monitor. -
Check RET_VAL. Open a VAT and monitor
#Aux1in 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). - 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.
- 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.
- 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.
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.