Implementing FIFO Priority Queue for 7 PLC Backwash Requests

David Krause14 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

1. Application Overview

Seven rapid sand filters in a water treatment plant share a single backwash pump. Each filter must be backwashed once every 24 hours of cumulative service. When a filter's run-time timer expires, it raises a request bit; the controller must then sequence the backwash cycles so that the pump is always assigned to the filter that requested service first. If a second filter's 24-hour timer expires while the pump is busy with the first filter, that second filter's request must be queued and serviced only after the in-progress cycle finishes.

This is a classic request queue problem. The natural data structure is a First In, First Out (FIFO) buffer of depth 7, where each entry encodes the filter ID (1-7) of the request that has been waiting the longest. The controller pops the head of the queue when the backwash pump becomes available and clears the entry when the cycle completes.

The article provides a complete Function Block (FB) implementation in STEP 7 (S7-300 / S7-400) and an SCL port for S7-1500 / S7-1200 with TIA Portal. It is also applicable, with minor renames, to any controller that supports indexed arrays and an FB/instance-DB pattern.

2. Prerequisites

Before commissioning the queue logic, confirm the following:

  • PLC: SIMATIC S7-300 (CPU 314 or higher) or S7-1500 with firmware V2.0 or higher. S7-1200 with firmware V4.0 or higher also works; reduce the array length if you only have 7 entries.
  • Software: STEP 7 V5.5 SP2+ (classic) or TIA Portal V13 SP1+ (modern). The SCL examples below compile in both.
  • I/O: Seven digital input bits wired to the 24-hour timer-done contacts of each filter's run-time accumulator. One digital output for the backwash pump starter; seven outputs for filter isolation valves (V1-V7) routed through the same FB.
  • HMI: A 7-row status panel showing Pending / Running / Idle per filter, plus a queue-position indicator (1st-7th in line).
Safety note: The backwash pump is a single point of failure for the entire filter gallery. Hard-wired pump overload, low-flow, and high-pressure interlocks must remain in the safety chain. The PLC queue manages arbitration, not protection.

3. FIFO vs Priority Encoder: Choosing the Right Architecture

Two encoding strategies are commonly confused in this kind of application. They behave very differently.

Approach Ordering Rule Behaviour When Filter 7 Requests First Best Fit
Priority encoder Position-based; IN7 > IN6 > IN5 ... > IN1 Filter 7 always wins, regardless of arrival time Alarm triage, interrupt controllers, fixed-hierarchy arbitration
FIFO queue Time-based; oldest pending request wins Filter 7 waits its turn; the request that arrived first gets the pump Round-robin service, FCFS resource sharing, backwash scheduling

A priority encoder as defined in the priority encoder reference collapses N input bits to a small binary code where the highest-priority line always wins. That is the wrong semantic here: the application demands time-of-arrival fairness, not positional preference. Use a FIFO.

Some hybrid designs scan a small priority table first to reject stale requests and then fall through to a FIFO. That is overkill for seven filters but becomes attractive when the population grows past ~30.

4. Data Structure Design

The queue is stored in an array of seven WORDs inside the instance DB of FB FB_FilterQueue. Filter IDs are encoded as W#16#1 through W#16#7 in classic STEP 7 syntax (equivalent to WORD#1 ... WORD#7 in TIA Portal SCL). W#16#0 marks an empty slot.

Tag Type Initial Value Description
Queue[1..7] ARRAY[1..7] OF WORD W#16#0 Seven-slot FIFO storage
Head INT 1 Slot to be read next (dequeue index)
Tail INT 1 Slot to be written next (enqueue index)
Count INT 0 Number of pending requests
PumpBusy BOOL FALSE TRUE while backwash in progress
ActiveFilter WORD W#16#0 Filter ID currently being backwashed
ReqLatched[1..7] ARRAY[1..7] OF BOOL FALSE Edge-latched copy of request inputs

Count is the canonical empty/full indicator: Count = 0 means the queue is empty, Count = 7 means the queue is full. A new request is only accepted when Count < 7; if all seven filters are already waiting, additional requests are dropped and a QueueOverflow alarm is raised.

5. State Machine for Queue Management

The backwash sequencer operates as a small state machine driven by the FIFO.

  • S_IDLE: Pump is off, all isolation valves closed. Controller waits for Count > 0.
  • S_OPEN_VALVE: Read Queue[Head] into ActiveFilter. Open the matching isolation valve (V1-V7). Start the backwash pump starter with a soft-start ramp.
  • S_BACKWASH: Run pump for the configured backwash duration (typical 8-12 min). Monitor flow, turbidity, and pressure interlock.
  • S_CLOSE_VALVE: Stop pump, close isolation valve, run filter-to-waste for 30-60 s, return filter to service.
  • S_DEQUEUE: Shift the queue by one slot: Queue[i] := Queue[i+1] for i = 1..6, clear Queue[7], decrement Count, and return to S_IDLE.

