Siemens STEP 7 STL: Decoding Auto-Generated LAD/FBD Code

David Krause23 min read
HMI ProgrammingSiemensTechnical Reference
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

Overview

When a Siemens S7-300/400 programmer writes logic in LAD (Ladder Diagram) or FBD (Function Block Diagram) inside Siemens STEP 7 V5.x, the editor stores the network in an internal representation that maps cleanly onto every IEC 61131-3 textual view. Opening the same block in STL (Statement List) reveals a stream of A, =, JNB, L, T, +D, *D, /D, and NOP 0 instructions that look unfamiliar to engineers trained on hand-written STL. The unfamiliar pieces — local variable L 12.0, jump labels _015/_016/_017, JNB guards, and trailing NOP 0 placeholders — are not stylistic choices. They are the mechanical translation of the LAD/FBD EN/ENO chain, the local stack snapshot pattern, and the network boundary markers that the STEP 7 compiler inserts to keep the three views round-trip safe.

This reference decodes one such auto-generated STL network produced from a LAD diagram and explains every construct: the reason M 0.1 is copied to L 12.0, why JNB _015 guards the addition, what _015 actually marks, and how the four arithmetic operations are sequenced through ACCU 1 and ACCU 2. It also documents the side-effect that STEP 7 sometimes loses the LAD view when the local stack addresses used by the compiler fall inside the declared TEMP area of the block, and gives the field-proven offset procedure to restore the graphical display.

All code samples in this article target the S7-300/400 CPU family running STEP 7 V5.4 through V5.7. STL on S7-1200/1500 (TIA Portal) uses a different language called SCL for structured text; the auto-generated STL pattern described here does not apply to TIA Portal's LAD/FBD, although the underlying EN/ENO concept is preserved.

LAD/FBD/STL Language Triad and Round-Trip Rules

STEP 7 V5.x implements the three IEC 61131-3 textual/graphical languages as projections of a single internal network model. The relevant manuals for the S7-300/400 system and the S7-400 CPU describe the triad in the STEP 7 programming reference set and the Programming with STEP 7 manual:

  • LAD (Ladder) — contact/coil network, top-to-bottom current-rail execution.
  • FBD (Function Block Diagram) — box-and-wire network, left-to-right signal flow.
  • STL (Statement List) — accumulator-based textual instruction list, the only one of the three that exposes the underlying CPU registers.

The editor is free to switch between views because the internal representation is closer to a directed acyclic graph of operations than to any of the three syntaxes. The trade-off is that any view-restoration code the editor injects to keep round-tripping safe will be visible only in STL. Hand-edited STL round-trips back to LAD as long as the constructs used can be expressed in the contact/box model. Constructs that cannot — for example, explicit ACCU manipulation, indirect memory addressing, or block calls with non-standard parameter passing — appear as "STL-only" segments and the editor will refuse to switch back to LAD.

Round-trip eligibility is computed per network, not per block. A single STL-only network inside an otherwise pristine block is enough to put the entire block into the "cannot show as LAD" bucket. The decision is also sticky: once the editor has decided a block is STL-only, the next compilation will preserve the L-stack snapshot pattern even if the offending construct is removed. The usual fix is to re-paste the network from a backup, or to manually re-trigger the LAD rebuild by toggling the ENO property of every box in the affected network.

EN/ENO Mechanism and the JNB Scaffold

Every LAD/FBD box that represents a function or function block has an EN (Enable) input on the left edge and an ENO (Enable Output) on the right edge. The first box in a network receives the network's left power rail. Each subsequent box's EN is wired to the previous box's ENO. The result of this cascade is that the failure of any one box halts the rest of the chain and clears the BR (binary result) bit so that downstream boxes are skipped.

STL has no native EN/ENO syntax, so the editor encodes the chain explicitly with three primitive operations:

  1. Snapshot the EN condition into a private local bit before the box body runs.
  2. Guard the box body with JNB <label> — Jump if RLO = 0 to a label placed at the end of the box body. If the EN snapshot is FALSE, the entire body is skipped, ACCU 1 is reset to 0, and BR is cleared so the next box's ENO becomes FALSE. After the JNB, RLO is set to 1 unconditionally; this lets the next box's ENO propagate cleanly through an OR chain.
  3. Mark the box body end with the label _nnn followed by NOP 0 — the label is a jump target; NOP 0 is a no-operation placeholder that the editor overwrites if it later needs to insert a falling-edge, negation, or P/N flag on the ENO branch.

