Ladder logic gets all the attention in PLC training courses, but anyone who has scaled a project past a few hundred rungs knows why Structured Text (ST) exists.
ST is the IEC 61131-3 high-level language that lets you write PLC logic the way you’d write Pascal or C, with IF/THEN, FOR loops, CASE statements, and functions, instead of drawing contacts and coils.
This article walks through 20 Structured Text examples that come up constantly in real automation work: math and comparisons, timers, counters, state machines, alarm handling, PID control, and array processing.
Each example includes the code, a plain-language explanation of what it does, and notes on where it tends to trip people up.
What Is Structured Text in PLC Programming?
Structured Text is one of the five IEC 61131-3 programming languages (alongside Ladder Diagram, Function Block Diagram, Instruction List, and Sequential Function Chart).
It’s a text-based language that resembles Pascal, and it’s the language of choice whenever logic involves the following.
- Complex math or algorithmic calculations
- Nested conditional logic (many
IF/ELSIFbranches) - Loops over arrays or data tables
- String manipulation
- Recipe management or data-driven logic
Most modern PLC platforms, including Siemens TIA Portal (SCL, Siemens’s ST dialect), Rockwell Studio 5000, CODESYS-based controllers (Beckhoff, WAGO, and B&R), and Schneider EcoStruxure, support ST natively, and you can typically mix ST with ladder logic in the same project, calling ST function blocks from ladder rungs.
Basic Syntax Examples
Variable Declaration and Assignment
VAR
Motor1_Speed : REAL := 0.0;
Motor1_Running : BOOL := FALSE;
Cycle_Count : INT := 0;
Tank_Name : STRING(20) := 'Tank_A';
END_VAR
Motor1_Speed := 1750.0;
Motor1_Running := TRUE;
Cycle_Count := Cycle_Count + 1;
Every ST program starts with a VAR block declaring the data types you’ll use. Assignment uses :=, not = a common source of syntax errors for programmers coming from other languages.
IF/THEN/ELSE Conditional Logic
IF Tank_Level >= 90.0 THEN
High_Level_Alarm := TRUE;
Fill_Valve := FALSE;
ELSIF Tank_Level <= 10.0 THEN
Low_Level_Alarm := TRUE;
Fill_Valve := TRUE;
ELSE
High_Level_Alarm := FALSE;
Low_Level_Alarm := FALSE;
END_IF;
This is the ST equivalent of parallel ladder rungs with multiple comparison instructions. Once you have more than two or three conditions, ST reads far more cleanly than the equivalent ladder logic.
CASE Statement for Multi-Way Branching
CASE Machine_State OF
0: Machine_Status := 'Idle';
1: Machine_Status := 'Starting';
2: Machine_Status := 'Running';
3: Machine_Status := 'Stopping';
4: Machine_Status := 'Fault';
ELSE
Machine_Status := 'Unknown';
END_CASE;
CASE statements are the backbone of state machine programming in ST and read far more clearly than a chain of IF/ELSIF blocks when you have more than four or five discrete states.
Timer and Counter Examples
On-Delay Timer (TON)
TON_Delay(IN := Start_Button, PT := T#5s);
Conveyor_Motor := TON_Delay.Q;
Function blocks like TON are called the same way in ST as they’re wired in ladder, IN starts the timer, PT sets the preset, and .Q goes true when the elapsed time reaches the preset.
Off-Delay Timer (TOF) for Fan Overrun
TOF_FanDelay(IN := Oven_Heater_On, PT := T#120s);
Exhaust_Fan := TOF_FanDelay.Q;
A common pattern in oven and dryer controls: the exhaust fan must run for 2 minutes after the heater shuts off to clear residual fumes.
Up/Down Counter for Parts Tracking
CTUD_Parts(CU := Part_Sensor, CD := Reject_Sensor, RESET := Shift_Reset, PV := 500);
Parts_Good := CTUD_Parts.CV;
Batch_Complete := CTUD_Parts.QU;
CTUD counts up on good parts and down on rejects, giving you a running net-good-parts total that resets at shift change.
Debounce Timer for Noisy Sensors
IF Raw_Sensor_Input THEN
Debounce_Timer(IN := TRUE, PT := T#50ms);
IF Debounce_Timer.Q THEN
Clean_Sensor_Signal := TRUE;
END_IF;
ELSE
Debounce_Timer(IN := FALSE, PT := T#50ms);
Clean_Sensor_Signal := FALSE;
END_IF;
A short delay filters out contact chatter or electrical noise before the signal is used anywhere else in the program.
Math and Scaling Examples
Analog Input Scaling (4-20mA to Engineering Units)
FUNCTION_BLOCK FB_ScaleAnalog
VAR_INPUT
RawValue : INT; // 0-32767 raw ADC counts
EU_Min : REAL; // Engineering units at 4mA
EU_Max : REAL; // Engineering units at 20mA
END_VAR
VAR_OUTPUT
ScaledValue : REAL;
END_VAR
ScaledValue := EU_Min + (INT_TO_REAL(RawValue) / 32767.0) * (EU_Max - EU_Min);
END_FUNCTION_BLOCK
Wrapping the scaling math in a reusable function block means you write the conversion formula once and call it for every analog input on the system pressure transmitters, level sensors, and flow meters just by passing different EU_Min/EU_Max values.
Moving Average Filter
FOR i := 1 TO 9 DO
Sample_Array[i] := Sample_Array[i+1];
END_FOR
Sample_Array[10] := New_Reading;
Sum := 0.0;
FOR i := 1 TO 10 DO
Sum := Sum + Sample_Array[i];
END_FOR
Filtered_Value := Sum / 10.0;
A rolling 10-sample average smooths out a noisy analog signal without the lag of a heavier low-pass filter.
This is a textbook example of where ST’s FOR loops beat ladder logic outright. The equivalent in rungs would take ten times the space.
PID Control Loop Call
PID_TempControl(
ACTUAL := Actual_Temperature,
SETPOINT := Temp_Setpoint,
KP := 2.5,
TN := T#30s,
TV := T#5s,
MANUAL := Manual_Mode,
LIMITS_ACTIVE := TRUE,
ULIMIT := 100.0,
LLIMIT := 0.0
);
Heater_Output := PID_TempControl.OUT;
Most PLC platforms ship a built-in PID function block; ST is typically how you configure and call it, tuning KP (proportional gain), TN (integral time), and TV (derivative time).
State Machine Examples
Simple Two-State Motor Control
CASE Motor_State OF
0: // Stopped
Motor_Output := FALSE;
IF Start_Command THEN
Motor_State := 1;
END_IF;
1: // Running
Motor_Output := TRUE;
IF Stop_Command OR Motor_Overload THEN
Motor_State := 0;
END_IF;
END_CASE;
Batch Process Sequencer
CASE Batch_Step OF
0: // Idle - wait for start
IF Start_Batch THEN
Batch_Step := 10;
END_IF;
10: // Fill
Fill_Valve := TRUE;
IF Tank_Level >= Fill_Setpoint THEN
Fill_Valve := FALSE;
Batch_Step := 20;
END_IF;
20: // Heat
Heater_On := TRUE;
IF Temperature >= Temp_Setpoint THEN
Heater_On := FALSE;
Batch_Step := 30;
END_IF;
30: // Mix
Mixer_On := TRUE;
Mix_Timer(IN := TRUE, PT := T#10m);
IF Mix_Timer.Q THEN
Mixer_On := FALSE;
Mix_Timer(IN := FALSE, PT := T#10m);
Batch_Step := 40;
END_IF;
40: // Discharge
Discharge_Valve := TRUE;
IF Tank_Level <= 5.0 THEN
Discharge_Valve := FALSE;
Batch_Step := 0;
END_IF;
END_CASE;
Numbering steps in increments of 10 (0, 10, 20, 30…) is a common convention. It leaves room to insert new steps (5, 15, 25) later without renumbering everything downstream.
Fault State with Recovery
CASE System_State OF
0: // Normal
IF Fault_Detected THEN
System_State := 99;
Fault_Timestamp := TIME();
END_IF;
99: // Fault
All_Outputs_Off := TRUE;
Alarm_Active := TRUE;
IF Fault_Reset_Button AND NOT Fault_Detected THEN
System_State := 0;
Alarm_Active := FALSE;
All_Outputs_Off := FALSE;
END_IF;
END_CASE;
Array and Data Handling Examples
Iterating Over an Array of Sensors
FOR i := 1 TO 8 DO
IF Temperature_Array[i] > High_Temp_Limit[i] THEN
Zone_Alarm[i] := TRUE;
ELSE
Zone_Alarm[i] := FALSE;
END_IF;
END_FOR
Eight zones checked in five lines instead of eight duplicated ladder rungs. This is the productivity case for ST in a sentence.
Finding the Maximum Value in an Array
Max_Pressure := Pressure_Array[1];
FOR i := 2 TO 20 DO
IF Pressure_Array[i] > Max_Pressure THEN
Max_Pressure := Pressure_Array[i];
END_IF;
END_FOR
WHILE Loop for Recipe Download
i := 1;
WHILE (i <= Recipe_Length) AND NOT Download_Error DO
Output_Array[i] := Recipe_Array[i];
IF NOT Write_Successful THEN
Download_Error := TRUE;
END_IF;
i := i + 1;
END_WHILE
WHILE loops are less common than FOR loops in PLC code because they can run indefinitely if the exit condition never becomes true.
Always include a fault/timeout condition alongside the primary exit condition, as shown here.
Alarm and Interlock Examples
Alarm Class with Acknowledgment Logic
IF (Pressure > Pressure_High_Limit) AND NOT Alarm_Acked THEN
Alarm_Active := TRUE;
Alarm_Flashing := TRUE;
END_IF;
IF Ack_Button AND Alarm_Active THEN
Alarm_Acked := TRUE;
Alarm_Flashing := FALSE;
END_IF;
IF Pressure <= Pressure_High_Limit THEN
Alarm_Active := FALSE;
Alarm_Acked := FALSE;
END_IF;
Safety Interlock Chain
Permissive_OK := Guard_Closed AND E_Stop_Clear AND
Lube_Pressure_OK AND NOT Overload_Trip;
IF Permissive_OK AND Start_Command THEN
Run_Permit := TRUE;
ELSE
Run_Permit := FALSE;
END_IF;
Chaining every permissive into a single BOOL expression makes the logic auditable at a glance, and it’s exactly the kind of line-by-line readability that makes ST easier to troubleshoot at 2 a.m. than tracing five separate ladder rungs.
Function and Function Block Examples
Reusable Function for Unit Conversion
FUNCTION F_CtoF : REAL
VAR_INPUT
Celsius : REAL;
END_VAR
F_CtoF := (Celsius * 9.0 / 5.0) + 32.0;
END_FUNCTION
Temp_F := F_CtoF(Temp_C);
Functions like this get written once and called from anywhere in the project, a small habit that keeps large ST codebases maintainable.
Function Block with Internal State (Tank Level Tracker)
FUNCTION_BLOCK FB_TankTracker
VAR_INPUT
Fill_Rate : REAL;
Drain_Rate : REAL;
Enable : BOOL;
END_VAR
VAR_OUTPUT
Current_Level : REAL;
END_VAR
VAR
Net_Rate : REAL;
END_VAR
IF Enable THEN
Net_Rate := Fill_Rate - Drain_Rate;
Current_Level := Current_Level + (Net_Rate * 0.1); // 100ms scan assumption
Current_Level := LIMIT(0.0, Current_Level, 100.0);
END_IF;
END_FUNCTION_BLOCK
Unlike a FUNCTION, a FUNCTION_BLOCK retains its internal state (Current_Level) between scans. You instantiate it once per tank, and each instance keeps its own running value.
Structured Text vs. Ladder Logic: When to Use Which
| Task | Better in ST | Better in Ladder |
|---|---|---|
| Complex math/scaling | ✅ | |
| State machines / sequencing | ✅ | |
| Array or table processing | ✅ | |
| Simple start/stop motor control | ✅ | |
| Discrete I/O with few conditions | ✅ | |
| Logic reviewed by electricians on the floor | ✅ | |
| Recipe or data-driven logic | ✅ | |
| Alarm/interlock chains with many conditions | ✅ |
Most production programs end up mixing both. Ladder for the I/O-facing rungs a technician needs to troubleshoot with a multimeter in hand and ST for the math, sequencing, and data-handling code behind the scenes.
Common Structured Text Syntax Mistakes
- Using
=instead of:=for assignment (=is only for comparison inIFstatements) - Forgetting the semicolon at the end of a statement
- Missing
END_IF,END_CASE,END_FOR, orEND_WHILEclosing keywords - Mixing data types without conversion functions (e.g., assigning an
INTto aREALwithoutINT_TO_REAL) - Writing a
WHILEloop with no guaranteed exit condition, which can lock up the scan cycle on some platforms
FAQ
Is structured text harder to learn than ladder logic?
It has a steeper initial learning curve for technicians coming from electrical backgrounds, since it looks like general-purpose code rather than a circuit diagram.
Programmers with prior coding experience in C, Pascal, or Python usually pick it up faster than ladder logic.
Can I mix structured text and ladder logic in the same PLC project?
Yes. Nearly every modern platform (Siemens TIA Portal, Rockwell Studio 5000, CODESYS-based systems) lets you call ST function blocks from ladder rungs and vice versa within the same project.
Which PLC brands support Structured Text?
All IEC 61131-3-compliant platforms support it, including Siemens (as SCL), Rockwell/Allen-Bradley, Beckhoff TwinCAT, WAGO, B&R, Schneider Electric EcoStruxure, and most CODESYS-based controllers.
Is Structured Text the same as C or Pascal?
No, but the syntax is closely modeled on Pascal := for assignment, IF/THEN/ELSIF/END_IF, FOR/END_FOR which makes it approachable for anyone with Pascal, VB, or similar structured-language experience.
When should I avoid structured text?
For simple, discrete on/off logic that a maintenance electrician needs to troubleshoot on the plant floor, ladder logic is usually still the better choice. It maps directly to the physical wiring they already understand.