Measuring Pulse Interval Time in Siemens S7 PLCs Using DTL

David Krause13 min read
S7-300SiemensTutorial / How-to
Licensed PE Working through this on a live machine? A Maine-licensed engineer can take it from here — included with IMD hardware, by the hour for everything else. Book an engineer

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).
Clock status matters. Even if the CPU clock is not set to the correct wall-clock time, the difference between two SFC 1 READ_CLK calls is unaffected. However, after a power loss the buffered clock continues only if a battery is present or the CPU is configured for clock retention; otherwise the first delta will be meaningless until initialization runs.

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.

  1. 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.
  2. 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.
  3. 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.
Pulse Input (DI) OB40 Hardware Interrupt SFC 1 READ_CLK / RD_SYS_T FB "PulseInterval" DT / DTL subtraction-> TIME result (ms) MW100 / DB result

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)
For field maintenance, prefer the open-source "DT_to_Time" / "Time_to_DT" library FCs available with the STEP 7 standard library (e.g., the IEC function blocks for date/time conversion). Avoid hand-rolling the BCD conversions - the leap-year and 30/31-day-month rules cause silent off-by-one errors that are painful to diagnose on a running line.

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.

Watch for divide-by-zero. On the first valid edge the delta is non-zero, but if a downstream function (RPM, frequency) is computed every cycle and the delta happens to be 0 (e.g., two pulses inside a 1 ms window on a 1 ms-resolution clock), the division will raise an OB121 programming error. Gate the divisor with IF DeltaMs > 0 THEN ... END_IF;.

Verification and Commissioning

  1. Force a pulse source (function generator, switch, sensor). Start at 1 Hz and confirm DeltaMs reads approximately 1000.
  2. 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.
  3. Power-cycle the CPU. Confirm that OB100 forces FirstRun = TRUE and the first edge returns delta = 0 (or a small value if OB40 already captured a stale timestamp).
  4. 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.
  5. Use the online SCL/STL monitor to break on the FB exit; verify PreviousDTL matches the previous CurrentDTL after 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.

Back to blog