Positive and Negative Edge Detection in SCL for Siemens S7

David Krause12 min read
SiemensTIA PortalTutorial / 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

Positive and Negative Edge Detection in SCL for Siemens S7

Structured Control Language (SCL) on Simatic S7-300, S7-400, S7-1200, and S7-1500 controllers does not expose a P or N contact the way ladder logic (LAD) does. Engineers migrating counters, alarm-latch logic, or one-shot triggers from LAD/FBD to SCL must re-implement the edge evaluator by hand. This reference covers the underlying boolean logic, the instance-bit pattern that makes multi-instance FBs safe, complete FB/FC source code, the IEC 61131-3 R_TRIG / F_TRIG blocks available in TIA Portal, and a worked alarm-counting application. Code samples compile in both STEP 7 V5.x (Simatic Manager) and STEP 7 V16+ (TIA Portal).

1. Why SCL Has No Native Edge Operator

Ladder diagram offers scan-synchronous edge contacts: --(P)-- for a positive (rising) edge and --(N)-- for a negative (falling) edge. The SCL grammar in IEC 61131-3 defines no equivalent lexical element. Edge detection in SCL is therefore a programmer responsibility, expressed in standard boolean statements and a storage bit that survives the PLC scan.

Two storage locations are common:

  • Instance DB static variable — preferred for FB-based code; each FB instance keeps its own previous-state bit, preventing cross-talk between calls.
  • Global M-bit (Merker) — acceptable for single-use, non-reusable code. Sharing an M-bit between two edge evaluations silently produces a single combined edge.
Scan semantics. SCL code in OB1 executes top-to-bottom once per scan. An "edge" is therefore defined as a state change between the value of CLK at the end of the previous scan and the value at the start of the current scan. The storage bit must be updated after the comparison, never before.

2. Boolean Foundation of Edge Detection

A positive edge fires for one scan when CLK transitions 0 → 1. The output expression is the conjunction of the new CLK state and the inverse of the stored previous state:

Q_pos = CLK AND NOT M    followed by    M := CLK

A negative edge is symmetric:

Q_neg = (NOT CLK) AND M    followed by    M := CLK

Both share the same previous-state update. The four possible state transitions are tabulated below.

Previous M Current CLK Q_pos Q_neg Meaning
0 0 0 0 idle low
0 1 1 0 positive edge
1 0 0 1 negative edge
1 1 0 0 idle high

2.1 Timing Diagram

CLK M (prev) Q_pos Q_neg scan 1 scan 2 scan 3

3. Inline Manual Edge in SCL (S7-300 / S7-400 / S7-1200 / S7-1500)

The simplest, dependency-free implementation requires one bit of static memory. In an FB, declare the bit in the static section so the instance DB owns it. In an FC, declare a TEMP variable and update it through the M-bit pattern only when no re-entrancy risk exists.

3.1 Inside an FB (recommended)

FUNCTION_BLOCK FB_Edge
VAR
    M_Pos : BOOL;   // previous state of CLK for positive edge
    M_Neg : BOOL;   // previous state of CLK for negative edge
END_VAR
VAR_INPUT
    CLK : BOOL;
END_VAR
VAR_OUTPUT
    Q_Pos : BOOL;
    Q_Neg : BOOL;
END_VAR
BEGIN
    Q_Pos := CLK AND NOT M_Pos;
    Q_Neg := (NOT CLK) AND M_Neg;
    M_Pos := CLK;
    M_Neg := CLK;
END_FUNCTION_BLOCK

Call this FB once per signal that needs an edge. Each instance keeps isolated M-bits in its instance DB, so two FBs on the same tag do not corrupt each other.

3.2 Inside an FC (single-shot, use M-bits outside)

FUNCTION FC_Edge_Alarm : INT
VAR_TEMP
    info : INT;
END_VAR
BEGIN
    info := "DB_Alarm".Count;        // load counter
    IF "Tag_Alarm" AND NOT M10.0 THEN
        info := info + 1;            // positive-edge increment
    END_IF;
    M10.0 := "Tag_Alarm";
    "DB_Alarm".Count := info;
    FC_Edge_Alarm := info;
END_FUNCTION
M-bit hygiene. Reserve exclusive M-byte ranges for every FC that uses edge memory. Document the allocation in the symbol table comment field to prevent collisions during commissioning.

