The Three Musketeers of Embedded Programming: Practical Uses of Structs, Unions, and Variants

Understand these three programming tools from a practical perspective to make your embedded code more elegant and efficient!

🎯 Introduction: Why Learn to Use Them Together?

Imagine you are managing a smart home system:

  • β€’ Structs are like a storage box that neatly organizes related items together.
  • β€’ Unions are like a transformer that can change into different forms within the same space.
  • β€’ Variants are like a multi-functional room that can transform into a bedroom, study, or gym as needed.

Core Question: Why can’t we use just one structure alone?

The answer is simple: The real world is complex! Embedded systems require:

  • β€’ βœ… Save memory space
  • β€’ βœ… Improve code readability
  • β€’ βœ… Simplify data management
  • β€’ βœ… Enhance operational efficiency

πŸ“š Core Concepts: The Essential Differences Among the Three Musketeers

Memory Layout Comparison

Union Concept

State 1
State 2
State 3
Union (union)

Member A: 4 bytes
Member B: 4 bytes
Member C: 4 bytes
Total Size: 4 bytes
Struct (struct)

Member A: 4 bytes
Member B: 2 bytes
Member C: 1 byte
Padding: 1 byte
Total Size: 8 bytes

Feature Comparison Table

Feature Struct Union Variant Concept
Memory Usage Sum of all member sizes Size of the largest member Dynamic change
Data Access Access all members simultaneously Only one member can be used at a time Mutual exclusion of states
Main Use Data organization Memory sharing State management
Typical Scenario Sensor data packets Data type conversion Device operating modes

πŸ”§ Practical Tip 1: Sensor Data Management

Problem Scenario

You are developing an environmental monitoring device that needs to handle data from multiple sensors: temperature, humidity, pressure, light intensity, etc. The traditional approach might look like this:

// Traditional approach - scattered management
float temperature;
float humidity;
uint32_t pressure;
uint16_t light;
uint8_t sensor_status;

// Parameters need to be passed one by one during data processing
void process_data(float temp, float humi, uint32_t press, uint16_t light, uint8_t status) {
    // Processing logic...
}

πŸ’‘ Struct Optimization Solution

// Struct solution - unified management
typedef struct {
    float temperature;    // 4 bytes
    float humidity;       // 4 bytes
    uint32_t pressure;    // 4 bytes
    uint16_t light;       // 2 bytes
    uint8_t status;       // 1 byte
    uint8_t reserved;     // 1 byte (alignment padding)
    // Total: 16 bytes
} sensor_data_t;

// Data processing becomes simpler
void process_sensor_data(sensor_data_t *data) {
    if (data->status & 0x01) {  // Check temperature sensor status
        printf("Temperature: %.2fΒ°C\n", data->temperature);
    }
    // Other processing logic...
}

πŸš€ Union Further Optimization

In some scenarios, sensor data needs to be transmitted via serial or network, and this is where unions come into play:

typedef union {
    sensor_data_t data;           // Structured access
    uint8_t bytes[16];           // Byte array access
    uint32_t words[4];           // 32-bit word access
} sensor_packet_t;

// Data transmission example
void send_sensor_data(sensor_packet_t *packet) {
    // Send directly by bytes without conversion
    uart_send_bytes(packet->bytes, sizeof(sensor_data_t));
}

// Data checksum example
uint32_t calculate_checksum(sensor_packet_t *packet) {
    uint32_t checksum = 0;
    // Calculate checksum by 32-bit words for higher efficiency
    for (int i = 0; i < 4; i++) {
        checksum ^= packet->words[i];
    }
    return checksum;
}

πŸ“Š Performance Comparison




Traditional Method
Function Parameters: 5
Memory Access: Scattered
Transmission Efficiency: Low
Struct Method
Function Parameters: 1 pointer
Memory Access: Continuous
Transmission Efficiency: Medium
Union Optimization
Function Parameters: 1 pointer
Memory Access: Continuous
Transmission Efficiency: High

βš™οΈ Practical Tip 2: Device State Machine Design

Problem Scenario

A smart socket has multiple operating modes: manual control, timer control, remote control, and energy-saving mode. Each mode has different configuration parameters.

Initialization
Set Timer
Enable Remote
Enter Energy Saving
Restore Manual
Manual Control Mode
Timer Control Mode
Remote Control Mode
Energy Saving Mode

πŸ’‘ Union State Management

// Define configuration parameters for various modes
typedef struct {
    uint8_t switch_state;    // Switch state
    uint8_t brightness;      // Brightness level
} manual_config_t;

typedef struct {
    uint8_t switch_state;
    uint32_t on_time;        // On time (seconds)
    uint32_t off_time;       // Off time (seconds)
    uint8_t repeat_days;     // Repeat days (bitmask)
} timer_config_t;

typedef struct {
    uint8_t switch_state;
    uint32_t server_ip;      // Server IP
    uint16_t port;          // Port number
    uint8_t auth_token[16]; // Authentication token
} remote_config_t;

