Overview
Storing the PLC clock into a data block (DB) is a routine requirement on the SIMATIC S7-300/400 platform, particularly on F-CPUs such as the CPU 319F-3 PN/DP (6ES7318-3FC01-0AB0) where event time-stamping, audit trail logging, and batch reporting depend on a consistent, PLC-resident time reference. The field report covers two implementation paths on STEP 7 V5.6 + SP2:
- Use the combined
DATE_AND_TIME(DT) format with SFC1READ_CLKand extract date and time-of-day parts with the standard IEC conversion functions. - Read the 8-byte BCD clock buffer directly and convert each byte with
BCD_TO_INTto obtain usable integers in the DB.
This reference implements both methods, documents the byte-level structure of the BCD clock buffer, and adds commissioning checks for the 319F-3 hardware clock, SFC64 tick count, and the additional standard library blocks (DT_DATE, DT_TOD, AD_DT_TM, D_TOD_DT, DT_DAY) required to obtain local time.
Prerequisites
- STEP 7 V5.6 + SP2 (or later) with the Standard Library containing IEC Function Blocks and System Function Blocks.
- CPU 319F-3 PN/DP (or any S7-300 CPU; SFC0/SFC1/SFC64 are available in all 31x/31xF/31xC/31xT CPUs from firmware V2.0 onward).
- An instance DB or a project DB (DB1 minimum for project source approach) declared with the target time format.
- The additional standard functions
DT_DATE,DT_TOD,DT_DAY,AD_DT_TM, andD_TOD_DTfrom the Standard Library > IEC Function Blocks folder must be present in the project S7 program. Without them, the local-time conversion path returns a runtime error (OB121).
Clock Data Types on S7-300/400
STEP 7 V5.x provides three relevant time data types. The S7-1200/1500 DTL type referenced in the TIA Portal manual collection is not available on S7-300/400; on those platforms the equivalent is DATE_AND_TIME.
| Type | Length | Format | Range | Use case |
|---|---|---|---|---|
| DATE | 2 bytes (DINT days since 01.01.1990) | IEC date | 01.01.1990 to 31.12.2168 | Calendar arithmetic, days offset |
| TIME_OF_DAY (TOD) | 4 bytes (DINT ms since 00:00) | IEC time-of-day | 00:00:00.000 to 23:59:59.999 | Daily scheduling, time-of-day math |
| DATE_AND_TIME (DT) | 8 bytes BCD | BCD wall-clock | 01.01.1990 00:00:00.000 to 31.12.2089 23:59:59.999 | Hardware clock buffer from SFC1 |
BYTE layout of DATE_AND_TIME (8 bytes BCD)
| Byte offset | Content | Encoding | Example (14.03.2024 09:41:23.456 Thursday) |
|---|---|---|---|
| Byte 0 | Year (BCD) | 90..89h = 1990..2089 | 24h = 36 dec |
| Byte 1 | Month (BCD) | 01..12 | 03h |
| Byte 2 | Day (BCD) | 01..31 | 14h |
| Byte 3 | Hour (BCD) | 00..23 | 09h |
| Byte 4 | Minute (BCD) | 00..59 | 41h |
| Byte 5 | Seconds (BCD) | 00..59 | 23h |
| Byte 6 | Milliseconds (BCD, top nibble = 100s, low = 10s, low byte = units) | 000..999 | 45h / 60h (456 ms) |
| Byte 7 | Day of week + reserved | 1=Sunday .. 7=Saturday | 05h (Thursday) |
16#24 on a 319F-3 is the year 2024, not decimal 36. Reading the buffer as INT will produce wrong results until you run each byte through BCD_TO_INT (FC 93 in the Standard Library, or built-in BTI in older code).System Functions for Clock Access
| SFC | Name | Function | Input | Output | Available on |
|---|---|---|---|---|---|
| SFC0 | SET_CLK | Set hardware clock (and 1..3 slaves) | PDT (pointer to DATE_AND_TIME), SET (BOOL) | RET_VAL (INT) | S7-300/400, WinAC |
| SFC1 | READ_CLK | Read hardware clock | RET_VAL (INT) | PDT (pointer to DATE_AND_TIME) | S7-300/400, WinAC |
| SFC64 | TIME_TCK | Read tick counter (10 ns ticks or 1 ms ticks depending on CPU) | RET_VAL | OUT (TIME, 32-bit) | S7-300/400 |
For S7-300 CPUs the tick counter of SFC64 advances in 1 ms increments (TIME format, 32-bit). The value rolls over approximately every 49.7 days (2^31 ms). Reset behavior: on STOP-to-RUN the counter is reset to 0, so it does not persist across restart; it is intended only for delta time measurement inside a continuous RUN.
DB Declaration
Create a shared DB (e.g. DB50 "Clock_DB") with the following structure. The combined DT path uses a single 8-byte element; the dual-tag path uses separate DATE and TOD tags and a scratch INT array for byte-level BCD decoding:
DATA_BLOCK "Clock_DB"
TITLE = PLC clock mirror
VERSION : 1.0
STRUCT
WallClock : DATE_AND_TIME; // 8 bytes, output of SFC1
DatePart : DATE; // 2 bytes, output of DT_DATE
TodPart : TIME_OF_DAY; // 4 bytes, output of DT_TOD
DayOfWeek : INT; // 1..7, output of DT_DAY
YearBCD : BYTE; // raw byte 0 of SFC1 buffer
MonthBCD : BYTE; // raw byte 1
DayBCD : BYTE; // raw byte 2
HourBCD : BYTE; // raw byte 3
MinuteBCD : BYTE; // raw byte 4
SecondBCD : BYTE; // raw byte 5
MsHighBCD : BYTE; // raw byte 6 (top nibble = 100s)
MsLowBCD : BYTE; // raw byte 6 (low nibble = 10s)
DOWbyte : BYTE; // raw byte 7
YearInt : INT; // BCD_TO_INT result
MonthInt : INT;
DayInt : INT;
HourInt : INT;
MinuteInt : INT;
SecondInt : INT;
MsInt : INT; // 0..999
DayOfWeekInt : INT;
Tick : TIME; // SFC64 output, 1 ms ticks
ClockStatusOK : BOOL; // SFC1 RET_VAL == 0
END_STRUCT;
END_DATA_BLOCK
Method 1: Read into DATE_AND_TIME and extract parts
This is the recommended path on S7-300/400. One call to SFC1 fills the 8-byte BCD buffer, and the standard conversion functions split it into IEC-native date and TOD values that compare, add, and subtract directly.
STL implementation in OB1
// Call SFC1 READ_CLK, store buffer in DB50.WallClock
CALL "READ_CLK"
RET_VAL := MW100
PDT := "Clock_DB".WallClock;
// Check return value
L MW100
L 0
==I
= "Clock_DB".ClockStatusOK;
// Extract date part (DT_DATE: returns DATE, days since 1990-01-01)
CALL "DT_DATE"
IN := "Clock_DB".WallClock
RET_VAL := "Clock_DB".DatePart;
// Extract time-of-day part (DT_TOD: returns TOD, ms since midnight)
CALL "DT_TOD"
IN := "Clock_DB".WallClock
RET_VAL := "Clock_DB".TodPart;
// Extract day of week (1=Sunday .. 7=Saturday)
CALL "DT_DAY"
IN := "Clock_DB".WallClock
RET_VAL := "Clock_DB".DayOfWeek;
Reading tick counter for delta time
CALL "TIME_TCK"
RET_VAL := "Clock_DB".Tick;
W#16#0000 = no error. W#16#8081 = internal clock error (RTC battery removed or failed). If 8081 is observed at first power-up after storage, the buffer returns 90-01-01 00:00:00.000 — verify the battery holder and replace the backup battery (3.6 V 1/2 AA, e.g. 6ES7971-1AA00-0AA0 or compatible).Method 2: Read raw BCD bytes and convert
Use this path when the destination application needs integer values (for ASCII conversion, HMI display, or hand-coded math) and the IEC conversion blocks are not available in the project.
LAD/FBD call (single network per byte)
-
Network 1 — Call SFC1 with PDT pointing to a 8-byte ANY scratch variable (or directly into the DB if the DB layout uses 8 individual BYTEs; in that case the buffer can be sliced at DB level only if the PDT pointer is byte-aligned — declare 8 individual
BYTEtags and pass the first tag's address as PDT). -
Network 2..7 — Use
BTI(BCD to Integer, built-in) on each tag:| "Clock_DB".YearBCD --- BTI --- "Clock_DB".YearInt | "Clock_DB".MonthBCD --- BTI --- "Clock_DB".MonthInt | "Clock_DB".DayBCD --- BTI --- "Clock_DB".DayInt | "Clock_DB".HourBCD --- BTI --- "Clock_DB".HourInt | "Clock_DB".MinuteBCD --- BTI --- "Clock_DB".MinuteInt | "Clock_DB".SecondBCD --- BTI --- "Clock_DB".SecondInt -
Network 8 — Decode millisecond field. Byte 6 carries three BCD digits: hundreds (high nibble), tens, units. Convert with the FBD
BCD_TO_INT(FB93)from the Standard Library for the full 0..999 value, or split manually:L "Clock_DB".MsHighBCD // e.g. 16#04 (hundreds) L 100 *I L "Clock_DB".MsLowBCD // e.g. 16#56 (tens + units) +I T "Clock_DB".MsInt
Why BCD conversion is mandatory
Direct byte-to-INT transfer on a BCD clock field produces silently wrong values. Example: the year 2024 is stored as 16#24. A plain L PB0; T MW10 moves 0x24 = 36 decimal. A magazine indexed by year will then show "year 36", or a comparison with constants in the 2000..2089 range will fail in a way that is hard to trace. The fix is one BTI per byte.
Writing the Clock with SFC0
SFC0 is the write counterpart. The 319F-3 supports a master and up to 3 slave clocks. Typical commissioning script:
// Master clock set to system time, slaves only listen (SET=0)
CALL "SET_CLK"
PDT := "Clock_DB".WallClock // source DT, populated manually or by FB
SET := TRUE // TRUE = this CPU sets its own clock
RET_VAL := MW110;
Common pattern: a one-shot FB that runs on OB100 (warm restart) and reads the operator's HMI-entered time, writes it with SFC0, and then loops once per minute using SFC64 to verify drift.
Local Time vs UTC
SFC1 returns the CPU's local wall-clock time (i.e. whatever SFC0 last wrote, plus the internal drift compensation). The 319F-3 hardware does not track a separate UTC offset; DST is not automatic. If the application needs UTC, run the conversion in user code:
// 1. Read DT
CALL "READ_CLK"; PDT := "Clock_DB".WallClock;
// 2. Split to DATE + TOD
CALL "DT_DATE"; IN := "Clock_DB".WallClock; RET_VAL := "Clock_DB".DatePart;
CALL "DT_TOD"; IN := "Clock_DB".WallClock; RET_VAL := "Clock_DB".TodPart;
// 3. Add (or subtract) the TZ offset in TOD units (1 TOD unit = 1 ms)
CALL "AD_DT_TM"; // adds TIME to DT (sum of DATE+TIME)
T1 := "Clock_DB".WallClock
T2 := "TZ_Offset" // TIME, e.g. T#0h0m0s for UTC+0
RET_VAL := "Clock_DB".UTC_DT;
// 4. Or convert TOD to TIME and add
CALL "D_TOD_DT"; IN := "Clock_DB".TodPart; RET_VAL := MW120;
IECFC library from Options > Manage System Data if it is not already present.Verification and Commissioning Checks
-
Online monitor DB50 in STEP 7 and confirm
WallClockincrements by exactly 1 s every 1 s when watched in the watch table. -
Tick continuity: add a difference calculation
Tick - PrevTickin a 1 s OB32 interrupt; expect 1000 ms ± 1 ms (the 319F-3 scans OB32 with a 1 s phase offset). -
BCD sanity: at year-end rollover, verify that byte 0 of
WallClockchanges from16#23(2023) to16#24(2024), and thatYearIntflips to 2024 after the next OB1 scan. -
Battery health: power down the CPU for 5 minutes; on power-up, confirm SFC1 RET_VAL = 0 and
WallClockis still current. IfW#16#8081is reported, replace the backup battery. - Drift check: log the tick counter at midnight UTC and 24 h later; difference should be 86,400,000 ms ± 1,000 ms on the 319F-3 (typical drift 1..5 s/day at 25 °C).
-
Time zone: simulate DST transition by adding
T#1hwithAD_DT_TMat a known trigger; verify the result is exactly 1 hour later in the TOD component.
Troubleshooting Matrix
| Symptom | Likely cause | Diagnostic | Remediation |
|---|---|---|---|
| SFC1 RET_VAL = W#16#8081 | RTC battery missing/dead | Check battery LED, hardware diagnostic buffer | Replace 3.6 V 1/2 AA backup battery; reset time via SFC0 |
| Year shows 36 instead of 2024 | Reading BCD byte as plain INT | Watch table on raw byte 0 = 16#24 | Insert BTI / BCD_TO_INT conversion per byte |
| DT_DATE / DT_TOD blocks not resolved in program | IEC Function Blocks library not installed | LAD/FBD compiler warning in STEP 7 | Open Standard Library > IEC Function Blocks and copy the FCs into the S7 program |
| OB121 STOP after enabling SFC0/SFC1 | PDT pointer is not aligned to a BYTE; or PDT points to a local Temp that is out of scope | Diagnostic buffer > I-stack error OB121 cause | Use a symbolic DB tag, or a 8-byte ANY pointer to a STAT area |
| Tick counter resets unexpectedly | STOP/RUN transition resets TIME_TCK on 319F | Monitor OB100 / OB101 trigger | Re-baseline the delta calculation; do not use SFC64 across restart |
| Local time is one hour off in spring/autumn | No DST logic in user code | Compare WallClock to UTC reference | Add AD_DT_TM call with TZ_Offset TIME tag, or move to a CPU that supports time-of-day zones (S7-1500) |
| Year shows 89 after power-up | Watchdog on SFC0 with the wrong format (binary PDT instead of BCD) | Inspect source DT before SFC0 call | Always populate PDT with a value obtained from SFC1 or a properly BCD-encoded DT literal |
Performance and Cycle Time Impact
On the 319F-3 PN/DP, SFC1 execution is 18..24 µs (typical), SFC0 is 22..30 µs, and SFC64 is 14..20 µs. The IEC conversion blocks DT_DATE, DT_TOD, DT_DAY are each 6..12 µs. The complete Method 1 sequence (SFC1 + three conversions) takes less than 60 µs in OB1 and is well below the 1 ms threshold for any scan cycle. Method 2 (raw bytes + BTI per field) is comparable. Avoid calling the full sequence in fast OB35/OB32 interrupts with a 1 ms phase; cache the DT in a DB and only re-read at OB1 scan or once per second.
Cross-Platform Notes
STEP 7 V5.x S7-300/400 versus TIA Portal S7-1200/1500 use different time models. The S7-1200/1500 DTL type is a 12-byte structure with the same semantic content (year..nanoseconds + day-of-week) but is stored as binary integers, so no BCD conversion is needed. The equivalent call on S7-1500 is RD_SYS_T (reads into a DTL tag) and WR_SYS_T (writes a DTL tag). The Standard Library analog of SFC1 on S7-1500 therefore returns DTL directly, and the BTI path in Method 2 is not required. ABB Automation Builder on AC500 series uses a similar IEC-61131-3 DT and provides DT_TO_DATE / DT_TO_TOD helpers in the standard library. The principle — read the hardware clock into a DB-resident tag, then split — is identical; only the names and byte ordering differ.
Frequently Asked Questions
What is the difference between SFC1 READ_CLK and SFC64 TIME_TCK on the 319F-3?
SFC1 returns the absolute wall-clock time (year, month, day, hour, minute, second, millisecond, day-of-week) encoded as an 8-byte BCD DATE_AND_TIME buffer. SFC64 returns a 32-bit TIME value (milliseconds since the last STOP-to-RUN transition) used for delta time measurement. SFC1 is for "what time is it?" SFC64 is for "how long has this been running?".
Why are the clock bytes in BCD and not binary on S7-300/400?
The SIMATIC S7-300/400 hardware clock buffer follows the legacy BCD encoding defined by the IEC DATE_AND_TIME data type. This allows each field to be displayed directly on a 7-segment display or an OP without conversion. SFC1 writes BCD; reading the bytes as plain binary will yield wrong values (e.g. 16#24 as decimal 36 instead of year 2024). Use BTI or the Standard Library FC93 BCD_TO_INT for each byte.
Do I need SFC0 SET_CLK every cycle, or only once?
Only once at commissioning, or any time the PLC must be re-synchronized. SFC0 sets the CPU's RTC and the slave clocks attached to the MPI/PROFIBUS segment. The 319F-3 keeps the time running on its battery-backed RTC after SFC0 has been executed. Calling SFC0 in every OB1 cycle is unnecessary and can introduce drift if the source pointer contains a time that is not the master reference.
How do I get day-of-week and year integer in a usable format?
Use the IEC standard function block DT_DAY (1=Sunday .. 7=Saturday) for the day-of-week. For year, do not rely on the BCD byte directly; convert the 8-byte DATE_AND_TIME first to a DATE with DT_DATE, then add the year offset (1990) and divide by 365.25 in user code, or just use a CASE ladder on the BCD year byte if you only need decade-level granularity.
What happens to SFC64 TIME_TCK on restart?
On the S7-300 CPU 319F-3 the tick counter resets to 0 on every STOP-to-RUN transition (cold and warm restart). It does not survive OB100. Use the absolute clock (SFC1) for any time value that must persist across restart, and reserve SFC64 for short-interval delta timing inside a single continuous RUN.