4. Reusable Multi-Instance FB for Edge + Counter

The pattern below bundles the edge bit and a counter inside one FB so the caller writes a single line per alarm. This is the recommended approach for machines that have to count occurrences of ten or more digital events.

FUNCTION_BLOCK FB_AlarmCounter
VAR
    sM_Trig   : BOOL;          // previous CLK state
    sCount    : DINT;          // event counter
END_VAR
VAR_INPUT
    iCLK      : BOOL;          // alarm input
    iReset    : BOOL;          // rising reset
    sM_Reset  : BOOL;          // previous iReset state
END_VAR
VAR_OUTPUT
    oCount    : DINT;
    oEdgePulse: BOOL;          // one-scan pulse
END_VAR
BEGIN
    // reset on positive edge of iReset
    IF iReset AND NOT sM_Reset THEN
        sCount := 0;
    END_IF;
    sM_Reset := iReset;

    // alarm positive edge
    oEdgePulse := iCLK AND NOT sM_Trig;
    IF oEdgePulse THEN
        sCount := sCount + 1;
    END_IF;
    sM_Trig := iCLK;

    oCount := sCount;
END_FUNCTION_BLOCK

Instantiate in OB1:

// instance DB names below are project-specific
"idb_Alarm001"(iCLK := "I0.0_Alarm1", iReset := "I0.1_Reset", oCount => "DB_Stats".Alarm001Cnt);
"idb_Alarm002"(iCLK := "I0.2_Alarm2", iReset := "I0.1_Reset", oCount => "DB_Stats".Alarm002Cnt);

5. TIA Portal: R_TRIG and F_TRIG (IEC 61131-3)

From S7-1200 firmware V2.0 and S7-1500 onward, TIA Portal exposes the IEC standard blocks R_TRIG (rising-edge detector) and F_TRIG (falling-edge detector). They live in the instruction list under Basic Instructions → Bit Logic Operations → Edge detection. Both expect an instance DB or can be used as multi-instances inside a parent FB.

Block Input Output Storage Function
R_TRIG (FB) CLK : BOOL Q : BOOL Instance DB, static CLK_old Q := CLK AND NOT CLK_old; CLK_old := CLK
F_TRIG (FB) CLK : BOOL Q : BOOL Instance DB, static CLK_old Q := (NOT CLK) AND CLK_old; CLK_old := CLK
---|P|--- / ---|N|--- (LAD contacts) <bit> RLO Implicit per network Same as above, ladder syntax

Usage in SCL under TIA Portal:

// "idb_R" and "idb_F" are instance DBs of R_TRIG / F_TRIG
// declared in the FB static section for multi-instance use
"idb_R"(CLK := "Tag_StartButton", Q => _startPulse);
"idb_F"(CLK := "Tag_StopButton",  Q => _stopPulse);

IF _startPulse THEN
    "DB_Cmd".Run := TRUE;
END_IF;
IF _stopPulse THEN
    "DB_Cmd".Run := FALSE;
END_IF;

The official Siemens manual collection documents both instructions in the Basic Instructions → Bit Logic Operations section (see TIA Portal S7-1200 manual collection — Positive and Negative Edge Instructions).

6. Comparing the Two Approaches

Criterion Manual FB/FC pattern R_TRIG / F_TRIG (TIA)
CPU families supported S7-300, S7-400, S7-1200, S7-1500 S7-1200 (FW ≥2.0), S7-1500
Editor STEP 7 V5.x and TIA Portal TIA Portal only
Library overhead None — boilerplate code One instance DB per call
Multi-instance inside parent FB Yes, declare static bit Yes, drop the block into the static section
Diagnostic visibility Full — bit is in your own DB Internal — bit is hidden in system FBs
Cycle-time impact Negligible (two ANDs + one assign) Negligible (same instructions internally)
Vendor validation Field-proven pattern across Siemens training Documented in the TIA Portal manual

7. Worked Application: Counting Alarms on a Packaging Line

Counting how many times each alarm fires during a shift, so the line manager can rank downtime causes, maps cleanly onto the FB_AlarmCounter template in Section 4. The pattern is also the right solution for SKU-changeover counters, reject counters on a vision system, and operator-button-press tallies.

