Implementing Hourly and Daily Production Counters in TIA Portal

David Krause16 min read
SiemensTIA PortalTutorial / 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

This reference describes how to implement an hourly and a 60-day daily production counter on a Siemens SIMATIC S7-1200 or S7-1500 controller inside TIA Portal, and how to bind the resulting tags to a Unified Basic Panel so that today's count always appears in the first I/O field, yesterday's count in the second, and so on for up to 60 days. The architecture uses three data blocks (one for the current cycle snapshot, one for the 24-element hourly history, one for the 60-element daily history), a function block that detects product cycles via R_TRIG, a scheduled OB that performs the midnight shift, and a small set of HMI scripts running on the Unified Basic Panel to map a 60-element PLC array onto 60 I/O fields without manually editing 60 tag references in the screen editor.

Two binding strategies are covered: (1) direct hard-mapping of the 60 I/O fields to indexed PLC tags, which is the most portable and the easiest to verify; and (2) a JavaScript-driven approach that reads the PLC array into an HMI-internal table at screen load, which dramatically reduces engineering effort at the cost of slightly more runtime code. Both are valid for the Unified Basic Panel; the choice depends on the panel's available memory and on the operator's need for live updates without screen refresh.

Panel scope. Unified Comfort Panels and Unified Basic Panels both support JavaScript (ECMAScript 2020 subset). The runtime API is documented in the Siemens support entry 109758536. WinCC Comfort / TIA WinCC Advanced targets (Classic panels) do not support scripting; on those panels the hard-mapping path is the only option.

Prerequisites

  • TIA Portal V17 or later (V19 recommended for the latest Unified Basic Panel firmware support and the most stable JavaScript runtime).
  • STEP 7 Professional for SCL programming.
  • WinCC Unified for the HMI project (Basic Panel variant).
  • An S7-1500 (recommended) or S7-1200 CPU with firmware that supports the date/time DTL type and the RTC read function block. S7-1500 from firmware V2.0 and S7-1200 from firmware V4.0 onward meet this requirement.
  • A digital input wired to the product-detection sensor (proximity, photo-eye, encoder pulse, etc.) that produces one rising edge per produced part.
  • Time synchronization between the PLC and the HMI. The PLC's RTC is the master clock; the panel reads it over the HMI connection and uses it to schedule the midnight shift trigger.

PLC Data Block Architecture

Create the following three global data blocks before writing any application code. Keeping production history in its own DB (separate from the HMI tag DB) makes the shift logic easier to read and avoids accidental writes from the HMI.

DB_PROD_CURRENT — current shift snapshot

Name Data type Initial value Comment
PartCounter DInt 0 Increments on every rising edge of the sensor input. Resets at midnight.
HourCounter DInt 0 Increments on every part, copy frozen into DB_PROD_HOURLY at the top of each hour.
CurrentHour Int 0 0..23, written by FB_TimeTick at minute 0 of every hour.
CurrentDayKey DTL DTL#1970-01-01-00:00:00 Mirror of PLC RTC for HMI display.
LastShiftDayKey DTL DTL#1970-01-01-00:00:00 Last day on which the midnight shift fired.
SensorInput Bool false Cyclically refreshed from the DI module.
PartDetected Bool false One-cycle pulse from R_TRIG. Read by FB_PulseCounter.

DB_PROD_HOURLY — 24-element hourly history

Name Data type Length Comment
HourlyCount Array[0..23] of DInt 24 HourlyCount[H] is the parts produced during hour H of the current day.
HourlyFrozen Array[0..23] of Bool 24 True after that hour has been committed, false while the hour is still open.

DB_PROD_DAILY — 60-element daily history

Name Data type Length Comment
DailyCount Array[1..60] of DInt 60 Index 1 = today, 2 = yesterday, 60 = 59 days ago.
DailyDate Array[1..60] of DTL 60 Calendar date that the count refers to.
DailyValid Array[1..60] of Bool 60 True once the slot has been written at least once.

Using Array[1..60] (instead of Array[0..59]) matches the HMI I/O field indexing requested by the operator and removes the off-by-one error that is the single most common bug in this kind of project.

FB_PulseCounter — Product-Cycle Detection

This block runs in OB1 (or in a cyclic OB with a shorter time slice if the sensor can pulse faster than 1 kHz) and detects a single rising edge per part. Use R_TRIG on the sensor input; do not feed the raw input directly to the counter, otherwise high-frequency bounce will multiply-count the part.

