Implementing a Rolling Buffer in Siemens S7 Data Blocks

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

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:

  1. Direct L/T shift in Statement List (STL) for S7-300/S7-400.
  2. Indexed ARRAY access with FOR loop in SCL for S7-1200/S7-1500.
  3. Ring buffer with a wrapping pointer, eliminating the data move entirely.
  4. Block move with the system function SFC20 BLKMOV as 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.

Trigger / Event NewMeasurement BOOL Shift Logic (FB) STL / SCL / SFC20 DB "EventLog" [10] Measurement10 (oldest) [9] Measurement9 [8] Measurement8 [7] Measurement7 [6] Measurement6 [5] Measurement5 [1] Measurement1 (newest) BOOL pulse TIME delta Shift slot N → N+1 (descending), then write new value at slot 1.

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
Optimized block access caveat: On S7-1200/S7-1500 the default for new global DBs is "optimized" (also called symbolic-only). Indexed access such as 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:

  1. Create the target DB. For the STL path name it DB100 "EventLog" (any free number).
  2. Inside the DB, declare 10 variables of type TIME (preferred) or DINT (for milliseconds). Name them Measurement1 through Measurement10, or declare one ARRAY[1..10] OF TIME.
  3. Set a retentive bit so the buffer is not cleared on STOP→RUN. Mark Measurement1..Measurement10 as retentive in the DB properties; on S7-1500 use the "Retain" attribute on the ARRAY element.
  4. Create an FB (e.g. FB50 "EventLogShifter") with an IN of type TIME named NewValue, an IN_OUT BOOL named Trigger (rising-edge detected inside the FB), and a STAT BOOL named TrigMem.
  5. Call FB50 in OB1 (cyclic) with its instance DB DB50 and pass the shared DB DB100 as an IN_OUT block 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
Why descending order matters: if you accidentally code 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
Optimized block fallback. If you cannot disable optimization (for example, the DB is consumed by a library that requires symbolic access), use an 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
Selection rule of thumb: for N ≤ 20 and event rate ≤ 100 Hz, the SCL FOR loop is the most maintainable. For N > 20, high event rates, or large UDT payloads, use a ring buffer or SFC20 block move.

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

  1. Offline simulation (S7-PLCSIM / PLCSIM Advanced): Force Trigger with the watch table, set NewValue = T#1s, T#2s … T#10s, and verify that after the 10th trigger Measurement[1] = 10 s and Measurement[10] = 1 s.
  2. Online monitor: open the DB in STEP 7 / TIA Portal, switch to "Data View", and watch the shift visually on each trigger.
  3. 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.
  4. 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.
  5. 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.
  6. Long-run timing test: log OB1 cycle time with the RTC block 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?

Yes. Declare 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.

Back to blog