Resolving GetTagBitStateQC Read Failures in WinCC C Scripts
Reading the Quality Code of an HMI tag from a C script in WinCC Runtime is a routine requirement for diagnostics, OEM communication validation, and degraded-state handling. Engineers commonly hit a wall when GetTagBitStateQC returns FALSE on every call, even when the target tag exists and is online. This reference documents the root cause, the correct data types, the function-family alternatives, and the verified fixes that compile cleanly inside WinCC's CScript environment.
Problem Details
A typical failing implementation looks like this:
#define TagName "QualityCheckTagName"
#define QualityTagName "QualityCheckTagQuality"
PDWORD Quality;
PDWORD State;
BOOL bValue;
bValue = GetTagBitStateQC( GetTagChar(TagName), State, Quality );
if ( bValue )
{
MessageBox( NULL, "WORKED", "Tag to Write Quality To", MB_OKCANCEL | MB_SETFOREGROUND );
}
else
{
MessageBox( NULL, "FAILED", "Tag to Write Quality To", MB_OKCANCEL | MB_SETFOREGROUND );
}
Symptom matrix reported in the field:
| Symptom | Observed Behavior | Trigger |
|---|---|---|
Function returns FALSE
|
MessageBox shows FAILED on every cycle |
Pointer not initialized or tag-name argument wrapped in GetTagChar()
|
Compiler rejects DWORD
|
Type mismatch error during script compile | Prototype requires PDWORD
|
VB script QualityCode returns random numbers |
Integer returned instead of OPC quality flags | Wrong property used; .QualityCode is VBS-only and returns a packed integer |
Value is always 0xC0 / BAD_COMMUNICATION
|
Tag exists in the project but no value arrives | AS-OS connection broken or tag name misspelled |
Root Cause Analysis
Three independent defects are stacked in the original snippet. Each one is sufficient to break the read on its own; the combination makes the failure appear total.
Defect 1 — Uninitialized PDWORD Pointers
The WinCC C-API prototypes for state and quality output parameters are typed as PDWORD (pointer to DWORD). Declaring the pointers is not enough — they must point at a valid DWORD storage location. The snippet declares:
PDWORD Quality;
PDWORD State;
Both pointers are NULL at call time. When GetTagBitStateQC writes through them, the runtime detects a NULL pointer and returns FALSE. The runtime never sees the function arguments as a reason to fail — the function itself succeeded; the host wrapper aborted because the user-supplied destination addresses were unusable.
Defect 2 — Wrapping the Tag Name in GetTagChar()
The first argument is the tag name as a string literal. GetTagChar() returns the value of a tag whose data type is a string. When the tag used as a name holder actually contains the target tag's name at runtime, GetTagChar() is the correct indirection. However, the indirection only works if the holding tag has been written before the C action fires and the runtime dispatcher has propagated the value. A more reliable pattern is to pass the literal directly or to assign the dereferenced value to a local buffer first.
Defect 3 — VB Script QualityCode Misinterpretation
In WinCC V7.x VBScript the property is HMIRuntime.Tags("TagName").QualityCode. The property returns an integer packed bitfield of OPC DA quality flags, not a discrete enumeration. Engineers reading it as a string or comparing it to a numeric constant directly without bitwise masking get results that "look random" because they are seeing the full bit pattern including the Quality Sub-status and Limit bits.
PDWORD vs DWORD — Pointer Type Discipline
The WinCC C-API is documented with pointer types because the function writes output through the address. The header declarations are:
BOOL GetTagBitStateQC ( LPCTSTR lpszTagName, PDWORD pdwState, PDWORD pdwQuality );
BOOL GetTagBitStateQCWait( LPCTSTR lpszTagName, PDWORD pdwState, PDWORD pdwQuality );
The correct pattern is to declare value storage and pass its address:
DWORD dwState = 0xFFFFFFFF; // Pre-fill so an uninitialized state is detectable
DWORD dwQuality = 0;
BOOL bResult;
bResult = GetTagBitStateQC( "MyBitTag", &dwState, &dwQuality );
If a developer prefers the PDWORD declaration, it must be initialized:
DWORD dwState = 0;
DWORD dwQuality = 0;
PDWORD pState = &dwState;
PDWORD pQuality = &dwQuality;
bResult = GetTagBitStateQC( "MyBitTag", pState, pQuality );
The compiler will accept DWORD with & in the call because the address-of operator promotes a non-const lvalue to a pointer. The trade-off is that the variables remain mutable inside the function — desirable for output parameters, but it does mean the function can legally overwrite the source.
DWORD storage with a sentinel such as 0xFFFFFFFF or 0xDEADBEEF. If the function returns FALSE and the sentinel survives, the failure happened inside the WinCC dispatcher before the writeback. If the sentinel is overwritten with zero, the function wrote zero — which is itself a meaningful OPC quality state.Function Variants — Get vs GetWait
WinCC provides two flavors of every quality-aware getter:
| Function | Trigger | Blocking? | Use When |
|---|---|---|---|
GetTagBitStateQC |
On update / cycle | No (returns immediately with current cached value) | Display layer, periodic polling, diagnostics screens |
GetTagBitStateQCWait |
Forces synchronous refresh | Yes (blocks until PLC response or timeout) | One-shot verification, OEM handshake validation, pre-write sanity checks |
GetTagWordStateQC |
On update / cycle | No | Word-sized tags |
GetTagDWordStateQC |
On update / cycle | No | Double-word tags |
GetTagFloatStateQC |
On update / cycle | No | 32-bit float tags |
GetTagDoubleStateQC |
On update / cycle | No | 64-bit double tags |
GetTagCharStateQC |
On update / cycle | No | String / 8-bit tags |
GetTagRawStateQC |
On update / cycle | No | Raw data block tags |
The naming convention is consistent: Get<Type>StateQC for asynchronous, Get<Type>StateQCWait for synchronous. The Wait variants are heavier because they force the data manager to refresh the tag before returning; do not call them from a high-frequency (≤ 250 ms) scheduled action or you will stall the dispatcher.
Solution Implementation
Fix A — Minimal Correct C Action
// ------------------------------------------------------------------
// WinCC C action: read quality code of a binary tag and write
// the decoded text into a string tag for HMI display.
// ------------------------------------------------------------------
#include "apdefap.h"
void OnClick( char* lpszPictureName, char* lpszObjectName, char* lpszPropertyName )
{
#define TARGET_TAG "OEM_Heartbeat_Bit"
#define TEXT_TAG "OEM_QualityText"
DWORD dwState = 0xFFFFFFFF;
DWORD dwQuality = 0xFFFFFFFF;
BOOL bOK;
char szText[64];
// Force synchronous read so the result reflects current PLC state.
bOK = GetTagBitStateQCWait( TARGET_TAG, &dwState, &dwQuality );
if ( bOK )
{
// dwQuality is an OPC DA quality bitfield. Mask the major
// quality bits (mask 0xC0) and the sub-status bits (mask 0x3F).
switch ( dwQuality & 0xC0 )
{
case 0x00: // Bad
sprintf( szText, "BAD (0x%02X)", dwQuality );
break;
case 0x40: // Uncertain
sprintf( szText, "UNCERTAIN (0x%02X)", dwQuality );
break;
case 0xC0: // Good — only valid if all three quality bits are set
sprintf( szText, "GOOD (0x%02X)", dwQuality );
break;
default:
sprintf( szText, "RESERVED (0x%02X)", dwQuality );
break;
}
}
else
{
sprintf( szText, "READ FAILED" );
}
SetTagChar( TEXT_TAG, szText );
return;
}
Fix B — Dynamic Tag Name from Holding Tag
When the target tag name is computed at runtime and stored in a string tag, dereference it explicitly and pass the buffer, not the wrapper:
DWORD dwState = 0xFFFFFFFF;
DWORD dwQuality = 0xFFFFFFFF;
BOOL bOK;
char szTagName[256];
// szTagName is filled from a holder tag without the wrapper function
GetTagChar( "QualityCheckTagName", szTagName, 256 );
bOK = GetTagBitStateQCWait( szTagName, &dwState, &dwQuality );
Fix C — VBScript Alternative Using OPC Quality Bitfield
For engineers who prefer VBScript, the property QualityCode is valid but must be masked:
Dim oTag
Dim iQC
Dim sQuality
Set oTag = HMIRuntime.Tags("OEM_Heartbeat_Bit")
oTag.Read
iQC = oTag.QualityCode
' Mask the major quality (bits 6-7)
Select Case (iQC And &HC0)
Case &H00 sQuality = "BAD"
Case &H40 sQuality = "UNCERTAIN"
Case &HC0 sQuality = "GOOD"
Case Else sQuality = "RESERVED"
End Select
HMIRuntime.Trace "Quality=" & sQuality & " raw=0x" & Hex(iQC) & vbCrLf
The "random numbers" reported in the original question are the full 16-bit packed field including the Quality Sub-status, Limit bits, and Vendor extension bits. Once the engineer masks off the major quality bits, the value resolves to one of three meaningful categories.
Quality Code Reference
The OPC DA quality byte follows the layout below. WinCC tags inherit this bitfield unchanged from the underlying OPC server. Quality Codes of HMI Tags (RT Professional) documents the same bit interpretation for the TIA Portal generation.
| Mask (hex) | Bits | Field | Meaning |
|---|---|---|---|
| 0xC0 | 7-6 | Quality | 0x00 = Bad, 0x40 = Uncertain, 0xC0 = Good |
| 0x3F | 5-0 | Sub-status | Depends on Quality — see sub-tables below |
| 0x300 | 9-8 | Limit | 0 = Not limited, 1 = Low limited, 2 = High limited, 3 = Constant |
| 0xFC00 | 15-10 | Vendor | Manufacturer-specific extension |
Bad Sub-status Codes (Quality = 0x00)
| Code | Symbolic Name | Engineering Meaning |
|---|---|---|
| 0x08 | BadCommunicationError | PLC link down, AS-OS connection severed |
| 0x10 | BadOutOfService | Tag disabled by configuration |
| 0x18 | BadDeviceFailure | Hardware-reported fault (CPU in STOP, module pulled) |
| 0x28 | BadConfigurationError | Address misconfigured, datatype mismatch |
| 0x40 | BadLastKnownValue | Connection lost, last valid value held |
Uncertain Sub-status Codes (Quality = 0x40)
| Code | Symbolic Name | Engineering Meaning |
|---|---|---|
| 0x40 | UncertainSubstituteValue | Substitute value configured and in use |
| 0x44 | UncertainLastUsableValue | No fresh update; last good value retained |
| 0x50 | UncertainSensorNotCalibrated | Analog input not calibrated |
| 0x68 | UncertainUsableValue | Value usable but degraded (e.g., simulation) |
Good Sub-status Codes (Quality = 0xC0)
| Code | Symbolic Name | Engineering Meaning |
|---|---|---|
| 0xC0 | GoodNonSpecific | Tag value is valid, no special condition |
| 0xC4 | GoodLocalOverride | Value forced via HMI write |
| 0xD8 | GoodInitialValue | Tag initialized but no PLC read yet |
Verification Procedure
- Open the WinCC Graphics Designer and place a button on the test picture. Wire its Click event to a C action that contains Fix A above.
- Configure RT (Start Runtime) on the engineering station or transfer the project to a runtime PC.
-
Click the button while the target tag is good (PLC in RUN, connection up). The string tag
OEM_QualityTextmust readGOOD (0xC0). -
Disconnect the AS-OS link by stopping the S7 connection in WinCC Explorer (or pulling the network cable to the PLC). Wait for one full acquisition cycle (250 ms default). Click the button again. The text must read
BAD (0x08)orBAD (0x18). -
Restore the connection and confirm the text returns to
GOOD (0xC0)within one acquisition cycle. - Open the WinCC Diagnosis Viewer (GfxRtm.exe / DiagMonitor) and filter on the string tag to confirm the same quality transitions appear in the diagnostic log.
Troubleshooting Matrix
| Observed | Likely Cause | Remediation |
|---|---|---|
Always FALSE
|
NULL PDWORD passed |
Initialize DWORD storage and pass & address |
Always FALSE
|
Tag name not visible to runtime | Verify tag exists in Tag Management; check RT column for project enable |
Always FALSE
|
C action called before picture is loaded | Use GetTagChar only inside an event triggered after picture load |
| Type mismatch compile error | Prototype is PDWORD
|
Switch to DWORD + & or initialize PDWORD
|
| Quality = 0x08 | PLC link down | Check S7 connection status, network path, CPU in RUN |
| Quality = 0x18 | Hardware fault | Check CPU diagnostic buffer, module status |
| Quality = 0x28 | Configuration error | Confirm address, datatype, and DB access rights match PLC |
| Quality = 0x40 + 0x44 | Last usable value held | Normal during transient link loss; verify recovery |
| Quality = 0xC0 + 0xD8 | Initial value | No PLC read yet — verify tag is requested in acquisition cycle |
VBS QualityCode > 255 |
Property read returns 16-bit | Mask with And &HFF before comparing to byte constants |
GetTagBitStateQCWait hangs |
PLC unreachable, no timeout | Do not call Wait variants from cyclic actions; restrict to event-triggered code |
Performance Considerations
The synchronous Wait variants force a data-manager refresh on the calling thread. A 250 ms cyclic action that calls GetTagDWordStateQCWait against ten tags adds ten synchronous refreshes per cycle. Each refresh can stall the dispatcher for the duration of the AS-OS round trip (typically 50–200 ms on a healthy PROFINET, multiple seconds on a degraded link). The accepted pattern is:
-
Cyclic display layer: use non-
Waitvariants —GetTag*StateQC. -
Event-triggered verification: use
Waitvariants only inside button-click or value-change handlers that fire rarely. -
Trend or alarm qualification: use the non-
Waitvariant and rely on the runtime's natural update cycle.
OEM Communication Validation Pattern
A typical use case is verifying that an OEM-supplied PLC is reachable before populating downstream tags. The recommended guard is:
DWORD dwState = 0;
DWORD dwQuality = 0;
BOOL bLinkOk = FALSE;
GetTagBitStateQCWait( "OEM_Heartbeat_Bit", &dwState, &dwQuality );
// Heartbeat is a Boolean toggle driven by the OEM PLC at 500 ms.
// A "GOOD" quality is necessary but not sufficient — we also
// require the bit to have flipped within the last 2 seconds.
bLinkOk = ( ( dwQuality & 0xC0 ) == 0xC0 );
if ( bLinkOk )
{
// Populate downstream tags with confidence
SetTagFloat( "OEM_Tag1", GetTagFloatWait( "OEM_Tag1_Raw" ) );
}
else
{
// Hold last good value, raise alarm
MSRT_Msg( "OEM_LINK_LOST", "Quality=0x%02X", dwQuality );
}
FAQ
Why does GetTagBitStateQC return FALSE on every call even though the tag exists?
The most common cause is uninitialized PDWORD output arguments. Declare DWORD dwState = 0xFFFFFFFF; DWORD dwQuality = 0; and pass &dwState, &dwQuality to the function. The runtime aborts the call when it detects NULL destination pointers even though the source tag is valid.
Should I use GetTagBitStateQC or GetTagBitStateQCWait?
Use the non-Wait variant inside cyclic actions and trends — it returns the cached value immediately. Use the Wait variant only inside event-triggered code (button clicks, value-change events) where you need a forced synchronous refresh from the PLC before the function returns.
Why does HMIRuntime.Tags("").QualityCode return "random" numbers in VBScript?
The property returns the full 16-bit OPC DA quality bitfield, not a discrete enumeration. Mask the major quality bits with And &HC0 to reduce the value to one of four states: 0x00 Bad, 0x40 Uncertain, 0xC0 Good, or 0x80 Reserved. The remaining bits hold sub-status, limit, and vendor-specific information.
What does Quality Code 0x18 mean?
Quality Code 0x18 is BadDeviceFailure — the major quality is Bad (0x00) and the sub-status is 0x18. In practice it means the underlying device (typically the S7 CPU) is reporting a hardware-level fault, the CPU is in STOP, or a configured I/O module is missing or faulty. Check the CPU diagnostic buffer and the WinCC Connection diagnostics.
Can I pass a runtime-built tag name to GetTagBitStateQCWait?
Yes. Read the holding tag with GetTagChar("HolderTag", szBuffer, 256) and pass szBuffer as the first argument. Do not pass the wrapper function call GetTagChar("HolderTag") directly — the runtime expects a LPCTSTR, not a string-typed tag's runtime handle. Always size the buffer to at least 256 characters to accommodate the longest legal tag name.