Siemens STL Conditional L/T and AND-Chained Comparisons

David Krause16 min read
S7-300SiemensTutorial / 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

Why STL L and T Are Unconditional

Statement List (STL) on the SIMATIC S7-300 and S7-400 treats the L (Load) and T (Transfer) instructions as pure accumulator operations. Unlike a Ladder Diagram (LAD) MOVE box, they do not condition on the Result of Logic Operation (RLO). The naive snippet below compiles but executes the transfer every scan regardless of I 0.0:

A I 0.0
L 20
T QB10

The misconception here is that the A instruction qualifies the subsequent L. It does not. A I 0.0 only writes bit 1 (RLO) of the Status Word; L then pushes the constant 20 into ACCU1, and T QB10 always writes ACCU1's low byte to output byte 10. The fix is to interpose a conditional construct (a jump, a A( ) block, or a MOVE-equivalent from LAD/FBD) that consults the RLO before doing the transfer.

Engineer field-note: STL Load and Transfer are independent of the control bits in the Status Word. To make them conditional, test the RLO via JC/JCN, branch into separate code blocks, or substitute the L/T pair with an instruction whose enable input is the RLO (such as a MOVE block generated in LAD/FBD, or a single-purpose FC).

The S7-300/400 Status Word Foundation

Every binary and math instruction on the S7-300/400 CPU updates a 16-bit register called the Status Word (STW). Understanding which bit is set when is mandatory before implementing conditional STL patterns. The relevant bits are:

Bit Mnemonic Name Set/Cleared By Read By
0 /FC First Check First bit-test in a logic string — (internal)
1 RLO Result of Logic Operation A, AN, O, ON, X, XN, NOT, SET, CLR, comparisons, math, shift, rotate, conversion JC, JCN, JCB, JNBI, JL, JO, JOS, =, S, R, BEC, CC/UC
2 /STA Status Bit instructions referencing memory — (informational)
3 /OR OR Requirement Bit O/ON chains (pre-AND before OR) — (informational)
4 OV Overflow Math, float, conversion, compare JO, JOS
5 OS Stored Overflow OV (latched until next status-updating instruction) JOS
6 CC0 Condition Code 0 Compare, math, shift, rotate, conversion, word logic JP, JM, JMZ, JPZ, JUO, JL via CC0/CC1
7 CC1 Condition Code 1 Same as CC0 Same as CC0
8 BR Binary Result SAVE, FB ENO pin, write instructions, FC/FB ENO JC on a CALL FB (CC, UC, BEC), JNBI, A BR, AN BR

Source: Siemens SIMATIC S7-300/400 STL Programming Manual – Status Word section. The RLO (bit 1) is the bit that controls JC (jump if RLO = 1), JCN (jump if RLO = 0), and the write-enable of =, S, R. BR (bit 8) is not loadable: there is no L BR instruction in S7-300/400 STL. You can only save the current RLO into BR with the SAVE instruction, or read BR via A BR / AN BR and via the ENO of a called FB.

Pattern 1 — Conditional Transfer with Jump Guards

To make a transfer behave like a Ladder MOVE gated on I 0.0, do not try to condition the T itself. Instead, skip the transfer when the guard is false. Two compact idioms work.

Idiom A — JC over a single transfer with explicit else

JC   _DO       // RLO already set by a prior A/AN
L    0
T    QB10
JU   _END
_DO: L    20
T    QB10
_END: NOP 0

The CPU executes the else-path (write 0) when the input is false, and the then-path (write 20) when true. The trailing JU _END is critical — without it, both transfers would execute on the truthy path.

Idiom B — Test guard then jump to skip transfer

AN   I 0.0     // RLO = 1 when I0.0 == 0
JC   _SKIP
L    20
T    QB10
_SKIP: NOP 0

Here AN inverts the guard, so JC jumps over the assignment when I 0.0 is 0. Both forms compile to the same stack depth; Idiom B is the cleanest for single conditional writes.

Performance: On an S7-314/315/317, a JC costs ~1 µs of execution time, an L/T pair costs ~1–2 µs. Conditional transfers in STL are typically cheaper than the equivalent LAD MOVE block, which is interpreted through the LAD/FBD compiler's intermediate form.

Idiom C — Conditional Call

