Converting C Code to SCL for Siemens S7-1200 and S7-1500 PLCs

David Krause10 min read
SiemensTIA PortalTutorial / 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

Overview: SCL as the Siemens Implementation of IEC 61131-3 Structured Text

SCL (Structured Control Language) is Siemens' high-level textual programming language for the S7-1200, S7-1500, S7-300, and S7-400 controller families, edited inside the TIA Portal and (for legacy targets) STEP 7 V5.x. SCL is the Siemens implementation of the Structured Text (ST) language defined in IEC 61131-3. Because the IEC standard is the common ancestor, SCL is syntactically closer to Pascal than to C, and a C-to-SCL port is best approached as a C → Pascal → SCL two-step translation rather than as a direct C → SCL rewrite.

There is no Siemens-blessed automated C-to-SCL converter. Conversion is a manual, code-review-driven exercise that relies on a working knowledge of both languages and a precise understanding of what the C code is supposed to do. This reference documents every language feature you will encounter in embedded C, the SCL equivalent, and the limits where the port becomes impractical.

Prerequisites

  • Working knowledge of C (data types, pointers, structs, control flow) and of PLC programming under TIA Portal.
  • Access to the target S7-1200 or S7-1500 firmware and a TIA Portal project at the matching STEP 7 version. STEP 7 V14 or later is required for recursive SCL functions on the S7-1200/S7-1500.
  • The Siemens SCL programming and programming manual for the target CPU, plus the STEP 7 Professional V18 (or current) documentation set.
  • The source C file, the C header describing structs/unions, and a clear functional specification. Without a spec, the port will diverge silently from the intent.

IEC 61131-3 Foundation and Pascal Bridge

Both SCL and Pascal descend from Wirth's block-structured design, while C uses curly-brace expression statements. Because the keyword sets overlap heavily (BEGIN/END, IF/THEN/ELSE, CASE, FOR/WHILE/REPEAT, FUNCTION/FUNCTION_BLOCK), automated C → Pascal converters produce code that an engineer can hand-clean into SCL in a fraction of the time required for a direct C → SCL port. Several freely available C-to-Pascal translators exist; their output is a starting point, not finished SCL.

The IEC 61131-3 third edition (2013) — which Siemens implements in current SCL — adds features that did not exist in earlier SCL, in particular recursive POUs with a bounded nesting depth. This is the single most important SCL feature for C ports, because the most common C patterns (tree walks, parsers, divide-and-conquer algorithms) rely on recursion.

Data Type Mapping: C to SCL

The PLC uses 8-bit, 16-bit, 32-bit, and 64-bit (S7-1500) elementary types that map cleanly to C, with the SCL names shown in the table below. All types are signed unless prefixed with UNSIGNED (or the older WORD/DWORD/LWORD for bit-string interpretation).

C type SCL equivalent Size (bits) Notes
char, signed char SINT 8 Range -128..127
unsigned char USINT 8 0..255
short, int16_t INT 16 -32 768..32 767
unsigned short, uint16_t UINT 16 0..65 535
int, long, int32_t DINT 32 -2 147 483 648..2 147 483 647
unsigned int, uint32_t UDINT 32 0..4 294 967 295
long long, int64_t LINT (S7-1500) 64 S7-1200 with V4.4+ also supports LINT
unsigned long long ULINT (S7-1500) 64
float REAL 32 IEEE-754 single precision
double LREAL 64 IEEE-754 double precision; not identical to double on every x86 toolchain
bool / _Bool BOOL 1 0/1 only, no implicit integer
char[] string STRING[n] 8*(n+1) 2-byte header; not NUL-terminated in the C sense
struct STRUCTEND_STRUCT sum Field alignment is byte-packed, not C-natural
union AT overlay on a STRUCT max See "Unions and AT" below
enum INT constant block 16/32 No native ENUM in classic SCL; use named constants

String caveat: SCL STRING is a Pascal-style length-prefixed string, not NUL-terminated. Any C code that depends on strlen, strcpy, or implicit NUL termination must be rewritten using SCL string functions (LEFT, RIGHT, MID, CONCAT, FIND, DELETE, INSERT, REPLACE) and explicit length handling.

Control Flow Translation

C's keyword-based control flow maps almost line-for-line to SCL. The only real difference is that Pascal-family languages use semicolons as statement separators rather than as statement terminators, and block delimiters are BEGIN/END rather than {/}.

