Convert a 12-bit signed number in C
Solution 1:
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.
Solution 2:
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:
- two's complement
- ones' complement
- 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 <inttypes.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: %"PRId16"\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>
#include <inttypes.h>
int main( void )
{
uint16_t U12=0xFFF;
int16_t converted = U12 - ( (U12 & 0x800) << 1 );
printf( "The converted value is: %"PRId16"\n", converted );
}
Solution 3:
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 = 0x9B2;
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)
Solution 4:
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 result0xFFF0
of typeint
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
expectunsigned int
andint
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 toint
, 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