Resolving SCL Rising Edge Detection Failures on Siemens S7

David Krause15 min read
SiemensTIA PortalTroubleshooting
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

Problem Statement

Engineers migrating from LAD/FBD to SCL frequently encounter a counter or edge-detection block that increments only occasionally, drops pulses, or works correctly in PLCSIM and then fails in the real CPU. The classic symptom pattern on SIMATIC S7-300, S7-400, S7-1200, and S7-1500 looks like this:

  • IF Trigger AND NOT Trigger_OLD THEN Count := Count + 1; END_IF; Trigger_OLD := Trigger;
  • The counter increments a few times and then stops, or it never increments at all.
  • A parallel LAD implementation with a |P| (positive edge) contact in OB1 works perfectly.
  • Hardware diagnostics show the input is toggling, but the SCL routine never sees the edge.

This article documents the two root causes that produce this exact behavior, the engineering rules that prevent it, and four field-proven solutions with complete, compilable code for STEP 7 V5.x, STEP 7 (TIA Portal), and the S7-1200/1500 firmware families.

Root Cause 1: TEMP Variables Are Cleared Every Call

The single most common defect in SCL edge detection is the choice of memory area for the edge-storage bit. The pattern

IF Trigger AND NOT Trigger_OLD THEN
    Count := Count + 1;
END_IF;
Trigger_OLD := Trigger;

requires Trigger_OLD to retain its value between two OB1 scans. In STEP 7, the lifetime of a variable is dictated by its declaration block, not by its data type:

Declaration Area Keyword Retention Across OB1 Cycles Suitable for Edge Bit?
FC temporary VAR_TEMP No - re-initialized to 0 every call No
FB static VAR (STAT) Yes - stored in instance DB Yes (preferred)
Global memory MERKER (M) / M_BIT Yes - retained across cycles Yes (acceptable)
Global DB DB / DBB / DBX Yes - retained across cycles Yes (acceptable)
OB1 temp VAR_TEMP in OB1 No - cleared at OB1 start No

If Trigger_OLD is declared in the VAR_TEMP section of an FC, the compiler allocates it on the local stack. Each time the FC is called - even if called twice in the same OB1 - the stack frame is reconstructed and all TEMP variables are zeroed. The result: Trigger_OLD is 0 on every scan, the condition NOT Trigger_OLD is therefore always true when Trigger is true, and the counter only increments correctly when the SCL block is called at exactly the moment the input toggles. In practice this manifests as the "counts a few times then stops" symptom described in the field reports.

Engineering rule: Never place an edge-memory bit, a one-shot, a debounce timer preset, or any state that must survive a scan cycle in VAR_TEMP. Use VAR in an FB (which lands in the instance DB), a global MERKER/M bit, or a tag in a global DB.

Root Cause 2: OB1 Scan Time Exceeds Pulse Width

Even when the edge bit is stored in the correct memory area, a second defect silently destroys pulses. S7 CPUs execute OB1 cyclically. The maximum OB1 scan time on the affected units typically runs 1-10 ms in a lightly loaded configuration, but can stretch to 50-200 ms when KNX/NET communication, Web server, OPC UA, or HMI update tasks are active.

Consider an S0 energy meter output: the S0 specification (IEC 62053-31) defines a pulse whose ON-time is at least 30 ms and whose OFF-time is at least 30 ms, for a minimum period of 60 ms at 1000 pulses per kWh. The S7 input module samples the S0 line at its hardware input filter, typically 0.1-3 ms on S7-1200/1500 digital inputs. If the OB1 scan time is longer than the ON-time of the pulse, the SCL block may sample the input only while it is high or only while it is low - the rising edge is then observed every other pulse, or never.

Pulse Source Typical ON Time Typical Period Min OB1 Scan to Capture Edge
S0 output (1000 imp/kWh) 30-40 ms 60-90 ms < 30 ms
S0 output (100 imp/kWh) 30-90 ms 360 ms+ < 30 ms
Reed relay flow meter 50-200 ms 0.5-30 s < 50 ms
Fast encoder (1 kHz) 0.5 ms 1 ms < 0.5 ms (impossible in OB1)
Proximity sensor, mechanical bounce 0.5-5 ms 10-1000 ms 0.1-1 ms + debounce