C construct SCL equivalent
if (cond) { ... } else { ... } IF cond THEN ... ELSE ... END_IF;
switch (x) { case 1: ...; break; } CASE x OF 1: ...; ELSE ... END_CASE; (no fall-through)
for (i=0; i FOR i := 0 TO n-1 DO ... END_FOR;
while (cond) { ... } WHILE cond DO ... END_WHILE;
do { ... } while (cond); REPEAT ... UNTIL cond END_REPEAT; (note inverted condition)
break; EXIT;
continue; CONTINUE;
goto label; GOTO label; with label: target
return value; RETURN value; (or RETURN; for void)

CASE in SCL has no implicit fall-through: every label must end with an explicit terminator. C code that depends on fall-through must be refactored.

Functions, Function Blocks, and the Missing Function Pointer

Procedures and functions translate directly:

  • FC (Function): stateless, returns one typed value, has no instance DB. Equivalent to a C function returning a value.
  • FB (Function Block): stateful, owns an Instance DB that retains static variables between calls. Equivalent to a C++ class with member data, or to a C function operating on a passed-in context struct.
  • Void C function → SCL FC that returns VOID and uses VAR_IN_OUT for parameters it must modify.

Three C features do not survive the port cleanly:

  1. Function pointers. There is no SCL equivalent. Replace with a CASE on an integer selector, or implement the strategy pattern through FB instantiation and an array of FB instances, each implementing a common interface (i.e., the same set of method FCs with the same input/output parameter list).
  2. Variadic functions (printf-style). Not supported. Use fixed-arity FCs or build a string with CONCAT and explicit type conversions.
  3. setjmp / longjmp. Not supported. Use a tagged-state machine in SCL instead of exception-style control flow.

Pointers, PEEK, POKE, and the ANY Pointer

SCL supports typed pointers declared with POINTER TO <type> and used through ^ dereference. The PLC pointer is not the C pointer: it can be assigned, passed to FBs, and dereferenced, but pointer arithmetic (p++, *(p+1)) is not allowed at the SCL language level.

Three workarounds cover almost every case:

  1. Index through an array instead of pointer arithmetic.
  2. Disassemble an ANY pointer into a base address and length, then step through with explicit offsets using PEEK (read) and POKE (write) over a BYTE view. PEEK and POKE are bit-oriented, not byte-oriented in the C sense; cast through AT to a BYTE array to get a byte stream.
  3. Variant / VARIANT in S7-1500 (STEP 7 V14+) for type-generic blocks analogous to void *.

Warning. Direct manipulation of absolute addresses (PEEK/POKE, PIW, PQW) bypasses PLC I/O consistency. Use it only when the C code accesses shared memory regions (DBs) and never for cyclic I/O update during the same OB1 cycle.

Unions and the AT Construct

SCL has no UNION keyword. Use the AT overlay to declare a second view of an existing variable at the same memory location:

VAR
    raw   : STRUCT
        b0 : BYTE;
        b1 : BYTE;
        b2 : BYTE;
        b3 : BYTE;
    END_STRUCT;
    asWord  AT raw : WORD;    // overlays the 4-byte struct as one 16-bit value
    asDWord AT raw : DWORD;   // overlays as one 32-bit value
END_VAR

Limitations: the overlaid view must be at an aligned offset for its declared type on S7-1500 (strict), and is more permissive on S7-1200. Read the CPU-specific SCL manual before assuming byte-packed overlay works the way it does in C.

Recursion in SCL — What Changed in STEP 7 V14

Classic SCL (pre-STEP 7 V14) forbade recursive POU calls, because PLCs traditionally disallow stack-heap interaction that could collide with deterministic cycle times. STEP 7 V14 relaxed this for the S7-1200 and S7-1500 families. A recursive FC or FB is allowed, but the call chain is bounded by the CPU's maximum nesting depth (a project property) and the recursion will fault with a nesting-stack overflow if exceeded.

Practical rules for porting recursive C:

  • Convert the C function to an SCL FC with the same signature.
  • Add a depth counter as an VAR_IN_OUT parameter; increment on entry, decrement on exit, and RETURN the base case at a fixed maximum.
  • Configure the CPU's nesting-depth setting in the device properties high enough to cover the realistic worst case of the input data set.
  • Verify with a stress-test that drives the recursion to its documented limit and confirms a clean RETURN rather than an SF LED.

Step-by-Step Conversion Procedure

  1. Catalogue the C source. List every function, struct, union, global variable, pointer, and recursion in the C file. Anything you miss here will silently break the port.
  2. Translate data types using the mapping table above. Convert enum to a CONST block in a global DB.
  3. Translate control flow keyword by keyword; replace switch/case with CASE and refactor any fall-through.
  4. Replace C functions with SCL FCs; replace functions with static state with FBs and an Instance DB. Refactor any function-pointer call site into a CASE dispatch or a strategy-block instance array.
  5. Replace pointer arithmetic with array indexing where possible. Use AT overlays for unions. Use PEEK/POKE through a BYTE view for any remaining absolute-address patterns.
  6. Replace C strings with STRING[n] and the SCL string built-ins; add explicit length handling at every boundary.
  7. Refactor printf/sprintf into CONCAT chains with explicit type conversions (SINT_TO_STRING, INT_TO_STRING, REAL_TO_STRING, etc.).
  8. Port recursion only if the target CPU runs STEP 7 V14 or later; set the nesting depth in device properties; add a depth-limit guard.
  9. Compile, download, and watch. Use the TIA Portal SCL editor's online watch tables to step through the program and compare against the C reference output for the same inputs.

Verification and Field-Proven Caveats

  • Bit-width trap: C int is 32-bit on almost every modern toolchain; SCL INT is 16-bit. Code that uses int for indices and assumes 32-bit range will overflow in SCL if you used the obvious INT mapping. Use DINT for any index or counter that can exceed 32 767.
  • Signed-vs-unsigned trap: Mixing UINT and INT in SCL expression evaluation can promote to a 32-bit signed result. Cast explicitly to avoid silent sign-extension bugs that are easy to miss because the PLC will compile and run.
  • Cycle-time blow-up: Loops that were cheap on a PC can dominate an S7-1500 OB1 cycle. Add an upper-bound watchdog to any loop translated from C, or move it to a time-driven OB (e.g., OB35 at 100 ms).
  • Floating-point determinism: LREAL results can differ from x86 double in the last bit. Do not assert exact equality; assert absolute or relative tolerance.
  • Watch table parity test: Build an HMI watch table or a test FB that drives both the C reference (offline) and the SCL block (online) with the same input vectors, and compare side-by-side for every test case in the original C unit tests.

Troubleshooting Matrix

Symptom Likely C-side cause SCL fix
SF LED + "Nesting depth exceeded" diagnostic buffer entry Recursive C function translated without depth limit Add a depth counter; raise CPU nesting depth property; both
Compile error: "Function pointer not allowed" C function pointer, callback, or qsort comparator Replace with CASE dispatch or strategy-FB array
Compile error: "UNION not supported" C union type Use AT overlay on a STRUCT of the same byte length
Wrong result, low byte correct high byte garbage C struct with implicit padding Pad explicitly in the SCL STRUCT; do not assume C-natural alignment
Compile error: variadic parameter not allowed printf, scanf, custom variadic Replace with fixed-arity FC or CONCAT with explicit conversions
String length off by 1 or 2 C strlen / NUL handling Use SCL string built-ins; remember the 2-byte S7 string header is not a NUL
Index out of range at runtime Pointer arithmetic mapped as p+n Replace with array indexing; do not pass addresses
Result flips sign on large values Unsigned/signed mix Cast to DINT / UDINT explicitly; do not rely on implicit promotion

FAQ

Is there an automated C-to-SCL converter from Siemens?

No. Siemens does not publish a C-to-SCL converter. The practical industry approach is C → Pascal via a third-party translator, then hand-clean the Pascal into SCL inside TIA Portal.

Does SCL support recursive function calls?

Yes, on S7-1200 and S7-1500 controllers running STEP 7 V14 or later. Recursion is bounded by the CPU's configured maximum nesting depth; a depth-limit guard parameter is strongly recommended to avoid a stack-overflow diagnostic-buffer entry.

Can SCL use function pointers like C?

No. SCL has no function-pointer type. Replace C function pointers with a CASE-on-selector dispatch, or instantiate one FB per strategy and store the instance reference in an array that is indexed at runtime.

How do I translate a C union into SCL?

Use the AT overlay. Declare a STRUCT with the same total byte length, then declare a second variable AT that STRUCT with the alternative type. SCL has no UNIONS keyword.

Why does my SCL result differ from the C reference for floating-point values?

LREAL is IEEE-754 double precision but rounding and instruction order differ from the x86 toolchain that produced the C reference. Do not compare with =; compare with an absolute or relative tolerance band suited to the algorithm.

Back to blog