Bit fields are ubiquitous in embedded computing. They appear in many communication protocols like CAN bus and Ethernet, in compression algorithems, and there is hardly a peripheral register that can be used without them.
Traditional methods of manipulating bit fields usualy end up in unreadable and unmaintainable code full of obscure bit masks, shifts and magic numbers. The resulting code can often only be described as write-once never touch again. If it needs any change, when for example it needs to be reused in a similar project, a deep dive into the protocol document or datasheet is required. And more often than not, the code just ends up to be rewritten from scratch, with all the associated dangers of repeating past mistakes and going through the learning curve all over again.
The goal of this article is to develop bit field manipulation code that is easy to read and maintain, self documenting, and closely maps to protocol definitions or peripheral datasheets.
For this article, it is assumed that bit fields adhere to read-modify-write semantics. This is generaly the case with communication protocols as data will be located in RAM memory, and often for contemporary peripheral registers. Alternate access semantics are left for a later discussion.
All code snippets have been compiled to assembly with Compiler Explorer using the C++17 standard. Some templates may benefit from concepts in the C++20, but at the moment a compliant C++17 is more easily found.
Bit fields are ranges of adjacent bits within a larger range of bits, the larger range normally being a full native word. The size of a native word is predominantly determined by the processor's databus width, while bit field widths are anywhere between 1 bit and the full width of the native word. It is furthermore common for multiple bit fields of different widths to be present within a single native word.
While modern processor architectures have build-in assembler instruction for extracting and inserting bit fields, it is good to understand how these work in order to understand how to translate a peripheral register desciption in a datasheet, or a protocol's packet description, into maintainable C++ code.
To obtain the content of a bit field, starting with a full native word, first all superfluous bits must be removed after which the pertinent bit field bits must be normalized (shifted left or right to align with the least significant bit).
Given a hypothetical word size of 8 bits and a bit field starting at bit 5 (with bit 0 being the least significant bit) with a width of 3 bits, obtaining the content of this bit field is illustrated by the following sequence of operations:
Read native word:
b7 | b6 | b5 | b4 | b3 | b2 | b1 | b0 |
---|---|---|---|---|---|---|---|
H/L | H/L | H/L | H/L | H/L | H/L | H/L | H/L |
Remove superfluous bits:
b7 | b6 | b5 | b4 | b3 | b2 | b1 | b0 |
---|---|---|---|---|---|---|---|
L | L | H/L | H/L | H/L | L | L | L |
Normalize pertinent bits:
b7 | b6 | b5 | b4 | b3 | b2 | b1 | b0 |
---|---|---|---|---|---|---|---|
L | L | L | L | L | H/L | H/L | H/L |
This sequence of operations could be coded in a C++ function as follows:
uint8_t GetBitField( uint8_t WordContent )
{
constexpr uint8_t FieldOffset = 3;
constexpr uint8_t FieldMask = 0b00111000;
const auto PertinentBits = WordContent & FieldMask;
const auto NormalizedBits = PertinentBits >> FieldOffset;
return NormalizedBits;
}
In this case the superfluous bits are removed using a (pre) denormalized mask and the resulting bits normalized afterwards.
It is also possible to first normalize the pertinent bits and remove the superfluous bits with a normalized mask:
uint8_t GetBitField( uint8_t WordContent )
{
constexpr uint8_t FieldOffset = 3;
constexpr uint8_t FieldMask = 0b00000111;
const auto PertinentBits = WordContent >> FieldOffset;
const auto NormalizedBits = PertinentBits & FieldMask;
return NormalizedBits;
}
In C++ those two sequences are equivalent: They yield the same indistinguishable result. In assembly though, the content of the condition code register might be different depending on the type of shift instruction being used (e.g. rotate, rotate through carry, arithmetic shift), or opportunities to use optimized bit field instructions might be missed. It is therefore always prudent to check the generated assembly code when every additional instruction counts.
When storing a new value into a bit field, care must be taken not to alter any of the other bit fields in the original data word. Thus after shifting the normalized bit field bits to their position within the native word, the bits that need to be overwritten are first cleared. The denormalized bit field bits can then be combined safely with the remaining bit fields in the original data word.
Again using the hypothetical word size of 8 bits and a bit field starting at bit 5 (with bit 0 being the least significant bit) with a width of 3 bits, setting the content of this bit field is illustrated by the following sequence of operations:
Field bits:
b7 | b6 | b5 | b4 | b3 | b2 | b1 | b0 |
---|---|---|---|---|---|---|---|
L | L | L | L | L | H | L | L |
Denormalized field bits:
b7 | b6 | b5 | b4 | b3 | b2 | b1 | b0 |
---|---|---|---|---|---|---|---|
L | L | H | L | L | L | L | L |
Original native word:
b7 | b6 | b5 | b4 | b3 | b2 | b1 | b0 |
---|---|---|---|---|---|---|---|
H/L | H/L | H/L | H/L | H/L | H/L | H/L | H/L |
Target bit field cleared:
b7 | b6 | b5 | b4 | b3 | b2 | b1 | b0 |
---|---|---|---|---|---|---|---|
H/L | H/L | L | L | L | H/L | H/L | H/L |
Updated native word:
b7 | b6 | b5 | b4 | b3 | b2 | b1 | b0 |
---|---|---|---|---|---|---|---|
H/L | H/L | H | L | L | H/L | H/L | H/L |
The equivalent C++ function would look like this:
uint8_t SetBitField( uint8_t WordContent, uint8_t PertinentBits )
{
constexpr uint8_t FieldOffset = 3;
constexpr uint8_t FieldMask = 0b00111000;
const auto DenormalizedBits = PertinentBits << FieldOffset;
WordContent &= ~FieldMask;
WordContent |= DenormalizedBits;
return WordContent;
}
From these process descriptions follows the key parameters which must be known to operate on bit fields:
One additional parameter, which becomes evident when reading ethernet protocol documents, is the bit numbering used in the documentation:
To write self documenting code, the code must closely reflect the information found in the documents on which it is based. These documents are often protocol standards or peripheral datasheets. And as every chip manufacturer, and every standards body has their own idea of the best way to present the relevant information to the unappreciated sods whom have to convert it to working code, there is no real one way in which that information is provided. But the likelyhood of finding them defined as an offset and mask combination approaches zero as can be seen in the examples below.
Take for example RFC791 which describes the Ethernet IPv4 packet header format as such:
0 | 1 | 2 | 3 | ||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 |
Version | IHL | DSCP | ECN | Total Length | |||||||||||||||||||||||||||
Identification | Flags | Fragment Offset | |||||||||||||||||||||||||||||
Time To Live | Protocol | Header Checksum | |||||||||||||||||||||||||||||
Source IP Address | |||||||||||||||||||||||||||||||
Destination IP Address |
In the early days of 8-bit microprocessors, Motorola documented the Control Register A (CRA) in the MC6821 PIA datasheet as follows:
Control Register A (CRA) | |||||||
---|---|---|---|---|---|---|---|
b7 | b6 | b5 | b4 | b3 | b2 | b1 | b0 |
IRQA 1 | IRQA 2 | CA2 Control | DDR Access | CA1 Control |
A contemporary datasheet for the NXP LPC810 describes its PIO0_17 Register this way:
Bit | Symbol | Description | Reset value | ||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
2:0 | - | Reserved | 0 | ||||||||||||||||||
4:3 | MODE |
|
0b10 | ||||||||||||||||||
5 | HYS |
|
0 | ||||||||||||||||||
6 | INV |
|
0 | ||||||||||||||||||
9:7 | - | Reserved | 0b001 | ||||||||||||||||||
10 | OD |
|
0 | ||||||||||||||||||
12:11 | S_MODE |
|
0 | ||||||||||||||||||
15:13 | CLK_DIV |
|
0 | ||||||||||||||||||
31:16 | - | Reserved | 0 |
In table 1, bit and octet numbers indicate the order in which they are send onto the wire. As binary numbers are still read left to write (see RFC1700 ), bit 0 is the most significant bit!
The definitions in table 2 and 3 use the more common bit numbering with bit 0 on the right being the least significant bit.
The commonality in the previous three examples is that bit fields are parameterized by pairs of bit locations within a native word.
The parameter which is often left implied is the bit order. In RFC791 it is explicitely metioned that the ordering is defined in another document, but for the MC6821 PIA and the LPC810 it is implied by the document's left to right reading order and the tradition of placing more significant digits left of less significant digits.
The "Version" bit field in Table 1 would be parameterized by:
The "CA2 Control" bit field in Table 2 would be parameterized by:
The "IRQA 1" bit field in Table 2 would be parameterized by:
The "CLK_DIV" bit field in Table 3 would be parameterized by:
As provided, these parameters can't be uses in actual code. To be usable they must be transformed into an offset and a mask. Ideally, this transformation is done automatically in the code to preserve the values as they appear in the original documentation. It is also desirable that the transformation is done at compile time to avoid the run time cost every time a bit field is used.
In the earlier example bit field functions, the number of bits in the native word is encoded in the variable type (uint_8
) and two literal constants are used to define the position of the bit field (FieldOffset
) and the width of the bit field (FieldMask
). Such functions work to show principles, but are hardly production quality functions.
A first step would be to inject the bit field parameters instead of having them hard-coded in the function:
struct BitField
{
//member data
constexpr static uint8_t Top = std::numeric_limits< uint8_t >::digits - 1;
constexpr static uint8_t AllOnes = std::numeric_limits< uint8_t >::max();
const uint8_t Offset;
const uint8_t Mask;
//constructor
constexpr BitField( uint8_t MSbit, uint8_t LSbit )
: Offset( LSbit )
, Mask ( (AllOnes >> (Top - MSbit + LSbit)) << LSbit )
{}
};
uint8_t GetBitField( uint8_t WordContent, const BitField& Field )
{
const auto PertinentBits = WordContent & Field.Mask;
const auto NormalizedBits = PertinentBits >> Field.Offset;
return NormalizedBits;
}
Using Compiler Explorer set to use ARM GCC 10.2 (none) with options -std=c++17 -O3 -mcpu=cortex‑m3 this code can be used to create a full program:
#include <cstdint>
#include <limits>
#include <fmt/core.h>
struct BitField
{
//member data
constexpr static uint8_t Top = std::numeric_limits< uint8_t >::digits - 1;
constexpr static uint8_t AllOnes = std::numeric_limits< uint8_t >::max();
const uint8_t Offset;
const uint8_t Mask;
//constructor
constexpr BitField( uint8_t MSbit, uint8_t LSbit )
: Offset( LSbit )
, Mask ( (AllOnes >> (Top - MSbit + LSbit)) << LSbit )
{}
};
constexpr uint8_t GetBitField( uint8_t WordContent, const BitField& Field )
{
const auto PertinentBits = WordContent & Field.Mask;
const auto NormalizedBits = PertinentBits >> Field.Offset;
return NormalizedBits;
}
constexpr BitField CA2Ctrl( 5, 3 );
constexpr BitField IRQA1 ( 7, 7 );
int main()
{
const uint8_t WordContent = 0b1010'1010;
fmt::print( "{:02x} {:02x}",
GetBitField( WordContent, CA2Ctrl ),
GetBitField( WordContent, IRQA1 ) );
return EXIT_SUCCESS;
}
//Program stdout
//05 01
CA2Ctrl
and IRQA1
have been precalculated by the compiler and the actual GetBitField
function is never called!The BitField struct
encapsulates the mask and offset required to manipulate bit fields and calculates their values from bit locations at compile time thanks to the constexpr
constructor. However, it still lacks the facility to capture the native word size.
It would of course be trivial to extend the BitField struct
with a Width
data member, but that would still leave the Mask
member variable type hard-coded. Encapsulating the type of the native word has a much higher utility.
To use a type as if it was a variable, the BitField struct
must be templatized. But this also allow types like int
or float
to be used as the type of the native word. Given the bitwise shift operators in the BitField struct
, this can introduce undefined behavior. It is therefore best to restrict the range of allowable types to unsigned integers.
template< typename T,
typename std::enable_if< std::is_unsigned< T >::value, T >::type = 0 >
struct BitField
{
//member types
using native_type = T;
//member data
constexpr static native_type Top = std::numeric_limits< native_type >::digits - 1;
constexpr static native_type AllOnes = std::numeric_limits< native_type >::max();
const native_type Offset;
const native_type Mask;
//constructor
constexpr BitField( native_type MSbit, native_type LSbit )
: Offset( LSbit )
, Mask ( (AllOnes >> (Top - MSbit + LSbit)) << Offset )
{}
};
This improved BitField
definition would be used in an actual program as follows:
#include <cstdint>
#include <limits>
#include <type_traits>
#include <fmt/core.h>
template< typename T,
typename std::enable_if< std::is_unsigned< T >::value, T >::type = 0 >
struct BitField
{
//member types
using native_type = T;
//member data
constexpr static native_type Top = std::numeric_limits< native_type >::digits - 1;
constexpr static native_type AllOnes = std::numeric_limits< native_type >::max();
const native_type Offset;
const native_type Mask;
//constructor
constexpr BitField( native_type MSbit, native_type LSbit )
: Offset( LSbit )
, Mask ( (AllOnes >> (Top - MSbit + LSbit)) << Offset )
{}
};
template< typename BitField >
constexpr typename BitField::native_type
GetBitField( typename BitField::native_type WordContent, const BitField& Field )
{
const auto PertinentBits = WordContent & Field.Mask;
const auto NormalizedBits = PertinentBits >> Field.Offset;
return NormalizedBits;
}
template< typename BitField >
constexpr typename BitField::native_type
SetBitField( typename BitField::native_type WordContent,
const BitField& Field,
typename BitField::native_type FieldBits )
{
const auto DenormalizedBits = Field.Mask & (FieldBits << Field.Offset);
WordContent &= ~Field.Mask;
WordContent |= DenormalizedBits;
return WordContent;
}
constexpr BitField< uint8_t > CA2Ctrl( 5, 3 );
constexpr BitField< uint8_t > IRQA1 ( 7, 7 );
constexpr BitField< uint32_t > CLK_DIV( 15, 13 );
int main()
{
const uint8_t Word8Content = 0b1010'1010;
const uint32_t Word32Content = 0xAAAA'AAAA;
fmt::print( "{:02x} {:02x} {:08x}\n",
GetBitField( Word8Content, CA2Ctrl ),
GetBitField( Word8Content, IRQA1 ),
GetBitField( Word32Content, CLK_DIV ) );
fmt::print( {:b} {:b}\n"",
Word8Content,
SetBitField( Word8Content, CA2Ctrl, 0x7 ) );
return EXIT_SUCCESS;
}
//Program stdout
//05 01 00000005
//10101010 10011010
Note that the SetBitField
function also masks the denormalized bit field bits. This provides a little bit of additional protection against incorrect bit field bits passed into the function.
At this stage, the definition of bit fields in the source code is matching the documentation closely. Unfortunatle, this code only works correctly for bit fields where the higher numbered bits are also more significant (so not for Ethernet packets).
The make the code work for reversed bit numbering the Mask
and Offset
calculations need to be changed.
template< typename T,
typename std::enable_if< std::is_unsigned< T >::value, T >::type = 0 >
struct BitField
{
//member types
using native_type = T;
//member data
constexpr static native_type Top = std::numeric_limits< native_type >::digits - 1;
constexpr static native_type AllOnes = std::numeric_limits< native_type >::max();
const native_type Offset;
const native_type Mask;
//constructor
constexpr BitField( native_type MSbit, native_type LSbit )
: Offset( Top - LSbit )
, Mask ( (AllOnes >> (Offset + MSbit)) << Offset )
{}
};
With a set of private helper functions and tag dispatching, both variants can be melded together.
struct ZeroIsMSB { constexpr static bool weird = true; };
struct ZeroIsLSB { constexpr static bool weird = false; };
template< typename T,
typename BitOrder,
typename std::enable_if< std::is_unsigned< T >::value, T >::type = 0 >
struct BitField
{
//member types
using native_type = T;
//member data
constexpr static native_type Top = std::numeric_limits< native_type >::digits - 1;
constexpr static native_type AllOnes = std::numeric_limits< native_type >::max();
const native_type Offset;
const native_type Mask;
//constructor
constexpr BitField( native_type MSbit, native_type LSbit )
: Offset( GetOffset( LSbit ) )
, Mask ( GetMask ( MSbit, LSbit ) )
{}
//member functions
private:
constexpr native_type GetOffset( native_type LSbit ) const
{
return BitOrder::weird? Top - LSbit : LSbit;
}
constexpr native_type GetMask( native_type MSbit, native_type LSbit ) const
{
if constexpr ( BitOrder::weird )
{
return (AllOnes >> ( Offset + MSbit )) << Offset;
}
else
{
return (AllOnes >> (Top - MSbit + LSbit)) << Offset;
}
}
};
And now there is only one, subtle, problem left: The BitField
arguments can still be transposed. In a number of cases, the compiler will fail to generate code because the Mask
calculation will be illegal. But this is not guaranteed to be the case and code may be generated which will have very hard to spot bugs.
Instead of hoping the compiler catches these mistakes, it is better to explicitely instruct the compiler to detect these mistakes. And since the bit field definitions now contain an explicit indication of bit ordering, this is possible with a little bit of extra code.
struct ZeroIsMSB { constexpr static bool weird = true; };
struct ZeroIsLSB { constexpr static bool weird = false; };
template< typename T,
typename BitOrder,
typename std::enable_if< std::is_unsigned< T >::value, T >::type = 0 >
struct BitField
{
//member types
using native_type = T;
//member data
constexpr static native_type Top = std::numeric_limits< native_type >::digits - 1;
constexpr static native_type AllOnes = std::numeric_limits< native_type >::max();
const native_type Offset;
const native_type Mask;
//constructor
constexpr BitField( native_type MSbit, native_type LSbit )
: Offset( GetOffset( LSbit ) )
, Mask ( GetMask ( MSbit, LSbit ) )
{}
//member functions
private:
constexpr native_type GetOffset( native_type LSbit ) const
{
return BitOrder::weird? Top - LSbit : LSbit;
}
constexpr native_type GetMask( native_type MSbit, native_type LSbit ) const
{
assert( BlessedOrder( MSbit, LSbit ) );
if constexpr ( BitOrder::weird )
{
return (AllOnes >> ( Offset + MSbit )) << Offset;
}
else
{
return (AllOnes >> (Top - MSbit + LSbit)) << Offset;
}
}
constexpr bool BlessedOrder( native_type MSbit, native_type LSbit ) const
{
return BitOrder::weird? MSbit <= LSbit : LSbit <= MSbit;
}
};
Now, trying to compile the following code (with a deliberate transcription misstake) will fail with a clear error message.
#include <cstdint>
#include <cassert>
#include <limits>
#include <type_traits>
#include <fmt/core.h>
struct ZeroIsMSB { constexpr static bool weird = true; };
struct ZeroIsLSB { constexpr static bool weird = false; };
template< typename T,
typename BitOrder,
typename std::enable_if< std::is_unsigned< T >::value, T >::type = 0 >
struct BitField
{
//member types
using native_type = T;
//member data
constexpr static native_type Top = std::numeric_limits< native_type >::digits - 1;
constexpr static native_type AllOnes = std::numeric_limits< native_type >::max();
const native_type Offset;
const native_type Mask;
//constructor
constexpr BitField( native_type MSbit, native_type LSbit )
: Offset( GetOffset( LSbit ) )
, Mask ( GetMask ( MSbit, LSbit ) )
{}
//member functions
private:
constexpr native_type GetOffset( native_type LSbit ) const
{
return BitOrder::weird? Top - LSbit : LSbit;
}
constexpr native_type GetMask( native_type MSbit, native_type LSbit ) const
{
assert( BlessedOrder( MSbit, LSbit ) );
if constexpr ( BitOrder::weird )
{
return (AllOnes >> ( Offset + MSbit )) << Offset;
}
else
{
return (AllOnes >> (Top - MSbit + LSbit)) << Offset;
}
}
constexpr bool BlessedOrder( native_type MSbit, native_type LSbit ) const
{
return BitOrder::weird? MSbit <= LSbit : LSbit <= MSbit;
}
};
template< typename BitField >
constexpr typename BitField::native_type
GetBitField( typename BitField::native_type WordContent, const BitField& Field )
{
const auto PertinentBits = WordContent & Field.Mask;
const auto NormalizedBits = PertinentBits >> Field.Offset;
return NormalizedBits;
}
template< typename BitField >
constexpr typename BitField::native_type
SetBitField( typename BitField::native_type WordContent,
const BitField& Field,
typename BitField::native_type FieldBits )
{
const auto DenormalizedBits = Field.Mask & (FieldBits << Field.Offset);
WordContent &= ~Field.Mask;
WordContent |= DenormalizedBits;
return WordContent;
}
constexpr BitField< uint8_t, ZeroIsLSB > CA2Ctrl( 3, 5 );
int main()
{
const uint8_t Word8Content = 0b1010'1010;
fmt::print( "{:02x}", GetBitField( Word8Content, CA2Ctrl ) );
return EXIT_SUCCESS;
}
In file included from /opt/compiler-explorer/arm/gcc-arm-none-eabi-10-2020-q4-major
/arm-none-eabi/include/c++/10.2.1/cassert:44,
from <source>:2:
<source>:82:56: in 'constexpr' expansion of 'BitField<unsigned char, ZeroIsLSB>(3, 5)'
<source>:28:28: in 'constexpr' expansion of '((BitField<unsigned char, ZeroIsLSB>*)this)
->BitField<unsigned char, ZeroIsLSB>::GetMask(MSbit, LSbit)'
<source>:47:13: error: call to non-'constexpr' function 'void __assert_func(const char*,
int, const char*, const char*)'
47 | assert( BlessedOrder( MSbit, LSbit ) );
| ^~~~~~
The compiler output shows both the reason for failing:
47 | assert( BlessedOrder( MSbit, LSbit ) );
as well as the line where the bit numbers were put in the wrong order:
<source>:82:56: in 'constexpr' expansion of 'BitField<unsigned char, ZeroIsLSB>(3, 5)'
With the self documenting requerement in place, it is time to look into methods of making the code as efficient as possible.
The code thus far is only capable of changing a single bit field at a time. Hand coders will see this as a serious short coming as they can craft magic numbers representing the combination of multiple bit fields, and thus out-perform the current code. All that they have to sacrifice is readability and maintainability, but the gains in code size and execution time may be worth that price.
To change a single bit field, the bit field bits are denormalized and combined with the original full native word with the aid of a bit field mask. To change muliple bit fields, the respective denormalized bit field bits as well as their masks must be combined first. Then, this result can be viewed as a single discontiguous bit field and used as before. Except that the result of combining the bit field bits is not a new type of bit field description, but actually represents an intermediary type (BitFieldValue
). This is obvious when considering that a BitField
description contains an Offset
and a Mask
value, while the new BitFieldValue
type consists of denormalized Bits
and a Mask
. Also obvious is that the minimum viable BitFieldValue
is the combination of a single bit field and bit field bits value.
template< typename BitField >
struct BitFieldValue
{
friend
constexpr BitFieldValue< BitField > operator|( const BitFieldValue< BitField >& lhs,
const BitFieldValue< BitField >& rhs )
{
//Catch transcription errors.
assert( 0u == (lhs.Mask & rhs.Mask) );
return BitFieldValue< BitField >( lhs.Mask | rhs.Mask, lhs.Bits | rhs.Bits );
}
//member types
using native_type = typename BitField::native_type;
//member data
const native_type Bits;
const native_type Mask;
//constructors
constexpr BitFieldValue( const BitField& Field, native_type FieldBits )
: Bits( (FieldBits << Field.Offset) & Field.Mask )
, Mask( Field.Mask )
{}
private:
constexpr BitFieldValue( native_type FieldMask, native_type FieldBits )
: Bits( FieldBits )
, Mask( FieldMask )
{}
};
This BitFieldValue
class would be used in an actual program as follows:
#include <cstdint>
#include <cassert>
#include <limits>
#include <type_traits>
#include <fmt/core.h>
struct ZeroIsMSB { constexpr static bool weird = true; };
struct ZeroIsLSB { constexpr static bool weird = false; };
template< typename T,
typename BitOrder,
typename std::enable_if< std::is_unsigned< T >::value, T >::type = 0 >
struct BitField
{
//member types
using native_type = T;
//member data
constexpr static native_type Top = std::numeric_limits< native_type >::digits - 1;
constexpr static native_type AllOnes = std::numeric_limits< native_type >::max();
const native_type Offset;
const native_type Mask;
//constructor
constexpr BitField( native_type MSbit, native_type LSbit )
: Offset( GetOffset( LSbit ) )
, Mask ( GetMask ( MSbit, LSbit ) )
{}
//member functions
private:
constexpr native_type GetOffset( native_type LSbit ) const
{
return BitOrder::weird? Top - LSbit : LSbit;
}
constexpr native_type GetMask( native_type MSbit, native_type LSbit ) const
{
//Catch transcription errors.
assert( BlessedOrder( MSbit, LSbit ) );
if constexpr ( BitOrder::weird )
{
return (AllOnes >> ( Offset + MSbit )) << Offset;
}
else
{
return (AllOnes >> (Top - MSbit + LSbit)) << Offset;
}
}
constexpr bool BlessedOrder( native_type MSbit, native_type LSbit ) const
{
return BitOrder::weird? MSbit <= LSbit : LSbit <= MSbit;
}
};
template< typename BitField >
struct BitFieldValue
{
friend
constexpr BitFieldValue< BitField > operator|( const BitFieldValue< BitField >& lhs,
const BitFieldValue< BitField >& rhs )
{
//Catch transcription errors.
assert( 0u == (lhs.Mask & rhs.Mask) );
return BitFieldValue< BitField >( lhs.Mask | rhs.Mask, lhs.Bits | rhs.Bits );
}
//member types
using native_type = typename BitField::native_type;
//member data
const native_type Bits;
const native_type Mask;
//constructors
constexpr BitFieldValue( const BitField& Field, native_type FieldBits )
: Bits( (FieldBits << Field.Offset) & Field.Mask )
, Mask( Field.Mask )
{}
private:
constexpr BitFieldValue( native_type FieldMask, native_type FieldBits )
: Bits( FieldBits )
, Mask( FieldMask )
{}
};
template< typename BitField >
constexpr typename BitField::native_type
GetBitField( typename BitField::native_type WordContent, const BitField& Field )
{
const auto PertinentBits = WordContent & Field.Mask;
const auto NormalizedBits = PertinentBits >> Field.Offset;
return NormalizedBits;
}
template< typename BitField >
constexpr typename BitField::native_type
SetBitField( typename BitField::native_type WordContent,
const BitFieldValue< BitField >& Field )
{
WordContent &= ~Field.Mask;
WordContent |= Field.Bits;
return WordContent;
}
constexpr BitField< uint8_t, ZeroIsLSB > IRQA1 ( 7, 7 );
constexpr BitField< uint8_t, ZeroIsLSB > CA2Ctrl( 5, 3 );
constexpr BitField< uint8_t, ZeroIsLSB > CA1Ctrl( 1, 0 );
int main()
{
const uint8_t WordContent = 0b1010'1010;
fmt::print( "{:08b} {:08b}\n",
WordContent,
SetBitField( WordContent,
BitFieldValue(CA2Ctrl, 0x2) |
BitFieldValue(CA1Ctrl, 0x3) |
BitFieldValue(IRQA1, 0x0) ) );
return EXIT_SUCCESS;
}
//Program stdout
//10101010 00010011