Calculating Min, Max, Average of Two Sensors on S7-1500 in SCL

David Krause12 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. Problem Definition and Engineering Scope

A typical industrial measurement task requires a single statistical summary (minimum, maximum, arithmetic mean) from two redundant process sensors over a defined observation window. The window is fixed at 30 minutes in this reference design, and the controller is a Siemens SIMATIC S7-1500 CPU programmed in Structured Control Language (SCL) under TIA Portal. The two inputs are REAL (32-bit IEEE-754) temperature values in °C, typically delivered by PT100/RTD modules (e.g., SM 1231 on S7-1200 or SM 531 on S7-1500) or by analog input cards with 4–20 mA transmitters.

The required outputs are:

  • Q_MIN — minimum value observed in the rolling window
  • Q_MAX — maximum value observed in the rolling window
  • Q_AV — arithmetic mean of the samples in the rolling window

The design is built as a reusable Function Block (FB) with its own Instance Data Block (IDB), so it can be called from any cyclic OB (typically OB1) or from a time-driven OB such as OB30 (cyclic interrupt) if a deterministic sample period is required.

Design decision: The two sensor readings are averaged per scan to a single composite Temperature value, and that composite is the only value stored in the rolling buffer. If you need to track min/max per sensor instead, you must allocate two parallel buffers and two parallel statistics calculations.

2. Prerequisites

Confirm the following before coding the FB:

  1. Hardware — S7-1500 CPU (any firmware version that supports TIA Portal V15 or later; the SCL syntax used here is compatible with all current S7-1500 firmware). See the SIMATIC S7-1500 product page for the current CPU portfolio.
  2. Software — TIA Portal with the SCL compiler installed. Open Project → Properties → Task cards → SCL to verify the package is active.
  3. Sensor scaling — the two inputs I_TEMPERATURE_1 and I_TEMPERATURE_2 must already be scaled to engineering units (°C) as REAL. A typical scaling block is SCALE/NORM_X plus SCALE_X from the TIA Portal instruction palette.
  4. Time base — a 1-second pulse I_PULSE generated by a clock bit (e.g., the system clock Clock_1Hz in the CPU properties → System and clock memory) or by a cyclic-interrupt OB.
  5. Memory budget — a 30-minute window at 1-second sampling requires 1800 REAL samples (1800 × 4 bytes = 7200 bytes) plus a 4-byte pointer. A 30-second window needs only 1800 / 60 = 30 samples for 30 seconds at 1 Hz, so verify your sample period before sizing the array.

3. Algorithm: Rolling Buffer with a Single-Pass Statistic

The implementation uses a circular buffer indexed by POINTER. Each pulse, the oldest sample is overwritten by the newest composite value. After every write, a single FOR loop walks the entire array once, computing min, max, and the running sum in the same pass. This is the same pattern recommended for aggregate functions in general-purpose languages: initialize the accumulators from the first element, then iterate.

The complexity is O(n) per output update and O(1) memory beyond the buffer itself. On an S7-1500 with a 1-second pulse and 1800 elements, the loop runs well inside the cycle-time budget; the OB1 scan is not measurably impacted.

4. Step-by-Step: Build the FB in TIA Portal

4.1 Declare the Block Interface

Create a new Function Block named FB_StatWindow (number range 1000–1999 is conventional). In the Block interface editor, declare the following variables exactly:

Section Name Type Initial value Comment
Input I_TEMPERATURE_1 Real 0.0 Sensor #1 in °C
Input I_TEMPERATURE_2 Real 0.0 Sensor #2 in °C
Input I_PULSE Bool FALSE 1 s sample enable
Input I_FIRST_SCAN Bool FALSE Init flag (one-shot)
Output Q_MIN Real 0.0 Window minimum
Output Q_MAX Real 0.0 Window maximum
Output Q_AV Real 0.0 Window mean
Temp Temperature Real — Composite per-scan value
Temp i Int — Loop counter
Static BUFFER Array[1..CONST_PERIOD] of Real — Rolling window
Static POINTER Int 1 Circular write index
Const CONST_PERIOD Int 30 Window length (interpret as seconds here; see §6)
Type-safety note: Always compute the running sum into a Real (or a DInt if your input is Int). Accumulating Int with 1800+ elements overflows at 32 767. The accumulator here is Real, so overflow is not an issue up to ~3.4 × 10³⁸.

