1. Overview
Generating a deterministic boolean trigger on every full hour (HH:00:00) of an S7-1200 CPU real-time clock is a recurring engineering task for shift-change logging, hourly report aggregation, energy-meter freeze registers, and time-of-use (TOU) tariff switching. This reference documents a compact SCL implementation that reads the CPU clock with RD_SYS_T, converts it to milliseconds-since-midnight, and uses the modulo operator to detect the exact hour boundary. The technique is scan-time tolerant, requires no external library, and ports to any S7-1200/1500 project built with TIA Portal V14 or later.
The complete one-line condition that produces the trigger is:
#Trig := ((TOD_TO_DINT(DTL_TO_TOD(RD_SYS_T(#t))) MOD 3600000) = 0);
The remainder of this document breaks down the math, the underlying data types, the edge cases (power-up behavior, scan period, 23:59:59 to 00:00:00 wrap), and the production-grade alternatives including the Siemens Schedule library and time-of-day hardware interrupts.
M0.5 for 1 Hz, M0.4 for 0.5 Hz) do not exist on S7-1200/1500. A 1-Hz flag is available in some firmware versions but does not provide a clean hourly trigger. Converting the system clock to seconds and applying a modulo test is the standard, scan-independent solution.2. Prerequisites
| Item | Required | Notes |
|---|---|---|
| CPU | S7-1200 any firmware 2.0 or later | RD_SYS_T available from V1.0; DTL/TOD available from V1.0 |
| Engineering tool | STEP 7 Basic / Professional V14 or later (TIA Portal) | V15.1 or later recommended for current library versions |
| Language | SCL (Structured Control Language) | Code below compiles in STL/LAD with equivalent boolean math but SCL is most readable |
| OB1 cycle | Any up to 1000 ms | Faster cycles produce cleaner pulses; OB35 (100 ms) is a typical choice |
| Clock source | Internal RTC or NTP | For drift-free hourly triggers use NTP (S7-1200 V4.4+ supports NTP client) |
3. Data Types: DTL and TOD
3.1 DTL (Date and Time, Long)
DTL is a 12-byte structured data type that stores the full calendar date and time with nanosecond resolution. The CPU writes a DTL value when RD_SYS_T is called. Layout (byte offsets in the PLC memory image):
| Byte offset | Field | Size (bytes) | Range |
|---|---|---|---|
| 0 | YEAR | 2 (INT) | 1970 to 2554 |
| 2 | MONTH | 1 (USINT) | 1 to 12 |
| 3 | DAY | 1 (USINT) | 1 to 31 |
| 4 | WEEKDAY | 1 (USINT) | 1 (Sun) to 7 (Sat) |
| 5 | HOUR | 1 (USINT) | 0 to 23 |
| 6 | MINUTE | 1 (USINT) | 0 to 59 |
| 7 | SECOND | 1 (USINT) | 0 to 59 |
| 8 | NANOSECOND | 4 (UDINT) | 0 to 999,999,999 |
3.2 TOD (Time of Day)
TOD is a 4-byte IEC 61131-3 type that holds milliseconds elapsed since midnight (00:00:00.000). It does not carry the date. Range: 0 to 86,399,999 ms which equals 24 hours minus 1 ms.
3.3 Conversion Path
The trigger logic chains three conversions:
-
DTL to TOD with
DTL_TO_TODwhich strips YEAR/MONTH/DAY/WEEKDAY/NANOSECOND and returns the time-of-day in milliseconds. -
TOD to DINT with
TOD_TO_DINTwhich returns the millisecond count as a 32-bit integer. -
Arithmetic: apply
MOD 3600000directly on the millisecond count. The remainder is 0 only during the second 00 to 999 of every hour, that is HH:00:00.000 to HH:00:00.999.
Alternative equivalent form: divide by 1000 first to obtain integer seconds, then test MOD 3600 = 0. Both forms yield the same boolean for an exact boundary; the MOD 3600000 form avoids a divide and matches the canonical Siemens example.
4. RD_SYS_T Instruction Reference
RD_SYS_T reads the current local time of the CPU's real-time clock and writes it to a DTL tag. Signature:
RD_SYS_T(OUT := <DTL tag>, RET_VAL := <status word>);
| Parameter | Declaration | Type | Description |
|---|---|---|---|
| OUT | Output | DTL | Receives the current local time |
| RET_VAL | Return | INT / WORD | 0 = no error; 80A1 = time-of-day invalid (for example RTC battery low on early S7-1200) |
The instruction is non-blocking and executes in a few microseconds. It must be called at least once per scan to refresh the working variable; the variable does not auto-update.
Error_Status output to the HMI alarm system on early S7-1200 hardware where the RTC battery is socketed.5. FB Interface Definition
Create a new function block (for example FB_HourTrigger) with the following interface in TIA Portal:
| Name | Type | Direction | Description |
|---|---|---|---|
| t | DTL | Static | Working variable written by RD_SYS_T |
| Time_Of_Day | TOD | Static | Working variable after DTL_TO_TOD |
| Trig | Bool | Output | TRUE during the second HH:00:00 |
| Trig_Pulse | Bool | Output | One-scan TRUE on the rising edge of Trig |
| Trig_Prev | Bool | Static | Previous cycle value of Trig for edge detection |
| Error_Status | Word | Output | Return value of RD_SYS_T (0 = OK) |
| R_Trig_Inst | R_TRIG | Static | Multi-instance rising-edge detector |
6. Implementation Method 1 - Explicit CASE Statement
The first method enumerates the 24 possible seconds-since-midnight values that represent an hour boundary. This form is easy to audit line-by-line in code review:
// FB_HourTrigger - Method 1 (verbose)
#Error_Status := RD_SYS_T(#t);
#Time_Of_Day := DTL_TO_TOD(#t);
#Trig := FALSE;
CASE (TOD_TO_DINT(#Time_Of_Day) / 1000) OF
0, // 00:00:00
3600, // 01:00:00
7200, // 02:00:00
10800, // 03:00:00
14400, // 04:00:00
18000, // 05:00:00
21600, // 06:00:00
25200, // 07:00:00
28800, // 08:00:00
32400, // 09:00:00
36000, // 10:00:00
39600, // 11:00:00
43200, // 12:00:00
46800, // 13:00:00
50400, // 14:00:00
54000, // 15:00:00
57600, // 16:00:00
61200, // 17:00:00
64800, // 18:00:00
68400, // 19:00:00
72000, // 20:00:00
75600, // 21:00:00
79200, // 22:00:00
82800: // 23:00:00
#Trig := TRUE;
END_CASE;
This method has zero math beyond integer division and is straightforward. The drawback is verbosity: 24 constants must be maintained. If the same FB is used for 15-minute or 30-minute triggers the constant list grows proportionally.
7. Implementation Method 2 - MOD 3600 (Recommended)
The modulo operator reduces the 24-element CASE list to a single expression. This is the production-grade form:
// FB_HourTrigger - Method 2 (recommended, full SCL source)
#Error_Status := RD_SYS_T(#t);
#Time_Of_Day := DTL_TO_TOD(#t);
#Trig := ((TOD_TO_DINT(#Time_Of_Day) MOD 3600000) = 0);
// Rising-edge detection for single-scan pulse
#R_Trig_Inst(CLK := #Trig, Q => #Trig_Pulse);
The trigger boolean is TRUE during the entire 1000 ms interval starting at HH:00:00.000. The R_TRIG instance converts this 1-second-wide pulse into a one-scan pulse suitable for counters, SOE records, or edge-triggered logs.
| OB1 cycle | Scans with Trig=TRUE per hour | Scans with Trig_Pulse=TRUE per hour |
|---|---|---|
| 10 ms | 100 | 1 |
| 50 ms | 20 | 1 |
| 100 ms (OB35) | 10 | 1 |
| 200 ms | 5 | 1 |
| 500 ms | 2 | 1 |
| 1000 ms (edge case) | 1 (may miss the 999 ms sub-second) | 0 or 1 |
8. Timing Behavior - SVG Diagram
9. Power-Up and Cold-Start Behavior
S7-1200 retains the RTC across power cycles if the optional battery card is installed (6ES7297-0AX30-0XA0 for early CPUs; integrated on later models). Without a battery, the RTC restarts at 01.01.2000 00:00:00 on power-up. The trigger will fire once at that wall-clock value, which is usually not the desired behavior.
Two practical remedies:
- Enable NTP synchronization. Configure the CPU NTP client in Device Configuration > Time of Day > NTP mode (S7-1200 V4.4 and later). The first trigger after boot may be late, but subsequent triggers are accurate to within the NTP poll interval.
- Suppress the first N triggers. Use a startup counter that ignores the first N hourly triggers after a STOP to RUN transition. Counter initial value should equal the suppression count; decrement on each Trig_Pulse; only allow downstream action when counter reaches 0.
// Suppress first 2 triggers after startup
IF "FirstScan" THEN
#SuppressCount := 2;
END_IF;
#Trig_Pulse := FALSE;
IF #R_Trig_Inst.Q AND #SuppressCount = 0 THEN
#Trig_Pulse := TRUE;
END_IF;
IF #R_Trig_Inst.Q AND #SuppressCount > 0 THEN
#SuppressCount := #SuppressCount - 1;
END_IF;
10. Alternative - Siemens Schedule Library (LGF_Schedule)
For shift schedules, weekend/holiday suppression, and multi-event cron-style timing, the Siemens LGF_Schedule block from the Library of General Functions (LGF) is the production-grade choice. Reference: Siemens Support entry 109479728 - LGF Schedule function block.
The LGF block accepts a structured schedule (start time, end time, weekdays, dates) and returns a boolean "active" output. It is heavier than the MOD technique (kilobytes of code versus tens of bytes) but eliminates the maintenance burden of listing 24 constants and supports complex weekly and monthly patterns out of the box.
11. Alternative - Time-of-Day Hardware Interrupt (OB10)
For applications that need the absolute minimum jitter (sub-scan accuracy), configure OB10 as a time-of-day interrupt and arm it to fire on the next HH:00:00. The CPU schedules the OB within plus or minus 1 ms of the requested time, irrespective of the OB1 cycle. Configuration is in Device Configuration > Properties > Time of Day Interrupts in TIA Portal.
OB10 is appropriate when:
- The action must happen at a precise instant (for example TOU tariff switch at midnight).
- Multiple time-of-day events are needed (arm OB10 for each, then re-arm inside the OB itself).
- OB1 is long (for example 500 ms) and the consumer cannot tolerate the latency.
For sub-hourly precision (for example firing every 15 minutes on a 60 ms OB1), OB10 is not the right tool - use a cyclic interrupt OB (OB30 to OB38) with the appropriate phase offset.
12. Cross-Platform Portability
The MOD-3600 technique translates directly to other IEC 61131-3 platforms by swapping the system-clock read instruction:
| Platform | Read clock | Type | Convert | Modulo test |
|---|---|---|---|---|
| Siemens S7-1200/1500 | RD_SYS_T | DTL | DTL_TO_TOD, TOD_TO_DINT | MOD 3600000 (ms) |
| Siemens S7-300/400 | SFC 1 READ_CLK | DT (8-byte) | FC 8 DT_TOD, DTB to DINT | MOD 3600000 (ms) |
| Allen-Bradley CompactLogix | GSV WallClockTime | DATE_AND_TIME | Subtract 1970 epoch, convert to seconds | MOD 3600 (s) |
| Schneider M340/M580 | RRTC_DT | DATE_AND_TIME | Substring TOD, convert ms | MOD 3600000 (ms) |
| CODESYS V3 | RTC (function block) | DATE_AND_TIME / DT | Same chain as Siemens | MOD 3600000 (ms) |
| Beckhoff TwinCAT 3 | FB_LocalSystemTime | TIMESTRUCT | Convert TOD to ms | MOD 3600000 (ms) |
The modulo test is platform-agnostic; only the clock source and conversion differ.
13. Variant Triggers - Minutes, Quarters, Days
The same FB supports any divisor of 86,400,000 ms (24 hours):
| Pattern | Modulo divisor (ms) | Trigger condition |
|---|---|---|
| Every minute (XX:YY:00) | 60,000 | (TOD ms) MOD 60000 = 0 |
| Every 15 minutes | 900,000 | (TOD ms) MOD 900000 = 0 |
| Every 30 minutes | 1,800,000 | (TOD ms) MOD 1800000 = 0 |
| Every hour | 3,600,000 | (TOD ms) MOD 3600000 = 0 |
| Midnight only | n/a | (TOD ms) = 0 |
For non-divisor intervals (for example "every 7 minutes" or "every 17 minutes") the LGF_Schedule library is required.
14. Verification and Commissioning
-
Watch table test. In TIA Portal, open the FB instance DB and add
Trig,Trig_Pulse,Error_Status, andTrig_Prevto a watch table. Force the CPU clock to within seconds of HH:00:00 and observe the boolean transitions. -
Cross-check with raw clock. Use
RD_SYS_Tdirectly in the watch table to confirm the displayed clock matches the expected wall-clock time. - Long-duration test. Set the PLC clock to 23:59:55, run for 10 seconds, and confirm exactly one trigger occurs on the 00:00:00 boundary (midnight transition).
-
Scatter-gather trace. In the trace tool, record
Trig,Trig_Pulse, and the rawt.HOUR/t.MINUTE/t.SECONDfields across one full hour to confirm pulse width and edge behavior. - NTP drift verification. With NTP enabled, run for 72 hours and log the wall-clock time of each trigger; drift should be within plus or minus 1 second of the requested boundary.
15. Troubleshooting Matrix
| Symptom | Likely cause | Fix |
|---|---|---|
| Trigger never fires | OB1 stopped, RD_SYS_T not called, scan time above 1000 ms missing the second | Place the FB call in OB1 or a cyclic OB; verify OB1 is running; shorten cycle to 100 ms (OB35) |
| Trigger fires on every scan | Constant 3600000 mistyped as 0; logic inversion wrong |
Check DINT constant; use PLC variable table to inspect intermediate values |
| Spurious trigger at power-up | RTC defaults to 2000-01-01 00:00:00 on un-batteried CPU | Install battery, enable NTP, or suppress first N triggers |
| Trigger drifts by minutes over weeks | Internal RTC drift (typically plus or minus 2 s/day) | Enable NTP synchronization in device configuration |
| Trigger fires at HH:00:00.500 | Operator manually set clock mid-second; trigger remains TRUE for full second | Expected behavior; use Trig_Pulse for single-scan consumer |
| Compiler error: TOD_TO_DINT unknown | SCL source not refreshed after re-import of types | Right-click project > Compile > Software (rebuild all) |
| Trigger fires twice within one second | OB1 cycle crosses the 999 to 0 ms boundary mid-scan and reads both states | Wrap the modulo result in a hysteresis test; use OB35 with 100 ms |
| Trigger absent on spring-forward day | DST skipped the 02:00 to 03:00 hour | Acceptable for UTC-based clocks; document the behavior in the consumer |
16. Safety and Operational Notes
17. Frequently Asked Questions
Why is the test on milliseconds (MOD 3600000) instead of seconds (MOD 3600)?
Both forms produce the same boolean for an exact HH:00:00.000 boundary, but MOD 3600000 avoids an explicit divide-by-1000 step. The DTL to TOD to DINT path returns milliseconds since midnight, and 3,600,000 ms = 1 hour, so the modulo test runs on the native scale of the data and the compiler does not have to optimize a divide away.
Will the trigger fire on the first scan after power-up?
Only if the RTC happens to be exactly on HH:00:00 at that instant. On S7-1200 CPUs without a battery the RTC restarts at 2000-01-01 00:00:00, so a single spurious trigger will occur on the first scan. Suppress it with a startup counter, install a battery, or enable NTP synchronization.
How do I generate triggers every 15 minutes or every 30 minutes?
Replace 3,600,000 with 900,000 (15 minutes) or 1,800,000 (30 minutes). The same FB works for any divisor of 24 hours. For arbitrary intervals that are not factors of 86,400,000 ms, use the LGF_Schedule library referenced above.
Does daylight saving time affect the trigger?
The S7-1200 internal RTC stores local time without DST handling. If DST is implemented at the HMI or SCADA layer by skipping 02:00 to 03:00, the trigger at 02:00:00 will simply not occur on the spring-forward day. Plan the consumer logic for the missing trigger or migrate to a UTC-based clock with NTP.
Can I use this technique on S7-300/400 with classic STL?
Yes. Read the system clock with SFC 1 (READ_CLK) into a DT (DATE_AND_TIME) buffer, use FC 8 (DT_TOD) to extract the TOD into a TIME buffer, convert the TIME to a DINT in milliseconds, then apply MOD 3,600,000. The math is identical to the S7-1200 path.
What is the worst-case latency between wall-clock HH:00:00 and the trigger boolean going TRUE?
For an OB1 cycle of 100 ms the worst-case latency is one full cycle (100 ms). For an OB10 time-of-day interrupt the worst-case is plus or minus 1 ms plus the OB10 priority-class latency, typically well under 5 ms on a CPU with light load. The MOD technique cannot beat OB10 for jitter; use OB10 when sub-10 ms accuracy is required.