04 — VAR SECTIONS
The contract — who may write, who may read, and what survives the scan.
Estimated 24 minutes.
The arguments happen upstairs, in the VAR block
Open a function block that has been in service ten years and the body is usually fine. The trouble is in the declaration: a setpoint somebody made global so the HMI could reach it, a counter in VAR_TEMP that never counts, a 240-byte recipe struct copied in on every call of a 1 ms task. Each section keyword is a promise about who may write it, who may read it, and whether it is still there next scan.
Route seven tags into the right sections
These are real tags off a bag filler. Put each one where it belongs — and put a few in the wrong place on purpose, because the bench will tell you exactly what that costs at runtime. Nothing resets, so move things around as much as you like.
The same block, both languages
Ladder shows you the pins; ST shows you the sections behind them. VAR and VAR_TEMP have no pin at all, because nobody outside is allowed to reach them.
VAR_INPUT is a photocopy; VAR_OUTPUT is a nameplate
At the top of every call the runtime copies the caller's values into the instance's input members, runs the body, and leaves the outputs sitting in the instance for anyone to read as Instance.Name. That copy is why an input you stop passing simply freezes at its last value, and why anything the block writes into its own input is gone before the next scan reads it.
The write that does not stick
Inside FB_Filler you write Setpoint := 0.0; on a fault. The caller keeps calling Filler1(Setpoint := HMI_Grams) every scan. What does Setpoint hold when your code runs next scan?
- 0.0 — the block's write is the last one, so it wins
- HMI_Grams — the copy-in at the top of the call overwrites it
- It alternates between the two on each scan
- The value is undefined, because two writers touched it
VAR_IN_OUT hands over the key, not a copy
An in-out parameter is a pointer under the hood. The caller supplies a real, writable variable and the block edits it in place — no copy in, no copy out. It is the only way to give a block a 2 kB recipe struct or an array of 500 setpoints without paying for the copy every scan, and the only section where the block can change something the caller can see before the call has even returned.
Watch one keyword decide who owns the counter
Two tally blocks with identical bodies. One takes Total as VAR_IN_OUT, the other as VAR_INPUT. This is the real ST engine — press Run, then scrub back and watch ByVal.Total climb to 1 and get wiped on every single carton.
The reason in-out exists: big things
A struct, an array, a STRING — anything you would rather not copy twice per call. The price: the caller must supply a variable, never a literal, and must supply it on every call.
The same struct, two tasks
FB_Filler takes Recipe as VAR_IN_OUT. Somebody calls Filler1(Recipe := Batch_Recipe) in the 10 ms main task and Filler2(Recipe := Batch_Recipe) in a 1 ms fast task. What have they built?
- Two independent copies of the recipe, one per instance
- Two blocks writing the same memory from two tasks, with nothing between them
- A compile error — an in-out may be passed to only one instance
- A read-only share; only the first caller may write
VAR remembers; VAR_TEMP forgets, on purpose
Plain VAR is instance memory: private, invisible from outside, and still holding last scan's value when the block is called again. VAR_TEMP is scratch — initialised at the top of every invocation and gone at the bottom. Use VAR_TEMP for arithmetic you compute and consume in the same pass; never for anything that has to compare "now" against "last time".
One keyword, one very different counter
Same eleven lines. The one on the right counts continuously while a carton sits in front of the eye, and the call-out is logged as "counter reading high" rather than as what it is.
What the plant actually sees
You move Prev from VAR to VAR_TEMP in that rising-edge detector and download it. A carton stops on the conveyor with the beam blocked for four seconds. What does Count do?
- Nothing changes — VAR_TEMP survives the scan too
- It counts once, as intended
- It counts on every scan the beam is blocked
- It never counts at all
VAR_STAT: one variable behind every instance
A VAR_STAT member is allocated once for the whole block type, so all twelve fillers share the same one. That is right for a handful of things — a calibration table too big to duplicate, a "how many of me exist" counter, a licence check — and wrong for anything a machine owns, because two instances writing it will trample each other silently. TwinCAT and CODESYS support it; Siemens has no equivalent and expects a global DB instead.
The memory picture
Every instance owns its own copy of every VAR_INPUT, VAR_OUTPUT and VAR. Only VAR_STAT and globals are shared — which is exactly why they are the two places a bug can travel between machines.
Globals, externals, and the things that never change
A VAR_GLOBAL lives in a Global Variable List and belongs to the project, not to any block. A block that needs one declares VAR_EXTERNAL with the identical name and type, which puts the dependency in the declaration where you can see it instead of on line 400 of the body. CONSTANT is the opposite move: a value fixed at compile time that no code anywhere may assign — the right home for a motor nameplate speed, a vessel volume, or an array bound.
Throw the disconnect, then download
A browser tab has no NVRAM and no download step, so this bench is a faithful model of what controller firmware does rather than a live run on the engine. The rules it applies are the CODESYS / IEC ones you would meet on a TwinCAT, WAGO or Schneider controller. Run a shift first, then take the controller away from itself four different ways.
A retentive counter, both ways
A counter instance can be retentive as a whole: declare Shift_CTU itself under VAR RETAIN and its CV comes back after the power cycle along with everything else in it.
AT: nailing a symbol to a terminal
AT binds a variable to a physical address — Start_PB AT %IX2.0 is the same bit the DI module writes, under a name a human can read. Do it once, in one GVL, next to the wiring drawing. Do it inside a reusable function block and you have welded that block to one station, which is the exact opposite of what the block was for. CODESYS allows AT %I* inside an FB for auto-mapping; treat that as a special case, not a habit.
Saturday morning download
A machine keeps its shift tally in VAR RETAIN and its total running hours in VAR PERSISTENT. On Saturday you download a modified program. What is on the HMI when the line starts Monday?
- Both values intact
- Shift tally intact, running hours cleared
- Shift tally cleared, running hours intact
- Both cleared
What you learned
- VAR_INPUT is copied in and owned by the caller; VAR_OUTPUT is written by the block and readable by everyone as Instance.Name.
- VAR_IN_OUT passes a reference — no copy, the caller's own variable, and the aliasing problem that comes with it.
- VAR is private and survives the scan; VAR_TEMP is initialised at every invocation, which is why an edge detector must never keep its history there.
- VAR_STAT is one variable behind every instance of a type: right for a shared table, wrong for anything a machine owns.
- Every instance owns its own copy of every input, output and VAR. Only VAR_STAT and globals are shared.
- RETAIN survives a power cycle but not a download; PERSISTENT survives both, and only reset origin clears it.
- CONSTANT fixes a nameplate figure at compile time, and AT binds a symbol to a terminal — in one GVL, never inside a reusable block.
VAR SECTIONS — done
- You can put any tag on a machine into the right declaration section and say what would break in the wrong one.
- You watched a VAR_IN_OUT block write into the caller's own counter on the real engine while a VAR_INPUT block could not.
- You can answer the retain-versus-persistent question that decides whether Monday's production report is right.