Convert Hex to Decimal from signed 2's complement

I am reading data from a meter over dlms protocol and I am getting hex values from there. I want to convert that hex string to signed decimal value. For example - ff9d denotes a value of -99 [reference].

My hex to decimal code is:

``````unsigned int hexToDec(String hexString)
{
unsigned int decValue = 0;
int nextInt;

for (int i = 0; i < hexString.length(); i++)
{
nextInt = int(hexString.charAt(i));
if (nextInt >= 48 && nextInt <= 57) nextInt = map(nextInt, 48, 57, 0, 9);
if (nextInt >= 65 && nextInt <= 70) nextInt = map(nextInt, 65, 70, 10, 15);
if (nextInt >= 97 && nextInt <= 102) nextInt = map(nextInt, 97, 102, 10, 15);
nextInt = constrain(nextInt, 0, 15);
decValue = (decValue * 16) + nextInt;
}
return decValue;
}
``````

• Have you tried googling something like "C++ convert hex string to int"? Commented Oct 1, 2020 at 7:36
• Yes, I have converted hex string to decimal. But can't figure out the signed part. I can share that code if you want? Commented Oct 1, 2020 at 7:38
• Yes, please share the code. You can edit your question for including it. Commented Oct 1, 2020 at 8:18
• I have edited my question @chrisl Commented Oct 1, 2020 at 8:51
• Any code snippet you have on how to do this in Arduino @EdgarBonet ? Commented Oct 1, 2020 at 9:09

For example – ff9d denotes a value of -99

The function you wrote returns an unsigned integer which, being unsigned, cannot be negative. The simple fix is to declare it as

``````int hexToDec(String hexString)
``````

That should be enough to get it working as expected. Note that the local variable `decValue` still has to be unsigned, otherwise you would get a signed overflow, which is undefined behavoir (i.e. “forbidden”) in C++. Returning an unsigned number from a function that is supposed to return a signed number is perfectly fine: you get an implicit conversion which is guaranteed to do the right thing.

• Use, if you can, plain C strings (`char *`) instead of `String` objects, as the latter are not very friendly to your Arduino's memory.
• Use fixed-length integers (`uint16_t` if your protocol dictates 16-bit integers) instead of the generic `int`, otherwise your code will fail if you move to a 32-bit platform, such as an ARM-powered Arduino.
• Use character literals (`'0'` instead of `48`) instead of hard-coding ASCII code points, as that makes the code more readable.
• Use a simple subtraction (`nextInt - '0'`) instead of the quite expensive `map()` function.

Here is my take at it. Note that it has a “classic C” accent you do not need to replicate. You may prefer using indices to crawl the string.

``````int16_t hex2int(const char *hex)
{
uint16_t value;  // unsigned to avoid signed overflow
for (value = 0; *hex; hex++) {
value <<= 4;
if (*hex >= '0' && *hex <= '9')
value |= *hex - '0';
else if (*hex >= 'A' && *hex <= 'F')
value |= *hex - 'A' + 10;
else if (*hex >= 'a' && *hex <= 'f')
value |= *hex - 'a' + 10;
else
break;  // stop at first non-hex digit
}
return value;
}
``````

Example usage:

``````Serial.println(hex2int("ff9d"));  // prints "-99"
``````

Edit: As revealed by the comments, the whole question is an XY problem. You are asking help about your misguided solution rather than your actual problem. The data you have is not hex: it's binary. You have a 16-bit signed integer stored in a byte array, most significant byte first. You figured out you can convert that to an integer by converting it first to a string representation as hexadecimal, then converting that representation back to a binary number. Of course you can do that, but it's an overly complicated and expensive way of doing something trivial.

The canonical way to recover an integer from a byte array is by assembling the bytes together using bitwise logic and bit shifts. In this case:

``````int16_t reactive_power = (uint16_t) data[dataSize - 5] << 8
| data[dataSize - 4];
``````
• How should I pass ff9d as string in the hex2int function? Commented Oct 1, 2020 at 9:42
• @Prateek: See the example I added. Commented Oct 1, 2020 at 9:50
• I am doing this: String reacP = String(data[dataSize - 5], HEX) + String(data[dataSize - 4], HEX); Serial.println(hexToInt(reacP)); but I am getting the error: cannot convert 'String' to 'const char*' for argument '1' to 'int16_t hexToInt(const char*)' Commented Oct 1, 2020 at 9:56
• @Prateek: Looks to me like you are converting binary data to hex string, only to convert it back to binary. Please edit your question showing how you get the data. Most importantly: what is the data type of `data`? Commented Oct 1, 2020 at 9:59
• That datatype of `data` is unsigned char as in `void ReactivePower(unsigned char data[], int dataSize)`. Commented Oct 1, 2020 at 10:08

