Creating Date and Time Variables in TIA Portal V15 for S7-1200

David Krause14 min read
SiemensTIA PortalTutorial / 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

Problem Overview and Engineering Context

Engineers using a Siemens SIMATIC S7-1200 CPU 1212C (order number 6ES7 212-1AE40-0XB0) programmed in TIA Portal V15 frequently need to combine three on-time counters (hours, minutes, seconds) and the current calendar date into a single human-readable timestamp string. The PLC tracks the uptime of a field device and must hand the value off to a Python-based SCADA bridge, historian, or analytics application. The problem is technically constrained: (1) the S7-1200 firmware must support the chosen date/time data type, (2) the format must be consumable by a Python parser, and (3) ASCII conversion should not consume PLC scan time when a binary transfer is possible.

This tutorial covers three production-grade approaches — SCL string composition with VAL_STRG and CONCAT, Ladder with S_CONV plus manual padding, and the recommended DTL binary structure with Python-side parsing. Each method is benchmarked on memory footprint, OB1 execution cost, and downstream parser simplicity.

Hardware used in this reference build: S7-1200 CPU 1212C DC/DC/DC, 6ES7 212-1AE40-0XB0, firmware V4.2.3. The DTL data type, the RD_SYS_T instruction, and the extended CONCAT/VAL_STRG instruction set all require S7-1200 firmware V4.0 or later. TIA Portal V15.1 (released March 2018) is the minimum engineering environment. For older firmware (V3.x), only the legacy DATE_AND_TIME (8-byte) type is available; the code in this article targets V4.x behavior.