FUNCTION_BLOCK FB_PulseCounter
VAR_INPUT
  iSensor : Bool;
END_VAR
VAR_OUTPUT
  oPartDetected : Bool; // one PLC cycle
END_VAR
VAR
  RTRIG_Inst : R_TRIG;
END_VAR
BEGIN
  RTRIG_Inst(CLK := iSensor);
  oPartDetected := RTRIG_Inst.Q;
END_FUNCTION_BLOCK

Call the block from OB1 and, on a positive edge, add 1 to DB_PROD_CURRENT.PartCounter and to DB_PROD_CURRENT.HourCounter. Do not call the counter write from inside an alarm OB; alarm-OB context disables most of the standard DB access optimizations and will inflate scan time on the S7-1200.

FB_TimeTick — Hourly and Daily Commits

This block reads the PLC RTC, freezes the current hour's count at the top of the next hour, and triggers the midnight shift. It runs once per PLC cycle but uses a one-second slow timer (or a time-of-day interrupt OB10, which is the preferred approach on S7-1500) to keep the load on the CPU predictable.

FUNCTION_BLOCK FB_TimeTick
VAR_INPUT
  iEnable : Bool;
END_VAR
VAR
  RTC_Inst : RTC; // IEC standard time-of-day clock
  tNow     : DTL;
  iHour    : Int;
  iMinute  : Int;
  bMidnightPulse : Bool;
  bHourPulse     : Bool;
  fbTonMidnight  : TON;
  fbTonHour      : TON;
  sLastDayKey    : DTL;
END_VAR
BEGIN
  IF NOT iEnable THEN RETURN; END_IF;

  RTC(); // updates the system clock; read the result with a separate FB or system clock tag
  tNow   := DTL_FROM_SYSTEM_CLOCK();
  iHour  := tNow.HOUR;
  iMinute:= tNow.MINUTE;

  // ---- Hourly commit: at HH:59:59 -> HH+1:00:00, snapshot the running counter into the previous slot
  IF (iHour <> DB_PROD_CURRENT.CurrentHour) AND (iMinute = 0) THEN
    DB_PROD_HOURLY.HourlyCount[DB_PROD_CURRENT.CurrentHour] := DB_PROD_CURRENT.HourCounter;
    DB_PROD_HOURLY.HourlyFrozen[DB_PROD_CURRENT.CurrentHour] := TRUE;
    DB_PROD_CURRENT.CurrentHour := iHour;
    DB_PROD_CURRENT.HourCounter := 0;
  END_IF;

  // ---- Midnight commit: 00:00:00 of a new day
  IF (tNow.HOUR = 0) AND (tNow.MINUTE = 0) AND (tNow.SECOND < 5) THEN
    IF sLastDayKey <> DTL#1970-01-01-00:00:00 THEN
      // Compare dates; only fire once per actual day change
      IF (sLastDayKey.YEAR <> tNow.YEAR)
         OR (sLastDayKey.MONTH <> tNow.MONTH)
         OR (sLastDayKey.DAY <> tNow.DAY) THEN
        ShiftDailyArray();
        sLastDayKey := tNow;
      END_IF;
    ELSE
      sLastDayKey := tNow;
    END_IF;
  END_IF;
END_FUNCTION_BLOCK
Why a 5-second window. A real-time clock ticks continuously and the OB1 scan can land on 00:00:00, 00:00:01, etc. multiple times if the scan is slow. The tNow.SECOND < 5 guard makes the shift fire exactly once per real day change without needing a one-shot flag.

ShiftDailyArray — The 60-Day Memory Move

The shift has to move index N+1 → N for N = 60 → 1, because if you move forward (1 → 2) you would overwrite yesterday's data with today's before you had read it. The correct loop walks from the bottom of the array to the top.

FUNCTION ShiftDailyArray : Void
VAR
  i : Int;