4.2 Network 1 — Average the two sensors

// Composite sample: arithmetic mean of the two sensors
Temperature := (I_TEMPERATURE_1 + I_TEMPERATURE_2) / 2.0;

4.3 Network 2 — First-scan initialization

On the very first call (driven from OB100 startup or a one-shot FirstScan tag in the IDB), the array is filled with the current composite so that min/max/mean are well-defined before the first full window elapses.

IF I_FIRST_SCAN THEN
    POINTER := 1;
    // FILL_BLK_INI would also work; a loop keeps the FB self-contained
    FOR i := 1 TO CONST_PERIOD DO
        BUFFER[i] := Temperature;
    END_FOR;
END_IF;

4.4 Network 3 — Gating on the sample pulse

Statistics are only updated on a rising edge of I_PULSE. This keeps the FB idempotent if it is called more often than the sample period.

IF NOT I_PULSE THEN
    RETURN;   // leave Q_MIN / Q_MAX / Q_AV unchanged this scan
END_IF;

4.5 Network 4 — Write the new sample into the ring buffer

POINTER := POINTER + 1;
IF POINTER > CONST_PERIOD OR POINTER < 1 THEN
    POINTER := 1;
END_IF;
BUFFER[POINTER] := Temperature;

4.6 Network 5 — Single-pass statistic

Initialize all three accumulators from BUFFER[1] and walk the rest of the array. This pattern (init from element 0 / 1, then iterate from element 1 / 2) is the standard idiom and is also documented for the Power Fx aggregate functions Average, Max, and Min.

Q_AV  := BUFFER[1];
Q_MIN := BUFFER[1];
Q_MAX := BUFFER[1];

FOR i := 2 TO CONST_PERIOD DO
    Temperature := BUFFER[i];
    Q_AV := Q_AV + Temperature;
    IF Q_MIN > Temperature THEN
        Q_MIN := Temperature;
    END_IF;
    IF Q_MAX < Temperature THEN
        Q_MAX := Temperature;
    END_IF;
END_FOR;

Q_AV := Q_AV / INT_TO_REAL(CONST_PERIOD);

5. Wiring the FB in OB1

Call the FB from the main cyclic OB. FirstScan is the standard first-cycle flag available on every S7-1500 CPU; Clock_1Hz must be enabled in CPU properties → System and clock memory.

// OB1 - Main
"iStatDB"(   // Instance DB auto-generated by TIA Portal
    I_TEMPERATURE_1 := "Raw_Temp_1",
    I_TEMPERATURE_2 := "Raw_Temp_2",
    I_PULSE         := "Clock_1Hz",
    I_FIRST_SCAN    := "FirstScan",
    Q_MIN           => "Stat_MIN",
    Q_MAX           => "Stat_MAX",
    Q_AV            => "Stat_AV"
);

6. Sizing the Window Correctly

The CONST_PERIOD parameter in the example above is set to 30. The unit depends entirely on the pulse rate driving I_PULSE:

Desired window Pulse source Required CONST_PERIOD Static memory
30 seconds 1 Hz clock 30 120 bytes
5 minutes 1 Hz clock 300 1 200 bytes
30 minutes 1 Hz clock 1 800 7 200 bytes
1 hour 1 Hz clock 3 600 14 400 bytes
30 minutes 10 Hz clock 18 000 72 000 bytes

For a 30-minute window at 1 Hz, change the constant to CONST_PERIOD : Int := 1800;. The arithmetic-mean divisor must be updated accordingly: Q_AV := Q_AV / INT_TO_REAL(CONST_PERIOD);.

Memory caution on smaller CPUs: The S7-1500 family ranges from the compact CPU 1511 (150 KB work memory) up to the CPU 1518 (4 MB). A 30-minute / 1 Hz window consumes ~7 KB and is trivial on every variant. If you push the window to 8 hours at 100 Hz you cross 11 MB, which only the CPU 1518 / 1518F can hold without retriggering the load memory.

