Siemens STL Code Analysis: Tracing a Compiled SCL Function Block

David Krause15 min read
SiemensTechnical ReferenceTIA 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

Siemens STL Code Analysis: Tracing a Compiled SCL Function Block

Overview

Statement List (STL) is the text-based, low-level IEC 61131-3 programming language for SIMATIC S7-300, S7-400, S7-1200 and S7-1500 controllers. It is the native machine representation of the STEP 7 runtime. Structured Control Language (SCL) is the higher-level, Pascal-like language that compiles down to STL networks. When a project is delivered without the SCL source — for example, when blocks were compiled and then have the source encrypted, deleted, or simply never checked into the archive — you are left with STL listings that must be reverse-engineered by hand.

This document walks line-by-line through a real compiled STL block that the SCL compiler emits for a common "integer-to-packed-BCD with digit-count overflow" function. It explains every instruction, the contents of ACCU1 and ACCU2 at each step, the meaning of the labels M001 through M004, and how to recover an equivalent SCL source. The same techniques apply to any other compiled SCL block you encounter in STEP 7 V5.x, TIA Portal V15–V18, or the current V19 release.

Before continuing, ensure you have access to the official SIMATIC references for the instruction set: the STEP 7 Programming Guideline and the SCL for S7-300/400 Programming Manual from Siemens Industry Online Support. For TIA Portal V18/V19 users, the S7-1200/1500 SCL Programming Manual documents the same instruction semantics.

Prerequisites

  • Familiarity with STEP 7 / TIA Portal block structure (OB / FB / FC / DB / IN / OUT / TEMP / STAT).
  • Basic understanding of the S7-300/400 or S7-1500 two-accumulators (ACCU1, ACCU2) execution model.
  • STEP 7 V5.5 SPx, TIA Portal V16, or any newer TIA Portal release with the STL editor enabled (note: in TIA Portal V18+ STL is opt-in and may need to be activated in the project options).
  • Watch table or STL Monitor for verification.
STL is a legacy view in TIA Portal. The editor still exists and the compiler still emits STL for every block, but the user interface hides it by default for LAD/FBD/SCL programming. You can switch any block to STL representation via Right-click block → Switch programming language → STL.

Why SCL Compiles to STL Networks

The SCL compiler does not generate native machine code. It parses the high-level statements, allocates temporaries, and emits a sequence of STL instructions equivalent to the abstract syntax tree. The block you see in the source snippet is therefore a faithful translation of an SCL function with the body roughly equivalent to:

FUNCTION_BLOCK digits_packed : WORD
VAR_INPUT
  IN0 : DINT;
END_VAR
VAR_TEMP
  TEMP1 : DINT;
  TEMP2 : DINT;
