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.
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
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
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_Alarm1throughI1.7_Alarm16in the default OB1 process image - Shared data block
DB_Statswith arrayAlarmCnt[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.
- 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.
-
Confirm the pulse width. Add a one-shot monitor in the FB output (
oEdgePulse) and verify with a VAT / trend thatoEdgePulseis TRUE for one OB1 scan only. -
Test the negative edge. Drive a tag from TRUE to FALSE and confirm the F_TRIG or the manual
Q_Negoutput is TRUE for one scan. - 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.