The Hardware Diagnostics (Online > Diagnostics > Scan Cycle Time) reported a longest scan time of 4 ms in the original case, which is below the 40 ms S0 pulse width, so this was not the limiting factor in that specific installation. The TEMP-variable defect was the primary cause. In heavier-loaded S7-1500 systems with PROFINET IRT, OPC UA server, and Web API enabled, however, the OB1 scan time can routinely exceed 50 ms and pulses shorter than that are simply lost.

Solution 1: Promote the Edge Bit to STAT, M, or DB

The minimal, drop-in fix is to relocate the edge-storage bit to a memory area that survives the call.

Option A - Global MERKER bit (quickest fix)

Reserve a free M bit, for example M10.0, and use it as the edge storage. This works in every S7-300/400/1200/1500 and requires no instance DB.

// FC "Pulse_Count" - calling from OB1
IF "DI_S0_Trigger" = TRUE AND "Edge_Trig_Old" = FALSE THEN
    "Pulse_Count" := "Pulse_Count" + 1;
END_IF;
"Edge_Trig_Old" := "DI_S0_Trigger";

Symbol table entries:

  • DI_S0_Trigger = I0.0 (BOOL) - the digital input wired to the S0 output
  • Edge_Trig_Old = M10.0 (BOOL) - edge storage bit, lives in the MERKER area
  • Pulse_Count = MW12 / DB1.DBD0 (DINT) - the counter

Option B - Symbol-named M bit (TIA Portal)

In TIA Portal, declare PLC tags with symbolic names and reference them by symbol. The compiler will not let you read a TEMP bit by symbol because TEMP variables do not have a global address.

IF "Tag_Trigger" AND NOT "Tag_Trigger_Old" THEN
    "Tag_Pulse_Count" := "Tag_Pulse_Count" + 1;
END_IF;
"Tag_Trigger_Old" := "Tag_Trigger";

Option C - Global DB tag (recommended for production code)

Create a global DB called DB_Edge with tags Trig, Trig_Old, and Count. Global DBs are retentive on the S7-1500 by default, so the counter survives a CPU restart (verify with the Retain column in the DB editor).

IF "DB_Edge".Trig AND NOT "DB_Edge".Trig_Old THEN
    "DB_Edge".Count := "DB_Edge".Count + 1;
END_IF;
"DB_Edge".Trig_Old := "DB_Edge".Trig;

Solution 2: Encapsulate in a Function Block with Instance DB

For reusable counting and edge logic, the canonical Siemens pattern is an FB with VAR (static) members. The compiler allocates the FB's STAT area in the instance DB, and the variables are guaranteed to retain their values between calls.

FUNCTION_BLOCK "FB_Pulse_Count"
{ S7_Optimized_Access := 'TRUE' }
VERSION : 0.1
VAR_INPUT
    Trig : BOOL;            // S0 input, sampled in OB or interrupt
    Reset : BOOL;           // rising edge resets Count to 0
END_VAR
VAR
    Trig_Old : BOOL;        // edge storage - lives in instance DB
    Count : DINT;           // pulse counter
    Reset_Old : BOOL;       // edge storage for Reset
END_VAR
VAR_TEMP
    info : ARRAY[0..19] OF BYTE;   // reserved by Siemens
END_VAR
BEGIN
    // Rising-edge detect on Trig
    IF Trig AND NOT Trig_Old THEN
        Count := Count + 1;
    END_IF;
    Trig_Old := Trig;

    // Rising-edge detect on Reset
    IF Reset AND NOT Reset_Old THEN
        Count := 0;
    END_IF;
    Reset_Old := Reset;
END_FUNCTION_BLOCK

Call the FB once in OB1 with a unique instance DB, for example DB_PulseCnt:

// OB1
"DB_PulseCnt"(Trig := "DI_S0_Trigger",
              Reset := "DI_Reset_PB");

