Overview: WinCC C Action Scripts and Bitwise DWORD Manipulation
The script under analysis is a WinCC V7.x ANSI C action bound to the OnLButtonDown mouse event of a graphics object. It demonstrates a recurring pattern in WinCC HMI development: reading a packed DWORD control or status word from the DataManager, forcing individual bits, and writing the result back to the PLC. Understanding the exact behaviour of the bitwise operators, the WinCC C API, and the underlying tag storage layout is essential before deploying similar code to production panels running WinCC Runtime Advanced, WinCC Runtime Professional, or a PC-based WinCC V7 station.
WinCC exposes two scripting environments: VBScript (originating from WinCC flexible, still supported in V7 for compatibility) and ANSI C via the WinCC C Editor. C actions compile to native WinCC function blocks and execute with deterministic timing on the order of microseconds, which is why they remain the preferred tool for high-frequency bitwise operations, scan-time-critical toggles, and tight polling loops. VBScript is interpreted and adds overhead in the range of 1–3 ms per call, which becomes significant when a C action is bound to a 250 ms trigger or to a screen-object event that fires on every mouse movement.
Function Signature and Trigger Event
The prototype originates in apdefap.h, the standard header inserted automatically by the WinCC C Editor for action projects:
void OnLButtonDown(char* lpszPictureName,
char* lpszObjectName,
char* lpszPropertyName,
UINT nFlags,
int x, int y);
| Parameter | Type | Description |
|---|---|---|
| lpszPictureName | char* | Name of the active picture (PDL file) hosting the clicked object. Empty in global actions. |
| lpszObjectName | char* | Name of the clicked graphics object as defined in Graphics Designer. |
| lpszPropertyName | char* | Property under which the event is configured. Typical values: EventState for static events, ... placeholder for dynamic event bindings. |
| nFlags | UINT | Virtual-key and mouse-state flags. Test with MK_LBUTTON, MK_SHIFT, MK_CONTROL, MK_RBUTTON etc. |
| x, y | int | Cursor coordinates in pixels relative to the top-left of the object. |
The action returns void. Any value placed in the return slot is ignored by the WinCC dispatcher. The function executes in the same thread as the Graphics Runtime, so blocking calls are permitted but should be kept under the configured screen-cycle time to avoid UI freeze.
TAGNAME_SECTION and PICNAME_SECTION Macros
The TAGNAME_SECTION_START / TAGNAME_SECTION_END block is a WinCC-specific C preprocessor wrapper. The C Editor intercepts these markers at build time and emits the appropriate GetTag / SetTag prototype declarations so that the tag is bound to a handle the moment the action is compiled. The legacy syntax is:
// WINCC:TAGNAME_SECTION_START
// syntax: #define TagNameInAction DMTagName
// next TagID : 1
#define tag0 "name"
// WINCC:TAGNAME_SECTION_END
The preprocessor substitutes tag0 with the string literal "name", which the WinCC C Editor then resolves against the project DataManager. The numeric suffix 0 is a local alias index; the same tag can be aliased multiple times within a single action as tag0, tag1, etc., which is useful when an action reads and writes the same tag in different logical contexts. The next TagID comment is informational only; the actual ID is assigned automatically.
The PICNAME_SECTION in the script is empty because the body never calls GetPictureName, SetPictureName, or any of the picture-navigation helpers. If the script needed to switch pictures, the macros would be filled in here.
HMIRuntime.Tags object, which resolves tags at runtime and returns Nothing on a missing tag.Bit Manipulation: SET, CLEAR, TOGGLE
Three primitive operations exist for DWORD tags in WinCC C:
| Operation | C Expression | Effect on bit n | Idempotent? |
|---|---|---|---|
| SET (force to 1) | value | (1u << n) |
Bit becomes 1 regardless of previous state | Yes |
| CLEAR (force to 0) | value & ~(1u << n) |
Bit becomes 0 regardless of previous state | Yes |
| TOGGLE (invert) | value ^ (1u << n) |
Bit flips: 0→1 or 1→0 | No (depends on prior state) |
| TEST (read only) | (value >> n) & 1u |
Returns 0 or 1; does not modify value | N/A (pure) |
Idempotence is critical for command logic: pressing the same button five times in a row should not toggle a bit five times. Only SET and CLEAR are idempotent. The script under review uses SET (bit 6) and CLEAR (bits 5, 7, 8, 9), which is the correct pattern for command-bit manipulation.
Step-by-Step Analysis of the Script Body
Step 1: Read the current DWORD value
DWORD dwHelp1;
dwHelp1 = GetTagDWordWait(tag0);
GetTagDWordWait is the synchronous read primitive. The action blocks until the DataManager returns the most recent value. The non-blocking counterpart, GetTagDWord, returns whatever the local cache holds, which may be stale by up to one acquisition cycle (typically 250 ms or 1 s depending on the configured update time). For read-modify-write sequences the blocking variant is mandatory; otherwise the write can clobber a value the PLC has just updated.
The DWORD type is unsigned long on every WinCC-supported platform (32-bit and 64-bit Windows). It is guaranteed to be at least 32 bits and to wrap modulo 2^32.
Step 2: SET bit 6 (value 0x40 = 64 decimal)
DWORD dwHelp2;
dwHelp2 = dwHelp1 | 64;
SetTagDWordWait(tag0, dwHelp2);
Decimal 64 equals hex 0x40 equals binary 0000 0000 0100 0000. The OR operation forces bit 6 to 1 and leaves every other bit unchanged. The result is written back to the DataManager via SetTagDWordWait, which blocks until the PLC acknowledges receipt (default timeout 5 s, configurable per tag in the channel diagnosis).
Step 3: CLEAR bits 5, 7, 8, 9 in sequence
DWORD dwHelp3, dwHelp4;
dwHelp3 = GetTagDWordWait(tag0);
dwHelp4 = dwHelp3 & (0xFFFFFFFF ^ 0x20); // clear bit 5
SetTagDWordWait(tag0, dwHelp4);
dwHelp3 = GetTagDWordWait(tag0);
dwHelp4 = dwHelp3 & (0xFFFFFFFF ^ 0x80); // clear bit 7
SetTagDWordWait(tag0, dwHelp4);
dwHelp3 = GetTagDWordWait(tag0);
dwHelp4 = dwHelp3 & (0xFFFFFFFF ^ 0x100); // clear bit 8
SetTagDWordWait(tag0, dwHelp4);
dwHelp3 = GetTagDWordWait(tag0);
dwHelp4 = dwHelp3 & (0xFFFFFFFF ^ 0x200); // clear bit 9
SetTagDWordWait(tag0, dwHelp4);
Each block follows the same shape: re-read, modify, write. The four masks 0x20, 0x80, 0x100, and 0x200 are inverted by XOR with all-ones (0xFFFFFFFF). The AND with the inverted mask zeros the corresponding bit. The four write operations each generate an independent telegram to the PLC, which doubles PLC scan load and creates a four-step race window.
Bit Position Reference for the Script
| Constant | Hex | Decimal | Binary (bits 11..0) | Bit Position | Effect on tag |
|---|---|---|---|---|---|
| 64 | 0x040 | 64 | 0000 0100 0000 | 6 | SET (force to 1) |
| 0x20 | 0x020 | 32 | 0000 0010 0000 | 5 | CLEAR (force to 0) |
| 0x80 | 0x080 | 128 | 0000 1000 0000 | 7 | CLEAR (force to 0) |
| 0x100 | 0x100 | 256 | 0001 0000 0000 | 8 | CLEAR (force to 0) |
| 0x200 | 0x200 | 512 | 0010 0000 0000 | 9 | CLEAR (force to 0) |
Why Decimal and Hexadecimal Notations Coexist
Both notations compile to identical machine code. The WinCC C Editor uses the standard C preprocessor, which accepts 64, 0x40, 0100 (octal — discouraged), and 64U as the same 32-bit constant. Production WinCC projects adopt either notation by convention:
| Notation | Best Used For | Example | Drawback |
|---|---|---|---|
| Decimal | Single-bit masks matching a "human" power of two (1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024 ...) | flag | 64; |
Bit position is implicit; for 0x100 the bit position (8) is not obvious |
| Hexadecimal | Multi-bit masks, masks above bit 7, or situations where the bit position is the focus | flag & 0xFFFFFF7F; |
Requires mental hex-to-binary conversion for bit position |
| Shift expression | Computed bit positions, loops, named-bit access | flag | (1u << 6); |
One extra CPU instruction on some legacy targets |
The author's choice — decimal 64 for the SET mask, hex for the four CLEAR masks — is consistent with the convention: 64 is a clean power-of-two that reads naturally in decimal, while the CLEAR masks need to span 32 bits (the leading 0xFF bytes are explicit in hex) and the bit positions are easier to count in hex. Mixing notations within one function is idiomatic and is not flagged by the compiler or by the WinCC static analyser.
Why the Script Is Not XOR (Correcting a Common Misreading)
Community responses to the original posting stated that bits 9, 11, 12, 13 are "invertet mit exclusive OR" (inverted with exclusive OR). This interpretation is incorrect. The expression:
dwHelp3 & (0xFFFFFFFF ^ 0x80)
is equivalent to dwHelp3 & ~0x80 or dwHelp3 & 0xFFFFFF7F. This is a bit CLEAR, not an XOR with the tag. The inner ^ operates on the two integer constants 0xFFFFFFFF and 0x80 — it builds the inverted mask 0xFFFFFF7F — but the ^ never sees the tag value dwHelp3. To toggle a bit in the tag itself, the code would have to read dwHelp3 ^ 0x80 and store the result, with no AND wrapping the XOR. The script does not do that; it clears.
Refactored Equivalent: One Read, One Write
Reading and writing the tag four times is unnecessary and creates a race window. The same effect in a single read-modify-write cycle:
#include "apdefap.h"
void OnLButtonDown(char* lpszPictureName, char* lpszObjectName,
char* lpszPropertyName, UINT nFlags, int x, int y)
{
#define TAG_NAME "name"
DWORD dwValue;
/* Single read */
dwValue = GetTagDWordWait(TAG_NAME);
/* Modify: set bit 6, clear bits 5, 7, 8, 9 */
dwValue |= 0x40UL; /* SET bit 6 */
dwValue &= ~(0x020UL | 0x080UL |
0x100UL | 0x200UL); /* CLEAR bits 5, 7, 8, 9 in one expression */
/* Single write */
SetTagDWordWait(TAG_NAME, dwValue);
#undef TAG_NAME
}
The combined mask 0x020 | 0x080 | 0x100 | 0x200 equals 0x3A0. The expression ~0x3A0 equals 0xFFFFFC5F, which zeros bits 5, 7, 8, and 9 in a single AND instruction. Compared to the original four-write version, this refactor reduces PLC telegrams from 5 to 2, halves the round-trip time, and eliminates the three intermediate race windows.
WinCC C API Reference for Tag I/O
| Function | Header | Behavior | Returns | Use When |
|---|---|---|---|---|
| GetTagDWord | apdefap.h | Non-blocking read; returns last cached value from DM | DWORD | Display logic, diagnostics, latched values |
| GetTagDWordWait | apdefap.h | Blocking read; waits for fresh value from AS | DWORD | Read-modify-write sequences, control actions |
| SetTagDWord | apdefap.h | Non-blocking write to DM cache | BOOL (success) | Status flags, internal HMI tags |
| SetTagDWordWait | apdefap.h | Blocking write; waits for PLC acknowledgement | BOOL (success) | Command bits, control words, handshakes |
| GetTagRaw | apdefap.h | Read raw bytes; bypasses conversion | BOOL + buffer | Binary protocol payloads, packed structs |
Each blocking call uses the configured wait time, default 5 s, configurable per channel and per tag in the Tag Management → Channel Diagnosis dialog. If the timeout elapses, the function returns zero and the GDiagnosticError tag is set on the action's internal error object.
Memory Layout and Endianness
WinCC stores tags in the local system byte order. On every x86 and x64 target supported by WinCC V7 (Windows 7 SP1 / Windows 10 / Windows Server 2012 R2 and later), the system is little-endian. This means that for the DWORD value 0x12345678:
- Byte at offset 0 =
0x78(least significant) - Byte at offset 1 =
0x56 - Byte at offset 2 =
0x34 - Byte at offset 3 =
0x12(most significant)
Bit 0 of the DWORD is the LSB of byte 0, which is the rightmost bit of the 8-bit field. The C expression (value >> n) & 1u therefore reads bit n of the WinCC-side DWORD.
DB1.DBX0.0 corresponds to bit 8 of the WinCC DWORD, not bit 0. Mismatches here are the single most common cause of "the HMI clears the wrong bit" field reports. The fix is to swap bytes on the PLC side (SWAP instruction) or to use a STRUCT tag in the WinCC channel that defines the bit order explicitly.PLC Integration Patterns
Command-Status Pattern (Most Common)
The script fits the command-status pattern: a single 32-bit DWORD contains both command bits (HMI → PLC) and status bits (PLC → HMI). Typical layout:
| Bit | Direction | Meaning in this script |
|---|---|---|
| 0–4 | — | Unchanged (other commands / statuses) |
| 5 | HMI → PLC | CLEARed on click (e.g., "acknowledge") |
| 6 | HMI → PLC | SET on click (e.g., "start") |
| 7 | HMI → PLC | CLEARed on click (e.g., "stop auxiliary") |
| 8 | HMI → PLC | CLEARed on click |
| 9 | HMI → PLC | CLEARed on click |
| 10–31 | — | Unchanged |
Handshake Pattern
For a robust start/ack handshake, the recommended pattern is to SET a bit, wait for the corresponding status bit to come back from the PLC, then CLEAR the command bit. A minimal C action for that sequence:
DWORD dw = GetTagDWordWait("Control");
dw |= 0x40; /* SET start command */
SetTagDWordWait("Control", dw);
/* Wait up to 3 s for the matching status bit */
DWORD t0 = GetTickCount();
while ( (GetTickCount() - t0) < 3000 )
{
DWORD st = GetTagDWordWait("Status");
if ( st & 0x40 ) break; /* handshake complete */
Sleep(50);
}
/* Clear the command bit regardless */
dw = GetTagDWordWait("Control");
dw &= ~0x40UL;
SetTagDWordWait("Control", dw);
Combining this with the original SET/clear pattern, the script is consistent with a "start with auxiliary disables" command: bit 6 is set to enable the function; bits 5, 7, 8, 9 are cleared to suppress any overlapping modes that must not run concurrently.
Verification Procedure
- Open WinCC Explorer → Tag Management → confirm tag
nameexists as typeUnsigned 32-bit value(DWORD). - In Graphics Designer, place a numeric I/O field bound to tag
name; configure display format = Decimal, output = yes. - Place a button on the same picture. Configure Event → Mouse → LButtonDown → C action; paste the refactored single-block version of the script.
- Compile the action (button → "Compile" in the C Editor) and confirm no errors in the output window.
- Activate the runtime.
- Click the button. The displayed DWORD should equal the previous value with bit 6 forced to 1 and bits 5, 7, 8, 9 forced to 0; all other bits unchanged.
- Open Windows Calculator in Programmer mode. Enter the displayed decimal value, switch to BIN. Verify that bits 5, 7, 8, 9 read 0 and bit 6 reads 1.
- If a PLCSIM or S7-1500 simulation is connected, monitor the corresponding DB or MW in TIA Portal and confirm the four bits transition as expected.
Field Commissioning Diagnostics
| Symptom | Likely Cause | Diagnostic Step |
|---|---|---|
| Bit 6 toggles but other bits also change | PLC is overwriting the DWORD between read and write | Reduce action frequency; use SetTagDWordWait with implicit lock; or move the logic to a single SetTagDWord call per cycle |
| Action compiles but runtime shows "Tag not found" | Tag deleted from DataManager after action was compiled | Recompile the action (right-click → Compile) to refresh the handle |
| Click does nothing; GDiagnosticError = 0x80070005 | Access denied — typically insufficient user authorization | Check User Administrator → Authorization levels; the operator role must include the tag's assigned level |
| Bit position appears off by 8 | S7 byte-swap not applied to the channel | Verify channel configuration; in the S7-TCP/IP channel, enable "Optimized block access" or insert a SWAP on the PLC |
| Action freezes the screen for several seconds | SetTagDWordWait blocked, PLC handshake timeout | Reduce the per-tag acquisition timeout; check PLC connection in WinCC Channel Diagnosis |
| Bit is set but PLC ignores it | PLC logic requires a rising edge detector and the bit was already 1 in the previous cycle | Add a "command pulse" pattern: SET bit, wait one PLC cycle, CLEAR bit (or use the handshake pattern above) |
Performance Notes
A single read-modify-write of a DWORD via the legacy MPI / PROFIBUS channel typically takes 8–20 ms; via Industrial Ethernet (ISO-on-TCP or S7-TCP/IP) it takes 3–8 ms. The original four-write version therefore burns roughly 12–80 ms of UI thread time per click, plus three extra PLC scan cycles. On a Comfort Panel with a 250 ms screen cycle, this is acceptable; on a Basic Panel with a 1000 ms screen cycle, a single click can dominate the cycle and freeze the screen until completion. The refactored single-write version fits in one cycle on every panel.
Code Review Checklist
- Tag name is a quoted string literal in the
TAGNAME_SECTION; spelling matches DataManager exactly. - Each blocking call (
...Wait) is justified by an actual need for fresh data, not used by default. - Bit masks use the
ULsuffix on constants to guarantee 32-bit unsigned arithmetic. - Multiple bit operations on the same tag are batched into one read and one write.
- No XOR is used where AND-with-inverted-mask is intended (and vice versa).
- If the action is bound to a global keyboard event rather than a per-object mouse event, the function signature is
OnKeyDownwith the same parameters minus thex, yoffsets.
WinCC Version Compatibility
The apdefap.h header and the TAGNAME_SECTION / PICNAME_SECTION macros are documented in WinCC V7.0 through V7.5 SP2 and in the corresponding WinCC Runtime Advanced / Professional variants used with TIA Portal V13 through V18 for legacy C actions imported from V7 projects. TIA Portal's native scripting is based on the .NET Framework and exposes C# and VB via the HMIRuntime object model; the legacy ANSI C function library is not available in fresh TIA Portal WinCC Professional projects. For new TIA Portal development, the equivalent bitwise operation is performed with HMIRuntime.Tags in C#:
uint val = (uint)Tags["name"].Read();
val |= 0x40u; // SET bit 6
val &= ~(0x20u | 0x80u | 0x100u | 0x200u); // CLEAR bits 5, 7, 8, 9
Tags["name"].Write(val);
For complex or large C-action projects, developers often pair the WinCC C Editor with a full external IDE such as Visual Studio for IntelliSense, refactoring, and static analysis. The Visual Studio C/C++ toolset is documented at Visual Studio C/C++ IDE and Compiler for Windows. The external IDE is used read-only; the source is then pasted into the WinCC C Editor for compilation, because the WinCC Editor injects the project-specific tag handles and channel bindings that the runtime requires.
Frequently Asked Questions
What does GetTagDWordWait do inside a WinCC C action?
It performs a synchronous read of a DWORD tag from the WinCC DataManager and blocks until the value is returned from the PLC (or the configured wait time elapses). It is the correct primitive for read-modify-write sequences on control words or status flags where a stale value would corrupt the next write.
Why does the script mix decimal 64 and hex constants like 0x80?
Decimal 64 and hex 0x40 represent the same single-bit mask for bit 6. Mixing notations is a style choice; the C preprocessor treats them as the same 32-bit constant. Use hex when the mask spans multiple bits or when the bit position is the focus, and decimal when the value is a clean power of two.
Does the expression (0xFFFFFFFF ^ 0x80) toggle a bit in the tag?
No. The XOR combines the two integer constants 0xFFFFFFFF and 0x80 to produce the inverted mask 0xFFFFFF7F. That mask is then ANDed with the tag value, which clears bit 7. To toggle a bit in the tag itself, the code must read value ^ 0x80 and store the result, with no AND wrapping the XOR.
How do I clear multiple bits in a single WinCC C expression?
Combine the bit masks with OR, then invert and AND: value & ~(0x20 | 0x80 | 0x100 | 0x200). This performs four clears in one CPU instruction on 32-bit targets and reduces PLC telegrams from five to one compared to the original sequential pattern.
Is the legacy WinCC C scripting API still supported in TIA Portal?
WinCC V7.x continues to ship the ANSI C API with apdefap.h and the TAGNAME_SECTION macros. TIA Portal WinCC Professional / Comfort replaced C actions with C# and VB scripts based on the .NET Framework; the legacy C function library is not available in those environments, and new TIA Portal projects use the HMIRuntime.Tags object instead.
Why does the clear-bit position sometimes appear off by eight bits on an S7-1500?
WinCC stores DWORDs in little-endian byte order; S7-1500 user-visible bit numbering starts at bit 0 of the high byte. The WinCC channel usually handles the swap transparently for S7-1500 tags mapped by symbolic name, but a raw MW or DBW mapping may require a SWAP instruction on the PLC side or an explicit struct tag in the channel to align the bit positions.