Resolving STEP 7 DT Output Errors in Siemens FC Blocks

David Krause14 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 Definition: STEP 7 Rejects DATE_AND_TIME as an FC Output

In STEP 7 V5.x (SIMATIC Manager) targeting S7-300 and S7-400 CPUs, declaring an OUTPUT parameter of type DATE_AND_TIME (DT) on a Function (FC) block produces a compile-time error. The compiler refuses to publish the block:

Error in block FCxx: Data type 'DATE_AND_TIME' is not permitted for output parameters of a function.

The same restriction applies to every other complex type in classic STEP 7: STRING, ARRAY, STRUCT, UDT, ANY, POINTER, and even the newer DTL introduced with S7-1500. The restriction originates from the FC interface model: an FC has no instance data block (DB), so a complex output cannot be initialized or written back to a stable memory area at the END of the call. The FC interface explicitly distinguishes:

  • INPUT — read-only on entry, copied from caller to local stack
  • OUTPUT — written back to caller at END (must fit into the FC's temp image)
  • IN_OUT — passed by reference (pointer to caller's variable)
  • TEMP — local, not initialized, not visible to caller
  • RETURN — a special OUTPUT slot used for the function's return value (must be elementary in classic FCs)

Complex types require a pointer-to-data semantic because they exceed the 32-bit word that the FC output mechanism can move. Only IN_OUT and TEMP can carry complex types on the temporary local stack.

2. Root Cause: How the FC Output Mechanism Works

When an FC is called, the CPU performs three steps:

  1. Copies the actual parameters into the FC's local (TEMP) area, sized to fit the declared interface plus any compiler-managed temporaries.
  2. Executes the FC code. Outputs are written into the local area.
  3. On END of the FC, the runtime copies the OUTPUT slots back to the caller's variable area. This copy is performed as a fixed-width transfer that is typed at compile time.

For elementary 32-bit types (BOOL, INT, DINT, REAL, DWORD, TIME, DATE, CHAR, BYTE) the runtime performs a direct word copy. For 64-bit elementary types (LREAL, LWORD, LINT) the same mechanism applies across two words. For complex types the runtime does not have the type-length metadata required to perform a generic copy, so the compiler rejects the OUT declaration outright. The only safe place to expose a complex value across an FC boundary is via IN_OUT (which is implemented internally as a pointer), or via TEMP followed by an explicit copy instruction inside the FC.

3. The DATE_AND_TIME (DT) Format in S7-300/400

DATE_AND_TIME occupies 8 contiguous bytes in BCD (binary-coded decimal) format. The internal layout, used by SFC1 (READ_CLK), SFC0 (SET_CLK), and the CPU real-time clock, is:

Byte Bit 7..4 / High Nibble Bit 3..0 / Low Nibble Encoding
0 Reserved (0) Padding, ignored on read
1 YY (BCD) Year: 90..99 = 1990..1999; 00..89 = 2000..2089
2 MO (BCD) Month: 01..12
3 DD (BCD) Day: 01..31
4 HH (BCD) Hour: 00..23
5 MM (BCD) Minute: 00..59
6 SS (BCD) Second: 00..59
7 MS (BCD) — ms hundreds/tens (00..99) DOW (BCD) — day of week 1..7 1=Sun, 2=Mon, ..., 7=Sat; 0=unspecified
Day-of-week encoding: Bits 0..3 of byte 7 encode the day of the week. 1 = Sunday, 2 = Monday, ..., 7 = Saturday. A value of 0 indicates the day-of-week is unspecified. The millisecond field is split across the upper nibble of byte 6 (hundreds of ms) and the upper nibble of byte 7 (tens of ms). Resolution is 10 ms; the unit digit of milliseconds is always 0 and is not stored.

The 8-byte DT type aligns to a double-word boundary in the FC local stack, so it can be moved in two DWORD transfers or as a single block copy of length 8. This is the foundation of the DWORD-split and BLKMOV solutions.

4. Solution 1 — Change OUTPUT to IN_OUT

The simplest fix is to change the parameter interface from:

FUNCTION FC100 : VOID
VAR_INPUT
  // ...
END_VAR
VAR_OUTPUT
  outDT : DATE_AND_TIME;   // <-- compiler error
END_VAR

to:

FUNCTION FC100 : VOID
VAR_INPUT
  // ...
END_VAR
VAR_IN_OUT
  ioDT : DATE_AND_TIME;    // legal: pointer-based
END_VAR

Semantically IN_OUT is implemented as a pointer to the caller's memory. The FC reads and writes the variable in place. There is no copy on entry or exit. Restrictions to observe:

  • The caller MUST pass a valid DT variable (cannot be passed an uninitialized temp or literal).
  • The caller sees the result only after the FC executes (same as OUT, but no implicit copy step).
  • EN/ENO behaviour: ENO reflects the state of BR after the last statement that modifies BR. Add explicit SET / CLR or RLO logic to control ENO.

This is the recommended approach when the FC is a setter that mutates an existing DT in place (for example, "add N seconds to the supplied DT").

5. Solution 2 — Split DT into Two DWORD Outputs (Legacy Pattern)

If the application contract requires a true OUT parameter (caller provides an empty destination and receives the value after the call), declare two DWORD outputs that hold the low and high halves of the 8-byte DT. The FC writes each half; the caller recombines them into a DT variable.

FUNCTION FC100 : VOID
VAR_INPUT
  // ...
END_VAR
VAR_OUTPUT
  outDT_lo : DWORD;   // bytes 0..3 of DATE_AND_TIME
  outDT_hi : DWORD;   // bytes 4..7 of DATE_AND_TIME
END_VAR
VAR_TEMP
  tmpDT : DATE_AND_TIME;
END_VAR
BEGIN
  // ... build tmpDT ...
  // Direct dword copy (two 32-bit transfers):
  outDT_lo := tmpDT.LOW_DWORD;   // pseudo — see STL below
  outDT_hi := tmpDT.HIGH_DWORD;
END_FUNCTION

Because STEP 7 does not expose LOW_DWORD / HIGH_DWORD directly on a DT variable in the FBD/LAD editor, use STL (or SCL) inside the FC to do the split:

// STL: extract low/high 32 bits of a DATE_AND_TIME local
L  LD [AR1,P#0.0]      // AR1 points to caller's OUT or temp DT
T  outDT_lo            // bytes 0..3 transferred
L  LD [AR1,P#4.0]
T  outDT_hi            // bytes 4..7 transferred
BE

And the inverse at the caller:

// Caller (STL) — reassemble DT from two DWORDs
CALL FC100
     outDT_lo := LW10
     outDT_hi := LW12
L     outDT_lo
T     LD 20
L     outDT_hi
T     LD 24
// DB or M area at DBX20 now holds a valid DATE_AND_TIME

This approach works on every S7-300/400 CPU and on S7-1200/1500 in compatibility mode. It is especially useful when the FC interface is part of an established library that downstream consumers cannot easily modify. The 8-byte round-trip is lossless because the BCD byte order inside each DWORD matches the FC's natural stack alignment.

6. Solution 3 — Indirect Addressing via Address Register AR1

The classic STEP 7 Help documents that complex types can be exposed through FC outputs if the FC uses an internal TEMP instance of the complex type and copies it to a destination whose address is computed at runtime. The mechanism uses AR1 as the address pointer.

FUNCTION FC100 : VOID
VAR_INPUT
  iDT      : DATE_AND_TIME;
END_VAR
VAR_TEMP
  YY       : BYTE;
  MO       : BYTE;
  DD       : BYTE;
  HH       : BYTE;
  MM       : BYTE;
  SS       : BYTE;
  MSxx     : BYTE;
  MSDOW    : BYTE;
  AR1_save : DINT;
END_VAR
VAR_OUTPUT
  // NOTE: cannot be DT as OUT.  Use RETURN slot instead.
  RET_VAL  : DATE_AND_TIME;   // legal: RETURN behaves as IN_OUT
END_VAR
BEGIN
  // 1. Save caller's AR1
  L     AR1;
  T     AR1_save;

  // 2. Point AR1 at source (iDT).  FC inputs are at fixed offsets
  //    from the DI/DB pointer established by the caller's CALL.
  L     P##iDT;
  LAR1  ;

  // 3. Copy 8 bytes (2 dwords) into temp area
  L     D [AR1,P#0.0];
  T     LD 0;            // bytes 0..3 -> local stack at L0.0
  L     D [AR1,P#4.0];
  T     LD 4;            // bytes 4..7 -> local stack at L4.0

  // 4. Manipulate as BCD byte fields
  L     LB 1;   T YY;    // year
  L     LB 2;   T MO;    // month
  // ... operate on LB 3..7 the same way ...

  // 5. Reassemble to RET_VAL (RETURN is legal for complex types
  //    because the runtime copies the full length back to caller)
  L     LD 0;
  T     D [AR1,P#0.0];   // AR1 still points to RET_VAL location
  L     LD 4;
  T     D [AR1,P#4.0];

  // 6. Restore AR1
  L     AR1_save;
  LAR1  ;
  SET   ;
  SAVE  ;                // ENO = TRUE
  BE    ;
END_FUNCTION

The key insight is that RETURN is the one slot in a classic FC that can hold a complex type, because the runtime treats the RETURN variable as a typed slot whose length is known at the END instruction. Declaring RET_VAL : DATE_AND_TIME at the FC level (rather than as an OUT) sidesteps the restriction. This pattern works for any complex type whose maximum length fits the CPU's local stack (8 bytes for DT, 32 bytes for typical STRINGs, etc.).

If a true "second DT output" is required in addition to RET_VAL, use IN_OUT or implement the FC as an FB with a STATIC variable (Section 8).

7. Solution 4 — Block Copy with SFC20 BLKMOV via ANY Pointer

For very large or variably-sized structures (STRUCT, ARRAY, UDT), SFC20 BLKMOV is the canonical tool. Build an ANY pointer that describes the source and destination, then call SFC20. The ANY pointer format for the S7ANY syntax ID is:

FUNCTION FC100 : VOID
VAR_IN_OUT
  ioDT : DATE_AND_TIME;
END_VAR
VAR_TEMP
  srcANY : ANY;
  dstANY : ANY;
  ret    : INT;
END_VAR
BEGIN
  // Build source ANY: type DT, length 8, area=L (local)
  srcANY.0  := B#16#10;   // syntax ID  = S7ANY
  srcANY.1  := B#16#02;   // transport size = BYTE
  srcANY.2  := B#16#00;   // length high
  srcANY.3  := B#16#08;   // length low = 8 bytes
  srcANY.4  := B#16#00;   // DB# high
  srcANY.5  := B#16#00;   // DB# low  (use L area for source)
  srcANY.6  := B#16#00;   // area code
  srcANY.7  := B#16#00;
  srcANY.8  := B#16#00;   // byte offset high
  srcANY.9  := B#16#00;   // byte offset low

  // ... fill dstANY for ioDT location ...

  CALL SFC20 (
    SRCBLK  := srcANY,
    RET_VAL := ret,
    DSTBLK  := dstANY);
END_FUNCTION

In practice, SFC20 is overkill for an 8-byte DT; prefer Solutions 1, 2, or 3 unless the structure is larger than 32 bytes or the source/destination area is in a different DB that must be addressed indirectly. The BLKMOV ANY-pointer mechanism is fully documented in the S7-300/400 System and Standard Functions Reference Manual.

8. Solution 5 — Convert the FC to an FB with STATIC DT

FBs (Function Blocks) have an instance DB that provides persistent storage between calls. A STATIC variable of type DATE_AND_TIME is perfectly legal in an FB and persists across scans. Convert the FC to an FB and the interface becomes:

FUNCTION_BLOCK FB100
VAR_INPUT
  iDT  : DATE_AND_TIME;
END_VAR
VAR_OUTPUT
  oDT  : DATE_AND_TIME;   // legal in FB
END_VAR
VAR
  statDT : DATE_AND_TIME; // instance DB persists between scans
END_VAR
BEGIN
  statDT := iDT;
  // ... operate on statDT ...
  oDT    := statDT;
END_FUNCTION_BLOCK

This is the recommended pattern for new code. Reserve FCs for pure functions (no state). Use FBs for any function that must remember information between calls or expose complex outputs. Note that converting FC to FB has wider implications: the call site must instantiate a DB (single, multi-instance, or system data block), and existing CALL instructions must be updated. The instance DB size is calculated by STEP 7 from the VAR section plus 36 bytes of FB header overhead.

9. Method Comparison

Method DT on Output? Persistent State? Memory Cost Caller Complexity Recommended Use
Change to IN_OUT (Sol. 1) Yes (by reference) No 8 B in local stack Low — pass an existing DT Set / transform existing DT
DWORD split (Sol. 2) Yes (as 2 DWORD) No 16 B caller + 8 B local Medium — caller reassembles Legacy interfaces, strict OUT required
RETURN : DT + AR1 (Sol. 3) Yes (as return value) No 8 B local stack Low — assign to DT at call site Pure functions whose result IS a DT
SFC20 BLKMOV (Sol. 4) Yes No ~40 B stack + ANY High — pointer management Large structs / cross-DB copies
Convert to FB (Sol. 5) Yes (output) Yes (instance DB) 8 B per instance + DB Low — single call, persistent Stateful DT processing, edge-triggered logic

10. TIA Portal Alternative — DTL (12-Byte Date_Time_Long)

In TIA Portal V13+ targeting S7-1200/S7-1500, the legacy 8-byte DT type is replaced by DTL (IEC 61131-3 DATE_AND_TIME_LONG), which occupies 12 bytes in pure binary (no BCD):

Field Bytes Type Range
YEAR 0..1 UINT (LE) 1970..2554
MONTH 2 USINT 1..12
DAY 3 USINT 1..31
WEEKDAY 4 USINT 1=Sunday..7=Saturday
HOUR 5 USINT 0..23
MINUTE 6 USINT 0..59
SECOND 7 USINT 0..59
NANOSECOND 8..11 UDINT (LE) 0..999,999,999

In TIA Portal FC outputs, the same complex-type restriction does NOT exist for DTL: the S7-1200/1500 FC mechanism handles 12-byte complex outputs natively because the runtime carries typed metadata for each OUTPUT. The classic DT restriction was lifted in TIA Portal for S7-1200/1500 targets but still applies to S7-300/400 firmware compiled in TIA Portal (compatibility mode). If the project must remain on classic STEP 7, keep DT and use one of Solutions 1-5. If the project is being ported to S7-1500, migrate to DTL and the problem disappears.

The TIA Portal conversion tool (STEP 7 → TIA Portal migration) automatically maps DT to DTL and adds BCD-to-binary conversion code in a wrapper block. Manually, byte 0 of DT becomes bytes 8..9 of DTL after applying: YEAR_DTL = DT_BYTE1 + 1900 (with the 1990-1999 vs 2000-2089 split applied automatically by the conversion wizard).

11. Verification and Commissioning

After implementing any of the solutions, verify with the following checklist:

  1. Compile the FC: no warnings in the "Compile" output window (F7 / right-click block > Compile). Any "data type not allowed" message indicates the chosen solution was not fully implemented.
  2. Download to the CPU: in the Online > Accessible Nodes view, monitor the FC in single-step (CTRL+F9 in SIMATIC Manager) and confirm BR / ENO transition as expected.
  3. Monitor the DT value in VAT (Variable Table). Create a VAT named "VAT_DT_Check", add the DT variable as a DT row, force-format the row to "DEC" to see BCD fields, then to "HEX" to verify byte-level integrity. Confirm year encoding matches the table in Section 3.
  4. Round-trip test: write a known DT (e.g. 2024-06-15 12:34:56.780, Saturday → DOW=7) into the FC's input, observe the output, and compare byte-for-byte. Expected hex layout: 00-24-06-15-12-34-56-78 with byte 7 low nibble = 7.
  5. Day-of-week check: load the DT, extract byte 7 low nibble, and compare to your calendar's day-of-week. If incorrect, the source DT was constructed manually without DOW populated; that is normal for user-constructed values but SFC1 (READ_CLK) always populates DOW.
  6. Cross-CPU test: if the FC is reused on an S7-400, repeat the test — DT layout is identical but stack sizes differ; verify local temp area does not exceed the CPU's max local stack.
  7. Edge cases: test month rollover (Dec 31 23:59:59 + 1 s), leap-day handling (Feb 29 2024 00:00:00), and DST transitions if the plant operates across time zones. SFC1/SFC0 do NOT apply DST automatically; the application must handle the offset.
Watch-out on S7-300 with large FCs: the maximum local stack per priority class is 256 bytes for older S7-300 CPUs (CPU 312, 314) and 1024 bytes for newer ones (CPU 315-2 DP, 317, 319). Two DWORD outputs plus 8-byte temp DT plus typical intermediate temporaries can push close to this limit. If "OB1 stack overflow" appears at runtime, the FC is too large; split into multiple FCs. Refer to the S7-300 CPU 31x Technical Data manual for exact local stack figures per CPU.

12. Frequently Asked Questions

Why does STEP 7 allow DATE_AND_TIME as IN_OUT but not as OUT?

IN_OUT is passed by pointer at runtime — the FC reads and writes the caller's memory in place. OUT is implemented as a copy-back mechanism whose length is fixed at compile time; complex types lack the fixed-length metadata required. RET_VAL is a special case that does support complex types because the runtime copies the full RETURN slot width on END.

Can I declare a DTL output on an S7-1500 FC and avoid this issue entirely?

Yes. TIA Portal V13+ on S7-1200/1500 supports DTL (12-byte) and other complex types as FC outputs natively. The legacy 8-byte DT restriction is specific to S7-300/400 classic STEP 7. Migrate to S7-1500 / S7-1200 or convert DT to DTL when porting legacy code.

Is byte 0 of DATE_AND_TIME always zero?

In S7-300/400 firmware versions prior to V3.x, byte 0 was reserved and the CPU ignored its value on read. Newer firmware (S7-318, S7-400, all S7-1500 compatibility mode) preserves byte 0 across SFC0/SFC1 round-trips. When constructing a DT manually, set byte 0 to B#16#00 to remain forward-compatible.

What is the largest millisecond value storable in DT?

The DT millisecond field is 3-digit BCD in the upper 12 bits of bytes 6 and 7, so the maximum representable value is 999 ms. Resolution is 10 ms — the unit digit is always zero. For higher resolution, use DTL on S7-1500, which stores nanoseconds (0..999,999,999).

Will splitting DT into two DWORDs change the SFC0 / SFC1 round-trip behaviour?

No. SFC0 (SET_CLK) and SFC1 (READ_CLK) accept and return the full 8-byte DT as a single block. The internal layout is preserved end-to-end. Splitting the DT into two DWORD outputs only affects how the FC transfers data to the caller; reassembly at the call site restores the original 8 bytes.

Back to blog