If the transfer lives inside an FC, you can call it conditionally with CC FCn instead of CALL FCn. CC consults the current RLO and, if it is 1, performs the call. UC FCn is unconditional and ignores the RLO entirely:

A    I 0.0
CC   FC 1       // executes FC1 only when I0.0 == 1
L    0
T    QB10

Pattern 2 — AND-Chained Comparisons (==I, ==R, ==D)

A common attempt at multi-condition logic looks like this:

A    "E1_3"
L    "sMM1"
L    "SPsMM1"
==I
L    "sMM2"
L    "SPsMM1"
==I
JC   _001

Why does this fail? Because ==I is a comparison, not a pure RLO-write. Each ==I replaces the RLO based on the result of the comparison (equal, not-equal, greater, less, etc.), and the accumulator is unaffected after the comparison is evaluated. So the second ==I overwrites the RLO from the first. The fix is to AND the comparison results into a single RLO explicitly, or to use the A( ... ) parenthesis construct.

Solution A — Use A( ... ) parentheses

A( 
L    "sMM1"
L    "SPsMM1"
==I
) 
A( 
L    "sMM2"
L    "SPsMM1"
==I
) 
JC   _001

Inside each A( ... ) block, the comparison's RLO is saved onto the OR stack (/OR), the parenthesis closes, and the A ahead of the next open-paren ANDs the saved RLO with the new one. After the second ), the final RLO is 1 only if both comparisons were equal.

Solution B — Manual AND using A ==I

STL allows the shortcut where the comparison is treated as a single bit-test instruction: A ==I directly writes the RLO from the comparison result and ANDs it with the previous RLO. The two L operands remain in ACCU1/ACCU2 for re-use across the next ==I:

A    "E1_3"          // optional precondition
A    ==I             // sMM1 == SPsMM1 ?
A    ==I             // sMM2 == SPsMM1 ?
JC   _001

The first ==I needs the two operands loaded with L ahead of the chain. The second ==I consumes the operands from ACCU1/ACCU2 (which were left there by the previous ==I). The explicit A prefix on the second ==I ANDs the new RLO with the previous one.

Solution C — Save RLO to BR and re-AND with A BR

A    "E1_3"
L    "sMM1"
L    "SPsMM1"
==I
SAVE                // RLO -> BR
A    BR             // re-AND with the current RLO state
L    "sMM2"
L    "SPsMM1"
==I
JC   _001

Important caveats:

  • A BR checks status word bit 8. On S7-300/400 this is a real addressable bit, so the pattern works.
  • On S7-1500 the Status Word layout is extended, but A BR is preserved as a compatibility instruction. Newer compilers prefer the A( ... ) pattern.
  • There is no L BR on either platform. The question's author asked the right thing; the answer is: you cannot load BR into ACCU1, you can only test it with A BR/AN BR or read it via the ENO of a called FB.
  • The SAVE instruction is destructive across nested calls — if you SAVE before calling an FB that itself returns an ENO, the BR is overwritten by that ENO. Use the JCB / JNBI pair (jump if RLO=1 with save, jump if BR=0) when you need to save and test BR across a call boundary.

Comparison Result → RLO and CC-Bit Mapping

For reference, the CC1/CC0 pair maps to the four possible integer comparison results. The S7-300/400 mapping is:

CC1 CC0 Integer Compare Float Compare Jump If Mnemonic
0 0 == 0 / == FALSE == 0.0 / == FALSE RLO = 1, ACCU1-L = 0 JZ / JPZ
0 1 < 0 / < < 0.0 / < JM less than
1 0 > 0 / > > 0.0 / > JP greater than
1 1 UO (unordered, NaN) UO (unordered, NaN) JUO invalid compare (only on real types)

For unsigned 16-bit, < and > are interpreted modulo 65536; use <D/>D (double integer) when you need a clean 0..32767 range.

Network Limits and CALL Instruction Scaling

There is no hard S7-300/400 CPU limit on the total number of CALL FCx instructions in OB1, but there is a network length limit: a single STL network is capped at 999 lines in the Step 7 editor. Within that, each CALL compiles to a BStack (Block Stack) push/pop of the return address plus a fixed call-setup cost. Practical limits observed in the field:

CPU BStack Depth (max nesting) LStack per Priority Class Re-entrancy
CPU 312 8 256 bytes No
CPU 313 / 314 8 256 bytes No
CPU 315-2 DP / 315-2 PN/DP 16 1024 bytes Yes (limited)
CPU 317-2 DP / 317-2 PN/DP 32 1024 bytes Yes
CPU 319-3 PN/DP 32 2048 bytes Yes
CPU 412-1 / 412-2 24 4096 bytes Yes (full)
CPU 414-2 / 414-3 32 4096 bytes Yes (full)
CPU 416-2 / 416-3 64 4096 bytes Yes (full)

If you write 50 sequential CALL FCn instructions back-to-back in OB1, you will not run out of BStack — the calls are stacked and unstacked sequentially, so the maximum depth at any instant is 1. What you will consume is scan time: each CALL takes ~5–10 µs of overhead (save RLO/STA, push return address, jump to FC, restore, return) plus the FC's own body. The "huge delay" that one engineer worried about does not exist for sequential unconditional calls; the delay is purely the cumulative body time of the 50 FCs.

Gotcha: A CALL FCn inside an A I 0.0 / JC block is still unconditional with respect to the RLO. Use CC FCn to gate the call on the current RLO, and UC FCn to call unconditionally. CALL FCn is the legacy unconditional form retained for compatibility.

S7-200 Special Memory (SMB) vs S7-300 System Resources

The S7-200 family ships with a rich Special Memory Byte (SMB) area that the S7-300/400 does not provide by default. The mapping for engineers porting S7-200 logic to S7-300 is:

SMB Range Function on S7-200 S7-300/400 Equivalent
SM0.0 Always_On (1 every scan) Implement via SET; = M 100.0 in OB1, or any FC
SM0.1 First_Scan_On (1 on first scan only) Set a flag inside OB100 (warm restart) or OB101/OB102 (hot/cold restart)
SM0.4 / SM0.5 1-min / 1-sec clock toggling Enable "Clock memory" in CPU hardware configuration and assign a marker byte (e.g. MB10)
SM0.6 / SM0.7 Scan-clock (2 × scan-time toggling) Use OB1_PREV_CYCLE TEMP for last-scan time, plus a clock memory byte
SMB1–SMB9 Error and instruction-status bits Diagnostic buffer + status bits in OB1 TEMP / OB100 TEMP
SMB28–SMB29 Analog-potentiometer raw values No equivalent on most S7-300 CPUs; S7-1500 reads analog inputs directly as tags
SMW22 / SMW24 Min / max scan time (ms) OB1_MIN_CYCLE and OB1_MAX_CYCLE in OB1 TEMP
SMB34 / SMB35 Timed-interrupt interval set Configure OB35 period in hardware config (Cyclic Interrupt, default 100 ms)

For an Always_On flag on the S7-300:

SET               // forces RLO = 1
=    M 100.0      // "always on" marker bit

For a First_Scan_On flag, set it in OB100 (or OB101/OB102 depending on restart type) and clear it in OB1 after the first scan:

// In OB100 (warm restart):
SET
=    "FirstScanDone"

// In OB1, at the end of the cycle:
AN   "FirstScanDone"
JC   _SKIP
// ... first-scan logic ...
SET
=    "FirstScanDone"
_SKIP: NOP 0
Important: The S7-300 does not have an inbuilt "first scan" OB that the user's S7-200 experience may have implied. OB100 is the warm-restart OB; the CPU enters it once after power-on or after a manual restart. If you place first-scan logic in OB1 it will run every cycle.

For clock bits (10 Hz, 5 Hz, 2.5 Hz, 2 Hz, 1.25 Hz, 1 Hz, 0.625 Hz, 0.5 Hz), open the CPU's properties in HW Config (Step 7) or device configuration (TIA Portal), tick "Clock memory", and assign a marker byte (commonly MB10 through MB255, excluding bytes used by the process).

OB1 TEMP: Scan-Time Variables for Free

The S7-300/400 OB1 block exposes a set of TEMP variables that the CPU updates every cycle. The most useful for diagnostics and trending:

TEMP Type Meaning
OB1_EV_CLASS BYTE Event class (B#16#11 = OB1 standard scan)
OB1_SCAN_1 TIME Scan time of the previous OB1 pass (ms resolution as TIME)
OB1_PRIORITY BYTE Priority class (default 1 for OB1)
OB1_OB_NUMBR BYTE OB number (1)
OB1_RESERVED_1 BYTE Reserved
OB1_RESERVED_2 BYTE Reserved
OB1_PREV_CYCLE TIME Previous-cycle execution time (ms, as TIME)
OB1_MIN_CYCLE TIME Minimum cycle time since last cold restart
OB1_MAX_CYCLE TIME Maximum cycle time since last cold restart
OB1_DATE_TIME DATE_AND_TIME Date/time OB1 was called (BCD-encoded)

These are read-only from your program. To log them to a DB, move them at the top of OB1:

L    #OB1_PREV_CYCLE
T    "Diag".LastCycle
L    #OB1_MAX_CYCLE
T    "Diag".MaxCycle
L    #OB1_MIN_CYCLE
T    "Diag".MinCycle
L    #OB1_DATE_TIME
T    "Diag".StampDTL

For a full list of OB1 TEMP variables, refer to the Siemens SIMATIC S7-300/400 OB block reference.

FB vs Direct Code: Performance and Memory Trade-off

A field case that is regularly reported on S5/115U and older S7-300 systems: an engineer wraps every conditional move in an FB call to "act like a one-shot" or "make a conditional transfer". Inlining those FBs into direct flag/network code produces a dramatic scan-time improvement. Two causes drive this:

  1. FB call overhead. Every CALL FB1 consumes 6–12 µs of marshalling time (parameter copy from instance DB to LStack, BStack push/pop, DI register setup, return). For a one-line "conditional transfer" that is 90 % overhead and 10 % useful work.
  2. Instance DB load. The FB's IN / OUT / STAT / TEMP variables are read from the instance DB on entry, copied to LStack, and (for OUT) written back on exit. A "small" FB with 2 IN / 1 OUT still marshals three values per call.

Modern S7-300 CPUs (315-2 PN/DP, 317, 319) and all S7-1500 CPUs have partially closed the gap via optimised FB calls (TIA Portal's "Optimised block access" and the multi-instance DB compiler pass can inline single-instance FBs), but the principle still holds: do not wrap a single STL instruction in an FB for "code clarity". If you need a one-shot, write the two-network pattern using the FP edge detection instruction directly:

A    "RunCmd"
FP   "RunEdge"      // FP uses RunEdge's own bit as edge memory
JCN  NOOP
// rising-edge body here
NOOP: NOP 0

The FP instruction handles the edge memory bit for you; no FB needed. Reserve FBs for reusable, parameterised code (PID blocks, valve sequencers, recipe state machines), not for one-line conditional moves.

Verification: A Worked Example

Combine the patterns into a complete STL block that runs an axis only when three sensor inputs agree, sets a command word only on the rising edge, and logs the previous scan time to a DB.

// Network 1: three-input AND
A    "E1_3"
A( 
L    "sMM1"
L    "SPsMM1"
==I
) 
A( 
L    "sMM2"
L    "SPsMM1"
==I
) 
=    "AllEqual"

// Network 2: rising-edge one-shot on AllEqual
A    "AllEqual"
FP   "AllEqualEdge"
=    "StartCmd"

// Network 3: conditional transfer — only write on the edge
A    "AllEqualEdge"
JCN  NOOP
L    20
T    "AxisSpeed"
NOOP: NOP 0

// Network 4: log scan time
L    #OB1_PREV_CYCLE
T    "Diag".LastCycleMs
L    #OB1_MAX_CYCLE
T    "Diag".MaxCycleMs

Walk-through of what the CPU does each scan:

  1. Network 1 sets AllEqual only when E1_3 == 1 and the two INT compares both succeeded. The A( ) blocks save each comparison RLO onto the OR stack and AND it with the next.
  2. Network 2 captures the rising edge of AllEqual into AllEqualEdge using the FP instruction's internal edge memory bit.
  3. Network 3 uses Idiom B: JCN NOOP skips the transfer unless the edge fired. No FB, no MOVE block, no race window.
  4. Network 4 reads the OB1_PREV_CYCLE TEMP into a DB for trending in WinCC.

Verify in STEP 7 / TIA Portal by setting breakpoints (or, in TIA Portal, using "Monitor/Modify" on the STW) and confirming:

  • RLO is 1 at the JCN NOOP location when AllEqualEdge == 1.
  • CC1/CC0 are both 1 (equal) immediately after each ==I.
  • The OR stack (visible in STL online monitor) collapses correctly after each closing ).
  • OB1_PREV_CYCLE reports a stable value within the configured max cycle time.

Troubleshooting Matrix

Symptom Likely Cause Fix
Transfer executes every scan, ignoring input Used A I x.y before L/T expecting gating Use JCN skip pattern (Idiom B), JC branch pattern (Idiom A), or A( block with conditional write
JC fires on the wrong condition Chained comparisons without A( ) or A ==I prefix Add A prefix to each comparison, or wrap in A( )
Compiler rejects L BR BR is a status-word bit, not an accumulator-loadable Use SAVE to write RLO to BR; read BR with A BR or via FB ENO
SF (System Fault) after CALL chain BStack overflow from recursive CALL Audit CALL graph; ensure no FC/FB calls itself directly or via mutual recursion
First-scan logic runs every scan Set FirstScan in OB1 instead of OB100 Move the SET+= from OB1 to OB100; clear FirstScan in OB1 if needed
Scan time spiked 5–10 ms after a refactor Inline logic replaced with multi-instance FB calls Inline small logic into direct networks; reserve FB for reusable, parameterised code
Clock bits (M10.0, M10.1, …) all 0 Clock memory not enabled in hardware configuration Open CPU properties → "Clock memory" → tick and assign a marker byte
Compiler reports "Cannot load status word bit" Tried L BR, L RLO, L OV Use A BR, JC, JO to test bits; do not attempt to load them into an accumulator
OB1_MIN_CYCLE / OB1_MAX_CYCLE always 0 Read before the first cold restart has elapsed Cycle counters reset on cold restart; wait one full warm restart or read after power-on
SAVE inside an FC followed by CC FB returns wrong ENO SAVE consumed by the FB's ENO write Move the SAVE after the FB call, or use JCB / JNBI to preserve the saved RLO across the call

How do I make a conditional Move in STL on S7-300/400?

Use the skip pattern: AN I 0.0 followed by JC _SKIP to bypass the L/T pair when the guard is false, or JC _DO to jump to the L/T pair when the guard is true. L and T themselves never condition on the RLO, so you must guard the transfer with a jump or an A( ) block.

Why does my second ==I overwrite the first comparison's result?

Each ==I replaces the RLO with the comparison result. To AND two compares, wrap them in A( ... ) blocks, or prefix the second ==I with A (e.g. A ==I) so the CPU ANDs the new result with the previous RLO.

Can I load the BR bit into ACCU1 with L BR?

No. BR is status-word bit 8 and is not loadable on S7-300/400 (the same rule applies to S7-1500). Use SAVE to write the current RLO into BR, and A BR / AN BR to test it. You can also read BR via the ENO of a called FB.

How do I get an Always_On or First_Scan_On bit on S7-300?

Set a flag in OB1 with SET; = M 100.0 for Always_On. For First_Scan_On, set a flag inside OB100 (warm restart) and clear it after the first execution in OB1; do not place first-scan logic in OB1 itself, because OB1 runs every cycle.

Is there a limit to how many CALL FC instructions I can chain in OB1?

No hard count, but a single STL network is capped at 999 lines and the BStack depth (8–64 depending on CPU) limits simultaneous nesting. Sequential unconditional calls only ever use depth 1, so 50 calls back-to-back are safe — the scan-time cost is the sum of the FCs' execution times plus roughly 5–10 µs call overhead each.

Why did my scan time drop dramatically when I inlined a small FB into a direct network?

FB calls add 6–12 µs of parameter-marshalling and BStack overhead per invocation. For a one-line conditional move, that overhead dwarfs the useful work. Inline trivial logic into direct networks and reserve FBs for reusable, parameterised code (PID blocks, sequencers, recipe state machines). Modern S7-300/400 firmware plus TIA Portal's "Optimised block access" partially closes the gap, but the rule of thumb still holds for S5-ported code on legacy CPUs.

Back to blog