PLC Programming SAPLC ProgrammingSOUTH AFRICA

learn · South Africa

Structured text (ST) — PLC programming for engineers

A practical structured text tutorial. IF / CASE / FOR / WHILE syntax, IEC 61131-3 data types, ST vs ladder, and worked examples in a free PLC simulator.

Structured text is the third major IEC 61131-3 language, and the one most South African PLC programmers postpone learning for two years longer than they should. It is text-based — no rungs, no boxes, no signal lines — just statements, expressions, and control-flow keywords that read like a slightly-strict dialect of Pascal. The syntax is small enough to learn in an afternoon. The payoff is large. Logic that takes thirty ladder rungs or fifteen FBD blocks often compresses into eight lines of ST that read top-to-bottom the way the engineer's notebook describes the process. This tutorial walks the syntax, the data type rules, the vendor flavours (Siemens calls it SCL, Rockwell calls it ST, CODESYS sticks closest to the IEC standard), three worked examples — a sequencer, an analog scaling formula, a recipe lookup — and the scan-time trap that catches every newcomer who writes their first FOR loop.

One opinion stated outright. Most SA programmers ignore ST for two years and then realise they could have written 30 ladder rungs as 8 lines of CASE. The reluctance is usually not technical — it is cultural. Maintenance teams read ladder; the next person to open the program reads ladder; the customer asked for ladder. Those constraints are real, but they apply per-program and per-rung, not per-language. Writing the analog scaling and the recipe handler in ST while keeping the motor-control rungs in ladder is not a betrayal of the maintenance team. It is using the right tool for each piece of logic, the same way a panel builder uses different terminal types for different signal classes.

Try the simulator →

What ST is

Structured text is a textual PLC programming language standardised in IEC 61131-3 alongside ladder, function block diagram, instruction list, and sequential function chart. The syntax is Pascal-flavoured — semicolon statement terminators, named control-flow blocks closed with END_IF / END_CASE / END_FOR, strict typing, explicit assignment with :=. The semantics are identical to the graphical languages. An ST program runs on the same scan cycle, reads the same input image, drives the same output image, and uses the same data types as a ladder program on the same processor.

The mental model. Where ladder asks "which contacts are closed and which coils should energise this scan," ST asks "what statements should execute this scan, in what order, with what conditions." A scan still happens at a fixed period (or as fast as the program can run on a continuous task). The program-organisation unit (POU) that holds the ST code is called a Function, a Function Block, or a Program depending on whether it has persistent state and how it is instanced — the same POU types ladder and FBD use. Inside the POU, you write statements instead of drawing rungs.

The reference standard is IEC 61131-3, available from the IEC at iec.ch/standards/iec-61131-3. Section 3 of part 3 is the ST reference; section 2 is the common element types — data types, variable declarations, literals — that apply to every IEC language. The standard defines a portable subset; every vendor extends it. The portable subset is what this tutorial covers.

When ST beats ladder and FBD

Five categories where ST is the right choice.

Math. Any non-trivial arithmetic — scaling formulas, mass-balance closures, polynomial corrections, statistical calculations — reads cleaner as a few lines of ST than as a stack of MOV / MUL / DIV ladder instructions or a graph of arithmetic FBD blocks. One line of ST is one line on the engineer's notepad. The translation is direct.

String handling. Concatenation, substring extraction, parsing — anything involving STRING data types — is awkward in ladder and verbose in FBD. ST has string-manipulation functions and concatenation operators that match the way text is described in code anywhere else.

Looping. A FOR loop iterating over a fifty-element array is one statement in ST. The same logic in ladder is fifty rungs (no), or one rung with a JSR loop and a manual index increment (painful), or a CODESYS-extended FBD pattern that is essentially ST in a box. Just write it as ST.