A simple state variable State of type INT (0-4) plus a CASE structure in SCL is the cleanest implementation. Transitions are guarded by timer-done, interlock-ok, and Count-not-zero conditions.

6. STEP 7 Implementation - Function Block Interface

Create FB100 (TIA: FB "FilterQueue") with the following interface:

FUNCTION_BLOCK FB_FilterQueue
{ S7_Optimized_Access := 'TRUE' }  // TIA Portal only
VAR_INPUT
    ReqIn    : ARRAY[1..7] OF BOOL;   // live request bits from filter timers
    Enable   : BOOL;                   // master enable (auto / manual / service)
    CycleTime: TIME;                   // backwash duration, e.g. T#10m
    FTWTime  : TIME;                   // filter-to-waste, e.g. T#45s
END_VAR
VAR_OUTPUT
    PumpStart : BOOL;                  // to pump starter contactor
    ValveCmd  : ARRAY[1..7] OF BOOL;   // V1..V7 isolation valve commands
    ActiveID  : INT;                   // 0 = none, 1..7 = filter being washed
    QueueFull : BOOL;                  // overflow alarm
    Position  : ARRAY[1..7] OF INT;    // 0 = idle, 1..7 = queue position
END_VAR
VAR
    Queue    : ARRAY[1..7] OF WORD;    // FIFO storage
    Head     : INT := 1;
    Tail     : INT := 1;
    Count    : INT := 0;
    Latch    : ARRAY[1..7] OF BOOL;    // edge memory
    State    : INT;                    // 0..4
    PumpBusy : BOOL;
    T_BW     : TON;                    // backwash timer
    T_FTW    : TON;                    // filter-to-waste timer
END_VAR

Allocate an instance DB (DB100 in classic STEP 7, or a single-instance DB in TIA Portal) and call FB100 once in OB1 (cyclic) with the I/O wired in.

7. Program Code in Structured Text (SCL)

The body of FB100 in TIA Portal SCL:

// ---------- 1. Edge-latch incoming requests ----------
FOR i := 1 TO 7 DO
    IF ReqIn[i] AND NOT Latch[i] THEN
        // rising edge: enqueue filter ID i
        IF Count < 7 THEN
            Queue[Tail] := WORD#16#i;
            Tail := Tail MOD 7 + 1;     // wrap 1..7
            Count := Count + 1;
        ELSE
            QueueFull := TRUE;          // overflow alarm
        END_IF;
    END_IF;
    Latch[i] := ReqIn[i];
END_FOR;

// ---------- 2. Update position display ----------
FOR i := 1 TO 7 DO Position[i] := 0; END_FOR;
IF Count > 0 THEN
    FOR k := 0 TO Count - 1 DO
        // walk k slots from Head to find the k-th pending filter
        idx := ((Head - 1 + k) MOD 7) + 1;
        Position[WORD_TO_INT(Queue[idx])] := k + 1;
    END_FOR;
END_IF;

// ---------- 3. Backwash state machine ----------
CASE State OF
    0:  // S_IDLE
        PumpStart := FALSE;
        FOR i := 1 TO 7 DO ValveCmd[i] := FALSE; END_FOR;
        IF Enable AND (Count > 0) THEN
            ActiveID := WORD_TO_INT(Queue[Head]);
            ValveCmd[ActiveID] := TRUE;
            T_BW(IN := FALSE);                  // reset timer
            State := 2;                          // go to S_BACKWASH
        END_IF;

    1:  // S_OPEN_VALVE (optional pre-purge; not used in this example)
        State := 2;

    2:  // S_BACKWASH
        PumpStart := TRUE;
        T_BW(IN := TRUE, PT := CycleTime);
        IF T_BW.Q THEN
            PumpStart := FALSE;
            T_FTW(IN := FALSE);
            State := 3;
        END_IF;

    3:  // S_CLOSE_VALVE / filter-to-waste
        ValveCmd[ActiveID] := FALSE;
        T_FTW(IN := TRUE, PT := FTWTime);
        IF T_FTW.Q THEN
            T_FTW(IN := FALSE);
            State := 4;
        END_IF;

    4:  // S_DEQUEUE
        FOR i := 1 TO 6 DO
            Queue[i] := Queue[i+1];
        END_FOR;
        Queue[7] := WORD#16#0;
        Head := Head MOD 7 + 1;
        Count := Count - 1;
        ActiveID := 0;
        State := 0;
        PumpBusy := FALSE;
