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:
- Copies the actual parameters into the FC's local (TEMP) area, sized to fit the declared interface plus any compiler-managed temporaries.
- Executes the FC code. Outputs are written into the local area.
- 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 |
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:
- 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.
- 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.
- 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.
-
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-78with byte 7 low nibble = 7. - 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.
- 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.
- 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.
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.