7.1 Hardware and Tag List

  • CPU 315-2 PN/DP (6ES7315-2EH14-0AB0) with firmware V3.3
  • 16-DI SM 321 (6ES7321-1BH02-0AA0) for alarm contacts
  • Symbol table entries: I0.0_Alarm1 through I1.7_Alarm16 in the default OB1 process image
  • Shared data block DB_Stats with array AlarmCnt[1..16] : DINT

7.2 OB1 Body (S7-300 / STEP 7 V5.5)

ORGANIZATION_BLOCK OB1
VAR_TEMP
    i : INT;
END_VAR
BEGIN
    FOR i := 1 TO 16 DO
        CASE i OF
            1:  "idb_Alarm01"(iCLK := "I0.0_Alarm1",  iReset := "I0.1_Reset",  oCount => "DB_Stats".AlarmCnt[1]);
            2:  "idb_Alarm02"(iCLK := "I0.2_Alarm2",  iReset := "I0.1_Reset",  oCount => "DB_Stats".AlarmCnt[2]);
            // ... continue through alarm 16
        END_CASE;
    END_FOR;
END_ORGANIZATION_BLOCK

With a CASE ladder the compiler emits only the active branch, so unused alarm slots consume no scan time beyond the FOR overhead.

7.3 TIA Portal Equivalent (S7-1500 / SCL)

// inside FB_Machine (multi-instance DB: idb_Machine)
"idb_Alarm01"(iCLK := "I0_Alarm01", iReset := "I_Reset", oCount => _cnt01);
"idb_Alarm02"(iCLK := "I0_Alarm02", iReset := "I_Reset", oCount => _cnt02);
// ... array-based loop (TIA V17+)
FOR _i := 1 TO 16 DO
    "idb_Alarm"[_i](
        iCLK   := "i_Alarm"[_i],
        iReset := "i_Reset",
        oCount => "DB_Stats".AlarmCnt[_i]);
END_FOR;

For arrayed calls the FB must support a parameterized instance DB; on S7-1500 this is selected by setting the FB's Optimized block access property and assigning a multi-instance or a parameter instance from the calling FB's static section.

8. Edge Evaluation in Time-Driven OBs

Edges are valid in any OB whose execution cycle is longer than the input filter time of the digital input module. SM 321 digital inputs apply a configurable input delay of typically 0.1 ms to 20 ms. If the calling OB is OB35 (cyclic interrupt at, say, 100 ms), a 1 ms alarm pulse is guaranteed to be sampled, but a 10 µs glitch on the wiring may be filtered out before OB35 ever sees it. Match the OB period to the slowest expected alarm pulse, not the fastest.

OB Trigger Typical period Edge safe? Use case
OB1 Cyclic, free-running 1–50 ms Yes Default logic, HMI commands
OB35 Cyclic interrupt 1 ms – 60 s, configurable Yes PID, fast alarms, batching
OB40 Hardware interrupt Event-driven Yes (one scan) Critical alarms, register-mark detection
OB82 / OB86 / OB121 Diagnostic / fault Event-driven Limited Use only with the system status byte, do not use for user IO

For the alarm counter pattern from Section 4, OB1 is sufficient on most packaging machines. Switch to OB35 only if the alarm signal can pulse faster than the OB1 cycle, in which case the input must also be wired to a hardware-interrupt-capable DI channel and the OB40 priority raised.

9. Common Pitfalls

9.1 M-bit Sharing Between Calls

Two FCs that both use M10.0 as the previous-state bit will produce a single combined edge for both inputs. The fix is per-instance static storage, an M-byte range per FC, or migration to R_TRIG / F_TRIG with multi-instance declaration.

9.2 Order of Statements

Always assign M := CLK after reading the value. Reversed order creates a tautology where the edge is never detected.

// WRONG — always reads CLK into M before the comparison
M := CLK;
Q := CLK AND NOT M;

// RIGHT
Q := CLK AND NOT M;
M := CLK;

9.3 Power-Fail Restart Behaviour

The previous-state bit lives in a volatile DB by default. If the controller loses power, OB100 (restart) sees M = 0, so the first OB1 scan can produce a spurious positive edge on any input that is already TRUE at power-up. Mitigate by initialising M in OB100 with the actual PLC input value:

