I'm having problems converting BCD (binary-coded decimal) to Decimal in VB6. At issue is the received value from a PLC that is already in BCD format (part of the problem is I don't know which flavor of BCD but I would guess it is 8 4-2-1) I know the existing value in BCD (serial port)and I know the real world value in degrees F. (coolant temperature) So, once I figure out the general BCD --> Decimal 10 algorithm I may be able to tweak it to determine the BCD format.
In general, my knowledge of VB allows me to convert a given value from decimal to BCD (any) and back to decimal - easy peasy, but in the problem mentioned above, I do not know what the incoming value was before the PLC converted it to BCD. I am sure it is something simple like getting the correct endianess...
Er, I think you mean convert between BCD and binary. VB6 never actually works in decimal at all. Even the Decimal Variant subtype is a binary format.
Can you show samples of the BCD you have there? Hex dump?
Usually it'll either be simple low-order digit first or the other way around, but since it arrives as bytes you need to know the nibble order, sign format, any padding, etc. as well.
Here is my number conversion to BCD where the input value is 331 and the output string is 0011 0011 0001. I also have code to do the reverse - convert 0011 0011 0001 back to 331 decimal. However, I am supplying a know input value that is NOT BCD. In my problem case, I do not know the value before the PLC converted it to BCD. Using my small group of functions I am merely converting 331 to and from decimal to BCD and back again. I just can't seam to figure out how to convert an unknown BCD value to its true value. IE what is the decimal form of 331?
Public Function getStringBCD(ByVal lNum As Long) As String
Dim i As Long
Dim z As Long
For i = 3 To 0 Step -1
z = lNum And 2 ^ i
getStringBCD = getStringBCD & Chr$(48 + Sgn(z))
Next
End Function
Private Function BCD(iValue As Integer) As String
Dim sRetVal As String
Dim sNum As String
Dim sValue As String
Dim i As Integer
Dim ilg As Integer
Dim sBin As String
If iValue < 0 Then iValue = -iValue
sValue = CStr(iValue)
ilg = Len(sValue)
For i = 1 To ilg
sNum = Mid(sValue, i, 1)
sBin = getStringBCD(CLng(sNum))
If i = 1 Then
sRetVal = sBin
Else
sRetVal = sRetVal & " " & sBin
End If
Are you receiving strings("331") or bytes(binary 0000 0011 0011 0001) from the PLC?
Yes, communication is functioning correctly. The values are also correct. The received integer value (while the equipment is at rest) is between 328 and 340. The binary is produced from the code presented earlier.
This is .Net code but hopefully you will see how it is done.
Code:
'00000000000000000000001100110001 'value - 331 in BCD
'00000000000000000000000000001111 mask pass number - 1
'00000000000000000000000011110000 mask pass number - 2
'00000000000000000000111100000000 mask pass number - 3
'00000000000000001111000000000000 mask pass number - 4
'00000000000011110000000000000000 mask pass number - 5
'00000000111100000000000000000000 mask pass number - 6
'00001111000000000000000000000000 mask pass number - 7
'11110000000000000000000000000000 mask pass number - 8
Dim test As Integer = Convert.ToInt32("0000001100110001", 2)
Dim converted As Integer = 0
For x As Integer = 0 To 7 'pick off four bits at a time, 8 * 4 = 32
Dim mask As Integer = &HF << (x * 4) 'create a mask in the correct location
Dim foo As Integer = test And mask 'get 4 bits
foo = foo >> (x * 4) 'shift them
converted += CInt(foo * 10 ^ x) 'multiply to get value and accumulate
Next
I ran your code and it produces a value of 331 which is the same as the PLC value. That PLC value is already in BCD format. What I need to do is un-BCD it into real world value.
The value is °F coolant temperature for a generator in BCD format. When the generator is not running a pre-heater keeps the coolant at a temperature of around 100 °F (unless the engine room is hotter). Right now the generator is off and the coolant has cooled down to 100 °F. So at 100 °F the PLC is sending me a value of around 331 (BCD). I need to convert that value from 331 BCD to decimal so it can be read by the local engineer as °F.
Ergo 331 = 100.
Once started, the BCD value jumps quickly to mid 300s then to 400. I do not know what the operating BCD value is but I understand the temperature is around 185°F.
That's the background on the code I am trying to develop.
We don't have an easy bit-shift in VB6 that respects the sign bit, so we have to work around that a little.
Maybe this does what you need:
Code:
Function DecodeBCD(ByVal LongBCD As Long) As Long
'Return binary long equivalent of the BCD value contained
'in long LongBCD. Assume no sign indicator within LongBCD.
Dim Nibble As Integer
Dim Mask As Long
Dim Divisor As Long
DecodeBCD = (LongBCD And &H70000000) \ &H10000000
If LongBCD < 0 Then
DecodeBCD = DecodeBCD + &H8&
LongBCD = LongBCD And &H7FFFFFFF
End If
Mask = &HF000000
Divisor = &H1000000
For Nibble = 6 To 0 Step -1
DecodeBCD = 10 * DecodeBCD + (LongBCD And Mask) \ Divisor
Mask = Mask \ &H10&
Divisor = Divisor \ &H10&
Next
End Function
I don't think the data is in BCD, or we are not seeing what the device is actually sending. If the decoded bits are 001100110001, and that IS BCD then the value is 331. Since the OP is adamant that isn't the value then the problem lies elsewhere.
... don't help to understand what's being asked for either.
What "form" does he think 331 is in? Sure looks like decimal to me.
The binary-coded decimal value received from the PLC is about 330. The value is somewhere around 120 °F. I spent 2 hours on the phone with the manufacturer's tech support and they were unable (or unwilling) to solve the riddle.
There is one serial connection and it takes a minute or so to switch software from a program that reads raw values (330) to one that displays as converted BCD (120).
Using a third party program (Automated Systems) it is possible to switch rapidly between raw data and BCD data. Each time the button is clicked the displayed value changes between 330 and 120. What this program does internally to produce this output is what I am trying to figure out. Pretty goofy, huh?
It's Modbus RTU. My program request Holding Register data in the form: Slave Address, Start Register, Number of Registers. The value I receive for the register in question is 330 and this value has been confirmed as correct both by direct observation using third party software as well as the manufacturer's tech support staff.
Dim cmdBuf(7) as Byte ' a separate routine from Sapia Inc. populates this array with the correct Modbus sentence.
serialPort.Write cmdBuf() ' The serial port activex is from National Instruments.
' Pause execution for 200ms.
' get number of bytes at port and if > 0 then
returnValue = serialPort.Read(numberOfBytes) ' The return is a string.
According to the documentation for Modbus RTU protocol the returned data is not a string. Apparently there are two modes of operation, ASCII and RTU. It sure seems that the Modbus protocol is messy for no apparent reason.
You are correct. The PLC outputs Modbus RTU. NI's serial activex returns either a byte array or a string. Switching to byte array causes an error. It should be noted that this program has been working perfectly since 2005 on many vessels, without so much as a hick-up . The data received is guaranteed to be correct. We are getting a value of 330, period. I just can't figure out how to convert 330 to °F. (it would be nice but 330 - 230 is not the answer )
Do you have a link to NI activex? Ican't find a reference that says RTU sends BCD.
Paste the string (as a string) that you receive here
returnValue = serialPort.Read(numberOfBytes) ' The return is a string.
"It should be noted that this program has been working perfectly since 2005 on many vessels, without so much as a hick-up." This is confusing. If it works what are you doing?
Last edited by dbasnett; Jun 17th, 2013 at 03:37 PM.
Do you have a link to NI activex? Ican't find a reference that says RTU sends BCD.
Paste the string (as a string) that you receive here
returnValue = serialPort.Read(numberOfBytes) ' The return is a string.
"It should be noted that this program has been working perfectly since 2005 on many vessels, without so much as a hick-up." This is confusing. If it works what are you doing?
The activex is serial port only; the Modbus routine is from a custom DLL from Sapia Inc. http://www.sapia-inc.com/. You will find some demos there but mine was a custom app. I have approached Sapia tech support on this issue.
The required data parameters are passed to the Sapia dll and the returned values are TX via the NI serial port activex. The serial RX value is passed to the Sapia dll and the resulting (array) contains the raw data from the PLC. It is then a simple matter to display that data on the operator's terminal.
The subject PLC is new to the system. None of the other installations require internal data conversion (except perhaps IEEE754) as we simply display the raw data. Sometimes the data requires minor manipulation and for that we give the end user the ability to supply a formula. For example: $*0.5 where $ represents the raw data (which happens to be the formula for Caterpillar PLC, main engine cranking battery voltage. A different PLC.)
The simple way to solve the issue is to take the raw data (330 for example) and apply a formula to it: $*0.363636 where $ represents 330 and the result would be rounded to 120. However, this only works when R = 330, as soon as this value changes the formula becomes useless. An alternative would be to chart BCD value against the temperature and create a more complex formula. But this is a patch not a permanent fix and would only work on the temperature data and not KW or lube oil pressure et cetera.
What is needed is to have the program perform a BCD to real world value conversion internally. A parameter will then be added to the user editable configuration file that simply states that this value is being read as BCD so branch to BCD algorithm (of course a formula could still be applied if necessary.)
Let's say the data arrives in BCD format. Odd, but let's assume that. Conversion is extra work but this isn't a big deal unless we're making bad assumptions, but since we have several attempts to convert above and they all get the right value (as you confirmed by using the Automated Systems program) BCD is no longer an issue.
So the real problem is how to convert a value like 330 to 120. I suspect you are correct that a simple linear conversion formula would do it.
Let's assume the 120 is Fahrenheit. Can we assume the 330 might be in some scaled Celsius units? If so that's another wrinkle but not the solution in itself.
I've worked with temperature sensors that contain embedded A/D and a microcontroller to send digital readings to computers before. The raw data almost always requires some "cooking" (and C is far more comon than F, but that's a separate issue).
These values never include a "decimal point" but arrive as integers, so in order to return "fractional" temperature values they are often scaled. To deal with that you have to divide by a scale factor to convert to fractional degree values.
These values often don't carry a sign directly, so they are returned as biased values in order to represent degrees below 0. To deal with that you have to subtract an offset value to get the proper range from - to + values.
So imagine you have a device that returns degrees F, offset by 30 degrees and scaled for 0.5 degree resolution. Such a device might return the value 260 to indicate 100.0 degrees F, 261 for 100.5 degrees F, etc.
I suspect that's what is at play here. That leaves you with 3 questions: Units (C or F)? Offset? Scale?
All of this ought to be in vendor documentation. I doubt it is possible to even guess without more than one data point (330, 120).
I have asked the engineer to run the generator and chart the raw data and live temperatures. Hopefully that will happen tomorrow. I will post the results.
I left it alone for the better part of a day. Later and with a clearer head I set about reviewing a number of BCD to Decimal and Decimal to BCD code snippets (some of which came from this forum as well as the good folks at Sapia Inc.) that, combined with my own code, I “pieced together” a procedure that worked.
To verify my work I was able to create a temperature chart from live data, in both Integer and BCD format, so really it was just a matter of tweaking the new code to match the real values. Voila!