Siemens S7 UDT Array Programming for LIFO Cage Sequencing

David Krause18 min read
SiemensTechnical ReferenceTIA Portal
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 of the LIFO/FIFO Cage Sequencing Problem

Laminated beam production requires a multi-stage material handling flow that defeats a naive linear recipe. Individual boards are pressed into a press cycle, loaded into drying/curing cages on a Last-In-First-Out (LIFO) basis, the cages themselves are then unloaded in First-In-First-Out (FIFO) order, and the resulting boards must be assembled into a beam whose outer sixth (1/6) is A-quality material and whose core is B-quality. A correctly loaded beam therefore requires the production order to be the reverse of the unloading order, intersected with the cage-stack group size and the per-cage board count.

A direct mapping from a recipe to a single flat data block is brittle: cage sizes change, the number of beams per press changes, the number of cages per press changes, and the boards-per-cage value is recipe-driven. The robust Siemens approach is to define a User-Defined Type (UDT) for each product entity, an array of UDTs for the cage contents, and a higher-level UDT for the cage itself, then let the program compute production order from the recipe at first scan or at recipe-load using SFC21 FILL, pointer-based loops, and symbolic addressing. This pattern is documented in the STEP 7 / TIA Portal programming references on the Siemens STEP 7 product page and the supporting programming guides on the Siemens SiePortal.

Prerequisites

  • STEP 7 V5.5 SP2+ (classic) or TIA Portal V15.1+ with the S7-300/S7-400 or S7-1500 support package installed.
  • CPU with sufficient work memory for the UDT arrays. A 20-cage × 20-board/cage array with a 44-byte cage footprint occupies 880 bytes per cage plus 16 bytes per board; allow 2-3x the theoretical minimum for retained / non-optimized block overhead on S7-300/400. On S7-1500 with optimized blocks, align structures on 8-byte boundaries to avoid padding waste.
  • Firmware target reference: S7-300 (6AG1/CPU 31x, FW ≥ V3.3), S7-400 (CPU 41x, FW ≥ V6.0), or S7-1500 (CPU 1511-1 PN through 1518-4 PN/DP, FW ≥ V2.0). STL is the language of choice for the pointer-arithmetic sections; LAD/FBD equivalents are available for S7-1500 with the indirect field-access instructions.
  • HMI tag interface planned for symbolic read access to all UDT elements. The programming patterns below are aligned with the Siemens programming skill blog recommendations on data types and optimized blocks.

UDT and Array Architecture

The architecture is three nested UDTs. Define a UDT_Board that captures the per-board properties, a UDT_Cage that contains an array of UDT_Board plus cage-level metadata, and a higher-level UDT_System (or UDT_Press) that contains an array of UDT_Cage and the recipe header.

UDT_Board Definition

TYPE UDT_Board
STRUCT
  ProductID        : INT;   // 2 bytes  - recipe or generated ID
  Length           : INT;   // 2 bytes  - mm
  Width            : INT;   // 2 bytes  - mm
  Thickness        : INT;   // 2 bytes  - 0.1 mm units typical
  Quality          : CHAR;  // 1 byte   - 'A' or 'B'
  SequenceNumber   : INT;   // 2 bytes  - position in the production list
  BeamInstance     : INT;   // 2 bytes  - 1..MaxBeams
  Flags            : WORD;  // 2 bytes  - bit 0 = SOB (start of beam), bit 1 = EOB (end of beam)
END_STRUCT
END_TYPE

Size: 14 bytes when packed in a classic DB, 16 bytes in optimized blocks (S7-1500 pads to word boundary). Always include the Flags word for future expansion of Start Of Beam, End Of Beam, Surface, Defect Marked, and Already Unloaded bits. The forum example deliberately stored only ProductID, Length, Width, and SequenceNumber (8 bytes per board) for visual clarity, but field deployments always need the additional flags.

UDT_Cage Definition

TYPE UDT_Cage
STRUCT
  LaminatedBoardID : INT;      // 2 bytes
  DesiredDryingTime: DINT;     // 4 bytes - seconds
  CurrentDryingTime: DINT;     // 4 bytes - seconds
  CageStatus       : WORD;     // 2 bytes - bit 0 = InPosition, bit 1 = DoorClosed, bit 2 = Loaded, bit 3 = Unloaded
  Boards           : ARRAY[1..20] OF UDT_Board;  // 20 × 16 = 320 bytes optimized
