Calculating Moving Average in WinCC with Global C Scripts

David Krause12 min read
SiemensTutorial / How-toWinCC
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

WinCC (TIA Portal and the legacy WinCC 7.x line) does not expose a moving-average operator in either the Tag Management or Tag Logging editors. Tag Management provides only linear scaling, and Tag Logging provides only arithmetic (time-weighted) averages over the archive window. Engineers who need a true running/floating mean must implement it either in the controller (PLC) or in a WinCC Global Script C action triggered cyclically.

This reference covers the recursive single-state formulation of a moving average (O(1) per sample), the equivalent O(N) sliding-window formulation using an internal array, and the TIA Portal FloatingAverage FB available on S7-1200/S7-1500 CPUs. It assumes familiarity with the WinCC Graphics Designer, the Global Script C editor, and basic PLC tag addressing.

Prerequisites

  • WinCC 7.4 SP1 / WinCC 7.5 / WinCC Professional (TIA Portal) V16 or later. The C API used here (GetTagFloat, SetTagFloat) is stable from WinCC 6.0 onward.
  • An external or internal WinCC tag of data type float (REAL) that holds the current measured value, e.g. ProcessValue.
  • An internal WinCC tag of data type float for the smoothed output, e.g. MovingAverage.
  • An internal WinCC tag of data type unsigned short or int for the sample counter, e.g. SampleCount.
  • Global Script Runtime licensed and enabled on the WinCC station. Verify in Computer Properties > Startup that Global Script Runtime is active.
  • For the TIA Portal section: a S7-1200 firmware V4.0+ or S7-1500 firmware V1.6+ CPU with the FloatingAverage FB available in the standard library.

Mathematical Foundation

Arithmetic Mean vs Moving Average

The arithmetic mean over N samples is

A(N) = (X1 + X2 + ... + XN) / N

A moving (rolling, floating) average recomputes the mean every time a new sample arrives. The naïve approach sums the last N values and divides by N; this is O(N) per sample and forces the script to retain N historical samples in memory.

Recursive Formulation (O(1) per Sample)

A first-order recursive filter yields exactly the same arithmetic mean of the last N samples, but requires only the previous mean and the new sample:

A_new = ((A_old · (N − 1)) + X_new) / N

Equivalently, expressed as a correction term:

A_new = A_old + (X_new − A_old) / N

Both forms are numerically equivalent. The first is preferred in single-precision float environments because it avoids catastrophic cancellation when X_new − A_old is small relative to A_old. This formulation is described in the NIST/SEMATECH e-Handbook of Statistical Methods as the running (cumulative) moving average and is the standard method used in industrial signal smoothing (NIST e-Handbook — Centered Moving Average).

WinCC Environment Constraints

WinCC separates data into two domains:

Editor Available Math Moving Average?
Tag Management > Linear Scaling y = a·x + b No
Tag Logging > Tag Statistics Min, Max, Sum, Arithmetic Mean (over archive interval) No (mean is over the whole archive window, not a sliding N)
Global Script C (Runtime) Full C99 subset, tag I/O via API Yes — implement manually
VBS / ANSI-C Actions Same C subset, plus VBS wrappers Yes — implement manually

The archive mean reported by Tag Logging is a time-weighted mean across the configured archiving cycle, not a sliding window of N consecutive samples. For smoothing process values (flow, level, pressure), the sliding recursive mean is almost always what the application requires.

Implementing Moving Average in WinCC Global Script C

Action Triggering

Open the Global Script C editor (WinCC Explorer > Global Script > C-Editor). Create a new Action with a cyclic trigger. The trigger interval defines the sampling period Δt; for a 1-second trigger and N = 15, the average window is 15 seconds.

Trigger selection:

  • Standard cycle 1 s / 2 s / 5 s / 10 s: best for process values archived at the same rate.
  • Tag trigger on the input value: ensures the average is recomputed exactly when the source tag changes; useful for event-driven tags.
  • Variable cycle: pass a custom 1-second tick tag to allow the runtime to change Δt dynamically.

Minimal Working C Action (Window Size N = 15)

#include "apdefap.h"
int gscAction( void )
{
    /* Persistent state across trigger cycles */
    static float  fAverageOld = 0.0f;
    static WORD   wCounter    = 0;     /* 1 .. N, wraps after N */

    float fNewValue;
    float fAverage;
    DWORD dwN = 15;                     /* window size */

    fNewValue = GetTagFloat("ProcessValue");   /* input  tag */

    if (wCounter == 0)
    {
        /* First cycle after WinCC startup: seed with current value */
        fAverage = fNewValue;
    }
    else
    {
        /* Recursive formula */
        fAverage = ((fAverageOld * (dwN - 1)) + fNewValue) / dwN;
    }

    /* Advance counter; wrap so we always average the last N samples */
    wCounter++;
    if (wCounter > dwN) wCounter = 1;

    fAverageOld = fAverage;

    SetTagFloat("MovingAverage", fAverage);     /* output tag */
    return 0;
}

