← All Posts

IEC 61131-3 Structured Text: A Practical Guide for Modern PLC Programming

Ladder logic was invented for electricians in 1969. It maps relay circuits to a screen. For simple start/stop logic, it's fine. For anything with math, loops, state machines, or data processing, it becomes a mess of tangled rungs that nobody — including the person who wrote it — can understand six months later.

Structured Text (ST) is the IEC 61131-3 language that looks and works like a modern programming language. If you've written Python, JavaScript, or C, ST will feel familiar. If you're new to PLC programming but comfortable with code, start with ST — not ladder logic. This guide covers everything you need to write your first ST program, with real examples you can run today using Voltrus PLC on macOS.

What Is IEC 61131-3 Structured Text?

IEC 61131-3 is the international standard for PLC programming languages. It defines five languages. Structured Text is the high-level, text-based one. Think of it as Pascal or a simplified C designed specifically for industrial control:

  • Text-based — you type code, not draw rungs. Version control works. Copy-paste works. Search-and-replace works.
  • Block structured — code is organized into PROGRAMs, FUNCTION_BLOCKs, and FUNCTIONs. Each is a self-contained unit with local variables and interfaces.
  • Cyclic execution — unlike Python scripts that run top-to-bottom once, ST programs run in a continuous scan loop: read inputs → execute logic → write outputs → repeat. This is fundamental to how PLCs work.
  • Strongly typed — variables have explicit types (BOOL, INT, REAL, etc.). Type mismatches are caught at compile time, not when the motor spins the wrong direction.

The other IEC 61131-3 languages — Ladder Diagram (LD), Function Block Diagram (FBD), Sequential Function Chart (SFC), Instruction List (IL) — each have their place. But ST is the one that scales to complex programs and the one that software-native engineers reach for first.

ST Syntax in 10 Minutes

Variables and Types

Every variable must be declared before use. Declarations go in a VAR...END_VAR block at the top of a PROGRAM or FUNCTION_BLOCK:

PROGRAM MotorControl
VAR
    start_button : BOOL;          (* true/false *)
    stop_button  : BOOL;
    motor_run    : BOOL;
    speed_sp     : REAL := 0.0;   (* floating point, initialized *)
    counter      : INT := 0;      (* signed 16-bit integer *)
    temperature  : REAL;
    alarm_active : BOOL := FALSE;
END_VAR