END_STRUCT
END_TYPE

Each cage has both per-cage properties (drying time, status) and an embedded array of boards. By storing the boards inside the cage UDT, a single MOVE_BLK or UBLKMOV call can move an entire cage as a unit, and symbolic access resolves as System01.Cage[2].Boards[5].Quality with no pointer arithmetic at the HMI side.

UDT_System / UDT_Press Definition

TYPE UDT_System
STRUCT
  RecipeID         : DWORD;    // 4 bytes
  BeamsPerPress    : INT;      // 2 bytes  - 1..10 typical
  BoardsPerCage    : INT;      // 2 bytes  - 5..20 typical, recipe-driven
  CagesPerPress    : INT;      // 2 bytes
  ActiveLaminateID : INT;      // 2 bytes  - for cross-check against board IDs
  ProductionList   : ARRAY[1..400] OF INT; // mirrors DB30 production order
  Cages            : ARRAY[1..20] OF UDT_Cage; // 20 × 340 = 6800 bytes optimized
END_STRUCT
END_TYPE

Symbolic vs Absolute Addressing

Set the Address Priority to Symbolic on every block that references these UDTs. Symbolic-only access (no mix of DB10.DBW16 and System01.Cage[1].Boards[3].Length in the same block) lets you change the UDT footprint, insert new properties, or change the array bounds without recompiling every consumer. This is the single most important configuration step to enable a maintainable cage-sequencing program.

Where you must use absolute pointers (the LIFO/board-count loops), localize them to a single FC and pass the symbolic block as INPUT of type POINTER or VARIANT. The pointer code below is the only place where the absolute offsets appear; everywhere else uses the symbolic path System01.Cage[i].Boards[j].Field.

Data Block Layout

The reference layout from the field example uses three DBs, each holding one role:

DB Role Typical Size Notes
DB10 Virtual cage contents (output of FC3) ~6800 B UDT_System instance
DB20 Recipe / beam data (input of FC2) Recipe-dependent First 40 B = beam header, then board list
DB30 Production list in LIFO order (output of FC4) 400-800 B One INT per board: sequence number and source

For S7-300/400, mark DB10 and DB30 as non-optimized to allow the SFC20/21 block-move operations. For S7-1500, use MOVE_BLK and FILL_BLK from the extended instructions library; they handle optimized blocks natively. Never use SFC21 FILL on an optimized block in S7-1500; it will return W#16#8090 ("DB number error") or W#16#80B1 ("Length error").

Block Architecture (OB1, FC1-FC4, DB10/20/30)

The reference program follows a strict pipeline driven from OB1:

  1. OB1 populates DB20 with hard-coded test data; in production the recipe download from the HMI/ERP writes to DB20. OB1 then calls FC1-FC4 in order, gated by a one-shot bit (M24.0 in the example) that the program clears itself.
  2. FC1 - Clear: Calls SFC21 FILL to zero the prior recipe's data in DB10 and DB30. Source is a 4-byte zero constant at P#M0.0 BYTE 4 with BVAL pointed at the same constant. Always re-issue the FILL with the actual destination pointer; a stale DBNO is the most common cause of W#16#80B0 / W#16#80A1 errors at first scan after power-on.
  3. FC2 - Beam Decomposition: For each beam in the recipe, reads NumberOfBoards, computes the A/B split (see Board Counting section), then writes a sequential list of board records into the working area of DB20.
  4. FC3 - Cage Fill: Walks the board list in sequence and inserts boards into the highest available position of each cage, capped by the recipe's BoardsPerCage value. Produces a mirror of the live production state in DB10.
  5. FC4 - Production List: Iterates the cages, picks the last loaded board from Cage 1 first, then the next, and so on across the cage array, writing the LIFO production list to DB30. This is the inverse of the cage-fill order combined with the cage-array order, which is exactly the recipe required to satisfy LIFO cage / FIFO cage sequencing.

SFC21 FILL Operation