END_VAR
BEGIN
  // Walk from 60 down to 2: slot i becomes slot i-1
  FOR i := 60 DOWNTO 2 DO
    DB_PROD_DAILY.DailyCount[i-1] := DB_PROD_DAILY.DailyCount[i];
    DB_PROD_DAILY.DailyDate[i-1]  := DB_PROD_DAILY.DailyDate[i];
    DB_PROD_DAILY.DailyValid[i-1] := DB_PROD_DAILY.DailyValid[i];
  END_FOR;

  // Slot 1 = today, fresh count of 0 because PartCounter is about to be reset
  DB_PROD_DAILY.DailyCount[1] := 0;
  DB_PROD_DAILY.DailyDate[1]  := DTL_FROM_SYSTEM_CLOCK();
  DB_PROD_DAILY.DailyValid[1] := TRUE;

  // Slot 60 (the oldest) is dropped
  DB_PROD_DAILY.DailyCount[60] := 0;
  DB_PROD_DAILY.DailyDate[60]  := DTL#1970-01-01-00:00:00;
  DB_PROD_DAILY.DailyValid[60] := FALSE;

  // Reset the live counters for the new day
  DB_PROD_CURRENT.PartCounter := 0;
  DB_PROD_CURRENT.HourCounter  := 0;
  DB_PROD_CURRENT.CurrentHour  := 0;
END_FUNCTION

Total bytes moved at midnight: 60 × (4 + 12 + 1) = 1020 bytes. On an S7-1500 this takes well under 1 ms. On an S7-1200 budget for up to 5 ms; the S7-1212C and S7-1214C scan still stays inside the 10 ms cyclic window.

HMI Tag Configuration (Unified Basic Panel)

Expose the three PLC DBs to the HMI as HMI tags. Use the absolute addressing view in the HMI tag editor and verify that each tag is a read-only tag in the access column, except the small set of operator-controlled tags (e.g. bResetHistory) that must be writeable from the panel.

HMI tag name PLC path Access Length / array
HMI_HourlyCount DB_PROD_HOURLY.HourlyCount read [0..23] of DInt
HMI_DailyCount DB_PROD_DAILY.DailyCount read [1..60] of DInt
HMI_DailyDate DB_PROD_DAILY.DailyDate read [1..60] of DTL
HMI_DailyValid DB_PROD_DAILY.DailyValid read [1..60] of Bool
HMI_CurrentPartCount DB_PROD_CURRENT.PartCounter read DInt
HMI_CurrentDayKey DB_PROD_CURRENT.CurrentDayKey read DTL

Why expose full arrays. WinCC Unified transfers array tags as a single block per acquisition cycle. Exposing HMI_DailyCount[1..60] as one tag means one read per cycle regardless of how many I/O fields read it. If you expose each element as a separate tag, the panel will do 60 reads, the 1-second update setting will queue, and the screen will lag visibly when the operator opens the daily page.

Page 1 — Hourly Production (24 I/O Fields)

Place 24 I/O fields on screen 1, label them 00:00, 01:00, …, 23:00. Bind each I/O field to HMI_HourlyCount[0], HMI_HourlyCount[1], …, HMI_HourlyCount[23] in order. Update cycle 1 s is acceptable; for a 250 ms visual feedback loop set the update to On change in the tag properties. The hour that is currently being written (the "open hour") should be visually distinguished: a small grey text in the field label like "00:00 → live" makes the operator understand that the displayed number is still ticking up.

Index 0 vs 1. The PLC array uses [0..23] for hours; the I/O fields can be configured to start at index 0 in the process tag. Do not rewrite the PLC array to [1..24] just to match the screen — the SCL code and the human-readable labels will then disagree.

Page 2 — Daily Production (60 I/O Fields, 3 Binding Strategies)

Strategy A — Hard-mapping (60 manual tag references)

This is the most boring but the safest path. For each of the 60 I/O fields, open the process tag, browse to HMI_DailyCount, and manually edit the index to 1, 2, …, 60. Repeat for the date I/O fields and the validity flag if you show one. Engineering time: about 90 minutes the first time, 30 minutes the second. Verification: open the page, force a value in DB_PROD_DAILY.DailyCount[3] from the watch table, and confirm the third I/O field updates within the configured update cycle.

Strategy B — User constants on the PLC and HMI side

Create a user constant INDEX_TODAY = 1, INDEX_YESTERDAY = 2, …, INDEX_DAY_59 = 60 in the PLC constants table and in the HMI constants table. In each I/O field's process tag, instead of typing the literal 3, type the constant name. This is functionally equivalent to Strategy A on the Unified Basic Panel, but the constants document the index meaning and survive a future DB renumbering.

Strategy C — JavaScript population (1-second per page load)

On the Unified Basic Panel the HMIRuntime API exposes both the live tags and the screen object model. The script below reads the 60-element array once when the operator opens the page and writes each element into the corresponding I/O field by name. The total payload is 60 × 4 bytes = 240 bytes of integer data plus 60 × 12 bytes of DTL, well inside the panel's transient buffer.

