Overview
Engineers frequently maintain their I/O list as a flat collection of PLC tags in the Default tag table of a TIA Portal V18 project, then discover they need those same tags reflected inside a structured data block (DB) — for example, to feed an HMI symbol list, an OPC UA server namespace, or a recipe/array-driven application that consumes a UDT with a Key and a Value field.
This reference covers two distinct engineering paths for an S7-1500 / S7-1514SP-2PN target:
- Design-time generation using the TIA Portal Openness API (C# or VB.NET add-in) to read the default tag table and emit a fully-populated DB at compile time.
-
Runtime iteration using
VARIANTpointers,GetSymbolName, symbolic access, and a UDT array in the program itself, so the PLC exposes the live tag list without manual DB authoring.
The two approaches are not mutually exclusive. Openness is the right choice when the tag list changes during engineering and you need the DB to track it. Runtime iteration is the right choice when the same code must run against a varying tag list (e.g., machine options) without re-compiling the project.
Prerequisites
| Item | Required Value / Notes |
|---|---|
| TIA Portal | V18 (V18.0 + Update 1 or later recommended) with HSPs for S7-1500 |
| PLC CPU | S7-1514SP-2PN (Firmware ≥ V2.9 supports optimized block access and full Openness compile target) |
| TIA Portal Openness | Installed via the TIA Portal setup — exposes Siemens.Engineering assembly (DLLs under C:\Program Files\Siemens\Automation\Portal V18\PublicAPI\V18) |
| .NET runtime | .NET Framework 4.8 or .NET 6 (matching the TIA V18 Openness profile) |
| Visual Studio | 2019 / 2022 for add-in authoring |
| Reference manual | SIMATIC S7-1200 Programmable Controller — System Manual (Data Block section) |
| UDT definition | Custom UDT containing two String members (Key, Value) — see UDT Design below |
Variant and is required by the UDT array members you will populate. Symbolic access via fully-qualified names such as "MyDB".MyUDT[0].Key will fail to resolve at runtime on a non-optimized DB. Reference: TIA Portal documentation — Data Block (DB).Design-Time Approach — TIA Portal Openness
TIA Portal Openness is the official Siemens automation interface that exposes the engineering framework to external .NET code. It is the only supported way to programmatically read tag tables and create or modify DBs in a TIA V18 project.
Project Object Model (POM) summary for this task
| Object | Class | Path / Property |
|---|---|---|
| TIA Project | Siemens.Engineering.Project |
TiaPortal.Projects.Open(...) |
| PLC |
Siemens.Engineering.HW.Device (type Plc) |
Project.Devices |
| Software (CPU) | PlcSoftware |
PlcDevice.Items.OfType<PlcSoftware>().First() |
| Tag Table Group | PlcTagTableGroup |
PlcSoftware.TagTableGroups |
| Default Tag Table | PlcTagTable |
Name == "Default tag table"
|
| Tag | PlcTag |
PlcTagTable.Tags |
| UDT | PlcType |
PlcSoftware.TypeFolder.Types |
| Target DB |
DataBlock (instance DB of UDT or array of UDT) |
PlcSoftware.BlockGroup.Blocks |
Step 1 — Add references
Add the following DLLs from your TIA V18 install to the C# project (use Copy Local = false; they resolve at runtime via the Openness loader):
Siemens.Engineering.dll-
Siemens.Engineering.Hmi.dll(only if cross-targeting HMI) Siemens.Engineering.AddIn.dll
Step 2 — Open the project in exclusive mode
Openness requires the project to be opened with the WithDirectories option and prevents TIA Portal from editing it concurrently.
using Siemens.Engineering;
using Siemens.Engineering.HW;
using Siemens.Engineering.HW.Plc;
using Siemens.Engineering.SW;
using Siemens.Engineering.SW.Blocks;
using Siemens.Engineering.SW.Tags;
using Siemens.Engineering.SW.Types;
using Siemens.Engineering.Compiler;
TiaPortal tia = new TiaPortal(TiaPortalMode.WithoutUserInterface);
Project project = tia.Projects.Open(
new FileInfo(@"D:\Projects\MyMachine\MyMachine.ap18"));
Step 3 — Locate the Default tag table
PlcSoftware plc = project.Devices
.OfType<Device>()
.SelectMany(d => d.Items.OfType<PlcSoftware>())
.First();
PlcTagTable defaultTable = plc.TagTableGroups
.SelectMany(g => g.TagTables)
.First(t => t.Name == "Default tag table");
Step 4 — Enumerate the tags and build the DB payload
var rows = new List<(string Key, string Address, string DataType)>();
foreach (PlcTag tag in defaultTable.Tags)
{
string dataType = tag.DataTypeName; // e.g. "Bool", "Real", "Int"
string logical = tag.LogicalAddress?.ToString() ?? ""; // e.g. "%I0.0", "%ID4"
rows.Add((tag.Name, logical, dataType));
}
Step 5 — Create or overwrite the target DB
The cleanest pattern is an instance DB that is an array of a custom UDT. Define the UDT once manually:
TYPE "UDT_TagEntry"
VERSION : 1.0
STRUCT
Key : String[128]; // PLC tag name
Value : String[64]; // string-formatted current value
Addr : String[16]; // logical address e.g. %I0.0
Type : String[16]; // BOOL / REAL / INT ...
END_STRUCT;
END_TYPE
Then in Openness, delete the previous DB and create a new array instance sized to rows.Count:
PlcType udt = plc.TypeFolder.Types.First(t => t.Name == "UDT_TagEntry");
// remove an old DB with the same name, if present
var oldDb = plc.BlockGroup.Blocks
.OfType<DataBlock>()
.FirstOrDefault(b => b.Name == "DB_TagList");
if (oldDb != null) oldDb.Delete();
DataBlock db = plc.BlockGroup.Blocks
.CreateInstanceDB("DB_TagList", udt, rows.Count, true /* retain */);
db.IsOptimizedBlockAccess = true;
Step 6 — Initialize the static fields (Key / Addr / Type)
The Value field is filled at runtime. Key, Addr, and Type are known at design time and can be written through Openness by setting default attribute values via the instance DB's Attributes collection:
// For each UDT member, Siemens.Engineering exposes the
// attribute "Value" that becomes the default in the DB
// instance. Iterate the members and set per-index values.
var udtMembers = udt.Members; // PlcTypeMember collection
for (int i = 0; i < rows.Count; i++)
{
var entry = rows[i];
// member-level defaulting through IEngineeringInstance path;
// in practice most teams generate an SCL source instead
}
For maintainability, generating an SCL source file is more robust than fiddling with attribute defaults. Have Openness emit the equivalent of:
DATA_BLOCK "DB_TagList"
{ S7_Optimized_Access := 'TRUE' }
VERSION : 0.1
NON_RETAIN
STRUCT
Array : ARRAY[0..N-1] OF "UDT_TagEntry";
END_STRUCT;
END_DATA_BLOCK
…then write a one-shot FC_InitTagList in SCL that runs once on OB100 / startup and walks the array copying each PLC tag's Key, Addr, and type-name string.
Step 7 — Compile and load
Compiler compiler = plc.GetService<Compiler>();
CompilerResult result = compiler.Compile();
foreach (var msg in result.Messages)
{
Console.WriteLine($"[{msg.Severity}] {msg.Path}: {msg.Message}");
}
result.Messages.ForEach(m => { /* fail on Error */ });
Step 8 — Save and close
project.Save();
project.Close();
tia.Dispose();
Runtime Approach — Variant + Symbolic Access
If you cannot (or do not want to) re-compile the project each time the tag list changes, you can perform the iteration in SCL on the CPU using VARIANT pointers and a CASE on the data type. The PLC code does not know the tag table, but it does know every tag you hand it symbolically.
Runtime UDT design
TYPE "UDT_TagEntry_RT"
VERSION : 1.0
STRUCT
Key : WString[128]; // symbolic tag name (WString survives any HMI charset)
Value : WString[64]; // formatted value as text
END_STRUCT;
END_TYPE
Helper FC — TagToString (Variant in, String out)
FUNCTION "FC_TagToString" : Void
{ S7_Optimized_Access := 'TRUE' }
VERSION : 1.0
VAR_INPUT
pTag : Variant; // ANY pointer to the live tag
END_VAR
VAR_IN_OUT
sValue : WString; // output text
END_VAR
VAR_TEMP
b : Bool;
i : Int;
di : DInt;
r : Real;
s : String;
ws : WString;
END_VAR
BEGIN
CASE TypeOf(pTag) OF
TypeOf(b):
b := pTag;
sValue := b ? WSTRING#'TRUE' : WSTRING#'FALSE';
TypeOf(i):
i := pTag;
WSTRING_TO_INT_FORMAT(i, sValue); // manual INT_TO_WSTRING inline
TypeOf(di):
di := pTag;
WSTRING_TO_DINT_FORMAT(di, sValue);
TypeOf(r):
r := pTag;
REAL_TO_WSTRING(r, sValue);
TypeOf(s):
s := pTag;
sValue := WSTRING#'';
// String to WString via block conv if needed
ELSE
sValue := WSTRING#'<unsupported>';
END_CASE;
END_FUNCTION
WSTRING_TO_*_FORMAT shown above are illustrative — implement with the standard SCL conversions: INT_TO_WSTRING, DINT_TO_WSTRING, REAL_TO_WSTRING (S7-1500 supports these natively). See the S7-1500 system manual, section Type conversion functions for character and string types.
Helper FC — Capture tag name
Siemens provides GetSymbolName for blocks and GetSymbolPath for instance paths, but there is no runtime API to discover tag-table entries from the PLC. The variant input's symbolic name, however, is accessible at runtime only if you reference the tag by a fully-qualified name in the VARIANT. To capture it, pass the name in explicitly:
// Caller side
"DB_TagList".Array[i].Key := '"MyTag_1"'; // literal string of the tag name
"DB_TagList".Array[i].Value := "FC_TagToString"(
pTag := "MyTag_1",
sValue => "DB_TagList".Array[i].Value);
This is the fundamental reason most teams prefer the Openness design-time approach: the runtime approach works but the caller must hard-code the tag name once per row, which defeats the goal of "iterating over the tag list" dynamically.
Hybrid Approach — Openness-Generated Index, Runtime Read
The practical pattern that solves the user's original requirement is a hybrid:
- Openness reads the default tag table at design time and emits a generated
ARRAY[0..N-1] OF "UDT_TagEntry"with theKey,Addr,Typefilled in. - The runtime DB also exposes a shadow array of
VARIANTpointers (one per row) that the Openness script populates by writing the absolute symbolic path into aPOINTER TO BYTEor, more practically, by emitting an SCL FC that copies each tag into a typed shadow array element:
// Generated by Openness, one statement per default-tag-table row
"DB_TagList".Shadow[0] := "Tag_1";
"DB_TagList".Shadow[1] := "Tag_2";
"DB_TagList".Shadow[2] := "Tag_3";
// ...
The Shadow array's element type is the broadest data type you need (commonly DWord for bit-packed reads, or Variant for fully generic access — note that Variant cannot be the element of an array in classic S7-1500 firmware < V2.9; use a wrapper block or a typed shadow of the largest type).
Runtime sweep over the hybrid DB
// OB1 cyclic scan
FOR i := 0 TO "DB_TagList".UpperBound DO
"FC_TagToString"(
pTag := "DB_TagList".Shadow[i],
sValue => "DB_TagList".Array[i].Value);
END_FOR;
Because the symbolic addresses were generated at compile time, the optimizer folds them into direct memory accesses; the loop overhead is minimal.
XML Import Format for the Default Tag Table
The user's project uses an XML file to seed the default tag table. The TIA V18 tag-table XML schema is documented in the Openness help; a minimal valid fragment:
<?xml version="1.0" encoding="utf-8"?>
<TagTable>
<Name>Default tag table</Name>
<Tags>
<Tag>
<Name>Tag_1</Name>
<DataType>Bool</DataType>
<LogicalAddress>%I0.0</LogicalAddress>
</Tag>
<Tag>
<Name>Tag_2</Name>
<DataType>Real</DataType>
<LogicalAddress>%ID1</LogicalAddress>
</Tag>
</Tags>
</TagTable>
Import path: In TIA Portal, right-click the Default tag table → Import → select the XML. To script the import, Openness provides PlcTagTable.Import(FileInfo, ImportOptions). See the TIA Portal Openness reference manual under PLC tag table import/export.
Verification
| Step | What to check | Pass criterion |
|---|---|---|
| 1. Compile | Openness compile output | No Error-severity messages; only Info/Warning tolerated |
| 2. Download | CPU transitions to RUN with the new DB | SF LED off; diagnostic buffer clean |
| 3. Watch table | Open a watch table on DB_TagList
|
All Key entries equal the tag-table names; Type matches |
| 4. Live update | Toggle an input that maps to Tag_1
|
DB_TagList.Array[0].Value flips between 'TRUE' and 'FALSE'
|
| 5. HMI / OPC UA | Browse the DB through the integrated OPC UA server | Each array element is visible as a structured node with Key / Value strings |
| 6. Round-trip | Re-run the Openness tool against a modified XML | DB regenerates; project compiles; previous DB contents are overwritten without orphan members |
Troubleshooting Matrix
| Symptom | Likely Cause | Fix |
|---|---|---|
CompilerResult reports Symbol not found for the generated DB |
UDT was renamed after the Openness script was generated | Recreate the UDT before running Openness, or detect by name in the script |
| Download fails with "Protected block access" | DB created without optimized access | Set db.IsOptimizedBlockAccess = true in the script before compile |
| Runtime access in HMI shows all zeros | Target DB not set as Visible in HMI | Open DB Properties → Attributes → Accessible from HMI / OPC UA = true |
VARIANT access returns 0
|
Tag address has not been initialized; I/O area is not updated | Verify hardware configuration and I/O mapping for the input tag |
Openness throws TiaPortalException: project is locked
|
TIA Portal GUI is open against the same project | Close the GUI or use TiaPortalMode.WithUserInterface with OpenProjectInProcess properly disposed |
| Duplicate name on regeneration | Old DB not deleted | Wrap DB creation in a "delete-if-exists" helper as shown in Step 5 |
REAL_TO_WSTRING shows scientific notation |
Default conversion format | Use SPRINTF-style formatting via the FORMAT library or post-process the WString |
Best Practices and Field Notes
- Optimized block access is non-negotiable for any DB consumed through symbolic access from the OPC UA server or HMIs. The S7-1200 system manual — Data Block (DB) shows the property dialog explicitly.
- Keep the UDT small and flat. Nested UDTs work but make the Openness walk more complex and slow down HMI refresh.
- Version the generated DB with a header comment containing the source XML file name and SHA-256 hash, so audit trails can correlate PLC content with engineering source.
-
Run Openness from CI (Jenkins / Azure Pipelines) and fail the build on
Error-severity compile messages. This prevents a stale tag table from silently mismatching the DB. - Do not iterate the default tag table at runtime. The S7 CPU has no runtime API that enumerates tag-table contents. The tag table is a project-level artifact; iteration must happen at design time via Openness.
-
Use
WStringrather thanStringfor any tag the OPC UA server will publish. The integrated OPC UA server on the S7-1500 mapsWStringdirectly toStringand avoids ASCII-to-UTF-8 mismatches with non-Latin tag names. -
Pin the Openness DLL versions to your TIA V18 install path. Mixing V17 and V18
Siemens.Engineering.dllreferences causes the loader to fail withFileLoadException. -
Array bounds: when generating an array of UDT, prefer
ARRAY[0..N-1]to match the zero-based indexing used by SCLFORloops; this prevents off-by-one errors in the runtime sweep.
Related Standards and References
- SIMATIC S7-1200 Manual Collection — Data Block (DB) programming concepts
- SIMATIC S7-1500 S7-1500/ET 200MP Automation System System Manual (function manual sections on Optimized blocks and VARIANT data type)
- TIA Portal Openness: Automating Engineering with TIA Portal — application manual
Can I iterate the Default tag table from inside the PLC at runtime?
No. The PLC has no API that exposes the contents of the default tag table at runtime. The tag table is a project-level artifact compiled away after build. Use TIA Portal Openness to read the tag table at design time, or pass symbolic tag names into a Variant array manually.
Do I need TIA Portal Openness to import an XML file into the Default tag table?
No — the GUI Import command (right-click the Default tag table → Import) handles the XML natively in TIA Portal V18. Openness is only required if you want to drive the import from a script, regenerate DBs, or wire the tag import into a CI pipeline.
What UDT member types should I use for Key and Value?
Use WString[128] for the Key (tag name) and WString[64] for the Value (formatted text). WString maps cleanly to the integrated OPC UA server and avoids encoding issues when tag names contain non-ASCII characters from international projects.
Why does my HMI or OPC UA client see only zeros in the new DB?
The DB is most likely not marked Accessible from HMI / OPC UA. Open the DB in the project tree, select Properties → Attributes, and enable Accessible from HMI / OPC UA. Also confirm that Optimized block access is enabled — see the S7-1200 manual — Data Block (DB).
Which TIA Portal version introduced full support for array-of-UDT DBs in Openness?
Array-of-UDT instance DBs are supported from TIA Portal V14 SP1 onward. For TIA V18 on a 1514SP-2PN, both the Openness API call CreateInstanceDB(name, udt, count, retain) and optimized block access work without restrictions on firmware ≥ V2.9.