The state diagram below shows the three states an auto-generated EN/ENO box can be in at runtime and the transitions triggered by the JNB / label scaffold.

State 1: EN snapshot = TRUEBody executes, BR := 1 State 2: EN snapshot = FALSEJNB jumps to _nnn, BR := 0 State 3: Box error (OV, /0)BR := 0, downstream skipped _nnn: NOP 0Merge point; ENO := BR; next box EN := this ENO TRUE: RLO := 1 FALSE: RLO := 1 ERROR: RLO := 0

The label naming convention _nnn is reserved for the compiler; hand-written STL must never use this form because subsequent compilations will collide with the auto-generated counter. Siemens documents the reserved label prefix in the STEP 7 STL reference manual for S7-300/400 and in the built-in help under "Reserved identifiers and labels".

Local Stack, Block Interface, and TEMP Layout

Every code block (OB, FB, FC, SFB, SFC) on S7-300/400 has a private 256-byte local stack frame (L 0.0 through L 255.7). The frame is allocated on block entry and released on block exit; it is never shared with other blocks running at the same priority. The frame holds:

Offset (bytes) Purpose Configured by
0 Block parameter save area (FB multi-instances and FC parameter copy-back region) Compiler
0–31 (typical) Compiler scratch (ACCU 1/2 spill, BR, RLO/STA/OR save) Compiler
32+ User-declared TEMP variables from the block interface Programmer
End of frame Auto-generated L snapshots for EN/ENO chains, working bits, and any local the compiler decided to materialize Compiler

The exact boundary between compiler scratch and user TEMP depends on the operations compiled into the network. Because the boundary is implicit, an L address that the compiler chose for an EN snapshot can collide with a TEMP the programmer declared manually. The collision is silent — both refer to the same byte — and is the root cause of the LAD-view loss described in the field report.

The block interface has five sections: IN, OUT, IN_OUT, STAT (instance state, FB only), and TEMP. Only TEMP variables live in the L frame. The others live in the instance DB (FB) or in the input/output image (FC). The compiler will never relocate STAT or IN_OUT into the L frame, so a collision can only happen between two TEMP declarations or between a TEMP and a compiler-chosen L scratch byte. FCs have no STAT section; all of their interface variables are passed via the L frame's parameter copy region at the bottom of the frame.

For S7-300 CPUs the L frame is always 256 bytes. For S7-400 CPUs the size is configurable per priority class in the CPU properties (default 256 bytes per priority). When a priority class overflows its L frame, the CPU goes to STOP with a local-data overflow diagnostic buffer entry. The relevant S7-400 diagnostic codes are described in the S7-300/400 CPU specifications manual.

Line-by-Line Decode of the Example STL

The reference program (taken verbatim from the source thread) is reproduced below and annotated in the table that follows.