Complex conditional logic. Nested IF / ELSIF chains, multi-way CASE statements on a step variable, decision tables — when the logic is fundamentally "if this then this else if that then that" with five or six branches, ST reads top-to-bottom the way the engineer thought about it. The same logic in ladder is a tangle of mutually-exclusive rungs that share permissive contacts and break when somebody adds a new branch in the middle.

Recipe management. Reading a row out of a recipe array, pushing parameters out to twenty downstream tags, validating ranges, logging the change to a fault buffer — recipe handling is array-indexed loop logic with conditional branches. ST is the only IEC language that does this well. Ladder cannot do array indexing without ugly indirect-addressing tricks. FBD can do it with a structured-array block, but the result is uglier than the ST equivalent.

The fifth case worth mentioning is encapsulation. A piece of logic written as an ST function block, with a typed interface, instanced multiple times — the same encapsulation pattern FBD supports — works the same way in ST. Define once, instance many times, each instance with its own state.

Syntax basics

The minimal syntax every ST programmer needs in their first week.

(* Assignment uses := not = *)
Counter := Counter + 1;

(* Comparison uses = (not ==), and <> (not !=) *)
IF Pressure >= Setpoint THEN
    ValveOpen := FALSE;
ELSIF Pressure < (Setpoint - Hysteresis) THEN
    ValveOpen := TRUE;
ELSE
    (* hold last state *)
    ;
END_IF;

(* CASE on a step variable *)
CASE Step OF
    0:  Pump := FALSE; Valve := FALSE;
    1:  Pump := TRUE;  Valve := FALSE;
    2:  Pump := TRUE;  Valve := TRUE;
    ELSE
        Step := 0;  (* fault recovery *)
END_CASE;

(* FOR loop with explicit step *)
FOR i := 0 TO 49 BY 1 DO
    Total := Total + Sample[i];
END_FOR;

(* WHILE loop — guard the condition *)
WHILE Buffer[Index] <> 0 AND Index < 100 DO
    Index := Index + 1;
END_WHILE;

(* REPEAT loop — runs at least once *)
REPEAT
    Index := Index + 1;
UNTIL Buffer[Index] = 0 OR Index >= 100
END_REPEAT;

A handful of details that catch first-time ST writers. Every statement ends with a semicolon — including the last one before an END_IF or END_CASE. Assignment is :=, not =. Equality comparison is =, not ==. Inequality is <>, not !=. Comments are (* like this *) in the IEC standard, though most vendors also accept // line comments. Block keywords (IF, CASE, FOR, WHILE, REPEAT) all have explicit closing keywords (END_IF, END_CASE, END_FOR, END_WHILE, END_REPEAT) — no Python-style indentation, no C-style braces. The compiler will tell you exactly where the missing terminator is.

Data types

ST is more strict about types than ladder. The IEC primitive types every program uses.

BOOL — single bit, TRUE or FALSE. BYTE — 8 bits, treated as a bit string (no sign). WORD — 16 bits, bit string. DWORD — 32 bits, bit string. LWORD — 64 bits, bit string (not all platforms support).

SINT — signed 8-bit integer, −128 to 127. INT — signed 16-bit, −32768 to 32767. DINT — signed 32-bit, ±2.1 billion. LINT — signed 64-bit. The unsigned variants are USINT, UINT, UDINT, ULINT.

REAL — 32-bit floating point, single precision, about seven significant digits. LREAL — 64-bit, double precision, about fifteen significant digits.

TIME — duration, written as T#1h30m15s500ms or T#5s. DATE, TIME_OF_DAY, DATE_AND_TIME — wall-clock types. STRING — variable-length character array, usually capped at 80 or 254 characters by the platform.

The strictness rule. You cannot mix INT and DINT in an arithmetic expression without an explicit conversion. The operation MyInt + MyDint is a compile error. The fix is INT_TO_DINT(MyInt) + MyDint — the conversion is explicit, the programmer chose the destination type, the compiler does what was asked. This feels pedantic the first time it bites you. It saves you from silent overflow the tenth time, when the INT value wraps past 32767 and you cannot work out why the integrator is producing negative numbers. Take the strictness as a feature, not a nuisance.

