SCL Positive Edge Detection for REQ-Based Function Calls

David Krause14 min read
Best PracticesSiemensTIA Portal
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

SCL Positive Edge Detection for REQ-Based Function Calls

Siemens SCL (Structured Control Language) does not ship a dedicated edge instruction that operates directly on a Boolean expression in the same way that the FBD/LAD edge boxes do. Engineers coming from C, C++, or ladder logic often struggle when they must wrap a library FB (such as WRREC, RDREC, WRIT_DBL, PN_InOut_Finish, or any motion or PID block) that takes a REQ input. The REQ input is a level-triggered signal that the FB must observe to start its work, and the FB clears it on DONE, BUSY=FALSE, or ERROR. Producing that pulse cleanly, deterministically, and without wasting an OB1 cycle is the subject of this reference.

1. Edge Instruction Fundamentals in SCL

Every positive edge is a transition from FALSE to TRUE. A negative edge is a transition from TRUE to FALSE. SCL has no keyword for this; the behaviour is synthesised with a static or instance memory bit. The official TIA Portal manual collection states the rule clearly: "Because the memory bit must be maintained from one execution to the next, you should use a unique bit for each edge instruction, and you should not use this bit elsewhere in the program." See the TIA Portal S7-1200 manual collection: Positive and negative edge instructions.

The two canonical ways to detect an edge in SCL are:

  • R_TRIG (FP) / F_TRIG (FN) system blocks - IEC 61131-3 standard rising/falling edge detectors. They can be instantiated as a multi-instance inside a parent FB or as a stand-alone instance DB.
  • Manual XOR / NEG pattern - synthesise the edge with one static Bool, one XOR, and one NOT. No block call overhead; works on S7-300/400 with classic STEP 7 as well as TIA Portal.
Edge detection method comparison
Method Block type Memory location S7-300/400 S7-1200/1500 RE-usable as multi-instance
R_TRIG / F_TRIG FB (system) Instance DB or multi-instance via FB call Native Yes
Manual XOR pattern Inline code Static Bool in FB / global Bool Yes Yes N/A (inline)
LAD/FBD P / N box Graphic Implicit in M-bit or instance Yes Yes Via instance

For REQ management, the manual XOR pattern is usually preferred because the REQ must remain set for many OB1 cycles until the FB reports DONE, ERROR, or BUSY=FALSE. A single-cycle pulse is too short.

2. The REQ Signal Pattern: Origin and Purpose

The REQ input on Siemens standard library FBs is a level signal that is sampled at the rising edge internally. Internally, the FB copies the REQ level into its own static edge memory on every call, which means:

  • The first time REQ is TRUE, the FB starts.
  • If REQ stays TRUE, the FB continues to process; it does not retrigger.
  • If REQ goes FALSE while the FB is busy, the FB keeps working; only DONE or ERROR releases the calling logic.
  • If REQ is already TRUE on the very first call (e.g. initial value TRUE after a restart), the FB does NOT start; it requires a FALSE → TRUE transition.

That last rule is the source of the well-known "first call with REQ=0" gotcha. It is documented in the Siemens online help for almost every standard FB: "The block must be called once with REQ = FALSE after a restart before REQ = TRUE will be accepted." This is why a clean reset on first call is mandatory.

3. State Machine Approach to REQ Management

The cleanest way to keep track of REQ in non-timing-critical code is a small state machine with three states: IDLE, ARMED, and RUNNING. This pattern is portable, scan-cycle deterministic, and free of one-shot races.

// State machine for REQ management
FUNCTION_BLOCK FB_ReqManager
VAR
    iState    : INT := 0;          // 0=IDLE, 1=ARMED, 2=RUNNING
    bReq      : BOOL;              // Level sent to wrapped FB
    bMechEdge : BOOL;              // Manual edge memory for the trigger
    bEdgeTrig : BOOL;              // One-cycle pulse from manual edge
END_VAR
BEGIN
    // 1. Detect rising edge on operator trigger
    bEdgeTrig := bTrigger AND NOT bMechEdge;
    bMechEdge := bTrigger;

    // 2. State machine
    CASE iState OF
        0:  // IDLE - waiting for trigger
            bReq := FALSE;
            IF bEdgeTrig THEN
                iState := 1;
            END_IF;

        1:  // ARMED - ensure first call with REQ = 0
            bReq := FALSE;
            iState := 2;

        2:  // RUNNING - raise REQ until FB reports finish
            bReq := TRUE;
            IF bDone OR bError THEN
                bReq := FALSE;
                iState := 0;
            END_IF;
    END_CASE;

    // 3. Call the wrapped FB
    WrappedFB(REQ := bReq,
              ...,
              DONE => bDone,
              BUSY => bBusy,
              ERROR => bError);