Prerequisites

  1. Installed TIA Portal V15 or V15.1 with the S7-1200 HSP (Hardware Support Package) for firmware V4.2.
  2. S7-1200 CPU 1212C, 6ES7 212-1AE40-0XB0, firmware V4.2 or later (check in the device's online > diagnostics > module information).
  3. A configured PROFINET or PROFIBUS interface to the Python host. The Python host in this example uses the snap7 library to exchange data via S7 communication (PUT/GET).
  4. A populated PLC tag table with the source counters. Recommended types: UINT for hour/minute/second (range 0-65535), not SInt (range -128 to +127). The sign bit on a runtime counter is wasted memory and a future rollover bug waiting to happen.
  5. For SCL paths: SCL source file or an SCL-enabled code block (FB/FC). SCL is part of the TIA Portal basic installation.

Data Type Selection Matrix

Data type Length (bytes) Range When to use
SInt 1 -128 to +127 Signed small values (avoid for time counters)
USInt 1 0 to 255 Minutes, seconds, hours up to 255
UINT 2 0 to 65535 Recommended for uptime counters
String 2 + n (max 254) ASCII string HMI display, text export
WString 4 + n (max 16382) Unicode (UTF-16) Multi-byte or non-ASCII characters
DTL 12 1970-01-01 to 2554-12-31 Date+time with sub-second precision; ideal for binary transport to Python
DATE 2 D#1990-01-01 to D#2168-12-31 Calendar day only (days since 1990-01-01)
TOD (Time_Of_Day) 4 TOD#00:00:00.000 to TOD#23:59:59.999 Time-of-day without calendar date
Date (VB .NET / .NET) 8 0001-01-01 to 9999-12-31 Reference: how the consumer system represents the same value (Microsoft Learn: Date Data Type)

Method 1 — SCL with VAL_STRG and CONCAT (string composition)

SCL (Structured Control Language) gives the cleanest control over string formatting. The two key instructions are:

  • VAL_STRG(SrcVal, Format, DecPoint, UseSep, Sep, Result) — converts a numeric value to a string with a configurable minimum width and decimal separator.
  • CONCAT(Str1, Str2) — concatenates two strings. Chain multiple calls to join more than two.

Block layout (FC, call from OB1 on each scan):

FUNCTION "FC_BuildTimestamp" : Void
VAR
    hrValue    : UINT;        // input from running counter, 0..65535
    minValue   : UINT;        // 0..59
    secValue   : UINT;        // 0..59
    tmpHr      : String[4];   // padded "0000".."9999"
    tmpMin     : String[2];
    tmpSec     : String[2];
    timeStr    : String[12];  // "HH:MM:SS"
    col1, col2 : String[14];  // intermediate concat buffers
END_VAR

BEGIN
    // 1) Convert with explicit width so leading zeros are written.
    // Format = digit count, DecPoint = 0, UseSep = FALSE.
    VAL_STRG(IN := hrValue,  FORMAT := 4, DECIMAL := 0, USEPOINT := FALSE, Result => tmpHr);
    VAL_STRG(IN := minValue, FORMAT := 2, DECIMAL := 0, USEPOINT := FALSE, Result => tmpMin);
    VAL_STRG(IN := secValue, FORMAT := 2, DECIMAL := 0, USEPOINT := FALSE, Result => tmpSec);

    // 2) Build "HH:MM:SS" by chaining CONCAT calls.
    col1     := CONCAT(tmpHr, ':');                 // "HHHH:"
    col2     := CONCAT(col1,  tmpMin);              // "HHHH:MM"
    timeStr  := CONCAT(CONCAT(col2, ':'), tmpSec);  // "HHHH:MM:SS"

    // 3) Move the final string into a globally visible tag for HMI/Python.
    "dbRuntime".timeAscii := timeStr;
END_FUNCTION

Parameters of VAL_STRG on S7-1200 / S7-1500:

Parameter Type Meaning
IN Input Numeric value (INT, DINT, REAL, …)
FORMAT WORD/INT Minimum field width in characters (not interpreted as zero-pad count, but enforced as right-aligned width). For zero-pad behavior, see notes below.
DECIMAL INT Number of digits after the decimal point
USEPOINT BOOL TRUE = '.' decimal point, FALSE = ',' decimal point
Result Output (String) Output string
Zero-padding caveat: VAL_STRG in S7-1200/S7-1500 right-aligns the value but does not automatically pad with leading zeros. The SCL workaround is to prefix the string with a literal '0' when the source value is below the format width, e.g. IF minValue < 10 THEN tmpMin := CONCAT('0', tmpMin); END_IF; or to use the DTL approach in Method 3 which does not need padding at all.

Method 2 — Ladder with S_CONV and manual padding

For teams that cannot use SCL, the same result is achievable in Ladder using S_CONV (scalar conversion), MOVE_BLK, and CONCAT. The catch is that S_CONV from UINT to STRING produces an un-padded result: hour 9 becomes the single character "9" rather than "09". You must insert the colon and the leading zero yourself.

Network 1 — convert each value to STRING

|  hrValue  |---[ S_CONV (UINT -> STRING) ]--->| tmpHr   |
|  minValue |---[ S_CONV (UINT -> STRING) ]--->| tmpMin  |
|  secValue |---[ S_CONV (UINT -> STRING) ]--->| tmpSec  |

Network 2 — pad minute and second when < 10

| minValue < 10 |--[ MOVE_BLK_VARIANT src:='0' dst:=tmpMin[1] len:=1 ]-->X
|                |--[ S_CONV minValue -> tmpMin[2] ]---|

Network 3 — assemble the final string with CONCAT in four steps

| tmpHr   |---[ CONCAT IN1: tmpHr   IN2: ':'   ]--->| col1    |
| col1    |---[ CONCAT IN1: col1    IN2: tmpMin ]--->| col2    |
| col2    |---[ CONCAT IN1: col2    IN2: ':'   ]--->| col3    |
| col3    |---[ CONCAT IN1: col3    IN2: tmpSec ]--->| timeStr |

Common Ladder failure modes

Symptom Cause Fix
String shows "9:5:3" with no zeros S_CONV does not pad Insert the leading-zero comparison network above
CONCAT errors with ENO=0 Result buffer shorter than combined length Increase the STRING length; default is 254, but intermediate buffers must be declared with enough capacity
Garbage in the last character Old characters in the string header (current length byte) not reset Reset the LEN field of the result with a MOVE of 0 to the second byte of the STRING header before re-using it

Method 3 — Recommended: DTL data type with binary transport to Python

The DTL (Date and Time Long) type was introduced for S7-1200 firmware V4.0 and S7-1500 from launch. It is a 12-byte structure that carries the full date, time, and nanosecond resolution without any ASCII overhead.

DTL structure on S7-1200/S7-1500 (little-endian, 12 bytes total)

Byte offset Field Type Range / meaning
0-1 YEAR UINT (2 bytes) 1970 to 2554
2 MONTH USINT 1 to 12
3 DAY USINT 1 to 31
4 WEEKDAY USINT 1 (Sunday) to 7 (Saturday)
5 HOUR USINT 0 to 23
6 MINUTE USINT 0 to 59
7 SECOND USINT 0 to 59
8-11 NANOSECOND UDINT (4 bytes) 0 to 999_999_999 (sub-second precision)

Step 1 — declare the tag in the data block

DATA_BLOCK "dbRuntime"
  STRUCT
      hrCounter  : UINT;      // uptime hours (0..65535)
      minCounter : UINT;      // uptime minutes (0..59)
      secCounter : UINT;      // uptime seconds (0..59)
      runTime    : DTL;       // 12 bytes, populated by FC_BuildTimestamp
      sysTime    : DTL;       // 12 bytes, populated by RD_SYS_T
  END_STRUCT;
END_DATA_BLOCK

Step 2 — populate the runtime DTL from the three counters (SCL)

FUNCTION "FC_BuildRuntimeDTL" : Void
VAR_TEMP
    tmpDTL : DTL;
END_VAR

BEGIN
    // read the PLC's local time once so weekday and date are correct
    RD_SYS_T(RET_VAL := #errTime, OUT := #tmpDTL);

    // overwrite the time-of-day fields with the uptime counters
    tmpDTL.HOUR   := INT_TO_USINT(#hrValue);
    tmpDTL.MINUTE := INT_TO_USINT(#minValue);
    tmpDTL.SECOND := INT_TO_USINT(#secValue);
    tmpDTL.NANOSECOND := 0;

    // publish
    "dbRuntime".runTime := tmpDTL;
END_FUNCTION

Step 3 — read the calendar date with RD_SYS_T

RD_SYS_T is in the Extended instructions > Date and time folder. It writes the current PLC time-of-day (driven by the internal hardware clock) into a DTL.

RD_SYS_T(RET_VAL := "dbRuntime".errSysTime,
         OUT      := "dbRuntime".sysTime);

Step 4 — combine uptime and date into one DTL

Both DTLs are concatenated by overriding the date fields of the system-time DTL with the year/month/day you want, leaving the time-of-day fields as the runtime values. In practice you typically do not need to merge them into a single DTL: hand both tags to Python and let the parser assemble a human-readable string.

Why DTL is the right choice: one DB tag = 12 bytes = one S7 read on the wire. The PLC does not spend a single microsecond on ASCII conversion. The Python side gets the raw bytes and formats the string at SCADA layer. CONCAT and S_CONV are CPU-bound on the S7-1200; on a 1212C with 75 KB of work memory, repeating those in a fast OB (e.g. OB35 cyclic interrupt) can chew 200-400 µs of scan time per block instance.

Python-Side Parsing with snap7

The snap7 library implements the S7 communication protocol natively. It pulls the 12 bytes from dbRuntime.runTime and the 12 bytes from dbRuntime.sysTime in a single read request per DTL.

import snap7
from snap7.util import get_dtl
from datetime import datetime

client = snap7.client.Client()
client.connect('192.168.0.10', 0, 1, 102)   # PLC IP, rack 0, slot 1, TCP port 102

# Read 24 bytes from DB 1 starting at byte 0
raw = client.db_read(1, 0, 24)

# Snap7 ships a DTL helper; if not available, unpack manually:
# YEAR (UINT, big-endian), MONTH, DAY, WEEKDAY, HOUR, MINUTE, SECOND,
# NANOSECOND (UDINT, big-endian). Format string = '> H B B B B B B I' (12 bytes)
import struct
run_unpacked   = struct.unpack('>HBBBBBB I', raw[0:12])
sys_unpacked   = struct.unpack('>HBBBBBB I', raw[12:24])

run_time = datetime(run_unpacked[0],            # YEAR
                    run_unpacked[1],            # MONTH
                    1,                          # placeholder day
                    run_unpacked[4],            # HOUR
                    run_unpacked[5],            # MINUTE
                    run_unpacked[6])            # SECOND

sys_date = datetime(sys_unpacked[0],            # YEAR
                    sys_unpacked[1],            # MONTH
                    sys_unpacked[2],            # DAY
                    sys_unpacked[4],            # HOUR
                    sys_unpacked[5],            # MINUTE
                    sys_unpacked[6])            # SECOND

# final stamp matches the requested "dd/mm/yyyy HH:MM:SS" shape
timestamp = sys_date.strftime('%d/%m/%Y') + ' ' + run_time.strftime('%H:%M:%S')
print(timestamp)   # >> 14/03/2025 09:35:42

The same datetime object is directly compatible with the .NET Date data type (8-byte floating point days-since-0001-01-01 in the lower layer) when the value is exchanged via OPC UA rather than raw S7 — useful if the Python host is a wrapper around a .NET industrial service.

Alternative: Format the String on the PLC and Ship the ASCII

For HMI display only, build the timestamp string in SCL and write it to a 32-byte STRING tag:

// "dd/mm/yyyy HH:MM:SS" = 19 characters
outStr.LEN := 19;
outStr[1]  := tmpDay[1];
outStr[2]  := tmpDay[2];
outStr[3]  := '/';
outStr[4]  := tmpMonth[1];
outStr[5]  := tmpMonth[2];
outStr[6]  := '/';
// ... and so on for the year, space, HH, :, MM, :, SS
// padding zeros for month < 10 / day < 10 is required

This approach is only recommended when there is no IT-side consumer and the value is human-readable on a Comfort Panel or KTP display. For OPC UA or S7 transport to Python, the DTL binary form is faster and safer.

Verification Steps

  1. Online watch table — open the DB, force hrValue = 9, minValue = 5, secValue = 3. Confirm the resulting STRING reads 09:05:03 in the SCL path, or the DTL fields read HOUR = 9, MINUTE = 5, SECOND = 3 in the DTL path.
  2. CPU diagnostic buffer — confirm no SF (system fault) entries. The DTL manipulation must not trigger an OB121 (programming error) when the source values are at the boundary (e.g. minute = 59 → increment hour).
  3. Watch RD_SYS_T update — place a cross-reference watch on dbRuntime.sysTime; the value must increment by one second per second against the system clock.
  4. Python round-trip — in the Python host, run a loop that calls client.db_read(1, 0, 24) every 1 s and assert that the parsed seconds field increments monotonically.
  5. Rollover test — force hour = 23, minute = 59, second = 59; pulse the block. Verify the DTL hour does not roll to 24 (the SCL is responsible for that if you are building a calendar date from runtime counters; for uptime counters, 9999:59:59 is a legitimate value).
  6. Endianness check — log the raw bytes returned by db_read. Year 2025 must appear as bytes 0x07 0xE9 (big-endian) — the S7 is little-endian on the wire, but snap7's get_dtl and your struct.unpack format string must use the matching '>' or '<' prefix.

Troubleshooting Matrix

Observed fault Likely root cause Fix
SF LED on, OB121 (programming error) DTL field assigned an out-of-range value (e.g. MONTH = 13) Clamp the input with a LIMIT instruction or IF check before assignment
String truncates at 254 characters Standard STRING max length exceeded by chained CONCAT Use WSTRING (max 16,382) or split the output into two tags
CONCAT ENO = FALSE Result buffer too small for combined input Declare the output STRING with sufficient declared length (default 254); the actual length byte is independent
RD_SYS_T returns 0x0001 in RET_VAL Real-time clock not set or battery low Online > diagnostics > set time, or use WR_SYS_T to write the time from an external source (e.g. NTP, GPS)
Python parser shows year 1970 or 1900 Endian mismatch in struct.unpack Try swapping '>' to '<' and vice versa; print raw bytes for confirmation
Hour/minute counters overflow SInt boundary Counter declared as SInt (max 127) Change to UINT in the data block; recompile and reload
VAL_STRG result string has trailing space FORMAT is interpreted as minimum width, right-aligned Use the IF-check / leading-zero padding pattern or switch to DTL
DIAG LED on PLC, DTL data length wrong DB downloaded but not initialized Perform an online > reset to factory settings, or use Initial values in the DB properties

Performance and Memory Notes

  • String composition with CONCAT allocates a new STRING every call. On the S7-1200 1212C, 3 × CONCAT per scan in OB1 is negligible; in OB35 at 100 ms cycle with 4 tags, reserve ~500 µs of CPU time.
  • DTL manipulation is essentially pointer-based — 12 bytes copied, no heap. It is the cheapest approach for a high-frequency OB.
  • String tag declared length includes the 2-byte header (max length word + current length word). A STRING[20] occupies 22 bytes of work memory and 22 bytes of load memory.
  • WSTRING costs 4 + n bytes (UTF-16) and is the only correct type for characters outside the Latin-1 set.

Field-Commissioning Checklist

  • Confirm firmware version of the S7-1200 (online > module information) is V4.0+ for DTL or V3.x-compatible legacy code only.
  • Place RD_SYS_T in OB1, not in a startup OB, so the value is refreshed each cycle.
  • If the plant has more than one CPU, give each PLC a unique DTL tag location in its DB to avoid snap7 byte misalignment on the Python side.
  • Use the PLC's battery-backed real-time clock for normal operation. For plants with NTP, drive WR_SYS_T from the network every 5 minutes to keep drift below 1 second.
  • Lock the data block against write access from the HMI to prevent operators from accidentally modifying the runtime counters.

FAQ

Can I concatenate the three time values into a single STRING in Ladder without SCL?

Yes. Use S_CONV to convert each UINT to STRING, then four sequential CONCAT blocks to assemble HH, the colon, MM, the second colon, and SS. Add a leading-zero MOVE of the literal '0' for any field less than 10. The DTL approach is recommended over this if a Python consumer exists.

Why does my S_CONV output show "9" instead of "09" for the minute field?

S_CONV from UINT to STRING does not pad with leading zeros — it produces the minimum digit count. Either prepend '0' manually with an IF/MOVE block, or use VAL_STRG in SCL and then add the leading zero explicitly because VAL_STRG also does not pad zeros automatically on S7-1200/S7-1500.

What is the difference between DTL and the older DATE_AND_TIME (DT) data type?

The legacy DT (8 bytes) is a BCD-encoded structure supported on S7-300/400 and S7-1200 firmware V3.x. DTL (12 bytes) is binary, supports nanosecond resolution, and is the recommended type on S7-1200 V4.x and S7-1500. Use DTL for new code.

How do I read the PLC clock and use it as the calendar date in the same DTL that holds my uptime counters?

Call RD_SYS_T to load the current date and time into a DTL, then overwrite the HOUR, MINUTE, and SECOND fields with the uptime counters. Leave YEAR, MONTH, and DAY untouched. The single 12-byte DTL now carries date + uptime.

What is the minimum TIA Portal and firmware combination that supports DTL on the S7-1200?

TIA Portal V13 SP1 with HSP for S7-1200 firmware V4.0 is the minimum. For the CPU 1212C with order number 6ES7 212-1AE40-0XB0, the matching firmware is V4.2.x. TIA Portal V15 (the version used in the source problem) fully supports DTL and the RD_SYS_T/WR_SYS_T/CONCAT/VAL_STRG instruction set.

Back to blog