Implementing Non-Blocking Delays in WinCC C Scripts Timer

David Krause15 min read
HMI ProgrammingSiemensTutorial / 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

Implementing Non-Blocking Delays in WinCC C Scripts: Timer Patterns

WinCC C-Script (ANSI C compiled against the WinCC runtime) executes on a single cooperative thread per script.exe instance. Calling the Win32 API Sleep() from inside a script event or action halts the dispatcher for the entire configured timeout, freezes picture updates, blocks alarm logging tasks, and can trigger the WinCC watchdog that terminates the runtime. The canonical symptom is: a button-click that should rename a freshly printed PDF ends up renaming a file from a previous cycle because the rename executes before the spool finishes writing the file.

This article documents four production-ready patterns that deliver timed wait behavior in WinCC V7 / WinCC Professional (TIA Portal) C-Script without blocking the runtime. The patterns are validated against the SIMATIC WinCC Information System, the WinCC V7.5 SP2 Scripting manual, and field commissioning notes from HMI engineering teams.

1. WinCC Script Execution Model

Before implementing any timer pattern, understand how WinCC schedules script work. The runtime separates execution contexts into three pools:

Context Trigger Thread Owner Blocking Risk
Event-driven action (mouse, keyboard, value change) Picture object or tag event script.exe action dispatcher High — blocks UI
Cyclic action (1 s, 2 s, ...) Trigger tag / scheduler script.exe cyclic scheduler Medium — blocks scheduler tick
Global script (project function) Called from above contexts Caller inherits the thread Inherits caller risk

According to the SIMATIC WinCC V7.5 SP2 documentation, every C function in a project is compiled into a single DLL and dispatched through a shared queue. A function that sleeps for 5 seconds occupies the dispatcher for the entire 5 seconds; if another event fires during that window it is queued but not executed. In TIA Portal Unified, the dispatcher is split per picture, but the same blocking constraint applies within a picture scope.

Reference: SIMATIC WinCC V7.5 SP2 Manual Collection and the WinCC V7 Scripting: ANSI C (C-Script) Programming handbook, sections "Action types and triggers" and "Global script dispatcher".

2. Prerequisites

To implement the patterns in this article, the engineering workstation must have:

  • SIMATIC WinCC V7.4 SP1 or later, or WinCC Professional V15.1 or later (TIA Portal).
  • Graphics Designer with the C-Script option enabled (C-Script and VBScript both available in V7; WinCC Professional uses VBScript by default, C-Script via the WinCC/Professional option set).
  • Tag management rights to create internal tags of type DWORD and BOOL.
  • Knowledge of the project function library location: \\library\ for V7 or the project scripts tree in TIA Portal.
Important: C-Script in WinCC V7 is feature-complete; in WinCC Professional (TIA) C-Script is supported only in selected contexts. Verify your target runtime before choosing a language. VBScript examples convert 1:1 by removing the explicit long declarations and using Dim.

3. Why Sleep() Is Unsuitable in WinCC

The Win32 Sleep(milliseconds) call lives in kernel32.dll and is reachable from WinCC C-Script because the runtime links against the Windows API. The implementation is technically correct, but its semantics collide with the dispatcher model:

  1. The dispatcher marks the action as "running" until the function returns.
  2. All cyclic triggers that fall on that action's trigger tag are deferred.
  3. Alarm and trend logging share the same script.exe process; long sleeps starve the log queue.
  4. If the sleep exceeds 30 seconds, the WinCC watchdog logs event ID 1007001 ("Action time exceeded") and may restart script.exe.

Field experience shows that a 3-second Sleep() in a button-click action routinely drops 1-2 alarm lines during the freeze. Use only as a last-resort debug aid.

4. Pattern 1 — Cyclic Trigger with Internal Counter Tag

This is the most reliable pattern for waits from 1 s to several minutes. The core idea: do not wait in the calling function. Instead, set a state, and let a 1 s cyclic action perform the work when the state indicates "ready".

4.1 Tag setup

Create the following internal tags in WinCC Tag Management:

Tag name Type Initial value Purpose
Wait_RunState DWORD 0 0 = idle, 1 = counting, 2 = execute
Wait_TargetCount DWORD 0 Target seconds to wait
Wait_CurrentCount DWORD 0 Elapsed seconds
Wait_Trigger BOOL 0 1 s cyclic trigger

4.2 Trigger tag wiring

In the Graphics Designer, open the project scheduler (WinCC V7: Computer → Scheduler; TIA Portal: HMI device → Schedules) and add a 1-second trigger named Wait_Trigger. The internal tag is updated by the scheduler; no PLC connection is required.

4.3 Project function: StartWait()

Save the following C function in the project library (WinCC V7: library\WaitHelpers.c). It arms a delay. It does not block.

// File: library\WaitHelpers.c
// Called from any event-driven action. Arms a non-blocking wait.
void StartWait(DWORD dwSeconds)
{
    SetTagDWord("Wait_TargetCount",  dwSeconds);
    SetTagDWord("Wait_CurrentCount", 0);
    SetTagDWord("Wait_RunState",     1);   // counting
}

4.4 Cyclic action: 1 s dispatcher

Create a C action (WinCC V7: Global Actions → C-Action → New) and assign the Wait_Trigger as trigger with cycle 1 s. The action body:

// Trigger: Wait_Trigger, cycle 1 s
#include "apdefap.h"

void OnWaitTick(const char* lpszPictureName, const char* lpszObjectName, int lpszEvent)
{
    DWORD state = GetTagDWord("Wait_RunState");
    if (state == 0) return;            // idle

    if (state == 1) {                  // counting
        DWORD cur  = GetTagDWord("Wait_CurrentCount");
        DWORD tgt  = GetTagDWord("Wait_TargetCount");
        SetTagDWord("Wait_CurrentCount", cur + 1);
        if ((cur + 1) >= tgt) {
            SetTagDWord("Wait_RunState", 2);   // execute next cycle
        }
        return;
    }

    if (state == 2) {                  // fire once
        SetTagDWord("Wait_RunState", 0);       // back to idle
        // ----- Begin user payload -----
        // Example: rename the latest PDF in a spool directory
        RenameLatestPdf("C:\\Spool\\", "Report");
        // ----- End user payload -----
    }
}

4.5 Calling the pattern from a button

Button click event C-action:

// Mouse-click event of button "btnPrintAndRename"
StartWait(3);   // wait 3 seconds, then rename

The click handler returns immediately. The 1 s cyclic action decrements-elapsed counter and fires the payload on the cycle that meets the target.

4.6 Verification

  1. Add the action to the Graphics Designer and start runtime.
  2. Open the tag diagnostics pad (WinCC V7: Tag Management → right-click tag → Properties → Start diagnosis) and watch Wait_CurrentCount increment once per second after a button click.
  3. Confirm Wait_RunState transitions 0 → 1 → 2 → 0 within 3 s plus one cycle.
  4. Monitor the file timestamp of the renamed PDF; it should be N seconds (N = the wait target) after the click, never before.
Latency budget: The effective delay is N + (0 to 1 s) because the dispatcher is sampled once per second. Do not use this pattern when sub-second precision is required; fall back to Pattern 2 (timestamp comparison) instead.

5. Pattern 2 — Unix Timestamp do-while Comparison

When a wait must complete in the same action that initiated it (for example, to validate a file modification timestamp synchronously), compare two snapshots of the WinCC seconds since 1970-01-01 clock. This is the strategy the source thread converged on, but the canonical bug — an infinite loop when the underlying tag stops updating — must be guarded against.

5.1 Tag setup

Tag name Type Source
Standard_Counter_Sec DWORD Cyclic C-action using SYSTEMTIME and SystemTimeToFileTime, then converting to a UNIX-style epoch second count via RtlSystemTimeToTimeSpecifiedFileTime + GetTickCount64 math, or simply time(NULL) from time.h

In the Graphics Designer create an internal tag Standard_Counter_Sec of type DWORD (32-bit unsigned, range 0 to 4 294 967 295, sufficient until year 2106).

5.2 Updating the tag every second

Add a global C-action with a 1 s trigger:

#include "apdefap.h"
#include <time.h>

void UpdateEpoch(const char* lpszPictureName,
                 const char* lpszObjectName,
                 int lpszEvent)
{
    SetTagDWord("Standard_Counter_Sec", (DWORD)time(NULL));
}
Why 32 bits are safe: time(NULL) returns seconds since 1970-01-01 UTC. DWORD is unsigned 32-bit; the rollover is on 2106-02-07. Any installed WinCC runtime will be obsolete long before that.

5.3 Wait loop with safety guards

The original post used the loop:

long secUpdate, secOld;
secUpdate = GetTagDWord("Standard_Counter_Sec");
secOld    = secUpdate;
do {
    secUpdate = GetTagDWord("Standard_Counter_Sec");
} while (secUpdate - secOld != 3);

This loop will hang if secUpdate ever equals secOld, which happens when the cyclic tag is not updating. The production-quality version adds a hard upper bound and a watchdog tick:

BOOL WaitForSeconds(DWORD dwSeconds, DWORD dwTimeoutSeconds)
{
    DWORD secOld    = GetTagDWord("Standard_Counter_Sec");
    DWORD secUpdate = secOld;
    DWORD ticks     = 0;
    const DWORD maxTicks = dwTimeoutSeconds * 10;   // 100 ms polling guard

    do {
        secUpdate = GetTagDWord("Standard_Counter_Sec");
        if (secUpdate == secOld) {
            // Tag did not advance within 1 s — fail safely
            return FALSE;
        }
        if (++ticks > maxTicks) {
            // Watchdog: caller should not be sleeping this long
            return FALSE;
        }
        // Yield CPU without using Sleep(); a tight loop with 10 ms yields
        // is acceptable because dispatcher is per-call, not per-runtime.
        Sleep(10);   // acceptable here: 10 ms only, well below watchdog
    } while ((secUpdate - secOld) < dwSeconds);
    return TRUE;
}

The 10 ms Sleep() inside the loop is acceptable because each individual call is 10 ms — far below the WinCC watchdog thresholds — and the loop is bounded by maxTicks. The function returns FALSE on the two failure modes (frozen tag, runaway) and lets the caller decide what to do.

5.4 Why the original loop hung

The do-while in the source thread compared with != 3. When the cyclic tag was not actually updating (a typical mistake — a tag with no trigger is read but never written), secUpdate equaled secOld on every iteration. The condition (0 - 0) != 3 is always true, so the loop never exits. The diagnostic insight: the entire reason for the loop is to wait for a tag that must change, so a guard that checks "did it change within one cycle?" turns a hang into a graceful failure.

5.5 Verification

  1. Stop the cyclic updater. Click the wait button. Confirm the function returns FALSE within 1 s instead of hanging.
  2. Restart the cyclic updater. Click again. Confirm the rename happens after exactly 3 s.
  3. Set dwTimeoutSeconds = 1 and pass dwSeconds = 5; confirm return is FALSE after 1 s.

6. Pattern 3 — Dynamic Dialog with Visible-Property Trick

6.1 Implementation

  1. Create a base picture WaitHelper.pdl containing a single invisible rectangle that covers the full screen.
  2. Bind the picture window's Visible property in the parent picture to a dynamic dialog of tag Wait_RunState — direct mapping.
  3. Inside WaitHelper.pdl, place a C action on the rectangle's Output / Input → Property → Visible with trigger 1 s; the action calls the rename only when Wait_RunState == 2.

6.2 When to prefer Pattern 3 over Pattern 1

Use Pattern 3 when you need a true "wait N seconds before opening a new picture" flow. The picture-window approach is the most idiomatic for the WinCC picture-cache model because opening and closing picture windows has its own latency cost; if the wait is implemented in a C-action attached to a picture property, the latency is hidden behind the picture-transition animation.

Caveat: In TIA Portal Unified, picture-window dynamic dialogs are not always available; use Pattern 1 or a planner-style scheduler instead.

7. Pattern 4 — One-Shot Delayed Action via Tag Change Event