END_FUNCTION_BLOCK

The state machine guarantees that the wrapped FB always sees one cycle with REQ = FALSE between any two starts. The manual XOR pattern in step 1 is unique to this block instance; the memory bit bMechEdge is never written anywhere else, satisfying the Siemens rule quoted in section 1.

4. First-Call Reset Technique (REQ-0 Cycle)

When a wrapper is called from OB1, the cold-restart / warm-restart path of the CPU may already have REQ initialised to its default value of FALSE (because the instance DB was loaded from the offline project). In that case the first call works. But after an online modification, after a STOP→RUN transition with retain, or when the FB is in a different priority class, the initial value of REQ can be TRUE. The FB refuses to start and the machine sits idle.

The two-line fix the Siemens community has converged on is:

// First-call reset pattern
IF NOT bReq THEN
    bReq := TRUE;          // arm the trigger
ELSE
    FB_Work(REQ := bReq,   // first call, REQ = TRUE
            ...
            DONE => bDone,
            ERROR => bError);
    IF bDone OR bError THEN
        bReq := FALSE;     // disarm
    END_IF;
END_IF;

Because FB_Work is called only on the second pass, it sees REQ = FALSE on cycle N, then REQ = TRUE on cycle N+1. That satisfies the internal edge requirement of every Siemens standard FB.

5. Timing-Critical Patterns (One-Cycle REQ)

When the application cannot afford the extra cycle consumed by the first-call reset (high-speed measuring, fast recipe download, or any path where the cycle budget is tight), the pattern collapses to a single call with the manual XOR on the operator trigger:

// Timing-critical one-shot pattern
bReq := FALSE;                                 // default off
IF bTrigger AND NOT bTriggerMech THEN          // rising edge
    bReq := TRUE;
    bTriggerMech := TRUE;
ELSIF NOT bTrigger THEN
    bTriggerMech := FALSE;
END_IF;

IF bDone OR bError THEN
    bReq := FALSE;                             // clear immediately
END_IF;

FB_Fast(REQ := bReq, DONE => bDone, ERROR => bError);

The trade-off is explicit: you save one OB1 cycle, but you must guarantee that bTrigger was FALSE at least once before the first arming. In a fresh restart the operator button is normally FALSE, so this works. In a hot restart with retained operator state, add a one-shot reset on first scan:

IF bFirstScan THEN
    bTriggerMech := FALSE;
    bReq := FALSE;
    bFirstScan := FALSE;
END_IF;

6. Wrapping Standard Library FBs (RDREC, WRREC, RD_DPAR, WR_DPAR)

The pattern in section 3 is best used as a generic wrapper. Concrete examples for the most common record / data-record FBs in TIA Portal:

Standard FBs that require the REQ-0 first-call pattern
FB DB number REQ behaviour Notes
RDREC (SFB/FB 52) DB 52 or instance Rising edge on REQ Used for acyclic PROFINET record reads
WRREC (SFB/FB 53) DB 53 or instance Rising edge on REQ Acyclic PROFINET record write
RD_DPAR (FB 55) DB 55 or instance Rising edge on REQ Read DP slave parameter
WR_DPAR (FB 56) DB 56 or instance Rising edge on REQ Write DP slave parameter
PN_InOut_Finish (FB 129) Instance REQ edge on PROFINET IO update End of IO update signal
WRIT_DBL (FB 213) Instance Rising edge on REQ Write to data block in remote CPU
READ_DBL (FB 212) Instance Rising edge on REQ Read data block from remote CPU

Wrap each instance in a parent FB and place the state machine of section 3 in the wrapper. The instance DB of the inner FB never travels to the caller; the caller's source uses only the wrapper's Execute input and the Done, Busy, Error, Status outputs. That is the architecture every clean SCL codebase in a Siemens environment should adopt.

7. Multi-Trigger Sequencing (do-while Equivalent)

When several REQ-triggered FBs must run in sequence within the same OB1 cycle (for example: write a recipe, then verify the recipe by reading it back), use a sequence register with a step counter and call each FB exactly once per step. The "do-while loop that looks for a negative edge on REQ" idea from the original SCL discussion maps to a re-entrancy flag in the step, not to a real loop:

// Sequence of three acyclic writes
CASE iStep OF
    0:
        IF bStart THEN
            iStep := 1;
            bReq1 := FALSE;       // reset on first call
        END_IF;
    1:
        bReq1 := TRUE;
        FB_Write1(REQ := bReq1, DONE => bDone1, ERROR => bError1);
        IF bDone1 OR bError1 THEN
            bReq1 := FALSE;
            IF bError1 THEN iStep := 99; ELSE iStep := 2; END_IF;
        END_IF;
    2:
        bReq2 := FALSE;           // reset on first call
        IF NOT bReq2 THEN
            iStep := 3;
        END_IF;
    3:
        bReq2 := TRUE;
        FB_Write2(REQ := bReq2, DONE => bDone2, ERROR => bError2);
        IF bDone2 OR bError2 THEN
            bReq2 := FALSE;
            IF bError2 THEN iStep := 99; ELSE iStep := 4; END_IF;
        END_IF;
    4:
        bReq3 := FALSE;
    5:
        bReq3 := TRUE;
        FB_Verify(REQ := bReq3, DONE => bDone3, ERROR => bError3);
        IF bDone3 OR bError3 THEN
            bReq3 := FALSE;
            iStep := 100;
        END_IF;
    99:  // error
        ...
    100: // done
        bSequenceDone := TRUE;
        iStep := 0;
END_CASE;

Each step has its own dedicated REQ bit. No bit is reused; no edge memory is shared between FBs. The cycle count per transition is exactly one OB1 cycle to reset, one to set, and the subsequent cycles to wait for DONE. That is the minimum deterministic count for a rising-edge-triggered FB.

8. Negative Edge Use Cases (N_TRIG, FN)

Negative edges on REQ are unusual but useful: detecting BUSY = FALSE after the call, or the falling edge of DONE to chain the next FB. The F_TRIG / FN block produces a one-cycle pulse on the 1→0 transition. The manual pattern is:

// Falling edge of BUSY
bBusyFalling := (NOT bBusy) AND bBusyMech;
bBusyMech := NOT bBusy;

This is the same memory-bit rule, just inverted. The community example at jorgemgn/scl-edge shows the equivalent code with an instance DB, useful when the wrapped block exposes a falling-edge requirement (e.g. some motion FBs need a falling edge on Enable to release the axis).

9. Diagnostics and Memory-Bit Hygiene

Sloppy edge management is the most common cause of "the FB does nothing" or "the FB starts twice in a row". Watch for these symptoms in the online watch table:

Troubleshooting matrix for REQ-based FBs
Symptom in watch table Likely cause Fix
BUSY never goes TRUE First call with REQ = TRUE, internal edge never seen Force REQ = 0 on first scan, then arm
BUSY goes TRUE twice in a row without DONE REQ bit is set permanently and the FB's internal edge memory is fed from a shared bit Use a dedicated memory bit per FB, never reuse it
DONE pulses but no data changes REQ stayed TRUE but DONE was latched from previous call Clear REQ in the same cycle that DONE was observed
Block runs only on cold restart Retain of the wrapper DB kept bReq = TRUE Add first-scan reset of bReq in INIT section
REQ follows trigger but DONE never appears REQ reset too early (before FB latched the edge) Hold REQ for at least one full cycle after setting it
Multiple FBs all start on the same trigger One shared memory bit used for several FBs Allocate one Mech bit per FB instance

Add these to the project standard: every FB that needs a REQ has a multi-instance R_TRIG or a local static bMech Bool; the Bool name embeds the FB instance name (e.g. statTrig_Motor1_Home); the Bool is never read or written outside that FB. Enforce the rule with a code review checklist and a grep over the source for double writes to any bMech* symbol.

10. Verification Procedure

  1. Compile the project. TIA Portal will warn if a static Bool is read before being written; resolve all such warnings.
  2. Download the project to the PLC. Force a STOP → RUN transition to verify cold-start behaviour.
  3. In the watch table, set the operator trigger. Confirm: cycle N-1 trigger = FALSE, cycle N trigger = TRUE, cycle N bReq = FALSE (first-call reset), cycle N+1 bReq = TRUE, cycle N+2 BUSY = TRUE.
  4. Wait for DONE. Confirm: bReq = FALSE in the same cycle, iState returns to IDLE.
  5. Repeat with the operator trigger pre-set to TRUE (simulate retain). Confirm the first-call reset still produces a clean start.
  6. Repeat with the operator trigger toggling at 100 ms. Confirm one and only one FB execution per trigger.
  7. Disconnect the device being addressed. Confirm ERROR path: iState moves to error, bReq clears, status word captures the Siemens error code (e.g. 0x80A1, 0x80B1, 0x80C3 for acyclic record-read failures).
  8. Trigger a STOP → RUN while a request is in flight. Confirm the first-scan reset prevents a stuck REQ after restart.