Use SFC21 FILL to clear DB10 and DB30 between recipes. STL call pattern (classic S7-300/400):

// Clear DB10 (entire UDT_System instance, symbolic -> use SFC20 with VARIANT instead on S7-1500)
CALL  "FILL"
  BVAL  :=P#M0.0 BYTE 4       // 4 bytes of zero in M0.0..M3.7
  RET_VAL:=MW100              // error code
  BLK   :=P#DB10.DBX0.0 BYTE 6800  // entire DB10
NOP 0

Common FILL error codes:

RET_VAL (hex) Meaning Field Fix
W#16#0000 No error
W#16#8090 BLK destination is a read-only DB Use instance DB or shared DB with write access
W#16#80A1 BVAL / BLK length error (odd byte count for WORD/DWORD boundary) Align length to byte multiple; BVAL must be a byte source
W#16#80B0 DB not loaded; symbolic DB is missing or array bounds changed without re-init Recompile and re-download HW config; run OB100 to re-init DBs
W#16#80B1 BLK is a local temp area (not allowed) Use a global DB or instance DB

For TIA Portal S7-1500, prefer FILL_BLK from the Extended Instructions > Memory operations folder. Its COUNT parameter is an UDInt and it returns ENO and a RET_VAL of type Int in the same coding scheme.

Board Counting Logic (A/B Quality Split)

The reference computes the number of A-quality boards as ceil(NumberOfBoards / 3) rounded up to the next even number, and the B-quality count as the remainder. The number of A-quality boards is forced to be even because the outer surfaces of the beam are mirrored. STL snippet from the field reference:

// Inputs: MD20 = NumberOfBoards (DINT)
// Locals: #NumberOfBoards (DINT), #ABoards (DINT), #BBoards (DINT)
      L     MD20
      T     #NumberOfBoards          // copy
      L     #NumberOfBoards
      DTR                              // convert to REAL
      L     3.000000e+000
      /R                               // NumberOfBoards / 3
      RND+                             // round up to next DINT
      T     #ABoards
      L     #ABoards
      L     L#2
      MOD                              // remainder mod 2
      ==0
      JC    m001                      // jump if already even
      L     #ABoards
      +     L#1
      T     #ABoards                  // round up to next even
m001: NOP   0
      L     #NumberOfBoards
      L     #ABoards
      -D
      T     #BBoards                  // BBoards = NumberOfBoards - ABoards

Validation for a 30-board beam: 30/3 = 10.0, RND+ = 10, 10 mod 2 = 0, ABoards = 10, BBoards = 20. For a 27-board beam: 27/3 = 9.0, ABoards = 9 (rounded to 10 for the even constraint), BBoards = 17. Warning: the even-rounding rule adds an extra A-board when the uncapped A-count is odd. The recipe must be checked for total board count consistency, or the FC should re-balance by adding a B-board to the centre and the QA step should be informed.

LIFO/FIFO Sequencing Algorithm

Given recipe parameters BeamsPerPress = B, BoardsPerCage = C, and total boards N:

  1. Compute CagesPerPress = ceil((B × boardsPerBeam) / C). Round up; the last cage may have fewer than C boards.
  2. Number the boards 1..N in recipe order (the order they will appear in the finished beam, surface to centre and back out).
  3. For production, the boards must come out of the cages in LIFO order within each cage and in FIFO order across cages. The board loaded last into Cage 1 is unloaded first; the cage unloaded first is the one loaded first.
  4. Map to production: the first board the operator must produce is the one that will be loaded last into Cage 1, which is board number min(C, N) in the recipe order. The second production board is the second-to-last loaded into Cage 1, etc.
  5. Once Cage 1 is exhausted in LIFO order, continue with Cage 2's LIFO order, then Cage 3, etc. Cage 1 was loaded first (FIFO across cages), so its LIFO boards are produced first; this is the LIFO/FIFO inversion that the problem requires.

The output list in DB30 is therefore an array of CagesPerPress sub-arrays, each of length C, read in cage-index order but with each cage's sub-array reversed. The reference field implementation writes this as a flat INT array where the lower 16 bits hold the beam instance and the upper 16 bits hold the original sequence number, so a single HMI tag can disambiguate source.

STL Implementation Details

