You can even go crazy and turn the value directly into a double.
So I'm going to flex my programming muscles and sorry if I loose you, but hopefully you and others can get something from this quite long reply.
Disclaimer, I'm a noob at electronics but not at coding, but I might make a mistake since I'm not even going to compile this to see if it does what I think it does.
So lets start by declaring some not too obvious C structures (they also work for C++).
//
// Create a double, little-endian sample. i.e. Intel (NOT Motorola that would be big endian.)
//
// Define a single bit to fit on an unsigned 64 bit long.
typedef unsigned long bit:1;
// Define a structure used to access bits individually from 0 to 63.
// not really needed but added for completeness.
typedef struct {
bit bits[64];
} b_section;
//
// Define a structure that splits a double in sections so we can construct them from the different parts that form them.
//
// Double Precision Float notes, taken from IEEE 754 i.e. wikipedia
//
// If exponent is 0x000 is signed zero if mantissa is 0 (-0)
// If exponent is 0x000 then is a subnormal if mantissa is not 0.
// If exponent is 0x7ff then is infinite if mantissa is 0
// If exponent is 0x7ff then is a NaN (Not a Number, ie. 0 divided by 0) if mantissa is not 0.
//
// Exponent is 1023 biased (0x3ff).
// Value if exponent is > 0x000 and <0x7ff, then mantissa has a [1.] implied
// -1*(sign)*2^(exponent-1023)*[1.]mantissa
// This is what we want for values equal or greater than 1.0 (denoting Vref+1LSB or higher)
//
// Value on sub normals (if Exponent is 0 and mantissa is different than 0)
// -1*(sign)*2^(1-1023)*[0.]mantissa
// This is what we want for the range less than 1.0 and into the negative.
//
// Example values from wikipedia:
// 0000 0000 0000 0000 = 0
// 8000 0000 0000 0000 = -0
// 7ff0 0000 0000 0000 = positive infinite
// fff0 0000 0000 0000 = negative infinite
//
// Little endian so less significant bits go first.
typedef struct {
unsigned long mantissa:52;
unsigned long exponent:11;
unsigned long sign:1;
} d_section;
typedef union {
double val;
b_section b_val; // b_ for bit value
d_section s_val; // s_ for sections value
} convert_to_double;
Note that a union allows you to see a memory location in different ways. So if you wanted to access bit 0 of a convert_to_double value you can access it by value.b_val.bits[0], or access bit 63 with value.b_val.bits[63]
Now that we have all those structures and the converting union defined, you read your full 28 bit value (including the sub less significant bit (subLSB) 4 bit value data) as well as the sign and the extra range. First declare your result value of type convert_to_double and also declare the extended range variable for later use.
convert_to_double result;
int extendedRange;
Then, going to your old code (you can adjust this to your newer code if you want) read the whole 32 bits of data, but now we need the space and the default int (32 bits usually) is signed, so we need to go for a long and better make it unsigned. Even if the top two bits are 0 and it shouldn't flip the sign but just in case.
unsigned long in = SpiRead(); // input must be an unsigned long make sure SpiRead returns an unsigned long as well.
Now you can assign the sign signal to the result (0 for positive in the double, 1 for negative in the double).
Lets assign the extended range as well (1 for True, or 0 for False for later use).
BTW this code:
register = (condition_to_evaluate)? value_if_true : value_if_false;
is the same as:
if(condition_to_evaluate)
register = value_if_true;
else
register = value_if_false;
Note, don't use the construct with macro definitions, because it usually yields to bad behavior, but on normal constructs it should be just fine.
result.s_val.sign = (in&0x20000000ul)?0:1; // bit 29 is 1 when positive, undetermined right at 0V and 0 when bellow 0V
extendedRange = (in&0x10000000ul)?1:0; // bit 28 is 1 if outside 0-Vref, 0 if within range.
If we want Vref to represent the value 0.99999999999(9) (within range where under parenthesis means periodic) and 1.0000(0) to represent Vref+1LSB (outside of the range) then we need to use the mantissa as a sub normal. (meaning that we have to force the exponent to be 0. We know this is true if the sign is positive (result.s_val.sign == 0) and if we are not on the extended range.
// Subnormal between 0 to 0.9999 [.0] implied before mantissa will take the value 0, otherwise 1023 to imply [1.] before mantissa
result.s_val.exp = ((result.s_val.sign == 0)&&(extendedRange == 0))? 0 : 0x3ff;
Note i'm using the logical and (&&) the bitwise and (&) will work too but makes it look strange, I could make it shorter and faster but then it would be bit harder to read.
For example:
result.s_val.exp = (result.s_val.sign|extendedRange)? 0x3ff : 0;
That would read if sign or extended range then exponent is biased by 1023 otherwise use subnormal.
And that is fine and faster to compute, so use it instead if you want.
Edit maniac added:
Let's make it a bit faster but more obscure coding.
result.s_val.exp = (result.s_val.sign|extendedRange)*0x3ff;
Almost there, we just need to put the mantissa in place.
So we have 28 bits (including the extra 4 subLSB bits) and the mantissa can hold 52.
So we need to shift to the left by the difference in precision, in our case is 52-28 so it's 24 left.
Edit: noticed I forgot to mask off the higher 4 control bits so I fixed it
// Adjust reading into the mantissa.
result.s_val.mantissa = (in&0xffffffful)<<24;
Now if you don't want the low 4 bits, you still shift by 24 but you mask the 4 bits first.
// Adjust reading into mantissa discarding low sub LSB 4 bits.
result.s_val.mantissa = (in&0xffffff0ul)<<24;
A note for shifting: Since you are on base 2 (binary) if you shift right you divide by powers of 2, if you shift left you multiply by powers of 2. Most people think about the truncation side of shifting but shifting makes fast dividers/multipliers (as long as it's a power of 2 you want to multiply or divide by).
Edit(nth) yet another edit to clarify one thing. So if you were doing this in assembly instead of C you will have the choice to rotate left with carry and other shift assembler instructions. it wont matter much other than the less significant bit will either stick while shifting or become 0, think about it as a math floor function or a ceil (ceiling) function. But in this context it wouldn't make a difference.
So what do you get after all this?
Well, you can access the double directly, say you want to return the value read, then you will return the following double (your function will have to be declared to return a double value as well).
// Return read value. Range will be from -0.125 up to 1.125.
// [-0.125 to 0.0) for extended negative range.
// [0.0 to 1.0) for within range
// [1.0 to 1.125] for extended positive range.
// multiply the value by Vref to obtain the actual voltage.
return result.val;
And there you have it, sorry for the length and detail of this, I hope it helps not just you and I also hope I didn't make any horrendous coding mistake, but like I said, I didn't test this or tried to compile it.
Edit: on the last code (the return value) in the comments about the range, the square bracket "[" means included, the parenthesis "(" means excluded.
One more Edit:
If you want to print the double you can do so with
printf("Input as in percent of Vref = %lf\n",result.val);
Yet one more edit:
This code assumes 1 byte data alignment, some programs change that and that can mess up the structure and the union.
Yet one more edit edit:
To help with the visualization here is an image that shows the double:
You can modify this to use floats instead but more constrained.
32 bits total.
8 bits exponent bias is 127 from the value depicted on the field
23 bits mantissa so you will have to stick to the 24bits and truncate one of those as well.
Apologize for the many edits, just correcting erratas as any good engineer (even if it's software) should do!
Also just noticed that this will allow for positive and negative 0 I think, so when the datasheet mentions the sign is undetermined at 0, this will gracefully handle it. (Not!, read bellow)
You can also define new structures to add into the union, like the full 64 bits divided by nibbles (half a byte or 4 bits), bytes, words and double words. then you can use those to help you read the data in more directly, reducing time.
Ok, last edit I swear, but I'm really not sure on the undetermined sign bit flip at 0 volts and had plenty of wine so I don't want to think about it. but if sign is is 1 (positive or 0 in the on the double bit field) and and the extended range is 0 then the exponent is the subnormal, meaning implied [0.]mantissa or 0, but if the sign flips to 0 (negative or 1 on the sign bit field in the double) and the extended range is 0, then the exponent will be the normal exponent implying [1.] so figure out how to solve that edge case, because it will spike to -1*Vref when it's really 0. You can add the mantissa being 0 to determine using the subnormal exponent or not. (expensive at 52 bits compare to 0, well not really if the compiler does the right thing and can use the z flag after some operation with the mantissa holding register to determine if it's 0 or not).
Anyways, that will fix it but don't use it as illustrated above because the bug (-1Vref spike) will be hard to find.