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.
| 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
REQisTRUE, the FB starts. - If
REQstaysTRUE, the FB continues to process; it does not retrigger. - If
REQgoesFALSEwhile the FB is busy, the FB keeps working; onlyDONEorERRORreleases the calling logic. - If
REQis alreadyTRUEon the very first call (e.g. initial valueTRUEafter a restart), the FB does NOT start; it requires aFALSE → TRUEtransition.
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:
| 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:
| 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
- Compile the project. TIA Portal will warn if a static Bool is read before being written; resolve all such warnings.
- Download the project to the PLC. Force a STOP → RUN transition to verify cold-start behaviour.
- 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.
- Wait for DONE. Confirm: bReq = FALSE in the same cycle, iState returns to IDLE.
- Repeat with the operator trigger pre-set to TRUE (simulate retain). Confirm the first-call reset still produces a clean start.
- Repeat with the operator trigger toggling at 100 ms. Confirm one and only one FB execution per trigger.
- 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,0x80C3for acyclic record-read failures). - 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
iStateorbReqis marked retain, a hot restart will resume the previous state and may leaveREQpermanently 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
tagin 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_TRIGinstance 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:
| 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.
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.