The pointer-based STL code in the reference uses these key patterns:

Two-byte incrementing pointer loop

// LOOP with byte pointer, two bytes per board (ProductID + Quality/ID2)
      L     L#0
      T     #LoopIndex                // zero counter
      LAR1  P#DB20.DBX0.0             // byte pointer at DB20 start
LOOP_NEXT:
      L     #LoopIndex
      L     #NumberOfBoards           // loop count from recipe
      >=I
      JC    LOOP_END
      L     DBW [AR1,P#0.0]           // read current 2-byte value
      T     MW 200                    // staging register
      // ... do the A/B classification, write to DB11 ...
      +AR1  P#2.0                     // advance pointer by 2 bytes
      L     #LoopIndex
      +     L#1
      T     #LoopIndex
      JU    LOOP_NEXT
LOOP_END: NOP 0

Stack flip with two pointers

The OP's original idea (DB10 → DB11 by reversing sub-stacks of 5-15) translates cleanly to STL with two byte pointers and a length register:

// Source pointer: P#DB10.DBX0.0, advancing by 2
// Dest pointer:   P#DB11.DBX.0, decrementing by 2
// Loop count:     NumberOfBoards per stack
      L     #StackLength
NEXT:  L     DBW [AR1,P#0.0]           // read from DB10
      T     DBW [AR2,P#0.0]           // write to DB11
      +AR1  P#2.0                     // source advances up
      +AR2  P#-2.0                    // destination advances down
      L     #StackLength
      LOOP  NEXT                      // decrement ACCU1 and loop while > 0

On S7-1500, the equivalent is the MOVE_BLK_VARIANT with SRC_INDEX/DST_INDEX parameters and a COUNT of StackLength reversed in the destination, or simply a single FOR loop over a tag-indexed array. Avoid LOOP on S7-1500; it is still present but considered legacy, and the optimized-block compiler may not honour the AR1/AR2 register conventions of S7-300/400.

Recipe Management

The recipe header in DB20 must contain, at minimum:

Offset Name Type Notes
0.0 RecipeID DWORD Unique recipe number, written by HMI
4.0 BeamsPerPress INT 1..10
6.0 BoardsPerCage INT 5..20, derived from board thickness
8.0 CagesPerPress INT Computed = ceil((Σ boards in beam × BeamsPerPress) / BoardsPerCage)
10.0 Beam[1].NumBoards INT Total boards in beam 1
... ... ... 10 × (DINT + INT + INT + INT) for length/width/thickness/NumBoards
~50.0 BoardList ARRAY[1..400] OF INT Filled by FC2

The recipe download from the HMI should be a single transaction (PUT/BSEND on S7-400, or a TIA Portal recipe view on Comfort Panels / WinCC Unified). The first 40 bytes of the beam header carry the beam-level data, and the board list follows. The operator must be prevented from downloading a recipe whose CagesPerPress × BoardsPerCage < (Σ NumBoards); enforce this client-side in the HMI script and server-side in OB100 / startup with a check FC.

HMI Integration

With the UDT-array architecture, the HMI binds to symbolic paths:

  • System01.RecipeID for the active recipe display
  • System01.Cage[i].CageStatus.DoorClosed for door-open fault visualization
  • System01.Cage[i].Boards[j].Quality for the board-by-board quality grid
  • System01.ProductionList[k] for the operator's production sequence

A recipe-view tag list of 200-300 tags is typical for a 10-beam press with 20 cages. For WinCC Comfort/ Unified on a TP1500 or TP2200, plan for 500 ms refresh on the production-list array and 100 ms on the cage-status word. On Classic WinCC with S7-400, set the tag update to "On change" for status words to reduce WinCC load.

Performance Considerations

The reference implementation completes a full recipe-to-production-list pipeline in approximately 2.5 ms on an S7-315-2 PN/DP (6ES7315-2EH14-0AB0, FW V3.3). On an S7-1516-3 PN/DP (6ES7516-3AN02-0AB0, FW V2.8), the same logic runs in under 200 µs. A first-scan recipe load is a one-shot event; an OB1 call budget of 5 ms is achievable on the S7-300 and 1 ms on the S7-1500. If you need faster, do two things:

  1. Move the A/B classification arithmetic out of FC2 into a separate FC called only when BeamsPerPress changes.
  2. Use MOVE_BLK (S7-1500) or UBLKMOV (S7-300/400) to copy whole board arrays between cages rather than element-by-element moves.

Watch the OB1 scan time in the diagnostic buffer; if it exceeds 80% of the watchdog, drop the one-shot trigger into OB35 (cyclic interrupt, 100 ms typical) to spread the load.

Verification and Commissioning

  1. Force MD20 = 30 in OB1 (or write a watch table value). Confirm the ABoards = 10, BBoards = 20 outputs in the VAT.
  2. Set BeamsPerPress = 3, BoardsPerCage = 9, populate the beam header with 10 boards per beam. Set M24.0 = TRUE to trigger the pipeline.
  3. Verify in DB10 that Cage[1].Boards[1..9].SequenceNumber is filled, Cage[2].Boards[1..9].SequenceNumber is filled, etc., and that the outer boards of every beam carry the Start Of Beam / End Of Beam flag in Flags.
  4. Inspect DB30. The first production board listed should be the last board loaded into Cage 1 (sequence number 9 in this example), the second should be sequence 8, and so on, until the cage is exhausted; then the sequence continues with the LIFO list from Cage 2.
  5. On the HMI, open the cage status view and confirm that toggling DB10.Cage[2].CageStatus.DoorClosed raises the door-open alarm and that the symbolic tag System01.Cage[2].CageStatus.DoorClosed reads the same value.

Troubleshooting Matrix

Symptom Likely Cause Diagnostic Step Fix
FC1 returns W#16#80B0 after UDT change DB not re-initialized Monitor OB100 / startup Re-download HW config and trigger OB100
Production list in DB30 is all zeros M24.0 cleared before FC4 ran Watch table on FC4 call Move self-clear of trigger to end of OB1 after FC4 returns
Boards assigned to wrong cage Off-by-one in array index Compare Cage[i].Boards[C].Quality to expected Confirm array lower bound = 1; check < vs <= in fill loop
First board unloaded is wrong A-quality Stack-flip loop ran forward Inspect DB30[0] vs DB30[StackSize-1] Verify destination pointer decrements (+AR2 P#-2.0)
Loop never terminates Counter never decremented Use LOOP instruction with ACCU1 preloaded Replace manual counter with LOOP NEXT after L #Count
Symbolic tag returns quality code in HMI Array uses CHAR; HMI expects INT Check tag data type in WinCC / TIA Portal Use STRING[1] or a 2-byte INT with code 1=A, 2=B
Optimized block on S7-1500 rejects SFC21 SFC21 cannot write optimized areas Check DB properties > Optimized block access Use FILL_BLK or uncheck optimized access for that DB
Cage door fault not visible in HMI Symbolic-only access disabled PLC > Properties > Address priority Set to Symbolic on every FB/FC that touches the UDT

Field-Proven Caveats

  • The reference example hard-codes 9 boards per cage in FC3 even though the UDT supports 20. Always drive BoardsPerCage from the recipe; a hard-coded constant silently breaks when the operator runs a thinner-board recipe.
  • The reference uses absolute addressing with pointers (DBW references) inside FC1-FC4 to avoid confusing the OP about symbolic access. In production, rewrite the four FCs to use symbolic-only access except in the loop bodies that require [AR1,P#0.0] indirection.
  • Always include min/max checks in FC2. The reference explicitly warns that a wild value like 1,000,000 boards will crash the loop counters. Add an IF #NumberOfBoards > 400 OR #NumberOfBoards < 1 THEN ... alarm at the entry to FC2.
  • Reserve bits in the CageStatus word for Loaded, Unloaded, and Fault even if you do not use them at first commissioning. They will be needed within six months.
  • The "1/6 of the total boards is A-quality" rule is a common lamination convention; verify with the actual QA specification. Some lamination processes require A-quality on both outer faces and on the centre line, in which case the A-count is fixed regardless of total board count and the B-count is the variable.

Migration to TIA Portal and S7-1500

When porting from S7-300/400 to S7-1500, the following substitutions apply:

Classic S7-300/400 S7-1500 (TIA Portal) Notes
SFC20 BLKMOV / UBLKMOV MOVE_BLK / MOVE_BLK_VARIANT Handles optimized blocks
SFC21 FILL FILL_BLK Same semantics, optimised-block safe
AR1 / AR2 byte pointers TAG-indexed FOR / WHILE loops AR1/AR2 still available but not recommended
STL LOOP FOR ... TO ... DO ... END_FOR Strongly preferred on S7-1500
DB with symbolic + absolute access Optimized block with symbolic-only Remove all absolute DBW references
LAD/FBD with I/O fields SCL with structured tags Recommended for UDT-heavy code

For new projects on S7-1500, write the FC1-FC4 logic in SCL rather than STL. The symbolic-UDT-array pattern is the same, but SCL's FOR loop and tag-indexed array access (#Cage[#i].Boards[#j].Quality) eliminate the pointer-arithmetic pitfalls entirely. The STL patterns above remain valid on S7-1500 firmware V2.0+ but should be considered a maintenance liability for greenfield code.

Standards and Safety Notes

Safety: This program is a recipe-sequencing and material-tracking function. It is not a safety function. The cage door interlock, the press closure interlock, and the curing-oven over-temperature trip must be implemented in a separate F-CPU (S7-1500F, ET200SP F-modules, or S7-300F with F-I/O) and wired through PROFIsafe. Do not mix recipe data with safety I/O in the same DB.

For the drying-time and curing-time functions, the recipe specifies DesiredDryingTime in seconds as a DINT (max 2,147,483,647 s ≈ 68 years; well within the data type). The HMI should display the time as HH:MM:SS using a standard conversion block, not as raw seconds.

For traceability, the ProductionList should be archived to a CSV or SQL table on each recipe completion (via the HMI's logging task or a separate S7-comm connection to a historian). The Q-quality / B-quality split is a regulated value in many laminated-beam processes (EN 14374 for timber, EN 312 for particle board, EN 314 for plywood — verify against the actual product standard).

What is the minimum CPU class for a 20-cage × 20-board LIFO/FIFO sequencing program?

An S7-315-2 PN/DP (6ES7315-2EH14-0AB0) with firmware V3.3 and at least 256 KB work memory handles the reference implementation in ~2.5 ms. The symbolic UDT array of 20 cages × 20 boards × ~16 bytes per board needs ~6.4 KB of work memory, well within budget. For new projects, use an S7-1511-1 PN (6ES7511-1AK02-0AB0) or higher on TIA Portal V17+.

Can I use SFC21 FILL on optimized blocks in TIA Portal?

No. SFC21 FILL returns W#16#80A1 or W#16#80B1 on optimized blocks because the byte-granular pointer cannot address them. Use FILL_BLK from the Extended Instructions > Memory operations folder, which operates on tag-typed (optimized) memory natively and supports UDInt COUNT parameters.

How do I change the boards-per-cage value at runtime without recompiling?

Set the address priority to Symbolic, define BoardsPerCage as a VAR_INPUT of FC3 (or as a UDT_System tag), and have the HMI write the new value before triggering the one-shot. Do not use a hard-coded constant inside FC3; the reference example hard-codes 9 boards per cage purely for clarity, but in production this is a maintenance hazard.

Why does my loop not terminate when the counter is loaded into ACCU1?

The S7 LOOP instruction decrements ACCU1-L and jumps to the label if the result is non-zero. If you preloaded ACCU1 with the loop count but the loop runs infinitely, you almost certainly have a L instruction inside the loop body that re-loads ACCU1 with a different value, or you are incrementing rather than decrementing the counter manually before the LOOP jump. Insert a watch table on ACCU1-L at the LOOP instruction to verify the decrement.

How do I add a Start-Of-Beam / End-Of-Beam flag to each board?

Add a WORD Flags member to UDT_Board (see UDT_Board definition above). In FC3, set bit 0 (SOB) on the first board placed into a cage, bit 1 (EOB) on the last, and clear all other bits. With symbolic addressing, the HMI binds to System01.Cage[i].Boards[j].Flags.X0 (S7-1500) or to the equivalent bit-of-word syntax in WinCC.

Back to blog