END_CASE;

The wrap expression (x MOD 7) + 1 keeps the head/tail pointer inside the array without conditional branching; this is preferred over IF-THEN chains in cyclic OB1 because it executes in constant time.

8. Ladder Logic Alternative

For crews that prefer ladder, the queue itself is awkward in LAD because array indexing is not native. The recommended approach is to:

  1. Use a DB of 7 consecutive MWs (e.g., DB100.DBW0..DBW12) as the queue storage.
  2. Use a counter block (e.g., S7 CTU) to track the count.
  3. Enqueue by moving the new ID into DBW[2 * (Tail - 1)] using indirect addressing: OPN DB100; L #Tail; SLW 1; T #DBW_Offset; L #NewID; T DBW[DBW_Offset]. This is the classic STEP 7 pointer-arithmetic pattern.
  4. Dequeue by using a SFC BLKMOV or a manual shift ladder network that copies DBW[n+2] -> DBW[n] for n = 0..5 and clears DBW[12].

For installations already migrated to TIA Portal, SCL is strongly preferred. The legacy ladder pattern is documented in the Siemens STEP 7 S7-300/400 Programming and Operating Manual.

9. Request Arbitration Logic

The trickiest part of the algorithm is preventing the same request from being enqueued twice. Three conditions can falsely double-enqueue:

  • The request bit is still TRUE when the cycle starts (filter re-arms itself immediately).
  • The request bit is held latched in the filter's PLC while the timer is reset.
  • Scan jitter: ReqIn and the dequeue happen in the same OB1 cycle.

Two safeguards are recommended:

  1. Use rising-edge detection on ReqIn before insertion (the Latch array in the code above). This guarantees that a steady TRUE request only enters the queue once.
  2. Before dequeuing, verify the request is still valid. If the corresponding ReqIn[ActiveID] has already cleared (e.g., operator reset the filter), still complete the in-progress cycle to avoid drying the filter; on completion, skip dequeue and just clear ActiveID and return to S_IDLE.
Edge case: If the operator manually resets all request bits while items are in the queue, the queue is not cleared automatically. Add a ClearAll input on the FB that resets Count, Head, Tail, and all Queue[i] to 0. Use it with care, and require a separate confirmation bit in the HMI.

10. Verification & Commissioning

Commission the FB on a simulator or with the pump locked out, then verify with a structured test plan.

Test Procedure Expected Result
T1 - Single request Set ReqIn[3] = 1, Enable = 1 ValveCmd[3] = 1, PumpStart = 1 after delay, ActiveID = 3, Position[3] = 1, Position[others] = 0
T2 - Burst of three Rising edge on ReqIn[5], ReqIn[2], ReqIn[7] in that order over 1 s Service order is 5 -> 2 -> 7. Position display updates each cycle
T3 - Request during cycle Start cycle on filter 1; 3 min in, raise ReqIn[4] Filter 1 completes; filter 4 is next; filter 4 not started before filter 1 finishes
T4 - Full queue Rising edge on all 7 ReqIn bits within 100 ms All 7 IDs in queue, QueueFull stays FALSE, Position[1..7] = 1..7 in arrival order
T5 - Overflow After T4, raise a 2nd pulse on ReqIn[3] QueueFull := TRUE, count stays at 7, second ReqIn[3] ignored (edge-latched)
T6 - ClearAll With queue full, pulse ClearAll := TRUE for 1 scan Count := 0, all Queue[i] := 0, PumpStart := 0, QueueFull := FALSE

Use a STEP 7 watch table (or TIA "Monitor & Force") on the instance DB to confirm Head, Tail, Count, and Queue[1..7] in real time. Force ReqIn bits manually to step through the test plan without disturbing the live filter timers.

11. Edge Cases & Fault Handling

Condition Detection Response
Pump does not start in S_OPEN_VALVE PumpStart = TRUE but starter feedback FALSE for > 5 s Abort cycle, return to S_IDLE, raise PumpFault alarm, leave queue intact
Valve does not open ValveCmd[k] = TRUE but limit-switch feedback FALSE for > 10 s Stop pump, raise ValveFault[k], skip filter on next attempt only if operator clears
Pressure interlock trips High-pressure input TRUE Immediate pump stop, isolate, dequeue, raise alarm, do not auto-retry
Power loss / CPU restart OB100 startup Initialize Count := 0, Head := 1, Tail := 1, all Queue := W#16#0. Treat restart as "no history".
Operating rule: after a CPU restart, the filter station typically runs a manual backwash of every filter before resuming auto service.
More than 7 filters later Static analysis Increase array length to 16 or 32 and update the wrap modulus. Keep ID encoding to one byte per slot for compactness.
Master enable drops mid-cycle Enable = FALSE during S_BACKWASH Continue to a safe stop: finish timer, close valve, dequeue, do not start a new cycle. This prevents filter bed collapse.

