The author has limited capabilities, and if there are any errors in the text, I would greatly appreciate it if friends could point them out. Thank you!
Concept of Bit Fields
A bit field (also known as a bit segment) is a data structure that allows data to be stored compactly in bits and enables programmers to manipulate the bits of this structure. The advantages of this data structure are:
-
It can save storage space for data units, which becomes particularly evident when a program requires thousands of data units.
-
Bit fields allow easy access to parts of an integer value, thereby simplifying the source code.
Definition of Bit Fields
In general, the definition of bit fields can be divided into two main categories: structure bit fields and union bit fields. Since the definitions of unions and structures are similar, the formal definition of bit fields appears the same for both.
Structure Bit Fields
The general form of defining a structure bit field is as follows:
struct BitFieldStructName
{
TypeSpecifier BitFieldName : Length;
} StructureVariableName;
Here is a simple example to illustrate:
struct example0
{
unsigned char x : 3;
unsigned char y : 2;
unsigned char z : 1;
} ex0_t;
What does the above definition mean? It can be clearly understood with a diagram. The following image shows the storage location of the defined structure bit field in memory:
From the image, we can see that although the type of x is unsigned char, it does not occupy 8 bits but rather 3 bits, and its value range becomes 0 to 2^3-1. From the above image, we can also infer the size of this structure bit field. The author used the printf function to output the size of the structure bit field as:
The Value of sizeof(ex0_t) is : 1 byte
The size of structure bit fields follows this principle: The total size of the structure bit field is a multiple of the size of the widest basic type member. This principle is the same as the one described in the author’s previous article “Analysis of Structure Memory Alignment” regarding the total size of structures.
Union Bit Fields
The general form of defining a union bit field is roughly similar to that of a structure definition. Here is a simple example:
union example1
{
unsigned char x : 3;
unsigned char y : 2;
unsigned char z : 1;
} ex1_u;
Similarly, the author provides the storage location of the union bit field in memory:
The author also provides the size of the union bit field:
The Value of sizeof(ex1_u) is : 1 byte
Thus, we can conclude that the principle governing the size of union bit fields is: The total size of the union bit field is the size of the largest basic type member.
Detailed Explanation of Structure Bit Fields
Using Unsigned Types for Bit Fields
As the title suggests, unsigned data types should be used in the process of using bit fields. Below is an example to illustrate this:
struct BitField_8
{
char a : 2;
char b : 3;
} BF8;
BF8.a = 0x3;/* 11 */
BF8.b = 0x5;/* 101 */
printf("%d,%d\n", BF8.a, BF8.b);
The output of the above is:
-1,-3
The output is not what we expected. The reason is that both BF8.a and BF8.b are signed, which means there is a sign bit present. Since the highest bit is 1, it represents a negative number, and negative numbers are stored in the computer in two’s complement form, leading to the above results. Therefore, to avoid this issue, the char in BitField_8 should be changed to unsigned char, resulting in an output of 3,5.
Prohibited Operations on Bit Fields
Due to the special nature of bit fields, there are some characteristics that differ from ordinary variables:
-
Structure bit field members cannot be addressed.
struct BitField_8
{
unsigned char a : 2;
} BF8;
printf("%p\n", &BF8.a); /* Error */
-
Structure bit field members cannot be declared static.
struct BitField_8
{
static unsigned char a : 2;/* Error */
} BF8;
-
Structure bit field members cannot be arrays.
struct BitField_8
{
unsigned char a[5] : 5;/* Error */
} BF8;
Impact of Different Processors and Compilers on Bit Fields
Although bit fields can manipulate data in bits, it is advised to use them cautiously due to the varying results produced by different processor architectures and compilers, which is the reason for the poor portability of bit fields.
Impact of Processors
The impact of processors on bit fields is easy to understand. Big-endian and little-endian processors will store the following structure bit fields differently. This is relatively simple; if friends are unclear about this issue, they can refer to the author’s article “The Concept of Union and Its Application in Embedded Programming”.
Impact of Compilers
Different Types of Structure Bit Field Members
Different compilers yield different results for bit fields. For example, consider the following code:
struct BitField_5
{
unsigned int a : 4;
unsigned char b : 4;
} BF_8;
int main(void)
{
printf("The Value of sizeof(BF_8) is:%lu bytes\n", sizeof(BF_8));
}
In the defined structure bit field, different data types for members are handled differently by different compilers. For Visual Studio, when faced with different data types, after storing the first member a, it will allocate a new 4-byte space for storage. Therefore, the running result of the above code in Visual Studio is:
The Value of sizeof(BF_8) is 8 bytes
It can be seen that in the VS environment, using bit fields in this way not only fails to save memory space but actually increases it compared to structures. The above is the test result in the VS environment, while the following is the test result in the GCC environment:
The Value of sizeof(BF_8) is 4 bytes
It can be seen that in the GCC environment, even if the data types of the structure bit field members are inconsistent, they are stored in a “compressed” manner, meaning that the members of the structure bit field are stored contiguously.
Sum of Member Sizes Exceeds One Basic Storage Space
In addition to the different handling of members of different types by different compilers, when the sum of member sizes exceeds one basic storage space, different compilers also have different handling methods. For example, consider the following code:
struct short_flag_t
{
unsigned short a : 7;
unsigned short b : 10;
};
For the above code, in addition to defining members of the same type in this way, it can also be defined as follows:
struct short_flag_t
{
unsigned short a : 7,/* Note the comma here */
b : 10;
};
The above code, since the size of unsigned short is 2 bytes, and the combined size of members a and b exceeds 2 bytes, there are two possible storage methods:
-
a and b are adjacent
-
b is allocated memory in the next available storage unit
Different compilers may adopt different storage methods in this situation. For GCC, the second method is used. If the compiler adopts the first method, but the program requires the second method for storage, how should it be handled? In this case, we need to use the syntax of anonymous zero-length bit field to force the bit field to be stored in the next storage unit. The example code is as follows:
struct short_flag_t
{
unsigned short a : 2;
unsigned short : 0;
unsigned short b : 3;
}
In the above code, for a and b, b will not be stored immediately next to a but will be forced to be stored in the next storage unit.
Applications of Bit Fields
The above discusses the basic concepts related to bit fields. Now that we understand the basic concepts, what can we do with bit fields? The most straightforward application is to use structure bit fields to define flags. In bare-metal development, where there are no mechanisms like semaphores or events, we typically define some binary variables that exist only in the range of 0 to 1. Before using bit fields, the smallest variable type was always 1 byte. By using structure bit fields, we can define the number of bits for these variables based on their value range, thus saving memory.
Accessing Microcontroller Registers
Bit fields are influenced by processors and compilers. Before using them, we must be clear about whether the current processor is big-endian or little-endian and understand how the current compiler affects the defined bit fields.
If we want to use bit fields to access an 8-bit register, the register might look something like this:
We can construct a data structure using structure bit fields as follows:
typedef union
{
unsigned char Byte;
struct
{
unsigned char bit012 : 3;
unsigned char bit34 : 2;
unsigned char bit5 : 1;
unsigned char bit6 : 1;
unsigned char bit7 : 1;
} bits;
} registerType;
Now, assuming the address of this register is 0x00008000, we can define a pointer to point to this address as follows:
registerType *pReg = (registerType *)0x00008000;
After making the above definition, we can operate on the register. First, we can manipulate the bits of the register using bit fields, like this:
pReg->bits.bit5 = 1;
pReg->bits.bit012 = 7;
Of course, we can also use the union’s feature to directly manipulate the entire register, as follows:
pReg->Byte = 0x55;
Using bit fields to access registers, we must note that this example is based on little-endian alignment.
Conclusion
Although the use of bit fields appears to be more flexible, it is essential to understand our processor and compiler. To write portable code, it is advisable to avoid using bit fields.
References:
[1] https://aticleworld.com/access-the-port-and-register-using-bit-field-in-embedded-c/
[2] https://www.raviyp.com/bitfields-in-c-for-accessing-microcontroller-registers/
[3] https://aticleworld.com/bit-field-in-c/
Your reading is my greatest encouragement, and your suggestions are my greatest improvement. Feel free to click the image below to enter the mini-program for comments, or add the author on WeChat for mutual exchange. The WeChat QR code can be found at the bottom of the public account.