Implementing FIFO Tank Fill Sequencing on Siemens S7 PLCs

David Krause11 min read
HMI ProgrammingSiemensTutorial / 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

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.
Note: On S7-1200 firmware 4.0 and earlier, the 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:

  1. Reserve an array QUEUE[0..9] of INT in a DB.
  2. Maintain HEAD, TAIL, and COUNT as separate tags (DBW20, DBW22, DBW24).
  3. Enqueue writes QUEUE[TAIL] and increments TAIL := (TAIL+1) MOD 10.
  4. Dequeue reads QUEUE[HEAD] and increments HEAD := (HEAD+1) MOD 10.
  5. Increment/decrement COUNT on 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

  1. Static test (simulation): Load the project in PLCSIM or PLCSIM Advanced, force LS1_EMPTY ON, verify queueCount increments to 1 and queue[0] = 1.
  2. 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.
  3. Power-cycle test: Energize the sequence to mid-fill, power down for 30 s, power up. Verify queueCount, head, and tail are intact and the next fill resumes the correct tank.
  4. 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.
  5. Watchdog test: Disconnect the full-level input. Verify the watchdog trips to FAULT after the configured timeout (default 600 s).
  6. Mutual-exclusion test: Attempt to force fillActive[2] while fillActive[1] is true. Verify the second fill is rejected (no double-pump start).
  7. 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.
Safety warning: All test steps that energize a real pump must be performed with the pump isolated and the discharge line open to a safe container. Never force 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.

Back to blog