Resolving AdvancedHMI BCD Read/Write Errors on Omron E Registers

James Nishida16 min read
HMI ProgrammingOmronTroubleshooting
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

Problem Overview

The InterpretValueAsBCD property on the BasicLabel control in AdvancedHMI exhibits a decoding defect when reading values from Omron E (Extended Data Memory) registers over Ethernet FINS. The same code path also fails when the BasicLabel keypad is used to write BCD-encoded values back to the same register. Operators see one or more of the following symptoms:

  • An empty ResultText string instead of "0" when the PLC register holds the value 0.
  • Wrong read values caused by the empty-string edge case in the BCD unpack loop on BasicLabel.vb.
  • Reverse-direction writes that do not match the keypad entry (for example, keypad entry 100 lands in the PLC as 64; entry 50 lands as 32).
  • Misaligned byte ordering when the @B address suffix is appended to force BCD handling.

The defect lives in BasicLabel.vb near line 566 and is correlated with changes in FINSBaseCom.vb beginning at line 551 in newer AHMI releases. Older AHMI builds do not show the defect because they delegate BCD decoding to the driver through the @B suffix instead of to the control.

Affected Versions and Components

Component File / Class Affected Behavior
BasicLabel control BasicLabel.vb ≈ line 566 BCD unpack loop produces empty string for value 0; reads become blank instead of showing 0.
FINSCom base driver FINSBaseCom.vb, BeginWrite, line 551 onward Bit-position default sends 0xFF in the bit slot when no bit is specified (24-SEP-15 fix).
Omron address parser OmronPlcAddress BCD flag set by @B suffix triggers write-side conversion in the driver.
Numeric display path m_InterpretValueAsBCD, m_BooleanDisplay Boolean conversion runs after the BCD step in V3.99v; ordering matters for result text.
Keypad handler KeypadPopUp.ButtonClick, BasicLabel.vb Calls m_ComComponent.Write directly; bypasses InterpretValueAsBCD on the write side.

The signature of the bug is consistent across the affected code paths: the unpack/conversion functions evaluate the input value before checking whether that value equals zero.

Technical Background: BCD Encoding and FINS Memory Areas

Omron's E-area (Extended Data Memory, prefix E) stores 16-bit words and is commonly used for numeric setpoints that must be exchanged with operator panels. The FINS commands used by AHMI for these transactions fall under the Memory Area group:

FINS Command Group Direction Address Example Purpose
Memory Area Read PLC → HMI E0_0001 Fetch a word from the extended data memory area.
Memory Area Write HMI → PLC E0_0001 + data Place a word into the extended data memory area.
Memory Area Fill HMI → PLC E0_0001, count, value Bulk-write a repeating value.
Multiple Memory Area Read PLC → HMI multiple E/D/W ranges Efficient batch read of heterogeneous areas.

Binary-Coded Decimal (BCD) packs a nibble per decimal digit, so a four-digit setpoint 1234 occupies one 16-bit word as 0x1234. The Omron PLC keeps the BCD representation in the data table when the data type is configured as BCD in CX-Programmer / Sysmac Studio. The HMI must, therefore:

  1. Decode the raw two-byte reply into a decimal integer for display (READ direction).
  2. Encode the decimal integer to nibbles before transmitting in the Write command's data field (WRITE direction).

AHMI exposes two distinct mechanisms to do this:

  • The control-level flag InterpretValueAsBCD on the BasicLabel.
  • The address-suffix mechanism @B on the address string itself (for example E0_0001@B), which sets address.IsBCD = True in the parser and forces the driver to apply BCD on both Read (post-decode) and Write (pre-encode).

The interaction between these two mechanisms, and between the COM-level TreatDataAsHex flag and the keypad's decimal entry, is where the new bug lives.

Hex-to-BCD Math Primer for Field Engineers

Three numbers show up constantly in this defect story and they confuse every first-time investigator. The math is small enough to memorize:

Decimal on Keypad Hex Equivalent BCD-Encoded Word What a Hex-Aware Tool Reads
0 0x00 0x0000 0
1 0x01 0x0001 1
50 0x32 0x0050 80 (raw binary)
99 0x63 0x0099 153 (raw binary)
100 0x64 0x0100 256 (raw binary)
1234 0x04D2 0x1234 4660 (raw binary)