Why this is the right pattern:

  • Trig_Old, Reset_Old, and Count all live in VAR, mapped to the instance DB. They are guaranteed to retain between OB1 scans.
  • The FB is reusable - instantiate it again for a second S0 input with a different instance DB.
  • With S7_Optimized_Access := 'TRUE' (default on S7-1200/1500) the compiler hides the absolute addresses and you cannot accidentally alias two counters to the same byte.
  • Retentivity is configured per-instance on the S7-1500: open the instance DB, set Count and Reset_Old to Retain, leave Trig_Old and Reset_Old as Non-retain.
Why not just put the code in OB1 directly? You can, and the original poster eventually did, by calling a single FC from OB1 and putting the edge bit in a global M or DB. The FB is preferred because the compiler enforces scoping and prevents one engineer's counter from clobbering another engineer's M bit.

Solution 3: Hardware Interrupt OB for Sub-Millisecond Pulses

When the S0 pulse is short (e.g. fast encoders, 1 kHz or more), the only safe strategy is to capture the rising edge in a hardware interrupt OB. The CPU enters the interrupt the moment the input edge fires, regardless of the OB1 scan time.

STEP 7 V5.x configuration

  1. Open the digital input module in HW Config.
  2. Right-click the input channel (e.g. I 0.0) and select Properties.
  3. Activate Hardware interrupt and choose Rising edge.
  4. Assign a free hardware interrupt OB, e.g. OB40.

TIA Portal configuration

  1. Open Devices & Networks and select the DI module.
  2. Open the channel properties for the input connected to the S0 line.
  3. Under Inputs > Hardware interrupts, enable Rising edge.
  4. Assign the hardware interrupt to OB40 (or any OB >= 40).

OB40 SCL body

// OB40 - hardware interrupt on rising edge of S0 input
IF "DB_Edge".Trig AND NOT "DB_Edge".Trig_Old THEN
    "DB_Edge".Count := "DB_Edge".Count + 1;
END_IF;
"DB_Edge".Trig_Old := "DB_Edge".Trig;

OB40 runs to completion before OB1 resumes, so the counter increments once per pulse. The disadvantage: OB40 must be short (no communications, no heavy math) to avoid starving OB1.

Solution 4: Cyclic Interrupt OB

For pulses that are too fast for OB1 but slow enough that a 1-5 ms cyclic interrupt will catch them, configure OB30-OB38 as a cyclic interrupt OB. The CPU calls the OB at a fixed interval, decoupled from the OB1 scan.

Configuration steps (TIA Portal)

  1. Project tree > Program blocks > Add new block > Organization block > OB30.
  2. Open OB30 properties and set Cycle time to, e.g., 2 ms (minimum on S7-1500; 1 ms on S7-1200 with firmware >= 4.4).
  3. Ensure the cyclic OB is started at runtime, e.g. in OB100 (warm restart):
    // OB100
    SET;
            "Cycle_OB30" := TRUE;   // only required for S7-300/400; auto-started on S7-1200/1500

OB30 SCL body

// Cyclic 2 ms - safely captures a 30 ms S0 pulse
IF "DI_S0_Trigger" AND NOT "DB_Edge".Trig_Old THEN
    "DB_Edge".Count := "DB_Edge".Count + 1;
END_IF;
"DB_Edge".Trig_Old" := "DI_S0_Trigger";

Selection rules for the OB timebase

Shortest Input Pulse Recommended OB Recommended Cycle
> 100 ms OB1 OB1 scan < 30 ms (rule of thumb)
10-100 ms OB30 (cyclic) 1-5 ms
1-10 ms OB40 (HW interrupt) edge-triggered
< 1 ms OB40 + counter HSC use high-speed counter

Using the Built-In Positive Edge Operator (RE and FP)

For LAD/FBD users, the |P| contact in the original poster's parallel test is the safest edge operator. SCL has an equivalent - the standard IEC 61131-3 edge function blocks R_TRIG (rising) and F_TRIG (falling) live in the Standard library > IEC Timer/Counter or the System_Info blocks. The S7-1500 firmware adds the inline operators &amp; for edge detection on tags:

// S7-1500 inline edge operator (firmware >= V1.0)
IF "Tag_Trigger" AND NOT "Tag_Trigger_Old" THEN
    // older syntax; works on all firmware
