Chapter 2 / 32 · Part 1 · First contact

Variables and data types

Declare BOOL, INT, DINT, REAL, TIME and STRING variables, give them initial values and choose the type that fits the signal.

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_PROGRAM
Live example — open this page with JavaScript to run and edit it. Inputs you can drive: Running.

Each 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):

TypeBitsRangeTypical use
SINT8−128 … 127rarely; small signed offsets
USINT80 … 255a byte from a serial device
INT16−32,768 … 32,767analog raw values, small setpoints
UINT160 … 65,535Modbus registers, recipe numbers
DINT32−2,147,483,648 … 2,147,483,647counts, totals, positions in µm
UDINT320 … 4,294,967,295large unsigned counters
LINT64about ±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 INTBit patternHexSame bits read as UINT
00000 0000 0000 000016#00000
10000 0000 0000 000116#00011
32,7670111 1111 1111 111116#7FFF32,767
−32,7681000 0000 0000 000016#800032,768
−21111 1111 1111 111016#FFFE65,534
−11111 1111 1111 111116#FFFF65,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_PROGRAM
Live example — open this page with JavaScript to run and edit it. Inputs you can drive: Signed.

Type -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_PROGRAM
Live example — open this page with JavaScript to run and edit it.

On 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:

TypeLargest valueTime to overflow at 10 counts/s
INT32,76755 minutes
UINT65,5351 hour 49 minutes
DINT2,147,483,647about 6.8 years
UDINT4,294,967,295about 13.6 years
LINTabout 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 aroundGap to the next REAL
1.00.00000012
1,0000.000061
100,0000.0078
1,000,0000.0625
16,777,2162.0
100,000,0008.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_PROGRAM
Live example — open this page with JavaScript to run and edit it.

Change 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_PROGRAM
Live example — open this page with JavaScript to run and edit it. Inputs you can drive: ZSW1.

Type 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 writeIt meansUse it for
1500, -40a decimal integercounts, setpoints
1_000_000the same as 1000000; underscores are ignoredlong numbers you need to read at a glance
16#00FFhexadecimalmasks, status words, register values from a manual
2#0000_0101binarybit patterns where each position matters
8#17octal (15)almost never in new code
3.14159, 1.5E3a real number, with optional exponentmeasurements, factors
INT#100, REAL#1a typed literalwhen the type of a constant must be explicit
TRUE, FALSEthe two BOOL valueseverything boolean
T#1m30s, TOD#06:00:00durations and times of daytimers, shift boundaries
'Line 3'a STRINGmessages; $' 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-3TIA Portal (S7-1200/1500)Studio 5000 (Logix)CODESYS / TwinCAT 3
BOOLBoolBOOLBOOL (1 byte); also BIT in structures
SINT, INT, DINTSInt, Int, DIntSINT, INT, DINTsame as IEC
LINTLInt (S7-1500 only)LINT, with limited instruction support on many controllersLINT
USINT … ULINTUSInt, UInt, UDInt, ULInt (64-bit on S7-1500 only)traditionally none; check your firmwaresame as IEC
REAL, LREALReal, LRealREAL; LREAL on 5380/5580same as IEC
BYTE … LWORDByte, Word, DWord; LWord on S7-1500 onlynone: use integerssame as IEC
TIME, LTIMETime (signed 32-bit ms), LTimenone: timers use DINT msTIME (unsigned 32-bit ms), LTIME
STRINGString, 254 characters by defaultSTRING, 82 charactersSTRING, 80 characters by default
DATE_AND_TIMEDate_And_Time, plus DTLnone: clock values via GSVDATE_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_PROGRAM
Live example — open this page with JavaScript to run and edit it. Inputs you can drive: Running.

Switch 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:

EventNormal VARVAR RETAIN
Warm restart (power returns, or a warm reset)back to initial valuekeeps its value
Cold restart (cold reset, memory reset)back to initial valueback 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:

SectionWhat it holdsWhere it is covered
VARinternal variables of this program or block; keep their value between scansfrom here on
VAR CONSTANTnamed values the compiler protectsthis chapter
VAR RETAIN, VAR PERSISTENTvalues that survive restarts (and, for persistent, downloads)this chapter, chapter 26
VAR_INPUT, VAR_OUTPUTparameters of a function or function blockchapter 17, chapter 18
VAR_IN_OUTa parameter passed by referencechapter 18
VAR_TEMPscratch values that do not survive the callchapter 26
VAR_GLOBAL, VAR_EXTERNALdata shared between programschapter 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:

SignalTypeWhy
Push button, limit switch, coilBOOLtwo states
Analog raw value from an S7 moduleINTthe module delivers 0 … 27,648
Temperature, pressure, flow in unitsREALmeasurement with decimals
Parts counted, cycles, totalsDINTwhole numbers that grow
Energy or flow totaliserDINT/LINT, or LREALbeyond REAL's 7 digits
Delay, cycle time, timeoutTIMEunit is part of the type
Drive or module status wordWORD/DWORDpacked flags, not a quantity
Recipe name, operator messageSTRING[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.

Practise this chapter