Siemens STL Precompiler Directives: #define, USES, USELIB

David Krause15 min read
SiemensTIA PortalTutorial / How-to
Licensed PE Working through this on a live machine? A Maine-licensed engineer can take it from here — included with IMD hardware, by the hour for everything else. Book an engineer

Overview

Siemens STL (Statement List) precompiler directives are the mechanism by which a single S7-300/400 or S7-1500 source tree can produce different firmware builds without duplicating the project. Unlike runtime logic, which decides what the CPU executes after the program is loaded, precompiler logic decides what the compiler accepts into the binary image at all. A code path wrapped in #ifdef is either physically present in the compiled FB or it is not — there is no runtime cost and there is no way for an HMI to toggle it.

The original forum question described a block that appeared to be testing for a label called teststand, conditionally defining a constant bTESTSTAND and a secondary label defTESTAND. That single pattern is the canonical use of the STL preprocessor for separating test-stand vs. production firmware, hardware revisions, and optional feature builds. This reference covers the full directive set shipped with Step 7 V5.x and TIA Portal V16 / V17 / V18, the difference between USES and USELIB, the correct way to trace a defined symbol back to its origin (and why the Cross Reference view is the wrong tool), and the diagnostic checks that confirm a directive is doing what you expect.

For the official STL language reference and the S7-1500 programming manual, see the SIMATIC S7-1500 Programming Guideline and the STEP 7 V5.x STL Reference.

Prerequisites

  • Siemens SIMATIC Step 7 V5.5 / V5.6 (for S7-300/400) or TIA Portal V16 / V17 / V18 (for S7-1200/1500), with the corresponding option package installed.
  • Programming language support: STL on S7-300/400; STL or SCL on S7-1500 — both support the same preprocessor directive set.
  • A project that contains at least one STL source file so that the import chain (USES) can be demonstrated.
  • For USELIB examples: a master copy or type library in the Library View (TIA Portal) or a *.s7l library (Step 7 V5.x).
  • Read/write access to the source files in the project's source folder so that temporary #define injections can be tested.

Precompiler Directive Reference

The STL preprocessor supports a deliberately small set of directives. Anything outside this list is not part of the language and will produce a compile error.

Directive Purpose Example Notes
#define name Define a preprocessor symbol with no value #define teststand Only the existence of the symbol matters; the value (if any) is ignored by #ifdef.
#define name value Define a macro substitution #define MAX_CNT 100 Legacy S7 code uses this for numeric constants; TIA Portal treats these as integer literals in SCL.
#undef name Remove a previous definition #undef teststand Useful inside an #else branch to force a downstream #ifdef off.
#ifdef name Open a conditional block if name is defined #ifdef teststand An empty or zero-value define still counts as defined.
#ifndef name Open a block if name is NOT defined #ifndef PRODUCTION Commonly used to guard safety code in a default-off posture.
#else Alternate branch in a conditional #else Closes the prior #ifdef or #ifndef branch.
#endif Closes a conditional block #endif Required. Unbalanced #endif causes a compile error in the S7-CPU error class.
USES Import another STL/SCL source into the same compilation unit USES "IO_Mapping.stl" Source is recompiled each pass. Path is relative to the project source folder.
USELIB Import a compiled library USELIB "MyLib.s7l" Library is compiled separately; its internal #define symbols do NOT propagate to the importer.
Note: STL does not implement #include, macro arguments, stringification (#x), or token pasting (##). Engineers porting C preprocessor patterns into STL will find that the import mechanism is USES / USELIB rather than #include, and that all defines in the imported source become global to the compilation pass.

Tracing the teststand Example Step-by-Step

The example block from the original post looked like the following when extracted into a typical project source.

FUNCTION_BLOCK FB_TestStand
VAR
   bTestActive : BOOL;
END_VAR

#ifdef teststand
   CONST bTESTSTAND : BOOL := TRUE;
   #define defTESTAND
#else
   CONST bTESTSTAND : BOOL := FALSE;
#endif

BEGIN
   bTestActive := bTESTSTAND;
END_FUNCTION_BLOCK

