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
ResultTextstring 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
100lands in the PLC as64; entry50lands as32). - Misaligned byte ordering when the
@Baddress 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:
- Decode the raw two-byte reply into a decimal integer for display (READ direction).
- 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
InterpretValueAsBCDon theBasicLabel. - The address-suffix mechanism
@Bon the address string itself (for exampleE0_0001@B), which setsaddress.IsBCD = Truein 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 of50is reinterpreted as0x32 = 50only by coincidence; a decimal entry of100is reinterpreted as0x100 = 256, not 100. - If
InterpretValueAsBCD = Trueon the control, the unpack loop reads the wire bytes as aInt32and then nibbles back into a decimal string. For0x1234, that is decimal4660as Int32, then nibble-decoded back to1234. The conversion is self-consistent on read. - If
@Bis on the address AND the control'sInterpretValueAsBCD = Trueis 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.
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".
(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:
-
Setting
TreatDataAsHex = Trueon the COM component while keeping the control'sInterpretValueAsBCD = True. TheIf m_TreatDataAsHexbranch is taken, so the BCD encode path is skipped entirely. Furthermore, the string coming in from the keypad is parsed byConvert.ToUInt16(dataToWrite(i), 16), meaning that the decimal entry100is parsed as hex0x100 = 256. A subsequent read returns"100"only becauseInterpretValueAsBCDreverses the conversion on read; the wire data is still wrong. -
Mixing the
@Bsuffix withTreatDataAsHex = True. The byte-order reversal still happens, but the BCD encoding does not. For a single-digit entry1, the bytes go out as0x00 0x01; the corresponding read via the control'sInterpretValueAsBCDthen nibble-decodes0x01as"1"— but multi-byte entries land in surprising patterns because the high-byte/low-byte assignment is now governed by the comma-separatedHexToBytecalls 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 |
-
Approach A (recommended for new projects): Leave
InterpretValueAsBCD = Falseon the control and use the address stringE0_0001@B. The driver handles both directions. -
Approach B (legacy): Keep
InterpretValueAsBCD = Trueon the control, set the address toE0_0001(no suffix), setTreatDataAsHex = Falseon the COM component, and apply the BasicLabel source patch above.
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:
- On the
OmronEthernetFINSCom1component — verifyTreatDataAsHex. - On the affected
BasicLabel1— verifyInterpretValueAsBCD. - On the same label's
PLCAddressValueandPLCAddressKeypad— 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
- Open the AHMI solution in Visual Studio (or open
BasicLabel.vbfrom the AHMI source archive). - Locate the line that reads
If m_InterpretValueAsBCD Theninside the property that updatesResultText. The current location is approximately line 566 in the V3.99v release. - Replace the body of that
Ifblock with the corrected snippet shown in the previous section. - Rebuild the AdvancedHMI DLLs (
AdvancedHMIDrivers.dll,MfgControl.AdvancedHMI.dll) and copy them to the project'sbin\Release(orbin\Debug) folder, replacing the existing files. Take a backup first. - Stop the running HMI process (or the SCADA service that hosts the form), replace the DLL, and relaunch.
- Write a known BCD value (for example
1234as decimal to the keypad, or set the register with CX-Programmer / Sysmac Studio to0x1234) and confirm the BasicLabel shows1234. - Set the register to
0and confirm the BasicLabel still shows0, not empty. - From the keypad, enter
100and verify the PLC register now contains the value100when 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.
- Write a known-good fixture. In CX-Programmer / Sysmac Studio, define
E0_0001as BCD and pre-load it with0x1234. - Read back from the HMI. Confirm the BasicLabel shows
1234. - Write
0toE0_0001. Confirm the BasicLabel shows0, not empty. - Write the maximum two-digit BCD value
99. Confirm the BasicLabel shows99. - Write
0x1234via the keypad (withTreatDataAsHex = False, this requires entering decimal4660, which is0x1234). On readback, confirm the BasicLabel shows the original intended value4660if the control is usingInterpretValueAsBCD = True, or confirm the wire is still0x1234if using the@Bdriver path. - Toggle the PLC Online Edit to a different value (for example
0x0007) without restarting AHMI. Confirm the BasicLabel updates within one polling interval. - 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:
-
Disable
InterpretValueAsBCDon every BasicLabel and append@Bto the address string. The driver takes over BCD on both directions. This is the cleanest configuration when migrating from old AHMI to V3.99v. -
Use a separate
BasicLabelfor display and aNumericInputorKeypadfor entry, both wired to the same register via the@Baddress. This sidesteps theBasicLabel.vbbug entirely because the entry code path goes throughKeypadPopUp.ButtonClick→m_ComComponent.Writedirectly, with noInterpretValueAsBCDinvolvement on the Write side. -
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.