Structures, Unions, Enums, and Bit Fields
Embedded C is often about giving names to memory. Structures, unions, enums, and bit fields help you describe related data, but they must be used with care when bytes cross a hardware or protocol boundary.
Learning Objectives
By the end of this lesson, you should be able to:
- define structures for grouped state and configuration;
- use enums for readable states and modes;
- explain what unions do and why they can be risky;
- recognize when bit fields are unsuitable for register maps or wire formats.
Structures
A structure groups related fields into one object.
#include <stdint.h>
typedef struct {
uint32_t baud_rate;
uint8_t data_bits;
uint8_t stop_bits;
uint8_t parity_enabled;
} uart_config_t;
This is useful because the function interface can now pass one meaningful object:
void uart_init(const uart_config_t *config);
Padding and Alignment
The compiler may insert padding between fields so the CPU can access values efficiently.
typedef struct {
uint8_t id;
uint32_t value;
} sample_t;
This may occupy 8 bytes instead of 5. Never assume a structure's byte layout unless you verify it for the target compiler, ABI, and packing rules.
A safer internal structure often orders wider fields first:
typedef struct {
uint32_t value;
uint8_t id;
} sample_compact_t;
This can reduce padding on many targets, but it is still not a portable wire format. Use sizeof, static assertions, map files, or debugger inspection when layout matters.
Enums
Enums give readable names to related integer values.
typedef enum {
MOTOR_STOPPED = 0,
MOTOR_STARTING,
MOTOR_RUNNING,
MOTOR_FAULT
} motor_state_t;
Enums are excellent for state machines and configuration choices.
Unions
A union stores different members in the same memory.
typedef union {
uint32_t word;
uint8_t bytes[4];
} word_view_t;
Unions are useful for controlled reinterpretation, but they are easy to misuse. Endianness, strict aliasing, and compiler behavior matter. For portable protocol parsing, prefer explicit shifts and masks.
Bit Fields
Bit fields look convenient:
typedef struct {
unsigned enable : 1;
unsigned mode : 2;
unsigned reserved : 5;
} control_bits_t;
They are readable, but their layout is implementation-defined. Bit order, packing, and access width may differ across compilers and targets.
Safer Register Access Pattern
For hardware registers, masks are usually clearer and more portable:
#define CTRL_ENABLE_MASK (1u << 0)
#define CTRL_MODE_SHIFT 1u
#define CTRL_MODE_MASK (3u << CTRL_MODE_SHIFT)
static inline uint32_t ctrl_make_mode(uint32_t mode)
{
return (mode << CTRL_MODE_SHIFT) & CTRL_MODE_MASK;
}
When reading a multi-byte protocol field, parse the byte order explicitly:
uint16_t read_u16_be(const uint8_t bytes[2])
{
return ((uint16_t)bytes[0] << 8) | (uint16_t)bytes[1];
}
That code documents that the field is big-endian and does not depend on the CPU's native endianness.
Worked Example: Sensor Sample
typedef enum {
SENSOR_OK = 0,
SENSOR_OPEN_CIRCUIT,
SENSOR_SHORT_CIRCUIT,
SENSOR_TIMEOUT
} sensor_status_t;
typedef struct {
int16_t temperature_c_x10;
uint16_t supply_mv;
sensor_status_t status;
} sensor_sample_t;
This structure communicates meaning without relying on a packed wire format.
Common Mistakes
- Casting received bytes directly to a structure pointer.
- Using bit fields for memory-mapped registers without checking compiler layout.
- Forgetting that enum size is compiler-dependent.
- Storing protocol data in native-endian integers without conversion.
- Hiding units in field names or comments instead of naming them clearly.
- Assuming
packedmakes unaligned accesses safe or fast on every MCU.
Summary
Structures group state, enums name modes, unions overlay memory, and bit fields name small fields. Use these tools to improve readability inside firmware, but use explicit masks, shifts, and byte parsing at hardware and protocol boundaries.