Data structures¶
Learning Objectives: Using data structures in C and their use cases in embedded systems programming.
A data structure (English struct) is a logical structure/combination/aggregation of several variables, where related information can be grouped for unified handling. Data structures are a very common concept in programming, and they also have use cases in embedded programming, so let's take a look at how they are used in C.
In C, the word reserved for defining a data structure is
struct
, and a structure is defined as a block as follows. The members of a data structure in C can be of any variable types.struct structure_type_name {
member declarations;
}
For example, let's define a data structure of type
point
, which has x and y coordinates as members and a point name as a string. Then, we define another structure rect
, which consists of point
type structures.struct point {
uint16_t x;
uint16_t y;
char name[16];
};
struct rect {
struct point max;
struct point min;
struct point all_points[10];
};
When defining a data structure (struct), you are not initializing any variables. Defining a struct simply creates a new data type that describes the structure. Later, in your program, you can create variables of this struct type and initialize them. The initialization happens when you assign values to the actual variables, not during the struct definition.
Data Structures in a C Program¶
To use the members of a data structure in a program, two new operators are needed:
- Members are referenced using the
.
operator, similar to referencing any other variable. - When the structure is a pointer or contains pointers, they are referenced using the
->
operator.
Example:
// Defining the structures
struct point {
uint16_t x;
uint16_t y;
};
struct rect {
struct point max;
struct point min;
struct point all_points[10];
};
// Variables and initialization
struct point min_pt;
min_pt.x = 100;
min_pt.y = 100;
struct rect box;
box.max.x = 320;
box.max.y = 200;
printf("%d,%d", box.max.x, box.max.y);
Declaration and Initialization¶
Syntactically, the declaration and initialization of a data structure can be done like any variable. In this, C language syntax is flexible, and there are several ways to initialize a structure.
// Variable definition immediately after structure declaration. Followed by initialization.
// here point1
struct point {
uint16_t x;
uint16_t y;
char name[20];
} point1;
point1.x = 320;
point1.y = 200;
char midpoint[] = "Midpoint";
// Use string library functions!
strncpy(point1.name, midpoint, strlen(midpoint));
// As a separate variable
struct point point2;
point2.x = 320;
point2.y = 200;
strncpy(point2.name, midpoint, strlen(midpoint));
struct point point3 = { 320, 200, "Midpoint" };
Please, observe how when initializing a string inside a structure, we need to use the
strncpy
function instead of a direct assignment. In C, strings are handled as arrays of characters, and the =
operator cannot be used to assign values to arrays after their declaration. Instead, strncpy
allows us to copy the contents of one string (such as "Midpoint") into the character array (name
in this case) defined in the structure.There are other ways to declare and initialize data structures, but the ones above are the clearest.
Data structures can also be "assigned" to each other, in which case each member is copied.
min_pt = max_pt;
Pointers and Data Structures¶
When used as function parameters, as we explained before, data structures are typically passed by pointers because when we pass an argument to a function, a copy of the data structure will be created, and we want to save memory.
To reference a member of a data structure through a pointer, we use the
->
operator. Notice that the asterisk (*) operator is not used, because the arrow operator replaces it!p1_ptr->point.x;// Equivalent to (*p1_ptr).point.x;
Below is a code example demonstrating its usage.
#include <stdio.h>
#include <inttypes.h>
struct point {
uint16_t x;
uint16_t y;
} point = { 160, 100 };
int main() {
struct point *p1_ptr = &point;
printf("x=%d y=%d\n", point.x, point.y);
printf("x=%d y=%d\n", p1_ptr->x, p1_ptr->y);
// Assign to the y member via pointer
p1_ptr->y = 103;
printf("x=%d y=%d\n", point.x, point.y);
return 0;
}
When referencing members of nested structures with a pointer, you need to be aware of which member the pointer is exactly pointing to.
#include <stdio.h>
#include <inttypes.h>
struct rect {
struct point {
uint16_t x;
uint16_t y;
} point;
} r = { .point.x = 101, .point.y = 102 };
int main() {
struct rect *p1_ptr = &r;
// Use the arrow operator for pointer_r's members
// Use the dot operator for point's members
printf("x=%d y=%d)\n", p1_ptr->point.x, p1_ptr->point.y);
return 0;
}
Note! Also check the string library function
memcpy
, which can be used to copy entire blocks of memory. In other words, the content of a data structure can be copied to another by copying the memory area allocated for the structure using pointers. The proximity to the hardware allows us to use this method.#include <stdio.h>
#include <string.h> // For memcpy
// Definition of the struct point
struct point {
uint16_t x;
uint16_t y;
char name[20];
};
int main() {
// Declare and initialize p1
struct point p1 = {320, 200, "Midpoint"};
// Declare another struct to copy into
struct point p2;
// Copy the contents of p1 into p2 using memcpy
memcpy(&p2, &p1, sizeof(struct point));
// Print both structs to show they are now identical
printf("p1: x = %d, y = %d, name = %s\n", p1.x, p1.y, p1.name);
printf("p2: x = %d, y = %d, name = %s\n", p2.x, p2.y, p2.name);
return 0;
}
Comparing Data Structures¶
Comparing data structures is perhaps most clearly done using a custom function, where each member's values are compared using pointers.
Example: The function returns a pointer to the data structure that has the larger value in the
x
member.#include <stdio.h>
#include <inttypes.h>
struct point {
uint16_t x;
uint16_t y;
};
// Function that returns a pointer to the data structure
struct point *which_is_larger(struct point *a, struct point *b);
int main() {
struct point first = { 320, 200}, second = { 321, 201 };
struct point *larger = which_is_larger(&first, &second);
printf("x=%d y=%d\n", larger->x, larger->y);
return 0;
}
struct point *which_is_larger(struct point *a, struct point *b) {
if (a->x > b->x) {
return a;
} else {
return b;
}
}
This pointer stuff is starting to get pretty advanced...
Embedded Data Structures¶
As stated earlier, a data structure is a convenient way to logically group related information into a cohesive package. In embedded systems, such a package could consist of sensor data collected at the same moment or perhaps the configuration parameters of a peripheral device or the transmission settings for wireless communication. Since sensor data is often analyzed as a time series, it is essential to know the exact time of measurement. Many applications also use data collected simultaneously from multiple sensors in the implementation of more intelligent services, where the timestamp ties together different types of data.
No lexer for alias 'csv-formatted' foundstruct sensor_data { uint32_t timestamp; float temperature; float humidity; };
And, when data is neatly packaged in a structure, it can easily be converted into a CSV-formatted string, which can then be wirelessly transmitted to an IoT backend system.
When we get to programming the hardware in this course, you will notice that many of the pre-made software components in the device's operating system actually expect their configuration parameters in the form of a data structure.
Custom Data Types¶
In addition to data structures, the C language offers the possibility to introduce custom data types by renaming or grouping standardized/derived data types. These custom types behave exactly like their original data types. The keyword reserved for this in C is
typedef
.For example:
// Replace the type uint16_t with the name Length
typedef uint16_t Length;
Length len = 100;
printf("%d\n", len);
In embedded systems (and sometimes in C programs on workstations),
typedef
is often used to write code that is portable (transferrable) from one hardware platform to another. Now, when a program is built around custom data types, its functionality remains consistent even when the code is moved from one platform to another. On different platforms, suitable corresponding data types can simply be introduced with typedef
. This feature is especially useful when working with more complex data structures, but sometimes renaming standardized data types can also be beneficial. For example, uint16_t
provides a guaranteed 16-bit unsigned integer on all platforms.This is exactly how we will do it in device programming part of the course. We will use both pre-made data structures and renamed data types because a large part of the libraries and other pre-defined configurations in the manufacturer's programming environment are based on them. The reason for this is, of course, compatibility and ease of transferring code from the manufacturer's devices to others, ensuring that all code works as expected in the manufacturer's embedded operating system.
In Conclusion¶
You don't need to stress about the various ways to declare and initialize data structures presented in the material. It's enough to learn one method and use it, as all are equally correct.
Give feedback on this content
Comments about this material