The resolution path is the same on Step 7 V5.x and TIA Portal, and follows five steps.

  1. Locate any #define teststand in the project. Open the source editor and press Ctrl+F to launch the standard Find dialog. The Search function in the Edit menu is the correct tool, because the Cross Reference view only tracks compiled symbol usage, not preprocessor symbols.
  2. If the search returns zero hits in the current source, expand scope. In Step 7 V5.x, right-click the S7 program node and choose Source Files > Show All Sources. In TIA Portal, expand PLC > External source files and the Program blocks > System blocks > Libraries tree. Re-run the search against all of them.
  3. If a hit is found in a source that is USES-imported, the define still propagates. The preprocessor builds a global symbol table for the entire compilation pass, not the file. Confirm the import path by searching for USES or USELIB at the top of the dependent source.
  4. If the search still returns no hit, the #ifdef teststand branch is dead. The compiler silently excludes the block, bTESTSTAND is set to FALSE, and the label defTESTAND is never defined. Any subsequent #ifdef defTESTAND in the same compilation pass will also evaluate false.
  5. To force the test-stand branch on for a single verification build, add a local #define teststand at the very top of the source, before the FUNCTION_BLOCK declaration. Recompile, confirm that bTESTSTAND is TRUE, and then remove the temporary define before downloading to production hardware.
Warning: A #define at the top of any source is local in syntax but global in effect within the compilation pass. Defining teststand in one FB will switch the branch on in every other FB compiled in the same source pass, including FBs that have no business seeing it. Use a clearly namespaced symbol such as FEATURE_TESTSTAND_V2 or BUILD_TS_2024Q3 to avoid silent collisions.

USES vs. USELIB — When to Use Each

Both directives insert content from another file at the point of use, but they differ in build-time behavior, version-control implications, and — critically for the test-stand pattern — define propagation.

Attribute USES USELIB
File type Raw STL / SCL / LAD / FBD source Pre-compiled S7 library (*.s7l on Step 7 V5.x; *.al* on TIA Portal)
Re-parsed on each build Yes No — only re-parsed if the library itself is recompiled
Editable from the consumer project Yes, but the change is local to the consumer's copy of the file No — read-only inside the consumer
Defines visible to importer All #define symbols in the imported source are visible to the importer Defines set inside the library are not propagated to the importer
Compile errors raised in The consumer's compile log, naming the imported file and line Inside the library; the consumer only sees a generic failure
Recommended for Project-local shared types, IO mapping tables, internal firmware branches, the test-stand flag source Vendor libraries, validated function blocks, third-party IP, machine-builder type libraries

The crucial difference for the bTESTSTAND pattern is the Defines visible to importer row. A #define placed inside a file brought in by USES will be visible to the importing source. A #define placed inside a file brought in by USELIB will not. If your project-wide search for #define teststand lands only inside a library, that is a strong hint that the bTESTSTAND branch is actually being toggled by a flag passed in a different way — most often a project-wide compile-time switch set in the library's own configuration block, or a project-level preprocessor variable set in the CPU properties.

How to Search for a Preprocessor Define

There are three places to look, in order of likelihood, and a fourth place engineers often look by mistake.

  1. The same source file. The standard Find dialog (Ctrl+F in Step 7 and TIA Portal) scans the open source by default. This is the right tool for preprocessor symbols.
  2. A source that the current source imports with USES. Use Find in Project. In Step 7 V5.x: Edit > Find in Project. In TIA Portal: right-click the project tree node and select Find and Replace, or use the global search bar in the project view.
  3. A library that the project references. Open the library master in the Library View (TIA Portal: Libraries > Master copies) or the Library folder (Step 7 V5.x: S7 Program > Sources > ... > Library), then run Find inside it. A find inside a compiled *.s7l will not show #define symbols; the source of the library must be opened in a writable form.
Common mistake: The Cross Reference view, despite its name, only enumerates symbol uses in compiled blocks: tags, instance DBs, FC/FB calls, and address operands. It does not see #define directives. This is the most common source of confusion when an engineer searches for teststand, finds nothing, and concludes the symbol is unused — when in reality it is a build-time switch that the Cross Reference is not designed to report.

Conditional Compilation Patterns

Three patterns show up repeatedly in production S7 code. Each is paired with the exact preprocessor syntax required to make it work.

Pattern 1: Test-Stand / Production Split

// Comment out the next line for production builds
#define teststand

FUNCTION_BLOCK FB_TestStand
VAR
   bTestActive : BOOL;
END_VAR

BEGIN
#ifdef teststand
   // Force test-stand inputs for dry-run validation
   "TS.Mode" := 1;
   "TS.DryRun" := TRUE;
#endif
   bTestActive := bTESTSTAND;
END_FUNCTION_BLOCK

