S7-300/400 Flow Totalizer: SCL Integration FB for Analog Input

David Krause13 min read
S7-300SiemensTutorial / 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: Why Totalize Flow in a PLC

Totalization converts an instantaneous rate signal (e.g. L/min, m³/h, kg/s) into a cumulative quantity (liters, cubic meters, kilograms) by integrating the rate over time. Industrial flowmeters (magnetic, vortex, Coriolis, ultrasonic) almost always expose a 4–20 mA or 0–10 V rate output; the controller's job is to read that rate, condition it, and accumulate the running total for batching, inventory, custody transfer, and reporting.

On a Siemens SIMATIC S7-300 or S7-400 there is no built-in "flow totalizer" function block. You have to construct one from the analog input, a scaling block, a time source, and an integrator. The integrator is the only non-trivial part: it must be numerically stable, survive CPU rollover, recover correctly from a power-off state, and behave predictably on the first scan after restart.

This reference walks through a production-grade implementation in SCL using a cyclic interrupt OB (OB35), the system clock TIME_TCK(), and a trapezoidal rule with Kahan compensated summation.

Prerequisites and Hardware Setup

Hardware

  • SIMATIC S7-300 (CPU 31x) or S7-400 (CPU 41x) with firmware supporting SCL (all standard CPUs do).
  • Analog input module: SM 331 (6ES7331-7KF02-0AB0) or SM 331 AI 8x12 (6ES7331-1KF02-0AB0) for 4–20 mA, configured for the flowmeter's output span.
  • Flowmeter with linear 4–20 mA output scaled to the desired engineering range (e.g. 4 mA = 0 L/min, 20 mA = 600 L/min).

Software

  • STEP 7 V5.5 / V5.6 with SCL option, or TIA Portal V15 or later.
  • The integrating FB is portable between STEP 7 and TIA Portal SCL; only the call environment (OB1, OB35, OB100) changes.

STEP 7 / TIA Portal reference manuals for the system clock and analog scaling:

Step 1: Scale the Analog Input to Engineering Units

The raw value from the SM 331 is a 16-bit integer in the range 0–27648 (unipolar) or -27648 to +27648 (bipolar). The standard Siemens scaling block FC105 "SCALE" (STEP 7 V5.x) or "NORM_X" / "SCALE_X" (TIA Portal) converts it to a floating-point engineering value.

For a 4–20 mA flowmeter scaled 0–600 L/min on a 0–27648 raw range:

// FC105 parameters
IN     := PIW256            // raw input word
HI_LIM := 600.0             // L/min at 20 mA
LO_LIM := 0.0               // L/min at 4 mA
BIPOLAR:= FALSE
RET_VAL:= LW0
OUT    := "DB_Flow".Flow_LPM // REAL, L/min

After this stage Flow_LPM carries the instantaneous rate in liters per minute, which is the input to the integrator.

Step 2: Select the Time Base

The totalizer must be called at a fixed, deterministic interval. Three options are available on the S7-300/400:

OB Default Period Use Case Trade-off
OB1 Application cycle (10–100 ms typical) Slow flows, low accuracy, simple programs Period drifts with program length; no guaranteed isochronism
OB32 1000 ms (configurable 1 ms–60 s) Slow flows, batch totals reported per minute/hour Cannot capture short transients
OB35 100 ms (configurable 1 ms–60 s) Standard for analog control and totalization Good balance of CPU load and accuracy
OB38 10 ms Fast batching, very small volumes per cycle Higher CPU load; verify scan headroom

For a 600 L/min flowmeter at OB35 (100 ms), the per-cycle increment is up to 1.0 L per cycle, which is acceptable for most inventory and batching applications. For custody transfer, shorten the OB period or use a pulse-input flowmeter with the integrated counting function described in Step 7.

Step 3: SCL Implementation of the Integral FB

The complete integrating function block below is a SCL port of the published "Integral" FB, with explicit unit handling, overflow protection, and a Kahan-style compensated sum for long-term numerical stability.

FUNCTION_BLOCK FB1
TITLE = 'Integral'
// Trapezoidal totalizer for analog flow rate
//
// OUT accumulates:  OUT_units = IN_units_per_sec * seconds
//
// If IN is in L/min, divide IN by 60 at the call site
// or change the time base to minutes (TIME_TCK()/60000.0).

VERSION : '1.0'
AUTHOR  : Totalizer
NAME    : Integral
FAMILY  : SIMATIC

VAR_INPUT
  IN     : REAL;   // instantaneous rate, engineering units per minute
  RESET  : BOOL;   // TRUE for one cycle clears OUT and compensator
  ENABLE : BOOL;   // FALSE freezes integration (e.g. during CPU startup)