// Script attached to the "OnLoaded" event of Screen "DailyProduction"
// Reads HMI_DailyCount[1..60] from the PLC and writes it into
// the 60 I/O fields named IOField_Day_001 .. IOField_Day_060
// and the 60 I/O fields named IOField_Date_001 .. IOField_Date_060

(async function() {
  try {
    const tagCount  = await HMIRuntime.Tags.SysFct.GetTagValue('HMI_DailyCount');
    const tagDate   = await HMIRuntime.Tags.SysFct.GetTagValue('HMI_DailyDate');
    const tagValid  = await HMIRuntime.Tags.SysFct.GetTagValue('HMI_DailyValid');
    const arrCount  = tagCount.Value;       // 60-element array
    const arrDate   = tagDate.Value;
    const arrValid  = tagValid.Value;

    for (let i = 1; i <= 60; i++) {
      const numField = Screen.FindItem('IOField_Day_' + (i < 10 ? '00' + i : '0' + i));
      const dateField = Screen.FindItem('IOField_Date_' + (i < 10 ? '00' + i : '0' + i));

      if (arrValid[i-1]) {
        numField.ProcessValue = arrCount[i-1];
        const d = arrDate[i-1];
        dateField.ProcessValue =
          ('20' + d.YEAR.toString().padStart(2,'0')) + '-' +
          (d.MONTH.toString().padStart(2,'0')) + '-' +
          (d.DAY.toString().padStart(2,'0'));
      } else {
        numField.ProcessValue = 0;
        dateField.ProcessValue = '--';
      }
    }
  } catch (e) {
    HMIRuntime.Trace('DailyProdLoad: ' + e.message);
  }
})();
Live update trade-off. Strategy C only refreshes on OnLoaded. If the operator leaves the page open across midnight, the displayed values stay frozen on the old day. Add a second script on the OnScheduled event (1-minute cycle) to re-pull the array, or schedule the screen to be reloaded at 00:00:30 via the HMI scheduler.

Alternative: TIA Openness for Bulk Screen Generation

If the project requires several production lines each with a 60-day page, the manual effort multiplies. TIA Openness exposes the WinCC Unified screen API in C# and Python. The pattern is to build a 60-row template in the screen XML, then loop over the rows in code, setting the process tag index of each I/O field. The Openness DLL set ships with the TIA Portal installation under C:\Program Files\Siemens\Automation\Portal V19\PublicAPI\V19. A minimal C# example that sets the index of a single I/O field:

var iotag = (IOTag)screen.TagItems.Find("IOField_Day_001").Tag;
iotag.PlcTag = plcTag;
iotag.TagPrefix = "HMI_DailyCount";
iotag.Index = 0; // 0-based into the array, slot 1
siemens.TIA.Openness.Project.Save();

Openess-based generation is the only way to keep 12 production lines maintainable; for a single line, prefer Strategy A or B.

Visualization Enhancements

  • Add a bar-chart control to the hourly page. Bind the value axis to HMI_HourlyCount[0..23] and the category axis to the static string array ["00:00","01:00",...,"23:00"]. The chart auto-updates because the source is a live array tag.
  • On the daily page, color the validity: green text for DailyValid[i] = TRUE, grey for FALSE. The grey slots (the first 59 on a freshly commissioned line) tell the operator the line is new.
  • Add a header I/O field that shows HMI_CurrentPartCount in a large font at the top of both pages, so the operator always sees the current shift total regardless of which view they are on.

Verification & Commissioning

  1. Download the PLC program and the HMI project. Open an online watch table on DB_PROD_CURRENT and confirm PartCounter increments by exactly 1 per part edge.
  2. Manually trigger an hour rollover: force DB_PROD_CURRENT.CurrentHour := 23 and force the PLC clock to 23:59:55. After 5 s the running HourCounter should land in DB_PROD_HOURLY.HourlyCount[23] and the live counter should reset to 0.
  3. Manually trigger a day rollover: force the clock to 23:59:58 and watch the daily array. Slot 1 should carry the day's total, slot 2 should carry the previous slot 1, and so on. Slot 60 should reset to invalid.
  4. From the watch table, force DB_PROD_DAILY.DailyCount[3] := 12345. Open the daily HMI page, scroll to row 3, confirm the value 12345 appears within the update cycle.
  5. Disconnect the network between the panel and the PLC for 60 s, reconnect, and confirm that the page recovers without a manual reload. The panel's reconnect logic should re-trigger the OnLoaded script automatically; verify this in the panel's diagnostic page.
  6. Power-cycle the PLC, then power-cycle the HMI, then re-trigger a day rollover. The persisted DB_PROD_DAILY (which is in the retain area) should be read back unchanged; only DB_PROD_CURRENT is expected to start at 0.