The define is left commented out in the production source. The build engineer uncomments it for the test-stand build only. Combined with the original forum example, the constant bTESTSTAND is then TRUE and downstream blocks (e.g. an HMI startup screen) can branch on it.

Pattern 2: Hardware Variant

// Set per project, not per build
#define HW_REV_B

#ifdef HW_REV_A
   CALL FB_RevA_Init
#else
   CALL FB_RevB_Init
#endif

The define is set per project, lives in a small configuration source imported with USES, and is rarely changed after commissioning.

Pattern 3: Optional Features Behind a Compile Flag

#ifndef DISABLE_SAFETY
   CALL FB_SafetyMonitor
#endif

The absence of the symbol is the switch. This is the most defensible default for safety-related code, because the compiled binary always contains the safety call unless an explicit #define DISABLE_SAFETY is added — the opposite of a typical "opt-in" pattern.

Pattern 4: Multi-Line Macro Substitution

#define LOG_WARN(tag, msg) \
   "Logger".Write(WARN_LEVEL := 1, TAG := tag, MSG := msg)

LOG_WARN("E-Stop", "Button pressed");

Multi-line macros use the C-style backslash continuation. TIA Portal V17+ accepts them in both STL and SCL; Step 7 V5.x accepts them in STL. Use sparingly — the resulting compile errors are difficult to localize.

Preprocessor Error Codes and Diagnostics

Step 7 and TIA Portal surface preprocessor errors as part of the regular compile log, but with specific error classes that engineers can recognize.

