Overview: Why PLC Programming Is Not "Just Code"
PLC programming is frequently taught backwards. Most vendor-sponsored courses and on-the-job training concentrate on the trivia associated with specialized PLC functions: which bit to set on a TON timer, which data block holds a PID block's instance DB, which input word corresponds to the analog card. That trivia is necessary, but it is not programming. It is the vocabulary. Writing sentences requires vocabulary; the sentences still must follow grammar, structure, and intent.
The discipline below treats PLC programming as a structured-engineering problem with the same shape regardless of the controller family — Siemens SIMATIC S7-1200 / S7-1500, Mitsubishi MELSEC iQ-R with GX Works3, Allen-Bradley ControlLogix with RSLogix 5000 / Studio 5000, or any IEC 61131-3 platform. The vocabulary changes; the grammar does not.
Prerequisites Before You Touch a PLC
Before opening any programming software, build a foundation in general program structure. A one-semester C or Fortran course is sufficient; you do not need to become a software engineer, but you do need to internalize the following concepts:
- Main program and subroutines (or functions / function blocks). A controller scans one main routine repeatedly; the work is divided into subroutines that are called, either conditionally or by event, from the main. See the IEC 61131-3 program organization unit (POU) model in IEC 61131-3:2013.
- Database / tag memory. Discrete tags, word tags, retentive vs non-retentive, and instance data blocks (Siemens) or program tags (Allen-Bradley). Each variable lives somewhere and survives scan cycles or does not.
- Fixed-point arithmetic. Real-world signals are scaled integers. A 4–20 mA loop at 12.00 mA is a raw 12,000 count, not 12.00. Plan the scaling before connecting anything.
- Looping and conditional branching. FOR/WHILE/IF and the PLC equivalents (LOOP, JMP, --( )-- contacts).
- Debugging with watch tables, cross-references, and single-step. You must be able to stop the scan, observe a tag, force a value, and resume. On Siemens this is the Watch and Force Table; on Allen-Bradley it is the Data Monitor within RSLogix 5000.
- Reusable code. A function block (FB) with an instance DB in Siemens terminology, or an Add-On Instruction (AOI) in Allen-Bradley terminology. The same scale_linear block should not be rewritten four times.
Once these concepts are in place, the PLC's specialized functions — bit manipulation, timers, counters, indirect addressing, HSC, PTO — become a matter of looking up the syntax, not inventing the architecture.
The 70/30 Rule: Where Project Time Actually Goes
Empirical observation across hundreds of industrial projects converges on the same ratio:
| Phase | Activity | Share of effort |
|---|---|---|
| 1 | Conceptual essay / functional description | ~20% |
| 2 | Comment-only skeleton in the editor | ~25% |
| 3 | I/O cross-check with mechanical and electrical design | ~15% |
| 4 | Writing the actual code (LD, FBD, ST, SCL) | ~30% |
| 5 | Desk simulation, FAT, SAT, field commissioning | ~10% |
Pressure to compress Phase 1 and 2 produces monolithic ladder files exceeding one hundred pages. These files cannot be debugged by anyone except the original author, fail the moment a sensor is replaced, and routinely get scrapped and rewritten. A disciplined three-page subroutine per POU is the practical ceiling for human readability.
Day 1: Write the Design Essay Before Any Code
Before opening TIA Portal, GX Works3, Studio 5000, or any other editor, write a plain-text essay that describes the block of code in unambiguous prose. Save it as a project document. The essay contains:
- Startup sequence. What must be true (or made true) for the machine to leave the safe state and enter Run.
- Run sequence. The state transitions during normal operation, with the conditions that drive each transition.
- Shutdown sequence. What must be parked, de-energized, or vented before power is removed or E-Stop is acknowledged.
- Fault handling. Detected faults, latched conditions, and the recovery path that does not require a power cycle.
- List of all machine states altered by this block. Valve positions, carriage positions, part counts, process durations, weights, temperatures, speeds, interlock flags.
- List of all input and output variables. Symbolic names, addresses, signal types (24 VDC discrete, 4–20 mA, encoder, PTO).
- Memory requirements. Approximate count of bool, int, real, and string tags; whether the data must be retentive.
With this essay in hand, walk it through with the mechanical designer. Confirm every motor, valve, limit switch, prox, photo-eye, and pressure transducer that the code expects is actually present on the drawing and physically mounted where the logic assumes. Then walk it through with the electrical designer. Confirm the wire list, the I/O card slotting, and the field-device terminal designations match the symbolic names in the essay.
Day 2: Write the Comments Before the Code
Open the PLC editor and create the routine(s) called for in the essay. Drop the essay into the routine header as a multi-line comment. Then write only the comments for every rung or statement. No contacts, no coils, no assignments. The comments must say what the line below them is supposed to do.
This step is iterative. Writing comments forces the following questions:
- Are the discrete inputs in the right order to express the interlock cleanly, or am I jumping through inverted contacts?
- Is the timing plausible? Will the cylinder actually reach the end-of-stroke within the timer preset? Will the motor accelerate within the interlock window?
- Are actuators sized for the duty cycle implied by the sequence?
- Do I need a third part that the original P&ID did not show?
The same step is also where you estimate scan-time headroom and memory headroom. If the comment block for a routine implies 800 timers and 2,000 booleans on a controller with 256 timers and 8 KB of work memory, that fact must surface during the comment phase — not when the controller throws a resource fault during download.
Editor Format: Why Statement List Plus Comments Reads Cleanly
Ladder logic is a graph, and dense ladder logic does not compress. A hundred-page ladder file is unreadable; a hundred-page statement list with one comment line per executable line is. Siemens STEP 7 / TIA Portal SCL (Structured Control Language, IEC 61131-3 ST) and Mitsubishi MELSEC structured text both lend themselves to this layout. One executable line, one comment line, executed in source order. The graph view of ladder is preserved for the I/O forcing and troubleshooting stage, but the source of truth is the structured text.
Day 3: Write the Code to Match the Comments
With the comments in place, the actual code becomes bookkeeping. For every comment line, drop in the contact, coil, timer, function block call, or assignment that implements it. Because the comment already names the inputs, outputs, and state, the code rarely requires a design decision — only an instruction lookup.
Repeat the three-day cycle for every subroutine. If a routine exceeds roughly three pages of structured text, it is a sign that the block should be decomposed into smaller POUs. Three pages is the human-readable ceiling; beyond that, debugging requires the author in the room.
State Modeling: The Machine as a Finite-State Machine
A PLC controls a machine, and a machine has a finite, named set of states. Common state names that must appear explicitly in the program:
| State category | Examples | Representation in code |
|---|---|---|
| Mechanical position | CarriageHome, ClampUp, GateClosed | Latched boolean in retentive memory |
| Process variable | TankLevel_High, Pressure_OK, TempInRange | Compare-result against scaled tag |
| Sequencer step | Step10_Dwell, Step22_Transfer | Integer step number in a state register |
| Mode | Auto, Manual, Service, Bypass | Enumerated (int) tag |
| Fault | E_StopActive, DriveFault, SensorTimeout | Latched bool, cleared by reset routine |
A simple drum sequencer in SCL illustrates the pattern:
// State machine for two-station index table
CASE iStep OF
0: // Idle - waiting for start and home confirmed
IF bStart AND bHomeOK AND NOT bEStopActive THEN
iStep := 10;
END_IF;
10: // Extend clamp cylinder
bClampExtend := TRUE;
IF bClampExtended THEN
bClampExtend := FALSE;
iStep := 20;
tDwell(IN:=TRUE, PT:=T#500ms);
END_IF;
20: // Dwell for clamp confirmation
IF tDwell.Q THEN
tDwell(IN:=FALSE);
iStep := 30;
END_IF;
30: // Rotate table to next station
bTableRotate := TRUE;
IF bTableInPosition THEN
bTableRotate := FALSE;
iStep := 40;
END_IF;
40: // Retract clamp
bClampRetract := TRUE;
IF bClampRetracted THEN
bClampRetract := FALSE;
iStep := 0;
bCycleComplete := TRUE;
END_IF;
ELSE
// Invalid step - latch fault, force to safe
bSequenceFault := TRUE;
iStep := 0;
END_CASE;
Every transition has an explicit guard. The state register (iStep) is retentive so a brief power loss does not leave the machine mid-stroke. The ELSE arm catches the impossible state — a code change, a firmware update, or a corruption event that pushes the step register to a value outside the design set.
PLC Database Initialization: The Most-Common Beginner Mistake
Non-programmers routinely overlook the startup problem. When the PLC powers up cold, every tag is at its default value: bools at FALSE, ints at 0, reals at 0.0. The machine is not in state zero; the machine is in an unknown state. The PLC must determine the actual physical state before it does anything else.
Initialization Subroutine (OB100 on Siemens, SFC_PRG / First Scan on Mitsubishi)
// OB100 - Startup / Warm Restart
// Initialize non-retentive tags and force safe outputs
// 1. Force all discrete outputs to safe state
bMotorRun := FALSE;
bValveOpen := FALSE;ClampExtend := FALSE;
bTableRotate := FALSE;
bHeaterEnable := FALSE;
// 2. Clear non-retentive flags
bCycleComplete := FALSE;
bSequenceFault := FALSE;
iCycleCounter := 0;
// 3. Initialize timers and counters
tDwell(IN := FALSE);
cRejectCount := 0;
// 4. Request physical state scan
// (reads inputs to determine actual machine position)
bInitInProgress := TRUE;
bRequestHomeMove := TRUE;
// 5. State machine returns to Step 0
iStep := 0;
On Siemens S7-1200/1500, OB100 runs once on warm restart and OB102 on cold restart. The retentive bit-memory area (M area marked retentive in the PLC tag table) and retentive instance DBs preserve state across warm restarts. On Mitsubishi iQ-R, the equivalent is the SFC that runs on the first scan after a STOP-to-RUN transition. On Allen-Bradley ControlLogix, the SFC forces routine execution on first scan via the S:FS system bit in RSLogix 5000.
Structured Programming: Main Routine Plus Subroutines
The recommended organization for a SIMATIC S7-1500 project:
| Block (OB / FB / FC) | Purpose | Scan behavior |
|---|---|---|
| OB1 / MainTask | Cyclic main — calls subroutines in fixed order | Every scan |
| OB100 / Startup | One-shot initialization at power-on | Once |
| OB82 / OB86 / OB121 | Error / diagnostics handlers | On event |
| FC10_Inputs | Scale and debounce raw inputs | Every scan |
| FC20_Sequencer | State machine | Every scan |
| FC30_Outputs | Apply output pattern with interlocks | Every scan |
| FC40_Alarms | Alarm detection and latching | Every scan |
| FC50_HMI | HMI data exchange | Every scan or event-driven |
| FB100_ScaleAnalog | Reusable analog scaling FB with instance DB | Called |
The main routine becomes a five-line dispatcher:
// OB1 - Main (cyclic)
FC10_Inputs(); // Read and scale all field I/O
FC20_Sequencer(); // Drive state machine
FC30_Outputs(); // Apply validated outputs to terminals
FC40_Alarms(); // Evaluate and latch alarm conditions
FC50_HMI(); // Update HMI tags and request packets
This pattern matches IEC 61131-3 program organization and works identically on Mitsubishi MELSEC, Allen-Bradley ControlLogix (RSLogix 5000 periodic task), and Beckhoff TwinCAT. The vendor-specific trivia lives inside the FCs; the architecture does not change.
Memory and Scan-Time Planning
The essay phase must produce a rough estimate. For a SIMATIC S7-1214C with 100 KB work memory, typical budget:
| Resource | Estimate | Margin | Decision |
|---|---|---|---|
| Program code (SCL) | 20 KB | 4× | Sufficient |
| Instance DBs (FBs) | 15 KB | 3× | Sufficient |
| Global tags | 8 KB | 5× | Sufficient |
| HMI buffers | 10 KB | 2× | Sufficient |
| Scan time (target 20 ms) | 8 ms estimated | 2.5× | Sufficient |
| Retentive tag count (max 8,192 bits) | 1,200 bits | 6.8× | Sufficient |
If the scan-time estimate exceeds the process requirement (a high-speed indexing application may require 1 ms scan), move the time-critical routine to a separate fast task (Siemens OB35 / cyclic interrupt) rather than shortening the main scan to unsafe levels. Validate the scan time after the comment-only phase using the PLC's online diagnostics — on Siemens this is Online & diagnostics → Scan cycle time.
Practical Walkthrough: Three-Stage Conveyor with E-Stop
Concrete application of the three-day method:
Day 1 Essay Excerpt
Conveyor system CV-100 has three motors (M1, M2, M3), three upstream photocells (PE1, PE2, PE3), and one downstream eye (PE4) for jam detection. On startup, all three motors must be off, downstream jam must be clear, and E-Stop circuit must be healthy. Run mode: each motor starts only when the upstream eye is blocked, with a 200 ms interlock to prevent simultaneous inrush. E-Stop must de-energize all motor contactors within 100 ms of circuit opening. Fault conditions: any motor overload, any photocell dark for > 10 s, downstream eye blocked for > 5 s.
Day 2 Comment Skeleton
// --- Conveyor Sequencer ---
// IF E-Stop circuit healthy AND no motor overload AND no jam THEN
// allow individual motor start logic
// ELSE
// force all motor contactors off, latch fault
// --- Per-motor start rung ---
// IF upstream eye blocked AND downstream interlock satisfied
// AND no upstream motor fault AND 200 ms delay elapsed
// THEN start motor, assert running flag
// --- Photocell debounce ---
// Latch photocell-active for 50 ms to ignore vibration-induced transitions
Day 3 Code (SCL extract)
// Global E-Stop and fault gate
bPermitRun := bEStopHealthy
AND NOT bM1Overload
AND NOT bM2Overload
AND NOT bM3Overload
AND NOT bJamDetected;
FOR iMotor := 1 TO 3 DO
IF bPermitRun THEN
// Upstream-eye interlock
IF aEye[iMotor].Q THEN
TON_StartDelay[iMotor](IN := TRUE, PT := T#200ms);
IF TON_StartDelay[iMotor].Q THEN
aMotorRun[iMotor] := TRUE;
END_IF;
ELSE
TON_StartDelay[iMotor](IN := FALSE);
aMotorRun[iMotor] := FALSE;
END_IF;
ELSE
aMotorRun[iMotor] := FALSE;
TON_StartDelay[iMotor](IN := FALSE);
END_IF;
END_FOR;
Three POUs, ~80 lines of SCL, single comment line per executable line. Field-troubleshootable by the on-shift electrician who was not the author.
Common Pitfalls and Field Caveats
| Pitfall | Symptom | Prevention |
|---|---|---|
| No initialization of non-retentive tags | Random outputs on power-up, intermittent faults after power dip | OB100 routine, explicit safe-output pattern |
| Monolithic ladder > 100 pages | Unmaintainable, single-author dependency | Three-page-per-POU ceiling, decompose into subroutines |
| State machine with implicit transitions | Mode confusion after fault, two sequences running in parallel | Explicit CASE / state register, ELSE arm catches invalid steps |
| Mixing retentive and non-retentive without a list | Counts and positions reset on warm restart unexpectedly | Tag-by-tag retentive table in the design essay |
| Hard-coded timing instead of timer tags | Timing change requires code edit, no live tuning | All preset values as HMI-writable tags |
| Forgetting encoder index pulse after battery backup | Drift after power cycle, wrong home position | Re-home routine on startup, verify with physical marker |
| Edge-triggered logic without debounce | Phantom counts from contact bounce | IEC 61131-3 R_TRIG with 50 ms input filter or hardware debounce |
Verification Before Site Commissioning
- Desk simulation. Run the project in PLCSIM (Siemens), GX Simulator3 (Mitsubishi), or Studio 5000 Logix Emulator. Force each input transition and confirm the expected output pattern and state register advance.
- Scan-time measurement. Read the controller's online scan-time diagnostic and confirm the maximum scan is below the process requirement (typically < 50% of the fastest required response).
- Memory utilization. Confirm program and data memory are below 70% of the controller's published capacity. Above 70%, fragmentation and download time become problems.
- Cross-reference audit. Generate a cross-reference list and confirm every input address used in code is wired to a real terminal and every output address drives a real load.
- Comment completeness check. Every executable line has a corresponding comment. If a line is missing a comment, the code is not finished.
- Power-cycle test. With the machine in mid-cycle, remove power. Restore power. Verify the initialization routine brings the machine to a known safe state without operator intervention.
Recommended Learning Path
- Complete a one-semester C or Fortran course covering program structure, loops, branches, functions, and debugging.
- Read the IEC 61131-3 standard overview to understand POUs, data types, and the program organization model.
- Complete the Mitsubishi Programming Basics self-study module for ladder fundamentals.
- Work through the Siemens S7-1200 easy book and the matching TIA Portal tutorial project.
- Pick a real machine — even a benchtop conveyor or pneumatic pick-and-place — and apply the three-day method end to end.
- After delivery, return to the code in six months and try to modify it without the original author present. If you cannot, the structure was insufficient.
FAQ
What is the single biggest beginner mistake in PLC programming?
Skipping PLC database initialization on startup. When the controller powers up, every non-retentive tag is zero — not the value the machine was in before power loss. A dedicated startup OB (OB100 on Siemens, first-scan SFC on Mitsubishi) must set every output to a known safe state and force a physical-state scan before the main logic is allowed to run.
How long should a single PLC routine be?
Roughly three pages of structured text (ST / SCL) or equivalent ladder. Beyond that, debugging requires the original author in the room because the cognitive load exceeds what a shift electrician can hold. If a routine is growing past three pages, decompose it into subroutines or function blocks.
Ladder logic or structured text — which should a beginner learn first?
Learn ladder first because it maps directly to the electrical drawings the mechanical and electrical engineers produce. Then learn structured text (SCL on Siemens, ST on Mitsubishi and Beckhoff) because it scales: state machines, math, and string handling are unreadable in ladder. Most production codebases mix both — ladder for I/O interlock, ST for sequencers and scaling.
How much of project time should be spent writing actual code?
About 30%. The remaining ~70% is split between a written functional description (the design essay), comment-only skeleton in the editor, and I/O cross-check with mechanical and electrical design. Compressing the thinking phase produces monolithic, unmaintainable code and routinely blows the project schedule.
Do I need to learn a general programming language before touching a PLC?
Yes — at least one semester of C, Fortran, Pascal, or similar. The PLC's specialized functions are vocabulary; the program structure (main, subroutines, data, loops, branches, debugging) is grammar. Without the grammar, the vocabulary produces spaghetti. Vendor courses teach vocabulary; you still need the grammar from somewhere.