A     M       0.1
=     L     12.0
A     L     12.0
JNB   _015
L     DB30.DBW  100
L     L#200
+D
T     #AC1
_015: NOP   0
A     L     12.0
JNB   _016
L     #AC1
L     L#60
*D
T     #AC1
_016: NOP   0
A     L     12.0
JNB   _017
L     #AC1
L     DB30.DBW   54
/D
T     #AC0
_017: NOP   0
Line STL What it does ACCU 1 after ACCU 2 after
1 A M 0.1 Build RLO from the always-1 flag bit M 0.1 (declared constant in this block). unchanged unchanged
2 = L 12.0 Write RLO into the compiler-chosen local bit L 12.0. This is the EN snapshot. unchanged unchanged
3 A L 12.0 Re-read the EN snapshot into RLO (decouples RLO from the live M 0.1). unchanged unchanged
4 JNB _015 If the EN snapshot is FALSE, jump over the first box body to label _015. 0 (reset on jump) unchanged
5 L DB30.DBW 100 Load the 16-bit word operand into ACCU 1; zero-extend to 32 bits. DBW100 previous ACCU 1
6 L L#200 Load the 32-bit DINT literal L#200 into ACCU 1. L#200 DBW100 (sign-ext)
7 +D Add ACCU 2 + ACCU 1 as 32-bit DINT. DBW100 + 200 undefined
8 T #AC1 Transfer ACCU 1 to the interface TEMP #AC1 (DINT). unchanged unchanged
9 _015: NOP 0 End of the first box body. NOP 0 is a placeholder. — —
10 A L 12.0 Re-load the EN snapshot for the second box. — —
11 JNB _016 Guard the second box body. 0 on jump —
12 L #AC1 Reload the partial sum from the previous box. sum previous ACCU 1
13 L L#60 Push multiplier constant L#60 into ACCU 1; #AC1 shifts to ACCU 2. L#60 sum (sign-ext)
14 *D 32-bit DINT multiply. sum * 60 undefined
15 T #AC1 Store the product back into #AC1. unchanged unchanged
16 _016: NOP 0 End of the second box body. — —
17 A L 12.0 Re-load the EN snapshot for the third box. — —
18 JNB _017 Guard the third box body. 0 on jump —
19 L #AC1 Reload the product from the previous box. product previous ACCU 1
20 L DB30.DBW 54 Load the divisor operand; #AC1 shifts to ACCU 2. DBW54 product (sign-ext)
21 /D 32-bit DINT divide; quotient in ACCU 1, remainder in ACCU 2. quotient remainder
22 T #AC0 Store the quotient into the result TEMP #AC0. unchanged unchanged
23 _017: NOP 0 End of the third box body and end of the network. — —

The complete mathematical expression encoded by the three boxes is:

AC0 = ((DB30.DBW100 + 200) * 60) / DB30.DBW54

with the L# prefix on the constants confirming that the entire pipeline is 32-bit DINT. The use of DBW (16-bit word) on the data block inputs and the L# constants on the literals is a common pitfall: the implicit widening from 16 to 32 bits happens in ACCU 1 only on the second L instruction, so the sequence L DBW / L L# / +D is correct, but the order L L# / L DBW / +D would sign-extend the DBW into the lower 16 bits of ACCU 1 and corrupt the result if the DBW was negative.

The M 0.1 to L 12.0 Snapshot Pattern

Copying the always-1 flag from M 0.1 into L 12.0 looks redundant on first reading. It is not. The reason is that the network may call other blocks (or be called by other blocks) whose execution can change the value of M 0.1 mid-network. M memory is global to the entire CPU and survives block boundaries. The L stack is per-block and dies on BE. By snapshotting M 0.1 into L 12.0 once at the top of the network, the compiler ensures that the EN condition seen by every box is the value at network entry, not whatever M 0.1 happens to be when the JNB is evaluated inside a deeper call.

The address L 12.0 is whatever byte in the L frame the compiler decided to use. The choice is deterministic for a given STEP 7 build, but is not part of any public API. Hand-written STL that targets the same compiler output should mirror the snapshot, or replace it with a named TEMP that the programmer declares manually, so that round-trip to LAD is preserved. If you write the snapshot by hand, the rule is "one A, one =, snapshot the EN once, never re-read the source operand directly".

A subtler reason for the snapshot is the way the LAD/FBD compiler handles power-rail state. The compiler treats the network's left power rail as a single boolean that the editor will set or clear based on the network's first contact. Once the editor has done the snapshot, the rail state is private to the network and cannot be affected by anything outside the network. This is a more reliable EN source than any global M bit, which is why the snapshot is mandatory even when the source operand is a "constant 1".

Jump Labels, Steps, and JNB Semantics

The labels _015, _016, _017 are jump targets, not "steps" as the original question called them. The underscore prefix is reserved for compiler-generated labels; the numeric suffix is a monotonically increasing counter that the editor maintains per block. Hand-written STL must not use this form because:

  • Re-compilation can rename or reuse a label, silently breaking a JNB that the programmer thought was pointing somewhere stable.
  • The STEP 7 online help and STL reference document the underscore prefix as reserved; using it for a user label is an unsupported deviation.
  • The label counter is reset every time the block is compiled from LAD/FBD, so a hand-written JNB pointing to a compiler label will be silently redirected when the network is re-rendered.

If you need a stable user jump target in hand-written STL, use a meaningful name (e.g., CALC_DONE: NOP 0) and place the label on a line by itself. The compiler does not rename non-underscore labels because the LAD/FBD generator never needs to inject control flow into them. The label counter is the underscore form; the colon after the label is the S7 syntax for declaring a label inline.

