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:
-
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.0in the example) that the program clears itself. -
FC1 - Clear: Calls
SFC21 FILLto zero the prior recipe's data in DB10 and DB30. Source is a 4-byte zero constant atP#M0.0 BYTE 4with BVAL pointed at the same constant. Always re-issue the FILL with the actual destination pointer; a staleDBNOis the most common cause ofW#16#80B0/W#16#80A1errors at first scan after power-on. -
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. -
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
BoardsPerCagevalue. Produces a mirror of the live production state in DB10. - 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:
- Compute
CagesPerPress = ceil((B × boardsPerBeam) / C). Round up; the last cage may have fewer thanCboards. - Number the boards 1..N in recipe order (the order they will appear in the finished beam, surface to centre and back out).
- 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.
- 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. - 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.RecipeIDfor the active recipe display -
System01.Cage[i].CageStatus.DoorClosedfor door-open fault visualization -
System01.Cage[i].Boards[j].Qualityfor 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:
- Move the A/B classification arithmetic out of FC2 into a separate FC called only when
BeamsPerPresschanges. - Use
MOVE_BLK(S7-1500) orUBLKMOV(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
- Force
MD20 = 30in OB1 (or write a watch table value). Confirm theABoards= 10,BBoards= 20 outputs in the VAT. - Set
BeamsPerPress = 3,BoardsPerCage = 9, populate the beam header with 10 boards per beam. SetM24.0 = TRUEto trigger the pipeline. - Verify in DB10 that
Cage[1].Boards[1..9].SequenceNumberis filled,Cage[2].Boards[1..9].SequenceNumberis filled, etc., and that the outer boards of every beam carry the Start Of Beam / End Of Beam flag inFlags. - 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.
- On the HMI, open the cage status view and confirm that toggling
DB10.Cage[2].CageStatus.DoorClosedraises the door-open alarm and that the symbolic tagSystem01.Cage[2].CageStatus.DoorClosedreads 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
9boards per cage in FC3 even though the UDT supports 20. Always driveBoardsPerCagefrom 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 ... alarmat the entry to FC2. - Reserve bits in the
CageStatusword 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
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.