This action satisfies three correctness requirements that the snippets circulating in informal sources frequently violate:

  1. The counter seeds with the current value on the first call (wCounter == 0), avoiding a divide-by-zero or a polluted mean during the first Δt after WinCC startup.
  2. The window size is a DWORD constant 15, not the dynamic counter. Using n as the divisor biases the mean during the warm-up phase and produces a transient step on every restart.
  3. State variables (fAverageOld, wCounter) are declared static. Without static the variables are reinitialised on every trigger, defeating the purpose of the recursion.
Note on static persistence. Static variables in a WinCC Global Script C action persist for the lifetime of the Global Script Runtime. They are reset on WinCC Runtime restart, on a project reactivation, or if the action is recompiled and reloaded. If the script is hosted on a redundant partner, the variables are not synchronised — average state must be re-seeded after a switchover.

Alternative: Explicit Window Using an Array (Sliding Window, O(N))

When the application requires the exact arithmetic mean of the last N samples (for example, to detect a spike outside the smoothed trend), use an explicit ring buffer. This is O(N) per cycle but auditable:

#include "apdefap.h"
#define N 15
int gscAction( void )
{
    static float fRing[N];
    static WORD  wIdx   = 0;
    static WORD  wCount = 0;
    static float fSum   = 0.0f;

    float fValue;
    float fOldest;
    float fAvg;
    int   i;

    fValue  = GetTagFloat("ProcessValue");
    fOldest = fRing[wIdx];         /* value about to be overwritten */

    fSum    = fSum - fOldest + fValue;
    fRing[wIdx] = fValue;

    wIdx = (wIdx + 1) % N;
    if (wCount < N) wCount++;

    fAvg = fSum / (float)wCount;   /* use wCount during warm-up */

    SetTagFloat("MovingAverage", fAvg);
    return 0;
}

The subtraction fSum - fOldest + fValue is mathematically identical to summing all N entries but executes in O(1) regardless of N. The wCount variable divides by the actual number of valid samples during warm-up (1, 2, …, N), eliminating the startup bias.

TIA Portal Floating Average FB for S7-1200 / S7-1500

For new projects the recommended implementation is on the controller side. Siemens publishes an official application example for a floating arithmetic mean on S7-1200/S7-1500 CPUs (Siemens Support Entry 39333120 — Floating Average for S7-1200/S7-1500).

The FB "FloatingAverage" operates on REAL inputs and exposes the following I/O:

Port Type Description
value IN — REAL Current process sample
reset IN — BOOL Rising edge clears the internal buffer
N IN — INT Window size (samples)
average OUT — REAL Floating arithmetic mean of the last N samples
valid OUT — BOOL TRUE once N samples have been collected

Call the FB in a cyclic OB (e.g. OB1 or OB30) at the desired sampling period. The PLC performs the smoothing in deterministic scan time and WinCC only needs to display the result — no Global Script action is required.

When to Choose PLC vs WinCC Implementation

Criterion PLC (FloatingAverage FB) WinCC Global Script
Determinism Scan-time deterministic Best-effort; affected by HMI load
Survives WinCC restart Yes No — state re-seeded
Visible in WinCC tag list Indirect (one OPC tag) Direct internal tag
Use when source is OPC from 3rd-party PLC No Yes
Computational cost on HMI None Negligible (1 mul + 1 add + 1 div per cycle)

Execution Time and Sizing

The recursive formulation executes in constant time regardless of N. A single WinCC C-action call performs:

  • 1 × GetTagFloat (WinCC tag manager dispatch)
  • 1 × float multiply
  • 1 × float add
  • 1 × float divide
  • 1 × SetTagFloat
  • Counter increment and bounds check

Measured runtime on a WinCC 7.5 station (Intel Core i5, 8 GB RAM) is < 50 µs per call for N = 15. Even at a 100 ms trigger cycle this represents < 0.05 % of one core.

Caution with very large N. When N > 221 (~2 million) and the source value drifts by less than the ULP of float, the recursion loses precision because (X_new − A_old) underflows. If very large windows are required, switch to double in the C action (the WinCC C compiler supports double through the standard headers) or implement on the PLC with REAL/LREAL.

