Every variable has a size
In the last chapter ScanCount was a DINT and Switch was a BOOL, and you took both on trust. Now look at what those few letters decide. A variable in Structured Text is a named piece of controller memory with a fixed size and a fixed meaning. The type you write after the colon tells the compiler how many bits to reserve, which values fit in them, and which operations make sense. A BOOL holds one decision. An INT holds a whole number between −32,768 and 32,767 in 16 bits. A REAL holds a measurement with a decimal point. None of them grows when a value gets too big, and none of them warns you at runtime when it does.
In a scripting language a number simply gets larger. In a PLC the memory map is fixed when you download the program, so you choose the size up front. Choose badly and the program still builds and still runs, and then produces a wrong number on the day the plant gets busy.
Declarations live in VAR … END_VAR blocks, one variable per line, in the form Name : TYPE := initial value;. The initial value is optional. Several names of the same type can share a line, separated by commas:
PROGRAM Main
VAR
Running : BOOL; (* input: line is running *)
FillValve : BOOL; (* output: filling valve solenoid *)
BottlesToday : DINT := 41250; (* a count that passes 32,767 by lunchtime *)
BottleSize_l : REAL := 0.5; (* litres per bottle *)
Volume_l : REAL; (* litres filled this shift *)
FillTime : TIME := T#2s500ms; (* valve open time per bottle *)
Recipe : STRING[20] := 'Still 0.5 l';
LowLevel, HighLevel : BOOL; (* two tank switches, one line *)
END_VAR
FillValve := Running AND NOT HighLevel;
Volume_l := DINT_TO_REAL(BottlesToday) * BottleSize_l;
END_PROGRAMEach type here was picked for its signal: the valve is on or off, the bottle count is a whole number that gets large, the bottle size is a measurement, the fill time is a duration, the recipe is text for the operator panel. The rest of this chapter is about making those choices on purpose.
BOOL and the integer family
BOOL is the most common type in any machine program: every push button, limit switch, contactor coil and pilot lamp is a BOOL, either TRUE or FALSE.
Whole numbers come in a family. The letter in front says how big, and a U says unsigned (no negative values):
| Type | Bits | Range | Typical use |
|---|---|---|---|
SINT | 8 | −128 … 127 | rarely; small signed offsets |
USINT | 8 | 0 … 255 | a byte from a serial device |
INT | 16 | −32,768 … 32,767 | analog raw values, small setpoints |
UINT | 16 | 0 … 65,535 | Modbus registers, recipe numbers |
DINT | 32 | −2,147,483,648 … 2,147,483,647 | counts, totals, positions in µm |
UDINT | 32 | 0 … 4,294,967,295 | large unsigned counters |
LINT | 64 | about ±9.2 × 10¹⁸ | energy totals, nanosecond timestamps |
The prefixes come from the standard: S for short, D for double, L for long. ULINT completes the set at 64 bits unsigned.
Two vendor facts are worth knowing before you choose. On a Siemens S7-1500, analog input modules deliver their nominal range as an INT from 0 to 27,648, so raw analog values are INT by convention in TIA Portal. On a Rockwell ControlLogix or CompactLogix, the native type is DINT: the controller converts SINT and INT to DINT before doing arithmetic on them, so using INT to "save memory" actually costs execution time. CODESYS and TwinCAT run on 32- or 64-bit processors and handle all of these at full speed.
What a declaration reserves in memory
A declaration is a request for memory, and how the compiler lays that memory out differs by platform. You rarely see it, but you will the day another device reads your data by address.
Siemens has two kinds of data block. In a standard (non-optimised) block, the one S7-300 and S7-400 programmers know, every variable has a fixed byte offset you can see in the block editor, and the rules are strict: BOOLs pack eight to a byte, and anything of 16 bits or more starts on an even byte. Declare four variables in this order:
(* Static listing: a standard (non-optimised) S7 data block *)
PumpRunning : BOOL; (* offset 0.0 *)
PumpFault : BOOL; (* offset 0.1 *)
Speed_rpm : INT; (* offset 2.0: byte 1 is left empty for alignment *)
Hours : DINT; (* offset 4.0 *)
Flow_m3h : REAL; (* offset 8.0 *)
The block is 12 bytes long, and byte 1 is padding. An HMI or a partner PLC that reads this block by address over S7 communication needs exactly these offsets. Insert a new BOOL between PumpFault and Speed_rpm and nothing moves, because it lands in bit 0.2; insert an INT there and every later offset moves by two bytes, and every device that reads by address now reads the wrong values. That is a real commissioning failure, and the symptom is a panel full of numbers that are plausible but wrong.
Optimised blocks, the default for new S7-1200 and S7-1500 projects, have no visible offsets. The CPU arranges the data itself and the programming tools and HMIs access it by name. You cannot use absolute addressing into them, and you do not need to.
Other platforms make other choices. CODESYS and TwinCAT give each BOOL a whole byte, so an ARRAY[0..15] OF BOOL takes 16 bytes; inside a structure you can use the CODESYS-specific type BIT, which packs to a single bit. Studio 5000 allocates every standalone tag on a 32-bit boundary, so a standalone SINT, INT or BOOL tag still uses 4 bytes, while BOOL members inside a user-defined type are packed together. None of this changes how your ST behaves. It changes how much memory a big array costs, and it decides whether a block can be read by address. Structures and their layout come back in chapter 15.
Two's complement: why the wrap goes negative
Signed integers on every PLC are stored in two's complement. The top bit is the sign, and the pattern for a negative number is the one you get by counting backwards from zero. In 16 bits:
Value as INT | Bit pattern | Hex | Same bits read as UINT |
|---|---|---|---|
| 0 | 0000 0000 0000 0000 | 16#0000 | 0 |
| 1 | 0000 0000 0000 0001 | 16#0001 | 1 |
| 32,767 | 0111 1111 1111 1111 | 16#7FFF | 32,767 |
| −32,768 | 1000 0000 0000 0000 | 16#8000 | 32,768 |
| −2 | 1111 1111 1111 1110 | 16#FFFE | 65,534 |
| −1 | 1111 1111 1111 1111 | 16#FFFF | 65,535 |
Read the table from 32,767 down to −32,768. Adding one to 16#7FFF gives 16#8000, which is exactly what an adder circuit does with those bits. The hardware has no idea the result "should" have been 32,768; it produced the next bit pattern, and that pattern means −32,768 to anything that treats it as signed. This is why overflow wraps instead of stopping.
The right-hand column shows the other consequence. The same sixteen bits mean different numbers depending on the type that reads them. −1 and 65,535 are the same word:
PROGRAM Main
VAR
Signed : INT := -1; (* input: try -1, -2, 32767 and -32768 *)
AsUINT : UINT; (* the same 16 bits, read as unsigned *)
AsWORD : WORD; (* the same 16 bits, as a bit string *)
Back : INT; (* and converted back again *)
END_VAR
AsUINT := INT_TO_UINT(Signed);
AsWORD := INT_TO_WORD(Signed);
Back := UINT_TO_INT(AsUINT);
END_PROGRAMType -1 into Signed and AsUINT reads 65,535. Type -50 and it reads 65,486. Nothing has been lost: Back turns the bits into the original value every time. The conversion functions between same-size integer types move the bits and change only how they are read.
That shows up constantly on fieldbuses. A Modbus holding register is 16 bits with no sign information at all. A temperature transmitter's gateway sends −5.0 °C as the signed integer −50, the bits travel as 16#FFCE, and a PLC or SCADA tag declared as unsigned shows 65,486, which a scaling formula then turns into 6,548.6 °C. Nobody's code is wrong in the usual sense; the two ends disagree about the type. The device manual says whether a register is signed. Declare your tag to match it, and test with a negative value before you sign off the interface. The challenge at the end of this chapter is exactly this bug.
Overflow wraps, it does not stop
When an integer is already at its maximum and you add one, it does not stop at the maximum and it does not raise an alarm. The bit pattern rolls over and the value jumps to the most negative number the type can hold. Step this program one scan at a time and watch the two counters part ways:
PROGRAM Main
VAR
CountINT : INT := 32760; (* 16 bits *)
CountDINT : DINT := 32760; (* 32 bits, same starting value *)
END_VAR
CountINT := CountINT + 1;
CountDINT := CountDINT + 1;
END_PROGRAMOn the eighth scan CountINT goes from 32,767 to −32,768. CountDINT carries on to 32,768 without noticing anything. A real controller behaves the same way. Siemens, CODESYS and TwinCAT wrap silently. Rockwell wraps too, and logs the overflow as a minor fault (type 4, code 4) that the controller runs straight through; Rockwell's documentation adds that expressions, which is what ST arithmetic is, do not set the math status flags such as the overflow bit S:V. Hardly anyone looks at minor faults on a running line. Treat wrapping as something your choice of type must prevent; the controller will not catch it for you.
This bug has a typical shape. A part counter declared as INT works through commissioning, where the line makes a few hundred parts. Months later a sorting conveyor running at 10 parts a second passes 32,767 in under an hour, the counter goes negative, and a downstream comparison like "batch complete when count ≥ 500" stays false forever.
The arithmetic that prevents it is a single division: the type's maximum over the worst-case rate. For a line counting 10 parts a second (600 a minute), around the clock:
| Type | Largest value | Time to overflow at 10 counts/s |
|---|---|---|
INT | 32,767 | 55 minutes |
UINT | 65,535 | 1 hour 49 minutes |
DINT | 2,147,483,647 | about 6.8 years |
UDINT | 4,294,967,295 | about 13.6 years |
LINT | about 9.2 × 10¹⁸ | about 29 billion years |
Use the worst-case rate, not the nominal one. A counter that counts encoder pulses rather than parts can run at thousands of counts a second, and then a DINT lasts days, not years. Seven years sounds like forever until you remember that many machines run for twenty-five. If a counter must never wrap, either use LINT, or decide what happens at the top and write it (a counter that resets at the end of each shift, after its value has been logged, never gets near the limit). Chapter 11 handles counters that are meant to stop at a limit.
Unsigned types wrap at the bottom as well as at the top. 0 - 1 in a UINT is 65,535. That catches people who use unsigned types for "values that can't be negative", like a remaining quantity:
REAL and LREAL: measurements, not counts
REAL is a 32-bit floating-point number, the type for anything you measure: temperatures, pressures, flows, speeds, levels, weights. It handles decimals and a huge range (up to about 3.4 × 10³⁸), but only about 7 significant digits. LREAL is the 64-bit version with about 15 to 16 digits. Siemens S7-1500, CODESYS and TwinCAT support both; on Logix, LREAL exists only on the newer 5380/5580 controllers and with a restricted instruction set, so REAL is the everyday choice there.
Seven digits is plenty for a tank level of 1,234.567 litres. It is not enough for a totaliser. A REAL can represent every whole number up to 16,777,216 exactly, and above that the gaps between representable values become larger than 1. A flow totaliser in litres that adds 1.0 per pulse will stop increasing at about 16.8 million litres, because 16777216.0 + 1.0 rounds back to 16777216.0. Nothing faults. The number just stops moving, which is worse than a fault.
(* Static listing: a 32-bit REAL totaliser that silently stalls *)
Total_l := Total_l + 1.0; (* stuck at 16777216.0 once it gets there *)
The fix is to count in an integer type (DINT litres, or LINT millilitres) and convert to REAL only for display, or to use LREAL. The same 7-digit limit is why you never compare two REAL values with =; chapter 3 shows that bug in action.
Inside a 32-bit REAL
Current PLCs store REAL in the IEEE 754 single-precision format. The 32 bits are split three ways: 1 sign bit, 8 bits of exponent and 23 bits of fraction. The value is the fraction, with an implied leading 1, multiplied by a power of two. LREAL uses the double-precision format: 1, 11 and 52 bits.
The consequence for you is that a REAL's precision is relative. The 23 fraction bits divide each power-of-two interval into 8,388,608 equal steps, so the step between neighbouring values doubles every time the value doubles:
| Value around | Gap to the next REAL |
|---|---|
| 1.0 | 0.00000012 |
| 1,000 | 0.000061 |
| 100,000 | 0.0078 |
| 1,000,000 | 0.0625 |
| 16,777,216 | 2.0 |
| 100,000,000 | 8.0 |
Read that table against your own signals. A temperature of 850.0 °C is held to about a ten-thousandth of a degree, far finer than any transmitter. A linear axis position held in micrometres as a REAL is a different story: at 20 m of travel the value is 20,000,000 µm, the gap is 2 µm, and a move of 1 µm cannot be represented at all. That is why motion systems keep positions in LREAL or in integer increments.
One caution for the live examples on these pages: the engine computes REAL with 64 bits internally, so it does not show you the 32-bit stall. Its LREAL behaves exactly like a PLC's. Chapter 28 uses it to show where even 64 bits run out.
TIME, DATE and TIME_OF_DAY
Durations have their own type. A TIME literal is written with T# and units from days down to milliseconds: T#250ms, T#2s500ms, T#1m30s, T#8h. You can add and subtract durations and compare them, and every timer in this course takes its preset as a TIME. Using TIME instead of an INT called Delay removes the classic question "is that milliseconds or seconds?" from your program for good.
PROGRAM Main
VAR
CycleTime : TIME := T#1m30s; (* one mixer batch *)
FillTime : TIME := T#45s;
MixTime : TIME := T#20s;
DrainTime : TIME; (* whatever is left of the cycle *)
CycleTime_ms : DINT;
ShiftStart : TIME_OF_DAY := TOD#06:00:00;
BatchDate : DATE := D#2026-10-23;
END_VAR
DrainTime := CycleTime - FillTime - MixTime;
CycleTime_ms := TIME_TO_DINT(CycleTime);
END_PROGRAMChange FillTime in the editor to T#1m and the subtraction asks for a negative duration. Controllers disagree about what you get: a Siemens CPU gives a negative TIME, CODESYS wraps to a huge positive value because its TIME is unsigned, and the engine on this page stops at T#0s (see chapter 16). None of those is a usable drain time. It is a recipe error, so compare FillTime + MixTime with CycleTime before you subtract, and refuse the recipe when it does not fit.
Under the hood a TIME is an integer count of milliseconds, and that is where the vendors differ. On Siemens it is a signed 32-bit value, so the largest duration is about 24 days 20 hours. In CODESYS and TwinCAT it is unsigned 32-bit, about 49 days 17 hours. Both offer LTIME, a 64-bit count of nanoseconds, for anything longer or finer. Rockwell Logix has no TIME type at all: a Logix TIMER has .PRE and .ACC members that are plain DINT milliseconds, which is worth remembering when you move code between platforms.
Calendar values are separate types: DATE (D#2026-10-23), TIME_OF_DAY or TOD (TOD#06:00:00) and DATE_AND_TIME or DT (DT#2026-10-23-06:00:00). You will use them for shift reports, batch records and timestamps on alarms, and almost never for control. The controller's clock drifts, gets corrected by NTP and jumps at daylight-saving changes; a delay that must be 20 seconds belongs in a timer, never in a subtraction of two clock readings.
STRING: text for people
A STRING holds text: recipe names, operator messages, barcode contents. The length is fixed at declaration. STRING[20] reserves room for 20 characters; plain STRING gets a vendor default: 80 characters in CODESYS and TwinCAT, 254 on Siemens. Rockwell's STRING is a predefined structure with a .LEN member and an 82-character .DATA array. Assign something longer than the declared length and the end is cut off without a warning: 'CONVEYOR 12' stored in a STRING[8] reads 'CONVEYOR'.
The declared length is not the whole cost. A Siemens String[20] occupies 22 bytes, because two header bytes hold the maximum and the current length. CODESYS stores a terminating zero, so a STRING[20] takes 21 bytes. An array of 500 alarm texts declared as plain STRING on Siemens costs 500 × 256 bytes, about 128 KB, which is a noticeable part of the work memory on a small CPU. Declare the length you need.
Text is for operators and logs. Decisions should be made on numbers, booleans and enumerations, which compare in one instruction and cannot be misspelled. A program that runs the "dark roast" sequence when Recipe = 'Dark Roast' will one day meet 'Dark roast' from a new HMI screen. String functions and time arithmetic get their own chapter 16.
BYTE, WORD and DWORD: bits, not numbers
BYTE (8 bits), WORD (16 bits), DWORD (32 bits) and LWORD (64 bits) are bit strings. They have the same sizes as USINT, UINT, UDINT and ULINT, but they mean something different: a collection of individual on/off flags packed together, rather than a quantity. You meet them wherever a device sends status as a packed word. A SINAMICS G120 drive on PROFINET, for example, reports its state in status word 1 (ZSW1, per the PROFIdrive profile): bit 0 "ready to switch on", bit 1 "ready to operate", bit 2 "operation enabled", bit 3 "fault present", bit 7 "warning present", and so on. Hexadecimal literals such as 16#0437 make the bit pattern readable: every hex digit is exactly four bits.
You read a single bit with a dot and the bit number, counting from zero:
PROGRAM Main
VAR
ZSW1 : WORD := 16#0437; (* input: drive status word 1 *)
ReadyToSwitch : BOOL; (* bit 0 *)
ReadyToRun : BOOL; (* bit 1 *)
Running : BOOL; (* bit 2 *)
DriveFault : BOOL; (* bit 3 *)
DriveWarning : BOOL; (* bit 7 *)
END_VAR
ReadyToSwitch := ZSW1.0;
ReadyToRun := ZSW1.1;
Running := ZSW1.2;
DriveFault := ZSW1.3;
DriveWarning := ZSW1.7;
END_PROGRAMType 8 into ZSW1 (only bit 3 set) and only DriveFault lights. Type 183 (16#00B7) and you have a running drive with a warning. CODESYS, TwinCAT and Logix accept the .n syntax shown here; in TIA Portal SCL the same bit is written ZSW1.%X3. The idea is identical.
The rule of thumb: if you would ever add two of these values, it is a number and belongs in an integer type; if you only test or set individual bits, it is a bit string. Masking several bits at once with AND comes in chapter 4, and chapter 25 covers shifts, rotations and the full PROFIdrive words.
Logix is the odd one out again. Studio 5000 has no BYTE, WORD or DWORD; you hold packed flags in a SINT, INT or DINT and address the bits the same way, StatusWord.3. Code imported from another platform needs its bit-string types replaced with the signed integer of the same size, and any comparison against a value above the signed maximum (16#8000 and up, for an INT) has to be rewritten, because the same bits now read as a negative number.
Writing values: literals
A literal is a value written directly in the code. The standard gives you more ways to write them than most programs use, and the right one makes a value easier to check:
| You write | It means | Use it for |
|---|---|---|
1500, -40 | a decimal integer | counts, setpoints |
1_000_000 | the same as 1000000; underscores are ignored | long numbers you need to read at a glance |
16#00FF | hexadecimal | masks, status words, register values from a manual |
2#0000_0101 | binary | bit patterns where each position matters |
8#17 | octal (15) | almost never in new code |
3.14159, 1.5E3 | a real number, with optional exponent | measurements, factors |
INT#100, REAL#1 | a typed literal | when the type of a constant must be explicit |
TRUE, FALSE | the two BOOL values | everything boolean |
T#1m30s, TOD#06:00:00 | durations and times of day | timers, shift boundaries |
'Line 3' | a STRING | messages; $' writes a quote and $N a new line inside it |
Two habits help. Write masks and bit patterns in hex or binary, never in decimal: 16#0088 says "bits 3 and 7" to anyone who reads hex, while 136 says nothing. And write real literals with a decimal point (2.0, 10.0) whenever the calculation is meant to be in REAL, so the reader and the compiler both see it. The next chapter shows what happens when an integer literal turns a REAL division into an integer one.
The generic types
The IEC tables of standard functions use names you cannot declare a variable with: ANY_INT, ANY_REAL, ANY_NUM, ANY_BIT, ANY_STRING, ANY_DATE. They are generic types, families that let the standard say "ABS accepts any number" once rather than eleven times. ANY_INT covers every signed and unsigned integer, ANY_REAL covers REAL and LREAL, ANY_NUM is both together, and ANY_BIT is BOOL, BYTE, WORD, DWORD and LWORD. When a manual says an input is ANY_NUM, it means you can wire any numeric type to it. Siemens extends the idea with parameter types of its own, such as Variant, covered in chapter 27.
The same types under vendor names
The IEC names are a good common language, but each editor spells them its own way and supports a different subset. When you read a vendor manual or port code, this is the table to keep open:
| IEC 61131-3 | TIA Portal (S7-1200/1500) | Studio 5000 (Logix) | CODESYS / TwinCAT 3 |
|---|---|---|---|
BOOL | Bool | BOOL | BOOL (1 byte); also BIT in structures |
SINT, INT, DINT | SInt, Int, DInt | SINT, INT, DINT | same as IEC |
LINT | LInt (S7-1500 only) | LINT, with limited instruction support on many controllers | LINT |
USINT … ULINT | USInt, UInt, UDInt, ULInt (64-bit on S7-1500 only) | traditionally none; check your firmware | same as IEC |
REAL, LREAL | Real, LReal | REAL; LREAL on 5380/5580 | same as IEC |
BYTE … LWORD | Byte, Word, DWord; LWord on S7-1500 only | none: use integers | same as IEC |
TIME, LTIME | Time (signed 32-bit ms), LTime | none: timers use DINT ms | TIME (unsigned 32-bit ms), LTIME |
STRING | String, 254 characters by default | STRING, 82 characters | STRING, 80 characters by default |
DATE_AND_TIME | Date_And_Time, plus DTL | none: clock values via GSV | DATE_AND_TIME / DT |
TIA Portal's editors accept the IEC spelling and display Siemens' mixed case, so DINT and DInt are the same type there. Logix expresses dates and times as structures and integers rather than types; chapter 28 covers that. The 64-bit integer types are the ones most likely to stop a port: an S7-1200 has no LInt, ULInt or LWord at all.
Initial values, constants and retained memory
Every variable has a value from the moment the program starts. If you write := 1250.0, that is the start value; if you write nothing, numbers start at 0, BOOL at FALSE, TIME at T#0s and strings empty. As you saw in chapter 1, the initial value is applied once at start-up, not every scan.
Values that must never change at runtime go in a VAR CONSTANT block. The compiler then refuses any assignment to them, which turns a whole class of "someone overwrote the setpoint" bugs into build errors. Constants also give magic numbers a name: BottlesPerCase explains itself, 12 in the middle of a formula does not.
Values that must survive a power failure go in VAR RETAIN. A retained variable is stored in memory that keeps its content when the controller loses power: battery-backed RAM on older hardware, flash or capacitor-buffered memory on current ones.
PROGRAM Main
VAR CONSTANT
BottlesPerCase : DINT := 12;
END_VAR
VAR RETAIN
LifetimeBottles : DINT := 0; (* survives power cycles: service interval counter *)
END_VAR
VAR
Running : BOOL; (* input: filler running, one bottle per scan here *)
CasesFull : DINT;
END_VAR
LifetimeBottles := LifetimeBottles + BOOL_TO_DINT(Running);
CasesFull := LifetimeBottles / BottlesPerCase;
END_PROGRAMSwitch Running on and the lifetime count climbs. Try adding BottlesPerCase := 6; to the body: the editor refuses to build, which is exactly what a constant is for. (BOOL_TO_DINT turns TRUE into 1 and FALSE into 0. Conversions are the next chapter's subject; here it just lets the counter add one per scan while the filler runs.)
What survives depends on how the controller restarts, and IEC 61131-3 defines two cases:
| Event | Normal VAR | VAR RETAIN |
|---|---|---|
| Warm restart (power returns, or a warm reset) | back to initial value | keeps its value |
| Cold restart (cold reset, memory reset) | back to initial value | back to initial value |
The vendors map onto that table differently. In CODESYS and TwinCAT, a warm reset or power cycle keeps RETAIN variables, while a cold reset, a reset to origin or a full download reinitialises them; VAR PERSISTENT goes one step further and survives a download as well. On a PC-based TwinCAT controller, persistent data is written to disk at shutdown, which is why those cabinets have a UPS. In TIA Portal you tick "Retain" per variable or per data block, and an S7-1500 only performs warm restarts. Rockwell Logix keeps every tag through a power cycle with no keyword at all; values only revert when you download the project.
Retentive memory is also finite, and smaller than work memory. Each S7-1500 CPU has a fixed amount of retentive memory listed in its data sheet, and TIA Portal shows how much of it a project uses; a project that ticks "Retain" on whole recipe arrays can run out of it. On a CODESYS controller the retain area is likewise a fixed size set by the device. Retain values, not tables of history.
Declaration sections and direct addresses
VAR and VAR CONSTANT are two of several declaration sections. You will meet the others as the course goes on; it helps to see the whole set once, because the section a variable sits in says who may read and write it:
| Section | What it holds | Where it is covered |
|---|---|---|
VAR | internal variables of this program or block; keep their value between scans | from here on |
VAR CONSTANT | named values the compiler protects | this chapter |
VAR RETAIN, VAR PERSISTENT | values that survive restarts (and, for persistent, downloads) | this chapter, chapter 26 |
VAR_INPUT, VAR_OUTPUT | parameters of a function or function block | chapter 17, chapter 18 |
VAR_IN_OUT | a parameter passed by reference | chapter 18 |
VAR_TEMP | scratch values that do not survive the call | chapter 26 |
VAR_GLOBAL, VAR_EXTERNAL | data shared between programs | chapter 26 |
The standard also lets a declaration name a physical address directly, with AT and a direct representation: %I for inputs, %Q for outputs, %M for memory, then a size letter (X for a bit, B byte, W word, D double word, L long word) and the address:
(* Static listing: IEC direct addresses in a declaration *)
VAR
StartPB AT %IX0.0 : BOOL; (* input byte 0, bit 0 *)
MotorRun AT %QX0.1 : BOOL; (* output byte 0, bit 1 *)
Level_raw AT %IW64 : INT; (* an analog input word *)
END_VAR
Siemens writes the same addresses without the X (%I0.0, %Q0.1, %IW64) and keeps them in the PLC tag table instead of in the code. Logix has no direct addresses at all; I/O appears as module-defined tags such as Local:1:I.Data, and you give them readable names with alias tags. In CODESYS and TwinCAT you can write AT %IX0.0, but most projects use AT %I* and link in the I/O configuration, as chapter 1 showed. Whatever the platform, keep the addresses in one place (the declarations, the tag table, the alias list) and never scatter raw addresses through the logic. The day a card is moved to another slot, you want to change one line.
Naming with units and choosing the type
A name should tell the reader what the signal is, and for anything physical, in which unit. Level invites mistakes; TankLevel_mm or TankLevel_pct does not. Put the unit at the end in a consistent style: _mm, _bar, _degC, _rpm, _l, _kW. A TIME carries its own unit, so MixTime is enough. Booleans read best as a statement that is true when the variable is TRUE: DoorClosed, PumpRunning, HighLevel. A boolean called Door or PumpStatus leaves the reader guessing which state TRUE means.
Choosing the type is choosing what the signal is:
| Signal | Type | Why |
|---|---|---|
| Push button, limit switch, coil | BOOL | two states |
| Analog raw value from an S7 module | INT | the module delivers 0 … 27,648 |
| Temperature, pressure, flow in units | REAL | measurement with decimals |
| Parts counted, cycles, totals | DINT | whole numbers that grow |
| Energy or flow totaliser | DINT/LINT, or LREAL | beyond REAL's 7 digits |
| Delay, cycle time, timeout | TIME | unit is part of the type |
| Drive or module status word | WORD/DWORD | packed flags, not a quantity |
| Recipe name, operator message | STRING[n] | text for people |
Two questions settle most choices: what is the largest value this can reach on the worst day of the year, and is it a quantity, a set of flags or a duration?
When you review someone else's declarations, and every program deserves one review before it goes to site, read them as a list of promises and check each one. Is every counter at least a DINT, or has someone worked out why a smaller type is safe? Is every REAL a measurement, and is anything that accumulates held somewhere with more digits? Does every register from another device have the signedness its manual gives? Does every BOOL name say which state is TRUE? Is anything in RETAIN that would restart the machine on its own? Ten minutes on the declarations finds bugs that a day of testing at commissioning rates will not, because the test never runs long enough to overflow.
What to take into the next chapter
A type is a promise about size and meaning. Integers are stored in two's complement and wrap silently at both ends of their range, so pick the size from the worst-case value and rate, and default to DINT for anything that counts. The same bits read as signed or unsigned give different numbers, so match every register to its device's manual. REAL is for measurements, its precision shrinks as values grow, and it runs out of digits at about seven; totals belong in integers or LREAL. TIME carries its unit, WORD and DWORD carry flags, STRING carries text for people, and the vendors spell and size all of these a little differently. Constants make the compiler guard your fixed values, and RETAIN keeps what the plant cannot rebuild after a power cut. It must never keep the commands that would restart a machine by themselves.
You have already seen hints of the next problem: INT_TO_DINT, DINT_TO_REAL, BOOL_TO_DINT, UINT_TO_INT. As soon as an expression mixes types, someone has to decide what the result is. The next chapter covers arithmetic, precedence and conversion, and the integer division that turns 37 out of 50 into zero percent.