I'm working on a 16 byte array or 128 bits fully populated with the following bit fields:
struct data {
uint ttg : 16; // time to go, units:mins
int volts : 16; // units:10mV
uint alarms : 16; // alarm flags
int mid_mv : 16; // mid-point volts, units:10mV
int aux_in : 2; // 0:Aux 1:Mid 2:Kelvin 3:none
int amps : 22; // units:ma
uint Ah : 20; // units:0.1Ah
uint soc : 10; // units:0.1%
uint pad : 10; // (pad to 16 bytes)
};
data datex;
void loadVals(){
datex.ttg = 0xFFFE; // mins = 45.5 days
datex.volts = 0x7FFE; // * 10mv = 327.66 volts
datex.alarms = 0x0000; // no alarms
datex.mid_mv = 0xFFFE; // * 10mv = 656.34 V (mid-point voltage)
datex.aux_in = 0x2; // mid point V
datex.amps = 0x1FFFFE; // 2097.150A
datex.Ah = 0xFFFFE; // 104857.4Ah
datex.soc = 0x320; // 80% (state of charge)
datex.pad = 0; // padding
}
If I then print:
#include <Streaming.h>
Serial << _HEX(datex.ttg ) << ','
<< _HEX(datex.volts ) << ','
<< _HEX(datex.alarms) << ','
<< _HEX(datex.mid_mv) << '/'
<< _HEX(datex.aux_in) << ','
<< _HEX(datex.amps ) << ','
<< _HEX(datex.Ah ) << ','
<< _HEX(datex.soc ) << ','
<< _HEX(datex.pad) << '\n';
I get this output:
FFFE,7FFE,0,FFFFFFFE/FFFFFFFE,1FFFFE,FFFFE,320,0
Questions:
TIA
am I approaching this the right way?
If you're trying to reduce the overall size of the struct, then, yes, this is a reasonable approach.
However, bitfields in C and C++ structs have traps and pitfalls though. Subtle mistakes are easy to make, which is why some will advice against ever using this approach.
If you're trying to specify precisely what every bit represents, then no, this is not the right approach.
Bitfields in C and C++ structs do not provide enough control to guarantee (in the general case). The same definition may yield a different layout when compiled by a different compiler. In these cases, you must use bitwise operations access individual fields.
Why are only mid_mv, aux_in reporting incorrectly?
They appear to be reporting incorrectly.
datex.mid_mv = 0xFFFE;
The literal 0xFFFE
requires 16 bits, which matches the size of the field, but it cannot be represented as a 16-bit signed int because that leaves no room for the sign.
In an ideal world, assigning a value to a variable that cannot represent that value would have been flagged by the compiler as a bug. But this isn't an ideal world. So the representation of the large positive number aliased to a negative number, and then that was sign-extended to a larger type, and then that was (possibly) reinterpreted as an unsigned number.
The solution is to define mid_mv
as a type that can represent the entire range of values it needs to. If you'll never need negative voltages, you could use uint
instead. If you do need to store negative voltages and also store positive ones larger than INT_MAX
, you'll need to use a larger integer type and ensure you don't overly restrict its size in the bitfield specification.
The problem with aux_in
is essentially the same, which may be surprising given that 2 is rather small. But here, it's the fact that the field is limited to 2 bits. The solution here is to use an unsigned type.
how best to combine these into a single 16 byte/128 bit array?
Keep in mind, the standards give the compiler more latitude than you might expect. Let me re-iterate that this is fine for packing a struct into less memory, but struct definitions do not provide enough guarantee interoperable binary formats (e.g., on disk or over a connection).
Generally, you have to think about the type, size, and alignment.
I recommend using the explicitly sized types when defining integer bitfields in a struct. This makes it easier to check the struct definition without having to know which compiler and processor is being used.
For flags and simple enumerations, choose an unsigned type.
For integer values, consider both ends of the the range of values you'll need. If you need [0..x]
, then you can use an unsigned type and you'll need at least n
bits where 2^n > x
. If you need [x..y]
where x < 0
, then you need at least n+1
bits where 2^n >= |x|
and 2^n > y
.
The type of the field determines its alignment requirement even if the allocated bit size would otherwise require less. This means you can get internal padding where you might not expect. Sometimes it makes sense to use a larger type that has the same alignment requirement as one or both of its neighboring fields. Sometimes you have to put the fields in a different order than may seem natural.