// OB100 / Startup
FOR i := 1 TO 16 DO
    "idb_Alarm"[i].sM_Trig := "i_Alarm"[i];
END_FOR;

9.4 Scan Time Exceeds Input Filter

With a heavily loaded OB1 (40 ms) and 0.1 ms input filter, every short pulse is captured. But a 0.1 ms filter on a 40 ms scan means single-sample noise can also be latched. Configure the input delay (DI module properties in HW Config / device configuration) to a value that matches the mechanical contact bounce, typically 3 ms to 10 ms for relays, 0.1 ms for solid-state sensors.

9.5 Optimised Block Access Side Effects

S7-1500 FBs with the "Optimized block access" flag enabled store static variables in a slot layout that the compiler may reorder. Edge bits remain functionally correct but the slot in the instance DB can change between firmware updates. Never access the bits by absolute address; use the symbolic name only.

10. Verification Procedure

After downloading the program and before running production, perform a four-step verification.

  1. Force a known pulse. In the watch table set the alarm tag TRUE, observe that the corresponding counter increments by exactly one. Set it FALSE, set it TRUE again, observe another increment. Repeat ten times and confirm the counter reads 10.
  2. Confirm the pulse width. Add a one-shot monitor in the FB output (oEdgePulse) and verify with a VAT / trend that oEdgePulse is TRUE for one OB1 scan only.
  3. Test the negative edge. Drive a tag from TRUE to FALSE and confirm the F_TRIG or the manual Q_Neg output is TRUE for one scan.
  4. Power-cycle the CPU. Pull the power for 5 s with an alarm already TRUE. After restart, verify that the counter did not increment spuriously. If it did, the OB100 init in Section 9.3 is missing.

For a unit-test rig, the open-source example at jorgemgn/scl-edge on GitHub provides SCL source for both edge directions and can be compiled in PLCSIM Advanced (S7-1500) or PLCSIM (S7-300/400) for offline regression testing.

11. Migrating Existing Ladder Edge Logic to SCL

A common maintenance task is to convert a working LAD network to SCL. The mapping rules are:

LAD element SCL equivalent Note
---|P|--- (positive edge contact) IF Tag_X AND NOT M_X THEN ... END_IF; M_X := Tag_X; Inline form
---|N|--- (negative edge contact) IF (NOT Tag_X) AND M_X THEN ... END_IF; M_X := Tag_X; Inline form
---(P)--- / ---(N)--- coil Output of R_TRIG / F_TRIG instance Cleanest for repeated logic
Edge with SET/RESET FB with latching static BOOL Matches the alarm counter pattern

When the LAD source used global M-bits, rename them to clearly project-scoped symbols (for example s_M_EdgeAlarm01 in a dedicated EdgeBits DB) before the conversion. A naming convention prevents the all-too-common commissioning bug where a new line of code accidentally re-uses the same M-bit.

12. Frequently Asked Questions

Does SCL have a built-in edge operator like LAD's P/N contacts?

No. SCL (IEC 61131-3 structured text) has no lexical edge operator. You must implement the edge manually with a previous-state bit or use the IEC blocks R_TRIG and F_TRIG provided in TIA Portal's Basic Instructions.

Where should the previous-state bit be stored?

In the static section of an FB (multi-instance safe), in a dedicated global M-byte range (single-use only), or in the instance DB of an R_TRIG / F_TRIG block. Never declare it as VAR_TEMP inside an FC that may be called more than once per scan, as the value will be overwritten between calls.

How do I count multiple alarms in SCL?

Create one FB that contains both the edge bit and a DINT counter, expose CLK and Reset as inputs, and instantiate it once per alarm tag. The FB_AlarmCounter template in Section 4 compiles on S7-300/400 and S7-1200/1500 without changes.

Why does my counter increment by two on the first power-up?

The previous-state bit starts at zero in the volatile instance DB. When OB1 first runs, the input is already TRUE, so the comparison 0 → 1 produces a spurious edge. Initialize the bit in OB100 from the actual input value, or use R_TRIG / F_TRIG with the start-up property "Set initial state on restart" enabled.

Can I use R_TRIG inside an SCL FB as a multi-instance?

Yes. In the static section of the parent FB, drop the R_TRIG instruction; the compiler creates a local instance that the parent FB owns. You avoid the need to declare a separate instance DB for every edge.

Back to blog