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:
- S7-300 CPU 31x/31xC, CPU 319 System Software - System Functions (TIME_TCK, RD_SYS_T)
- S7-300/400 Counting - Operating Parameters
- S7-300 Module Data: SM 331 Analog Input
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:
-
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 comparingACTUAL_TIME < LAST_TIMEand adds the rollover span (2 147 483.647 s) to the time delta. This is correct and runs indefinitely. - 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
-
Static zero test. Disconnect the analog input or force the scaled value to 0.0 L/min. Run the CPU for one minute.
OUTmust remain at 0.000 ± 0.001 L. -
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 minwithin 0.1 %. - 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.
-
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. - 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.
- 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
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 |
Related Documentation
- S7-300/400 System and Standard Functions Reference Manual (TIME_TCK, RD_SYS_T, etc.)
- S7-300 SM 331 Analog Input Module Manual
- S7-300/400 Counting - Operating Parameters
- STEP 7 V5.5 SCL Reference (FB / FB call syntax)
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.