Detecting S7-300 Real-Time Clock Changes by a Programming Device
When a programming device (PG) connects to a Siemens S7-300 PLC and pushes a new date/time through the online "Set Time" command, the on-board real-time clock is overwritten silently. OB10 time-of-day interrupts, shift registers, batch timers, energy counters, and password aging logic all silently re-anchor to the new value. Detecting the change from inside the user program is therefore a frequent requirement in plant-floor applications, audit trails, and tamper-evident logs. This reference covers the three proven methods available on S7-300 CPUs, the SFC/SZL machinery behind them, and ready-to-paste code for STEP 7 V5.x and TIA Portal.
Problem Definition and Engineering Impact
A typical complaint from the field: an OB10 (time-of-day interrupt) triggers on the expected date, but the next invocation is ten days late because the system clock was rolled back between cycles. The PLC continues running with a corrupted time base, and any code that derives elapsed time from the system clock (SFC2 SET_RTM, SFC3 CTRL_RTM, SFC64 TIME_TCK) returns wrong values. Common symptoms reported on commissioning calls:
- Audit logs for pharmaceutical or food-and-beverage batches show impossible timestamps, breaking 21 CFR Part 11 trails.
- Energy meters that sum kWh between 00:00 and 06:00 lose a full day of data because the clock was jumped backward at 02:00.
- Equipment runtime counters roll back, causing premature or delayed maintenance alerts.
- Token-based session timers, password aging, and license expiry logic expire early or never.
- Recipe phase control misses a sunrise or sunset event because OB10 was rearmed against the wrong time.
What makes detection non-trivial is that SFC1 READ_CLK returns the current value of the hardware clock, but no single status bit inside the CPU tells the user program that the clock was just set. The information exists in three places: in the diagnostic buffer (a fresh entry of class "Time set"), in the OB restart information captured at the most recent restart, and in the change of the time value itself between two consecutive reads. The three methods below exploit each of those sources. The first is the most portable; the second is the most authoritative; the third is a useful sanity check at restart.
S7-300 Clock Architecture and Programmer Access Path
The hardware clock is integrated into the CPU module (CPU 31x, CPU 31xC, CPU 31x PN/DP, ET 200S IM151-7 CPU, and the CPU 31xT/F for technology). It is backed by a buffer capacitor that holds the value through a power cycle of typically 6 weeks (CPU 312) to 6 months (CPU 319 PN/DP) at 25 °C. The same clock is shared with all time-aware services: OB10-OB17 time-of-day interrupts, runtime meters (SFC2/SFC3), TOD clock interrupts in PROFINET IO devices, and the diagnostic buffer timestamps.
Three legitimate paths exist to write the clock:
-
Programming device — Online > "Set Time and Date" in STEP 7 or TIA Portal. Sends a time to the CPU that is then written via the internal SFC0
SET_CLKservice. -
User program — Calls to SFC0
SET_CLKfrom any OB. The diagnostic buffer records "Time set by user program". - SIMATIC time-of-day synchronization — NTP via CP, S7 time master/slave via MPI/PROFIBUS/PROFINET, or the "Synchronize clock via LAN" job from WinCC. These produce a different diagnostic-buffer event class so they can be distinguished from a manual PG action.
For the purpose of tamper detection, the manual PG path is the one that typically lacks accountability. The diagnostic buffer records a single event (typically Event ID W#16#42A1 on most S7-300 firmware) with the "old time" / "new time" delta in its 20-byte payload. Methods 1 and 2 below recover this information without polling the PG side.
Prerequisites: Hardware, Firmware, and STEP 7 Setup
Detecting clock changes works on virtually every S7-300 CPU produced, but the diagnostic buffer event IDs vary slightly with firmware. The matrix below is the minimum environment for each method:
| Method | Minimum CPU | Minimum Firmware | STEP 7 Tool | Notes |
|---|---|---|---|---|
| 1 — Cyclic SFC1 compare | CPU 312 | All | STEP 7 V5.4+ or TIA V13+ | Works on every S7-300, including the 312 IFM. |
| 2 — Diagnostic buffer scan (SFC51) | CPU 313C-2 DP | V2.6+ recommended | STEP 7 V5.4+ or TIA V13+ | Older 312 with 16 KB work memory can fail on large buffers; use method 1 as fallback. |
| 3 — OB100/OB102 start info | CPU 31x with restart OBs | All | STEP 7 V5.x or TIA V13+ | Detects time change only at restart; pair with method 1 for in-cycle coverage. |
The SFCs and SZLs you need are documented in the STEP 7 Standard and System Functions reference (entry ID 109751826) and the S7-300 CPU 31xC / 31x manual (entry ID 12996906). Add the following function blocks to the project before you begin:
- FB / FC container for the monitor logic (e.g.
FB_RTC_Monitor). - Instance DB for the FB (auto-generated).
- Two global DB tags:
g_dtlLast(DTL) andg_bClockSetFlag(BOOL). - Optional: a global DB tag
g_diagLastIdx(INT) holding the index of the last diagnostic buffer entry you processed.
If you run on TIA Portal, drag the standard "READ_CLK" and "RDSYSST" blocks from the Instructions > System function blocks palette rather than re-typing the FBs.
SFC1 READ_CLK Fundamentals and Time Format
SFC1 READ_CLK is the only standard block that returns the full calendar clock. The declaration is identical in STEP 7 V5.x and TIA Portal:
| Parameter | Declaration | Data type | Description |
|---|---|---|---|
| RET_VAL | OUTPUT | INT | Error code; 0 = OK, 8080 = CPU in STOP, 8081 = wrong parameter, 80B0 = real-time clock failure. |
| CDT | OUTPUT | DTL (TIA) or DT (V5.x) | Current date and time. TIA: 8-byte DTL; V5.x: 8-byte BCD-encoded DATE_AND_TIME. |
Under TIA Portal, the return is a DTL struct with 12 bytes (year WORD, month BYTE, day BYTE, weekday BYTE, hour BYTE, minute BYTE, second BYTE, nanoseconds DWORD). Under STEP 7 V5.x, the return is a DATE_AND_TIME 8-byte BCD tag. The comparison operators <, <=, =, >=, > work on both types directly, which is what makes cyclic comparison straightforward.
Call SFC1 from OB1 (cyclic priority class 1) or from a watchdog OB such as OB35. A typical 100 ms cycle from OB35 is fast enough to catch any human-scale clock change (manual keystrokes take seconds) but light enough to not show up in the cycle-time statistics.
Method 1 — Cyclic Reference Comparison (Recommended)
The cyclic comparison method is the simplest and works on every S7-300 CPU. It is purely software-side, requires no SZL access, and adds fewer than 200 bytes of work memory for the monitor FB. The principle: store the last read of the clock, and on every cycle compare the new read to the stored value. A normal CPU never decreases the clock except during the first 4 seconds after power-up while the buffer capacitor recharges. Any backward jump longer than the cycle time is therefore a clock change event.
Three refinements improve robustness in the field:
- Skip the first cycle — The very first read after restart will be far in the past relative to the previous stored value. Initialize the reference on the first read instead of comparing.
- Apply a hysteresis window — Tolerate small backward movements up to the cycle time + 1 s to absorb jitter and the normal pre-tick behaviour of the TOD clock. Only flag movements larger than this window.
- Latch and require reset — Set a sticky BOOL that the operator must acknowledge from the HMI. This forces the alarm into the audit trail rather than disappearing on the next cycle.
The full SCL implementation for TIA Portal is shown below. The same logic ports directly to STEP 7 V5.x by changing the READ_CLK call to CALL SFC 1 with the CDT declared as DATE_AND_TIME:
FUNCTION_BLOCK "FB_RTC_Monitor"
VAR
sCurrent : DTL;
sLast : DTL;
sDiff : TIME;
bInit : BOOL;
iRetVal : INT;
END_VAR
BEGIN
iRetVal := READ_CLK(CDT := sCurrent);
// 80B0 = clock defective; surface immediately
IF iRetVal = 16#80B0 THEN
"g_bClockDefect" := TRUE;
RETURN;
END_IF;
IF NOT bInit THEN
sLast := sCurrent;
bInit := TRUE;
RETURN;
END_IF;
IF sCurrent < sLast THEN
// Backward jump larger than 1 s hysteresis
IF (sLast - sCurrent) > T#1S THEN
"g_bClockSetFlag" := TRUE;
"g_dtlLastSet" := sLast;
"g_dtlNewSet" := sCurrent;
END_IF;
END_IF;
sLast := sCurrent;
END_FUNCTION_BLOCK
Limitations to keep in mind when commissioning Method 1:
- The method cannot tell you who changed the clock. It detects the value change, not the source. If the SIMATIC time-of-day slave port is enabled, routine NTP corrections of a few seconds per day can produce a small backward jump. Tune the hysteresis window to at least 2× the maximum expected sync step (typically 5-10 s for S7 time sync).
- If the CPU stops and restarts while a backward time is in effect, the "first cycle" branch initializes the reference to the new (wrong) value, and a further PG set is required to be detected. Use Method 3 at restart to close that gap.
- The cyclic compare will not detect a forward jump that is masked by a backward jump of identical magnitude across the same cycle — not realistic in practice, but worth knowing if you also need to alarm on forward jumps (e.g. licensed-time exploitation).
Method 2 — Diagnostic Buffer Polling via SFC51
For applications that must distinguish a manual PG "Set Time" from a SIMATIC synchronization, the diagnostic buffer is the only authoritative source. SFC51 RDSYSST reads partial SZLs and can be used to walk the diagnostic buffer at SZL ID W#16#00A0. Each buffer entry is 20 bytes laid out as follows:
| Offset (bytes) | Content | Notes |
|---|---|---|
| 0-1 | Event ID (WORD) | Includes class nibble (bits 12-15) and identifier (bits 0-11). |
| 2-3 | Event class / priority (WORD) | Class 0x42A1 is "Time set" on most S7-300 firmware. |
| 4-11 | Timestamp (DTL/DT) | When the event was logged, not when the clock was set. |
| 12-19 | Event-specific data | For "Time set" events: OLD_TIME (8 bytes) at offset 12 and DELTA (4 bytes) at offset 20 of the second frame. |
The Event IDs reported in practice on S7-300/400 firmware V2.x and V3.x for clock writes are:
| Event ID (hex) | Source | Diagnostic message text (approximate) |
|---|---|---|
| 0x42A1 | PG / user program (SFC0) | "Time of day has been set" |
| 0x4302 | Older firmware variants | "Clock set by operator" |
| 0xE001 | Time synchronization | "Time sync from master" (informational) |
| 0x80B0 | Hardware | "Real-time clock defective" (alarm class) |
To read one diagnostic buffer entry, call SFC51 with SSL_ID = W#16#00A0 and INDEX = 1..N (index 1 is the most recent). SFC51 returns the 20-byte entry in the RET_VAL 4-byte header followed by the payload. A typical scanner looks like the SCL below; the same logic in STL is shown in the next section.
// Read newest entry (INDEX = 1) and decode the Event ID
iRDSRet := RDSYSST(
REQ := bScanTrigger,
SSL_ID := W#16#00A0,
INDEX := 1,
RET_VAL := iSFC51Ret,
BUSY := bBusy,
SSL_RECORD:= abDiag[0]
);
IF (iSFC51Ret = 0) AND NOT bBusy THEN
// Event ID is in the first WORD of the payload
wEventID := WORD_TO_INT(abDiag[0]);
IF wEventID = 16#42A1 THEN
"g_bClockSetFlag" := TRUE;
"g_dtlOldTime" := abDiag[12];
END_IF;
bScanTrigger := FALSE;
END_IF;
On a 100 ms OB35 cycle, scanning the entire diagnostic buffer (typically 100 entries, 2 KB of work memory) takes 1-3 cycles per pass, so throttle the scanner to one pass every 5-10 s and only walk from g_diagLastIdx forward. This avoids putting the CPU into a "watchdog near miss" state at startup when the buffer is large.
WR_USMSG at the end of cold restart to push a "monitor active" event and verify that you can read it back through SZL 0x00A0 before relying on the method in production.OB10 Time-of-Day Interrupts and SFC28 SET_TINT
OB10 is the standard mechanism for scheduling time-of-day actions on S7-300. The PLC has 8 time-of-day OBs (OB10-OB17), but only one can be armed at a time per OB. Arming follows the standard four-call pattern:
-
SFC31
QRY_TINT— check whether the OB is free, active, and the configured phase. -
SFC28
SET_TINT— write the next execution time, period, and offset. -
SFC29
CAN_TINT— cancel the previously armed time, if any. -
SFC30
ACT_TINT— activate the alarm so OB10 starts firing on its phase.
The interaction with a backward clock jump is the very reason the field report exists. SET_TINT interprets the parameter SDT as a calendar value: if the CPU's clock is rolled back 10 days, OB10 will simply not fire for 10 days + the originally scheduled offset. The user program must either re-arm OB10 immediately after detecting a clock change, or use a different scheduling mechanism (e.g. cyclic OB35 with its own timestamp comparison).
Re-arming pattern after a clock-change alarm (SCL):
// Re-arm OB10 with the same phase it had before
wActRet := ACT_TINT(OB_NR := 10); // ignored if not SET_TINT'd yet
// Force a fresh SET_TINT against the new clock
iSetRet := SET_TINT(
OB_NR := 10,
SDT := sNewTriggerTime,
PERIOD := OB_PERIOD_HOUR,
RET_VAL:= iSetRet
);
iActRet := ACT_TINT(OB_NR := 10);
Two more notes specific to OB10 and clock changes:
-
OB10 must exist in the project. If it does not, SFC28 returns
W#16#8090("OB does not exist") and SFC30 returnsW#16#8090. Always check the RET_VAL and fall back to logging the alarm if the OB is missing. -
Period and phase are sticky across a SET_TINT. If you only want to re-schedule the next occurrence, keep the
PERIODandRET_VALarguments identical. The time-of-day OBs also have a "phase" in the hardware config (every minute / hour / day / week / month / year) that controls the resolution; reconfiguring the phase requires a hardware download.
SCL and STL Code Examples
Below is a consolidated SCL example that combines Methods 1, 2, and the OB10 re-arm sequence. It is structured as a single FB so the HMI can poll a small handful of tags rather than diving into the diagnostic buffer directly.
FUNCTION_BLOCK "FB_ClockGuard"
{ S7_Optimized_Access := 'TRUE' }
VAR
sCurrent : DTL;
sLast : DTL;
bInit : BOOL;
iRetCLK : INT;
iRetRDS : INT;
wEventID : WORD;
abDiag : ARRAY[0..19] OF BYTE;
bScan : BOOL;
bBusy : BOOL;
iScanIdx : INT;
END_VAR
BEGIN
// --- Method 1: cyclic SFC1 compare ---
iRetCLK := READ_CLK(CDT := sCurrent);
IF iRetCLK = 16#80B0 THEN
"g_bClockDefect" := TRUE;
RETURN;
END_IF;
IF NOT bInit THEN
sLast := sCurrent;
bInit := TRUE;
ELSIF sCurrent < sLast AND (sLast - sCurrent) > T#1S THEN
"g_bClockSetFlag" := TRUE;
"g_dtlOldTime" := sLast;
"g_dtlNewTime" := sCurrent;
// force an immediate diagnostic-buffer scan
bScan := TRUE;
iScanIdx := 1;
// re-arm OB10 against the new clock
SET_TINT(OB_NR := 10, SDT := sCurrent + T#1M,
PERIOD := OB_PERIOD_HOUR);
ACT_TINT(OB_NR := 10);
ELSE
sLast := sCurrent;
END_IF;
// --- Method 2: SFC51 scan of SZL 0x00A0 ---
IF bScan AND NOT bBusy THEN
iRetRDS := RDSYSST(REQ := TRUE, SSL_ID := W#16#00A0,
INDEX := iScanIdx, RET_VAL := iRetRDS,
BUSY := bBusy, SSL_RECORD := abDiag);
END_IF;
IF bBusy = FALSE AND iRetRDS = 0 AND bScan THEN
wEventID := WORD_OF_BLK(abDiag[0], abDiag[1]);
IF wEventID = 16#42A1 OR wEventID = 16#4302 THEN
"g_bClockSetFlag" := TRUE;
END_IF;
iScanIdx := iScanIdx + 1;
IF iScanIdx > 5 THEN
bScan := FALSE; // scanned 5 newest entries
END_IF;
END_IF;
END_FUNCTION_BLOCK
The same logic in STL (STEP 7 V5.x) is shorter and avoids the need to declare a DTL struct:
// Read clock
CALL SFC 1
CDT := #sCurrent // DATE_AND_TIME, 8 bytes
RET_VAL := #iRetCLK
// First cycle initialization
U #bInit
SPB INIT
L #sCurrent
T #sLast
SET
S #bInit
SPA CONT
INIT: L #sCurrent
L #sLast
>D // DT is comparable
SPB CONT // forward jump, normal
// else backward jump: set alarm
SET
S "g_bClockSetFlag"
CONT: L #sCurrent
T #sLast
BE
For LAD, the comparator block CMP <DTL> from the "Comparator" folder under "Bit logic" handles the same comparison with two clicks, then a negative-coil output latches the alarm into the global DB.
Diagnostic Event ID Reference Table
Keep the following table in your maintenance binder. It maps the Event IDs you will encounter when scanning SZL 0x00A0 to a concrete action in the user program.
| Event ID | Class | Source | Action in user program |
|---|---|---|---|
16#42A1 |
Operational message | PG "Set Time" or SFC0 from user | Latch g_bClockSetFlag, re-arm OB10, write audit record. |
16#4302 |
Operational message | Older firmware (pre-V3) | Same as above; treat as canonical time set event. |
16#E001 |
Information | SIMATIC time-of-day sync (NTP / S7 master) | Suppress alarm if NTP is the legitimate source; update the reference time only. |
16#80B0 |
Alarm / fault | Hardware TOD clock defective | Set g_bClockDefect, call OB80 to enter the user diagnostic handler. |
16#4214 |
Information | Restart (cold/warm) completed | Use to read OB100 start info and confirm a recent clock write was the cause. |
Always verify the Event IDs against your specific CPU firmware before deploying. The mapping above is compiled from the STEP 7 V5.7 standard functions reference (entry 109751826) and the S7-300 CPU 31x manual (entry 12996906). If a firmware update changes an Event ID, the user program will quietly stop detecting the change; add a self-test in OB100 that reads entry index 1 and confirms the Event ID matches the expected mask.
Verification and Field Test Procedure
Before relying on the clock-change monitor in production, run the following five-step verification. Total time: about 20 minutes per CPU on the bench.
-
Set known time — from STEP 7 or TIA Portal, push a known time (e.g. 2024-01-01 00:00:00) to the CPU. Confirm the OB1
g_bClockSetFlaggoes high within one cycle of OB35. -
Roll backward — disconnect the PG, wait one minute, reconnect, and push a time 10 days in the past. Confirm
g_bClockSetFlaglatches a second time, withg_dtlOldTimeandg_dtlNewTimeshowing the delta correctly. - Roll forward — push a time 5 days in the future. Confirm Method 1 does not flag (forward jumps are normal during NTP sync) and that the diagnostic buffer entry 0x42A1 is recorded but ignored by the audit logic if you only want to alarm on backward changes.
- Power cycle test — power off for 60 s to discharge the buffer capacitor partially, restore power, and observe OB100 start information. Confirm Method 3 fires if you have wired the start info to the alarm path.
- OB10 re-arm test — if the application uses OB10, set OB10 to fire every minute, roll the clock back 5 minutes, and confirm OB10 fires within the next 60 s after the alarm latch.
Capture the diagnostic buffer via the PG after each step and compare the Event ID list to the table in the previous section. If the Event IDs do not match, you are looking at a different firmware revision and the program must be tuned to that revision's event set.
Edge Cases, Limitations, and Time Synchronization
Several real-world situations will trip a naive implementation. Plan for them in the design phase.
Cold restart with a "wrong" time
If the PLC is powered off long enough to lose the buffered time, OB102 (cold restart) initializes the clock from the PG at the moment of restart, not from the previous buffered value. Method 1 will not detect this as a backward change because there is no prior value to compare against. Use Method 3 (OB100/OB102 start info) to capture the restart and the new "power-on time" against the time at the moment of power loss (if available in a non-volatile tag).
SIMATIC time-of-day sync
If the S7-300 is configured as a time slave (CPU properties > Diagnostics/Clock > Synchronization), NTP-corrected values are written through SFC0 every cycle. A 1 s step backward in NTP will look like a clock change to Method 1 unless the hysteresis window is widened. The safe setting for an S7-300 is T#10S hysteresis when NTP sync is enabled. The Event ID 0xE001 in the diagnostic buffer is the discriminator if you also implement Method 2.
PROFINET send clock and TOD drift
On S7-300 CPUs with PROFINET interface (CPU 315-2 PN/DP, CPU 317-2 PN/DP, CPU 319-3 PN/DP), the send clock defines how often the I/O cycle runs and indirectly how often the TOD clock is read by the PN stack. The Siemens TIA Portal documentation on the send clock for S7-300/400/1500 explains the relationship between the send clock and the IO device update times. From a clock-tamper perspective, a fast send clock (e.g. 0.5 ms) is irrelevant; the more important effect is that the same CPU is also the time master for the PROFINET IO devices, so a clock change will propagate downstream within a few cycles. Re-arm OB10 only after the master has had a chance to send the new time, otherwise OB10 will fire on a stale value at one of the IOs.
Multi-CPU racks
On racks with two CPUs (rare on S7-300 but possible in some 318 configurations and IM-151-7 foot-print systems), only the master CPU owns the hardware TOD clock. The slave reads the master's time via SFC1 over the backplane bus. If the master is the one being clock-changed, the slave will observe the same backward jump on its own SFC1 call. Your monitor FB should run on both CPUs and the alarms can be OR'd at the HMI or WinCC tag side.
Loss of buffer capacitor
An aged buffer capacitor on a CPU that has been in service 10+ years can drift dramatically at every power cycle. This will look like a constant stream of clock-change alarms. Diagnose by reading the Event ID; a 0x80B0 ("clock defective") entry confirms the hardware, not the operator. Replace the CPU if the capacitor test in HW Config reports a buffer time below the rated minimum (typically 6 weeks for the 31x series).
Does the S7-300 expose a single "clock was set" status bit I can read directly?
No. S7-300 CPUs do not provide a single status bit for clock change events. You must infer the change either by comparing two SFC1 reads (Method 1) or by scanning the diagnostic buffer at SZL 0x00A0 for Event ID 0x42A1 (Method 2). The diagnostic buffer entry includes both the previous time and the new time, which is the most authoritative way to log the event.
Can I tell the difference between a PG-driven time set and a SIMATIC time-of-day sync?
Yes, by reading the diagnostic buffer. Manual PG "Set Time" and SFC0 from the user program generate Event ID 0x42A1 ("Time of day has been set"). NTP, S7 time master, or CP-initiated sync generate 0xE001 ("Time sync from master"). Use the Event ID to gate the alarm: alarm on 0x42A1, ignore 0xE001 if sync is expected.
What happens to OB10 if the clock is rolled backward?
OB10 does not fire retroactively. If you armed OB10 for 2024-06-15 12:00 and someone rolls the clock back to 2024-06-05 12:00, OB10 will not fire for another 10 days. The fix is to call SFC28 SET_TINT again against the new clock and then SFC30 ACT_TINT immediately after detecting the clock change in your monitor FB.
Does OB80 (time error) fire on a clock change?
No. OB80 is generated when the CPU detects a time-jump in the operating system, but a manual PG clock set is handled by the clock driver and is not surfaced as a time error. OB80 typically fires when OB10 cannot be started in time, when a cyclic OB overruns, or when a delay-too-long condition is detected. For detecting a PG clock set, rely on the diagnostic buffer and SFC1, not OB80.
Will Method 1 work after a power cycle on a CPU with a dead buffer capacitor?
Partially. On first power-up the buffer capacitor is exhausted so the TOD clock starts at the last "Set Time" value pushed by the PG. Method 1 will detect the "first cycle after restart" only as an initialization event, not as a clock change, and will not trigger the alarm. Use Method 3 (OB100/OB102 start info) for this case, and consider replacing the CPU if 0x80B0 events appear in the diagnostic buffer.
What is the best OB to call SFC1 from?
OB35 at 100 ms is the conventional choice for time monitors on S7-300 because it has a fixed, well-defined priority. OB1 also works but its cycle time is application-dependent and can be very long on heavily loaded CPUs. For a fast response to a clock change, OB35 with a 100 ms period is the recommended location.