Key observations:

  • If the COM component has TreatDataAsHex = True, the keypad string is parsed as hex. A decimal entry of 50 is reinterpreted as 0x32 = 50 only by coincidence; a decimal entry of 100 is reinterpreted as 0x100 = 256, not 100.
  • If InterpretValueAsBCD = True on the control, the unpack loop reads the wire bytes as a Int32 and then nibbles back into a decimal string. For 0x1234, that is decimal 4660 as Int32, then nibble-decoded back to 1234. The conversion is self-consistent on read.
  • If @B is on the address AND the control's InterpretValueAsBCD = True is also on, the read path runs BCD twice — once by the driver, once by the control — producing a value that looks similar to the input for symmetric cases (for example, 0x1234 → "4660" → "1234") but is wrong for asymmetric cases.
Critical: Never enable both InterpretValueAsBCD on the control and @B on the address. Pick one BCD path; mixing them is the single most common cause of the reported symptoms.

Root Cause: BasicLabel.vb Read Path (≈ line 566)

The V3.99v BasicLabel block reads the raw register value into the ResultText string variable and then optionally runs a BCD decode loop. The original block looks like this:

'* V3.99v
If m_InterpretValueAsBCD Then
    Try
        Dim b() As Byte = BitConverter.GetBytes(CInt(ResultText))
        ResultText = ""
        For index = 3 To 0 Step -1
            If (b(index) And 240) > 0 Or ResultText.Length > 0 Then
                ResultText &= CStr((b(index) And 240) >> 4)
            End If
            If (b(index) And 15) > 0 Or ResultText.Length > 0 Then
                ResultText &= CStr((b(index) And 15))
            End If
        Next
    Catch ex As Exception
        ResultText = "BCD Error"
    End Try
End If

The loop walks the four-byte Integer from most-significant byte (index 3) to least-significant (index 0). Each byte is split into a high nibble (b(index) And 240) and a low nibble (b(index) And 15). The conditions (nibble > 0) Or ResultText.Length > 0 implement leading-zero suppression: the loop only emits a digit once it has started writing.

The defect appears when the source ResultText represents zero. In that case CInt(ResultText) = 0, every nibble evaluates to 0, every condition evaluates false, and the loop appends nothing. ResultText is left at the empty string that was set on the previous line. Downstream, an empty ResultText cascades into the scale-factor and format code, which then propagates to the screen.

The corrected block adds an explicit zero branch:

If m_InterpretValueAsBCD Then
    Try
        Dim b() As Byte = BitConverter.GetBytes(CInt(ResultText))
        If (CInt(ResultText)) > 0 Then
            ResultText = ""
            For index = 3 To 0 Step -1
                If (b(index) And 240) > 0 Or ResultText.Length > 0 Then
                    ResultText &= CStr((b(index) And 240) >> 4)
                End If
                If (b(index) And 15) > 0 Or ResultText.Length > 0 Then
                    ResultText &= CStr((b(index) And 15))
                End If
            Next
        Else
            ResultText = "0"
        End If
    Catch ex As Exception
        ResultText = "BCD Error"
    End Try
End If

Place a breakpoint on the line containing CInt(ResultText) to confirm the bad value: when the PLC register is 0, ResultText should show "0" on entry and the corrected branch should produce an output of "0". The simplest diagnostic on the HMI side is to enter this in the Immediate window:

?Microsoft.VisualBasic.Conversion.Val(BasicLabel1.Text)
 0

An incorrect build returns 0 with the label visibly empty; a corrected build returns 0 with the label showing "0".

Critical: The leading-zero suppression logic is intentional. Do NOT change (b(index) And 240) > 0 to >= 0, and do NOT change ResultText.Length > 0 to >= 0. Doing so produces an output of "00000000" for every input, including values such as 12345 that overflow a single 16-bit BCD word, and breaks subsequent decoding.

Root Cause: BeginWrite Path in FINSBaseCom.vb

The write side lives in FINSBaseCom.vb, BeginWrite. The relevant section of the current code (line 551 onward) is:

Public Overrides Function BeginWrite( _
    ByVal address As MfgControl.AdvancedHMI.Drivers.Omron.OmronPlcAddress, _
    ByVal dataToWrite() As String) As Integer

    If address Is Nothing Then
        Throw New ArgumentNullException("WriteData Address parameter cannot be null.")
    End If

    If dataToWrite.Length <= 0 Then Return 0

    Dim CurrentTNS As Byte
    CurrentTNS = CByte(GetNextTransactionID(255))

    Dim Header As New MfgControl.AdvancedHMI.Drivers.Omron.FINSHeaderFrame( _
        MfgControl.AdvancedHMI.Drivers.Omron.GatewayCountOption.Three, _
        TargetAddress, SourceAddress, CByte(CurrentTNS))

    ActiveTNSs.Add(CurrentTNS)
    address.IsWrite = True
    Requests(CurrentTNS) = address

    Dim dataPacket As New List(Of Byte)
    dataPacket.Add(address.MemoryAreaCode)
    dataPacket.Add(CByte((address.ElementNumber >> 8) And 255))
    dataPacket.Add(CByte((address.ElementNumber) And 255))

    '* 24-SEP-15: 0xFF was being placed in the bit position if no bit specified.
    Dim BitNumberByte As Integer = Requests(CurrentTNS).BitNumber
    If Requests(CurrentTNS).BitNumber < 0 Or Requests(CurrentTNS).BitNumber > 64 Then
        BitNumberByte = 0
    End If
    dataPacket.Add(CByte(BitNumberByte))
    dataPacket.Add(CByte((address.NumberOfElements >> 8) And 255))
    dataPacket.Add(CByte((address.NumberOfElements) And 255))

    If address.BitsPerElement = 16 Then
        Dim x(1) As Byte
        For i As Integer = 0 To dataToWrite.Length - 1
            If m_TreatDataAsHex Then
                Dim data As Integer
                Try
                    data = Convert.ToUInt16(dataToWrite(i), 16)
                Catch ex As Exception
                    Throw New MfgControl.AdvancedHMI.Drivers.Common.PLCDriverException( _
                        "Invalid hexadecimal value " & dataToWrite(i))
                End Try
                x(0) = CByte(data And 255)
                x(1) = CByte(data >> 8)
            Else
                x = BitConverter.GetBytes(CUShort(dataToWrite(i)))
                If address.IsBCD Then
                    '* Convert to BCD
                    x(1) = CByte(CUShort(Math.Floor(CDbl(dataToWrite(i)) / 100)))
                    x(0) = MfgControl.AdvancedHMI.Drivers.Common.CalculationsAndConversions.HexToByte( _
                        Convert.ToString(CUShort(dataToWrite(i)) - (x(1) * 100)))
                    x(1) = MfgControl.AdvancedHMI.Drivers.Common.CalculationsAndConversions.HexToByte( _
                        Convert.ToString(x(1)))
                End If
            End If

            '* BitConverter uses LittleEndian, Omron uses BigEndian, so reverse
            dataPacket.Add(x(1))
            dataPacket.Add(x(0))
        Next
    Else
        '* Bit level
        For i As Integer = 0 To dataToWrite.Length - 1
            If Convert.ToInt32(dataToWrite(i)) > 0 Then
                dataPacket.Add(1)
            Else
                dataPacket.Add(0)
            End If
        Next
    End If

    '* ... raw socket send to TargetNode ...
End Function

The interesting line is the If address.IsBCD branch when m_TreatDataAsHex = False. The BCD encoding is computed digit-by-digit: hundreds-and-above go to the high byte, tens-and-units to the low byte. That is fine for two-digit values such as 50, where Math.Floor(50/100) = 0 puts 0 in the high byte and HexToByte("50") = 0x50 = 80 in the low byte. The PLC then sees 0x0050, which matches the operator's entry when read back through BCD-aware code. The values entered on the keypad hit this branch whenever the address string uses the @B suffix.

Two configuration mistakes result in wrong writes:

  1. Setting TreatDataAsHex = True on the COM component while keeping the control's InterpretValueAsBCD = True. The If m_TreatDataAsHex branch is taken, so the BCD encode path is skipped entirely. Furthermore, the string coming in from the keypad is parsed by Convert.ToUInt16(dataToWrite(i), 16), meaning that the decimal entry 100 is parsed as hex 0x100 = 256. A subsequent read returns "100" only because InterpretValueAsBCD reverses the conversion on read; the wire data is still wrong.
  2. Mixing the @B suffix with TreatDataAsHex = True. The byte-order reversal still happens, but the BCD encoding does not. For a single-digit entry 1, the bytes go out as 0x00 0x01; the corresponding read via the control's InterpretValueAsBCD then nibble-decodes 0x01 as "1" — but multi-byte entries land in surprising patterns because the high-byte/low-byte assignment is now governed by the comma-separated HexToByte calls and not by the actual magnitude.
