Understanding Embedded C: What Are Structures?
After introducing structures, we’ll take a look at some of the important applications of this powerful data object. Then, we’ll examine the C language syntax to declare a structure. Finally, we’ll briefly introduce the data alignment requirement. We’ll see that we may be able to reduce the size of a structure by simply rearranging the order of its members.
This article provides some basic information about structures in embedded C programming.
After introducing structures, we’ll take a look at some of the important applications of this powerful data object. Then, we’ll examine the C language syntax to declare a structure. Finally, we’ll briefly introduce the data alignment requirement. We’ll see that we may be able to reduce the size of a structure by simply rearranging the order of its members.
Structures
A number of variables of the same type that are logically related to one another can be grouped as an array. Working on a group rather than a collection of independent variables allows us to arrange the data and use it more conveniently. For example, we may define the following array to store the last 50 samples of an ADC that digitizes a voice input:
uint16_t voice[50];
Note that uint16_t is an unsigned integer type with a width of exactly 16 bits. This is defined in the C standard library stdint.h, which provides data types of a specific bit length independent of the system specifications.
Arrays can be used to group a number of variables that are of the same data type. What if there is a connection between variables of different data types? Can we treat these variables as a group in our program? For example, assume that we need to specify the sampling rate of the ADC that generates the voice array above. We can define a float variable to store the sample rate:
float sample_rate;
Although the variables voice and sample_rate are related to each other, they are defined as two independent variables. To associate these two variables with each other, we can use a powerful data construct of the C language called a structure. Structures allow us to group different data types and deal with them as a single data object. A structure can include different sorts of variable types such as other structures, pointers to functions, pointers to structures, etc. For the voice example, we can use the following structure:
struct record {
uint16_t voice[50];
float sample_rate;
};
In this case, we have a structure called record that has two different members or fields: the first member is an array of uint16_t elements, and the second member is a variable of type float. The syntax begins with the keyword struct. The word after the struct keyword is an optional name used for referencing the structure later. We’ll discuss other details of defining and using structures in the rest of the article.
Why Are Structures Important?
The above example points out an important application of structures, i.e., defining application-dependent data objects that can associate individual variables of different types with each other. This not only leads to an efficient way of manipulating the data but also allows us to implement specialized structures called data structures.
Data structures can be used for various applications such as messaging between two embedded systems and storing data gathered from a sensor in noncontiguous memory locations.
Figure 1. Structures can be used to implement a linked list.
Additionally, structures are useful data objects when the program needs to access the registers of a memory-mapped microcontroller peripheral. We’ll take a look at structure applications in the next article.
Figure 2. Memory map of an STM32 MCU. Image courtesy of Embedded Systems with ARM.
Declaring a Structure
To use structures, we first need to specify a structure template. Consider the example code below:
struct record {
uint16_t voice[4];
float sample_rate;
};
This specifies a layout or template for creating the future variables of this type. This template includes an array of uint16_t and a variable of type float. The name of the template is record, and this comes after the keyword struct. It’s worthwhile to mention that there is no memory allocation for storing a structure template. Memory allocation occurs only after a structure variable based on this layout is defined. The following code declares the variable mic1 of the above template:
struct record mic1;
Now, a section of memory is allocated for the variable mic1. It has space to store the four uint16_t elements of the array and one float variable.
The members of a structure can be accessed using the member operator (.). For example, the following code assigns 100 to the first element of the array and copies the value of sample_rate to the fs variable (which must be of type float).
mic1.voice[0]=100;
fs=mic1.sample_rate;
Other Ways to Declare a Structure
We looked at one way of declaring structures in the previous section. The C language supports some other formats that will be reviewed in this section. You’ll probably stick to one format throughout your programs, but being familiar with the other ones can be helpful at times.
The general syntax for declaring the template of a structure is:
struct tag_name {
type_1 member_1;
type_2 member_2;
…
type_n member_n;
} variable_name;
The tag_name and variable_name are optional identifiers. We’ll usually see at least one of these two identifiers, but there are cases where we can eliminate both of them.
Syntax 1: When both tag_name and variable_name are present, we’re defining the structure variable just after the template. Using this syntax, we can rewrite the previous example as follows:
struct record {
uint16_t voice[4];
float sample_rate;
} mic1;
Now, if we need to define another variable (mic2), we can write
struct record mic2;
Syntax 2: Only variable_name is included. Using this syntax, we can rewrite the example in the previous section as follows:
struct {
uint16_t voice[4];
float sample_rate;
} mic1;
In this case, we have to define all of our variables just after the template and we cannot define any other variable later in our program (because the template doesn’t have a name and we cannot refer to it later).
Syntax 3: In this case, there is no tag_name or variable_name. Structure templates defined in this way are referred to as anonymous structures. An anonymous structure can be defined within another structure or union. An example is given below:
struct test {
// Anonymous structure
struct {
float f;
char a;
};
} test_var;
To access the members of the above anonymous structure, we can use the member operator (.). The following code assigns 1.2 to the member f.
test_var.f=1.2;
Since the structure is anonymous, we access its members by using the member operator only once. If it had a name as in the following example, we would have to use the member operator twice:
struct test {
struct {
float f;
char a;
} nested;
} test_var;
In this case, we should use the following code to assign 1.2 to f:
test_var.nested.f=1.2;
As you can see, anonymous structures can make the code more readable and less verbose. It is also possible to use the typedef keyword along with a structure to define a new data type. We’ll look at this method in a future article.
Memory Layout for a Structure
The C standard guarantees that the members of a structure will be located in memory one after another in the order in which the members are declared within the structure. The memory address of the first member will be the same as the address of the structure itself. Consider the following example:
struct Test2{
uint8_t c;
uint32_t d;
uint8_t e;
uint16_t f;
} MyStruct;
Four memory locations will be allocated to store the variables c, d, e, and f. The order of memory locations will match that of declaring the members: the location for c will have the lowest address, then d, e, and finally, f will appear. How many bytes do we need to store this structure? Considering the size of the variables, we know that, at least, 1+4+1+2=8 bytes are required to store this structure. However, if we compile this code for a 32-bit machine, we’ll surprisingly observe that the size of MyStruct is 12 bytes rather than 8! This is due to the fact that a compiler has certain constraints when allocating memory for different members of a structure. For example, a 32-bit integer can be stored only at memory locations whose address is divisible by four. Such constraints, referred to as data alignment requirements, are implemented to let the processor access variables more efficiently. Data alignment leads to some wasted space (or padding) in the memory layout. This topic is only introduced here; we’ll go through the details in the next article of this series.
Figure 3. Data alignment leads to some wasted space (or padding) in the memory layout.
Being aware of the data alignment requirements, we may be able to rearrange the order of members within a structure and make memory usage more efficient. For example, if we rewrite the above structure as given below, its size will decrease to 8 bytes on a 32-bit machine.
struct Test2{
uint32_t d;
uint16_t f;
uint8_t c;
uint8_t e;
} MyStruct;
For a memory-constrained embedded system, it’s a significant savings to reduce the size of a data object from 12 bytes to 8 bytes, particularly when a program requires many of these data objects.
The next article will discuss data alignment in greater detail and examine some examples of using structures in embedded systems.
Summary
- Structures allow us to define application-dependent data objects that can associate individual variables of different types with one another. This leads to an efficient means of manipulating the data.
- Specialized structures, called data structures, can be used for various applications such as messaging between two embedded systems and storing data gathered from a sensor in noncontiguous memory locations.
- Structures are useful when we need to access the registers of a memory-mapped microcontroller peripheral.
- We may be able to make memory usage more efficient by rearranging the order of the members within a structure.
To see a complete list of my articles, please visit this page.
Nicely done @Steve Arar!!!
I recommend you use typedef when declaring your structs and enumerated.
Cheers Steve, I am trying to improve my C literacy and this series has been very helpful