Home > OS >  How to convert two's complement to an int - using C?
How to convert two's complement to an int - using C?

Time:12-22

I'm trying to read a register from an INA260 using a Pico microcontroller. My program is in C. The register I'm having trouble with contains the value of the electrical current measured by the INA260. Since the INA260 accommodates measurement of current flowing in either direction, the mfr decided to cast the two bytes of measurement data in two's complement. This allows for a "negative" current - meaning only that the current is flowing in one direction instead of the other. I can appreciate this is a clever solution from a hardware perspective, but from the software perspective, I am very fuzzy on two's complement.

In an effort to help frame my question & give it some context, here's how I read & process the INA260's measured electrical current in the designated register. This code may not be elegant, but it gives the correct answer:

    uint8_t rcvdata[2];

/*  Read the Current Register (01h); INA260_MILAMP_REG _u(0x01)  */

    reg = INA260_MILAMP_REG;
    i2c_write_blocking (i2c_default, ADDR, &reg, 1, true);
    i2c_read_blocking(i2c_default, ADDR, rcvdata, 2, false);
    int milliamps = rcvdata[1] | rcvdata[0] << 8;
    printf("\ninteger value in register: %d milliamps", milliamps);
    printf("\nmeasure current by INA260: %.*f amps\n", 2, milliamps*1.25/1000);

In summary:

  • define the data register location (reg) & send that to the INA260 at its bus address
  • read 2 bytes (MSB first) into the rcvdata array
  • convert the 2 byte unsigned data to an integer
  • convert the result to amps (from milliamps; 1.25mA is one unit), and print
  • Register bytes are sent via I2C interface, MSB first.

Following is the output from this code:

integer value in register 0x01: 39 
measured current at IN  pin: 0.05 amps (i.e. 47.5 mA)

The code above yielded the correct answer in this case. In other words, as I have the INA260 wired, the current flows in the direction designated as positive by the manufacturer. As I understand it, the two's complement format is exactly the same as unsigned data when the value is positive, and so I "got lucky". However, there will be other situations when the INA260 is wired in such a way that the current is considered "negative", and I'm in a fog as to how to deal with two's complement when the value is negative.

Now then, at last, my questions are these:

  1. What changes are needed to the code listed above so that it can recover the negative integer value? i.e. what do I need to do instead of int milliamps = rcvdata[1] | rcvdata[0] << 8; to get the negative value?

  2. As I understand it, two's complement format is not covered by a standard, nor is it a data type in C. If that's true, how one can know if a block of data is cast as two's complement?

CodePudding user response:

As n. m. explained in the comments, signed integers are two's complement. All you need to do is choose the right size of integer.

RP2040 is a 32-bit microcontroller, so int will be 4-bytes. Your sensor outputs only two bytes, so the code should be something like this:

int16_t milliamps = (int16_t)(rcvdata[1] | rcvdata[0] << 8);

Assuming you get the byte order right, this will automatically give you correctly signed values.

CodePudding user response:

Because conversion of an out-of-range value to a signed integer type is implementation-defined, casting to int16_t is not a proper solution without knowing what the implementation-defined behavior is. It is also unnecessary because there are alternatives defined by the C standard.

Given two eight-bit unsigned bytes with the more significant byte in rcvdata[0] and the less significant byte in rcvdata[1], four options for constructing the 16-bit two’s complement value they represent are:

  • Assemble the bytes in the desired order and copy them into an int16_t: uint16_t t = (uint16_t) rcvdata[0] << 8 | rcvdata[1]; int16_t result; memcpy(&result, &t, sizeof result);.
  • Assemble the bytes in the desired order and use a union to reinterpret them: int16_t result = (union { uint16_t u; int16_t i; }) { (uint16_t) rcvdata[0] << 8 | rcvdata[1] } .i;.
  • Construct the result arithmetically: int16_t result = ((rcvdata[0] ^ 128) - 128) * 256 rcvdata[1];.
  • Target only C implementations that define conversion to a signed integer to wrap and use a converison: int16_t result = (int16_t) ((uint16_t) rcvdata[0] << 8 | rcvdata[1]);.

(In the last, the conversion to int16_t is implicit in the initialization, but a cast is used because, without it, some compilers will produce a warning or error, depending on switches.)

CodePudding user response:

Literally every microcontroller on the market is already using 2's complement for signed integers.

The datasheet you linked says that the part uses 2's complement, but it also says this at 8.5.3.2:

All data bytes are transmitted most significant byte first.

This means that the network endianess of the I2C serial bus is big endian. So it appears that you will receive 16 bit integers byte by byte, resulting in big endian format. To use them in as int16_t in your C code, you will have to convert them to little endian, since that's what Pico uses.

Exactly how this is done depends on the I2C hardware peripheral registers of the MCU, but generally it goes:

int16_t adc_val;
adc_val = (int16_t) ( (uint32_t)ms_byte << 8 | (uint32_t)ls_byte );

Assuming that the registers are of uint8_t, the uint32_t casts are strictly speaking not necessary, it's just good practice to prevent implicit promotion to a signed int before shifting. What's most important is that the bitwise operations aren't carried out on a signed type to prevent bugs such as accidental sign extension before the bytes are even swapped.

As for why rcvdata[1] | rcvdata[0] << 8; doesn't work - there is no sign anywhere, just raw data. If the number is for example {0xAA, 0xBB} you get uint8_t with value 0xAA promoted to int, no sign. Then uint8_t 0xBB promoted, still no sign, then 0xBB | 0xAA, large enough to fit an int, still no sign.

That's what the (int16_t) in my example fixes. A 16 bit number such as 0xBBAA will now get treated as -17494 decimal. And if stored in a 32 bit int, sign extended - still -17494 decimal but on the binary level it is "sign extended" to 0xFFFFBBAA.

  • Related