7. LAD Implementation on S7-1200 and S7-1500

The same logic is implementable in LAD if SCL is not available on the target package. The recommended structure is:

  1. Add instruction → Math functions → ADD + DIV by 2.0 to compute the per-scan composite (use REAL arithmetic, not INT).
  2. Sample-and-hold on pulse — feed the composite into a MOVE_BLK / FILL_BLK chain that updates a DB array indexed by an INC-style counter that wraps at CONST_PERIOD.
  3. Min — use the Compare instruction < inside a loop; on the S7-1500 the Comparison group includes a dedicated MIN/MAX instruction that returns the smaller / larger of two REALs.
  4. Max — same as min, using the > compare and the MAX instruction.
  5. Mean — accumulate into a REAL tag and divide once at the end.

For the loop on S7-1200/1500 LAD, the cleanest approach is to write a small SCL FB and call it from LAD, rather than building a long ladder chain with explicit compares per array element.

8. S7-300 Variant — Memory-Indirect Addressing in STL

On an S7-300 (or any classic S7-300/400 target), the S7-1500-style SCL array syntax is not always available; you typically use STL with memory-indirect addressing. The user requirement is the same: rolling min, max, mean from a 1 Hz sample. The implementation pattern is:

// STL sketch (S7-300, SCL not licensed)
// DBx.DBX0.0  = composite value (REAL, 4 bytes)
// DBx.DBD4    = pointer / index (DINT)
// DBx.DBD100  = sum accumulator (REAL)
// DBx.DBD104  = current min (REAL)
// DBx.DBD108  = current max (REAL)

      L     DBx.DBD4       // pointer
      L     1
      +I
      T     DBx.DBD4
      L     CONST_PERIOD   // e.g. 1800
      >I
      JC    RES
      L     0
      <I
      JC    RES
      JU    CONT
RES:  L     1
      T     DBx.DBD4
