Siemens SCL R_TRIG Edge Detection Failure in IF Blocks: Fix

David Krause15 min read
SiemensTIA PortalTroubleshooting
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

Problem Statement

A common failure pattern appears in Siemens SCL programs on S7-1200 and S7-1500 controllers: an R_TRIG instance is placed inside a conditional IF block together with the variable whose rising edge it is supposed to detect. The intended behaviour — increment a counter once per transition — never fires, even though the input toggles correctly. This article documents the root cause at the level of the IEC 61131-3 cyclic evaluation model, then presents five field-proven patterns that guarantee correct edge detection on Siemens TIA Portal V15 and later, including SCL for S7-1200 (firmware V4.x) and S7-1500 (firmware V1.8 and V2.x).

The diagnostic symptom is unambiguous: the count variable never changes, no matter how reliably the upstream comparator toggles. A second symptom, less obvious but equally damaging, is that the instance DB element STAT_BIT shows a stale TRUE on the first scan after a power-cycle because it is never initialised by the cyclic path.

Why R_TRIG Inside an IF Block Fails: The Cyclic Evaluation Model

Every S7 program runs inside an organisation block (typically OB1 or a cyclic OB such as OB30–OB38) whose execution time defines one cycle. In SCL, the entire code body of a block is compiled to a sequence of operations executed once per cycle. The IEC 61131-3 standard, implemented in STEP 7 since S7-300 days and documented in the current S7-1500 SCL programming manual, defines an R_TRIG function block as an instance that stores the previous value of its CLK input in STAT_BIT and reports a one-cycle pulse on Q whenever CLK is TRUE and the previous CLK was FALSE.

The decisive property is that R_TRIG must be called every cycle with the current value of the input. If the surrounding IF condition is FALSE on the cycles immediately preceding the rising edge, the R_TRIG call is skipped — STAT_BIT is not refreshed — and when the condition finally becomes TRUE and m_signal is assigned the value 1, the block sees CLK = 1 against an undefined or stale STAT_BIT. No edge is detected.

The standard behaviour is documented in the Siemens function-block help: "R_TRIG detects a signal change from 0 to 1 at input CLK and outputs this as a pulse of one cycle duration at output Q." The implementation is defined as: Q := CLK AND NOT STAT_BIT followed by STAT_BIT := CLK at the end of each call. Without the assignment on every cycle, the previous-value latch is broken.

Timing Diagram of the Failure

The diagram below shows five consecutive cycles. The comparator m_signal is only assigned on cycles 1, 3 and 5 because the second-level condition is true. The R_TRIG is only invoked on the same cycles, so on cycle 3 it sees CLK=1 while STAT_BIT is still whatever it was on cycle 1. Depending on initialisation it is already 1, and no rising edge is reported.

Cycle Time → IF cond TRUE FALSE TRUE FALSE m_signal 1 0 1 0 STAT_BIT 1 1 1 1 not refreshed Q (edge) no pulse — k never increments C1 C2 C3 C4 C5

The diagram corresponds to the original failing snippet. Even though m_signal transitions 0 → 1 at cycle 3, the R_TRIG instance was not invoked during cycles 2 and 4 with that input, so its internal STAT_BIT continues to reflect the value written at cycle 1 (1). Result: Q stays FALSE and the counter k does not increment.

Original Failing Code (Annotated)

The following SCL block reproduces the bug. It is intended to read the current second from a DTL tag, set a one-cycle pulse at m_signal at seconds 20, 40 and 59, then use R_TRIG_DB_1 to generate a rising-edge pulse that increments a counter k.

// FB_SecondCounter — INCORRECT
VAR
    m_edge   : BOOL;     // instance-only, do not retain
    m_signal : BOOL;     // instance-only, do not retain
    sec      : INT;
    k        : DINT;     // counter to be incremented