Edge Cases and Field-Proven Caveats

  1. NaN / Inf propagation. If ProcessValue returns NaN or Inf (broken sensor, divide-by-zero upstream), the recursive formula propagates the bad value forever and all subsequent averages are NaN. Add a guard:
    if (!(fNewValue == fNewValue))   /* NaN check */
        return 0;
    if (fNewValue > 1.0e38f || fNewValue < -1.0e38f)
        return 0;
  2. Counter never advances on the first call. Several snippets in informal sources increment n after using it as the divisor and seed incorrectly, producing a transient step on startup. Use the pattern above: seed with wCounter == 0, then advance after the assignment.
  3. Tag value reset on HMI restart. When WinCC Runtime restarts, MovingAverage is initialised from the tag's start value. Make sure the start value in the Tag Management (Properties > Values) is set to 0 and that the script re-seeds within one trigger cycle. Otherwise screens display the stale 0 for up to one cycle.
  4. Redundancy switchover. In a WinCC redundant server pair, static variables are not replicated. The standby server will start with uninitialised averages. Acceptable for non-critical displays; unacceptable if the average drives a control loop. Push the smoothing to the PLC in that case.
  5. Trigger faster than source update. If the trigger cycle is shorter than the archive cycle of the source tag, the action reads the same value twice. The recursive formula correctly weights it the same as a fresh sample, but if you intended "one new value per second" you must trigger on the tag, not on a time cycle.
  6. Time-weighted vs sample-weighted. If the source updates irregularly (event-driven), the simple recursive mean is biased toward periods of higher activity. Switch to the TIA Portal FB which supports a time-stamped buffer, or trigger on a fixed 1 s cycle.

Verification Procedure

  1. Open the WinCC Graphics Designer and place an I/O field bound to MovingAverage.
  2. Force the input tag ProcessValue to a step: 100 → 0 → 100 → 0 with a 5-second hold each, using the Tag Simulator or PLCSim.
  3. Observe the output trace in the WinCC Trend Control. For N = 15 the output should reach ~93.3 % of the step within 15 sample periods (exponential rise).
  4. Verify the mean is unbiased: with a constant input of 50.0, the average must settle to exactly 50.0 (within float epsilon) after N cycles.
  5. Check the ApDiag output (WinCC 7) or the diagnostic window (TIA Portal) for compile errors. The C compiler reports on the first run; subsequent runs use the cached .cpl.
  6. Stop and restart WinCC Runtime. Confirm that the average re-seeds correctly within one trigger cycle (no persistent off-by-N bias).

Troubleshooting Matrix

Symptom Likely Cause Remediation
Average always equals the input Static state not preserved (action recompiled, or triggered with by query in C-Editor) Confirm trigger type is standard cycle or tag trigger, not a manual query
Average jumps to 0 on every trigger static removed; variables reinitialise each call Re-add static to fAverageOld and wCounter
Average is NaN after a few hours Broken sensor reading propagated Add NaN/Inf guard shown above
Average lags more than expected Counter used as divisor (warm-up bias) Use constant N as divisor; only use wCount during the warm-up of the array version
Action does not run Global Script Runtime disabled or script not assigned to a trigger Right-click action → Assign Trigger; verify Runtime licence
Output value oscillates between two numbers Counter wraps incorrectly (off-by-one) causing the first cycle of every window to seed Seed only when wCounter == 0; advance after seeding

Parameter Reference

Parameter Symbol Recommended Range Notes
Window size N 5 – 60 (typical process); 1 – 1000 (audit) Sample count, not seconds
Trigger period Δt 0.1 s – 10 s Match the source archive cycle
Effective smoothing time T = N · Δt 1 s – 600 s 63 % rise time ≈ T
Numerical type — REAL / float (32-bit) is sufficient up to N ≈ 221 Use LREAL on S7-1500 for large windows

Notes on Standards and References

The recursive formulation used here is mathematically equivalent to a first-order discrete-time low-pass filter with gain 1/N and is documented as the standard "running average" in NIST statistical methodology (NIST/SEMATECH e-Handbook of Statistical Methods, Section 6.4.2.2). For the controller-side implementation, follow the Siemens application example (Siemens Support Entry 39333120).

FAQ

Does WinCC have a built-in moving-average tag operator?

No. Tag Management only provides linear scaling (y = a·x + b) and Tag Logging only provides arithmetic mean over the configured archive interval. To get a true sliding-window moving average you must either compute it in a WinCC Global Script C action or in the PLC.

Which formula is most efficient for large windows (N > 100)?

Use the recursive form A_new = ((A_old · (N − 1)) + X_new) / N. It is O(1) per sample and needs only two persistent variables. For very large N (> 221) switch to double in C or use the LREAL TIA Portal FB on the S7-1500 to preserve precision.

Why does my average jump to 0 on every trigger?

The static storage class is missing from the state variables. In WinCC C actions, local variables without static are reinitialised each call, so the recursion loses its previous mean. Add static float fAverageOld and static WORD wCounter.

Should I implement the moving average in the PLC or in WinCC?

Implement in the PLC if the average drives a control loop, must survive WinCC restarts, or runs on a redundant WinCC pair (state is not synchronised). Implement in WinCC if the source data comes from an OPC server to which the PLC has no direct connection.

How do I avoid bias during the first N cycles after WinCC startup?

Seed the recursion with the current input value on the first call (if (wCounter == 0) fAverage = fNewValue;). For the array version, divide the running sum by the actual valid count, not by N, until N samples have been collected.

Back to blog