END_VAR

VAR_OUTPUT
  OUT          : REAL;   // cumulative total, engineering units
  RESET_ACTIV  : BOOL;   // echo of RESET for HMI diagnostics
END_VAR

VAR
  OUT_LOW    : REAL;   // Kahan compensation term
  LAST_IN    : REAL;   // IN from previous cycle
  LAST_OUT   : REAL;   // OUT from previous cycle (pre-compensation)
  LAST_TIME  : REAL;   // previous timestamp, seconds
  ACTUAL_TIME: REAL;   // current timestamp, seconds
  X          : REAL;   // per-cycle trapezoidal increment
  n          : INT;    // first-cycle flag (0 = uninitialized)
END_VAR

BEGIN
  // -------- 1. Reset handling --------
  RESET_ACTIV := RESET;
  IF RESET = TRUE THEN
    OUT         := 0.0;
    OUT_LOW     := 0.0;
    LAST_OUT    := 0.0;
    ACTUAL_TIME := DINT_TO_REAL(TIME_TO_DINT(TIME_TCK())) / 1000.0;
    LAST_TIME   := ACTUAL_TIME;
    X           := 0.0;
    n           := 0;

  // -------- 2. Disabled state --------
  ELSIF ENABLE = FALSE THEN
    n := 0;   // force re-initialization on next ENABLE

  // -------- 3. Normal operation --------
  ELSE
    // First cycle after ENABLE: just record the time and input
    IF n = 0 THEN
      ACTUAL_TIME := DINT_TO_REAL(TIME_TO_DINT(TIME_TCK())) / 1000.0;
      LAST_TIME   := ACTUAL_TIME;
      LAST_IN     := IN;
      n           := 1;

    // Subsequent cycles: trapezoidal rule with rollover protection
    ELSE
      ACTUAL_TIME := DINT_TO_REAL(TIME_TO_DINT(TIME_TCK())) / 1000.0;

      // TIME_TCK rolls over when the 31-bit ms counter exceeds
      // 2 147 483 647 ms (~24.86 days). The maximum DINT divided
      // by 1000 gives the rollover span in seconds.
      IF ACTUAL_TIME < LAST_TIME THEN
        X := (ACTUAL_TIME - LAST_TIME + 2147483.647)
             * (IN + LAST_IN) / 2.0;
      ELSE
        X := (ACTUAL_TIME - LAST_TIME)
             * (IN + LAST_IN) / 2.0;
      END_IF;

      LAST_TIME := ACTUAL_TIME;
      LAST_IN   := IN;

      // -------- 4. Kahan compensated summation --------
      LAST_OUT := OUT;
      OUT      := LAST_OUT + X;
      OUT_LOW  := (OUT - LAST_OUT) - X + OUT_LOW;

      // Only correct when the loss term is meaningful relative
      // to the running total; this prevents the FB from
      // chasing single-precision noise.
      IF OUT_LOW <> 0.0 THEN
        IF ABS(OUT / OUT_LOW) < 10000000.0 THEN
          LAST_OUT := OUT;
          OUT      := OUT - OUT_LOW;
          OUT_LOW  := (OUT - LAST_OUT) + OUT_LOW;
        END_IF;
      END_IF;
    END_IF;
  END_IF;
END_FUNCTION_BLOCK

Unit Math, Made Explicit

The block stores timestamps in seconds (TIME_TCK() returns milliseconds, so divide by 1000). The trapezoidal increment is therefore:

X = (IN_new + IN_old) / 2 * Δt_seconds

If IN is in L/min, then X is in L/min · s. To obtain liters, divide IN by 60 at the call site so the FB integrates L/s, or call the FB with IN in L/s already. The published snippet does not perform this conversion; it is the integrator's responsibility to keep the units consistent. A common wrapper is:

// In OB35, all in L/min -> L/s for the integrator
"FB_Integral"(IN := "DB_Flow".Flow_LPM / 60.0,
              RESET  := "DB_Control".ResetTotalizer,
              ENABLE := TRUE);

Step 4: Call, Reset, and Power-Up Initialization

Call the FB from OB35 (cyclic interrupt). Call it a second time from OB100 (warm restart) with ENABLE = FALSE so the integrator does not interpolate a phantom volume during the first scan after a power-off:

// OB35 (100 ms cyclic)
"DB_Integral".ENABLE := TRUE;
"DB_Integral".RESET  := "DB_Control".ResetCmd AND NOT "DB_Control".ResetAck;
"FB_Integral"(IN     := "DB_Flow".Flow_LPM / 60.0,
              RESET  := "DB_Integral".RESET,
              ENABLE := "DB_Integral".ENABLE,
              OUT    => "DB_Total".Volume_L);