Keypad Entry TreatDataAsHex Address Suffix Wire Bytes (hex) Wire Value (dec) BCD-Aware Readback Plain INT Readback
100 False @B 01 00 256 10 256
100 True none 01 00 256 10 256
100 False none 00 64 100 100 100
50 False @B 00 50 80 50 80
1 False @B 00 01 1 1 1
Recommended configuration: Choose one BCD mechanism and stick to it.
  • Approach A (recommended for new projects): Leave InterpretValueAsBCD = False on the control and use the address string E0_0001@B. The driver handles both directions.
  • Approach B (legacy): Keep InterpretValueAsBCD = True on the control, set the address to E0_0001 (no suffix), set TreatDataAsHex = False on the COM component, and apply the BasicLabel source patch above.
Never mix TreatDataAsHex = True with @B, and never mix TreatDataAsHex = True with InterpretValueAsBCD = True.

Symptom-to-Configuration Matrix

Capture the live project and inspect the following three properties:

  1. On the OmronEthernetFINSCom1 component — verify TreatDataAsHex.
  2. On the affected BasicLabel1 — verify InterpretValueAsBCD.
  3. On the same label's PLCAddressValue and PLCAddressKeypad — verify the presence or absence of @B.

Cross-check the truth table below against the symptoms in the field:

TreatDataAsHex InterpretValueAsBCD Address Suffix Read of 0 Write Behavior Likely Root Cause
False True none Blank (bug) Decimal out, BCD-in encode skipped BasicLabel.vb unpack loop bug
False True @B Reads as raw binary Writes BCD-encoded twice Double conversion between control and driver
True True none Reads correctly after patch Decimal entry parsed as hex (100 → 256) Convert.ToUInt16 with base 16
True True @B Reads as hex Writes parsed as hex then re-encoded Triple conversion; worst case
False False none Reads as binary (correct) Decimal out, no encode (correct) No BCD — acceptable if register is INT, not BCD
False False @B Reads as BCD (correct) Writes as BCD (correct) Correct driver-only BCD handling

Reverse-Decoding the Symptom Numbers

For engineers inheriting a misconfigured panel, the symptom numbers 64, 32, and 61 correlate to specific configuration flaws:

Reported Symptom Decimal Keypad Entry Bug Path Underlying Hex Result
Write 100 → register reads 64 100 TreatDataAsHex = True, InterpretValueAsBCD = True, no @B 0x100 / 256 decimal, mis-displayed lower-byte monitor showing 0x40 / 64
Write 50 → register reads 32 50 TreatDataAsHex = True, InterpretValueAsBCD = True, no @B 0x50 / 80 decimal, mis-displayed as 0x20 / 32 in some monitoring views
Write 1 → register reads 61 1 TreatDataAsHex = True, @B suffix active Wire bytes 0x00 0x01 with byte-order mismatch producing a low-byte/high-byte inversion in the unpack loop

Use this table when reviewing a project before signing off on the patch; the numbers in the existing panel are diagnostic, not a permanent state.

Step-by-Step Fix Procedure

  1. Open the AHMI solution in Visual Studio (or open BasicLabel.vb from the AHMI source archive).
  2. Locate the line that reads If m_InterpretValueAsBCD Then inside the property that updates ResultText. The current location is approximately line 566 in the V3.99v release.
  3. Replace the body of that If block with the corrected snippet shown in the previous section.
  4. Rebuild the AdvancedHMI DLLs (AdvancedHMIDrivers.dll, MfgControl.AdvancedHMI.dll) and copy them to the project's bin\Release (or bin\Debug) folder, replacing the existing files. Take a backup first.
  5. Stop the running HMI process (or the SCADA service that hosts the form), replace the DLL, and relaunch.
  6. Write a known BCD value (for example 1234 as decimal to the keypad, or set the register with CX-Programmer / Sysmac Studio to 0x1234) and confirm the BasicLabel shows 1234.
  7. Set the register to 0 and confirm the BasicLabel still shows 0, not empty.
  8. From the keypad, enter 100 and verify the PLC register now contains the value 100 when read back with CX-Programmer / Sysmac Studio as BCD or as INT (depending on the chosen approach).

Verification Procedure