A subtle point on REAL. Comparing two REALs with = is almost never what you want. Floating-point arithmetic accumulates rounding error; 0.1 + 0.2 = 0.3 evaluates to FALSE on every IEEE-754 implementation, ST included. Compare with a tolerance — ABS(A - B) < 0.001 — or compare in fixed-point integers if the precision matters.

Vendor flavours — SCL, ST, Rockwell ST

Three platform names for the same language.

Siemens TIA Portal calls structured text SCL (Structured Control Language). It is the same IEC 61131-3 ST with Siemens-specific extensions for accessing S7 data blocks, calling Siemens system function blocks, and integrating with the Siemens-flavoured POU types (FB, FC, OB, DB). The SCL programming reference is published by Siemens at support.industry.siemens.com — the S7-1500 SCL manual is the canonical source if you are programming a 1500. The S7-1200 SCL subset is similar with a smaller standard library.

Rockwell Studio 5000 calls it ST (Structured Text). Available on ControlLogix and CompactLogix processors. The Rockwell flavour adds Rockwell-specific bit-level addressing (Tag.0, Tag.1 to access individual bits of a DINT), and integrates with Add-On Instructions and the Studio 5000 tag database. The Rockwell ST manual is publication 1756-PM007.

CODESYS is the closest to vanilla IEC. CODESYS underpins many third-party platforms — Schneider Modicon M-series, Wago, Bosch Rexroth, Eaton XC, ifm, dozens of others — so learning CODESYS-flavour ST gives you portability across a long list of physical PLCs. The CODESYS Online Help is the reference; their ST documentation matches IEC 61131-3 with minimal extension.

Beckhoff TwinCAT uses CODESYS-derived ST with TwinCAT-specific extensions for motion control, EtherCAT, and the TwinCAT 3 object-oriented features (METHODS, PROPERTIES, INTERFACES, EXTENDS). TwinCAT 3 is the one platform that pushed ST genuinely into object-oriented territory — class hierarchies, virtual methods, abstract types — which the IEC standard hints at but most platforms do not implement.

The portable subset. If you stick to IF / CASE / FOR / WHILE / REPEAT, the IEC primitive types, and the standard math and string functions, your ST will move between platforms with minor adjustments. The moment you reach for vendor-specific extensions — SCL's AT %M0.0 direct-address syntax, Rockwell's bit-tag addressing, TwinCAT's interfaces — the code becomes platform-locked.

Worked example 1 — a sequencer in ST

A five-step batch sequence. Fill the tank, heat to setpoint, hold for thirty minutes, drain, idle. The ladder version is roughly thirty rungs once you include the transition logic, the output drives, and the timer interlocks. The ST version is a CASE statement.

