Let the program read like decisions about the machine
Structured Text is useful when decisions, calculations, and data structures are clearer in text than in a drawing. You do not need to think like a computer before writing it. Begin with a precise machine statement, then express that statement using the language's rules.
“If the carton is present and the mass measurement is valid, compare mass with the target; otherwise refuse filling.” That sentence suggests both a Boolean condition and a branch. The harder engineering work is deciding what “valid” and “refuse” mean. Syntax makes the decision executable; it does not supply the missing requirement.
Examples here use IEC-style notation. Real projects also need the target's declarations, libraries, task configuration, and I/O mapping. Vendor extensions and supported language editions differ, so use the target's compiler and documentation when transferring an example.
Choose types that fit the meaning
A BOOL holds a true-or-false condition. Integer types represent whole numbers such as counts or state identifiers. REAL represents fractional quantities with finite precision. TIME expresses durations. A string may hold a diagnostic label, but it is usually a poor primary representation of machine state.
Do not choose a type merely because an existing variable used it. A batch count needs enough range for the maximum allowed batch and intermediate operations. A mass value needs a defined unit and precision. A duration needs a documented upper bound. Conversion between types may round, truncate, or exceed range depending on the operation and target; make conversions explicit and test boundaries.
Units deserve a place in the design even when the language cannot enforce them. FillTargetKg, BeltSpeedMps, and ArrivalLimit are more informative than Value1, Speed, and TimerValue. Do not add kilograms to grams just because both happen to be REAL values.
Assignment changes a value; comparison asks a question
In these examples, := assigns and = compares.
TargetReached := MassKg >= FillTargetKg;
SameRecipe := ActiveRecipeId = RequestedRecipeId;
The semicolon finishes a statement. Indentation groups ideas for the reader, while keywords such as END_IF close the language structure. Use both consistently. A long expression should be split across lines at meaningful conditions rather than at an arbitrary screen width.
Avoid clever combinations with hidden side effects. Calculate inputs, call stateful blocks deliberately, then consume their outputs. Another person should be able to trace the data without guessing which part of an expression executes first.
IF chooses a path; omitted assignments preserve state
This excerpt assigns the command on every invocation:
IF FillRequested AND CartonPresent AND MassValid THEN
DispenserCmd := MassKg < FillTargetKg;
ELSE
DispenserCmd := FALSE;
END_IF;
If the ELSE branch is removed, a previously true command can remain true when a permissive disappears, provided it is persistent program state and no other assignment changes it. That may be intended for a memory variable, but it is a serious mistake when the author meant a continuously evaluated command.
One alternative is to assign a default first and then override it in the permitted branch. Keep those assignments within the same clear owner. Do not spread defaults and overrides across distant routines until output priority becomes a scavenger hunt.
The excerpt is a simplified demand calculation, not a full filling controller. It lacks overshoot management, actuator feedback, faults, and restart handling. A code example should say what problem it solves and leave its omissions visible.
CASE makes mutually exclusive behaviour readable
When a variable selects one of several distinct states, CASE often reads more clearly than a chain of comparisons. Named constants make the state values explainable.
(* Excerpt; state constants and variables are declared elsewhere. *)
NextState := State;
CASE State OF
STATE_READY:
IF StartEvent AND CyclePermitted AND NOT StopRequest THEN
NextState := STATE_SEEKING;
END_IF;
STATE_SEEKING:
IF StopRequest THEN
NextState := STATE_READY;
ELSIF CartonAtFill THEN
NextState := STATE_POSITIONED;
ELSIF ArrivalTimeout THEN
NextState := STATE_FAULTED;
END_IF;
STATE_POSITIONED:
(* Waiting for the next explicitly designed operation. *)
NextState := State;
STATE_FAULTED:
(* Recovery is handled by the fault owner. *)
NextState := State;
ELSE
NextState := STATE_FAULTED;
END_CASE;
State := NextState;
The default keeps the current state unless a transition is selected. The ELSE handles an unexpected state value. It should also produce an appropriate diagnostic in a full application. Assigning NextState does not cause CASE to jump into another branch during this invocation.
Avoid pretending that the numeric order of states represents process permission. State := State + 10 hides the destination and makes inserted states awkward. Name the next state explicitly.
Loops process data; states wait for the world
A bounded FOR loop is useful for examining a known set of values. This example counts active diagnostic flags in an eight-element array during one invocation:
ActiveFaultCount := 0;
FOR Index := 1 TO 8 DO
IF FaultFlags[Index] THEN
ActiveFaultCount := ActiveFaultCount + 1;
END_IF;
END_FOR;
Declare FaultFlags with matching bounds and choose integer types that contain the range. All eight iterations occur as program work; they are not eight PLC scans. For a large collection, estimate execution cost and consider distributing work across invocations if appropriate.
Do not write a WHILE loop that waits for CartonAtFill to become true. In the snapshot model, the observed input may not change while the loop runs, and the program can exceed its watchdog limit. A Seeking state returns control to the runtime and checks again on the next invocation.
The misconception: compiling means the logic is complete
A compiler can reject misspelled variables and incompatible operations. It cannot know that a carton target was specified in grams while your formula assumed kilograms. It cannot know that Stop should win over Start, or that a fault reset must not restart a dispenser.
Resolve compiler warnings about conversion and truncation by checking the intended value range; do not silence them just to produce a clean build.
After compilation, return to the behaviour contract. Trace one normal cycle and the uncomfortable cases. A short, well-typed program can implement the wrong machine perfectly.
Try it
FillPercent is a REAL. FilledCount and BatchTarget are integers. A learner writes a percentage calculation without checking BatchTarget, and places it inside a loop that waits until FilledCount reaches the target.
Describe three problems, then write an excerpt that calculates a display percentage once per invocation. Use explicit conversion and a validity indication. Do not change the production count.
Work through the answer
The denominator can be zero or otherwise invalid. Integer arithmetic can lose the fractional part before assignment to REAL. Waiting for future cartons inside a loop prevents ordinary cyclic progress.
PercentValid := BatchTarget > 0;
IF PercentValid THEN
FillPercent := 100.0 * DINT_TO_REAL(FilledCount)
/ DINT_TO_REAL(BatchTarget);
ELSE
FillPercent := 0.0;
END_IF;
This excerpt assumes both count variables are DINT. The display must use PercentValid so zero is not mistaken for a valid measurement. Decide separately whether values above 100 percent should be displayed, capped, or diagnosed; hiding them can conceal a count problem. Measuring time uses the same discipline: explicit units, persistent state, and no waiting loops.