The JNB instruction itself has two effects that engineers sometimes miss. First, on a taken jump, ACCU 1 is reset to 0 and ACCU 2 is undefined. This is the mechanism the compiler uses to clear the box's "result so far" when the box is skipped. Second, RLO is set to 1 after the JNB regardless of whether the jump was taken. This is what makes the ENO cascade work: every box ends with RLO = 1, and the next box's EN = previous ENO = BR = (RLO at merge point). Without the RLO := 1, the next JNB would not see the ENO at all.

ACCU 1 / ACCU 2 Math Pipeline

All arithmetic on the S7-300/400 CPU runs through two 32-bit accumulators. The load instruction L shifts the previous ACCU 1 into ACCU 2 and writes the new operand into ACCU 1. The arithmetic operations +D, -D, *D, /D consume both accumulators and write the result to ACCU 1. The diagram below shows the pipeline for the first box of the example.

Step 5: L DBW 100 ACCU 1: DBW 100 (16-bit) ACCU 2: undefined Step 6: L L#200 (shifts) ACCU 1: L#200 (32-bit) ACCU 2: DBW 100 (sign-ext) Step 7: +D (consumes both) ACCU 1: DBW100 + 200 ACCU 2: undefined Step 8: T #AC1 (store) #AC1 (TEMP DINT) = sum

The key rules to remember when reading or hand-writing math STL:

  • Stack order matters. L A / L B / +D computes A + B; L B / L A / +D computes B + A. For commutative operators the result is the same; for non-commutative ones (-D, /D) the order determines the sign.
  • After /D, ACCU 2 contains the remainder and ACCU 1 contains the quotient. Use MOD to extract the remainder as a separate operation if you need it.
  • Division by zero on /D sets OV (overflow) and OS (stuck overflow). The box's ENO becomes FALSE and the downstream JNB chain halts. Trap this by testing OV before the next JNB with JO <label>.
  • Constant prefix matters: L 200 loads a 16-bit INT and zero-extends to 32 bits; L L#200 loads a 32-bit DINT literal. Use the form that matches the data type you want the math to operate on.
  • Real-number math uses L with a real literal, then +R, -R, *R, /R. The accumulator width is 32 bits for REAL; use LDR / ADR / MDR / SDR for 64-bit LREAL (available on S7-400 from CPU 412 onward).

The TEMP Variable Overlap Problem and Repair Procedure

STEP 7's LAD/FBD editor stores the network in a graphical representation. When the network contains constructs the editor cannot fully render — typically anything the user wrote in STL and pasted in, or a block call with non-default parameter passing — the editor will sometimes "fall back" to displaying the network as raw STL on the next open. The reason given in the field report is that the L addresses the compiler chose for EN snapshots fall inside the user-declared TEMP area, and the editor decides the network is no longer round-trippable.

The check the editor performs is conservative: if any L x.y address used in the network overlaps with the byte range of a declared TEMP, the editor refuses to re-render the network as LAD. The fix is to relocate the L references the compiler is using to an offset above the highest TEMP byte. Because the L frame is fixed at 256 bytes, any free region above the largest TEMP will do. The most common choice in field practice is to add a fixed offset — typically 100 — to every L access in the block. The editor will re-scan, find the L references are now safely above the TEMP range, and rebuild the LAD view.

If you add an offset to a compiler-generated L address, you must add the same offset to every L access in the same block, including the ones in TEMP variable references. The compiler is unaware of the manual shift; a partial shift will leave dangling L references and may corrupt execution.