END_VAR
BEGIN
    #sec := #DTL.SECOND;

    IF (#sec = 20) OR (#sec = 40) OR (#sec = 59) THEN
        "m_signal" := 1;                              // (A) set
        "R_TRIG_DB_1"(CLK := "m_signal",              // (B) gated
                        Q   => "m_edge");
        IF "m_edge" THEN
            "k" := "k" + 1;                            // (C) dead code
        END_IF;
    END_IF;

    IF (#sec = 21) OR (#sec = 41) OR (#sec = 1) THEN
        "m_signal" := 0;                              // (D) reset
    END_IF;
END

Three bugs compound:

  1. The R_TRIG_DB_1 call at line B is reachable only when the second equals 20, 40 or 59. On every other cycle the instance is bypassed, so STAT_BIT never sees m_signal = 0 during the off-seconds.
  2. The reset at line D assigns m_signal := 0 only on seconds 21, 41 and 1. Between cycles 1 and 20 of the following minute the tag holds the stale value 1, but the R_TRIG is not even called, so the assignment is invisible to the instance.
  3. Because the previous cycle's value is not latched, the apparent 0 → 1 transition at second 20 (when the user expects a fresh edge) actually looks like 1 → 1 to R_TRIG_DB_1, producing no pulse and never incrementing k.
Diagnostic tip. Open the instance DB in the TIA Portal watch table, force m_signal FALSE, then watch STAT_BIT as you drive m_signal high. If STAT_BIT does not follow m_signal within the same cycle, the gating of the R_TRIG call is the cause.

Solution 1: Hoist R_TRIG Out of the IF Block

The cleanest fix is to perform the comparator assignment in one IF and call R_TRIG unconditionally on every cycle. The instance then maintains a correct previous-value latch.

// FB_SecondCounter — PATTERN A: hoist R_TRIG
VAR
    m_edge   : BOOL;
    m_signal : BOOL;
    sec      : INT;
    k        : DINT;
END_VAR
BEGIN
    #sec := #DTL.SECOND;

    // Step 1 — maintain m_signal each cycle
    "m_signal" := 0;                                   // default
    IF (#sec = 20) OR (#sec = 40) OR (#sec = 59) THEN
        "m_signal" := 1;
    END_IF;

    // Step 2 — R_TRIG is called unconditionally
    "R_TRIG_DB_1"(CLK := "m_signal",
                    Q   => "m_edge");

    // Step 3 — act on the edge
    IF "m_edge" THEN
        "k" := "k" + 1;
    END_IF;
END

Properties:

  • One assignment, one call, one read — minimum scan-time impact.
  • R_TRIG_DB_1 sees the input on every cycle, including the FALSE cycles, so the previous-value latch is always correct.
  • Compatible with both global instance DBs (R_TRIG_DB_1 as a separate data block) and multi-instance FBs (call as #inst_R_TRIG(...) inside an FB).
  • Works on S7-1200 (firmware V4.2 onward) and S7-1500 (firmware V1.8 onward) without modification.

Solution 2: Manual STAT_BIT Reset Pattern

For legacy blocks where the structure cannot be re-arranged, the IEC 61131-3 R_TRIG source can be re-implemented inline by manually resetting the static bit and the output during the off-seconds. This matches the R_TRIG_DB_1 semantics without reordering the block body.

// FB_SecondCounter — PATTERN B: manual edge with STAT_BIT
VAR
    m_edge   : BOOL;
    m_signal : BOOL;
    sec      : INT;
    k        : DINT;
END_VAR
BEGIN
    #sec := #DTL.SECOND;

    IF (#sec = 20) OR (#sec = 40) OR (#sec = 59) THEN
        "m_signal" := 1;
        // Edge is detected as: Q = CLK AND NOT STAT_BIT
        IF NOT "R_TRIG_DB_1".STAT_BIT THEN
            "m_edge" := 1;
            "k" := "k" + 1;
            "R_TRIG_DB_1".STAT_BIT := 1;
        END_IF;
    ELSIF (#sec = 21) OR (#sec = 41) OR (#sec = 1) THEN
        "m_signal" := 0;
        "R_TRIG_DB_1".STAT_BIT := 0;
        "R_TRIG_DB_1".Q         := 0;
        "m_edge"                := 0;
    END_IF;
END
Note on instance-DB visibility. Direct access to R_TRIG_DB_1.STAT_BIT requires the instance DB to have non-optimised access. In TIA Portal V15 and later, right-click the instance DB, choose Properties → Attributes and clear the "Optimised block access" checkbox. For an FB-resident multi-instance, the same fields appear under the static section of the enclosing FB.

Pattern B is useful when the comparator logic and the edge consumer live in physically separate networks and the developer cannot combine them, but it loses the self-documenting nature of the standard R_TRIG call. Use it sparingly.

Solution 3: Replace R_TRIG with EDGEPOS

TIA Portal exposes the IEC 61131-3 EDGEPOS / EDGENEG functions (also referred to as EDGE_DETECT in some versions) for use directly in expressions. They are stateless calls that the compiler expands into the equivalent of an R_TRIG with a hidden static variable, and they behave identically regardless of where they appear in the network.

// FB_SecondCounter — PATTERN C: EDGEPOS
VAR
    sec : INT;
    k   : DINT;
END_VAR
BEGIN
    #sec := #DTL.SECOND;

    // EDGEPOS evaluates "m_signal" and latches its previous value
    IF EDGEPOS(CLK := ((#sec = 20) OR (#sec = 40) OR (#sec = 59))) THEN
        "k" := "k" + 1;
    END_IF;
END

EDGEPOS characteristics on S7-1200/1500:

  • Implemented in the SCL compiler; no instance DB required.
  • The compiler allocates a hidden static variable per call site; renaming or duplicating the call line creates a fresh latch.
  • Returns BOOL, so it can be used directly as an IF condition or as a term in a larger expression.
  • For SCL, the companion EDGENEG detects 1 → 0 transitions.

Solution 4: IEC Timer as Pulse Generator

When the application needs a fixed pulse width rather than a single-cycle edge, a self-resetting TON timer is the canonical choice. The block below produces a 200 ms pulse every 5 s.

// FB_PulseGen — PATTERN D: TON self-resetting
VAR
    tOff : TON_TIME;       // on-delay, time base IEC
    sec  : INT;
    k    : DINT;
END_VAR
BEGIN
    #sec := #DTL.SECOND;

    #tOff(IN := NOT #tOff.Q,                // self-reset
          PT := T#200MS);

    IF EDGEPOS(CLK := #tOff.Q) THEN
        "k" := "k" + 1;
    END_IF;
END

The NOT #tOff.Q feedback converts the on-delay into a flip-flop with a 50 % duty cycle. A pulse generator built this way is robust against the original bug because the input to EDGEPOS is always defined, even on cycles where the surrounding logic is bypassed.

Solution 5: Branchless Increment with BOOL_TO_INT

The original snippet increments k with an IF … END_IF. A common compact idiom removes the branch by treating the edge as 0 or 1 and adding it directly. The compiler generates the same machine code on S7-1500, but the intent is clearer on one line:

// FB_SecondCounter — PATTERN E: branchless
VAR
    sec : INT;
    k   : DINT;
END_VAR
BEGIN
    #sec := #DTL.SECOND;

    "R_TRIG_DB_1"(CLK := ((#sec = 20) OR (#sec = 40) OR (#sec = 59)),
                    Q   => "m_edge");
    "k" := "k" + BOOL_TO_INT("m_edge");
END

The pattern scales: if you have sixteen edge-driven increments against a 16-bit status word, write

FOR #i := 0 TO 15 DO
    "word16_bits"[#i](CLK := "stat_word".%X#i);
    "counters[#i]" := "counters[#i]"
                    + BOOL_TO_INT("word16_bits"[#i].Q);
END_FOR;

On S7-1500 the compiler replaces BOOL_TO_INT with a zero-extend AND, so there is no runtime cost relative to IF … END_IF. On S7-1200 the conversion is a single load-immediate instruction; the saving from removing the jump outweighs the conversion when the alternative is a deeply nested IF tree.

Type-safety. IEC 61131-3 strict mode ("Enable strict syntax checking" in the SCL compiler options) rejects implicit BOOL → INT conversion. BOOL_TO_INT is the portable, strict-mode-safe alternative.

Pattern Comparison Table

Pattern Code size DB overhead Strict-mode safe Best for
A — Hoist R_TRIG Small One instance DB or multi-instance slot Yes General-purpose; the canonical Siemens pattern.
B — Manual STAT_BIT reset Small Same as A Yes, but requires non-optimised access Legacy blocks where the body cannot be re-arranged.
C — EDGEPOS One line None (compiler-managed) Yes Compact code; S7-1200 firmware V4.0 and later, S7-1500 firmware V1.8 and later.
D — TON self-resetting Medium Ton instance Yes Variable pulse width or rate-limited event detection.
E — Branchless increment One line None Yes (with BOOL_TO_INT) Bit-sliced arrays of counters, hot loops.

TIA Portal Implementation Notes

Three project-level decisions control which pattern is selectable.

  1. Block access. Pattern B requires non-optimised instance DB access. In the project tree, select the instance DB, choose Properties → Attributes and clear "Optimised block access". Without this step the SCL compiler reports "Access to the instance data is not possible" when you reference R_TRIG_DB_1.STAT_BIT.
  2. Strict syntax checking. Patterns C and E both rely on explicit conversions. Open the PLC properties, switch to Programming language SCL → Compiler → Enable strict syntax checking; this prevents the implicit BOOL → INT conversion that older SCL blocks sometimes hid.
  3. Firmware support. EDGEPOS is available in S7-1200 firmware V4.0 and S7-1500 firmware V1.8. Earlier firmware rejects the function with "Unknown identifier". Verify the firmware version in Online → Accessible devices → PLC → Diagnostics → Device Information.

For multi-instance FBs (preferred over separate instance DBs because they avoid name pollution in the project tree), declare the edge as a static:

FUNCTION_BLOCK FB_SecondCounter
VAR
    edge_R   : R_TRIG;        // multi-instance
    edge_C   : R_TRIG;
END_VAR
BEGIN
    "edge_R"(CLK := "m_signal_R",
              Q   => "m_edge_R");
    "edge_C"(CLK := "m_signal_C",
              Q   => "m_edge_C");
END

Multi-instance FBs reuse one DB per enclosing FB, which simplifies the memory model and keeps the watch table compact. The two patterns (instance DB vs. multi-instance) are functionally equivalent; choose the multi-instance form unless you need to expose the edge across blocks by symbolic name.

Commissioning Verification Steps

The following procedure verifies that the corrected pattern detects exactly one rising edge per transition.

  1. Compile and download the FB. Set the PLC to Run; confirm the diagnostic buffer shows no errors and the cycle time remains inside the configured maximum (typically 150 ms for an OB1 cycle, 5 ms for OB35).
  2. Open the instance DB watch table. Force m_signal to FALSE for at least 10 cycles.
  3. Force m_signal to TRUE for one cycle, then back to FALSE. m_edge should pulse TRUE for exactly one cycle, and k should increment by one.
  4. Repeat 10 times. k should equal the number of 0 → 1 transitions applied to m_signal. If it diverges, return to Pattern A and confirm the R_TRIG call is unconditional.
  5. For Pattern C, repeat the same sequence using the watch table to drive m_signal; the compiler-allocated latch variable can be inspected in the project tree under Program blocks → System blocks → Generated blocks → EDGEPOS_<n>_DB.
  6. For Pattern D, force tOff with a 5 s PT in the watch table and confirm the pulse width matches PT on an oscilloscope attached to a digital output wired from tOff.Q.

Common Pitfalls and Edge Cases

Even after the structural fix, several recurring pitfalls cause false or missed edges:

  • Calling R_TRIG twice with the same DB. TIA Portal permits the same instance DB to be referenced from multiple networks. The second call overwrites STAT_BIT, masking the edge detected on the first call. Use distinct instances or a multi-instance FB.
  • Cold-restart initialisation. On an S7-1500 cold restart (STOP → RUN with retentivity lost), STAT_BIT starts at FALSE. If the input m_signal is already TRUE on the first cycle, no edge is produced. Where this matters, gate the consumer with a one-shot firstScan bit.
  • OB priority and time-slice. Edge detection inside high-priority OBs (OB80–OB87) is dangerous because the OB may not be called on every cycle. Always place edge logic in OB1 or in a cyclic OB whose period is at least 4× shorter than the fastest input transition.
  • Bit-sliced arrays. Using IF "%X#i" THEN … END_IF in a FOR loop is equivalent to Pattern E but requires careful handling of partial-word bit numbering on S7-1200 (where byte/bit ordering differs from S7-1500). Test with the actual PLC online.
  • Implicit BOOL → INT. Older code writes "k" := "k" + "m_edge"; relying on a non-strict conversion. With strict syntax enabled this triggers error SCL0015; replace with BOOL_TO_INT("m_edge") as in Pattern E.
  • Cross-block edge. If the edge must be visible in another FB, do not pass the raw R_TRIG.Q across the boundary; pass the latch and let the consumer call a local R_TRIG. The cross-block Q is only TRUE for a single cycle of the consumer, which may be one cycle of a different OB than the producer.

Quick Diagnostic Checklist

Symptom Likely root cause Confirm by Fix
Counter never increments R_TRIG called inside gated IF Watch STAT_BIT while forcing input; it does not follow Pattern A or C
Counter increments twice on one transition Two calls share the same instance DB Cross-reference search for the instance DB name Use multi-instance FB or distinct instances
Counter increments once on power-up Input is already TRUE on first scan Force m_signal to TRUE before download Add first-scan gate
Strict-mode compile error SCL0015 Implicit BOOL→INT conversion Compile message line and column Replace with BOOL_TO_INT
EDGEPOS not recognised PLC firmware too old Online → Device information → Firmware Upgrade S7-1200 to V4.0+ or S7-1500 to V1.8+

Refer to the S7-1500 system manual and the SCL programming reference in the Siemens Industry Online Support portal for the normative description of R_TRIG, F_TRIG, EDGEPOS and EDGENEG; consult the IEC 61131-3 third-edition standard (specifically section 6.4.3 on bistable elements and edge detectors) for the formal semantics that the Siemens implementation follows.

FAQ

Why does R_TRIG inside an IF block never see a rising edge in SCL?

The R_TRIG function block updates its internal STAT_BIT latch only when it is invoked. When placed inside a conditional IF whose condition is false on most cycles, the latch is never refreshed with the input value of zero, so when the input finally transitions high, the function block reads CLK=1 against a stale STAT_BIT=1 and reports no edge. The fix is to call R_TRIG unconditionally every cycle.

Can I reset R_TRIG.STAT_BIT manually to make it work inside an IF?

Yes. Access R_TRIG_DB.STAT_BIT directly (requires non-optimised block access) and assign it to zero during the off-cycles, mirroring the function-block's internal semantics. A cleaner alternative is to use the IEC EDGEPOS function or to hoist the R_TRIG call out of the conditional block so it runs every cycle.

What is the difference between R_TRIG and EDGEPOS in TIA Portal?

R_TRIG is a function block whose previous-value latch is held in a named instance DB or multi-instance slot; you see the latch as STAT_BIT. EDGEPOS is a function whose latch is allocated by the SCL compiler and is invisible to the user. Both detect a 0→1 transition and produce a one-cycle pulse on the output.

Does BOOL_TO_INT add a real cycle cost on S7-1500?

No. The SCL compiler lowers BOOL_TO_INT to a 32-bit AND with an immediate mask, executed in a single MC7 instruction. Removing a branch by replacing IF m_edge THEN k := k + 1; END_IF; with k := k + BOOL_TO_INT(m_edge); typically improves cycle time on S7-1500 when the alternative is a deeply nested IF tree.

Which firmware versions of S7-1200 and S7-1500 support EDGEPOS?

EDGEPOS and EDGENEG are available in S7-1200 firmware V4.0 and later, and in S7-1500 firmware V1.8 and later. Earlier firmware rejects the function with an "unknown identifier" error during the SCL compile step. Verify the firmware under Online → Accessible devices → PLC → Device Information in TIA Portal.

Why does my counter increment once on the first PLC start-up cycle?

On a cold restart the instance data is zero-initialised, so STAT_BIT starts as FALSE. If the input signal is already TRUE on the first scan, the function block sees a false-to-true transition and pulses Q. Where this is undesirable, gate the consumer with a one-shot first-scan bit or wait for a confirmed zero on the input.

Back to blog