For a deep dive on CPU restart behaviour and the role of OB100, see the S7-300 CPU 31x/31xC Operating Manual. For S7-1500, the equivalent reference is the S7-1500 System Manual.

12. Tuning the Backwash Cycle

Field-proven starting points for typical rapid sand filters (verify against the filter manufacturer's O&M manual):

Phase Duration Notes
Air scour (optional) 1-2 min Only if the filter is plumbed for air scour; bypass in this example
Backwash (water) 8-12 min Set CycleTime accordingly. Stop on turbidity drop or fixed time
Filter-to-waste 30-60 s Discard the first filter efflux to drain until turbidity is within spec
Settling / return to service 5-15 s Built into the state transition from S_CLOSE_VALVE to S_IDLE

For a more sophisticated sequencer, add turbidity and differential-pressure inputs to terminate S_BACKWASH early. The basic FIFO mechanism is unchanged; only the exit condition of S_BACKWASH becomes T_BW.Q OR TurbidityOK instead of just T_BW.Q.

13. Scaling Beyond 7 Filters

When the number of filters grows to ~15 or more, the O(n) shift used in S_DEQUEUE becomes wasteful on slow scan cycles. Two refactor paths:

  • Ring buffer with head/tail read: keep the same array, but service the head slot directly and never shift. Tracks absolute service count vs relative service count. The HMI must walk from Head for k slots to display positions.
  • Move-to-front heuristic: the FB can be promoted to a function block that swaps head/tail pairs on read; the dequeue becomes a constant-time write of W#16#0 to the head slot, not a 6-step shift.

For a baseline 7-filter install, the simple shift is fine. Revisit the data structure if cycle time or filter count grows by an order of magnitude.

14. Summary

A 7-slot FIFO of WORD values, indexed by a 1..7 head/tail pair and guarded by a count, is the smallest correct implementation of the backwash request queue. The Function Block FB_FilterQueue presented here integrates:

  • Rising-edge latched request acceptance with overflow protection.
  • Live queue-position calculation for the HMI.
  • A 5-state backwash sequencer (IDLE, OPEN_VALVE, BACKWASH, CLOSE_VALVE, DEQUEUE).
  • Fault handling for pump, valve, and pressure interlock.
  • Deterministic restart behaviour in OB100.

Drop the FB into OB1 of any S7-300/S7-400/S7-1500 project, wire seven digital inputs and seven valve outputs plus the pump starter, and the station will round-robin its filters through the shared pump without operator intervention.

Why is FIFO better than a priority encoder for 7 filter backwash requests?

A priority encoder always favours the highest-indexed input, so filter 7 would jump the queue every cycle, starving filter 1. The application requires first-come, first-served fairness, which a 7-slot FIFO provides. Use a priority encoder only when positional priority (e.g., alarm triage) is the intended semantic; see the priority encoder definition for the contrast.

How many filter IDs can I store in a single queue slot?

One ID per slot when stored as a WORD (W#16#1..W#16#7 for 7 filters, up to W#16#FF for 255 filters). Bumping the slot type to DWORD (W#32#) leaves room for status bits if you later need per-slot flags, but for 7 filters WORD is sufficient and saves instance-DB memory.

What happens if two filter requests arrive in the same OB1 cycle?

The SCL code processes them in the order of the FOR loop (1 to 7), so filter 1 wins the tie-break over filter 2, etc. If true arrival-time ordering is critical, add a 1 ms cyclic interrupt OB (OB35 on S7-300, or a configured time-of-day interrupt) and read the request bits there; the first filter seen TRUE in OB35 is the highest-priority one.

Can the queue survive a CPU restart without losing requests?

Not in the implementation shown: OB100 clears the queue because the runtime history is lost. If the application needs restart-survival, mark the instance DB as retentive (S7-300: set the DB to "Non-retain" = FALSE; TIA Portal: set the relevant tags' "Retain" attribute). Note that retentive queue data is not a substitute for restarting the backwash sequence from a known state on power loss.

Is there a built-in Siemens FB that implements FIFO?

STEP 7 ships the standard library blocks FC84 "ATT" (insert into FIFO) and FC85 "FIFO" (remove from FIFO) for S7-300/400. TIA Portal no longer ships these as a separate library; instead, the same pattern is implemented in user SCL as shown above, or you can wrap the classic FC84/FC85 calls inside a ported FB. See the STEP 7 Standard Library reference for the legacy FCs.

Back to blog