typedef struct {
    uint8_t switch_state;
    uint8_t power_threshold; // Power threshold
    uint16_t sleep_delay;    // Sleep delay
} powersave_config_t;

// Key: Use union to manage configuration of different modes
typedef enum {
    MODE_MANUAL,
    MODE_TIMER,
    MODE_REMOTE,
    MODE_POWERSAVE
} device_mode_t;

typedef struct {
    device_mode_t current_mode;

    union {
        manual_config_t manual;
        timer_config_t timer;
        remote_config_t remote;
        powersave_config_t powersave;
    } config;

    // Common state
    uint32_t uptime;
    float power_consumption;
} smart_socket_t;

🎯 Mode Switching Implementation

// Unified mode switching function
int switch_device_mode(smart_socket_t *device, device_mode_t new_mode) {
    // Save current state
    uint8_t current_switch = 0;

    switch (device->current_mode) {
        case MODE_MANUAL:
            current_switch = device->config.manual.switch_state;
            break;
        case MODE_TIMER:
            current_switch = device->config.timer.switch_state;
            break;
        case MODE_REMOTE:
            current_switch = device->config.remote.switch_state;
            break;
        case MODE_POWERSAVE:
            current_switch = device->config.powersave.switch_state;
            break;
    }

    // Switch to new mode
    device->current_mode = new_mode;

    // Initialize configuration for new mode
    switch (new_mode) {
        case MODE_MANUAL:
            device->config.manual.switch_state = current_switch;
            device->config.manual.brightness = 50;  // Default brightness
            break;

        case MODE_TIMER:
            device->config.timer.switch_state = current_switch;
            device->config.timer.on_time = 8 * 3600;   // 8:00
            device->config.timer.off_time = 22 * 3600; // 22:00
            device->config.timer.repeat_days = 0x7F;   // Every day
            break;

        // Other modes initialization...
    }

    printf("Device switched to mode: %d\n", new_mode);
    return 0;
}

⚑ Memory Optimization Effects

// Memory usage comparison
sizeof(manual_config_t);     // 2 bytes
sizeof(timer_config_t);      // 13 bytes
sizeof(remote_config_t);     // 23 bytes
sizeof(powersave_config_t);  // 4 bytes

// If using separate variables
// Total memory: 2 + 13 + 23 + 4 = 42 bytes

// Using union
sizeof(smart_socket_t);      // Approximately 35 bytes (including common fields)
// Memory saved: About 17%

πŸ› οΈ Practical Tip 3: Configuration Parameter Management

Problem Scenario

Embedded devices often have many configuration parameters: network settings, sensor calibration values, user preferences, etc. A clear hierarchical management scheme is needed.

πŸ’‘ Nested Struct Design

// Network configuration
typedef struct {
    uint32_t ip_address;
    uint32_t subnet_mask;
    uint32_t gateway;
    uint32_t dns_server;
    char ssid[32];
    char password[64];
} network_config_t;

// Sensor configuration
typedef struct {
    float temp_offset;       // Temperature offset calibration
    float temp_scale;        // Temperature scaling factor
    uint16_t sample_interval; // Sampling interval (milliseconds)
    uint8_t filter_level;    // Filter level 0-5
} sensor_config_t;

// User interface configuration
typedef struct {
    uint8_t language;        // 0: Chinese 1: English
    uint8_t brightness;      // Screen brightness 0-100
    uint8_t volume;          // Volume 0-100
    uint32_t screen_timeout; // Screen timeout (seconds)
} ui_config_t;

// Main configuration structure - hierarchical management
typedef struct {
    // Configuration version number
    uint32_t version;
    uint32_t magic_number;   // Used to verify configuration validity

    // Classified configurations
    network_config_t network;
    sensor_config_t sensor;
    ui_config_t ui;

    // Configuration checksum
    uint32_t checksum;
} device_config_t;

🎯 Configuration Management Functions

// Default configuration initialization
void init_default_config(device_config_t *config) {
    config->version = 0x00010001;      // v1.0.1
    config->magic_number = 0x12345678;

    // Default network configuration
    config->network.ip_address = 0xC0A80101;  // 192.168.1.1
    config->network.subnet_mask = 0xFFFFFF00; // 255.255.255.0
    strcpy(config->network.ssid, "MyDevice");
    strcpy(config->network.password, "");

    // Default sensor configuration
    config->sensor.temp_offset = 0.0f;
    config->sensor.temp_scale = 1.0f;
    config->sensor.sample_interval = 1000;    // 1 second
    config->sensor.filter_level = 2;

    // Default UI configuration
    config->ui.language = 0;                  // Chinese
    config->ui.brightness = 80;
    config->ui.volume = 50;
    config->ui.screen_timeout = 60;           // 60 seconds

    // Calculate checksum
    config->checksum = calculate_config_checksum(config);
}

// Configuration validation
bool validate_config(device_config_t *config) {
    // Check magic number
    if (config->magic_number != 0x12345678) {
        printf("Configuration file corrupted: Magic number error\n");
        return false;
    }

    // Check version compatibility
    if ((config->version >> 16) > 1) {
        printf("Configuration version incompatible\n");
        return false;
    }

    // Checksum verification
    uint32_t calculated = calculate_config_checksum(config);
    if (calculated != config->checksum) {
        printf("Configuration file corrupted: Checksum error\n");
        return false;
    }

    return true;
}