Build a one-screen test rig that exercises all four corners of the value space. From FINSBaseCom.vb, the request TNS counter starts at 0 and rolls over at 255, so enable verbose logging on the COM component and ensure each transaction logs the FINS command, address, and data bytes.

  1. Write a known-good fixture. In CX-Programmer / Sysmac Studio, define E0_0001 as BCD and pre-load it with 0x1234.
  2. Read back from the HMI. Confirm the BasicLabel shows 1234.
  3. Write 0 to E0_0001. Confirm the BasicLabel shows 0, not empty.
  4. Write the maximum two-digit BCD value 99. Confirm the BasicLabel shows 99.
  5. Write 0x1234 via the keypad (with TreatDataAsHex = False, this requires entering decimal 4660, which is 0x1234). On readback, confirm the BasicLabel shows the original intended value 4660 if the control is using InterpretValueAsBCD = True, or confirm the wire is still 0x1234 if using the @B driver path.
  6. Toggle the PLC Online Edit to a different value (for example 0x0007) without restarting AHMI. Confirm the BasicLabel updates within one polling interval.
  7. Disconnect the Ethernet cable and reconnect. Confirm the COM component reconnects, the BasicLabel repopulates, and no zero-value freeze occurs.

Pass criteria: every numeric test case shows the expected text on the BasicLabel, and CX-Programmer / Sysmac Studio shows the same value when monitoring E0_0001 in BCD view.

Workarounds Without Recompiling

If you do not have Visual Studio access to AHMI's source code, three runtime workarounds recover correct operation:

  1. Disable InterpretValueAsBCD on every BasicLabel and append @B to the address string. The driver takes over BCD on both directions. This is the cleanest configuration when migrating from old AHMI to V3.99v.
  2. Use a separate BasicLabel for display and a NumericInput or Keypad for entry, both wired to the same register via the @B address. This sidesteps the BasicLabel.vb bug entirely because the entry code path goes through KeypadPopUp.ButtonClickm_ComComponent.Write directly, with no InterpretValueAsBCD involvement on the Write side.
  3. Wrap the E register in CX-Programmer with a BCD-to-binary conversion function block. Map two INT registers (D0 = binary value, D1 = presentation as BCD) and have the PLC do the conversion. The HMI then never has to know about BCD at all.

If the application is already in production and none of the above can be applied immediately, the temporary mitigation is to enter BCD values through CX-Programmer / Sysmac Studio rather than from the HMI keypad. This keeps the read side functional (assuming the patched BasicLabel.vb) until the project can be repackaged with the corrected DLL.

SCADA Diagnostics and Frequently Asked Questions

For engineers running AHMI under a SCADA host or as a Windows service, enable the verbose log on OmronEthernetFINSCom to capture every transaction. Each FINS write should record its address and data bytes. Compare the wire bytes against the table in the Reverse-Decoding section to localize any new symptom. Cross-reference the AHMI version with the affected-version table at the start of this article; an unchanged older release may not need any patch.

Why does my BasicLabel show an empty field instead of zero?

The unpack loop in BasicLabel.vb around line 566 has no branch for the case where the entire value is zero. Every nibble evaluates to 0, the leading-zero suppression keeps the output suppressed, and ResultText is left at the empty string. Patch the loop with an If CInt(ResultText) > 0 Then ... Else ResultText = "0" wrapper.

I enter 100 on the keypad but the register ends up at 64. Why?

The string "100" is being parsed by Convert.ToUInt16(dataToWrite, 16), which interprets it as hex. 0x100 = 256, and the displayed value depends on which monitor view you use. Set TreatDataAsHex = False on the OmronEthernetFINSCom component unless you genuinely want to enter hex on the keypad.

Should I use InterpretValueAsBCD on the control, or the @B suffix on the address?

Pick one, not both. The control-level flag works only on the read path that updates ResultText; it does not encode the value back on write. The @B suffix is parsed by the driver and applies BCD on both the read decode and the write encode. For new projects use the @B suffix and keep the control flag off.

What FINS commands handle E-register reads and writes, and what memory area code is used?

Reads use the Memory Area Read command and writes use the Memory Area Write command. The Extended Data Memory (E) area is identified by a memory area code in the FINS header; consult the Omron Ethernet FINS reference manual for the exact code by CPU family and operating mode before issuing any requests against a specific target.

I do not have the AHMI source. Can I fix this without recompiling?

Yes. Set InterpretValueAsBCD = False on every BasicLabel that addresses an E register, change the address string from E0_0001 to E0_0001@B, leave TreatDataAsHex = False on the COM component, and rebuild the project. The driver handles BCD on both directions, so no source patch is required.

Will the patch affect non-BCD labels or other memory areas?

No. The patched block is gated by If m_InterpretValueAsBCD Then; labels with InterpretValueAsBCD = False skip the entire loop. The wrap-around zero-handling only matters when the loop is active, so D, W, H, and other integer-style memory areas are unaffected.

Back to blog