END_IF;

The S7-1500 also supports a built-in FP (Flanke Positiv) inline call:

// S7-1500 / TIA Portal V16+ inline edge
"FP_DB"(CLK := "Tag_Trigger", Q := "Tag_Edge_Pulse");
IF "Tag_Edge_Pulse" THEN
    "DB_Edge".Count := "DB_Edge".Count + 1;
END_IF;

The FP_DB instance stores its edge bit in the instance DB by definition, eliminating the TEMP defect entirely. This is the recommended pattern on S7-1500.

Why Parallel LAD Worked and SCL Did Not

The user observed that a |P| counter in OB1 counted correctly while the SCL version did not. The |P| operator stores its edge bit in a hidden bit of the M or DB area, never in TEMP. The SCL version, when written with the edge bit in VAR_TEMP, was effectively recreating the edge bit on every call. In one specific call of the FC, Trigger_OLD happened to be 1 from a previous evaluation that cycle (because OB1 ran the FC twice in the same scan for some reason - the original FC was called from multiple points), so the second call saw NOT Trigger_OLD = FALSE and the counter did not increment. The M/STAT/DB fix restores deterministic single-increment-per-edge behavior.

Verification & Commissioning Procedure

  1. Open the project in TIA Portal / STEP 7 and go online with the CPU.
  2. Open the instance DB or MERKER in Monitor / Modify and force Trig_Old := FALSE and Count := 0.
  3. Force the input DI_S0_Trigger := TRUE for at least one OB1 cycle, then back to FALSE. Verify that Count = 1 and Trig_Old = FALSE.
  4. Repeat the force-TRUE/force-FALSE sequence 10 times. Verify that Count = 10 exactly. If Count is 0, 5, or any multiple other than 10, the edge bit is still in TEMP or the OB1 scan is missing the pulse.
  5. Check Online &amp; Diagnostics &gt; Cycle time and confirm the longest OB1 scan is below 1/3 of the shortest input pulse.
  6. For OB30 / OB40 solutions, open the online block and confirm that the OB ran at the expected rate (cyclic OB: OB start information shows timestamps; HW interrupt OB shows the assigned event).

Troubleshooting Matrix

Symptom Likely Cause Diagnostic Step Fix
Counter never increments Edge bit in TEMP Cross-reference Trig_Old - is the address in L-stack? Move to STAT / M / DB
Counter increments once, then stops Edge bit in TEMP, FC called twice per scan Search for FC call sites; reduce to one call Move to STAT / M / DB
Counter misses every other pulse OB1 scan > pulse width Read longest OB1 scan time Use OB30 (cyclic) or OB40 (HW interrupt)
Counter increments continuously while input held HIGH Edge bit forced by something, or FB called inside the IF Cross-reference Trig_Old; check for any other writer Use unique STAT tag per FB instance
Counter resets on CPU stop-run DB not retentive, M not retentive Check DB Retain property, M area Retain property Set the Count tag Retain = 'TRUE' on the S7-1500
Counter works in PLCSIM, fails in real CPU Real input filter masks short pulse Reduce DI input filter to 0.1 ms in module config Adjust filter in device configuration
Count = 0 in PLCSIM, fine in real CPU PLCSIM doesn't update the input I area the same way Force the input from PLCSIM API No code change; test method only

Common Pitfalls and Field Notes

  • Reusing the same M bit across multiple FBs: two FBs sharing M10.0 will crosstalk. Either give each a unique M range or, better, use the FB's instance DB.
  • Optimized access and absolute addresses: on the S7-1500 with optimized block access, the compiler reorders the data. Symbolic access is mandatory; the absolute address shown in the symbol table is informational only.
  • Retain vs non-retain on the S7-1500: the instance DB is non-retain by default. Open the instance DB and set Retain on Count if you need to survive a power cycle.
  • Knock-out of MERKER on the S7-300/400: the entire MERKER area is non-retain by default. Use a global DB (which is retain by default on the S7-300/400) for any data that must survive STOP-RUN.
  • Input filter on S7-1200/1500 SM modules: the default is typically 6.4 ms, which silently filters out anything shorter. Open the DI module properties > Inputs > Input filter and set to 0.1 ms for fast pulses.
  • Placing the FB in OB1 vs OB35: the FB itself does not impose a call interval - it depends entirely on the OB that hosts it. Move the call from OB1 to OB30 to change the effective sampling rate.
  • Reading the input twice in the same OB1: a single read of %I0.0 per scan is the correct behavior. Two reads can produce two increments of Count for one pulse if the second read happens to land on the opposite phase due to scan time variation.

