Resolving GetTagBitStateQC Read Failures in WinCC C Scripts

David Krause12 min read
SiemensTroubleshootingWinCC
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

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.

Engineering Scope: This article covers WinCC V7.x C-API runtime functions used inside the WinCC Graphics Designer C action editor and the global C script editor. TIA Portal WinCC Unified and WinCC Professional use a different API surface (HMIRuntime object model) and are referenced only for context.

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.

Rule of thumb: For output parameters in WinCC C actions, always pre-initialize 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 );
Buffer size: WinCC tag names are limited to 128 characters in V7.x and 256 characters in TIA Portal. Always size the destination buffer to the documented maximum to prevent stack truncation when a stray configuration uses a long hierarchical name.

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

  1. 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.
  2. Configure RT (Start Runtime) on the engineering station or transfer the project to a runtime PC.
  3. Click the button while the target tag is good (PLC in RUN, connection up). The string tag OEM_QualityText must read GOOD (0xC0).
  4. 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) or BAD (0x18).
  5. Restore the connection and confirm the text returns to GOOD (0xC0) within one acquisition cycle.
  6. 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-Wait variants — GetTag*StateQC.
  • Event-triggered verification: use Wait variants only inside button-click or value-change handlers that fire rarely.
  • Trend or alarm qualification: use the non-Wait variant 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.

Back to blog