Retain & Restart Behavior

Mark DB_PROD_HOURLY and DB_PROD_DAILY as non-optimized with retain in the DB properties, or, if the project is on an S7-1500 with firmware V2.6 or later, leave them as optimized-block-access DBs and set the retain attribute on the DB itself. DB_PROD_CURRENT must not be retain-marked: it should restart from 0 after a power loss to avoid the "ghost part" bug that double-counts the first product after a restart. The first product after a restart of a retain DB will, however, get lost — the operator must be informed via an HMI message that the shift total starts at 0 after a power cycle.

Troubleshooting Matrix

Symptom Likely cause Diagnostic Fix
Counter increments by 2 or 3 per part Sensor bounce; R_TRIG missing Oscilloscope the DI; check FB_PulseCounter wiring Add 5 ms input debounce in HW config of the DI module or use the IEC R_TRIG as in this guide
Hourly counts are all zero on the HMI PLC array bound to wrong DB Watch table on DB_PROD_HOURLY.HourlyCount Re-bind HMI tag to DB_PROD_HOURLY, not to a soft-retired DB_OLD_PROD
Daily shift fires twice in one night 5-second window too wide for slow scan Check OB1 scan time; check sLastDayKey in watch table Reduce window to 2 s, or move the shift trigger into a 1-s time-of-day interrupt OB10
Daily shift misses one night PLC RTC not synchronized Check DTL_FROM_SYSTEM_CLOCK vs. SNTP server Enable SNTP on the S7-1500 with an industrial NTP source
HMI page shows dashes after midnight Strategy C script not reloaded Check the OnScheduled event binding Re-add the script to OnScheduled with a 60 s period
Yesterday's count shows today's count Shift loop direction wrong (1→2 instead of 60→1) Read the FOR statement in ShiftDailyArray Change to FOR i := 60 DOWNTO 2 as shown in the SCL block above
JavaScript on the Unified Basic Panel returns undefined Tag access on Classic HMI; wrong API namespace Check panel firmware; check the HMIRuntime namespace Upgrade to Unified firmware V16.4+; verify with the Siemens JavaScript reference 109758536
DB_PROD_DAILY is empty after a restart Retain attribute not set Open DB properties, "Retain" tab Set the retain bit on the DB; re-download to PLC

Standards & Documentation Pointers

For production-counter OEE calculations, the formulas in the VDI 2870 Part 1 sheet can be cross-checked against the hourly and daily arrays exported via the panel's recipe or trace mechanism. The retention behavior described in this article aligns with the default S7-1500 system manual section "Behavior of retentive data blocks in the event of a power failure" — see the S7-1500 System Manual, chapter "Backup and restore", and the function manual "S7-1500 ET 200MP Automation System" available on the Siemens support portal.

How do I make sure today's count is always in the first I/O field on the HMI?

Bind the first I/O field to DB_PROD_DAILY.DailyCount[1] and execute the shift in ShiftDailyArray with a FOR i := 60 DOWNTO 2 loop at midnight. Today's count is written to slot 1 by the same shift, so the binding never has to be changed.

Why does the counter sometimes increment by more than one per part?

The sensor input is not debounced. Wire the DI through an IEC R_TRIG in FB_PulseCounter and add the DI module's hardware input filter (typical 5 ms) so that contact bounce and EMC pulses do not produce multiple rising edges per part.

How do I keep the daily history across a power loss?

Set the retain attribute on DB_PROD_DAILY and DB_PROD_HOURLY. Do not retain DB_PROD_CURRENT, otherwise the first part after a restart will be double-counted as a "ghost part" from the previous shift.

Can I use WinCC Comfort (Classic) instead of Unified for the 60 I/O fields?

Yes, but only with Strategy A (manual hard-mapping) or Strategy B (constants). WinCC Comfort does not support JavaScript, so Strategy C is not available on Classic panels. The 60 tag references have to be configured by hand, which is tedious but fully supported.

How do I trigger the midnight shift on a S7-1200 reliably?

Move the shift logic from OB1 into a time-of-day interrupt OB (OB10 on S7-1200 / S7-1500) configured to fire at 00:00:00 with a 60-second phase offset. The interrupt OB runs at a known time independent of OB1 scan jitter, which removes the "5-second window" hack used in the example above.
Back to blog