Symptom in compile log Likely cause Resolution
"Syntax error in preprocessor section" A #define placed after a code statement, or a directive outside the preprocessor zone (S7-1500 only) Move all #define lines to the top of the source, before the first executable statement
"Preprocessor block not closed" Missing #endif Pair every #ifdef / #ifndef with a matching #endif
"Unexpected identifier after #" Directive name misspelled (e.g. #defiine) or non-directive token starting with # Verify spelling against the reference table above
"Cannot resolve imported source" USES path does not resolve from the project's source folder Place the imported source in External source files (TIA Portal) or S7 Program > Sources (Step 7)
"Library version mismatch" Library referenced by USELIB was recompiled with a different interface Update the consumer project to match the library's current version, or downgrade the library
"Symbol already defined" Two #define statements define the same name with different values Use #undef before redefining, or pick distinct names

Troubleshooting Matrix

Symptom Likely cause Verification Fix
bTESTSTAND is FALSE when it should be TRUE No #define teststand reached the preprocessor Find in project for the literal string teststand Add a project-wide #define in a config source imported with USES, or uncomment the build-time flag
Compile error: "Unbalanced #endif" Missing or extra #endif Step 7 reports the source and line number Add or remove the matching directive to balance the block
Define visible in source A but not in source B Source B uses USELIB to import the defining file Check the directive at the top of source B Switch to USES, or move the define to a shared USES-imported source
Define visible in all FBs unexpectedly Define is global to the compilation pass, not the file Search the project for the symbol Use a namespaced symbol and move the define to a file imported only by the FBs that need it
Cross Reference shows nothing for teststand Cross Reference only tracks compiled symbols Use Edit > Find instead None — the editor behavior is correct
Compiler accepts the code but the runtime behavior is wrong Wrong branch selected because of a stray define in another file Force the value of bTESTSTAND into a watch table Strip stray defines; rebuild from a clean source folder
Define was present in Step 7 V5.x, missing in TIA Portal V18 TIA Portal does not propagate defines from USELIB libraries the same way Open the library in TIA Portal and inspect its interface Move the define to a USES-imported source, or use the library's exposed configuration block

Verification Checklist

  1. Open the source containing the #ifdef teststand block and confirm the constant bTESTSTAND is declared exactly once.
  2. Run Edit > Find in Project for the literal string teststand and note every hit.
  3. For each hit, verify whether the defining file is brought in via USES (defines propagate) or USELIB (defines do not propagate).
  4. Add a temporary #define teststand at the top of the consumer source, recompile, and confirm bTESTSTAND changes to TRUE in the watch table.
  5. Remove the temporary define, recompile, and confirm bTESTSTAND returns to FALSE.
  6. Run the generated block through the offline / online compare (Step 7: PLC > Compare Blocks; TIA Portal: Online > Compare) and confirm the compiled STL matches the expected branch.
  7. Document the define location in the project README so the next engineer does not have to repeat the search.
  8. For production builds, confirm no #define for teststand is present in any source or library imported with USES into the production firmware.

Notes on S7-1500 / TIA Portal Differences

On S7-1500 CPUs the STL language is supported, but TIA Portal's preprocessor is stricter in three ways that engineers moving code from Step 7 V5.x will encounter.

  • All #define lines must precede the first executable statement. TIA Portal does not allow a #define to appear after any code line, including declarations, in the same compilation unit.
  • Warning on unused defines. TIA Portal warns — but does not error — when a defined symbol is never used. The warning appears in the compile log with a "Preprocessor symbol defined but not used" message and an entry in the information pane.
  • Stricter USES resolution. TIA Portal resolves USES paths relative to the project's External source files folder. A source that lives in the same folder but is not added to the project will not be found, even though Step 7 V5.x would have picked it up from the local source directory.

The semantics of USES and USELIB are unchanged, and SCL on S7-1500 supports the same preprocessor directive set, so the bTESTSTAND pattern is portable across STL and SCL with no source changes.

Safety implication: Never use a preprocessor define as the sole gate on a safety-rated code path. The define is a build-time artifact; once the block is compiled out, no PLC-side audit can prove the safety logic ever existed in that binary. The IEC 61131-3 and IEC 61508 expectation is that the safety function is present in every build and its activation is decided at runtime by a tag, not by the compiler. For SIL-rated applications, see the SIMATIC Safety Programming Guideline for the supported patterns.

Field-Proven Caveats

Three issues that surface regularly on real commissioning sites.

  1. Hidden defines inside generated code. Some Siemens tool add-ons (HMI tag generators, library manager scripts) insert #define lines into source files that the application engineer never sees in the editor. If teststand appears in a project that nobody typed it into, search the generated sources folder before concluding the symbol is unused.
  2. Case sensitivity in Step 7 V5.x vs. TIA Portal. Step 7 V5.x is case-insensitive for preprocessor symbols; TIA Portal is case-sensitive. A define of TestStand will not match an #ifdef teststand in TIA Portal even though it did in Step 7 V5.x. Normalize the case at the start of a TIA Portal migration.
  3. Watch table names with the same string. Engineers sometimes add a watch table named "teststand" while debugging, then confuse its presence with a defined preprocessor symbol. The Cross Reference view will show the watch table reference, but it is unrelated to the preprocessor state.

FAQ

What does the #ifdef teststand block actually do at compile time?

The preprocessor reads every source file in the compilation pass, builds a global table of defined symbols, and then either includes or excludes each #ifdef block before the STL compiler ever sees the code. If teststand is defined anywhere in that pass — in the same file, in a USES-imported file, or in the source that the current file indirectly imports — the branch is included and bTESTSTAND is set to TRUE. If no #define teststand is found, the block is dropped and bTESTSTAND is set to FALSE.

Why does the Cross Reference view show nothing for teststand?

Cross Reference only reports symbol uses in compiled blocks: tags, instance DBs, FC/FB calls, and address operands. Preprocessor directives are not part of the compiled symbol table, so they do not appear. Use the standard Find dialog (Ctrl+F) or Edit > Find in Project to locate #define statements.

Can a define inside a USELIB-imported library turn on a branch in the importing source?

No. A library brought in with USELIB is pre-compiled and its internal #define symbols are not visible to the importer. If you need a define to be visible across the project, put it in a source imported with USES, or place the #define directly in the source that needs the branch.

How do I temporarily force the test-stand branch on without modifying library code?

Add a #define teststand at the very top of the source that contains the #ifdef block — before the FUNCTION_BLOCK declaration. Recompile, verify bTESTSTAND becomes TRUE in a watch table, then remove the temporary define and recompile before downloading to production hardware.

Is the STL preprocessor the same as the C preprocessor?

The syntax is the same for #define, #undef, #ifdef, #ifndef, #else, and #endif, but STL does not implement #include, macro arguments, stringification, or token pasting. The import mechanism in STL is USES and USELIB, which is closer to Pascal's uses clause than to C's #include.

Does the bTESTSTAND pattern work on S7-1200 / S7-1500 in TIA Portal?

Yes. S7-1200 and S7-1500 CPUs in TIA Portal V16+ support the same #define, #ifdef, #ifndef, #else, #endif, USES, and USELIB directives in both STL and SCL. The only practical difference is that TIA Portal requires all #define lines to precede the first executable statement and warns on unused defines.

Back to blog