简体   繁体   中英

Convert a 12-bit signed number in C

Say I have a signed number coded on an unusual number of bits, for instance 12. How do I convert it efficiently to a standard C value? The following works but requires an intermediate variable:

#include <stdio.h>
int main() { 
  unsigned short U12=0xFFF; // 12-bit signed number, as coded in hex
  unsigned short XT=U12<<4; // 16 bits minus 12 is 4...
  short SX=(*(short*)&XT)>>4; // Signed shift. Is that standard C ?
  printf("%08X %d\n", SX, SX);
  return 0;
}

Ouput:

U12=0x0:   00000000 0
U12=0x1:   00000001 1
U12=0x7FF: 000007FF 2047
U12=0x800: FFFFF800 -2048
U12=0x801: FFFFF801 -2047
U12=0xFFF: FFFFFFFF -1

Is there a more direct way to do this without intermediate variable?

Here's one portable way (untested):

 short SX = (short)U12 - ((U12 & 0x800) << 1);

(Replace 0x800 and 0x1000 with (1<<whatever) for a different number of bits).

Methods that directly manipulate the sign bit using bit shift operations tend to invoke UB.

One problem with your approach is that you seem to be assuming that the width of a short and unsigned short is 16 bits. However, that is not guaranteed by the ISO C standard. The standard merely specifies that the width must be at least 16 bits.

If you want to use a data type that is guaranteed to be 16 bits wide, then you should use the data types uint16_t and int16_t instead.

Another problem is that your code is assuming that the platform your program is running on represents integers with negative values the same way as your "12-bit signed number". However, in contrast to the latest ISO C++ standard, the ISO C standard allows platforms to use any of the following representations:

  1. two's complement
  2. ones' complement
  3. signed magnitude

Assuming that your "12-bit signed number" has a two's complement representation, but your want your code to run correctly on all platforms, irrespective of which representation is used internally by the platform, then you would have to write code such as the following:

#include <stdio.h>
#include <stdint.h>
#include <stdbool.h>

int main( void )
{
    uint16_t U12=0xFFF;
    int16_t converted;

    //determine sign bit
    bool sign = U12 & 0x800;

    if ( sign )
    {
        //value is negative
        converted = -2048 + ( U12 & 0x7FF );
    }
    else
    {
        //value is positive
        converted = U12;
    }

    printf( "The converted value is: %hd\n", converted );
}

This program has the following output:

The converted value is: -1

As pointed out in the comments section, this program can be simplified to the following:

#include <stdio.h>
#include <stdint.h>

int main( void )
{
    uint16_t U12=0xFFF;

    int16_t converted = U12 - ( (U12 & 0x800) << 1 );

    printf( "The converted value is: %hd\n", converted );
}

Assuming that 0xFFF is a 12 bit number expressed with two's complement representation (not necessarily the case), that is equivalent to -1 and assuming that our CPU also uses 2's complement (extremely likely), then:

Using small integer types such as (unsigned) char or short inside bitwise operations is dangerous, because of implicit type promotion. Assuming 32 bit system with 16 bit short , if such a variable (signed or unsigned) is handed to the left operand of a shift, it will always get promoted to (signed) int .

Under the above assumptions, then:

  • U12<<4 gives a result 0xFFF0 of type int which is signed. You then convert it to unsigned short upon assignment.
  • The conversion *(short*)&XT is smelly but allowed by the pointer aliasing rules in C. The contents of the memory is now re-interpreted as the CPU's signed format.
  • the_signed_short >> 4 invokes implementation-defined behavior when the left operator is negative. It does not necessarily result in an arithmetic shift as you expect. It could as well be a logical shift.
  • %X and %d expect unsigned int and int respectively, so passing a short is wrong. Here you get saved by the mandatory default promotion of a variadic function's argument, in this case a promotion to int , again.

So overall there's a lot of code smell here.

A better and mostly well-defined way to do this on the mentioned 32 bit system is this:

int32_t u12_to_i32 (uint32_t u12)
{
  u12 &= 0xFFF; // optionally, mask out potential clutter in upper bytes

  if(u12 & (1u<<11)) // if signed, bit 11 set?
  {
    u12 |= 0xFFFFFFu << 12; // "sign extend"
  }

  return u12; // unsigned to signed conversion, impl.defined  
}

All bit manipulations here are done on an unsigned type which will not get silently promoted on a 32 bit system. This method also have the advantage of using pure hex bit masks and no "magic numbers".

Complete example with test cases:

#include <stdint.h>
#include <stdio.h>
#include <inttypes.h>

int32_t u12_to_i32 (uint32_t u12)
{
    u12 &= 0xFFF; // optionally, mask out potential clutter in upper bytes

    if(u12 & (1u<<11)) // if signed, bit 11 set?
    {
    u12 |= 0xFFFFFFu << 12; // "sign extend"
    }

    return u12; // unsigned to signed conversion, impl.defined  
}

int main (void) 
{ 
  uint32_t u12;
  int32_t  i32;
  
  u12=0; i32 = u12_to_i32(u12);
  printf("%08"PRIX32 "-> %08"PRIX32 " = %"PRIi32 "\n", u12, (uint32_t)i32, i32);

  u12=0x7FF; i32 = u12_to_i32(u12);
  printf("%08"PRIX32 "-> %08"PRIX32 " = %"PRIi32 "\n", u12, (uint32_t)i32, i32);

  u12=0x800; i32 = u12_to_i32(u12);
  printf("%08"PRIX32 "-> %08"PRIX32 " = %"PRIi32 "\n", u12, (uint32_t)i32, i32);

  u12=0xFFF; i32 = u12_to_i32(u12);
  printf("%08"PRIX32 "-> %08"PRIX32 " = %"PRIi32 "\n", u12, (uint32_t)i32, i32);

  return 0;
}

Output (gcc x86_64 Linux):

00000000-> 00000000 = 0
000007FF-> 000007FF = 2047
00000800-> FFFFF800 = -2048
00000FFF-> FFFFFFFF = -1

Let the compiler do the hard work:

#define TOINT(val, bits) (((struct {int val: bits;}){val}).val)

or more general

#define TOINT(type, v, bits) (((struct {type val: bits;}){v}).val)

usage:

int main(void)
{
    int int12bit = 0xfff;

    printf("%d\n", TOINT(int, int12bit, 12));
}

or the more simple version:

int main(void)
{
    int int12bit = 0b100110110010;

    printf("%d\n", TOINT(int12bit, 12));
}

and the compiler will choose the most efficient method for your target platform and target type:

int convert12(int val)
{
    return TOINT(val,12);
}

long long convert12ll(unsigned val)
{
    return TOINT(long long, val,12);
}

https://godbolt.org/z/P4b4rM4TT

Similar way you can convert the N bits signed integer to 'M' bits signed integer

#define TOINT(v, bits) (((struct {long long val: bits;}){v}).val)
#define FROM_N_TO_M(val, N, M) TOINT(TOINT(val, N), M)

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM