May 11, 2026 · siemenssimatic-axtia-portalplc-programmingindustrial-automationstructured-textiec-61131-3apax
In TIA Portal, variables are spread across several familiar places.
You have PLC tags. You have DB variables. You have UDTs. You have enum types. You have function block inputs, outputs, in-outs, static variables, temp variables, and constants. If you are working close to hardware, you also have direct I/O addresses.
SIMATIC AX does not remove these PLC concepts. It writes them down as Structured Text.
That is the main mental shift for this article. The question is not “where is the tag table?” The better question is:
Where does this variable live, who owns it, and how strongly should its type be defined?
I am still studying AX through demo projects, not claiming customer deployment experience with it. But the TIA Portal side of this mapping is familiar territory. Once I stopped looking for a graphical project tree, the AX variable model started to feel much less foreign.
Correction note. After publication, a reader pointed out that several patterns described below are not AX-specific — TIA Portal SCL uses the same VAR_INPUT / VAR_OUTPUT / VAR text syntax for FB interfaces, accepts inline STRUCT declarations inside VAR sections, and supports typed literals like REAL#1.0 and INT#5. The article has been updated to make that clearer, and to keep the focus on what AX actually adds: namespaces, CONFIGURATION blocks, VAR_EXTERNAL contracts, and namespace-scoped types.
Start With A Function Block Interface
The cleanest place to start is the TempController function block from my demo project.
In TIA Portal, this would be a normal FB interface: measured value in, setpoint in, hysteresis in, enable in, heater output out, fault output out.
In AX, the same interface is text:
NAMESPACE Otomakeit.Demo.TempControl
FUNCTION_BLOCK TempController
VAR_INPUT
i_rActualTemp : REAL; // Measured temperature
i_rSetpoint : REAL; // Desired temperature setpoint
i_rHysteresis : REAL; // Hysteresis band
i_xEnable : BOOL; // Enable controller
END_VAR
VAR_OUTPUT
q_xHeating : BOOL; // Heating output
q_xFault : BOOL; // Fault flag
q_rError : REAL; // Setpoint - actual
END_VAR
VAR
_xHeatingState : BOOL; // Internal latched state
END_VAR
VAR CONSTANT
TEMP_MIN_VALID : REAL := REAL#-50.0;
TEMP_MAX_VALID : REAL := REAL#500.0;
END_VAR
// Logic body runs when the FB instance is called.
END_FUNCTION_BLOCK
END_NAMESPACE
If you write FBs in TIA Portal SCL, this will look familiar. The section keywords — VAR_INPUT, VAR_OUTPUT, VAR, VAR_TEMP, VAR CONSTANT — are the same in TIA SCL and have been since S7-SCL in SIMATIC Manager. What changes in AX is not the FB-interface syntax itself, but what surrounds it: namespaces above the FB, and an explicit CONFIGURATION / PROGRAM block higher up.
For a TIA engineer, the sections are the important part:
VAR_INPUTis the FB input interface.VAR_OUTPUTis the FB output interface.VARis internal static data. It persists as part of the FB instance.VAR CONSTANTis for local constants.
The syntax is different, but the PLC idea is not. The FB still has an interface. It still owns internal memory. Its outputs still belong to the FB instance.
The constants are worth noticing. I do not write TEMP_MIN_VALID := -50.0. I write:
TEMP_MIN_VALID : REAL := REAL#-50.0;
TEMP_MAX_VALID : REAL := REAL#500.0;
That REAL# prefix is a typed literal. It tells the compiler the literal is a REAL, not just “some number that looks like a decimal.” In the unit testing article, the same pattern appeared in test calls:
gTempController(
i_rActualTemp := REAL#94.0,
i_rSetpoint := REAL#100.0,
i_rHysteresis := REAL#5.0,
i_xEnable := TRUE
);
This is not just cosmetic. It makes the code say exactly what the type contract is. When the formal parameter is REAL, the literal is visibly REAL. When a constant expression uses an integer, you can write INT#5. Siemens’ ST documentation also uses examples like REAL#1.0 and INT#5 in constant expressions.
For PLC code, I like that. It reduces the quiet guessing that can creep into numeric logic.
Typed literals are not AX-specific either. They are IEC 61131-3 syntax, and TIA Portal SCL accepts the same REAL#, INT#, TIME#, and related prefixes. What changes in AX is how visibly the codebase leans on them — library APIs tend to require them where TIA SCL would tolerate implicit conversion.
The TIA Portal Mapping
Here is the practical map I use when moving from TIA Portal thinking to AX text:
| TIA Portal idea | AX location or section |
|---|---|
| FB inputs | VAR_INPUT inside FUNCTION_BLOCK |
| FB outputs | VAR_OUTPUT inside FUNCTION_BLOCK |
| FB in-out parameters | VAR_IN_OUT |
| FB static/internal variables | VAR inside the FB |
| Temporary calculation variables | VAR_TEMP |
| FB constants | VAR CONSTANT |
| Global DB variables | VAR_GLOBAL inside CONFIGURATION |
| Reading global variables from a program | VAR_EXTERNAL inside PROGRAM |
| Read-only global constants | VAR_GLOBAL CONSTANT plus VAR_EXTERNAL CONSTANT |
| UDT | TYPE ... STRUCT ... END_STRUCT; END_TYPE |
| PLC enum type | TYPE Name : (Value1, Value2, ...) := Default; END_TYPE |
| Direct I/O address | variable declared with AT %I..., AT %Q..., or similar address area |
This is not the full language reference. It is the first map I would want as a TIA Portal engineer opening an AX project.
Globals Are Not A Free-For-All
In TIA Portal, a global DB is easy to reach from anywhere. That convenience is useful, but it also makes ownership blurry.
AX makes the relationship more explicit. In the TempController demo, configuration.st declares global variables:
USING Otomakeit.Demo.TempControl;
USING Otomakeit.Demo.Safety;
{OpcUa.NodeGenerator.IdType = SymbolName}
CONFIGURATION MyConfiguration
TASK Main(Priority := 1);
PROGRAM P1 WITH Main: MainProgram;
VAR_GLOBAL
gTempController : TempController;
gOverheatProtect : OverheatProtection;
END_VAR
{S7.extern = ReadWrite}
{OpcUa.AccessLevel = ReadWrite}
VAR_GLOBAL
g_rActualTemp : REAL;
g_rSetpoint : REAL;
g_xHeating : BOOL;
g_xFault : BOOL;
END_VAR
END_CONFIGURATION
The global variables exist at configuration level. But MainProgram still redeclares the ones it uses:
PROGRAM MainProgram
VAR_EXTERNAL
gTempController : TempController;
gOverheatProtect : OverheatProtection;
g_rActualTemp : REAL;
g_rSetpoint : REAL;
g_xHeating : BOOL;
g_xFault : BOOL;
END_VAR
END_PROGRAM
This is a useful discipline. The global owner is the configuration. The program explicitly says, “I use these external variables.” That is closer to an import contract than to casually reaching into a global DB.
I do not want to turn this article into the full CONFIGURATION, TASK, and PROGRAM article. That deserves its own treatment. For variables, the key point is simple: VAR_GLOBAL declares global data, and VAR_EXTERNAL makes that data visible inside the program organization unit that uses it.
STRUCT Is The AX Version Of The UDT Mental Model
TIA Portal engineers already understand UDTs. You define one structured type, then reuse it everywhere.
In AX, the demo project does this with TempControlStatus:
NAMESPACE Otomakeit.Demo.Types
TYPE TempControlStatus : STRUCT
xHeating : BOOL;
xFault : BOOL;
rActualTemp : REAL;
rError : REAL;
END_STRUCT;
END_TYPE
END_NAMESPACE
That is the closest mental equivalent to a TIA UDT.
The type lives in a namespace: Otomakeit.Demo.Types. Then MainProgram imports that namespace and declares a local variable of that type:
USING Otomakeit.Demo.Types;
PROGRAM MainProgram
VAR
_stStatus : TempControlStatus;
END_VAR
END_PROGRAM
The program fills the structure from the controller outputs:
_stStatus.xHeating := gTempController.q_xHeating;
_stStatus.xFault := gTempController.q_xFault;
_stStatus.rActualTemp := g_rActualTemp;
_stStatus.rError := gTempController.q_rError;
This is normal PLC engineering. Package related values into a structured contract, pass that contract between subsystems, and avoid a pile of separate loose variables.
One thing worth being precise about: an inline STRUCT declared in the VAR section of an FB, or a named UDT-equivalent declared via TYPE ... STRUCT ... END_TYPE, also works in TIA Portal SCL. The bare type machinery is shared. The TIA-side limitation is that the type does not live in a namespace, which is what AX changes next.
The namespace piece is the part that differs from TIA Portal. In TIA, a PLC data type sits in the project tree. In AX, the type sits in source code and belongs to a namespace. That means two libraries can both have a type called Status without colliding, because their full names are different.
A STRUCT Can Be A Contract Between Namespaces
The namespace demo project has an even smaller example:
NAMESPACE Otomakeit.Demo.WaterTreatment.Types
TYPE AnalogReading :
STRUCT
Value : REAL; // Engineering value
RawValue : INT; // Raw analog count
InRange : BOOL; // Range validity flag
END_STRUCT;
END_TYPE
END_NAMESPACE
This is exactly the kind of type I would normally make as a TIA UDT: engineering value, raw value, diagnostic bit. In AX, I like putting this in a Types namespace because it makes the ownership obvious. The dosing controller, diagnostics logic, and any HMI-facing mapping can agree on the same shape without owning each other.
That is the practical value of STRUCT in AX: not just a group of fields, but a named contract.
ENUM Is For Named States
The same namespace demo has a pump status type:
NAMESPACE Otomakeit.Demo.WaterTreatment.Types
TYPE PumpStatus : (
Stopped,
Running,
Fault
) := Stopped;
END_TYPE
END_NAMESPACE
From the TIA side, think of this as a PLC enum type. Instead of passing 0, 1, and 2 around the project, the code can say Stopped, Running, and Fault.
That matters more than it looks.
An INT called _pumpStatus tells me almost nothing unless I go find the comment or the HMI text list. A PumpStatus variable tells me the allowed states right at the declaration. It also prevents the accidental fourth state that nobody designed.
For state machines, alarms, modes, and statuses, I would rather read this:
_pumpState : PumpStatus;
than this:
_pumpState : INT;
The second one may be normal in older PLC projects. The first one is a better contract.
Direct Addresses Still Exist
The TempController demo currently uses HMI-visible globals and OPC UA. It does not show a real input card mapped to a sensor.
But AX does support direct represented variables with the AT keyword. Siemens’ hardware documentation shows generated I/O address variables like this:
The snippet below is an excerpt of the generated I/O-address configuration. It shows the relevant VAR_GLOBAL declarations, but omits the surrounding task/program lines from the full Siemens example so it should not be read as a complete standalone CONFIGURATION.
CONFIGURATION IoAddresses
VAR_GLOBAL
MyIODevice_MyDigitalInputModule_Input AT %IB100
: MyIODevice_MyDigitalInputModule_Input_Layout;
MyIODevice_MyAnalogOutputModule_Output AT %QB100
: MyIODevice_MyAnalogOutputModule_Output_Layout;
END_VAR
END_CONFIGURATION
The generated layout type can then expose channels inside the input or output image:
TYPE
MyIODevice_MyDigitalInputModule_Input_Layout : STRUCT
Channel_0 AT %X0.0 : BOOL;
Channel_1 AT %X0.1 : BOOL;
Channel_2 AT %X0.2 : BOOL;
Channel_3 AT %X0.3 : BOOL;
END_STRUCT;
END_TYPE
Those Channel_0 AT %X0.0 fields are relative bit offsets inside the generated layout type that starts at %IB100 in the example above. They are not separate global %I0.0 declarations.
That maps cleanly to the TIA Portal idea of PLC tags bound to physical addresses. The difference is that the address binding is visible as text, and in hardware-configuration workflows it may be generated into system constants rather than typed by hand.
I would not mix this topic too deeply into the variable article, because I/O abstraction deserves its own article. The important point here is that variables can still be tied to %I, %Q, and related PLC memory areas. AX did not remove the PLC memory model.
The Practical Rule
When I add a variable in AX, I try to ask three questions before writing code:
- Does this variable belong to an FB instance, a program, the configuration, or a shared type?
- Is this a simple scalar, or should it be a
STRUCTorENUM? - Should the variable be local, external, global, constant, temporary, or physically addressed?
That order helps.
If it belongs to a function block interface, it is probably VAR_INPUT, VAR_OUTPUT, or VAR_IN_OUT.
If it is internal FB memory, it is VAR.
If it is just scratch calculation data, it is VAR_TEMP.
If it is a shared machine-wide value, it may be VAR_GLOBAL in the configuration and VAR_EXTERNAL where it is used.
If it represents a repeated shape of data, make a STRUCT.
If it represents a finite set of states, make an ENUM.
If it is tied to a physical address, use AT at the right level and keep the hardware boundary obvious.
The thing I like about AX is that these decisions become reviewable. In TIA Portal, you often have to click through block interfaces, DBs, tag tables, and PLC data types to understand the same thing. In AX, it is text. A pull request can show that someone changed a setpoint from REAL to LREAL, added a new field to a status STRUCT, or exposed a global via OPC UA.
That does not make the engineering automatically good. Bad variable ownership is still bad variable ownership. But it makes the design visible.
What Is Next
This article stayed on the foundation: variables, sections, scalar types, STRUCT, ENUM, typed literals, and direct address binding.
Next I want to go into the apax CLI in more detail. The earlier articles showed that the CLI works; the next one should map the commands by lifecycle: create, install, build, test, package, clean, and diagnose.
If you have done AX on a real project and organize variables differently, I want to hear it. I am especially interested in where teams draw the line between globals, structured status types, and hardware abstraction. I would rather learn the better field pattern than defend my first demo-project pattern.
