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.
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
CountandReset_Oldto Retain, leaveTrig_OldandReset_Oldas Non-retain.
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
- Open the digital input module in HW Config.
- Right-click the input channel (e.g.
I 0.0) and select Properties. - Activate Hardware interrupt and choose Rising edge.
- Assign a free hardware interrupt OB, e.g. OB40.
TIA Portal configuration
- Open Devices & Networks and select the DI module.
- Open the channel properties for the input connected to the S0 line.
- Under Inputs > Hardware interrupts, enable Rising edge.
- 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)
- Project tree > Program blocks > Add new block > Organization block > OB30.
- 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).
- 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 & 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
- Open the project in TIA Portal / STEP 7 and go online with the CPU.
- Open the instance DB or MERKER in Monitor / Modify and force
Trig_Old := FALSEandCount := 0. - Force the input
DI_S0_Trigger := TRUEfor at least one OB1 cycle, then back to FALSE. Verify thatCount = 1andTrig_Old = FALSE. - Repeat the force-TRUE/force-FALSE sequence 10 times. Verify that
Count = 10exactly. IfCountis 0, 5, or any multiple other than 10, the edge bit is still in TEMP or the OB1 scan is missing the pulse. - Check Online & Diagnostics > Cycle time and confirm the longest OB1 scan is below 1/3 of the shortest input pulse.
- 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.0will 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
Countif 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.0per scan is the correct behavior. Two reads can produce two increments ofCountfor 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:
- Set DI module input filter to 0.1 ms (or the lowest available).
- Confirm OB30 cycle time is 2 ms in HW config > Properties > Cycle.
- Wire a pushbutton simulator producing 1 Hz / 50% duty cycle to the input.
- Watch
DB_S0_Counter_1.Countonline. Expect +1 per second. - 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.