// OB100 (restart / power-up)
"FB_Integral"(IN     := 0.0,
              RESET  := FALSE,
              ENABLE := FALSE,
              OUT    => "DB_Total".Volume_L);

The OB100 call is critical. Without it, the FB will compute a Δt equal to the entire down-time of the CPU on the first scan and inject a phantom flow equal to (IN_new + 0) / 2 * down_time_seconds. In the worst case (a flowmeter that has been powered for hours, integrated with 0 L/s while offline), the first post-restart accumulator jump is the entire missed volume.

Step 5: Overflow Handling and Long-Term Stability

Two separate overflow risks must be addressed:

  1. Time overflow. TIME_TCK() is a 32-bit millisecond counter that rolls over every 2 147 483 647 ms ≈ 24.86 days. The block detects the rollover by comparing ACTUAL_TIME < LAST_TIME and adds the rollover span (2 147 483.647 s) to the time delta. This is correct and runs indefinitely.
  2. Accumulator overflow. A REAL (IEEE-754 single precision) has 24 bits of mantissa, which is about 16.7 million discrete values. A 600 L/min flow running for one year accumulates roughly 3.16 × 10⁸ L. This exceeds 2²⁴ ≈ 1.68 × 10⁷ by a factor of 19, so the least-significant bit grows from 0.001 L to roughly 0.02 L. Acceptable for batch reporting; not acceptable for fiscal metering.

For high-precision applications, write OUT to a double-integer pair (two DINTs: high word = GIGA-units, low word = units) on each cycle, or scale the engineering range so OUT fits in REAL for the longest expected run between resets.

Numerical Compensation (Kahan Summation)

The block uses a Kahan-style compensator stored in OUT_LOW. The compensation is only applied when ABS(OUT/OUT_LOW) < 1.0e7; outside that range the loss term is noise and would corrupt the integrator. The net effect: long-running totalizers lose roughly one ULP per cycle of precision, which is materially better than naive summation for integration horizons of days.

Step 6: Verification and Commissioning

  1. Static zero test. Disconnect the analog input or force the scaled value to 0.0 L/min. Run the CPU for one minute. OUT must remain at 0.000 ± 0.001 L.
  2. Step input test. Apply a known fixed rate (use a calibrator or force the scaled value in VAT). After exactly 60 s, the expected increment is Rate_L_per_min × 1 min within 0.1 %.
  3. Ramp test. Ramp the input from 0 to 100 % over 10 s. The trapezoidal rule should yield an error of order (d²F/dt²)·Δt²/12 per cycle, dominated by the discrete-time step size.
  4. Power-cycle test. Force ENABLE = FALSE, power the CPU off for 60 s, restart. The first integrated Δt must be one OB35 period, not 60 s. Confirm by observing the HMI totalizer — it should not jump on restart.
  5. 24-hour drift test. With a constant input of 300 L/min, run for 24 hours. Expected total ≈ 432 000 L. Allowable error: ±0.1 % for non-custody use, ±0.05 % for custody.
  6. Rollover test (long-term). Operate the CPU continuously for > 25 days and verify the totalizer does not jump backwards or exhibit a step at the rollover boundary.

Accuracy, Drift, and Quantization Limits

Source of Error Magnitude Mitigation
Analog input quantization (12-bit, 0–27648) ~0.024 % of full scale Use 14- or 15-bit SM 331 variants for high-accuracy meters
Scaling round-off in FC105 ~1 ULP of OUT Done in REAL; negligible
Trapezoidal discretization O(Δt²) Shorten OB35 period; use Simpson's rule for sinusoidal rates
OB35 period jitter ±1 ms typical Use hardware time source; consider isochronous mode
Floating-point accumulation ~1 ULP/cycle (Kahan) or ~N·ULP (naive) Kahan summation (already in FB); reset on overflow
Time counter rollover Step every ~24.86 days Compensated in FB; do not rely on the default RD_SYS_T for integration

Alternative Architecture 1: Pulse-Input Flowmeter + S7 Counting Function

If the flowmeter exposes a frequency or pulse output (e.g. 1 pulse per liter), the S7-300/400 integrated counting function is preferable to software integration. The counting function is hardware-accumulated in the CPU and survives short power interruptions if the counter value is retentive.

Configure the CPU's counting channel via HW Config (STEP 7) or Device Configuration (TIA Portal) and select the gate function behavior — for example, "cancel count value when gate closes" vs "interrupt and continue when gate reopens." See the official manual:

S7-300/400 Counting - Operating Parameters for Counting

Advantages: deterministic, no discretization error, no floating-point drift, no rollover handling required.

Alternative Architecture 2: Latched ANY-Pointer Summation