Field-proven repair procedure for restoring the LAD view of a block whose networks have reverted to raw STL because of L / TEMP overlap. Apply the procedure per-block, not per-project.

  1. Open the affected FB/FC in STEP 7 V5.x and switch to the STL view.
  2. Use Options > Block Check (or Edit > Check Block Consistency) to confirm the only complaints are L-address / TEMP overlap warnings, not real compile errors.
  3. Determine the highest byte offset of any declared TEMP. Open the block interface; the TEMP area starts at the first declared TEMP and runs to the end. Note the highest L byte referenced (e.g., L 31.7 is byte 31).
  4. Pick an offset that places all compiler-generated L references safely above the TEMP area. A field-tested value is 100 (matches the example in the field report). Add the offset to every L access in the block:
    • L 12.0 becomes L 112.0
    • L 12.1 becomes L 112.1
    • And so on for every L reference in the block.
  5. Use Edit > Find and Replace with the regex L\s+\d+\.\d+ to make sure no L access is missed. Cross-check by compiling with PLC > Compile; the compiler will report any L reference it cannot resolve.
  6. Save the block to the offline S7 program, then switch the view to LAD. The editor should now rebuild the graphical representation.
  7. Download the corrected block to the CPU with PLC > Download. The next online open of the block should show the LAD view.
  8. Re-test the program on the bench (or in PLCSIM) to confirm that the shift did not change runtime behavior. The L frame contents above the TEMP area are scratch, so a shift in scratch should not affect any signal the program can observe.
The offset is a per-block decision. Different blocks may have different TEMP sizes and therefore need different offsets. Document the offset in the block header comment so the next maintainer does not undo it.

Troubleshooting Matrix: Common STL-View Regressions