Common data types:

  • BOOL — true/false
  • INT, DINT, SINT — signed integers (16-bit, 32-bit, 8-bit)
  • UINT, UDINT — unsigned integers
  • REAL, LREAL — floating point (32-bit, 64-bit)
  • TIME — time duration (e.g., T#500ms, T#2s, T#1m30s)
  • STRING — text strings
  • ARRAY [0..9] OF INT — fixed-size arrays

Conditional Logic: IF / ELSIF / ELSE

IF temperature > 80.0 THEN
    cooling_fan := TRUE;
    alarm_active := TRUE;
ELSIF temperature > 60.0 THEN
    cooling_fan := TRUE;
    alarm_active := FALSE;
ELSE
    cooling_fan := FALSE;
    alarm_active := FALSE;
END_IF

Nested IFs work as you'd expect. Parentheses around conditions are optional — ST uses AND, OR, NOT, and XOR instead of &&, ||, !.

Multi-Way Branch: CASE

CASE pump_state OF
    0:  (* idle *)
        pump_run := FALSE;
        valve_open := FALSE;
    1:  (* starting *)
        valve_open := TRUE;
    2:  (* running *)
        pump_run := TRUE;
    3:  (* stopping *)
        pump_run := FALSE;
ELSE
    pump_state := 0;  (* fallback *)
END_CASE

Loops: FOR, WHILE, REPEAT

(* FOR loop — known iteration count *)
FOR i := 0 TO 9 DO
    readings[i] := 0;
END_FOR

(* WHILE loop — condition at start *)
WHILE count < max_count DO
    count := count + 1;
END_WHILE

(* REPEAT loop — condition at end, always runs at least once *)
REPEAT
    result := result * 2;
    steps := steps - 1;
UNTIL steps = 0
END_REPEAT
Watch out: Avoid infinite loops in ST. Remember your program runs in a scan cycle. A WHILE loop that doesn't terminate blocks the scan, and the PLC's watchdog timer will fault. Keep loops bounded.

Timers: The Heartbeat of PLC Programs

Timers are the single most-used function blocks in PLC programming. Every motor start has a delay. Every alarm has a debounce. Every sequence has a timeout. IEC 61131-3 defines three standard timer types:

TON — Timer On-Delay

Turns output ON after a delay when input goes TRUE. The most common timer — used for startup delays, sequential operations, and debouncing.

(* Turn on an output 2 seconds after the input goes true *)
my_timer(IN := start_signal, PT := T#2s);
IF my_timer.Q THEN
    output := TRUE;
END_IF

If start_signal goes FALSE before 2 seconds, the timer resets. ET (elapsed time) shows current progress. Q is TRUE when elapsed time ≥ preset time.

TOF — Timer Off-Delay

Turns output OFF after a delay when input goes FALSE. Used for cooling fans that run after the motor stops, or lights that stay on after the door closes.

(* Keep fan running 30 seconds after motor stops *)
my_tof(IN := motor_running, PT := T#30s);
fan := my_tof.Q;

TP — Timer Pulse

Generates a pulse of fixed duration when triggered. Used for one-shot operations — open a valve for exactly 5 seconds, regardless of how long the trigger signal stays high.

(* Open valve for exactly 5 seconds on trigger *)
my_tp(IN := trigger, PT := T#5s);
valve_open := my_tp.Q;

Counters: CTU, CTD, CTUD

Counters track events — parts produced, cycles completed, errors detected. IEC 61131-3 defines three types:

(* Count up — increment on rising edge of CU *)
part_counter(CTU, CU := sensor_on, PV := 1000);
IF part_counter.Q THEN
    batch_full := TRUE;  (* Q is TRUE when CV >= PV *)
END_IF

(* CTD — count down from preset *)
(* CTUD — combined up/down counter *)

Edge Detection: R_TRIG and F_TRIG

PLCs scan continuously. Without edge detection, a button press that's TRUE for 10 scans would trigger your logic 10 times. Edge detectors fire only on the transition:

(* R_TRIG — detect rising edge (FALSE → TRUE) *)
button_edge(R_TRIG, CLK := start_button);
IF button_edge.Q THEN
    cycle_count := cycle_count + 1;  (* increments exactly once per press *)
END_IF

(* F_TRIG — detect falling edge (TRUE → FALSE) *)
stop_edge(F_TRIG, CLK := stop_button);
IF stop_edge.Q THEN
    motor_run := FALSE;
END_IF

Real-World Example: Motor Start/Stop with Overload Protection

Here's a complete motor control program in Structured Text. It handles start/stop buttons, a motor contactor output, an overload input, an auto-reset delay, and a trip counter:

PROGRAM MotorStarter
VAR
    (* I/O *)
    start_btn    : BOOL;   (* normally-open pushbutton *)
    stop_btn     : BOOL;   (* normally-closed pushbutton *)
    overload     : BOOL;   (* thermal overload relay *)
    contactor    : BOOL;   (* motor contactor output *)

    (* Internal *)
    motor_run    : BOOL;
    reset_timer  : TON;
    start_edge   : R_TRIG;
    stop_edge    : R_TRIG;
    trip_count   : INT := 0;
END_VAR

(* Edge detection *)
start_edge(CLK := start_btn);
stop_edge(CLK := NOT stop_btn);  (* NC contact — TRUE when pressed *)

(* Start logic — latch on, unlatch on stop or overload *)
IF start_edge.Q AND NOT overload THEN
    motor_run := TRUE;
END_IF;

IF stop_edge.Q OR overload THEN
    motor_run := FALSE;
END_IF;

(* Auto-reset after 5 second cooldown if no overload *)
reset_timer(IN := NOT motor_run AND NOT overload, PT := T#5s);
IF reset_timer.Q THEN
    (* ready to restart *)
END_IF;

(* Count overload trips *)
IF overload THEN
    trip_count := trip_count + 1;
END_IF;

(* Output *)
contactor := motor_run;
END_PROGRAM

This program runs every scan (typically every 10-100ms). Each scan: check inputs → run logic → update outputs. The edge detectors ensure the start button increments the trip counter once per press, not once per scan.

ST vs Ladder Logic: When to Use Each

This is the wrong question. The right question is: what's the best language for this specific task? Modern PLCs support multiple languages in the same project. Use ST for the complex logic, ladder for the simple I/O mapping, and SFC for the sequence orchestration.

But if you're starting fresh and you have any programming experience: start with ST. It's more expressive, more maintainable, and version-control friendly. You can always add ladder diagrams later for the specific rungs where they add clarity.

Try Structured Text on Your Mac

Voltrus PLC is a native macOS PLC IDE with full IEC 61131-3 Structured Text support. Write ST in a modern CodeMirror 6 editor with syntax highlighting, compile it with the built-in compiler, and run it on the softPLC runtime — all on your Mac. No Windows, no hardware required.

The editor supports autocomplete for keywords, types, and standard functions. The diagnostics panel shows compile errors with line numbers. The watch table lets you monitor variables in real-time as the scan runs. And the Modbus TCP driver lets your ST program talk to real devices.

Frequently Asked Questions

What is IEC 61131-3 Structured Text?

Structured Text (ST) is one of the five programming languages defined by IEC 61131-3, the international standard for PLC programming. It's a high-level, text-based language with syntax similar to Pascal. It supports conditional logic (IF/THEN/ELSE, CASE), loops (FOR, WHILE, REPEAT), arithmetic, function calls, and function blocks. ST is the preferred language for complex control logic, math, and data processing in PLCs.

What are the five IEC 61131-3 languages?

The five IEC 61131-3 languages are: Structured Text (ST) — high-level text language; Ladder Diagram (LD) — relay-style graphical; Function Block Diagram (FBD) — node-based data flow; Sequential Function Chart (SFC) — state machine diagrams; Instruction List (IL) — deprecated low-level assembly-like language.

Is Structured Text better than ladder logic?

ST is better for complex logic, math, loops, and data processing. Ladder logic is better for simple boolean sequences and maintenance electricians tracing rungs. Most modern PLCs support both, and the best approach is mixing languages within a project — ST for the complex calculations, ladder for the simple I/O rungs.

Can I learn Structured Text without a PLC?

Yes. Voltrus PLC includes a built-in softPLC runtime that runs on your Mac. You can write, compile, and run ST programs entirely in software — no PLC hardware required. The watch table lets you monitor variables, and when you're ready, the Modbus TCP driver connects to real devices.

Write Structured Text on Your Mac

Voltrus PLC is a native macOS IEC 61131-3 Structured Text editor with compiler, softPLC runtime, and Modbus I/O. Learn ST, test programs, and connect to real devices — all from your Mac. v1.0 available now — $29.99 one-time.

Download for Mac

Further Reading