For batch programs that must accumulate an array of pre-scaled values, the Siemens Beitrags-ID 19345299 FAQ documents an ANY-pointer-driven average and sum routine. The technique is useful for moving-window flow averages that feed an LPR (liters per revolution) calculation, but is not required for single-rate totalization.

Integration Flow Diagram

Analog Input (SM 331) PIW256 = 0..27648 FC105 / SCALE_X Flow_LPM (REAL) Convert L/min -> L/s Flow_LPM / 60.0 FB_Integral (OB35) X = (IN+LAST_IN)/2 * Δt Kahan compensated sum OB100 Startup ENABLE = FALSE Prevents phantom volume Reset (HMI / Program) RESET = TRUE for 1 cycle OUT := 0; OUT_LOW := 0 OUT -> DB / HMI Volume_L (REAL) RESET_ACTIV for status ob100 reset

Troubleshooting Matrix

Symptom Likely Cause Diagnostic / Fix
Totalizer jumps on every CPU restart OB100 not called with ENABLE=FALSE; integrator uses full down-time as Δt Add OB100 call with ENABLE = FALSE; verify with VAT that n = 0 on first scan
Totalizer drifts negative or wraps at ~24.86 days Rollover branch missing or wrong constant Confirm 2147483.647 is the rollover span in seconds (DINT_max / 1000); test by advancing PLC time and watching OUT
Totalizer increments even with 0 L/min input Trapezoidal rule assumes non-zero Δt; floating-point noise on a 0 input still yields a tiny X Add a deadband: if ABS(IN) < 0.001 then skip integration; or clamp the analog input below 4 mA to 0
Totalizer shows fractional liters when the meter pulses 1 L/pulse Using rate integration instead of pulse counting Switch to a pulse-input flowmeter + S7 counting function; or use the integrator only as a fast proxy, not as the legal total
Totalizer loses precision after long runs REAL mantissa exhausted Split OUT into two DINTs (high + low 32 bits); reset OUT to zero when high word increments
HMI shows totalizer value changing in steps every 100 ms OB35 integration increments in discrete steps; HMI polls faster Normal — interpolate on the HMI between updates; or reduce OB35 period
Reset from HMI does not zero the total RESET is pulsed for one cycle only; HMI button may not be held Use a rising-edge detector on the HMI flag and latch for one full OB35 cycle
Safety notice. If the totalizer is used for custody transfer, fiscal metering, or any legally regulated quantity, validate the implementation against the meter's type approval certificate and your local weights-and-measures authority. Software integration of a rate signal is not equivalent to a metrologically certified pulse counter. The Kahan summation and trapezoidal rule described here provide good but not certification-grade accuracy.

Related Documentation

How do I convert L/min to L/s for the SCL totalizer?

Divide the scaled L/min value by 60.0 at the call site: FB_Integral(IN := Flow_LPM / 60.0, ...). The integrator's time base is seconds (TIME_TCK()/1000.0), so the per-cycle increment X = Δt_s × (IN_new + IN_old) / 2 will then be in liters. If you forget this conversion, the totalizer output will be in "liter-minutes per 60," off by a factor of 60.

Why does my totalizer jump on CPU power-up?

You are missing the OB100 call with ENABLE = FALSE. On the first scan after restart the FB records a Δt equal to the full down-time (potentially hours), then multiplies that by the current rate. Adding an OB100 instance that re-initializes the timestamps eliminates the phantom volume.

How do I handle TIME_TCK rollover after ~24.86 days?

The SCL block already detects rollover by checking ACTUAL_TIME < LAST_TIME and adds the maximum DINT in seconds (2 147 483.647) to the delta. Verify by leaving the CPU powered for more than 25 days and confirming the totalizer does not step backward. For indefinite operation also consider the precision limit of REAL accumulation; for fiscal metering, store the total as two DINTs.

Can I call the FB from OB1 instead of OB35?

Technically yes, but OB1's cycle time is not constant — it drifts with the program length and can introduce integration error on transients. Use a cyclic interrupt OB (OB32 at 1 s, OB35 at 100 ms, OB38 at 10 ms) for deterministic totalization. For pulse-type flowmeters, use the S7-300/400 hardware counting function described in the CPU manual instead of software integration.

How accurate is the trapezoidal integration versus a true integral?

Trapezoidal rule error is O(Δt²) per cycle and O(Δt²) globally. For a 100 ms OB35 period and a flow rate changing slower than ~0.1 Hz, the discretization error is well under 0.01 % and is dominated by analog-input quantization (~0.024 % at 12 bits) rather than the integration method. For rapidly varying rates, shorten the OB period or use Simpson's 1/3 rule.

Back to blog