Symptom Likely cause Fix Verify with
Network displays as raw STL on every open L reference collides with declared TEMP Shift L references above TEMP area (add 100) Switch view to LAD, re-save
Network displays as STL only on some blocks Inconsistent TEMP layout across the block Reorganize TEMPs at higher offsets, recompile Block Check consistency report
ENO does not propagate to next box ENO branch was hand-edited and lost the negation flag Restore ENO from the box properties dialog Force EN=FALSE; observe downstream ENO
Math result is wrong after a recompile Constant prefix lost (200 vs L#200) on save Use named constants in a DB; never use bare literals VAT watch on the result
CPU goes to STOP on first scan Indirect addressing written in STL fails to round-trip Move the indirect access into SCL; keep STL for housekeeping only Diagnostic buffer "local-data overflow"
Block check reports "local data area overlap" Two TEMPs or a TEMP and a compiler scratch byte at the same offset Rename one of the colliding TEMPs to force the compiler to reallocate Block Check; LAD round-trip
OV stays set after /D, BR stays cleared Divide-by-zero not trapped; ENO chain dead Insert JO <safe_label> after every /D Force divisor = 0, watch BR in VAT
Compiler chooses a different L address on every save TEMP layout has a hole and the compiler backfills it Declare all TEMPs at the top of the interface, contiguous Diff the STL view across two saves
JNB jumps to the wrong label after a recompile Hand-written STL used the reserved _nnn form Rename the user label to a non-underscore form Cross-reference; consistent behavior across recompiles

Best Practices, Monitoring, and SCL Migration

The following rules keep the three views round-trippable and avoid the L/TEMP overlap class of bug. They are derived from the S7-300/400 STL/LAD/FBD programming manuals and from field experience with the auto-generated scaffold pattern described in this article.

Practice Reason Manual reference
Declare every local you reference as a TEMP Prevents the compiler from picking an L address inside your scratch area STEP 7 block interface manual
Use the "Generated STL" view for review only; edit in LAD/FBD where possible Hand edits to auto-generated STL are the most common cause of the overlap STEP 7 LAD/FBD editor manual
Never use the _nnn label form in hand-written STL Reserved by the compiler; will collide on recompile STEP 7 STL reference
Document any manual L-address shift in the block header Future maintainers will undo undocumented shifts Block header convention
Use SCL for math-heavy blocks SCL compiles to STL but is round-trippable to LAD/FBD; the auto-generated STL pattern is hidden from the source S7-SCL programming manual
For the EN snapshot bit, use a named TEMP of type BOOL Makes the network readable in STL view without losing round-trip STEP 7 STL reference
Test with PLCSIM before downloading to the live CPU STEP 7 will catch some overlaps at compile, but not all S7-PLCSIM manual
Always trap OV after a /D if the divisor can be zero A divide-by-zero clears BR and breaks the ENO cascade STEP 7 status word manual
Keep TEMPs contiguous at the top of the interface Compiler backfilling into holes picks addresses that change every save Block interface convention

The auto-generated STL pattern is efficient and round-trippable for short math chains, but it scales poorly. If a block contains nested math, conditional execution, or any kind of loop, the equivalent SCL is one-tenth the line count and round-trips to LAD/FBD without the EN/ENO scaffolding. SCL is the recommended language for:

  • Any block with more than three math operations chained together.
  • Any block that needs IF / CASE / FOR / WHILE control flow.
  • Any block that needs to access array elements by computed index.
  • Any block that needs to be commissioned by a control engineer who is more comfortable with high-level languages than with ACCU semantics.

Migration rule of thumb: if the equivalent LAD/FBD is a single network, keep it in LAD/FBD. If it would be a multi-network chain of math boxes, rewrite in SCL and call the SCL FC from the LAD network. The SCL FC will still be visible to the cross-reference tool and to the symbol table, so the migration loses nothing in terms of maintenance access. STEP 7 V5.7's SCL compiler emits STL that uses the same EN/ENO scaffold, so the run-time behavior is identical; the only difference is the source representation.

Verification Checklist

Before considering a LAD/FBD/STL round-trip issue resolved, run through the following checks. All of them are available offline in PLCSIM and online against the live CPU.

  • Block check passes — Options > Block Check reports no errors and no L/TEMP warnings.
  • View switch test — Switch the block from STL to LAD to FBD and back; the editor should not fall back to STL on any view.
  • Cross-reference intact — Options > Reference Data > Display shows the same cross-references before and after the shift.
  • PLCSIM run — Download to PLCSIM, force the EN condition to FALSE, then TRUE, and confirm the math result matches a hand calculation.
  • Runtime check — On the live CPU, monitor the result variable in a VAT and confirm the math expression evaluates correctly for at least one full production cycle.
  • ENO cascade — With the EN condition forced FALSE at network entry, confirm via VAT that all downstream boxes are skipped (their ENOs are FALSE).
  • BR propagation — Force a divide-by-zero on the /D instruction; confirm BR is cleared and downstream JNB jumps to their labels.
  • L frame footprint — On S7-400, monitor the priority-class local-data usage in the diagnostic buffer to confirm the block does not push the priority over the configured limit.

FAQ

What does STL stand for in Siemens STEP 7?

STL stands for Statement List, the textual IEC 61131-3 language for the S7-300/400 CPU family. It is unrelated to the STL file format used for 3D printing; the Wikipedia article on STL (file format) describes a stereolithography 3D geometry exchange format and is not applicable to PLC programming.

Why does the auto-generated STL use jump labels like _015, _016, _017?

Those are compiler-generated jump targets that delimit each LAD/FBD box body for the EN/ENO cascade. The underscore prefix is reserved; the numeric suffix is a per-block counter. The label is followed by a NOP 0 placeholder that the editor can overwrite if it later needs to insert edge detection, negation, or P/N flags on the ENO branch.

What is the purpose of copying M 0.1 into L 12.0 at the top of the network?

It snapshots the EN condition into the per-block local stack so that the value seen by every JNB is the network-entry value of M 0.1, not whatever the global M bit has become after intervening block calls. Without the snapshot, the EN chain would race with global memory writes from nested blocks, and a downstream JNB could see a different value than the upstream snapshot.

Why does STEP 7 sometimes display my LAD block as raw STL?

Usually because an L address the compiler chose for an EN snapshot or scratch bit falls inside the byte range of a declared TEMP. The editor conservatively refuses to round-trip the network and falls back to STL. The fix is to shift the L references above the TEMP area using a fixed offset (e.g., add 100 to every L access in the block) and re-save the block to trigger the rebuild.

Can I edit auto-generated STL directly and still keep the LAD view?

You can, but only if every construct you use has a LAD/FBD equivalent. The auto-generated L12.0 snapshot, the JNB / _nnn / NOP 0 scaffolding, and the ACCU-based math are all valid round-trip targets, so you can edit them. Constructs that break round-trip include indirect memory addressing, multi-instance block calls with non-default parameter passing, and explicit ACCU manipulation. Use SCL if your block contains any of those.

How do I trap a divide-by-zero inside an auto-generated math chain?

After the /D instruction, insert JO <safe_label>. The JO instruction jumps to safe_label if the OV (overflow) bit is set, which is what /D does on a zero divisor. The safe_label should reset BR, clear ACCU 1, and let the ENO cascade propagate the failure to the next box's EN, which the compiler will already have wired to the next JNB guard.

Back to blog