CASE Step OF
    0:  (* idle *)
        FillValve := FALSE; Heater := FALSE; DrainValve := FALSE;
        IF StartButton THEN Step := 1; END_IF;

    1:  (* fill *)
        FillValve := TRUE; Heater := FALSE; DrainValve := FALSE;
        IF Level >= FillSetpoint THEN
            FillValve := FALSE;
            Step := 2;
        END_IF;

    2:  (* heat *)
        Heater := (Temperature < TempSetpoint);
        IF Temperature >= TempSetpoint THEN
            HoldTimer(IN := TRUE, PT := T#30m);
            Step := 3;
        END_IF;

    3:  (* hold *)
        Heater := (Temperature < TempSetpoint);
        HoldTimer(IN := TRUE, PT := T#30m);
        IF HoldTimer.Q THEN
            HoldTimer(IN := FALSE, PT := T#30m);
            Heater := FALSE;
            Step := 4;
        END_IF;

    4:  (* drain *)
        DrainValve := TRUE;
        IF Level <= 0.5 THEN
            DrainValve := FALSE;
            Step := 0;
        END_IF;

    ELSE
        Step := 0;  (* fault recovery — bad step value *)
END_CASE;

Twenty-five lines of ST replace thirty rungs of ladder, and the program reads top-to-bottom the way the batch sheet describes the process. Adding a sixth step (a clean-in-place rinse, say) is adding a new branch to the CASE — five lines of code in one place. Adding the same step in ladder is finding the right place in the rung sequence, inserting new rungs, fixing the cross-references in two or three other rungs that referenced the old next-step transition. The maintenance burden of the ST version is dramatically lower once the sequence has more than a handful of steps.

Worked example 2 — analog scaling

The most common analog scaling formula is the linear two-point map — given a raw value between InMin and InMax, produce a scaled value between OutMin and OutMax. In ST, one line.

Scaled := (Raw - InMin) * (OutMax - OutMin) / (InMax - InMin) + OutMin;

That single statement is a four-block FBD diagram (subtract, subtract, multiply, divide-and-add) or a six-rung ladder routine using MOV, SUB, MUL, DIV instructions. The ST version is the formula as written on a control engineer's notepad. The translation overhead is zero. The maintainability is high — somebody looking at the program three years from now sees the formula and recognises it.

A defensive version that handles the edge case where InMax equals InMin (which would otherwise be a divide-by-zero):

IF (InMax - InMin) <> 0.0 THEN
    Scaled := (Raw - InMin) * (OutMax - OutMin) / (InMax - InMin) + OutMin;
ELSE
    Scaled := OutMin;
    ScalingFault := TRUE;
END_IF;

Six lines, including the fault flag. Every analog input on the program goes through this pattern. Wrap it as a function or a function block and call it once per analog tag. This is the kind of repetitive math-heavy logic that makes ST shine.

Worked example 3 — recipe lookup

A recipe is an array of records, each holding the parameters for one product. Selecting recipe number 7 means reading row 7 of the array and pushing the values to the live setpoints. In ladder this requires indirect addressing, MOV instructions per parameter, and a separate rung per row of the table. In ST it is a FOR loop.

TYPE RecipeRow : STRUCT
    TempSetpoint : REAL;
    HoldMinutes  : INT;
    FillLitres   : REAL;
    Pressure     : REAL;
END_STRUCT;
END_TYPE

VAR_GLOBAL
    Recipes  : ARRAY[0..49] OF RecipeRow;
    Selected : INT := 0;
    Active   : RecipeRow;
END_VAR

(* program *)
IF Selected >= 0 AND Selected <= 49 THEN
    Active := Recipes[Selected];
ELSE
    RecipeFault := TRUE;
END_IF;

Twelve lines. The Active record now holds the current recipe parameters, and the rest of the program reads Active.TempSetpoint and Active.HoldMinutes directly. Changing a recipe is updating one row of the Recipes array — done from the HMI, the engineering station, or a recipe-import routine that reads a CSV file. Fifty recipes, four parameters each, one ST program. The ladder equivalent is two hundred rungs.

A FOR-loop variant — copying every recipe parameter into the live tags one by one — is useful when the live tags are referenced individually elsewhere in the program rather than as a struct.

FOR i := 0 TO 49 DO
    IF Recipes[i].Pressure > MaxAllowedPressure THEN
        RecipeValidationFault[i] := TRUE;
    ELSE
        RecipeValidationFault[i] := FALSE;
    END_IF;
END_FOR;

A range-check across all fifty recipes, one scan. The same logic in ladder is fifty rungs.

The scan-time trap

ST is fast. A FOR loop iterating over a hundred elements compiles to a tight machine-code loop on a modern PLC processor, and runs in tens of microseconds. That is fast — but it is not free. A continuous task on an S7-1500 might have a target scan time of 10 ms; a continuous task on a CompactLogix might be 5 ms. Burning a millisecond on a single FOR loop is ten percent of your scan-time budget, and ten such loops will overrun the budget and trigger a watchdog fault.

The pattern that catches newcomers. Sample code says "iterate over the array, compute the average, write the result." The newcomer writes a 1000-iteration FOR loop on a continuous task. The PLC runs fine in the office on the simulator. On the line, with a real I/O scan and three hundred other rungs running, the watchdog trips and the controller faults to STOP. The fix is one of three things — move the loop to a periodic task with a lower frequency (run the average once a second instead of every scan), iterate a slice of the array per scan (process ten elements per scan, finish the array over a hundred scans), or move the heavy math to a different processor (on Siemens S7-1500, consider offloading to the OPC UA server's compute capability or to an edge module).

The general rule. Profile before assuming. Most platforms expose a scan-time tag — Task.LastScanTime, OB1.PrevCycle, or similar — that the program can read at runtime. Watch the scan time as you add ST loops. If it climbs over half your budget on a benign day, the fault-day spike will push it over the edge. Refactor before that happens.

A second trap. A WHILE loop with a condition that does not become false runs forever in a single scan, and the watchdog kills the program. Always include a counter-based safety bound — WHILE Condition AND Iterations < 1000 DO ... END_WHILE — so that a faulty condition cannot lock up the scan. The same applies to REPEAT-UNTIL. Bounded loops are safe; unbounded loops are how PLC programs fault to STOP at three in the morning.

Common newcomer mistakes

Five mistakes that catch first-month ST programmers, and the fixes.

Forgetting the semicolon. Every statement ends with ;. Even the last one before END_IF. The compiler error message is usually clear ("expected ';' before END_IF") but the error confuses newcomers who are used to ladder where there is no statement terminator. Get into the habit early.

Mixing types. As above — MyInt + MyDint is a compile error. The fix is INT_TO_DINT(MyInt) + MyDint. Read the conversion functions in the IEC standard library; learn the half-dozen you use weekly (INT_TO_DINT, DINT_TO_INT, INT_TO_REAL, REAL_TO_INT, BOOL_TO_INT and their reverses).

Comparing REALs with =. Floating-point equality is almost never what you want. Use ABS(A - B) < tolerance instead. The bug shows up as a setpoint comparison that intermittently fails for no apparent reason; the cause is rounding noise on the last bit of the REAL.

Infinite WHILE loops. A WHILE whose condition does not become false hangs the scan and faults the controller. Always bound the loop with a counter as well as the original condition. The watchdog timeout on a typical PLC is 100 ms — easy to overrun with a tight loop.

Writing ladder logic in ST syntax. A common newcomer pattern is to write the ladder version of the logic and then mechanically translate it into ST — a stack of IF / END_IF blocks that are each a single contact-and-coil rung. The result is uglier than the ladder original. ST shines when you let it be ST — use CASE for sequencers, FOR for arrays, REAL math for analog. If your ST is just transliterated ladder, leave it as ladder.

The remedy for all five is the same — practice in the simulator. Open the ST editor, write a ten-line program, watch it run, fix the type mismatch the compiler complains about, fix the missing semicolon, watch the scan time climb when you add a loop. The reflex layer for ST takes a weekend to build. After that you reach for it instinctively when ladder would take thirty rungs and FBD would take fifteen blocks.

The full IEC 61131-3 standard text covering structured text and the common element types is published by the IEC at iec.ch/standards/iec-61131-3. Section 3 of part 3 is the ST reference. The Siemens SCL programming reference, covering the platform-specific extensions on S7-1200 and S7-1500, is at support.industry.siemens.com. Read both. The IEC document is the portable language; the Siemens document is what runs on the controllers most SA panel shops install.

The next tutorial covers sequential function chart (SFC) — the fourth IEC language, designed specifically for state machines and batch sequences. SFC and ST work well together; the SFC graph holds the high-level state, and each step's actions are written as ST snippets. The combination is what runs the supervisory layer on most modern process control programs. Build the ST reflex first, then the SFC layer drops in cleanly on top.

Try the simulator →

By PLC Programming SA · Last updated 2026-05-05