Structures and User Types
Introduction
An int holds one number. Real firmware deals with bundles of related data: a motor has a pin, a voltage, and an RPM; a sensor reading has a timestamp, a value, and a unit; a CAN frame has an ID, length, and payload bytes. Structures let you group those fields into one named type so your code matches how you think about the hardware.
This article also covers typedef (shorter names), unions (same memory, different views), and enums (named constants). Together they are how HAL headers, protocol specs, and driver code stay readable.
Structures — a custom type with named fields
1 2 3 4 5 | |
struct motor is a blueprint. Each variable you create from it gets its own copy of all three fields laid out next to each other in memory.
Creating and using variables
1 2 3 4 5 6 7 | |
In plain terms
A struct is a lunch box with compartments. m1.port opens the "port" compartment. m1.rpm opens another. You carry one box, not three separate bags.
If you forget to initialize, fields contain garbage until you assign — same as plain int variables.
Arrays of structures
When you have several motors or sensors, use an array:
1 2 3 4 5 6 | |
Looping is natural: for (int i = 0; i < NUM_MOTORS; i++) { ... motors[i].rpm ... }. This pattern appears in ADC channel tables, LED strips, and multi-axis robot configs.
Pointers to structures — arrow vs dot
When you have a pointer to a struct, use -> instead of (*p).field:
1 2 3 4 5 | |
Why pass pointers to functions? Copying a large struct costs time and stack space. Passing struct motor *m passes one address; the function can read and update the original:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | |
This is the same "give me the address so I can fill it in" idea as in Pointers in depth.
Nested structures — structs inside structs
Real models are hierarchical. A circle has a center point and a radius:
1 2 3 4 5 6 7 8 9 10 11 | |
You read inside-out: circ.center.x. For a robot, you might nest pose (x, y, theta) inside waypoint, and an array of waypoint inside path.
Dynamic arrays of structures
When the count is only known at runtime (user input, number of nodes on a bus):
1 2 3 4 5 6 7 8 9 10 11 12 | |
(ptr + k) points at the k-th circle — pointer arithmetic from Pointers in C.
typedef — a shorter name for a type
In C you normally write:
1 | |
typedef creates an alias:
1 2 3 4 5 6 7 | |
Embedded codebases use _t suffix by convention (gpio_config_t, uart_handle_t). ST HAL and CMSIS are full of these.
You can typedef plain types too:
1 | |
That documents intent: "this char is a small counter, not ASCII text".
Union — one memory slot, different interpretations
A union holds several fields, but only one at a time — they share the same bytes:
1 2 3 4 5 | |
sizeof(number_t) is roughly the largest member (here 20 bytes for the string), not the sum.
1 2 3 4 5 | |
In plain terms
A union is a power outlet with different shaped plugs in the same hole. Only one plug fits at a time. If you stick in the float plug, the int plug's meaning is gone until you write again.
Why use this in embedded?
- Hardware registers — read as full
uint32_tor as bitfields - Protocol parsing — same two bytes as
uint16_tlength or twouint8_tfields - Tagged variants — pair a
enum typewith a union of payloads
You must track which member is active — C does not do that for you.
Enumeration — named numbers instead of magic constants
1 2 3 4 5 6 7 8 | |
Compiler assigns 0, 1, 2, 3 unless you specify values. Compare to:
1 | |
vs
1 | |
Use enums for states, error codes, pin modes, and protocol message types. They make switch statements self-documenting.
Padding and alignment — why sizeof surprises you
The compiler may insert empty bytes between fields so int and double start at addresses they can read efficiently:
1 2 3 4 5 | |
For network packets and hardware registers, wrong layout breaks interoperability. Use stdint types, #pragma pack, or __attribute__((packed)) only when you understand the trade-off — unaligned access can fault on some ARM cores.
Relevant topics
Starting points
- Define
sensor_reading_twith timestamp, value, and unit enum. - Pass it by pointer to a
print_reading(const sensor_reading_t *r)function. - Show union overwrite — print
num.iafter assigningnum.f. - Print
sizeofa struct before and after#pragma pack(1)on a test struct (lab only).
Focus points
- Dot on values, arrow on pointers — mixing them up is a compile error.
- Initialize structs in safety code —
{0}zeros all fields. - Unions need a tag or convention for which member is valid.
- Padding matters for DMA and wire formats — verify with
sizeofand layout diagrams.
Key points
structgroups related fields into one type.typedefcreates readable aliases (motor_t).unionoverlays fields in one memory region — one active view at a time.enumreplaces magic numbers with meaningful names.