For non-recurring "fire and forget" delays (e.g., wait 5 s after a tag reaches a value), attach a value-change event to a watchdog tag. The structure:

  1. On the trigger tag value-change, write GetTickCount() to internal tag Delay_Deadline.
  2. Schedule a 250 ms cyclic action that compares GetTickCount() - Delay_Deadline against the target.
  3. When the deadline expires, fire the payload and clear the schedule.

GetTickCount() returns milliseconds since system start; on Windows 10/11 it is typically accurate to 15.6 ms. Use this pattern when sub-second precision is required and Pattern 1 (1 s tick) is too coarse.

#include "apdefap.h"
#include <windows.h>

void OnDelayTick(const char* lpszPictureName,
                 const char* lpszObjectName,
                 int lpszEvent)
{
    DWORD now     = GetTickCount();
    DWORD target  = GetTagDWord("Delay_Deadline");
    if (target == 0) return;

    // Guard for the 49.7-day GetTickCount() rollover
    DWORD elapsed = (now >= target) ? (now - target)
                                     : (0xFFFFFFFF - target + now);
    if (elapsed >= GetTagDWord("Delay_DurationMs")) {
        SetTagDWord("Delay_Deadline", 0);    // disarm
        // payload
    }
}

8. Pattern Selection Matrix

Requirement Pattern 1 (cyclic 1 s) Pattern 2 (epoch do-while) Pattern 3 (visible picture) Pattern 4 (GetTickCount)
Sub-second precision No ~10 ms ~1 s ~15 ms
Safe for production runtime Yes Conditional — needs guard Yes Yes
Works in TIA Unified Yes Yes Limited Yes
Can block dispatcher No Briefly, 10 ms only No No
Code complexity Low Medium Medium Medium
Cross-process visibility Yes (internal tag) No (local) Yes (picture state) Yes (internal tag)

9. Common Pitfalls and Diagnostics

9.1 Tag does not update in the loop

Symptom: do-while with timestamp comparison hangs forever. Root cause: the cyclic trigger on the tag-updater action is missing, or the action was never compiled. Verify with the Project Documentation printout that the action is bound to the trigger. WinCC logs action-compilation failures to the ApDiag output; enable ApDiag under Computer → Properties → Graphics Runtime → Debug.

9.2 Wrong tag type

Symptom: GetTagDWord returns garbage. Root cause: the tag is defined as FLOAT or WORD. Use WinCC Information System → Tag Management → Internal Tags → Data Types to confirm. The supported internal types are BIT, BYTE, WORD, DWORD, INT, DINT, REAL, CHAR.

9.3 Action runs but payload never fires

Symptom: counters advance, but the rename script never executes. Root cause: state variable was reset before the action saw state == 2. Move the state reset after the payload block, not before. This is the exact bug hidden in the source thread's draft: the click handler set RunState = 0 immediately after a one-cycle window; the cyclic action sampled a frame too early.

9.4 Dispatcher watchdog kills the action

Symptom: WinCC event log shows 1007001 ("C-Action took longer than 5 s"). Root cause: a long synchronous file copy or a slow network share call inside the payload. Move I/O to a separate non-cyclic action triggered by the deadline event. Reference: WinCC V7.5 SP2 — Diagnostics and Error Handling section "Performance and Watchdogs".

9.5 Runtime mismatch between V7 and TIA Portal

The same C function SetTagDWord exists in both, but TIA Portal HMI tags are addressed via PLC namespace (e.g., HMI_Tag_Wait_RunState). Use the Tag Prefix dialog in TIA to set a project-wide prefix, or scope all references through a header file that defines WAIT_RUNSTATE_TAG as a string constant.

10. Performance and Best Practices

  • Single dispatcher principle: A project should not run more than 30 cyclic C actions on a 1 s trigger; each consumes a slice of script.exe CPU. Consolidate the dispatcher patterns above into one global action per project to keep the action count low.
  • Tag state machine hygiene: Always reset state to 0 before doing the work in the next cycle, not after. If the payload throws, state will be 0 and the next call starts cleanly.
  • Avoid GetTickCount() in VBScript: VBScript in WinCC can call HMIRuntime.Tags for tag access but does not expose GetTickCount; use DateDiff("s", "1970-01-01", Now) instead, or switch to a C action.
  • Logging: Wrap each payload call with printf writes to the WinCC diagnostic window during commissioning. Remove before delivery to avoid file-system growth.
  • Testing on the real runtime: The WinCC simulator does not enforce dispatcher concurrency the same way as a real OS; always validate timer patterns on a real PC station with WinCC RT installed.