Complete Working Example: S7-1500 with OB30 + FB

This is the recommended production pattern for an S0 energy meter on an S7-1511 with a 40 ms pulse.

// FB "FB_S0_Counter"
{ S7_Optimized_Access := 'TRUE' }
VERSION : 0.1
VAR_INPUT
    Trig : BOOL;
    Reset : BOOL;
END_VAR
VAR RETAIN
    Count : DINT;            // retain across STOP-RUN and power cycle
END_VAR
VAR
    Trig_Old : BOOL;         // non-retain; rebuilt every restart
    Reset_Old : BOOL;        // non-retain
END_VAR
VAR_TEMP
    info : ARRAY[0..19] OF BYTE;
END_VAR
BEGIN
    IF Trig AND NOT Trig_Old THEN
        Count := Count + 1;
    END_IF;
    Trig_Old := Trig;

    IF Reset AND NOT Reset_Old THEN
        Count := 0;
    END_IF;
    Reset_Old := Reset;
END_FUNCTION_BLOCK
// OB30 - cyclic 2 ms
"DB_S0_Counter_1"(Trig := "DI_S0_Pulse",
                 Reset := "DI_Reset_Counter");

Commissioning checklist:

  1. Set DI module input filter to 0.1 ms (or the lowest available).
  2. Confirm OB30 cycle time is 2 ms in HW config > Properties > Cycle.
  3. Wire a pushbutton simulator producing 1 Hz / 50% duty cycle to the input.
  4. Watch DB_S0_Counter_1.Count online. Expect +1 per second.
  5. Verify the S0 meter's pulse indicator LED flashes once per increment.

FAQ

Why does my SCL rising edge work in PLCSIM but fail on the real CPU?

PLCSIM uses a simulated digital input that toggles perfectly aligned to the OB1 scan, so the edge is always visible. On a real CPU, the input filter of the DI module (often 6.4 ms default on SM 1223 / SM 1521) suppresses short pulses, and the OB1 scan time can land between transitions. Lower the input filter to 0.1 ms in the device configuration and verify the longest OB1 scan is < 1/3 of the shortest pulse width.

Can I use VAR_TEMP at all for edge detection?

Only if the edge bit is consumed in the same call where it is written and never needs to be re-read in the next call. That is true for one-shot debouncing inside a single FB, but never for the classic Trigger / Trigger_OLD pattern. For a true edge memory use VAR (STAT), M, or a global DB tag.

What's the difference between R_TRIG and the inline FP operator on the S7-1500?

R_TRIG is an IEC 61131-3 standard function block that you instantiate as a single-instance DB; the edge bit is stored inside that instance DB. The inline FP call "FP_DB"(CLK := ...) on the S7-1500 is syntactic sugar that the compiler expands into the same R_TRIG instance. Both are safe by construction because they cannot be placed in TEMP.

Do I need a hardware interrupt for an S0 energy meter pulse?

Not normally. S0 pulses are at least 30 ms wide. A 2 ms cyclic OB30 will catch them reliably, and OB1 will too if the longest scan is < 10 ms. Reserve OB40 hardware interrupts for sub-millisecond pulses (encoders at > 500 Hz) where OB30's 1 ms minimum is not fast enough.

Why does my counter reset every time I stop and restart the CPU?

Either the counter is in the MERKER area (non-retain on S7-300/400) or the instance DB is not marked as retain. On the S7-1500, open the instance DB, click on the Count tag, and set the Retain column to Set in IDB. On S7-300/400, declare the count tag in a global DB (which is retain by default) or in the FB's STAT section and configure the instance DB as retain in HW Config.

Back to blog