Which mode of DLMS you are you using?

IEC 61107 or currently IEC 62056-21, was an international standard for a computer protocol to read utility meters. It is designed to operate over any media, including the Internet. A meter sends ASCII (in modes A..D) or HDLC (mode E) data to a nearby hand-held unit (HHU) using a serial port. IEC 62056

hex string

which means a string of hex characters using ASCII (modes A to D).

However, in the comments you mentioned:

`void ReactivePower(unsigned char data[], int dataSize)` `String reacP = String(data[dataSize - 5], HEX) + String(data[dataSize - 4], HEX);`

which means an array of bytes obtained from a stream of bytes using HDLC (mode E).

ASCII (Modes A to D)

For ASCII (modes A to D), there is a simple transformation to get the digit value from the hex character.

``````              ASCII
Hex   --------------------                            Digit
char  Decimal  Hexadecimal    Op1    Low nibble  Op2  Base 10
-----  -------  -----------  -------  ----------  ---  -------
'0'      48        30        & 0x0F       0              0
'1'      49        31        & 0x0F       1              1
'2'      50        32        & 0x0F       2              2
'3'      51        33        & 0x0F       3              3
'4'      52        34        & 0x0F       4              4
'5'      53        35        & 0x0F       5              5
'6'      54        36        & 0x0F       6              6
'7'      55        37        & 0x0F       7              7
'8'      56        38        & 0x0F       8              8
'9'      57        39        & 0x0F       9              9
'A'      65        41        & 0x0F       1       +9    10
'B'      66        42        & 0x0F       2       +9    11
'C'      67        43        & 0x0F       3       +9    12
'D'      68        44        & 0x0F       4       +9    13
'E'      69        45        & 0x0F       5       +9    14
'F'      70        46        & 0x0F       6       +9    15
'a'      97        61        & 0x0F       1       +9    10
'b'      98        62        & 0x0F       2       +9    11
'c'      99        63        & 0x0F       3       +9    12
'd'     100        64        & 0x0F       4       +9    13
'e'     101        65        & 0x0F       5       +9    14
'f'     102        66        & 0x0F       6       +9    15
``````
• For characters '0' to '9', simply `AND` with 0x0F.
• For characters 'A' to 'F' and 'a' to 'f', `AND` with 0x0F and `add` 9.
``````char ch = '1';
byte b = 0;
if (ch >= '0' && ch <= '9')
{
b = ch & 0x0F;
}
else if ((ch >= 'A' && ch <= 'F') || (ch >= 'a' && ch <= 'f'))
{
b = (ch & 0x0F) + 9;
}
``````

It's also important to know whether the transformation was successful, e.g. the first character could be bad (either a null terminator or non hex character), so this needs to be tested for during the transformation. Also, there are four nibbles in an `int16` so the transformation needs to quit when a maximum of four hex characters have been processed:

``````void setup()
{
pinMode(13, OUTPUT);
Serial.begin(115200);
}

void loop()
{
char string[] = "A";
//char string[] = "Waffle";
//char string[] = "ff9d";
int16_t the_int = 0;
int count = AsciiToInt16(string, the_int);

Serial.print(string);
Serial.print(",  ");
if (count)
{
Serial.print("Ok,  ");
Serial.print(count);
Serial.print(",  ");
Serial.println(the_int);
}
else
{
}
}

int AsciiToInt16(const char* ch, int16_t& value)
{
int count = 0;
byte nibble = 0;

// Stop after 4th nibble or null terminator.
for(int i = 0; i < 4 && ch[i] != 0; i++)
{
if (AsciiToNibble(ch[i], nibble))
{
value <<= 4;
value |= nibble;
count++;
}
else
{
break;  // Stop after non hex digit.
}
}
return count;
}

bool AsciiToNibble(const char ch, byte& b)
{
if (ch >= '0' && ch <= '9')
{
b = ch & 0x0F;
return true;
}
else if ((ch >= 'A' && ch <= 'F') || (ch >= 'a' && ch <= 'f'))
{
b = (ch & 0x0F) + 9;
return true;
}
return false;
}
``````

Output

``````A,  Ok,  1,  10
ff9d,  Ok,  4,  -99
``````

HDLC (Mode E)

For HDLC (mode E), simply do:

``````void ReactivePower(byte data[], int dataSize)
{
uint16_t reacP = (data[dataSize - 5] << 8) | data[dataSize - 4];
Serial.println(dataSize);
Serial.println(reacP);
}
. . .
byte data[] = { 1, 3, 5, 2, 4, 8 };
ReactivePower(data, sizeof(data));
``````

Output

``````6
773
``````