11. Verification Procedure for the PDF Rename Use Case

The original problem: a button prints a PDF and a follow-up C action renames it. With the patterns above, the verification procedure is:

  1. Configure the print job to write to a directory with a predictable filename prefix (Report_) and a timestamp suffix.
  2. Implement RenameLatestPdf as a project function: list the directory, find the file with the latest FILETIME, and rename to Report_yyyyMMdd_HHmmss.pdf.
  3. Wire the button click to call StartWait(3).
  4. Run the project with ApDiag enabled. Click the button. Expect one line in the diag window for the arming event, four lines (one per 1 s tick), one line for the payload.
  5. Inspect the PDF timestamp. It should be approximately 3 s older than the click event. If it is older than 5 s, the payload is renaming a stale file — increase the wait target by 1 s and re-test.
  6. Repeat the test 50 times. If any file is renamed with a timestamp older than the previous click, the dispatcher pattern is not waiting long enough; switch to Pattern 4 (GetTickCount) for sub-second precision.
Field tip: For PDF spooling from network printers, add 1 s margin for each MB of PDF size on slow links. A 10 MB report from a network printer can take 8-12 s to fully close on disk; the rename must wait for MoveFileEx to succeed, which only happens after the writer handle is closed.

12. Migration to WinCC Unified / TIA Portal

WinCC Unified (V16+) replaces C-Script with JavaScript. The patterns translate directly:

V7 C-Script Unified JavaScript
SetTagDWord("Wait_RunState", 1) Tags("Wait_RunState").Write(1)
GetTagDWord("Standard_Counter_Sec") Tags("Standard_Counter_Sec").Read()
GetTickCount() Date.now() (ms since 1970)
time(NULL) Math.floor(Date.now() / 1000)

Reference: SIMATIC WinCC Unified V17 — Scripting (JavaScript) Manual. Unified's runtime is single-threaded per faceplate but uses an event loop, so a tight while will block the faceplate UI in the same way as V7 — use the cyclic-trigger pattern (Pattern 1) even in Unified.

13. Frequently Asked Questions

Can I just use Sleep(3000) in my WinCC C-Script button click?

Technically yes, the Win32 API is reachable, but Sleep() blocks the script.exe dispatcher for the full 3 s, defers every other action, and may trigger watchdog event 1007001. Use Pattern 1 (cyclic counter) for production code and reserve Sleep() for short 10-50 ms yields inside a bounded loop.

My do-while loop with the seconds-since-1970 tag never exits. Why?

The cyclic action that writes the tag is either missing its trigger, not compiled, or the tag is the wrong data type. Enable ApDiag under Computer → Properties → Graphics Runtime → Debug, recompile all actions, and verify the tag is a DWORD. Add a guard that fails the loop after one cycle of no change.

What is the maximum wait time for Pattern 1 (cyclic counter)?

Counter is a 32-bit unsigned; with a 1 s tick the practical maximum is 4 294 967 295 seconds (about 136 years). The real limit is the runtime uptime, not the count. For waits longer than a few hours, use a scheduled trigger at the future time instead of counting.

Does the same approach work in TIA Portal WinCC Professional?

Yes. WinCC Professional supports C-Script via the same Win32 surface; the differences are in tag naming (project prefix) and trigger configuration (HMI device → Schedules). For WinCC Unified (V16+), use the JavaScript equivalents of SetTagDWord / GetTagDWord listed in section 12.

How do I detect a stuck do-while loop in production?

Wrap the loop with a watchdog tag and a finite timeout. Pattern 2 in this article demonstrates a version that returns FALSE on either a frozen tag or a runaway. Pair with a global alarm tag that fires once per stuck event so the control room can be notified immediately.

Back to blog