1. Overview: What Edge Detection Means in an SCL Program
Edge detection is the act of converting a level-driven Boolean signal into a transition-driven one-cycle pulse. In SCL (Structured Control Language) for SIMATIC S7-1200 and S7-1500 controllers, the concept is identical to ladder logic: a rising edge (positive edge) is detected when a Boolean variable transitions from FALSE to TRUE between two evaluations, and a falling edge (negative edge) when it transitions from TRUE to FALSE. The output pulse is held for exactly one PLC scan (one OB1 cycle by default) unless the implementation deliberately stretches it.
The phrase X AND NOT X_old is the canonical manual SCL implementation. It is functionally equivalent to the built-in R_TRIG instruction documented in the SIMATIC S7-1200 manual collection, but it requires the engineer to manage the "previous value" buffer (X_old) explicitly. Understanding when the comparison is TRUE and how the buffer is updated is the central engineering problem this article solves.
The rest of this reference covers the boolean truth table, the manual implementation pattern, the built-in function blocks, memory-class selection (STAT, TEMP, VAR_GLOBAL, instance DB), cycle-time interaction, debouncing, diagnostics, and field-verification procedures.
2. The Boolean Logic Behind X AND NOT X_old
The expression X AND NOT X_old reads literally as "the current input is TRUE AND the previously stored input is FALSE". The truth table below shows every possible combination of the two operands and the resulting edge flag.
| Current X | Previous X_old | X AND NOT X_old | Interpretation |
|---|---|---|---|
| FALSE | FALSE | FALSE | Held low — no transition |
| FALSE | TRUE | FALSE | Falling edge (not detected by this expression) |
| TRUE | FALSE | TRUE | Rising edge detected — pulse this cycle |
| TRUE | TRUE | FALSE | Held high — no transition |
Two conditions must be true for the output to be TRUE:
- The signal
Xmust have beenFALSEon the previous evaluation (soX_oldisFALSE). - The signal
Xmust beTRUEon the current evaluation.
This is precisely the definition of a positive edge: a 0 → 1 transition. The complementary expression NOT X AND X_old detects the 1 → 0 (falling) transition.
3. Manual Positive Edge Implementation in SCL
The minimum viable SCL snippet contains two parts: the conditional assignment of the edge flag, and the unconditional update of X_old. Place both inside the same code block that runs every OB1 cycle.
// Manual rising-edge detection in SCL
// X : current input (BOOL, e.g. %I0.0 or symbolic tag)
// X_old : previous-cycle value (BOOL, retained between cycles)
// pos_edge : one-cycle pulse (BOOL)
IF X AND NOT X_old THEN
pos_edge := TRUE;
END_IF;
X_old := X;
X_old := X; must occur after the IF block so that the comparison sees the previous cycle's value, not the just-updated one. Reversing the order would make the edge flag perpetually FALSE.A complete function block (FB) wrapping the logic in an instance-DB-friendly form looks like this:
FUNCTION_BLOCK "fbManualEdge"
VAR
xSignal : BOOL; // input value
xOld : BOOL; // previous-cycle value
xEdge : BOOL; // one-cycle pulse output
END_VAR
BEGIN
IF xSignal AND NOT xOld THEN
xEdge := TRUE;
END_IF;
xOld := xSignal;
END_FUNCTION_BLOCK
Call the FB once per OB1 cycle from the main program (cyclic task) and read xEdge for the rising-edge-triggered actions.
4. Negative Edge Detection (Falling Edge) in SCL
Mirror the boolean expression: NOT X AND X_old is TRUE only when X has just dropped. The structure is otherwise identical.
// Manual falling-edge detection in SCL
IF (NOT X) AND X_old THEN
neg_edge := TRUE;
END_IF;
X_old := X;
For combined rising/falling detection inside a single block, branch the two conditions and use a shared X_old buffer:
IF X AND NOT xOld THEN
pos_edge := TRUE;
ELSIF (NOT X) AND xOld THEN
neg_edge := TRUE;
END_IF;
xOld := X;
5. Built-in TIA Portal Edge Instructions: R_TRIG, F_TRIG, and Their DB Variants
SIMATIC S7-1200/S7-1500 firmware ships three documented edge instructions referenced in the TIA Portal positive and negative edge instructions manual:
| Instruction | Type | Edge Direction | Persistent Storage | Notes |
|---|---|---|---|---|
R_TRIG |
FB | Rising (0 → 1) | Instance DB | Requires an instance data block; CLK input, Q output, EO status output |
F_TRIG |
FB | Falling (1 → 0) | Instance DB | Same interface as R_TRIG with reversed polarity |
R_TRIG_DB / F_TRIG_DB
|
FB | DB-managed edge | Global DB | Edge bit lives in a caller-supplied data block |
---|P|--- / ---|N|---
|
LAD contact | Both | Implicit (LAD scope) | Direct equivalent in ladder logic; not applicable to SCL source |
In SCL, call the FB with an instance DB:
// Rising-edge detection using R_TRIG
VAR
rtInstance : "R_TRIG"; // generates a system instance DB
END_VAR
BEGIN
rtInstance(CLK := X);
IF rtInstance.Q THEN
// 0 → 1 transition detected this cycle
pos_edge := TRUE;
END_IF;
END_FUNCTION_BLOCK
The CLK input accepts a Boolean signal. Internally, the FB stores the previous-cycle value in its instance data block and updates it at the end of the call; the engineer does not need to manage X_old explicitly. Output Q is TRUE for exactly one PLC scan after a rising edge is detected.
TEMP variables. Use the FB whenever the calling OB is not the cyclic OB1 (e.g. cyclic interrupt OB30, time-of-day OB10) or whenever the SCL code is inside an FB that may be called from multiple call sites.6. Memory-Class Selection: STAT, TEMP, VAR_GLOBAL, or Instance DB
The single most common cause of "my edge never fires" or "my edge fires every cycle" is the wrong memory class for X_old. Choose according to where the SCL code lives and how it is called.
| Storage Location | Lifecycle | Cross-Cycle Persistence | Recommended Use | Risk if Misused |
|---|---|---|---|---|
VAR_TEMP (TEMP) |
One call | None — re-initialized to default each call | Scratch registers only | Edge fires every cycle because X_old is reset to 0 every OB1 pass |
VAR (STAT) inside an FB |
One call instance | Persistent in instance DB | Default for FB-encapsulated edges | None when used correctly |
VAR_GLOBAL in a global DB |
Entire program | Persistent | Cross-block edges, single-instance edges | Name clashes if two blocks reuse the same global tag |
VAR_INPUT / VAR_OUTPUT
|
Call scope | None (read-only inputs) | Never for X_old
|
Compiler error (input cannot be written) |
Field rule: when writing the manual pattern, X_old must be declared as VAR (a static, instance-DB-residing variable) inside an FB, or as a VAR_GLOBAL tag in a global DB if the SCL block is a function (FC). Declaring X_old as VAR_TEMP will produce a non-functional edge detector because temporaries are overwritten with the initial value at the start of every call.
7. Edge Detection Across Different OB Contexts
The PLC samples inputs at the start of each OB1 cycle. Edge detection logic must therefore be called exactly once per OB1 cycle to remain consistent with the input image. When the calling context changes, update the design accordingly.
| Calling OB | Execution Pattern | Edge Handling |
|---|---|---|
| OB1 (main cyclic) | Every scan | Manual pattern works; R_TRIG instance DB persists between calls |
| OB30–OB38 (cyclic interrupt) | Fixed time interval | Use the same FB instance; do not duplicate X_old in a separate OB |
| OB10–OB17 (time-of-day) | Scheduled once or periodically | Manual pattern still valid if the FB instance survives between calls |
| OB40–OB47 (hardware interrupt) | Event-driven, asynchronous | Prefer HW-input edge evaluation; software edge only if event rate < scan rate |
| OB82 / OB121 / OB122 (error) | Fault path | Avoid new edge state — only diagnostics |
When an FB containing the manual edge logic is instantiated multiple times, each instance gets its own copy of X_old in its instance DB. That is correct and is the recommended pattern.
8. Ladder Logic Edge Contacts vs SCL Boolean Comparison
In ladder logic, the ---|P|--- (positive edge contact) and ---|N|--- (negative edge contact) implicitly maintain their edge state within the network, so the engineer never sees a buffer variable. Translating a ladder network to SCL exposes the buffer. The translation is mechanical:
// Ladder: ----[ ]----[P]----------------(OUT)
// means: OUT := I0.0 AND P_edge_of_I0.1
// In SCL:
IF I0_0 AND posEdge_I0_1 THEN
OUT := TRUE;
ELSE
OUT := FALSE;
END_IF;
The edge bit posEdge_I0_1 is computed by a separate R_TRIG instance or by the manual pattern. Failure to compute it on every cycle yields the same "fires once" or "fires every cycle" symptoms as incorrect memory class.
9. Hysteresis, Debounce, and Counting on Top of an Edge
A raw Boolean input can bounce for several milliseconds; in that interval the PLC will see multiple 0 → 1 transitions and produce multiple edge pulses. Counter logic built on top of X AND NOT X_old will over-count. Two remedies are standard:
-
Time-based debounce: require the input to remain
TRUEfor N milliseconds before the edge is accepted. Implement this with anIEC_TIMER(TP) and a qualifier. The edge fires only when the timer outputQtransitions toTRUE. - Hardware debounce: configure the input filter in the S7-1200 device configuration. The default input filter is 6.4 ms; raise it for noisy mechanical contacts (typical values: 1.6 ms to 12.8 ms).
// Debounced edge detection using TP timer
VAR
tDebounce : TON; // on-delay timer
tInstance : TP; // pulse timer for clean edge
END_VAR
BEGIN
tDebounce(IN := X, PT := T#20ms);
tInstance(CLK := tDebounce.Q);
IF tInstance.Q THEN
pos_edge_debounced := TRUE;
END_IF;
END_FUNCTION_BLOCK
10. Diagnostics: Cross-Reference, Watch Table, and Trace
When the edge does not behave as expected, run the diagnostics in this order:
-
Watch table: force
X, observeX_oldand the edge flag. ConfirmX_oldactually changes value after each call. If it never changes, the variable is not persistent (likely declaredVAR_TEMP). -
Cross-reference: right-click
X_oldin the project tree → "Cross-references". Confirm exactly one assignment site and one read site. Multiple writes will corrupt the edge state. -
Online & diagnostics: open the FB online and watch the
STATsection of the instance DB live. The buffer must toggle each cycle the input toggles. -
Trace: for high-speed inputs, configure a trace recording both
Xandpos_edgeat the configured OB1 cycle rate. A correctly functioning detector shows onepos_edge = TRUEpulse per 0 → 1 transition ofX.
11. Verification Procedure
After implementing either the manual pattern or the R_TRIG call, verify on the bench before deploying to production.
- Compile and download the project to the target S7-1200 (or S7-1500) CPU. Confirm the program status is RUN and no SF / BF LEDs are lit.
-
Open a watch table containing the input tag
X, the bufferX_old, and the edge flagpos_edge. Set the monitoring update rate to "On change" or 200 ms. -
Force a 0 → 1 transition on the input (either physically or with "Modify" → "Force"). Observe that
pos_edgerises toTRUEfor exactly one OB1 scan, andX_oldupdates toTRUEin the same cycle. -
Hold the input at
TRUEfor at least 10 OB1 cycles. Confirmpos_edgeremainsFALSEafter the first pulse. This validates the buffer. - Release the input and re-trigger. Confirm a second pulse appears. This validates re-triggerability.
-
Cycle power to the CPU (cold restart). Confirm the edge detector behaves identically — both manual
STATbuffers and instance-DB-residentR_TRIGbits restart with the configured initial value, so the first edge after a cold start may be lost; document this as a known characteristic if relevant.
12. Common Pitfalls and Field-Tested Counter-Measures
| Symptom | Likely Cause | Fix |
|---|---|---|
Edge flag stays FALSE
|
X_old declared as VAR_TEMP, never persists |
Move to VAR (instance DB) or to a global DB |
Edge flag stays TRUE permanently |
X_old is updated before the comparison |
Swap the order: comparison first, then X_old := X
|
| Edge fires every cycle the input is high | Two FBs share the same global buffer | Use per-instance VAR buffers or unique VAR_GLOBAL names |
| Edge fires randomly during commissioning | Watch table "Force" toggling faster than OB1 scans, plus input filter mismatch | Raise input filter or slow the test stimuli |
| First edge after power-on is missed | Instance DB initialized with X_old = X
|
Pre-initialize X_old in the startup OB or use R_TRIG consistently |
| Counter increments by 2 for one button press | Mechanical contact bounce shorter than the input filter | Increase input filter to ≥ 10 ms or add TP-timer debounce |
R_TRIG or F_TRIG FB from the SIMATIC S7-1200 instruction set. The instance DB eliminates the persistence bug class by construction, and the code is reviewable by any engineer familiar with TIA Portal.FAQ
When is X AND NOT X_old true in an SCL program?
It is true only on the OB1 cycle where the input X is currently TRUE and the stored previous value X_old is still FALSE. This is the 0 → 1 transition (rising edge). In all other combinations of the two operands the expression evaluates to FALSE.
Should I write my own edge detection or use the built-in R_TRIG block?
Prefer the built-in R_TRIG or F_TRIG FB from the SIMATIC S7-1200 / S7-1500 instruction set for production code. The instance DB handles persistence automatically, eliminating the most common manual-pattern bug (declaring the buffer as VAR_TEMP). The manual X AND NOT X_old pattern is useful for teaching, for diagnostic one-offs, or for environments where the FB overhead is undesirable.
Why does my edge fire every cycle instead of just once?
The buffer X_old is not persisting between calls. The most common cause is declaring X_old as VAR_TEMP inside an FC, so it is reinitialized to FALSE every OB1 pass and the next cycle's comparison always sees X_old = FALSE while X is TRUE. Move X_old into the VAR section of an FB (instance DB) or into a global DB.
Can I detect a falling edge with the same buffer variable?
Yes. Use the expression (NOT X) AND X_old. The same X_old buffer drives both rising and falling detection; branch the two IF blocks and update the buffer once at the end. Alternatively, instantiate both R_TRIG and F_TRIG in parallel with the same CLK input.
What happens to the edge state after a CPU cold restart?
Both the manual STAT buffer and the R_TRIG instance-DB bit restart at their initial values (typically FALSE). The very first 0 → 1 transition after a restart will be detected normally; however, if the input is already TRUE at startup, no edge is reported until the input drops and re-asserts. Account for this in startup sequencing or initialize the buffer explicitly in OB100.