Overview
On SIMATIC S7-300 and S7-400 systems the absence of a level transmitter on a vented tank is not a reason to skip the level reading. Where two volumetric flow transmitters are already fitted to the inlet and outlet of a tank, the level can be reconstructed on the CPU by integrating the difference between the two flows inside the cyclic interrupt OB35. This approach is mathematically a textbook continuity equation, but it has three field-proven pitfalls that catch engineers new to STEP 7:
- Understanding that FC105 already returns the flow in engineering units (m³/s) — applying another scaling stage on top is the classic bug and was the root of the original question.
- Getting the unit conversion m³/s → m³/ms right. The factor is 1000 because 1 second equals 1000 milliseconds, not 10 and not 100.
- Treating the integrator as the sole source of truth for level control. A redundant high-level discrete switch is mandatory for safe operation.
This article walks through the math, the STL source code for the integrator inside OB35, the parameter set of FC105, and the validation steps required to commission the level safely. Worked examples assume a cylindrical tank with a 1.0 m² cross-section and OB35 set to a 100 ms interrupt period.
Prerequisites
- STEP 7 V5.5 SPx or compatible engineering tool, targeting an S7-300 CPU 314/315/317/319 or an S7-400 CPU 412/414/416/417 (OB35 is the standard cyclic interrupt on these families; on S7-1500 the equivalent is OB30, configurable in TIA Portal task configuration).
- Two 4-20 mA flow transmitters hardwired to an analog input module, e.g. 6ES7 331-7KF02-0AB0 (8AI, 13-bit) or 6ES7 331-1KF02-0AB0 (8AI, 12-bit).
- Tank physical data: known floor area A in m², maximum flow Q_max for each transmitter, and a known safe working level for sanity-check at commissioning.
- OB35 enabled in Hardware → CPU Properties → Cyclic Interrupts with a fixed period (default 100 ms).
- FC105 (TI-S7 Converting Blocks, "SCALE") and FC106 ("UNSCALE") available in the project library — these ship with the SIMATIC S7 Standard Library.
- A backup high-level float or tuning-fork switch wired to a discrete input, electrically independent from the analog loop.
Mathematics of Tank Level Integration
For an incompressible fluid in an atmospheric (vented) tank, continuity gives
dV/dt = Q_in − Q_out
where V is the volume in m³ and Q_in, Q_out are the volumetric flow rates in m³/s. The free-surface level h is the volume divided by cross-sectional area A:
h = V / A
Discretizing on the OB35 sample period Δt (typically 100 ms = 0.100 s), the volume increment per cycle is
ΔV = (Q_in − Q_out) × Δt
and the new level
h[n] = h[n−1] + (Q_in − Q_out) × Δt / A
In the original configuration A = 1.000 m², so the area term collapses to 1 and the per-cycle level delta simplifies to
Δh = (Q_in − Q_out) × 0.100 (m)
Two non-trivial consequences fall out:
- The integrator integrates any zero-drift of Q into the level reading. A 1 µL/s zero drift over a year gives ~31.5 m of level error in a 1 m² tank. Quarterly re-zero of the flowmeters is a baseline requirement.
- Without a periodic reset or an independent reference, the integrator has no absolute reference. This is the technical reason a backup level switch must be installed (see safety section).
| Symbol | Meaning | Unit |
|---|---|---|
| h | Liquid level in the tank | m |
| V | Liquid volume in the tank | m³ |
| A | Tank floor cross-section | m² |
| Q_in | Inlet volumetric flow | m³/s |
| Q_out | Outlet volumetric flow | m³/s |
| Δt | OB35 cycle period | s (= ms/1000) |
Calculation Flow Diagram
Understanding FC105 SCALE in Detail
FC105 sits under Standard Library → TI-S7 Converting Blocks and converts a 16-bit INT analog input word to a REAL engineering value. Its signature is fixed and well documented in the SIMATIC Standard Library V5.5 reference. The conversion math is
OUT = ((IN − K1) × (HI_LIM − LO_LIM)) / (K2 − K1) + LO_LIM
where K1/K2 depend on the BIPOLAR flag: K1 = 0, K2 = 27648 for unipolar; K1 = −27648, K2 = 27648 for bipolar.
| Parameter | Direction | Type | Meaning |
|---|---|---|---|
| IN | INPUT | INT | Analog input word as a value in 0..27648 (or ±27648) |
| HI_LIM | INPUT | REAL | Engineering upper limit (e.g. 0.05 m³/s) |
| LO_LIM | INPUT | REAL | Engineering lower limit (typically 0.0) |
| BIPOLAR | INPUT | BOOL | FALSE for 4-20 mA unipolar; TRUE for ±10 V bipolar |
| RET_VAL | OUTPUT | BOOL | TRUE → error; FALSE → OUT is valid |
| OUT | OUTPUT | REAL | Scaled flow in engineering units (m³/s) |
FC105 automatically clamps OUT at HI_LIM / LO_LIM if the raw IN exceeds K2 or falls below K1. It does not, however, indicate an open transmitter — it will simply return the saturated value. Use the AI module's diagnostic bits (e.g. PIW bit 7, "wire-break") to detect that condition.
Understanding OB35 Timing
OB35 is the SIMATIC cyclic-interrupt organization block. Its period is configured in the CPU's Hardware → Properties → Cyclic Interrupts tab, default 100 ms, range typically 1 to 60 000 ms depending on the CPU. STEP 7 publishes the period as the temporary local variable OB35_EXC_FREQ in OB35's interface — but, despite the suggestive name, the value stored there is the period in milliseconds, not a frequency. The original poster hit this confusion directly when he had been told his code had to multiply by the OB35 "frequency".
Other OB35 properties that affect the integrator:
- OB35 priority: 12 (default). If multiple cyclic OBs are configured (OB30..OB38) only one can run at this priority unless managed explicitly.
- OB80 time-error OB: fires if OB35 has not finished before the next trigger. The integrator's ΔV for that cycle is lost unless compensated.
- Phase offset: configurable so that multiple cyclic OBs do not collide with each other or with OB1.
For very small tanks (A < 0.1 m²) shorten the OB35 period to 20 ms so each per-cycle ΔV remains well above the analog-input quantization step (see Drift Caveats below). For very large tanks (A > 10 m²) you can keep the period at 100 ms or even raise it to 500 ms to free CPU bandwidth.
Resolving the m³/s → m³/ms Conversion Factor
The original question was whether to divide by 10 (one suggestion) or 100 (a second attempt). Both answers were wrong. The unit conversion is
Q [m³/s] × Δt [ms] × (1 s / 1000 ms) = Q × Δt / 1000 [m³]
The factor 1000 appears because 1 second contains exactly 1000 milliseconds. The 100 ms OB35 period does not change the conversion — it enters only inside the numerator. This is why the final STL network uses the constant 1.000e+3, not 1.000e+1 or 1.000e+2.
| Flow transmitter unit | OB35 period | Volume per cycle [m³] |
|---|---|---|
| m³/s | 100 ms | Q × 0.100 |
| m³/min | 100 ms | Q × (1/600) |
| m³/h | 100 ms | Q × (1/36000) |
| l/s | 100 ms | (Q × 0.100) / 1000 |
| kg/s (water) | 100 ms | Q / 1000 (assumes ρ = 1000 kg/m³) |
If the integrator logic reads the OB period explicitly from OB35_EXC_FREQ at runtime (so the code survives a period change), the corrected universal formulation is:
ΔV = Q_net × OB35_EXC_FREQ / 1000 [m³]
When the period is fixed at 100 ms and A = 1 m², Δh = ΔV / A = ΔV directly.
STL Source Code for the Integrator
The program lives inside OB35. Two FC105 instances run first, the math converts m³/s into m³-per-cycle and the integrator updates DB10. The following is the production code used by the integration; copy it into OB35 directly.
// =====================================================================
// OB35 — Tank level by flow integration (STEP 7 V5.x STL)
// =====================================================================
// Assumes:
// PIW272 = inlet flow 4-20 mA, 0..0.05 m^3/s
// PIW274 = outlet flow 4-20 mA, 0..0.04 m^3/s
// OB35 = 100 ms cyclic interrupt
// Tank A = 1.000 m^2
// DB10 = Tank_Data (retentive)
// =====================================================================
// ----- Network 1 — read & scale inlet flow -----
CALL #FC_In // wrapper around FC105 SCALE
IN := PIW272
HI_LIM := 5.000e-2
LO_LIM := 0.000e+0
BIPOLAR:= FALSE
RET_VAL:= DB10.DBX20.0
OUT := DB10.DBD12 // Q_in [m^3/s]
// ----- Network 2 — read & scale outlet flow -----
CALL #FC_Out // wrapper around FC105 SCALE
IN := PIW274
HI_LIM := 4.000e-2
LO_LIM := 0.000e+0
BIPOLAR:= FALSE
RET_VAL:= DB10.DBX20.1
OUT := DB10.DBD16 // Q_out [m^3/s]
// ----- Network 3 — net flow -----
L DB10.DBD12 // Q_in
L DB10.DBD16 // Q_out
-R // Q_net (m^3/s)
T DB10.DBD8 // store Q_net
// ----- Network 4 — dV per cycle -----
// dV = Q_net [m^3/s] × period [ms] / 1000
L #OB35_EXC_FREQ // period [ms] (default 100)
ITD
DTR // ms as REAL
L 1.000e+3
/ R // period [s]
L DB10.DBD8 // Q_net [m^3/s]
* R // dV [m^3]
T DB10.DBD24 // Delta_V (temporary)
// ----- Network 5 — integrator -----
L DB10.DBD4 // V_prev [m^3]
L DB10.DBD24 // dV [m^3]
+R
T DB10.DBD4 // V_new [m^3]
// ----- Network 6 — level = volume / area -----
L DB10.DBD4 // V [m^3]
L 1.000e+0 // A = 1 m^2
/R
T DB10.DBD0 // h [m]
// ----- Network 7 — clamp to physical HI / LO -----
L DB10.DBD0
L 0.000e+0
>R
SPB _HI
L 0.000e+0
T DB10.DBD0
_HI: NOP 0
Points to notice:
- Both FC105 calls already convert the AI word into engineering units. There is no second scale block.
- The conversion factor 1000 appears exactly once, on Network 4, where seconds turn into milliseconds.
- If the tank area is not 1 m², replace the constant on Network 6 with the actual area.
- Mark DB10 as retentive (DB10 → Properties → Retain) so the integrator survives an unexpected CPU STOP.
- Network 7 enforces a non-negative level. Add an upper clamp tied to the high-high level of the tank so a saturated FC105 reading cannot push the integrator past the working range.
Why a Single Scaling Operation is Sufficient
Folding the conversion into FC105 by narrowing HI_LIM/LO_LIM is technically possible (set LO_LIM to 0 and HI_LIM to Q_max/1000, treating the output as m³/ms) but it is illegible in code reviews and breaks if the OB period changes. Keeping the unit system straight (flow always in m³/s, period always in ms, divide by 1000 in the integrator) is the sustainable choice.
Modern Alternative: S7-1200 / S7-1500 with NORM_X and SCALE_X
On a S7-1200 or S7-1500 the FC105/FC106 pair no longer ships. The IEC-equivalent blocks are NORM_X (normalized to 0.0..1.0) and SCALE_X (0.0..1.0 to engineering range). For an AI module that returns the raw INT, the call sequence is:
// SCL — S7-1500 example running in OB30 (configurable cyclic interrupt)
#Q_in := SCALE_X(
MIN := 0.0,
MAX := #Q_in_max, // e.g., 5.000e-2 m^3/s
VALUE := NORM_X(
MIN := 0,
MAX := 27648,
VALUE := "i_Q_in_raw")
);
// Same integrator as Networks 4..6 above
#dV := (#Q_in - #Q_out) * #OB30_DT_ms / 1.0e+3;
#V := #V + #dV;
#h := #V / 1.0; // A = 1 m^2
Document the integrator inside a function block named FB_TankLevelIntegrator with multi-instance capability, then call it from OB30. The OB period in S7-1500 is read via the system input OB30_DT_MS (a TIME value that you must convert: DWORD_TO_REAL(DWORD_OF(#OB30_DT_MS)) / 1000.0 to get seconds). The Siemens support portal documents the system-input names per OB; this differs from S7-300/400 where OB35_EXC_FREQ is the temporary variable.
Comparison: FC105 + OB35 (S7-300/400) vs NORM_X + OB30 (S7-1500)
| Aspect | S7-300/400 (FC105 + OB35) | S7-1200/1500 (NORM_X / SCALE_X + OB30) |
|---|---|---|
| Scaling library block | FC105 (single call) | NORM_X then SCALE_X (two calls) |
| Period access |
OB35_EXC_FREQ (period in ms, despite the name) |
OB30_DT_MS (must be DWORD-converted) |
| Period range | 1..60 000 ms | 1..60 000 ms (TIA Portal) |
| Engineering tool | STEP 7 V5.5 SPx | TIA Portal V18 or later |
| Code language | STL, LAD, FBD | SCL, LAD, FBD, GRAPH |
| OB80 time-error OB | OB80 on overrun, suspend OB35 | OB80 on overrun, runtime still continues |
| Retentivity | DB Retain attribute | Optimised DB with retain flag |
Commissioning Step-by-Step
- Pre-power zero check. With both pumps off, read both PIW values through Monitor. Zero-flow should sit between 0 and approximately 500 counts (4 mA = 0 counts; 0.5 % above zero is 138 counts). If either reading drifts outside that band, perform the flowmeter's zero-trim procedure before proceeding.
-
Inlet-only ramp. Start the inlet pump at a known speed giving 0.010 m³/s. Force
DB10.DBD4 := 0in Monitor / Modify first. WatchDB10.DBD0: it must rise linearly at 0.010 m / s ±5 %. If the slope is 10× too low the integrator is missing the divisor; if 100× too low it has ÷100 instead of ÷1000. Re-check Network 4. - Outlet-only ramp. Stop the inlet; start the outlet at 0.005 m³/s. Verify the level falls at 0.005 m / s ±5 %.
- Matched-flow cancellation. Run both pumps at 0.010 m³/s simultaneously. The level must be stationary within ±0.0005 m / minute. If it drifts linearly, either flowmeter has a zero error — re-step 1.
-
Retentivity. Switch the CPU from RUN to STOP, then back to RUN.
DB10.DBD0must return to the value held just before the STOP. If it resets, the DB is not retentive (missing Retain attribute in DB properties). - OB80 log audit. Open CPU → Diagnostic Buffer. There must be no OB80 events during the four ramp tests. Any "OB35 exceeded scan time" entry means the OB is too long; either increase the period or move the integrator math into an FB called from OB35.
- Backup switch test. Independently trigger the high-level float switch. The discrete input LED must light, the PLC must record the HH event, and the pump contactor must drop. This is a safety-relevant test for the backup interlock — do it last.
Verification Matrix
| Test | Set-up | Expected slope | Pass criterion |
|---|---|---|---|
| Zero-flow stability | Pumps off, 5 min observation | 0 m / min | ±0.0005 m / min |
| Inlet-step response | Q_in = 0.010 m³/s | +0.010 m / s | ±5 % |
| Outlet-step response | Q_out = 0.005 m³/s | −0.005 m / s | ±5 % |
| Matched-flow hold | Q_in = Q_out = 0.010 m³/s | 0 m / min | ±0.0005 m / min |
| OB overrun audit | Diagnostic buffer | No OB80 entries | 0 events in 1 h |
| Retentivity | STOP → RUN cycle | Value preserved | Exact match ±1 LSB |
| HH switch | Lift float manually | Discrete + alarm | Contactor drop within 200 ms |
Operating-Performance and Drift Caveats
- Analog-input resolution. An AI module with 13-bit signed resolution (e.g. 6ES7 331-1KF01) gives one count = 1 / 2^13 ≈ 1.2 µA. For a 4-20 mA, 0..0.05 m³/s transmitter this is one count ≈ 0.6 µL/s. Over a year the integrator will collect ±13 cm of level noise in a 1 m² tank if the AI quantization is not filtered. Apply a 4-tap moving average or a low-pass with τ = 1 s on the raw PIW before FC105.
- Transmitter zero drift. Coriolis and vortex meters drift 0.05..0.1 % of URL/year. For a 50 L/s URL meter that is 25..50 mL/s of zero drift → 158..315 m³/year of integrated error → 158..315 m of level in a 1 m² tank. Quarterly zero-trim is the only practical answer.
- OB overrun. If OB35 exceeds its 100 ms budget, OB80 fires and the cycle is dropped. Each dropped cycle loses 100 ms of integrator data. With a STEP 7 V5.5 CPU 315-2 PN/DP at default priority keep OB35 under 30 ms of wall-clock time; beyond that, raise the period or move work out.
- PLC scan ≠ OB period. Do not place the integrator in OB1. The OB1 cycle time is not deterministic, the integrator's Δt would float, and the level reading would jitter.
-
Cavitation. Sudden two-phase conditions (vapor or entrained gas) cause vortex and Coriolis meters to report phantom flow. Gate the integrator with a
Vessel_Pressure_Stableflag and freeze ΔV when cavitation is detected. - Round-off in REAL. A REAL has 23 mantissa bits — about 7 decimal digits. Over many millions of OB35 cycles the accumulator can round off sub-nanolitre level changes. For a 1000 m³ tank this is invisible; for a 1 mL-level application use LREAL (REAL64) inside the integrator DB.
Fault Codes and OB80 Time-Error Handling
The integrator should never silently drop a cycle. Where OB35 is exactly used (no overrun) the math is straightforward; on overrun the S7-300/400 firmware raises OB80 with a fault code inside OB80_ERROR_CODE. Reference values:
| OB80 fault code | Meaning | Integrator response |
|---|---|---|
| 0x0001 | OB start time exceeded | ΔV of last cycle was lost — re-run the previous ΔV on next cycle to recover |
| 0x0002 | Cyclic interrupt overrun | Same as 0x0001; second cycle is skipped after this |
| 0x0003 | OB called too early | Reduce OB35 priority load, do not re-run ΔV |
Recommended pattern: on every OB80 hit, latch a DB10.DBX30.0 flag, sum the missed ΔV from the last known good cycle, and apply it in OB35 once the flag clears. Avoid over-engineering this in simple tanks — for < 1 m³/h duty cycles the overrun is rare and may be ignored.
Other useful fault OBs to wire into the integrator HMI tag:
- OB82 — diagnostic interrupt (e.g., wire-break on the AI module). Halt the integrator and put a "BAD AI" badge on the HMI.
- OB121 — programming error. Symptom of a wrong FC105 interface (e.g., calling FC105 with a constant instead of a DB-addressable INT).
- OB122 — I/O access error. Symptom of a missing or failed AI/DI module — the integrator's PIW read is the most common trigger.
FC106 UNSCALE — Inverse Direction
FC106 (UNSCALE) is the inverse of FC105 and converts a REAL engineering value back to an INT analog word. It is used for analog output modules (e.g. 6ES7 332-5HF00) where the PLC must hand back an INT in the range 0..27648 to drive a 4-20 mA signal — for example, sending the integrated level to a chart recorder. The parameter set mirrors FC105 with the same K1/K2 constants for bipolar/unipolar. Plug FC106 into Network 8 of OB35 if a redundant analog re-transmission of level is required.
HMI and WinCC Integration
In WinCC flexible, WinCC V7, or TIA Portal HMI, expose at minimum the following tags:
-
DB10.DBD0(Level, m) — gauge widget with engineering range 0..max_level. -
DB10.DBD4(Volume, m³) — readout widget, 0..max_volume. -
DB10.DBD8(Q_net, m³/s) — bar widget, bipolar ±Q_max. -
DB10.DBX20.0andDB10.DBX20.1— FC105 RET_VAL flags; colour-coded red on TRUE. -
DB10.DBX30.0— OB80 latch; latched visible until operator ack.
Add a Set Level button authorized to Service class (and not Operator). It writes an operator-entered level into DB10.DBD0 and computes the matched volume DB10.DBD4 := DB10.DBD0 × A. Do not allow this button when the dynamic flow rate exceeds 0.5 % of Q_max — i.e. when the tank is actively moving.
Glossary of Variables
| Variable | Type | Location | Meaning |
|---|---|---|---|
| PIW272 / PIW274 | INT | Process image | AI words for inlet / outlet flow |
| FC_In / FC_Out | FC (FC105 wrap) | Project | Scale blocks, one per transmitter |
| DB10.DBD0 | REAL | DB10 | Tank level h in metres |
| DB10.DBD4 | REAL | DB10 | Tank volume V in cubic metres |
| DB10.DBD8 | REAL | DB10 | Net flow Q_net in m³/s |
| DB10.DBD12 | REAL | DB10 | Q_in in m³/s |
| DB10.DBD16 | REAL | DB10 | Q_out in m³/s |
| DB10.DBD24 | REAL | DB10 | ΔV for the current OB35 cycle |
| DB10.DBX20.0 | BOOL | DB10 | FC_In RET_VAL (error) |
| DB10.DBX20.1 | BOOL | DB10 | FC_Out RET_VAL (error) |
| DB10.DBX30.0 | BOOL | DB10 | OB80 time-error latch |
| OB35_EXC_FREQ | TIME | OB35 interface | Period in ms (despite the name) |
Safety: High-Level Backup Switch Is Mandatory
Hard-wire at minimum one of the following on a discrete input module that is electrically independent of the AI module used for FC105:
- A self-contained float level switch routed directly to the pump starter's safety chain.
- A tuning-fork level switch at high-high level.
- A weigh-frame or load-cell based level estimator on the support.
Whichever you choose must be independent of the AI module's potential. Connect the backup to a DI module on a separate power feed, and route it to the same stop command (pump contactor or fast-acting valve) via hard-wired logic, not via the PLC. The integrator is a comfort signal; the switch is the safety signal.
FAQ
Why do I divide by 1000 and not by 10 or 100?
Because the OB35 parameter is in milliseconds and the FC105 output is in m³/s. To convert the product Q × Δt into m³ you must divide by 1000 ms/s — that factor is fixed by SI, it does not change with the OB35 period. The 100 ms period appears in the numerator (as 100), the 1000 ms/s conversion appears in the denominator, and the result is ×0.100.
Can I skip FC105 and do the scaling myself in STL?
Yes. The same formula is REAL(IN) * (HI_LIM - LO_LIM) / 27648.0 + LO_LIM. Doing it manually is fine for one channel, but FC105 is preferable because it handles the unipolar/bipolar selection and saturation clipping, which is rarely worth reinventing.
What happens to the integrator on a cold start?
If the DB is retentive (default for global DBs), the level returns to the last good value. If not, it starts at 0 m. Treat a cold start as a level-init event — drive the integrator from an HMI Set Level pushbutton after physically dipping the tank, or accept that the level reading will need minutes to climb into range.
How do I handle 4-20 mA wires that have an open circuit?
FC105 clamps OUT at HI_LIM/LO_LIM, so an open circuit returns the saturated flow and the integrator will spike. Enable the AI module's diagnostic-interrupt (OB82) on wire-break, latch it into a BAD_AI flag, freeze the integrator, and alarm the operator.
What OB35 period should I use?
For tanks ≥ 1 m² and flows ≤ 0.1 m³/s, 100 ms (default) is fine. For very small tanks (A < 0.1 m²) shorten OB35 to ≤ 20 ms to keep per-cycle ΔV above the AI quantization step.
Why does my integrator drift even when the flow readings match?
Three sources: (1) zero drift of the flow transmitters — largest contributor, (2) analog-input quantization noise integrating into a long sum, (3) floating-point rounding in FC105 itself. Quarterly auto-zero, add a moving-average filter on PIW, and re-zero at least daily on continuous-batch lines.
Does this work for the S7-1500?
Yes, with the same math. Use NORM_X and SCALE_X in place of FC105, run the integrator in OB30 (default 100 ms), and read the period from the system input OB30_DT_MS converted with DWORD_TO_REAL. See the Modern Alternative section above for the SCL snippet.