4

I have a sensor which gives its output in three bytes. I read it like this:

unsigned char byte0,byte1,byte2;

byte0=readRegister(0x25);
byte1=readRegister(0x26);
byte2=readRegister(0x27);

Now I want these three bytes merged into one number:

int value;
value=byte0 + (byte1 << 8) + (byte2 << 16);

it gives me values from 0 to 16,777,215 but I'm expecting values from -8,388,608 to 8,388,607. I though that int was already signed by its implementation. Even if I try define it like signed int value; it still gives me only positive numbers. So I guess my question is how to convert int to its two's complement?

Thanks!

muliku
  • 416
  • 3
  • 17
  • You should not assume that `int` is 2's complement or any other implementation detail. That would, at best, lead to unportable and brittle code. Just do the arithmetic yourself to get the correct result. In fact, you shouldn't even assume it can hold a 3-byte number! – underscore_d Apr 11 '18 at 08:47
  • What size is int on your architecture? – DukeOfMarmalade Apr 11 '18 at 08:48
  • 1
    Don't assume sizes of `int` or even `char`. Use proper types like `uint8_t` and `int32_t` etc. – Grigory Rechistov Apr 11 '18 at 08:51
  • I don't understand. Even if byte0 == byte1 == byte2 == 0xff, byte0 + (byte1 << 8) + (byte2 << 16) is only 0x00ffffff, assuming `int` has 4 bytes. Why should it be negative? – Stan Apr 11 '18 at 08:52
  • @Stan it should be negative if you want it to be negative. Extending of N-bit integer to (N+M)-bit integer can be done in two ways, depending on whether one treats the N-bit value as signed or unsigned. Even though 2-complement machines use the same arithmetics for them. – Grigory Rechistov Apr 11 '18 at 08:56
  • 1
    @underscore_d [there is a proposal](http://www.open-std.org/jtc1/sc22/wg14/www/docs/n2218.htm) to fix this in the next version of the standard, as in practice non-two's-complement arithmetic doesn't seem to be a concern IRL for any code that is likely to be actively updated or newly written. – Alex Celeste Apr 11 '18 at 08:58
  • @GrigoryRechistov just want to check what the original motive is. (Why not use signed char at the first place?) – Stan Apr 11 '18 at 09:07
  • 1
    @Stan Even if one uses signed types, the (un)signed extension happens, it is only performed by compiler, not programmer. It is good to know however the underlying logic behind it. Sometimes you have to know how to do it yourself: 1) you cannot control signedness of input values (3rd-party code) 2) your desired integer type is not supported by compiler (e.g. a 13-bit signed int, or 256-bit signed int). – Grigory Rechistov Apr 11 '18 at 09:24
  • @Stan "what the original motive is" — it might be that `readRegister()` returns an unsigned type and you cannot change it. From that point, you have a number of options how to proceed, including the explicit one I gave in my answer below. Note that `byte0` and `byte1` should be kept as unsigned during the merging process (otherwise their higher bits would overlap with each other), while `byte2` may be casted to signed 32-bit int. – Grigory Rechistov Apr 11 '18 at 09:30
  • @GrigoryRechistov Thanks for the detailed reply :) I meant ... we could write `unsigned char byte0,byte1; char byte2; ... int32_t value = byte0 | (byte1 << 8) | (byte2 << 16);` and let the compiler do its job, which could save a `? :` operation. Anyway it depends on the original motive. – Stan Apr 11 '18 at 09:49

2 Answers2

7

What you need to perform is called sign extension. You have 24 significant bits but want 32 significant bits (note that you assume int to be 32-bit wide, which is not always true; you'd better use type int32_t defined in stdint.h). Missing 8 top bits should be either all zeroes for positive values or all ones for negative. It is defined by the most significant bit of the 24 bit value.

int32_t value;
uint8_t extension = byte2 & 0x80 ? 0xff:00; /* checks bit 7 */
value = (int32_t)byte0 | ((int32_t)byte1 << 8) | ((int32_t)byte2 << 16) | ((int32_t)extension << 24);

EDIT: Note that you cannot shift an 8 bit value by 8 or more bits, it is undefined behavior. You'll have to cast it to a wider type first.

Grigory Rechistov
  • 2,104
  • 16
  • 25
  • 2
    Integer promotion is applied to the operands of `<<`; you cannot shift an 8-bit value in C at all, because it will be promoted to `int` or `unsigned int` first. However, you may want to prevent it from choosing an inappropriate conversion path (e.g. unexpected sign extension on an unsigned operand). – Alex Celeste Apr 11 '18 at 08:55
  • @Leushenko, thanks, I did not know that `char` has special treatment in shifting operations. You can never be too much careful with shifting in C though. – Grigory Rechistov Apr 11 '18 at 08:58
  • 1
    @Leushenko: It is true that you cannot shift an eight-bit value by eight bits without risking undefined behavior because `128 << 8` is 32768, which is larger than an `int` is guaranteed to be able to represent. – Eric Postpischil Apr 11 '18 at 14:07
  • 2
    @GrigoryRechistov: This is not special treatment for `char` in shifting operations. In C, all integer expressions (including `_Bool`, `char`, and `short`) are promoted to at least `int` in most contexts. – Eric Postpischil Apr 11 '18 at 14:34
2
#include <stdint.h>
uint8_t byte0,byte1,byte2;
int32_t answer;

// assuming reg 0x25 is the signed MSB of the number 
// but you need to read unsigned for some reason
byte0=readRegister(0x25);
byte1=readRegister(0x26);
byte2=readRegister(0x27);

// so the trick is you need to get the byte to sign extend to 32 bits
// so force it signed then cast it up
answer = (int32_t)((int8_t)byte0); // this should sign extend the number
answer <<= 8;
answer |= (int32_t)byte1; // this should just make 8 bit field, not extended
answer <<= 8;
answer |= (int32_t)byte2;

This should also work

answer = (((int32_t)((int8_t)byte0))<<16) + (((int32_t)byte1)<< 8) + byte2;

I may be overly aggressive with parentheses but I never trust myself with shift operators :)

bd2357
  • 704
  • 9
  • 17