// Save configuration to Flash
int save_config_to_flash(device_config_t *config) {
    // Recalculate checksum
    config->checksum = calculate_config_checksum(config);

    // Write to Flash (simplified here)
    return flash_write(CONFIG_FLASH_ADDR, (uint8_t*)config, sizeof(device_config_t));
}

πŸ“Š Configuration Management Flowchart



Yes
No


Yes
No


Yes
No

Device Startup
Read Configuration from Flash
Is Configuration Valid?
Load User Configuration
Load Default Configuration
Configuration Applied Successfully
Runtime Configuration Modification
Need to Save?
Calculate Checksum
Write to Flash
Write Successful?
Update Confirmation
Error Handling

πŸ’‘ Best Practice Summary

Usage Principles

Scenario Recommended Solution Reason
Data Packet Transmission Struct + Union Need both structured access and byte-level operations
Multiple Operating Modes Enum + Union Save memory, type safety
Hierarchical Configuration Nested Structs Logical clarity, easy maintenance
Data Type Conversion Union Efficient type conversion, no extra overhead

⚠️ Cautions

  1. 1. Memory Alignment Issues
    // Possible alignment issue
    typedef struct {
        char a;      // 1 byte
        int b;       // 4 bytes, may start from address 4
        char c;      // 1 byte
    } bad_struct_t;  // May occupy 12 bytes instead of 6 bytes
    
    // Consider alignment in design
    typedef struct {
        int b;       // 4 bytes, starts from address 0
        char a;      // 1 byte
        char c;      // 1 byte
        char padding[2]; // Explicit padding
    } good_struct_t; // Clearly occupies 8 bytes
    
  2. 2. Union Type Safety
    typedef struct {
        enum { TYPE_INT, TYPE_FLOAT } type;  // Type identifier
        union {
            int i;
            float f;
        } value;
    } safe_union_t;
    
    // Check type when using
    void print_value(safe_union_t *data) {
        switch (data->type) {
            case TYPE_INT:
                printf("Integer: %d\n", data->value.i);
                break;
            case TYPE_FLOAT:
                printf("Float: %.2f\n", data->value.f);
                break;
        }
    }
    
  3. 3. Byte Order Issues
    typedef union {
        uint32_t value;
        struct {
            uint8_t byte0;  // Lowest byte in little-endian systems
            uint8_t byte1;
            uint8_t byte2;
            uint8_t byte3;  // Highest byte
        } bytes;
    } endian_test_t;
    

πŸš€ Performance Optimization Tips

  1. 1. Reduce Function Parameters: Use struct pointers instead of multiple independent parameters
  2. 2. Memory Locality: Keep related data in the same struct
  3. 3. Cache Friendly: Avoid overly large structs, consider cache line size
  4. 4. Compiler Optimization: Use <span>const</span> and <span>restrict</span> keywords to help the compiler optimize

πŸŽ“ Advanced Learning Recommendations

Subsequent Learning Path







Master Basic Concepts
Practical Project Exercises
Deepen Memory Management
Learn Design Patterns
Performance Optimization
Temperature and Humidity Monitoring System
Smart Home Controller
Simple Communication Protocol
Memory Alignment Mechanism
Cache Optimization Strategies
DMA and Data Structures
State Machine Pattern
Observer Pattern
Factory Pattern

Recommended Practice Projects

  1. 1. Environmental Monitoring Station
  • β€’ Multi-sensor data collection
  • β€’ Data packet transmission
  • β€’ Configuration parameter management
  • 2. Smart Switch
    • β€’ Multiple operating modes
    • β€’ State persistence
    • β€’ Remote control protocol
  • 3. Simple Oscilloscope
    • β€’ High-speed data acquisition
    • β€’ Memory buffer management
    • β€’ Data compression algorithms

    πŸ“š Further Reading

    • β€’ “Embedded C Programming Practice”
    • β€’ “Deep Exploration of C++ Object Model”
    • ARM Cortex-M Series Processor Programming Manual
    • Real-Time System Design Principles

    πŸŽ‰ Conclusion

    Mastering the combination of structs, unions, and variants is like mastering the “inner skills” of embedded programming. They not only make your code more elegant but also significantly enhance system performance and maintainability.

    Key Points Review:

    • β€’ βœ… Structs are responsible for data organization, aggregating related information together.
    • β€’ βœ… Unions are responsible for memory optimization, allowing different types to share the same space.
    • β€’ βœ… State machine design makes complex logic clear and controllable.
    • β€’ βœ… Hierarchical configuration keeps parameter management orderly.

    πŸ’¬ Interactive Communication

    What data management challenges have you encountered in embedded projects? Feel free to share your experiences in the comments, and let’s discuss better solutions together!

    πŸ”” Follow us for more embedded development insights!

    Leave a Comment