END_VAR
BEGIN
  TEMP1 := 0;
  TEMP2 := IN0;
  IF IN0 < 0 OR IN0 > 9990000 THEN
    digits_packed := 16#0000;
    ENO := FALSE;
    RETURN;
  END_IF;
  WHILE TEMP2 > 999 DO
    TEMP2 := TEMP2 / 10;
    TEMP1 := TEMP1 + 1;
  END_WHILE;
  digits_packed := SHL(WORD#16#0,0) OR (DTB(TEMP2) AND 16#0FFF) OR SHL(DTB(TEMP1),12);
END_FUNCTION_BLOCK

The labels M001, M002, M003, M004 are synthesized by the compiler as jump targets that encode the IF / WHILE control flow.

Symbolic Variable Map

Symbol Type Scope Role
IN0 DINT (L#) VAR_INPUT Input integer whose digit structure is to be packed.
TEMP1 DINT VAR_TEMP Loop counter: number of times TEMP2 was divided by 10 before reaching ≤ 999. Equals digit-count minus 3.
TEMP2 DINT VAR_TEMP Working copy of IN0 that is repeatedly integer-divided by 10 until it fits in 3 BCD digits.
RET_VAL WORD Return value (function result) Packed BCD result: bits 0–11 = BCD(TEMP2), bits 12–15 = BCD(TEMP1).
ENO BOOL Status output (BR bit) Set FALSE on input range violation; otherwise TRUE.

The Source Listing in STL

The block under analysis is reproduced below with line numbers. The right column is the human-readable comment that a programmer would normally see if they opened the SCL source in TIA Portal.

  1  SET              ; RLO := 1
  2  SAVE             ; BR := 1   (ENO will be TRUE unless cleared)
  3  L     0          ; ACC1 := 0
  4  T     #TEMP1     ; TEMP1 := 0
  5  L     #IN0       ; ACC1 := IN0
  6  T     #TEMP2     ; TEMP2 := IN0
  7  L     0          ; ACC1 := 0
  8  <D               ; ACC2 (IN0) < ACC1 (0) ?
  9  JC    M001       ; if TRUE jump to M001 (error path)
 10  TAK              ; swap ACC1/ACC2 (restore IN0 to ACC1)
 11  L     L#9990000  ; ACC1 := 9990000
 12  <=D              ; ACC2 (IN0) <= ACC1 (9990000) ?
 13  JC    M002       ; if TRUE jump to M002 (main path)
 14  L     W#16#3999  ; ACC1 := W#16#3999
 15  JC    M001       ; unconditional-looking jump to M001 (dead code path)
 16  M002: L     #TEMP2
 17  L     L#10
 18  /D               ; ACC1 := TEMP2 / 10
 19  T     #TEMP2
 20  L     L#999
 21  <=D              ; ACC2 (TEMP2) <= ACC1 (999) ?
 22  JC    M003       ; if TRUE jump to M003 (done)
 23  L     #TEMP1
 24  INC   1
 25  T     #TEMP1
 26  JU    M002       ; unconditional loop back
 27  M003: TAK
 28  DTB               ; ACC1 := BCD of TEMP2 (TEMP2 was in ACC1)
 29  L     #TEMP1
 30  SLW   12         ; ACC1 := TEMP1_shl_12
 31  OW                ; ACC1 := ACC2 OR ACC1
 32  JU    M004
 33  M001: CLR
 34  SAVE             ; BR := 0   (ENO := FALSE)
 35  M004: T     #RET_VAL
 36  BE               ; block end
In STEP 7 the labels M001…M004 are compiler-generated jump targets. They are not user symbols; they map to internal branch IDs in the compiled network. If you switch the block to SCL the labels disappear.

ACCU1 / ACCU2 State Trace

The table below shows ACCU1 and ACCU2 after every executed instruction for two representative inputs. RLO is the binary result bit used by JC / JCN / JU. Negative values of IN0 or values above L#9990000 short-circuit through M001 and return 16#0000 with ENO cleared.

Case A: IN0 = L#12345 (positive, in range)

Line Instruction ACCU1 ACCU2 RLO / BR TEMP1 TEMP2 Comment
1 SET - - RLO=1 - - Initial RLO
2 SAVE - - BR=1 - - ENO := TRUE
3 L 0 0 old ACC1 - - - Load zero
4 T #TEMP1 0 old - 0 - Init counter
5 L #IN0 12345 0 - 0 - Load input
6 T #TEMP2 12345 0 - 0 12345 Copy input
7 L 0 0 12345 - 0 12345 Prepare comparison
8 <D 0 12345 RLO=0 (12345<0 false) 0 12345 Sign check
9 JC M001 not taken Sign test passed
10 TAK 12345 0 - 0 12345 Swap back
11 L L#9990000 9990000 12345 - 0 12345 Range upper
12 <=D 9990000 12345 RLO=1 (12345<=9990000) 0 12345 Range test
13 JC M002 taken Enter main path
16 L #TEMP2 12345 9990000 - 0 12345 Loop start
17 L L#10 10 12345 - 0 12345 Divisor
18 /D 1234 10 - 0 12345 Integer divide
19 T #TEMP2 1234 10 - 0 1234 Store quotient
20 L L#999 999 1234 - 0 1234 Threshold
21 <=D 999 1234 RLO=0 (1234<=999 false) 0 1234 Continue loop
22 JC M003 not taken More divisions needed
23 L #TEMP1 0 999 - 0 1234 Load counter
24 INC 1 1 999 - 0 1234 Increment
25 T #TEMP1 1 999 - 1 1234 Store counter
26 JU M002 taken Loop
16 L #TEMP2 1234 1 - 1 1234 Loop iter 2
17 L L#10 10 1234 - 1 1234 Divisor
18 /D 123 10 - 1 1234 Integer divide
19 T #TEMP2 123 10 - 1 123 Store
20 L L#999 999 123 - 1 123 Threshold
21 <=D 999 123 RLO=1 (123<=999) 1 123 Loop exit
22 JC M003 taken Exit loop
27 TAK 123 999 - 1 123 Bring TEMP2 to ACC1
28 DTB 16#0123 999 - 1 123 Convert to BCD
29 L #TEMP1 1 16#0123 - 1 123 Load counter
30 SLW 12 16#1000 16#0123 - 1 123 Shift to high nibble
31 OW 16#1123 16#0123 - 1 123 Bitwise OR
32 JU M004 taken Skip error path
35 T #RET_VAL 16#1123 16#0123 - 1 123 Store result
36 BE - - BR=1 1 123 Block end, ENO=TRUE

Final RET_VAL = W#16#1123. High nibble 1 is the BCD-encoded digit count above 3 (here, 5 digits total → 5 − 3 = 2? See note below). Low 12 bits 123 are the BCD-encoded last three digits of the input.

The high nibble in this example is 1 after only two loop iterations. That does not represent "total digit count" — it represents the number of times the value was divided by 10 until it fit in three decimal digits. For a 5-digit input that is two divisions. Whether the function interprets that nibble as overflow digits, page selector, or 7-segment digit index depends entirely on the calling context. Treat the nibble as an opaque count and consult the calling block.

Case B: IN0 = L#−1 (negative, out of range)

Line Instruction ACCU1 ACCU2 RLO / BR Branch
7 L 0 0 -1 - -
8 <D 0 -1 RLO=1 (-1<0) -
9 JC M001 taken To error path
33 CLR - - RLO=0 -
34 SAVE - - BR=0 ENO := FALSE
35 T #RET_VAL (previous ACC1) - - Stores whatever was in ACC1
36 BE - - BR=0 Block end

For IN0 = -1, RET_VAL retains the value from ACCU1 at the moment of the T instruction. In this trace ACCU1 is still 0 from line 3 (TEMP1 initial), so RET_VAL = W#16#0000. ENO is FALSE. This is the documented "input invalid" contract.

Case C: IN0 = L#10000000 (above upper bound)

Line Instruction RLO Branch
10 TAK - After <D RLO=0, IN0 restored to ACC1
11 L L#9990000 - -
12 <=D RLO=0 (10000000 > 9990000) Continue past JC
13 JC M002 not taken Fall through
14 L W#16#3999 ACC1 := 16#3999 Constant loaded (dead-strip hint)
15 JC M001 not taken — RLO=0 Fall through to M001 anyway
33 CLR + SAVE BR=0 Error path

Line 14–15 are a defensive pattern the compiler emits whenever an IF/ELSIF chain has an unbounded range. The JC M001 after the L W#16#3999 will never be taken because the previous comparison set RLO=0 and L does not change RLO, but the compiler emits the conditional jump anyway as a safe fall-through. The constant W#16#3999 is effectively dead code.

Reconstructing Equivalent SCL

From the trace above you can write back the original SCL source. The block returns a 16-bit word whose high nibble is a BCD digit counter and whose low 12 bits hold the BCD of the last three digits:

FUNCTION "FC_digits_packed" : WORD
{ S7_Optimized_Access := 'FALSE' }
VERSION : 0.1
   VAR_INPUT
      IN0 : DINT;
   END_VAR
   VAR_TEMP
      TEMP1 : DINT;
      TEMP2 : DINT;
   END_VAR

BEGIN
   TEMP1 := 0;
   TEMP2 := IN0;
   IF (IN0 < 0) OR (IN0 > L#9990000) THEN
      "FC_digits_packed" := W#16#0;
      ENO := FALSE;
      RETURN;
   END_IF;
   WHILE TEMP2 > L#999 DO
      TEMP2 := TEMP2 / L#10;
      TEMP1 := TEMP1 + L#1;
   END_WHILE;
   "FC_digits_packed" := SHL(IN:=DWORD_TO_WORD(DTB(TEMP1)),
                              N:=12) OR (DTB(TEMP2) AND W#16#0FFF);
   ENO := TRUE;
END_FUNCTION

Round-trip this against a known STL compile in your TIA Portal installation: the byte layout of the compiled STL must match the source listing exactly.

Why TAK Appears Twice

The TAK (toggle accumulator) instruction is the SCL compiler's way of preserving a value across a comparison. After L #IN0 in line 5, IN0 sits in ACCU1. The <D comparison in line 8 needs IN0 in ACCU2 and 0 in ACCU1, hence L 0 in line 7. Now ACCU1 holds the constant 0 and ACCU2 holds IN0, but the next step (loading L#9990000 for the upper bound check) needs IN0 back in ACCU2. TAK swaps them. The same pattern repeats at line 27 after the loop: the comparison leaves L#999 in ACCU1 and the just-divided TEMP2 in ACCU2, and DTB needs TEMP2 in ACCU1.

If you mentally remove both TAK instructions you can see that the compiler could have used TAR1/TAR2 AR manipulation or a second T to a scratch TEMP, but TAK is cheaper and side-effect free.

The Role of SET / SAVE / CLR

SET sets RLO to 1 unconditionally. SAVE copies the current RLO into the binary result bit (BR). The status output ENO of every STEP 7 block is wired to BR. The pattern SET; SAVE; at the top of a block therefore sets ENO to TRUE at block start. The mirror pattern CLR; SAVE; clears it. This is the standard STEP 7 idiom for explicit ENO control. See STEP 7 Programming Guideline, section 5.4 "EN/ENO handling".

Be aware that SAVE is sometimes emitted in places where it does not actually have any effect on ENO — for example, right after a comparison that sets RLO=1 inside a network that subsequently falls through without branching. This is benign. The SCL compiler may emit redundant SAVE instructions because its intermediate representation tracks ENO state conservatively.

Common Pitfalls When Reading Compiled SCL

  • Assuming JC vs JCN vs JU is meaningful. JC only jumps on RLO=1; JU jumps unconditionally. Compilers almost always use JC for IF/ELSIF and JU for loops, but you should always verify by inspecting RLO.
  • Ignoring the side effect of L on ACCU2. Every L instruction pushes the old ACCU1 down into ACCU2. If you only track ACCU1 you will miss implicit dependencies.
  • Forgetting that integer comparison is signed. <D, <=D, >D, >=D are 32-bit signed. For unsigned comparisons use <DW, <=DW, etc.
  • Misreading /D as floating-point division. /D is integer division. The remainder is dropped. For floating point use /R (REAL) or /LREAL on S7-1500.
  • Forgetting that SLW is a 16-bit shift. The accumulator is 32 bits wide on S7-300/400, but SLW truncates to 16 bits and shifts within that 16-bit word. On S7-1500 the same semantics apply but the underlying word width is 16 bits by definition.

Verification in the Online Watch Table

  1. Compile and download the block to the PLC.
  2. Open an online watch table and add DB_digits.IN0 and FC_digits_packed as points to monitor.
  3. Force IN0 to 12345. Observe RET_VAL = W#16#1123 and ENO = TRUE.
  4. Force IN0 to -1. Observe RET_VAL = W#16#0000 and ENO = FALSE.
  5. Force IN0 to 9990000. Observe RET_VAL = W#16#0999 (TEMP1=0 after one division would make 9990000→999000, then 99900, then 9990, then 999 — four iterations; TEMP1=4, so RET_VAL = W#16#4999). Trace by hand to confirm.
  6. Force IN0 to 10000001. Observe RET_VAL = W#16#0000 and ENO = FALSE.
In TIA Portal you can monitor the STL network directly by switching the block view to STL after download, then selecting "Monitor". Stepping is not supported in production firmware; use PLCSIM or PLCSIM Advanced for instruction-by-instruction stepping.

Tools for Further Reverse-Engineering

Tool Use case Source
TIA Portal "Switch programming language" Convert STL block back to SCL when source is missing but symbolic info is intact. Siemens TIA Portal
STEP 7 V5.5 SourceCompare Diff a re-compiled SCL source against an STL block to validate equivalence. Siemens STEP 7 V5.5
PLCSIM / PLCSIM Advanced Single-step execution and ACCU inspection. Siemens
S7-PLCSIM STL Watch Inspect ACCU1/ACCU2, AR1, AR2, DB, DI registers per instruction. Siemens
Library "LGF" (Library of General Functions) Open-source Siemens reference library with idiomatic SCL source to compare against. Siemens LGF GitHub mirror

Edge Cases and Boundary Behaviour

  • IN0 = 0: Loop body not entered, TEMP1=0, TEMP2=0, DTB(0)=W#16#0000, RET_VAL = 16#0000, ENO=TRUE.
  • IN0 = 999: Loop body not entered (TEMP2=999 satisfies ≤999 check before division), RET_VAL = 16#0999, ENO=TRUE.
  • IN0 = 1000: One division, TEMP2=100, TEMP1=1, RET_VAL = 16#1100.
  • IN0 = 9990000: Four divisions (9990000→999000→99900→9990→999), TEMP1=4, TEMP2=999, RET_VAL = 16#4999.
  • IN0 = 9990001: Falls through to M001 error path, RET_VAL=16#0000, ENO=FALSE.
  • IN0 = L#-2147483648 (DINT min): Signed comparison catches it on the <D test, error path, ENO=FALSE.
  • IN0 = L#2147483647 (DINT max): Greater than L#9990000, error path, ENO=FALSE.
  • IN0 = real value passed to a DINT IN: Conversion happens at the call site, not inside the FB. If the caller passes an out-of-range REAL the FB sees an undefined DINT and behaviour depends on the runtime.

Standards Compliance Notes

The function implements a strict subset of the IEC 61131-3 BCD conversion family. The behaviour is conformant with the BCD-to-Integer and Integer-to-BCD standard functions for inputs within the documented range. For inputs outside the range, the SCL compiler emits the explicit error-flag pattern (CLR/SAVE) that is the STEP 7 idiom for ENO-driven error propagation. When re-implementing the function in pure SCL, use RETURN early to keep the compiled STL close to this shape; do not use nested IFs that would produce additional labels.

If you need to certify the function for SIL 2/3 applications on an S7-1500F, consult the S7-1500F Programming Manual and re-run the converter through the F-channel compiler — some unsafe constructs are rejected.

Frequently Asked Questions

What does the DTB instruction do in Siemens STL?

DTB (Double Integer to BCD) converts the 32-bit signed integer in ACCU1 into its Binary-Coded Decimal representation, placing the result back into ACCU1. ACCU2 is unchanged. The result occupies only the low 24 bits (up to 8 BCD digits); out-of-range values set the OV bit. See the STEP 7 STL instruction reference for the full encoding.

Why does the function pack the result with SLW 12 and OW?

The output is a 16-bit word that holds two pieces of information: the digit counter in the high nibble (bits 12–15) and the BCD of the last three digits in the low 12 bits (bits 0–11). SLW 12 moves the counter into the high nibble; OW merges the BCD value into the low 12 bits using bitwise OR. This is the standard STEP 7 pattern for packing multiple BCD nibbles into a single word.

How do I view the SCL source for a block that only has STL in the project?

If the symbolic information and SCL source are still in the offline program, right-click the block in the project tree and choose Switch programming language → SCL. The editor will attempt to regenerate SCL from STL. If the source was deleted or encrypted, this fails — you must use the method in this document to reconstruct it manually.

What happens if I pass a negative number to a function like this?

The <D comparison on line 8 of the trace detects IN0 < 0 and jumps to M001, which executes CLR; SAVE; to clear ENO and stores W#16#0000 in RET_VAL. The calling block must check ENO before using the return value.

Is there a standard Siemens FC that produces this exact packed-BCD output?

No Siemens standard FC in the IEC library produces this exact format. The closest standard function is DTB (DINT-to-BCD) which converts the whole 32-bit value; the function in this article performs a digit truncation to 3 BCD digits plus a separate overflow counter. It looks like a customer or OEM-specific utility, possibly for a 7-segment display driver that selects digit groups via the high nibble. Always verify against the calling code's documentation.

Back to blog