Implementing FIFO Tank Fill Sequencing on Siemens S7 PLCs
Application: Three process tanks must be refilled in the exact order in which they were emptied, with a hard interlock that only one tank may be filled or emptied at any given moment. The FIFO (First-In-First-Out) queue is the natural algorithmic fit, and on the Siemens S7-1200 and S7-1500 controller families the built-in Att/Detach instructions handle the queue mechanics in a handful of network rungs.
Problem Definition
The mechanical scope of the application is small, but the sequencing logic is non-trivial:
- Three process tanks (T1, T2, T3) share a single fill pump and a single discharge pump.
- Only one tank can be filling or emptying at a time (mutual exclusion).
- When a tank reports
empty, its identifier is enqueued. - The fill controller dequeues the oldest identifier and starts the corresponding fill cycle.
- Discharge requests may arrive in any order (e.g., 2 → 3 → 1); the refill order must mirror that exactly.
The order is critical because downstream process consumers assume a deterministic recovery sequence (heat exchanger pre-heat, residence time, pH correction residence). A LIFO (Last-In-First-Out) or random refill violates residence guarantees and may exceed downstream concentration limits.
Why a FIFO Queue Beats Indirect Addressing
A common first attempt uses indirect addressing: the empty-event pointer is stored in one DB and dereferenced to access a second DB holding per-tank tags. The pointer arithmetic is feasible but introduces four recurring failure modes:
| Failure Mode | Indirect Addressing | FIFO Queue |
|---|---|---|
| Index wrap-around on overflow | Must track bounds manually | Handled by Att/Detach
|
| Duplicate entries when sensor bounces | Requires de-bounce logic | De-bounce blocks enqueue, not the queue itself |
| Pointer corruption on power cycle | Must be retentive and re-validated | Queue DB declared RETAIN handles retention natively |
| Out-of-sequence service after restart | Manual replay logic | Queue content is the source of truth |
For three tanks, indirect addressing is over-engineered. The FIFO data structure has the same semantics as the requirement statement: "first tank emptied, first tank refilled."
Prerequisites
- Siemens S7-1200 (CPU 1214C/DC/DC or higher, firmware 4.2 or later) or S7-1500 (CPU 1511-1 PN or higher, firmware 2.0 or later).
- TIA Portal V16 or later with STEP 7 Professional (S7-1500) or STEP 7 Basic (S7-1200).
- For S7-300/400 retrofits: SIMATIC Manager V5.5 SP2 with STEP 7 V5.5 and at least one of SCL or STL editors.
- HMI panel (KTP700 Basic or higher) optional but recommended for sequence visualization.
- Three level-switch inputs (LS1_EMPTY, LS2_EMPTY, LS3_EMPTY) wired to digital inputs, plus three full-level switches (LS1_FULL, LS2_FULL, LS3_FULL) for interlock feedback.
Att/Detach instructions are not available. Upgrade firmware before commissioning, or fall back to the manual array-based FIFO shown later in this article.Data Block Architecture
Two global data blocks hold the application state. The queue is the source of truth; the tank-status block mirrors field I/O for the HMI.
| Symbol | DB | Type | Purpose |
|---|---|---|---|
tankQueue |
DB10 | Array[0..9] of INT | FIFO buffer of tank identifiers (1, 2, or 3). Max depth 10 covers any practical burst. |
tankStatus |
DB11 | Struct | Per-tank state, current command, level flags, and timer accumulators. |
queueCount |
DB10 | INT | Number of valid entries (managed automatically by Att/Detach). |
currentTank |
MW20 | INT | Tank currently being serviced (0 = idle, 1/2/3 = T1/T2/T3). |
The tankStatus structure layout:
TYPE "tankStatusType" :
STRUCT
emptyRequest : ARRAY[1..3] OF BOOL; // rising-edge of EMPTY switch
fullReached : ARRAY[1..3] OF BOOL; // rising-edge of FULL switch
fillActive : ARRAY[1..3] OF BOOL; // pump currently filling this tank
emptyActive : ARRAY[1..3] OF BOOL; // pump currently emptying this tank
fillTime_s : ARRAY[1..3] OF INT; // last cycle fill time, seconds
END_STRUCT;
END_TYPE
Declare the queue DB as non-optimized (or set the "Accessible from HMI" attribute) so that the Att/Detach instructions can address it as a VARIANT. Mark the queue array as RETAIN so the order is preserved across power cycles.
Implementing the FIFO with Att/Detach on S7-1200/1500
Siemens provides two instructions under Extended Instructions → FIFO/LIFO that do the work:
| Instruction | Operation | Inputs | Output |
|---|---|---|---|
Att (Attach) |
Append entry to FIFO |
VARIABLE, FIFO_DB
|
POSITION (current tail index), ENO, ERROR
|
Detach (Detach) |
Remove oldest entry from FIFO | FIFO_DB |
VARIABLE (dequeued value), POSITION (new head index), ENO, ERROR
|
The instructions operate on a DB whose first byte is the queue management word and whose payload begins at offset 4. They manage a head pointer, tail pointer, and count automatically. You must not write to the DB outside of Att/Detach, otherwise the internal pointers desync.
Enqueue Logic (OB1 / Main)
// Ladder logic summary (FC100 "enqueueEmpty")
// Rung 1: detect rising edge of empty switch for each tank
A "tankStatus".emptyRequest[1]
FP "edge_T1_empty"
= #tank1JustEmptied
// Repeat for tank 2 and tank 3 with #tank2JustEmptied, #tank3JustEmptied
// Rung 2: de-bounce - only enqueue if no tank is currently being filled
A #tank1JustEmptied
AN "tankStatus".fillActive[1]
AN "tankStatus".fillActive[2]
AN "tankStatus".fillActive[3]
JCN NO1
CALL "Att"
VARIABLE := 1
FIFO_DB := "tankQueue"
POSITION := "tankQueue".tail
NO1: NOP 0
The same pattern is repeated for tanks 2 and 3. If two empty events arrive within one OB1 cycle, only the first is enqueued because JCN short-circuits the call; this preserves order and prevents duplicate enqueue.
Dequeue and Start Fill
// FC101 "dequeueAndFill" - runs when system is idle
A "queueCount" > 0
AN "tankStatus".fillActive[1]
AN "tankStatus".fillActive[2]
AN "tankStatus".fillActive[3]
JCN NOFILL
CALL "Detach"
FIFO_DB := "tankQueue"
VARIABLE := #nextTankId
POSITION := "tankQueue".head
// Start the corresponding pump output
L #nextTankId
L 1
-I
SLD 3
LAR1
A "DB_MotorCmd".cmd[AR1,P#0.0] // indirect on the dequeued index
S "DB_MotorCmd".cmd[AR1,P#0.0]
// Mark tank as fill-active
L #nextTankId
L 1
-I
SLD 3
LAR1
S "tankStatus".fillActive[AR1,P#0.0]
NOFILL: NOP 0
The Detach instruction removes the oldest entry (first to empty) and writes its value into #nextTankId. The fill-active flag prevents double-start while the pump is running.
Fill Complete Detection
// FC102 "fillCompleteMonitor"
// For each tank: when full switch closes, drop the fill-active bit and reset pump
A "tankStatus".fullReached[1]
S "tankStatus".fillDone[1]
R "tankStatus".fillActive[1]
R "DB_MotorCmd".cmd[0] // T1 pump off
// Repeat for tank 2 (offset 1 byte) and tank 3 (offset 2 bytes)
Manual FIFO Implementation for S7-300/400 and Classic STEP 7
For plants still running S7-300/400 controllers, or for S7-1200 firmware below 4.0, the queue must be managed manually using a pointer-based ring buffer. The algorithm:
- Reserve an array
QUEUE[0..9]of INT in a DB. - Maintain
HEAD,TAIL, andCOUNTas separate tags (DBW20, DBW22, DBW24). - Enqueue writes
QUEUE[TAIL]and incrementsTAIL := (TAIL+1) MOD 10. - Dequeue reads
QUEUE[HEAD]and incrementsHEAD := (HEAD+1) MOD 10. - Increment/decrement
COUNTon each operation; reject enqueue when COUNT=10 and reject dequeue when COUNT=0.
Sample SCL (works on S7-300/400 with S7-SCL V5.3 or S7-1200/1500):
FUNCTION_BLOCK "manualFifo"
VAR
queue : ARRAY[0..9] OF INT;
head : INT := 0;
tail : INT := 0;
count : INT := 0;
END_VAR
FUNCTION "enqueue" : BOOL
VAR_INPUT id : INT; END_VAR
BEGIN
IF count = 10 THEN RETURN := FALSE; END_IF;
queue[tail] := id;
tail := (tail + 1) MOD 10;
count := count + 1;
RETURN := TRUE;
END_FUNCTION
FUNCTION "dequeue" : BOOL
VAR_OUTPUT id : INT; END_VAR
BEGIN
IF count = 0 THEN RETURN := FALSE; END_IF;
id := queue[head];
head := (head + 1) MOD 10;
count := count - 1;
RETURN := TRUE;
END_FUNCTION
END_FUNCTION_BLOCK
Declare the head, tail, and count tags as RETAIN so the queue survives a power cycle. Without retentive behavior, a brown-out would invalidate the ordering guarantee.
State Machine for Tank Sequencing
The fill controller is best implemented as a four-state machine. State transitions are driven by the queue content and field feedback:
| State Code | Name | Entry Condition | Exit Condition | Action |
|---|---|---|---|---|
| 0 | IDLE | Power-on, queueCount = 0, no tank fill/empty active |
Queue has at least one entry | Detach, transition to FILLING |
| 10 | FILLING | Just dequeued a tank ID | Tank-full input transitions high | Stop pump, transition to SETTLING |
| 20 | SETTLING | Fill pump stopped, settling timer running | Settling timer elapsed (typ. 5 s) | Transition to IDLE |
| 99 | FAULT | Fill timeout exceeded OR level-switch mismatch | Operator acknowledge via HMI | Stop all pumps, latch alarm |
Add a watchdog timer (e.g., 600 s for a typical 50 m³ tank at 100 L/min fill rate) that triggers FAULT if a full-level signal is not received. This catches failed pumps, blocked valves, and defective level switches.
HMI Tag Mapping
On a WinCC Comfort/Advanced panel, expose the following tags for operator visibility:
| HMI Tag | PLC Address | Display Element | Purpose |
|---|---|---|---|
queueCount |
DB10.DBW0 | Numeric output | Number of pending tanks |
queueHead |
DB10.DBW2 | Numeric output | Pointer to oldest entry (debug) |
queueTail |
DB10.DBW4 | Numeric output | Pointer to next free slot (debug) |
currentTank |
MW20 | Symbolic I/O field | Currently serviced tank |
tankStatus |
DB11 entire struct | 3x status indicators | Per-tank fill state, empty request |
Enable Audit Trail logging on the dequeue event so that the historical sequence is recorded. This is invaluable for post-trip root-cause analysis.
Verification and Commissioning Procedure
-
Static test (simulation): Load the project in PLCSIM or PLCSIM Advanced, force
LS1_EMPTYON, verifyqueueCountincrements to 1 andqueue[0] = 1. -
Order-of-service test: Force empty events in the sequence 2 → 3 → 1, then force
queueCount > 0. Verify the fill-active sequence runs 2, then 3, then 1. -
Power-cycle test: Energize the sequence to mid-fill, power down for 30 s, power up. Verify
queueCount,head, andtailare intact and the next fill resumes the correct tank. - Full-level test: With the pump running, force the full-level input. Verify the pump stops within 200 ms and the state machine transitions FILLING → SETTLING → IDLE.
- Watchdog test: Disconnect the full-level input. Verify the watchdog trips to FAULT after the configured timeout (default 600 s).
-
Mutual-exclusion test: Attempt to force
fillActive[2]whilefillActive[1]is true. Verify the second fill is rejected (no double-pump start). - Burst test: Empty all three tanks within 10 s. Verify the queue accepts three entries, drains them in order, and that no entries are lost.
fillActive without confirming the corresponding level switches are functional.Troubleshooting Matrix
| Symptom | Likely Cause | Diagnostic Step | Corrective Action |
|---|---|---|---|
| Pump never starts after empty event | Queue full (count = 10) or DB not retentive | Monitor queueCount online; check DB attributes |
Clear queue via HMI after root-cause investigation; set RETAIN |
| Tanks refilled in wrong order | Detach reading wrong head pointer; non-optimized DB mismatch | Watch head and tail in online monitor |
Verify DB layout, remove any user writes to the queue DB |
| Duplicate tank ID in queue | Level-switch contact bounce | Monitor emptyRequest with 100 ms sampling |
Add 2 s on-delay filter in input conditioning |
| Queue empty after power-up | Queue DB not marked RETAIN | Check DB properties → Retain attribute | Re-download project with RETAIN set; acknowledge that the prior order is lost |
| Att/Detach ENO = FALSE | Queue overflow (Att) or underflow (Detach) | Read STATUS output word |
Status = 1: overflow, clear queue; Status = 2: underflow, ignore dequeue |
| Fill never completes | Full-level switch wired normally-closed but logic expects NO | Force full-level input; monitor fullReached
|
Invert logic or swap contact type |
| Watchdog trips immediately | Timer preset set to 0 in DB | Inspect faultWatchdog_s in OB1 static area |
Load preset (e.g., 600 s) and re-download |
Performance and Sizing Notes
For an S7-1214C running OB1 at 10 ms, the FIFO operations add <0.3 ms of scan time per enqueue or dequeue. The state machine adds another 0.5 ms. Total cycle time remains well within the 10 ms budget. On an S7-1500 CPU 1511, the same logic runs in <0.05 ms. Memory footprint: ~120 bytes of DB plus ~600 bytes of FC code.
If the application expands to more than three tanks (e.g., 10 or 20 tanks), the queue approach scales linearly. Indirect addressing, by contrast, requires re-validation of pointer bounds and duplicate-entry checks at each tank. For plants with 50+ tanks, replace the simple queue with a priority queue or a round-robin scheduler to balance wear on the pumps.
What Siemens TIA Portal version is required for the Att/Detach FIFO instructions?
Att and Detach are available in TIA Portal V13 SP1 or later, but the S7-1200 CPU firmware must be 4.2 or higher. On S7-1500, firmware 2.0 or higher is required. Older firmware versions must use the manual array-based FIFO described in the S7-300/400 section.
How do I preserve the FIFO order across a power cycle?
Mark the queue data block as RETAIN in the DB properties. On S7-1200/1500 this preserves the head pointer, tail pointer, count, and array contents in the controller's retentive memory. On S7-300/400, declare head, tail, and count in a RETAIN DB and the array in the same DB.
Can two tanks empty at exactly the same moment and still preserve order?
Yes, but you must sequence the enqueue. Place a single FC upstream of Att that latches the two requests and calls Att for the lower tank ID first. Alternatively, accept that OB1 execution order determines the sequence and verify the determinism in the HMI audit trail.
What happens if a tank fills while the queue contains its own ID again?
This indicates a level-switch failure or a logic bug. The watchdog timer should trip the FAULT state before the pump restarts. Add a per-tank minimum-rest interval (e.g., 30 s) to prevent thermal cycling of the pump motor.
How do I migrate this code from S7-300 to S7-1500?
Use the TIA Portal migration tool (Project → Migrate project). Manually rewrite the manual-FIFO SCL block to use Att/Detach because the S7-1500 firmware supports them natively. Re-test the power-cycle retention on the new hardware before live commissioning.