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).
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]intoActiveFilter. 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, clearQueue[7], decrementCount, 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:
- Use a DB of 7 consecutive MWs (e.g., DB100.DBW0..DBW12) as the queue storage.
- Use a counter block (e.g., S7
CTU) to track the count. - 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. - 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:
- Use rising-edge detection on
ReqInbefore insertion (theLatcharray in the code above). This guarantees that a steady TRUE request only enters the queue once. - 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 clearActiveIDand return to S_IDLE.
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.