Each of those checks takes seconds with a watch table. They are the minimum field-proven regression set for any new REQ-wrapping wrapper.

11. Common Pitfalls and Field-Proven Caveats

  • Retain on the wrapper DB - if iState or bReq is marked retain, a hot restart will resume the previous state and may leave REQ permanently TRUE. Mark only non-control data as retain; keep the state machine in non-retain area.
  • Optimised block access - the optimised access in S7-1200/1500 hides the static memory bit from the watch table. Add a tag in the wrapper DB to expose the edge memory if you need to debug from HMI or watch table.
  • Multi-instance vs single-instance - if you call the same wrapper FB ten times (one per motor), give each a separate instance DB or multi-instance, and the compiler will give each one its own copy of the static edge memory automatically.
  • Calling in a different OB - calling the wrapper in OB35 (cyclic interrupt) with the standard FB whose REQ expects OB1 semantics works, but you must also arm the first-call reset on first execution of that OB after a restart, not only on OB1 first scan.
  • Mixing edge detectors - do not mix a R_TRIG instance with a manual XOR on the same signal. Pick one per FB and document the choice in the header comment.
  • Re-entry - the wrapper FB must not be called from a higher-priority OB while a lower-priority call is still busy. The internal edge memory of the wrapped FB will lose state. Guard with a re-entrancy lock, or use a job queue.

12. Performance Budget

The state-machine wrapper of section 3 adds the following deterministic cost per call:

Wrapper overhead per OB1 cycle
Operation Typical cycles on S7-1516
CASE state machine (3 states) ~12 ns
Manual XOR edge (1 input) ~3 ns
REQ assignment and reset ~3 ns
One FB instance call ~150 ns - 2 µs depending on FB
Total wrapper overhead ~25 ns

For ten acyclic FBs running per OB1 cycle, the wrapper overhead is in the order of a few hundred nanoseconds - negligible against a 1 ms OB1. The cost of the extra cycle to reset REQ, when used, is the OB1 cycle time itself; on a 1 ms cycle that is 0.0001% of throughput, but it can matter in motion or high-speed measuring applications. Use section 5's pattern in those cases.

Note. The patterns shown here apply to S7-1200 with firmware V4.0 and above, and to S7-1500 with firmware V1.8 and above. Earlier S7-1200 firmware (V1.0 - V3.x) does not support the optimised access that the SCL compiler generates for the manual XOR pattern; use the R_TRIG / F_TRIG system blocks instead. Always verify the exact behaviour against the firmware release notes shipped with the TIA Portal version in use.

Why does my Siemens FB ignore the first REQ pulse after a restart?

The FB requires a FALSE → TRUE transition on REQ; the first call after a restart often has REQ = TRUE if the wrapper DB retained that value. Force REQ = FALSE on the first scan of the wrapper, then arm the trigger on the next cycle. See the first-call reset pattern in section 4.

Can I use a single global Bool for the REQ edge memory of multiple FBs?

No. Each FB needs a unique memory bit. Reusing the bit makes the second FB see the wrong edge state and either retrigger or fail to trigger. The TIA Portal manual is explicit: use a unique bit for each edge instruction. See section 1 and section 9.

How many OB1 cycles does the first-call reset pattern add?

Exactly one OB1 cycle to set REQ = FALSE, then one OB1 cycle to set REQ = TRUE. The FB then takes however many cycles it needs to report DONE or ERROR. For a 1 ms cycle the overhead is 2 ms of wall-clock time, which is acceptable for most non-motion code.

Should I use R_TRIG or a manual XOR for REQ generation?

Use R_TRIG / F_TRIG when you need a clean, IEC 61131-3 portable instance; use the manual XOR when you want minimum code size and zero block call overhead. The patterns are functionally identical if the memory bit is unique to that FB. See the comparison table in section 1.

What happens if the wrapped FB is in BUSY when the PLC goes STOP → RUN?

Without retain, the instance DB is re-initialised: iState = 0, bReq = FALSE, bMechEdge = FALSE. The wrapped FB starts fresh. With retain on the wrapper, the previous BUSY state is lost from the wrapped FB (it does not retain BUSY) but the wrapper's bReq may be TRUE; the first-call reset clears it on the first scan. Mark the wrapper DB as non-retain for the state machine portion.

Back to blog