Measuring Pulse Interval Time in Siemens S7 PLCs Using DTL Timestamps
Capturing the elapsed time between two consecutive process pulses is a routine requirement on Siemens S7-300/400 and S7-1200/1500 controllers: tachometer feedback on conveyors, flow-meter pulse trains, period measurement on frequency-output sensors, and rotational-speed sampling on packaging lines. The IEC pulse timer (S_PULSE / TP) only produces a fixed output of defined length and never exposes a free-running "elapsed-since-last-edge" value, so a custom implementation is required. The reliable engineering approach is to read the CPU time-of-day clock on every pulse, store the previous reading, and compute the difference between the two DTL (or DATE_AND_TIME) values.
This reference walks through a field-proven implementation: data-type selection, the Function Block (FB) and Instance Data Block (DB) build, first-scan initialization, OB1 versus hardware-interrupt accuracy, storage scaling, and commissioning checks. STL/SCL listings are provided for STEP 7 Classic (S7-300/400) with notes on the S7-1500 RD_SYS_T / WR_SYS_T instructions in TIA Portal.
Prerequisites
- SIMATIC S7-300 CPU 31x (any firmware) or CPU 41x; firmware V3.0 or later recommended for native DTL support. S7-1500/1200 with TIA Portal V14+ is interchangeable with the S7-1500 portions of this article.
- STEP 7 Classic V5.5 / V5.6 with SCL or STL editor, or TIA Portal V15.1+ with SCL.
- Source of the pulse on a digital input module wired to a hardware-interrupt capable DI (e.g., SM 321, 6ES7321-1BH02-0AA0, for high accuracy) or to a standard DI scanned in OB1 for low-frequency, low-jitter applications.
- Free bit memory (M), a Merker word (MW) for the result, and one Instance DB for the FB.
- CPU clock set to local time and synchronized (HMI, NTP via CP, or SFC 0 SET_CLK).
Choosing the Time Data Type
| Property | DATE_AND_TIME (DT) - S7-300/400 | DTL - S7-1500 / S7-1200 |
|---|---|---|
| Length | 8 bytes (BCD) | 16 bytes |
| Resolution | 1 second (no sub-second field) | 1 nanosecond |
| Obtained via | SFC 1 READ_CLK | RD_SYS_T instruction |
| Set via | SFC 0 SET_CLK | WR_SYS_T instruction |
| Arithmetic | Manual byte conversion; use FC 6 / FC 7 (DT_DATE, DT_DAY) plus TIME arithmetic on separate values | Native: assign to TIME / LTIME / DTL differences via T_SUB / T_DIFF |
| Range | 1990-01-01 to 2089-12-31 | 1970-01-01 to 2554-12-31 |
For applications needing sub-second resolution, the DTL path on S7-1500 is the cleaner choice. On S7-300/400 the engineer typically pairs SFC 1 with a free-running IEC timer or a hardware-counter/timestamp mechanism; the design below shows both an OB1 path and an OB40 hardware-interrupt path. See the SIMATIC S7-300 CPU 31xC and CPU 31x Technical Specifications manual for hardware-interrupt capability per CPU model.
Program Architecture
The recommended structure isolates the timing logic from the application so the FB is reusable on any project.
- Hardware-interrupt OB (OB40) - fires on the rising edge of the pulse input. Reads SFC 1 / RD_SYS_T, stores the timestamp into a static variable of the instance DB, and sets a "new sample available" flag.
- Cyclic OB (OB1) - calls a function block that subtracts the previous timestamp from the current one, writes the result to a Merker word (or process image), and shifts the current timestamp into the previous-slot for the next call.
- Startup OB (OB100) - clears the previous-timestamp storage and forces a first-scan initialization path inside the FB so the first delta is not a garbage value.
Step-by-Step Implementation (STEP 7 Classic, S7-300/400)
Step 1 - Create the FB and declare the static variables
Insert a new Function Block (FB 100) named PulseInterval. In the FB header, declare the variables below in the static section so they are stored in the assigned instance DB.
| Name | Type | Initial value | Comment |
|---|---|---|---|
| FirstRun | BOOL | TRUE | First-scan flag; cleared after first valid sample |
| NewSample | BOOL | FALSE | Set by OB40 when a fresh timestamp is ready |
| PreviousDTL | DTL | DT#1970-01-01-00:00:00.0 | Timestamp of the previous pulse |
| CurrentDTL | DTL | DT#1970-01-01-00:00:00.0 | Timestamp of the most recent pulse |
| DeltaMs | DINT | 0 | Interval in milliseconds |
| DeltaNs | DINT | 0 | Sub-millisecond remainder (signed) |
STEP 7 will prompt for the Instance DB the first time the FB is called from OB1. Use DB 100, name iPulseInterval, and make it non-retain if you want first-run initialization on every warm restart, or retain if the controller must keep the last delta across power cycles.
Step 2 - Read the clock on the pulse edge (OB40 STL)
// OB40 - hardware interrupt OB for the pulse input
// Temporary DTL field in OB40 temp area
L LB 12 // OB40 local byte 12 = event info (rising / falling)
L B#16#1 // 1 = rising edge event
==I
JC PULS
BEU
PULS: CALL SFC 1 // READ_CLK
RET_VAL: MW 200
OUT : DB100.DBD 0 // First 8 bytes = PreviousDTL (DTL = 16 bytes: 0..15)
// Set flag for OB1 to consume
SET
= DB100.DBX 24.0 // NewSample := TRUE
BE
Note the DTL layout in the instance DB: bytes 0-15 hold the DTL (PreviousDTL), bytes 16-23 are not used in this implementation, bytes 24+ are flag/status. Adjust the offset to suit the FB variable layout above.
Step 3 - Compute the delta in OB1 (FB 100 in SCL)
FUNCTION_BLOCK PulseInterval
VAR
FirstRun : BOOL := TRUE;
NewSample : BOOL;
PreviousDTL: DTL;
CurrentDTL : DTL;
DeltaMs : DINT;
DeltaNs : DINT;
END_VAR
BEGIN
// Latch the new timestamp from OB40 on every cycle
IF NewSample THEN
CurrentDTL := CurrentDTL; // No-op; placeholder for clarity
NewSample := FALSE;
END_IF;
// First-run initialization
IF FirstRun THEN
PreviousDTL := CurrentDTL;
FirstRun := FALSE;
DeltaMs := 0;
RETURN;
END_IF;
// Compute interval in milliseconds
DeltaMs := DINT_TO_TIME(TIME()) - DINT_TO_TIME(TIME());
// (Above no-op; replace with the T_SUB / direct DTL subtraction shown below
// for the target CPU firmware.)
END_FUNCTION_BLOCK
For firmware that supports native DTL subtraction, the cleaner form is:
// S7-1500 / SCL-on-300 with V3.0+ firmware:
DeltaMs := DTL_TO_TIME(CurrentDTL - PreviousDTL); // result in ms, as TIME
PreviousDTL := CurrentDTL; // shift for next call
For an S7-300 with classic DATE_AND_TIME, use the byte-level conversion in the next section.
Step 4 - S7-300/400 path with DATE_AND_TIME (1 s resolution)
The legacy DT format stores BCD year/month/day/hour/minute/second across 8 bytes with no sub-second field. For pulse periods longer than 1 second this is sufficient; combine it with a TONR (time accumulator) or a high-speed counter to interpolate within a second. Example STL that reads SFC 1 and stores the result:
CALL SFC 1
RET_VAL: MW 210
CDT : DB200.DBD 0 // DATE_AND_TIME, 8 bytes BCD
// At evaluation, convert DT -> DINT seconds since 1990-01-01 using FC 192
// (custom) or compose the offset from YEAR (B#16#B0+year-1990) etc.
// Typical sequence (illustrative, not optimized):
L DB200.DBB 0 // YEAR (BCD)
SRW 4
L 90
-I
L 365
*I // days for years
L DB200.DBB 1 // MONTH (BCD) -- table lookup follows
... (truncated; in practice use the library FC 6/7)
Step 5 - First-scan initialization (OB100)
// OB100 - startup / warm restart
SET
= DB100.DBX 20.0 // FirstRun := TRUE
CLR
= DB100.DBX 24.0 // NewSample := FALSE
L 0
T DB100.DBD 28 // DeltaMs := 0
T DB100.DBD 32 // DeltaNs := 0
BE
On the first pulse after startup, FirstRun is TRUE so the FB copies the freshly captured CurrentDTL into PreviousDTL and exits with DeltaMs = 0. From the second pulse onward the difference is meaningful.
OB1 Versus Hardware-Interrupt Accuracy
| Implementation | Latency to timestamp | Typical jitter | Suitable for |
|---|---|---|---|
| Read in OB1 on a digital-input flag | 0 to OB1 cycle time (typ. 5-50 ms on S7-300/400) | One full scan | Low-frequency pulses (1 Hz and below) |
| Read in OB40 on a hardware-interrupt DI | Module-dependent; typically <1 ms on SM 321 BH02 | ~100 us on S7-1500 DI; ~500 us on S7-300 DI | Medium frequency (up to a few kHz), tachometry, flow metering |
| SFB 38 / time-stamping digital inputs (S7-300, specific modules) | <1 ms with module-timestamped data | ~1 ms deterministic | High-accuracy event logging, SOE (Sequence of Events) |
| High-speed counter (CTU/HSC) + interrupt | 10 us-class | Depends on counter module | Encoder pulse periods, fast batch counting |
The decisive parameter is the worst-case latency between the rising edge on the input terminal and the SFC 1 call. The OB1 path inherits the full scan time, so on a 20 ms OB1 cycle a 30 Hz pulse train will occasionally be missed or double-counted. The OB40 path, by contrast, fires the interrupt as soon as the DI module latches the edge and the CPU dispatches the OB, with a documented reaction time given in the S7-300 Module Data reference. For S7-1500 the S7-1500 System Manual describes DTL nanosecond resolution and digital-input interrupt handling.
Scaling and Storing the Result
The TIME data type in STEP 7 is a DINT holding milliseconds. To express the period in microseconds, in hertz (1 / period), or as RPM, convert at the output of the FB:
// Convert ms delta to microseconds
DeltaUs := DeltaMs * 1000 + DeltaNs / 1000;
// Frequency in Hz (DINT 0.001 Hz resolution from a REAL cast)
FreqHz := 1000.0 / DINT_TO_REAL(DeltaMs);
// RPM for a one-pulse-per-revolution tachometer
Rpm := 60000.0 / DINT_TO_REAL(DeltaMs);
Place the converted value in a Merker word/double word (MW100 / MD100) or write it to a global DB that the HMI polls. On S7-1500, HMI tags of type LREAL or DINT cycle at the configured update rate; the FB must run at least that fast to deliver a fresh value each poll.
IF DeltaMs > 0 THEN ... END_IF;.Verification and Commissioning
- Force a pulse source (function generator, switch, sensor). Start at 1 Hz and confirm
DeltaMsreads approximately 1000. - Step to 10 Hz, 100 Hz, 1 kHz. On the OB1-only build, expect loss of accuracy above 10-20 Hz; on the OB40 build, accuracy should hold within the DI module's documented reaction time.
- Power-cycle the CPU. Confirm that OB100 forces
FirstRun = TRUEand the first edge returns delta = 0 (or a small value if OB40 already captured a stale timestamp). - Watch the SF (system fault) LED during the test. If an OB121 priority-class error fires, the timestamp subtraction went out of range or the divisor was zero.
- Use the online SCL/STL monitor to break on the FB exit; verify
PreviousDTLmatches the previousCurrentDTLafter each call.
Troubleshooting Matrix
| Symptom | Likely root cause | Corrective action |
|---|---|---|
| DeltaMs reads 0 forever | First-run flag never cleared, or OB40 is not firing | Verify OB40 is assigned to the DI in HW Config; monitor NewSample online; check that the DI is wired NO and the input LED toggles. |
| DeltaMs is huge on the first sample | PreviousDTL is the initial value 1990-01-01 and the current clock is the real date | Confirm OB100 sets FirstRun := TRUE; confirm the first edge forces PreviousDTL := CurrentDTL before subtraction. |
| Jitter of one OB1 cycle time | Reading the timestamp in OB1 instead of OB40 | Move the SFC 1 call into OB40 hardware interrupt. |
| Missing edges at high frequency | OB40 reaction time exceeded; input filter too long | Reduce DI input filter in HW Config; use a high-speed counter module (FM 350-1 / SM 321 fast inputs). |
| OB121 "area length error" on DTL subtraction | Mixing DT and DTL types, or subtracting across a non-supported date range | Confirm both operands are DTL; check the value of CurrentDTL is in the supported range (1970-01-01 to 2554-12-31 on S7-1500). |
| Result is correct in seconds, wrong in milliseconds | Source returns DATE_AND_TIME (1 s resolution) on S7-300, not DTL | Either accept 1 s resolution or migrate to a high-speed counter / DTL-capable S7-1500 path. |
| CPU clock loses time across power cycles | No battery; clock not retained | Install backup battery; enable clock retention in HW Config; consider NTP sync via CP. |
Field-Proven Caveats
- Place the pulse on a DI channel that has hardware-interrupt capability and is enabled in HW Config. Not every channel of an SM 321 supports OB40; on a 32-channel SM 321-1BL00 only specific groups do.
- Keep OB40 lean - do not perform the subtraction inside OB40, only capture the timestamp and set a flag. Long OB40 runtimes delay the next OB1 cycle and can cause priority-class inversion.
- If the pulse is slow (<0.5 Hz) and you do not need high resolution, use the OB1 path with SFC 1 - it is simpler and avoids the OB40 configuration overhead.
- On S7-1500 prefer the RD_SYS_T / WR_SYS_T ladder or SCL instructions over the legacy SFC 0 / SFC 1 form. They are well-documented in the S7-1500 System Manual and the TIA Portal help.
- For SIL/PL-rated pulse acquisition, route the input through a safety DI module (SM 326F / SM 1516F) and use the safety-oriented time stamp mechanism - the standard OB40 timestamp is not safety-qualified.
- Retain the instance DB on warm restarts only if the application requires a valid delta on the first pulse after restart. Otherwise clear it in OB100 to force a clean first-run path.
Migrating to TIA Portal (S7-1500 / S7-1200)
On S7-1500, replace SFC 1 with the RD_SYS_T instruction block from the "Time" folder in the TIA Portal instructions catalog. The output is a DTL tag (declared as a static in the FB or in a global DB). Subtraction of two DTL values directly yields a TIME value (DINT, milliseconds), which can be further divided to LREAL for nanosecond resolution. The Instance DB concept from STEP 7 Classic is preserved in TIA Portal as "Multi-instance" or "Single-instance" DB, but the FB is added under "Program blocks" with optimized or non-optimized access selectable in the FB properties. For S7-1500 with optimized blocks, avoid absolute addressing like DB100.DBD 0; use the symbolic tag "iPulseInterval".PreviousDTL instead.
What is the simplest way to measure the time between two pulses in a Siemens S7 PLC?
Read the CPU clock with SFC 1 (S7-300/400) or the RD_SYS_T instruction (S7-1500) on every pulse, store the value in a static DTL or DATE_AND_TIME variable in an instance DB, and subtract the previous reading from the current one. The difference is your pulse interval, in milliseconds for DTL or seconds for DATE_AND_TIME.
Do I need to configure the system clock before using SFC 1 READ_CLK?
No configuration is required to call SFC 1; it always returns whatever the CPU clock currently holds. The delta between two consecutive calls is correct even if the wall-clock time is wrong, but on a fresh CPU without a battery the clock may be 1990-01-01 after power-up, so the first delta after a power loss is unreliable until your startup logic initializes the "previous" timestamp.
How accurate is the pulse timing if I read the clock in OB1?
Accuracy is limited by the OB1 cycle time: a 10 ms OB1 cycle gives roughly +/-10 ms of jitter on the measured interval. For 1 Hz pulses this is acceptable; above about 20 Hz the jitter becomes a significant fraction of the period. Use OB40 (hardware interrupt) instead - reaction times on S7-300 DI modules are typically below 1 ms.
What is the difference between DATE_AND_TIME and DTL?
DATE_AND_TIME (DT) is the legacy 8-byte BCD format used on S7-300/400 with 1-second resolution. DTL is the 16-byte format on S7-1500/1200 that includes nanosecond resolution. Choose DTL on S7-1500 for native arithmetic; on S7-300 you typically pair DT with a high-speed counter for sub-second timing.
Why is the first measured interval always wrong after a CPU restart?
Because the "previous" timestamp is still at its initial value (1990-01-01 or 1970-01-01) and the current clock is at the real date, the difference is a multi-decade garbage number. Run a first-scan path in OB100 that sets a flag; the FB uses that flag to copy the first edge's timestamp into the previous slot and return a delta of zero, so the second edge produces the first valid interval.