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
DWORDandBOOL. - Knowledge of the project function library location:
\for V7 or the project scripts tree in TIA Portal.\library\
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:
- The dispatcher marks the action as "running" until the function returns.
- All cyclic triggers that fall on that action's trigger tag are deferred.
- Alarm and trend logging share the same
script.exeprocess; long sleeps starve the log queue. - If the sleep exceeds 30 seconds, the WinCC watchdog logs event ID
1007001("Action time exceeded") and may restartscript.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
- Add the action to the Graphics Designer and start runtime.
- Open the tag diagnostics pad (WinCC V7: Tag Management → right-click tag → Properties → Start diagnosis) and watch
Wait_CurrentCountincrement once per second after a button click. - Confirm
Wait_RunStatetransitions 0 → 1 → 2 → 0 within 3 s plus one cycle. - Monitor the file timestamp of the renamed PDF; it should be N seconds (N = the wait target) after the click, never before.
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));
}
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
- Stop the cyclic updater. Click the wait button. Confirm the function returns
FALSEwithin 1 s instead of hanging. - Restart the cyclic updater. Click again. Confirm the rename happens after exactly 3 s.
- Set
dwTimeoutSeconds = 1and passdwSeconds = 5; confirm return isFALSEafter 1 s.
6. Pattern 3 — Dynamic Dialog with Visible-Property Trick
6.1 Implementation
- Create a base picture
WaitHelper.pdlcontaining a single invisible rectangle that covers the full screen. - Bind the picture window's Visible property in the parent picture to a dynamic dialog of tag
Wait_RunState— direct mapping. - 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 whenWait_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.
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:
- On the trigger tag value-change, write
GetTickCount()to internal tagDelay_Deadline. - Schedule a 250 ms cyclic action that compares
GetTickCount() - Delay_Deadlineagainst the target. - 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.exeCPU. 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 callHMIRuntime.Tagsfor tag access but does not exposeGetTickCount; useDateDiff("s", "1970-01-01", Now)instead, or switch to a C action. -
Logging: Wrap each payload call with
printfwrites 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:
- Configure the print job to write to a directory with a predictable filename prefix (
Report_) and a timestamp suffix. - Implement
RenameLatestPdfas a project function: list the directory, find the file with the latestFILETIME, and rename toReport_yyyyMMdd_HHmmss.pdf. - Wire the button click to call
StartWait(3). - 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.
- 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.
- 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.
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.