SCL Edge Detection in TIA Portal: Manual Logic vs R_TRIG

David Krause11 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

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:

  1. The signal X must have been FALSE on the previous evaluation (so X_old is FALSE).
  2. The signal X must be TRUE on 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;
Order matters. The assignment 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.

Why use the FB? The instance DB guarantees persistence across the OB cycle, which removes a class of bugs caused by misuse of 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:

  1. Time-based debounce: require the input to remain TRUE for N milliseconds before the edge is accepted. Implement this with an IEC_TIMER (TP) and a qualifier. The edge fires only when the timer output Q transitions to TRUE.
  2. 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:

  1. Watch table: force X, observe X_old and the edge flag. Confirm X_old actually changes value after each call. If it never changes, the variable is not persistent (likely declared VAR_TEMP).
  2. Cross-reference: right-click X_old in the project tree → "Cross-references". Confirm exactly one assignment site and one read site. Multiple writes will corrupt the edge state.
  3. Online & diagnostics: open the FB online and watch the STAT section of the instance DB live. The buffer must toggle each cycle the input toggles.
  4. Trace: for high-speed inputs, configure a trace recording both X and pos_edge at the configured OB1 cycle rate. A correctly functioning detector shows one pos_edge = TRUE pulse per 0 → 1 transition of X.

11. Verification Procedure

After implementing either the manual pattern or the R_TRIG call, verify on the bench before deploying to production.

  1. 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.
  2. Open a watch table containing the input tag X, the buffer X_old, and the edge flag pos_edge. Set the monitoring update rate to "On change" or 200 ms.
  3. Force a 0 → 1 transition on the input (either physically or with "Modify" → "Force"). Observe that pos_edge rises to TRUE for exactly one OB1 scan, and X_old updates to TRUE in the same cycle.
  4. Hold the input at TRUE for at least 10 OB1 cycles. Confirm pos_edge remains FALSE after the first pulse. This validates the buffer.
  5. Release the input and re-trigger. Confirm a second pulse appears. This validates re-triggerability.
  6. Cycle power to the CPU (cold restart). Confirm the edge detector behaves identically — both manual STAT buffers and instance-DB-resident R_TRIG bits 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
Engineering rule of thumb: when in doubt, replace the manual pattern with the documented 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.

Back to blog