Troubleshooting Multiple Object Animation in WinCC V7 Global Script C
Symptom: an operator presses two pushbuttons (or the PLC toggles two output bits in the same OB1 scan) and only one animated object moves on the WinCC picture. The conveyor scrolls but the piston is frozen, or the piston drives forward but the workpiece on the conveyor does not translate. Each individual animation runs correctly when triggered in isolation. The combined motion does not.
This failure mode is not a PLC bug, not a tag-prefix mistake, and not a Graphics Designer fault. It is a side-effect of how WinCC V7 schedules ANSI-C global actions across its two internal script queues. This article reconstructs the failure, isolates the root cause, and provides drop-in ANSI-C and VBS replacements that animate any number of objects in lockstep.
Problem Details
Reported Behavior
A WinCC V7 project drives a screen animation through three independent global C actions:
-
ConveyorAction— triggered on change of bitDC_bt1(PLC tagQ124.0). Loops six rectangles (hcn1throughhcn6) leftward across the picture at 1 pixel per iteration until the rectangles exit at the left edge of the conveyor bodyBT1, at which point the rectangles reappear on the right edge. -
PistonAction— triggered on change of bitDC_capSP(PLC tagQ124.1). Drives the piston rodCanDaySPBT1from its rest position towardx = 1017at 1 pixel per iteration. - Embryo-plastic action — driven by output bit
Q124.2. Inherits the same code skeleton as the two above.
Source Code (Failing Pattern)
/* ConveyorAction - WINCC:GLOBAL_ACTION tag-triggered on DC_bt1 */
#include "apdefap.h"
int gscAction( void )
{
float xb, xbb;
float xa1, xa2, xa3, xa4, xa5, xa6;
xb = GetLeft("HeThongChinh.Pdl","BT1");
xbb = GetWidth("HeThongChinh.Pdl","BT1") + xb;
while ( GetTagBit("DC_bt1") == 1 )
{
xa1 = GetLeft("HeThongChinh.Pdl","hcn1");
if ( xa1 >= xb ) xa1 = xa1 - 1;
else xa1 = xbb - GetWidth("HeThongChinh.Pdl","hcn1");
SetLeft("HeThongChinh.Pdl","hcn1", xa1);
xa2 = GetLeft("HeThongChinh.Pdl","hcn2");
if ( xa2 >= xb ) xa2 = xa2 - 1;
else xa2 = xbb - GetWidth("HeThongChinh.Pdl","hcn1");
SetLeft("HeThongChinh.Pdl","hcn2", xa2);
/* ... hcn3..hcn6 identical pattern ... */
}
return 0;
}
/* PistonAction - WINCC:GLOBAL_ACTION tag-triggered on DC_capSP */
#include "apdefap.h"
int gscAction( void )
{
float mr2, mr2s, mr1;
mr2s = 1017;
mr2 = GetLeft("HeThongChinh.Pdl","CanDaySPBT1");
mr1 = GetLeft("HeThongChinh.Pdl","BT1") + GetWidth("HeThongChinh.Pdl","BT1");
while ( GetTagBit("DC_capSP") == 1 || ( mr2 < mr2s ) )
{
if ( mr1 <= mr2 ) mr2 = mr2 - 1;
else mr2 = mr2s;
SetLeft("HeThongChinh.Pdl","CanDaySPBT1", mr2);
}
return 0;
}
When each tag is flipped individually with S7-PLCSIM, the corresponding action animates smoothly. When two tags change in the same OB1 cycle — Q124.0 and Q124.1 both rising, for example — only one of the two animations runs. The user reports "the conveyor moves, the piston freezes" or "the piston moves, the conveyor freezes," and the loser is whichever action was registered first in the WinCC scheduler.
Expected Behavior
Both the conveyor rectangles and the piston rod should animate simultaneously, with frame-to-frame position deltas in sync with the operator's view of the process. This is the standard behavior of WinCC V7 when configured correctly.
Root Cause
WinCC V7 Global Script Scheduler Topology
WinCC V7 exposes two independent runtime queues for ANSI-C global actions, as documented in the official scripting manual. From the Siemens support portal, "Scripting for WinCC and WinCC Professional":
In global scripting, there are two queues for ANSI-C: one queue for cyclical triggers (periodically, e.g. "1 second"), and one queue for tag triggers.
The relevant characteristics of these queues for the reported bug:
| Queue | Trigger Source | Service Cycle | Concurrency Model |
|---|---|---|---|
| Cyclical queue | Periodic timer (250 ms, 500 ms, 1 s, custom) | Fixed, deterministic | Single-threaded cooperative; each action runs to completion before the next is dispatched |
| Tag-trigger queue | Tag value change (rising, falling, both, any) | Best-effort, processed by the Tag-Logging / Runtime scheduler | Same as above; tag-change events are queued and dispatched one at a time per tag-trigger action |
Both queues are strictly single-threaded. An action that is currently running blocks the queue. If a second tag changes while the first action is inside its while() loop, the second action's trigger event is buffered but cannot fire until the first action returns.
The Real Culprit: Blocking while() Loops Inside Tag-Triggered Actions
The source code embeds an infinite while ( GetTagBit(...) == 1 ) loop inside each tag-triggered global action. This pattern is the actual cause of the apparent "only one thing moves" symptom. The mechanics are:
- PLC toggles
DC_bt1from 0 to 1 in OB1 cycle N. - WinCC Runtime detects the tag change, schedules
ConveyorActionin the tag-trigger queue. - The dispatcher starts
ConveyorAction, which enters thewhile()loop. The loop never returns as long asDC_bt1 == 1, and the dispatcher cannot preempt it. - PLC toggles
DC_capSPfrom 0 to 1 in OB1 cycle N+1 (or the operator presses a second button). - WinCC Runtime buffers the tag-change event for
PistonAction. - PLC toggles
DC_bt1back to 0 in OB1 cycle N+k. Thewhile()condition fails; the action returns0; the queue dispatchesPistonAction;PistonActionenters its ownwhile()loop and runs to completion.
From the operator's perspective, only the conveyor (or only the piston, depending on which while() exits first) is visible moving at any given moment. The two animations never overlap in time.
A Secondary Defect: Read-Modify-Write Inefficiency
Inside the conveyor loop, each iteration performs six GetLeft() calls, six arithmetic branches, and six SetLeft() calls. With C-function call overhead, tag-pre-prefix resolution, and the Graphics Designer property setter, a single iteration can take 5 to 20 ms per object on a typical WinCC station. With six objects, a single screen refresh cycle takes 30 to 120 ms — long enough that the visible motion stutters even when running alone.
Hidden Bug in the Piston Code
Inside the piston loop, the wrap-around reference is hard-coded:
mr2 = mr2s; /* jump back to 1017 when mr1 <= mr2 */
This resets the rod to the far-right extreme whenever the conveyor reference crosses it, producing a visible snap rather than a continuous extension. Acceptable in some animations, but the loop semantics should be while ( GetTagBit(...) == 1 ) only — not while ( tag == 1 || (mr2 < mr2s) ). The compound condition causes the piston to keep sliding right even after the operator releases the button, which is rarely the desired physical behavior.
Solution
The fix is structural, not cosmetic. Three changes are required:
- Move all animations out of tag-triggered global actions.
- Drive them from a single cyclical global action (recommended 100 to 250 ms period).
- Consolidate per-frame work into one read-calculate-write pass.
Optionally replace ANSI-C with VBS for visibly smoother motion. Per the Siemens scripting manual, VBS carries lower per-call overhead for Graphics Designer property setters than ANSI-C, because the VB interpreter marshals the Get/Set calls through COM without crossing the C-runtime function-call boundary that ANSI-C requires.
Pattern A — Single ANSI-C Cyclical Action Driving All Objects
Create a global C function that holds the per-frame motion logic for every object. Create a single cyclical global action that calls it every 250 ms. The trigger for the action is periodic, not a tag.
/* Project function: Anim_Frame — called from cyclical action */
#include "apdefap.h"
void Anim_Frame( void )
{
/* Read phase — read tags and current positions ONCE */
int bConveyor = GetTagBit("DC_bt1");
int bPiston = GetTagBit("DC_capSP");
float convLeft = GetLeft("HeThongChinh.Pdl","BT1");
float convWidth = GetWidth("HeThongChinh.Pdl","BT1");
float convRight = convLeft + convWidth;
float hcnW = GetWidth("HeThongChinh.Pdl","hcn1");
float hcnX[6];
int i;
char name[16];
for ( i = 0; i < 6; i++ )
{
sprintf( name, "hcn%d", i + 1 );
hcnX[i] = GetLeft("HeThongChinh.Pdl", name);
}
float rodX = GetLeft("HeThongChinh.Pdl","CanDaySPBT1");
float rodTarget = 1017.0f;
/* Calculate phase */
if ( bConveyor == 1 )
{
for ( i = 0; i < 6; i++ )
{
hcnX[i] -= 2.0f; /* 2 px per cycle, smoother */
if ( hcnX[i] < convLeft )
hcnX[i] = convRight - hcnW;
}
}
if ( bPiston == 1 )
{
rodX += 4.0f; /* 4 px per cycle */
if ( rodX > rodTarget ) rodX = convRight; /* retract to start */
}
/* Write phase — set positions ONCE */
for ( i = 0; i < 6; i++ )
{
sprintf( name, "hcn%d", i + 1 );
SetLeft("HeThongChinh.Pdl", name, hcnX[i]);
}
SetLeft("HeThongChinh.Pdl","CanDaySPBT1", rodX);
}
/* Global action: ConveyorAndPiston — trigger = 250 ms cyclic */
#include "apdefap.h"
int gscAction( void )
{
Anim_Frame();
return 0;
}
Register the global action with trigger Standard cycle / 250 ms instead of Tag trigger. Both animations are now evaluated from the same frame tick. They cannot drift relative to each other because they share the read-calculate-write boundary.
Pattern B — VBS Cyclical Action (Lower Latency Per Frame)
For HMI stations that show visible juddering under ANSI-C, convert the per-frame logic to VBS and register the action with the same 250 ms cyclic trigger:
' Global action: ConveyorAndPiston_VBS — trigger = 250 ms cyclic
Sub ConveyorAndPiston_VBS()
Dim bConv, bPist, convL, convR, hcnW, rodT, rodX
Dim hcnX(5), i, objName
bConv = HMIRuntime.Tags("DC_bt1").Read
bPist = HMIRuntime.Tags("DC_capSP").Read
convL = HMIRuntime.Screens("HeThongChinh").ScreenItems("BT1").Left
convR = convL + HMIRuntime.Screens("HeThongChinh").ScreenItems("BT1").Width
hcnW = HMIRuntime.Screens("HeThongChinh").ScreenItems("hcn1").Width
rodT = 1017
rodX = HMIRuntime.Screens("HeThongChinh").ScreenItems("CanDaySPBT1").Left
For i = 0 To 5
objName = "hcn" & (i + 1)
hcnX(i) = HMIRuntime.Screens("HeThongChinh").ScreenItems(objName).Left
Next
If bConv = True Then
For i = 0 To 5
hcnX(i) = hcnX(i) - 2
If hcnX(i) < convL Then hcnX(i) = convR - hcnW
Next
End If
If bPist = True Then
rodX = rodX + 4
If rodX > rodT Then rodX = convR
End If
For i = 0 To 5
objName = "hcn" & (i + 1)
HMIRuntime.Screens("HeThongChinh").ScreenItems(objName).Left = hcnX(i)
Next
HMIRuntime.Screens("HeThongChinh").ScreenItems("CanDaySPBT1").Left = rodX
End Sub
Pattern C — Function Library Called From Cyclical Action (Recommended for WinCC V7.0 SP3 and Later)
For larger projects, factor the animation into a separate function in the Global Script project module:
- In the WinCC Explorer tree, right-click Global Script → Project Functions, choose New → Function, name it
Anim_Frame. - Paste the function body from Pattern A above.
- Create a new Global Action with trigger Standard cycle 250 ms, body
Anim_Frame();.
This keeps the dispatch logic trivial and the animation logic reusable from other actions (e.g., a "Stop All" button that calls Anim_Reset()).
Verification
After applying Pattern A, B, or C, confirm the fix with the following checks.
Step-by-Step Verification
-
Compile the global action. In the editor, choose File → Compile. Any syntax error in the C or VBS body halts the action and WinCC Runtime logs a
Script error: line Nentry inWinCC_SysLog_*.LOG. Confirm the log contains no error lines for the new action. -
Activate WinCC Runtime. Open the project, switch to runtime, navigate to
HeThongChinh.Pdl. -
Toggle one tag. In S7-PLCSIM, force
Q124.0 = 1. The six rectangles should scroll left at 2 pixels per 250 ms tick (~8 px/s), wrapping around at the conveyor's right edge. -
Toggle the second tag while the first is still set. Force
Q124.1 = 1without resettingQ124.0. The piston rod should extend at 4 px per tick (~16 px/s) while the conveyor continues to scroll. - Reset both tags. Force both bits to 0. Both animations should freeze at their current positions on the next 250 ms tick.
-
Toggle in reverse order. Force
Q124.1first, thenQ124.0whileQ124.1is still set. Both animations should still run in lockstep.
Diagnostic Output to Confirm Single-Tick Execution
For field debugging, add a transient tag to write the action's execution count:
/* inside Anim_Frame, top of function */
long n = GetTagDWord("Anim_Frame_Counter") + 1;
SetTagDWord("Anim_Frame_Counter", n);
Watch this tag in the tag monitor. It should increment at exactly 4 per second when the cyclical trigger is 250 ms. If it increments at the tag-change rate instead, the trigger is still misconfigured as a tag trigger.
Advanced Tuning
Frame-Rate Selection
| Cycle | Visual Effect | CPU Load (WinCC station, 100 objects) | Recommended Use |
|---|---|---|---|
| 100 ms | Smooth, near-60 Hz | High — 10 dispatches/s | Short, fast motions (jog, eject) |
| 250 ms | Smooth for most HMIs | Moderate — 4 dispatches/s | Conveyor, piston, dial (default) |
| 500 ms | Visible stepping | Low — 2 dispatches/s | Slow drift gauges, fill levels |
| 1000 ms | Discrete jumps | Minimal | Background indicators only |
Velocity as a Tag, Not a Constant
Hard-coding 2 px/tick inside the function forces recompilation whenever the line speed changes. Expose velocity as a tag:
float vConv = GetTagFloat("V_Conv_PxPerTick");
/* ... */
hcnX[i] -= vConv;
The PLC writes V_Conv_PxPerTick from a runtime scaling of the actual line speed. Operators can change it from a faceplate without recompiling the script.
Handling Picture Window Navigation
Global actions run regardless of which picture is open. If HeThongChinh.Pdl is in a picture window that is closed, the SetLeft() calls return -1 and log a warning. Guard against this with:
if ( GetLeft("HeThongChinh.Pdl","BT1") < 0 ) return; /* picture not loaded */
Or query GetOpenPictureName() from inside the action and only update when the target picture is foregrounded.
WinCC Unified Migration Note
If the project is later migrated to WinCC Unified (TIA Portal V17+), the C-API used here is not available. Use the JavaScript-based runtime scripting instead. The global-module structure is similar: project-side Global modules hold reusable functions, Global functions live inside them, and scheduled tasks register them as triggers. The conceptual mapping is direct:
| WinCC V7 ANSI-C | WinCC Unified JavaScript |
|---|---|
GetLeft() |
HMIRuntime.Screens(...).ScreenItems(...).Left |
SetTagBit() |
HMIRuntime.Tags(...).Write |
| Global action (cyclic) | Scheduled task (cyclic trigger) |
| Project function | Global module / global function |
Troubleshooting Matrix
| Symptom | Likely Cause | Fix |
|---|---|---|
| Only one object moves when two tags toggle | Each object in a separate tag-triggered action with blocking while()
|
Consolidate into one cyclical action |
| Animation stutters visibly | ANSI-C function-call overhead; too many Get/Set per cycle | Switch to VBS or raise cycle to 250 ms |
| Action never fires | Tag trigger filter is "rising" but bit toggles both edges | Set trigger to "change" or fix PLC edge logic |
| Pictures flicker | Cyclical action writes properties every cycle even when tag = 0 | Wrap Set calls in if (bConveyor == 1) guard |
Runtime reports Script error: line N
|
Bad object name, bad tag prefix, or wrong PDL reference | Cross-check in Graphics Designer and tag management |
| CPU at 100 % on the WinCC station | Cyclical trigger set to < 100 ms with many objects | Raise cycle to 250 ms or offload to VBS |
| Animation continues after tag reset | Compound while condition with position check |
Use while (tag == 1) only |
Common Pitfalls
while() inside a tag-triggered global action. The action cannot be preempted; the queue is blocked for the entire loop duration. Use a periodic trigger and read the tag inside.GetLeft / SetLeft pair.hcn1 ≠ HCN1. The field report uses lowercase; match exactly.Safety and Operational Considerations
These patterns drive visual feedback only. They do not replace hard-wired safety circuits, E-Stops, or PLC-side interlocks. The WinCC station may lose power, lose Runtime activation, or have its global scripts disabled for debugging — in any of these cases, the screen freeze is acceptable. Real motion control must remain inside the PLC's fail-safe logic, not in a graphics script.
References Used in This Article
All technical claims about queue architecture and ANSI-C / VBS performance characteristics are drawn from the official Siemens scripting documentation:
- Scripting for WinCC and WinCC Professional — Siemens Support (PDF, two-queue scheduler and ANSI-C vs VBS characteristics).
- Global modules (RT Unified) — TIA Portal documentation (for migration mapping).
FAQ
Why does only one object move when I trigger two animations at the same time in WinCC V7 Global Script C?
WinCC V7 runs ANSI-C global actions in a single-threaded scheduler with separate queues for cyclic and tag-triggered actions. If each animation is in its own tag-triggered action with a blocking while() loop, the second action cannot start until the first returns. Consolidate all animations into one cyclic global action (e.g., 250 ms) that reads all relevant tags, computes all new positions, and writes them in a single pass.
Is VBS really smoother than ANSI-C for WinCC screen animations?
For Graphics Designer property updates, yes. The VBS interpreter's COM marshalling path is shorter than the ANSI-C function-call → property-getter → COM path required for every GetLeft / SetLeft. For screen-object animation with 250 ms cycles, VBS typically shows less frame-to-frame jitter than ANSI-C.
What trigger type should I use for a continuously animated screen object in WinCC V7?
Use a standard cyclical trigger (typically 250 ms for conveyor / piston animations) on a single global action, and read the enabling tag inside the action body. Do not use a tag trigger for continuously animated motion; tag triggers are best for one-shot events such as state transitions.
How many objects can a single 250 ms WinCC V7 global action animate?
On a typical WinCC station (Intel i5 / i7, 8 GB RAM, WinCC V7.4 SP1), one 250 ms action can comfortably update 100 to 200 screen-object properties per frame. Beyond that, raise the cycle to 500 ms, split the work across two actions on alternating cycles, or reduce the number of objects.
Does this approach carry over to WinCC Unified in TIA Portal?
The conceptual pattern does, but the APIs differ. ANSI-C functions like GetLeft and SetLeft are replaced by JavaScript access through HMIRuntime.Screens(...).ScreenItems(...). Cyclical triggers become scheduled tasks. Project functions become global modules. See the TIA Portal runtime scripting documentation for the full mapping.