CONT: L     DBx.DBD4
      SLD   3              // index * 8 because stride is 8 here
      LAR1  P#DBx.DBX200.0 // base of array
      +AR1
      L     DBx.DBD0       // composite
      T     DBD [AR1,P#0.0]

The scan-loop that recomputes min / max / mean uses a separate counter from 0 to CONST_PERIOD–1 and the same indirect-addressing pattern. For a REAL array the index stride is 4 bytes (or 8 for a paired DINT); for a pure INT array the stride is 2 bytes. The source notes the stride difference explicitly: REAL/DINT addresses change by 4, INT addresses change by 2. Use a DINT accumulator when summing INT to prevent overflow.

9. Verification and Commissioning Steps

  1. Static check — in TIA Portal, right-click the FB and select Compile → SCL. Fix any syntax errors. Open the IDB and confirm the BUFFER array size matches CONST_PERIOD.
  2. Online watch — go online with the CPU, open the IDB in Monitoring mode, and force I_TEMPERATURE_1 = I_TEMPERATURE_2 = 25.0. Within one scan, Stat_MIN = Stat_MAX = Stat_AV = 25.0.
  3. Min/max step test — toggle the inputs to known fixed values (e.g. 20.0 / 30.0 / 22.5 / 27.0) at every pulse, and verify that Q_MIN settles at the lowest value within the window and Q_MAX at the highest.
  4. Ring-buffer wrap test — set CONST_PERIOD := 5 temporarily, set a 1 Hz pulse, and feed a saw-tooth pattern. The Q_MAX should equal the largest of the last five samples, and Q_MIN the smallest.
  5. Mean sanity check — feed ten identical samples (e.g. all 50.0). Q_AV must equal 50.0 exactly. Then feed samples of 50 and 100 in alternation. After an even number of samples, Q_AV must be 75.0.
  6. Cycle-time check — open the CPU diagnostics view and confirm the OB1 cycle time is well below the configured maximum (typically < 50 ms for a small S7-1511 with this block). If the cycle time approaches the limit, raise the constant or move the FB into a slower OB30 with a 100 ms cyclic interrupt.

10. Common Pitfalls and Field-Notes

Symptom Likely cause Fix
Q_AV stuck at 0.0 after power-up First-scan init never fired, and the array is still zero-filled Drive I_FIRST_SCAN from OB100 startup or use the CPU's FirstScan flag
Q_MIN / Q_MAX are constant while inputs vary I_PULSE is wired to a 10 ms or 100 ms clock but CONST_PERIOD is set for a 1-second window; the buffer wraps too fast Match CONST_PERIOD to the actual pulse period × window duration
Mean is slightly off (e.g. 24.997 instead of 25.000) Mixing REAL and DINT in the accumulator, or dividing with integer math Cast CONST_PERIOD to REAL before division: Q_AV := Q_AV / INT_TO_REAL(CONST_PERIOD);
Q_MIN / Q_MAX equal Q_AV on a single anomaly sample Single corrupted reading overwrites the buffer; a 4–20 mA loop fault produced a -3276.8 °C value Add a plausibility check: discard samples outside a process-defined range before writing to the buffer
CPU goes to STOP with SF LED on, diagnostic buffer mentions array bounds Buffer access violation in the FOR loop; CONST_PERIOD was reduced without recompiling callers Re-compile all callers; ensure the loop bounds match the array declaration
On S7-300: STL reports "Invalid pointer" AR1 / AR2 clash with the FB's address register use Save and restore AR1 / AR2 with TAR1 / LAR1 at FB entry and exit

11. Extensions and Production-Grade Variants

  • Median — copy the buffer into a temporary array and run a simple in-place sort; the median is BUFFER[CONST_PERIOD / 2]. Useful when one sensor occasionally produces an outlier.
  • Standard deviation — store the running sum of squares Σx² in parallel with the running sum Σx. Then σ = sqrt( (Σx² / n) − (Σx / n)² ). The Power Fx aggregate reference also lists StdevP and VarP for population-based dispersion.
  • Per-sensor statistics — duplicate the buffer as BUFFER_1 and BUFFER_2 and run two parallel single-pass loops.
  • HMI trend — publish Q_MIN, Q_MAX, Q_AV to an HMI tag and graph them on a WinCC / Unified faceplate for operator visibility.
  • Alarm on excursion — if Q_MAX − Q_MIN exceeds a process-defined spread (e.g. > 5 °C for a tank), raise a "sensor drift" alarm to drive maintenance.

12. Frequently Asked Questions

Why do I get a wrong mean when the sensors disagree by a large amount?

Because the algorithm averages the two readings into one composite before storing it in the buffer. If one sensor is faulty, the faulty value is halved and contaminates the entire 30-minute window. Add a plausibility filter (e.g. reject samples more than 2 °C from the previous value) or switch to per-sensor statistics with a separate fault flag.

What is the minimum CPU class that can run a 30-minute / 1 Hz window?

Any S7-1500 CPU from the 1511-1 PN upward. The block needs ~7.2 KB of static data and a single FOR loop over 1800 REALs, well inside the cycle-time budget of even the smallest S7-1511. See the S7-1500 product selector for current models and work-memory sizes.

Can I run the same FB on an S7-300 in STL instead of SCL?

Yes, but the SCL array syntax becomes manual memory-indirect addressing in STL. The key rules: stride is 4 bytes per REAL/DINT element and 2 bytes per INT element, and the accumulator must be a DINT (or REAL) to avoid overflow when summing thousands of INT samples. Save and restore AR1 / AR2 around the loop because the FB uses them.

How do I trigger a 1 Hz sample deterministically without using a clock memory byte?

Use a cyclic interrupt OB such as OB30 with a 1000 ms period and call the FB from there, or use a hardware timer in the analog input module. Driving I_PULSE from CPU properties → System and clock memory → Clock_1Hz is the simplest approach and is sufficient for non-safety statistics.

Is the algorithm numerically stable for thousands of samples?

Yes, as long as the accumulator is a Real (IEEE-754 single precision) and the divisor is also a Real. For windows beyond a few hours at high sample rates, consider a compensated summation algorithm (Kahan) or switch to LReal (64-bit double) on the S7-1500 to reduce rounding drift.

Back to blog