Overview
A rolling buffer is the canonical pattern for storing the "last N events" on a PLC: every new measurement is pushed into the buffer while the oldest measurement is discarded. On Siemens SIMATIC controllers this pattern is implemented inside a Data Block (DB) because global DBs survive every cycle, retain their contents across stop/start transitions, and are addressable from every OB, FB, and FC in the program. The two storage shapes most often used are a fixed-element DB (one named variable per slot) and an ARRAY of a structured type; both are supported on S7-300/S7-400 in STEP 7 V5.x and on S7-1200/S7-1500 in TIA Portal.
This article covers four field-proven implementations of a 10-element rolling buffer for time-stamped event intervals:
- Direct L/T shift in Statement List (STL) for S7-300/S7-400.
- Indexed
ARRAYaccess withFORloop in SCL for S7-1200/S7-1500. - Ring buffer with a wrapping pointer, eliminating the data move entirely.
- Block move with the system function
SFC20 BLKMOVas a portable alternative.
Each method is annotated with the exact instructions, operand conventions, optimized-block caveats, and verification steps required for production deployment. The reference architecture is shown below.
Prerequisites and Hardware Compatibility
| Platform | Programming Environment | Language | Block Type | Optimized Access |
|---|---|---|---|---|
| S7-300 / S7-400 | STEP 7 V5.5 / V5.6 | STL, LAD, FBD, SCL | Shared DB (DB1..DB65535) | Not applicable (standard) |
| S7-1200 (CPU 1211..1215) | TIA Portal V13+ | LAD, FBD, SCL | Global DB (optimized by default) | Yes – indexed access limited* |
| S7-1500 (CPU 1511..1518, ET200SP CPU) | TIA Portal V15+ | LAD, FBD, SCL, GRAPH | Global DB / Instance DB | Yes – use ARRAY[*] or AT view |
DB.Array[i] with a variable index is not permitted on optimized ARRAYs. Either disable optimization in the DB properties, declare the ARRAY as ARRAY[*] with AT overlay on a non-optimized area, or use a multi-instance DB on a non-optimized FB. See the Siemens "Programming and Operating Manual – S7-1200 Programmable Controller" and "S7-1500 Automation System" manuals for the exact rules.Required STEP 7 / TIA Portal configuration before writing code:
- Create the target DB. For the STL path name it
DB100 "EventLog"(any free number). - Inside the DB, declare 10 variables of type
TIME(preferred) orDINT(for milliseconds). Name themMeasurement1throughMeasurement10, or declare oneARRAY[1..10] OF TIME. - Set a retentive bit so the buffer is not cleared on STOP→RUN. Mark
Measurement1..Measurement10as retentive in the DB properties; on S7-1500 use the "Retain" attribute on the ARRAY element. - Create an FB (e.g.
FB50 "EventLogShifter") with anINof typeTIMEnamedNewValue, anIN_OUTBOOLnamedTrigger(rising-edge detected inside the FB), and aSTATBOOLnamedTrigMem. - Call
FB50inOB1(cyclic) with its instance DBDB50and pass the shared DBDB100as anIN_OUTblock parameter.
Method 1 – Direct Shift with STL (S7-300 / S7-400)
Statement List is the most concise way to express the shift on the classic S7-300 and S7-400 controllers because the L (Load) and T (Transfer) instructions operate directly on accumulators without symbolic resolution overhead. The cardinal rule is to copy from the bottom of the buffer to the top so that you never overwrite a value before it has been read.
STL Source for FB50 "EventLogShifter"
FUNCTION_BLOCK FB50
VAR
TrigMem : BOOL; // Edge memory
i : INT; // Scratch (only needed in Method 2)
END_VAR
BEGIN
NETWORK 1 // Edge detection on Trigger
A #Trigger;
AN #TrigMem;
= #tempEdge; // Internal BOOL tempEdge (STAT)
A #Trigger;
= #TrigMem;
NETWORK 2 // Shift older values down (descending index)
A #tempEdge;
BEC; // Skip entire block if no new event
L DB100.DBW 18; // Measurement9 (TIME = 4 bytes; offset 18)
T DB100.DBW 22; // Measurement10
L DB100.DBW 14; // Measurement8
T DB100.DBW 18; // → Measurement9
L DB100.DBW 10; // Measurement7
T DB100.DBW 14; // → Measurement8
L DB100.DBW 6; // Measurement6
T DB100.DBW 10; // → Measurement7
L DB100.DBW 2; // Measurement5
T DB100.DBW 6; // → Measurement6
L DB100.DBW 30; // Measurement4
T DB100.DBW 2; // → Measurement5
L DB100.DBW 26; // Measurement3
T DB100.DBW 30; // → Measurement4
L DB100.DBW 22; // Measurement2
T DB100.DBW 26; // → Measurement3
L DB100.DBW 18; // Measurement1
T DB100.DBW 22; // → Measurement2
NETWORK 3 // Insert new value at slot 1
L #NewValue;
T DB100.DBW 18; // → Measurement1
END_FUNCTION_BLOCK
L Measurement1 / T Measurement2 before L Measurement2 / T Measurement3, the value of Measurement1 propagates upward in one scan and all original data is destroyed. Always transfer the value that you no longer need into the slot that is about to be overwritten, starting from the highest slot.The offsets in the listing assume each TIME occupies 4 bytes and Measurement1 starts at byte offset 14 inside DB100. Use the STEP 7 "Address Priority" or "Monitor/Modify" tool to confirm offsets after symbolic declaration, or compile with symbolic references only:
L "EventLog".Measurement9;
T "EventLog".Measurement10;
...
L "EventLog".Measurement1;
T "EventLog".Measurement2;
L #NewValue;
T "EventLog".Measurement1;
Time-Base and Range
| Type | Bytes | Range | Resolution |
|---|---|---|---|
| TIME | 4 | T#-24d20h31m23s647ms .. T#+24d20h31m23s647ms | 1 ms |
| DINT (ms) | 4 | -2,147,483,648 .. +2,147,483,647 ms ≈ ±24.8 d | 1 ms |
| REAL (seconds) | 4 | ±3.4e38 | IEEE-754 single |
For an "elapsed time between two events" use case the maximum expected interval must be below the type range. A 24-day span is generally fine; for longer buffers, switch to TIME in two DINT words (high/low) or store a timestamp pair.
Method 2 – Indexed ARRAY Access with SCL (S7-1200 / S7-1500)
On S7-1200 and S7-1500 the recommended pattern is one ARRAY of TIME and a FOR loop in SCL. SCL compiles to the same MC7 / STL code but is far easier to maintain.
DB Declaration
DATA_BLOCK "EventLog"
{ S7_Optimized_Access := 'FALSE' } // Required for variable index
STRUCT
Measurement : ARRAY[1..10] OF TIME; // Measurement[1] = newest
Trigger : BOOL;
END_STRUCT;
BEGIN
END_DATA_BLOCK
FB "EventLogShifter" in SCL
FUNCTION_BLOCK "EventLogShifter"
VAR
TrigMem : BOOL;
ix : INT;
END_VAR
BEGIN
// Rising edge detection
IF "EventLog".Trigger AND NOT #TrigMem THEN
// Shift descending: oldest value at index 10 will be discarded
FOR #ix := 10 TO 2 BY -1 DO
"EventLog".Measurement[#ix] :=
"EventLog".Measurement[#ix - 1];
END_FOR;
// Insert the new value at the head
"EventLog".Measurement[1] := #NewValue;
END_IF;
#TrigMem := "EventLog".Trigger;
END_FUNCTION_BLOCK
AT overlay over a fixed-length ARRAY OF BYTE:DATA_BLOCK "EventLog"
{ S7_Optimized_Access := 'TRUE' }
STRUCT
Raw : ARRAY[0..39] OF BYTE; // 10 * 4-byte TIME
Measurement AT "Raw" : ARRAY[1..10] OF TIME; // Symbolic view
Trigger : BOOL;
END_STRUCT;
The AT view inherits a non-optimized memory layout, so variable indexing works while the rest of the block remains optimized.
Loop Boundary Pitfall
The FOR loop direction in the example is intentionally 10 TO 2 BY -1. Running it forward (2 TO 10) would copy Measurement[2] into Measurement[3], then Measurement[3] (already overwritten with the previous value of [2]) into Measurement[4], etc., collapsing the buffer into a single replicated value. Always verify loop direction in the compiled SCL with "Go to → Used as STL" in TIA Portal.
Method 3 – Ring Buffer with Wrapping Pointer
The shift approach performs 9 assignments per event. A ring buffer performs 1 write per event and requires zero data movement; the "rolling" is implicit in the pointer advance. The trade-off is that the consumer code (HMI, archive, statistics) must compute the chronological order from the pointer rather than reading the buffer in linear order.
DB Layout
DATA_BLOCK "EventLog"
{ S7_Optimized_Access := 'FALSE' }
STRUCT
Measurement : ARRAY[0..9] OF TIME; // Zero-based for pointer math
WriteIndex : INT := 0; // 0..9, increments then wraps
SampleCount : DINT := 0; // Monotonic, used for "newest" detection
END_STRUCT;
END_DATA_BLOCK
Producer FB
FUNCTION_BLOCK "EventLogWriter"
BEGIN
IF "EventLog".Trigger AND NOT #TrigMem THEN
"EventLog".WriteIndex :=
("EventLog".WriteIndex + 1) MOD 10;
"EventLog".Measurement["EventLog".WriteIndex] := #NewValue;
"EventLog".SampleCount := "EventLog".SampleCount + 1;
END_IF;
#TrigMem := "EventLog".Trigger;
END_FUNCTION_BLOCK
Consumer Logic (read last 10 in chronological order)
// ixOut is the output index 0..9 (0 = oldest, 9 = newest)
#ixOut := ("EventLog".WriteIndex + 1 + #ixOut) MOD 10;
#value := "EventLog".Measurement[#ixOut];
Ring buffers are preferred when the buffer is large (>= 50 entries) or when the event rate is high (every < 10 ms), because each shift consumes CPU time proportional to buffer size while a ring-buffer write is O(1).
Method 4 – Block Move with SFC20 BLKMOV
SFC20 "BLKMOV" (Block Move) is available on every S7-300/400/1200/1500 and copies a contiguous memory range in a single call, which is useful when the data type is a UDT or a structure that you do not want to break into individual moves. BLKMOV works on the byte level, so the source and destination byte counts must match exactly.
CALL SFC20 (
SRCBLK := P#DB100.DBX 14.0 BYTE 36, // Measurement[1..9] = 9 * 4 B
RET_VAL := #ret,
DSTBLK := P#DB100.DBX 18.0 BYTE 36); // → Measurement[2..10]
IF #ret <> 0 THEN
// BLKMOV finished with error – see SFC20 manual for #ret codes
END_IF;
// Insert new value at slot 1
L #NewValue;
T DB100.DBD 14;
SFC20 RET_VAL Error Codes
| RET_VAL (hex) | Meaning |
|---|---|
| 0000 | No error |
| 8091 | Source area exceeds source DB or is outside the work DB |
| 8092 | Destination area exceeds destination DB or read-only |
| 80A1 | Source area not in DB (e.g. bit address crossed) |
| 80B1 | Destination area not in DB |
| 80B4 | SFC20 cannot copy because the operand is an FB/DB with optimized access |
| 80B5 | Length = 0 |
On S7-1500 you can also use MOVE_BLK / MOVE_BLK_VARIANT from the "Basic Instructions" palette for symbolic block copies without leaving the symbolic world.
Method Comparison
| Criterion | STL L/T Shift | SCL FOR Loop | Ring Buffer | SFC20 BLKMOV |
|---|---|---|---|---|
| CPU load per event (10 slots) | 9 × 4 B transfers ≈ 9 µs | 9 × 4 B transfers ≈ 12 µs (SCL overhead) | 1 store + 1 MOD ≈ 2 µs | 1 system call ≈ 8 µs + 1 store |
| Code size | ~18 network rungs | 4 lines SCL | 4 lines SCL | ~6 lines STL + SFC call |
| Supports UDT / Struct? | Yes (manual L/T per field) | Yes (single assignment) | Yes (single write) | Yes (BLKMOV is byte-agnostic) |
| Works on optimized blocks? | Yes (symbolic) | Only with AT overlay or non-optimized |
Only with AT overlay or non-optimized |
No – 80B4 error |
| Consumer complexity | Trivial (linear) | Trivial (linear) | Must compute order from pointer | Trivial (linear) |
| Scalability (N = 1000) | Poor (999 moves) | Poor | Excellent | Good (single SFC call) |
| CPU platform | S7-300/400 only (native STL) | All | All | All |
Edge Cases, Pitfalls, and Optimized Block Considerations
1. Double-Trigger Within One Scan
If the trigger input is a level (for example, a button wired directly without debounce), the shift will execute every cycle while the signal is high. Always perform rising-edge detection with a static edge memory bit, or use the R_TRIG instance in the "Bit Logic" palette (S7-1200/1500).
2. Buffer Size Mismatch
If the ARRAY lower bound is 0 instead of 1, every index shifts by one and the first event goes into the last slot. Always validate the lower and upper bounds against the loop limits:
FOR #ix := UPPER_BOUND("EventLog".Measurement) DOWNTO LOWER_BOUND("EventLog".Measurement) + 1 DO
"EventLog".Measurement[#ix] := "EventLog".Measurement[#ix - 1];
END_FOR;
3. Partial Initialization
On the first cold start the buffer is empty. The first event produces nine "valid" entries and nine copies of the new value. Add an Initialized : BOOL flag that zeroes all slots during the first cycle, or use the OB100 (warm restart) to clear the DB explicitly:
// OB100 – Complete restart
FOR #ix := 1 TO 10 DO
"EventLog".Measurement[#ix] := T#0s;
END_FOR;
"EventLog".WriteIndex := -1;
"EventLog".SampleCount := 0;
4. Retentivity Across STOP→RUN
If the controller is stopped while a partial buffer is held, an S7-300 retains the DB only if the OB1 is configured for "Restart" rather than "Cold restart", or if the variables are explicitly marked as retentive in the DB properties. On S7-1200/1500 set the "Retain" attribute on the ARRAY itself – it is inherited by all elements.
5. Access from a Different Priority Class (OB35, OB82, etc.)
If the shift runs in OB1 and the buffer is also read by an OB35 cyclic interrupt (for example, for an HMI update), the read may catch the buffer mid-shift. Use a lock-free protocol (only one writer, readers always see a consistent snapshot because the writes are sequential and not interleaved at the bit level) or guard with a BUSY flag.
6. Endianness on Cross-Platform Project Migration
STEP 7 V5 and TIA Portal both store multi-byte values in little-endian order on S7-300/400/1200/1500. There is no byte-swap issue when migrating from one to the other, but always recheck the offset layout if you switch from a UDT to individual TIME fields.
7. HMI Polling Cadence
A WinCC Comfort/Advanced tag polling at 250 ms against a 10-element buffer reads 40 bytes every cycle. Aggregate the buffer into a UDT and expose a single tag with "S7-Optimized" transfer to reduce round-trips to 1.
Verification and Commissioning
-
Offline simulation (S7-PLCSIM / PLCSIM Advanced): Force
Triggerwith the watch table, setNewValue = T#1s, T#2s … T#10s, and verify that after the 10th triggerMeasurement[1]= 10 s andMeasurement[10]= 1 s. - Online monitor: open the DB in STEP 7 / TIA Portal, switch to "Data View", and watch the shift visually on each trigger.
- Edge test: trigger twice within one OB1 cycle (level high for two consecutive cycles). Confirm that only one shift occurs because of the rising-edge gate.
- Retention test: trigger 5 events, stop the CPU via the mode switch, switch back to RUN, and verify that the 5 values are still present and a 6th trigger shifts them down.
- Cold restart test: perform MRES (memory reset) on the S7-300 or "Format memory card" on the S7-1500, restart, and confirm the buffer is zeroed before the first event.
-
Long-run timing test: log
OB1cycle time with theRTCblock for 1 hour at the expected event rate and verify that the worst-case cycle time does not exceed the OB1 watchdog (default 150 ms on S7-300, configurable up to 6 000 ms on S7-1500).
Troubleshooting Matrix
| Symptom | Probable Cause | Fix |
|---|---|---|
| All slots contain the same value after a few events | FOR loop runs in ascending direction (overwrites before reading) | Reverse loop: 10 TO 2 BY -1
|
| Buffer never updates | Trigger is a level, not a pulse; BEC in STL is true every cycle | Add rising-edge detection or a 1-shot (P) coil |
| SCL compile error "Indexed access not allowed on optimized ARRAY" | DB is optimized by default | Disable optimization, or use AT overlay on a non-optimized byte array |
| SFC20 returns 80B4 | DB has optimized access | Same fix as above |
| First event appears at slot 10 instead of slot 1 | Off-by-one: shifted from Measurement[2] to Measurement[1]
|
Check ARRAY lower bound; loop must end at lower bound + 1 |
| Old values still present after STOP→RUN | DB marked non-retentive | Set Retain on the ARRAY or on individual slots |
| Cycle time spike every event | BLKMOV used with very large payload | Reduce payload, switch to ring buffer |
| SCL online monitor shows wrong values | SCL compiled in "non-real-time" mode during edits | Full re-download and STOP→RUN restart |
FAQ
Why does my S7-1200 SCL FOR loop fail with "The symbolic ID was not found" or "Indexed access is not permitted"?
The default DB access on TIA Portal V13+ is "optimized". Optimized ARRAYs only accept constant indices. Either uncheck "Optimized block access" in the DB properties, declare an AT overlay on a non-optimized ARRAY OF BYTE, or use a copy of the ARRAY in a non-optimized instance DB of an FB.
Which approach uses the least CPU time: STL L/T, SCL FOR, SFC20 BLKMOV, or ring buffer?
For a 10-element TIME buffer the ring buffer is fastest because it does only one indexed write and one MOD per event (~2 µs). SFC20 BLKMOV follows at ~8 µs. The SCL FOR loop and the STL L/T both do 9 transfers and are comparable (~9–12 µs). For N > 50 the gap widens and the ring buffer is strongly preferred.
Can I move a UDT (struct) instead of individual TIME fields?
ARRAY[1..10] OF "MyEventUDT" and use the same loop or SFC20 BLKMOV with the byte size of the UDT × 10. On S7-1500 prefer MOVE_BLK_VARIANT with the UDT as a Variant to keep the code symbolic.How do I make the buffer survive a power cycle?
Mark the DB or the individual ARRAY elements as retentive. On S7-300/400 this is the "Non-Retain" column in the DB editor; on S7-1200/1500 it is the "Retain" attribute. Combined with a "Complete restart" OB100 that zeroes the buffer, this gives deterministic behavior on every cold start.
What is the maximum buffer size I can implement with SFC20 BLKMOV?
SFC20 limits the byte count to the size of the source DB. On a standard global DB the limit is the CPU-specific maximum DB size (S7-315: 16 KB, S7-317: 64 KB, S7-1516: 16 MB). For very large buffers, switch to MOVE_BLK_VARIANT on S7-1500, which has no static byte cap beyond the available work memory.