A PLC program is a loop you never write
Picture a conveyor in a packing hall. An operator presses a green button, a 0.75 kW motor starts, cartons move. Behind the cabinet door a PLC is running a program that decides whether the motor contactor gets 24 V. It might be a Siemens S7-1500, an Allen-Bradley CompactLogix or a CODESYS soft-PLC on an industrial PC. The program is a few lines of Structured Text, and what makes it a PLC program is how it runs.
A desktop program starts, does its work and ends. A PLC program never ends. The controller runs it again and again, typically every 1 to 100 milliseconds, for as long as the machine has power. Each pass is called a scan (or a cycle), and every scan has the same three steps:
- Read inputs. The CPU copies the state of every input terminal (buttons, sensors, analog transmitters) into memory. This copy is the process image.
- Execute the program. Your Structured Text runs once, top to bottom, using those copied values.
- Write outputs. The values your program left in the output variables are copied out to the output terminals: contactors, valves, lamps.
Then it starts again. You do not write this loop; the controller owns it. You write what happens in step 2, and you write it knowing it will happen again in a few milliseconds with fresh inputs.
That single idea explains most of what surprises new PLC programmers. A line of ST does not run "once, when the button is pressed". It runs on every scan, whether or not anything changed. Keep that picture in your head and the rest of this course will make sense.
The three-step scan is the model every IEC 61131-3 controller presents to you, and on Siemens, CODESYS and TwinCAT targets it is also close to what the runtime really does. Rockwell Logix controllers are the well-known exception: their I/O modules update tags on their own schedule, at each module's requested packet interval (RPI), with no input image frozen at the start of the scan. You will learn to work with the simple model first. Chapter 26 shows what changes when inputs can move under your feet.
Where the scan came from
Before PLCs, machine logic was built from relays. A panel of a few hundred relays has no "order of execution": every coil responds to its contacts at once, limited only by how fast the armatures move. Changing the logic meant rewiring the panel, and in a car plant that happened at every model change.
In 1968 General Motors' Hydra-Matic transmission division asked suppliers for a programmable replacement for relay panels. One of the answers was the Modicon 084 from Bedford Associates, where Dick Morley led the design. The computers of the day could only do one thing at a time, so the designers made the computer look like a relay panel by evaluating every rung in turn, very quickly, and then updating all the outputs together. That is the scan. It survives because it gives a sequential processor the property that made relay logic easy to reason about: every decision in one pass is based on the same picture of the machine.
IEC 61131-3 came much later, in 1993, to give the many vendor dialects a common set of languages: Ladder Diagram, Function Block Diagram, Structured Text, Instruction List and Sequential Function Chart. The second edition followed in 2003, the third in 2013 (which deprecated Instruction List and added object-oriented features) and the fourth in 2025, which drops Instruction List. The engine on these pages runs the second-edition core, which is also the part that every major vendor implements in much the same way.
Your first program
Here is a complete Structured Text program. It is the electrical equivalent of a switch wired straight to a lamp, except that the wire runs through the CPU.
PROGRAM Main
VAR
Switch : BOOL; (* input: a selector switch on the panel *)
Lamp : BOOL; (* output: a 24 V pilot lamp *)
END_VAR
Lamp := Switch;
END_PROGRAMThis box is a live program. It runs on a Structured Text engine inside this page, one scan every 100 milliseconds of virtual time, just like a controller. Flip Switch on the right and watch Lamp follow it on the next scan. The online-watch table shows what an engineer sees when they connect to a real PLC: every variable and its current value.
Read the program from top to bottom:
PROGRAM Main…END_PROGRAMwraps the whole program and gives it a name. Real projects have several programs, and the controller's task configuration decides which ones run and how often. Here there is one, and it runs every scan.VAR…END_VARis the declaration section. Every variable the program uses must be declared here with a name and a type.BOOLis the type for a signal that is eitherTRUEorFALSE.(* ... *)is a comment. The controller ignores it; the next engineer will read it.Lamp := Switch;is the only statement. The symbol:=means assign: compute the value on the right, store it in the variable on the left. The semicolon ends the statement.
In a real project, Switch and Lamp would be mapped to physical terminals: %I0.0 and %Q0.0 on a Siemens CPU, Local:1:I.Data.0 on a Rockwell controller, a linked variable in TwinCAT. The mapping is configuration, it varies by vendor, and it lives outside the logic. The logic itself is the line you just ran, and it is the same on all of them. That portability is why IEC 61131-3 exists.
The same program in four editors
Every environment wraps that one line differently, and the wrapping is the first thing that confuses someone moving between them. Here is Lamp := Switch; as each vendor's editor holds it. These are static listings in vendor syntax; they do not run on this page.
CODESYS and TwinCAT 3 are closest to the standard. A new CODESYS standard project contains a program called PLC_PRG; a new TwinCAT PLC project calls it MAIN. The editor shows the declaration in the top pane and the code in the bottom pane, but the text is the same as ours:
PROGRAM MAIN
VAR
Switch AT %I* : BOOL; (* TwinCAT: linked to a terminal in the I/O tree *)
Lamp AT %Q* : BOOL;
END_VAR
Lamp := Switch;
The AT %I* and AT %Q* mark a variable as an input or output whose address the system manager assigns when you link it to an EtherCAT terminal, such as one channel of an EL1008 input card.
Siemens calls its Structured Text SCL, and in TIA Portal there is no PROGRAM keyword. The cyclic program is an organisation block, OB 1 "Main", and the I/O tags live in the PLC tag table with absolute addresses. In SCL a global tag is written in double quotes and a block-local variable with a #:
// TIA Portal SCL, inside OB 1 or a function called from it
"Lamp" := "Switch"; // tags from the PLC tag table: %Q0.0 and %I0.0
#LocalCopy := "Switch"; // a variable declared in this block's interface
Rockwell's Studio 5000 has a Structured Text routine that holds statements only. Tags are declared separately, in the controller or program tag database, so the routine is literally one line:
// Studio 5000 ST routine. Lamp and Switch are tags, possibly aliases for I/O points
Lamp := Switch;
Logix ST also accepts /* ... */ comments alongside // and (* ... *), and it has a second assignment operator, [:=], the non-retentive assignment. A tag assigned with [:=] is reset to zero each time the controller enters Run mode (and, in an SFC set up for automatic reset, when the step that assigns it is left). It has no equivalent in the standard, which is a good reason to avoid it in code you might port.
The declaration moves and the quoting changes from one editor to the next, but the statement and the scan around it are the same everywhere, and those are what this course teaches.
What "every scan" really means
The program above looks like it does nothing until the switch moves. In fact it runs ten times a second here, and up to a thousand times a second on a fast controller. Every scan it copies Switch into Lamp again, even when neither changed. Let a program count its own scans and you can see the loop directly:
PROGRAM Main
VAR
ScanCount : DINT := 0; (* starts at zero when the PLC starts *)
Seconds : REAL;
END_VAR
ScanCount := ScanCount + 1;
Seconds := DINT_TO_REAL(ScanCount) * 0.1; (* this engine scans every 0.1 s *)
END_PROGRAMScanCount := ScanCount + 1; reads the current value, adds one and stores the result. Because the statement runs on every scan, the count climbs by one per scan for as long as the program runs. Use the pause and Step one scan buttons in the header to advance a single scan at a time. It is the most useful debugging habit you will learn in this course.
Two details in that program are easy to miss:
:= 0in the declaration is an initial value. It is applied when the controller starts the program, not on every scan. If it were applied every scan, the counter would never pass one.DINTis a 32-bit integer. A 16-bitINTwould overflow at 32,767 scans: under an hour at 100 ms, under a minute at 1 ms. Choosing types is the subject of the next chapter.
A scan counter like this one is worth keeping in real projects too. Put one in each task, show it on a diagnostics screen, and you can tell at a glance whether a program is still being executed. A counter that has stopped moving means the task is not running, whatever the rest of the screen says.
The first scan
Some work must happen exactly once, when the controller starts: clear a fault latch, load a default recipe number, put a sequence into its idle step. Initial values cover the simple cases. For anything that needs a calculation, programs use a first-scan flag, a BOOL that is TRUE on the first scan and FALSE on every scan after it.
The portable way to make one is to declare it yourself, with an initial value of TRUE, and clear it as the very last statement of the program:
PROGRAM Main
VAR
FirstScan : BOOL := TRUE; (* TRUE until the end of scan 1 *)
SawFirstScan : BOOL; (* a copy taken at the top of each scan *)
StartupCount : DINT; (* how many scans ran the startup work *)
ScanCount : DINT;
END_VAR
SawFirstScan := FirstScan;
StartupCount := StartupCount + BOOL_TO_DINT(FirstScan); (* the "startup work" *)
ScanCount := ScanCount + 1;
FirstScan := FALSE; (* last line: every later scan sees FALSE *)
END_PROGRAMStep it once from a fresh start. SawFirstScan is TRUE after scan 1 and FALSE from scan 2 on, and StartupCount stops at 1. (BOOL_TO_DINT turns TRUE into 1 and FALSE into 0; it is covered in the next chapter. With IF from chapter 5 you would write the startup work as a branch.)
Now look at FirstScan itself in the watch table. It never shows TRUE, even after the very first scan, because the program sets it FALSE before the scan ends and the watch table only ever shows end-of-scan values. On a real controller you would see the same. That is why SawFirstScan exists in this example, and it is a pattern you will use when debugging: if a value only exists in the middle of a scan, copy it to another variable where it is still there at the end.
The vendors also give you a ready-made first-scan signal, each in its own way:
| Platform | Built-in first-scan signal | Notes |
|---|---|---|
| Studio 5000 (Logix) | S:FS, the first-scan status bit | TRUE during the first scan after the controller enters Run mode |
| TIA Portal (S7-1200/1500) | Startup OB, OB 100, runs once before the first OB 1 cycle; the system memory byte also has a FirstScan bit | System and clock memory must be enabled in the CPU properties |
| TwinCAT 3 | _TaskInfo[GETCURTASKINDEXEX()].FirstCycle | One flag per task |
| CODESYS | A flag of your own, as above, is the common habit |
Whichever you use, put only cheap, bounded work behind it. A first scan that loops over ten thousand recipe entries can trip the cycle-time watchdog before the machine has done anything. And decide deliberately what the first scan must not do. A sequence that starts in its idle step after a power cut is safe; one that resumes "running" because a variable survived is not. Retained memory and what it should hold come up in the next chapter.
Statement order inside one scan
Within a scan, statements run in the order they are written, and each one sees the results of the lines above it. This program copies A into B before incrementing A:
PROGRAM Main
VAR
A : INT := 0;
B : INT := 0;
END_VAR
B := A; (* B gets A's value from the end of the previous scan... *)
A := A + 1; (* ...then A moves on *)
END_PROGRAMStep it one scan at a time. After every scan B is exactly one behind A, because B was assigned before A changed. Swap the two lines in the editor (the program rebuilds when you stop typing) and B catches up with A. The variables are the same in both versions; only the order changed.
On a machine this turns up as decisions made on stale data. Suppose one line calculates a fill target and a later line starts a valve when the weight is below the target. Put them in the wrong order and the valve decision uses last scan's target. At 10 ms per scan that is usually harmless; on the scan where a recipe changes it is not. The rule is simple: calculate before you decide, decide before you command. Most well-organised ST programs read in exactly that order.
Here is a smaller version you will meet on real panels. A conveyor runs when it is requested and the guard is closed, and a green beacon shows that it is running. The beacon line was written first:
PROGRAM Main
VAR
RunRequest : BOOL; (* input: auto sequence asks for the belt *)
GuardClosed : BOOL := TRUE; (* input: guard door switch *)
Beacon : BOOL; (* output: green "running" beacon *)
Conveyor : BOOL; (* output: belt contactor *)
END_VAR
Beacon := Conveyor; (* reads last scan's decision *)
Conveyor := RunRequest AND GuardClosed; (* today's decision *)
END_PROGRAMPause the example, switch RunRequest on and step one scan. Trace it the way the controller does:
| Scan | RunRequest | GuardClosed | Conveyor when line 1 runs | Beacon at end of scan | Conveyor at end of scan |
|---|---|---|---|---|---|
| 1 | FALSE | TRUE | FALSE | FALSE | FALSE |
| 2 | TRUE | TRUE | FALSE | FALSE | TRUE |
| 3 | TRUE | TRUE | TRUE | TRUE | TRUE |
| 4 | TRUE | FALSE | TRUE | TRUE | FALSE |
| 5 | TRUE | FALSE | FALSE | FALSE | FALSE |
On scan 2 the contactor pulls in and the beacon is still dark. On scan 4 the guard opens, the contactor drops, and for one more scan the beacon says "running". At 10 ms nobody sees a 10 ms lag on a beacon. The same structure on a part counter, a handshake bit to a robot or an interlock that another PLC reads over PROFINET is a real bug, and it is the kind that appears once a day at random. You will fix this one in the challenge at the end of the chapter.
Inputs are a snapshot, outputs are a promise
Because inputs are copied once at the start of the scan, your program sees a frozen picture of the machine for the whole scan. If a photo-eye flickers in the middle of your program's execution, your program will not see it until the next scan. Every line in one scan agrees about what the inputs were, and that agreement is what makes a program like the beacon example traceable at all.
Outputs work the other way round. Writing Lamp := TRUE; does not switch the lamp at that moment. It changes a value in memory, and the terminal follows when the outputs are written at the end of the scan. So if a program writes an output twice in one scan, only the last value reaches the terminal:
PROGRAM Main
VAR
Fault : BOOL; (* input from a drive *)
RunLamp : BOOL; (* output *)
END_VAR
RunLamp := TRUE; (* an earlier part of the program says "on" *)
RunLamp := RunLamp AND NOT Fault; (* a later part has the final word *)
END_PROGRAMWith Fault off, the lamp is on. Switch Fault on and the lamp never lights, not even for an instant, because the terminal only ever receives the final value. Engineers use this deliberately (a "last writer wins" override at the end of a program) and by accident (two people writing the same output in different routines). In chapter 21 you will see why every output should have exactly one owner.
Siemens shows the two halves of this model in its addressing. %I0.0 reads the process image, the copy taken at the start of the cycle; %I0.0:P (in TIA Portal, a tag with :P appended) reads the terminal directly, at the moment the instruction executes. %Q0.0:P writes straight to the module. CODESYS and TwinCAT update their I/O at the task boundary, and Logix, as you saw at the start, updates I/O tags asynchronously, so a direct read is effectively what you always get there. If your code is meant to run on more than one of these, write it so that it does not care: copy each input you use into a variable once, near the top of the program, and use only the copy below. That one habit makes all three platforms behave like the simple model.
How long a scan takes, and what it costs you
The scan time is the time from the start of one scan to the start of the next. It is not a fixed number. It is made up of the input transfer, your program, the output transfer and whatever housekeeping the operating system does in between, such as serving the HMI, the programming laptop and the web server. On an S7-1500, communication runs at a higher priority than the cyclic program and is allowed to take a configurable share of the CPU, so a busy HMI makes the cycle longer.
Your program's share also changes from scan to scan. An IF branch that only runs when a batch completes, a loop that only runs when a recipe is downloaded, a string conversion for a report: each makes some scans longer than others. What you must design for is the longest scan, and every platform tells you what it is:
- In TIA Portal, Online & diagnostics on the CPU shows the shortest, current and longest cycle time since the last start.
- In Studio 5000, the Monitor tab of each task's properties shows its maximum and last scan time, and lets you reset them.
- In CODESYS and TwinCAT, the task configuration shows each task's last, average and maximum execution time while you are logged in.
Look at the maximum after every significant change, not just the average. A program that averages 4 ms and occasionally takes 40 ms is a 40 ms program for anything time-critical.
Controllers can run the main program in two ways. Freewheeling (Siemens OB 1, a Logix continuous task, a CODESYS freewheeling task) starts the next scan as soon as the last one ends, so the scan time drifts with the load. Cyclic (a Logix periodic task, a TwinCAT or CODESYS cyclic task, a Siemens cyclic interrupt OB such as OB 30) starts at a fixed interval and idles in between. TIA Portal can also hold OB 1 to a minimum cycle time. Fixed intervals make timing predictable, which you will want for PID loops; chapter 26 covers how to choose.
The scan time sets a floor on how fast the program can react. Work through one example. A photo-eye on a conveyor changes state; the program must switch off a reject gate's solenoid. Assume a 10 ms cyclic task, a digital input card with a 3 ms input filter (a common default) and an output card that switches in under a millisecond. The photo-eye changes at an unlucky moment, just after the input transfer:
| Step | Best case | Worst case |
|---|---|---|
| Input filter on the module | 3 ms | 3 ms |
| Waiting for the next input transfer | 0 ms | 10 ms |
| Program execution until the output is written | 2 ms | 2 ms |
| Output card switching | < 1 ms | < 1 ms |
| Total, sensor to terminal | about 6 ms | about 16 ms |
The worst case is a whole interval of waiting on top of the filter and the program, and that is before the solenoid's own response. Some controllers transfer the outputs at the start of the next cycle rather than straight after the program, which can add up to one more interval; the CPU manual describes the order for yours. If the specification says "the gate must close within 15 ms of the product passing", a 10 ms task cannot meet it, however tidy the code. You need a faster task, a hardware-timed output, or a sensor placed further upstream.
The same arithmetic tells you what a program can miss. A signal has to be present at an input transfer to be seen at all. A label on a bottle passing a sensor at 2 m/s, 6 mm long, blocks the beam for 3 ms. On a 10 ms task the program sees it on some bottles and misses it on others. No line of ST can recover a pulse the input image never contained. The fixes live outside the program: a high-speed counter input, a module with pulse stretching, or a faster task with the input filter reduced. Chapters 8 and 11 come back to this with edges and counters.
RUN, STOP and the state of the outputs
A controller is not always scanning. It has operating modes, and you will spend part of every commissioning day switching between them.
In RUN the program is executed and the outputs follow it. In STOP (Siemens), Program mode (Logix) or with the application stopped (CODESYS, TwinCAT), the program is not executed, and the outputs do whatever their module is configured to do. Logix controllers have a key switch, or a mode setting, with RUN, REM and PROG positions; REM lets the programming software change the mode remotely. S7-1500 CPUs have a RUN/STOP selector on the front, and a display that shows the mode. CODESYS and TwinCAT have no switch: you log in and start or stop the application from the IDE, and TwinCAT also distinguishes Config mode, where the real-time system is not running, from Run mode.
What the outputs do in STOP is a parameter of the output module or channel, not of your program. The usual choice is to switch outputs off. Most output modules also offer holding the last value or applying a configured substitute value, and on a Logix output module you set the state for Program mode and for Fault mode, channel by channel, with options of off, on or hold. Find out what yours is set to before you stop a controller on a running plant.
One case catches people who come from software. A valve that must stay open while the PLC is stopped, to cool a reactor jacket for example, is configured as "hold last value" by someone who knows the process. Then a technician stops the CPU to load a change, the valve holds open as intended, and the program that would have closed it on a high-pressure alarm is not running. Holding outputs in STOP means the plant is running with no logic watching it. If you inherit such a setting, find out why it is there and write it down next to the I/O list.
Neither STOP nor a fault in the standard PLC is a safe state for people. Stopping a CPU stops your program, but a hydraulic press, a falling load or a motor with a held output does not care. Protection of people is the job of a safety relay or a safety PLC, designed to ISO 13849-1 or IEC 62061, which removes energy from the hazard independently of the standard program. The standard program can monitor and report that circuit; it is never the circuit.
Reading a program like the controller does
When you meet an unfamiliar ST program, read it the way the controller executes it. Start at the declarations and note which variables are inputs, which are outputs and which are internal memory. Then take one scan with concrete input values and walk down the statements, writing each variable's new value as you go, as in the beacon table above. Do it twice: once for the scan where an input changes, once for the scan after. Most bugs show up on one of those two scans.
The live examples on these pages do that for you. The Step one scan button is the controller's loop with a pause in it. Use it whenever an example surprises you.
A few rules of the language are worth fixing in memory now:
- Case is ignored in keywords and names.
Lamp,LAMPandlampare the same variable. Pick one style and keep it; this course usesPascalCasefor variables and capitals for keywords. - Whitespace is ignored too. Line breaks and indentation are for people.
Lamp:=Switch;is legal, and harder to read. - Every statement ends with a semicolon. Missing semicolons are the most common build error you will see.
- Comments are written
(* like this *)and can span lines. Most environments also accept// to the end of the line. - Names start with a letter or underscore and contain letters, digits and underscores. No spaces, and no accented characters on most targets. Two underscores in a row are not allowed by the standard, and a name cannot be a keyword: a variable called
IForTIMEwill not build.
When a build fails, read the first error and fix that one only, then build again. A single missing END_VAR or semicolon can produce a page of follow-on errors that all disappear together.
Where your program lives on a real controller
The single PROGRAM Main in these examples hides a little structure that every real project has. IEC 61131-3 describes it as a nest. A configuration is the whole PLC system. It contains one or more resources, roughly one per CPU. A resource runs tasks, and a task tells the controller how to run programs: cyclically every 10 ms, as fast as possible, or when an event fires. Each task runs one or more program instances. Programs call functions and function blocks (chapters 17 and 18) to avoid repeating themselves.
Vendors name these differently. Siemens has OBs, FBs and FCs; Rockwell has tasks, programs and routines; CODESYS and TwinCAT follow the IEC names closely. The scan you just watched is the same idea everywhere.
Two more numbers belong to every task. Its interval is how often it starts. Its watchdog is how long it may run before the controller decides something is wrong and stops. An S7-1500 allows OB 1 a maximum cycle time of 150 ms by default, and a Logix task's watchdog is 500 ms by default; both are configurable. A program that loops waiting for a sensor never reaches its last line, the outputs are never written, and the watchdog stops the controller. Chapter 14 shows that failure on purpose.
Three commissioning stories
These are the shapes the scan takes when it goes wrong on site. The details are generic; the patterns are not rare.
The lamp that flickers on the HMI
A packaging machine's "door locked" indicator on the panel flickers, while the lamp on the door itself is steady. The program writes the HMI tag twice: once in the door routine, TRUE when the lock feedback is made, and once in a diagnostics routine written later, which sets every status tag FALSE at the start of its own section and then re-evaluates. The HMI reads tags over the network at moments that have nothing to do with the scan, so some of its reads land between the two writes. The fix is the rule from chapter 21: one owner per output, and a status tag is an output.
The counter that loses a few parts an hour
A case counter at the end of a conveyor is short by about one percent against the palletiser count. The sensor is fine and the logic is a correct rising-edge count. The cause is a gap between cases of a few milliseconds when the line runs at full speed, on a 20 ms task. Some gaps fall between two input transfers and the program sees one long case where there were two. The investigation is the arithmetic from the timing section: gap length against scan time plus filter. The fix moved the count to a high-speed counter input on the module.
The scan time that grew
A line runs well for a year. Then a data-logging feature is added: every scan, a loop formats twenty values into strings for a report. The average cycle time goes from 6 ms to 9 ms, which nobody notices, and the maximum goes to 70 ms when the report buffer is flushed. A reject gate that relied on a 10 ms response starts missing bad bottles once a minute. Looking at the maximum cycle time would have shown it the day the change went in. Moving the report into a slower, lower-priority task solved it.
What to take into the next chapter
You now know the shape every Structured Text program shares (declarations, then statements, wrapped in a PROGRAM) and, more usefully, how it runs. The controller reads inputs, executes your statements in order and writes outputs, over and over. Initial values happen once; statements happen every scan; a first-scan flag covers the work in between. Later lines see earlier lines' results, and the watch table only shows what is left at the end of a scan. The longest scan, plus the input filter, sets how fast the machine can react and which short pulses it can miss.
Every chapter from here builds on that loop. The next one is about the variables themselves: which types exist, how much they can hold and how to choose the right one for a signal before it overflows on a Friday night.