Key Points for Designing Compatibility in Embedded Software!

In the process of embedded development, if compatibility design is not taken into account, it may lead to:

  • Loss of functionality after software upgrades
  • Protocol changes causing communication issues between old and new versions
  • Data structure changes leading to system crashes
  • APP and device version mismatches causing functional anomalies

Today, I will share the five golden rules of embedded software compatibility design:

1. The Importance of Compatibility Design

1.1 Five Dimensions of Compatibility

  1. Data Compatibility: Forward compatibility of data structures and protocols
  2. Interface Compatibility: Ensuring the stability of API interfaces
  3. System Compatibility: Cross-platform and cross-version compatibility
  4. Functional Compatibility: Consistency in user experience
  5. Performance Compatibility: Stability of performance metrics

2. Rule One: Data Compatibility – The Art of Protocol Design

2.1 Forward-Looking Protocol Design

Flawed Protocol Design

// Shortsighted protocol design
typedef struct {
    uint8_t id;        // 1-byte ID - may not be enough later!
    uint8_t length;    // 1-byte length - limits packet size!
    uint8_t data[];    // Data content
} protocol_bad_t;

Problem Analysis:

  • ID is only 1 byte, allowing for a maximum of 255 commands
  • Length is only 1 byte, maximum packet size is 255 bytes
  • Poor extensibility, protocol will inevitably need to be changed later

Correct Protocol Design

// Forward-looking protocol design
typedef struct {
    uint16_t magic;     // Magic number for protocol identification
    uint8_t version;    // Protocol version number
    uint8_t reserved;   // Reserved field for future expansion
    uint16_t id;        // 2-byte ID, enough for 65536 commands
    uint16_t length;    // 2-byte length, supports 64KB packets
    uint8_t data[];     // Data content
    uint16_t checksum;  // Checksum
} protocol_good_t;

Design Advantages:

  • Version Field: Supports protocol evolution
  • Reserved Field: Space reserved for future expansion
  • Reasonable Length: Balances resources and extensibility
  • Checksum Mechanism: Ensures data integrity

2.2 Correct Way to Add Data

Scenario Description

The original device information includes IP and MAC addresses:

#define MSG_ID_DEV_INFO   0x0001

typedef struct {
    char dev_ip[16];    // Device IP
    char dev_mac[18];   // Device MAC
} dev_info_t;

Later, if a device serial number needs to be added, what should be done?

Destructive Modification

// Directly modifying the original structure - will break compatibility!
typedef struct {
    char dev_ip[16];
    char dev_mac[18];
    char dev_sn[32];    // New field breaks the original structure
} dev_info_t;

Consequences: The old version APP will fail to parse new data!

Compatible Addition

// Keep the original structure unchanged
#define MSG_ID_DEV_INFO   0x0001
#define MSG_ID_DEV_SN     0x0002  // New message ID

typedef struct {
    char dev_ip[16];
    char dev_mac[18];
} dev_info_t;

typedef struct {
    char dev_sn[32];
} dev_sn_t;

Advantages:

  • The old version APP can still display IP and MAC normally
  • The new version APP can access all information
  • Gradual upgrades, controllable risks

3. Rule Two: Interface Compatibility – The Wisdom of API Design

3.1 Traps of Interface Modification

Real Case

Original system status enumeration:

typedef enum {
    SYS_STATUS_IDLE,     // 0 - Idle
    SYS_STATUS_RUNNING,  // 1 - Running
    SYS_STATUS_STOP,     // 2 - Stopped
} sys_status_t;

A new status needs to be added, and the developer modifies it like this:

// Incorrect modification method!
typedef enum {
    SYS_STATUS_IDLE,        // 0
    SYS_STATUS_NEW_STATUS,  // 1 - Inserted new status
    SYS_STATUS_RUNNING,     // 2 - Value changed!
    SYS_STATUS_STOP,        // 3 - Value changed!
} sys_status_t;

Disastrous Consequences:

  • All displayed status icons are scrambled
  • Running status is displayed as the new status
  • Users cannot understand the device status at all

Correct Modification Method

typedef enum {
    SYS_STATUS_IDLE = 0,     // Explicitly specify values
    SYS_STATUS_RUNNING = 1,   // Keep original values unchanged
    SYS_STATUS_STOP = 2,      // Keep original values unchanged
    SYS_STATUS_NEW_STATUS = 3, // New status placed at the end
} sys_status_t;

3.2 Best Practices for Interface Design

Versioned API Design

// API version management
typedef struct {
    uint8_t major;    // Major version: incompatible API changes
    uint8_t minor;    // Minor version: backward-compatible feature additions
    uint8_t patch;    // Patch version: backward-compatible bug fixes
} api_version_t;

// Get API version
api_version_t get_api_version(void) {
    return (api_version_t){1, 2, 3};  // v1.2.3
}

// Compatibility check
bool is_api_compatible(api_version_t required) {
    api_version_t current = get_api_version();
    
    // Major version must be the same
    if(current.major != required.major) {
        return false;
    }
    
    // Minor version must be greater than or equal to the required version
    if(current.minor < required.minor) {
        return false;
    }
    
    return true;
}

Extensible Interface Design

// Extensible configuration structure
typedef struct {
    uint32_t size;        // Structure size for version identification
    uint32_t version;     // Configuration version
    
    // Basic configuration (v1.0)
    uint32_t baudrate;
    uint8_t data_bits;
    uint8_t stop_bits;
    
    // Extended configuration (v1.1+)
    uint8_t flow_control;  // New field
    uint8_t reserved[3];   // Reserved field
} uart_config_t;

// Compatibility configuration function
int uart_config(uart_config_t *config) {
    // Check structure size and version
    if(config->size < sizeof(uart_config_t) || 
       config->version < 0x0100) {
        // Use default values for old version configurations
        return uart_config_v1_0(config);
    }
    
    // Handle new version configurations
    return uart_config_v1_1(config);
}

4. Rule Three: System Compatibility – Wisdom Across Platforms

4.1 Choosing Between Dynamic and Static Libraries

Advantages and Risks of Dynamic Libraries

// Using dynamic libraries
#include <dlfcn.h>

void *handle = dlopen("libsensor.so", RTLD_LAZY);
if(!handle) {
    fprintf(stderr, "Cannot load library: %s\n", dlerror());
    return -1;
}

// Get function pointer
int (*sensor_init)(void) = dlsym(handle, "sensor_init");

Advantages:

  • Saves memory space
  • Library can be upgraded independently
  • High degree of modularity

Risks:

  • Library version mismatches can lead to crashes
  • Increased deployment complexity
  • Difficult dependency management

Stability of Static Libraries

// Static linking, all dependencies compiled into the executable
gcc -static main.c libsensor.a -o app

Advantages:

  • Simple deployment, single-file execution
  • Version consistency is guaranteed
  • Not affected by changes in system libraries

Disadvantages:

  • File size is larger
  • Upgrading requires replacing the entire program

4.2 Cross-Platform Compatibility Strategies

// Platform abstraction layer design
typedef struct {
    int (*gpio_init)(int pin);
    int (*gpio_write)(int pin, int value);
    int (*gpio_read)(int pin);
    int (*delay_ms)(int ms);
} platform_ops_t;

// STM32 platform implementation
static platform_ops_t stm32_ops = {
    .gpio_init = stm32_gpio_init,
    .gpio_write = stm32_gpio_write,
    .gpio_read = stm32_gpio_read,
    .delay_ms = stm32_delay_ms,
};

// ESP32 platform implementation  
static platform_ops_t esp32_ops = {
    .gpio_init = esp32_gpio_init,
    .gpio_write = esp32_gpio_write,
    .gpio_read = esp32_gpio_read,
    .delay_ms = esp32_delay_ms,
};

// Application layer code, platform-independent
void app_main(platform_ops_t *ops) {
    ops->gpio_init(LED_PIN);
    while(1) {
        ops->gpio_write(LED_PIN, 1);
        ops->delay_ms(500);
        ops->gpio_write(LED_PIN, 0);
        ops->delay_ms(500);
    }
}

5. Rule Four: Functional Compatibility – Consistency in User Experience

5.1 Compatibility of User Interfaces

// Indicator light state definition - once defined, do not change the meaning
typedef enum {
    LED_STATE_OFF = 0,           // Off
    LED_STATE_SLOW_BLINK = 1,    // Slow blink: normal operation
    LED_STATE_FAST_BLINK = 2,    // Fast blink: data transmission
    LED_STATE_ALWAYS_ON = 3,     // Always on: system error
    // New states can only be added at the end, cannot be inserted in the middle
    LED_STATE_BREATH = 4,        // Breathing light: standby state
} led_state_t;

5.2 Compatibility of Configuration Parameters

// Versioned management of configuration files
typedef struct {
    uint32_t config_version;    // Configuration version number
    
    // v1.0 Basic configuration
    uint32_t network_timeout;
    uint32_t retry_count;
    
    // v1.1 New configurations (keep backward compatible)
    uint32_t keep_alive_interval;  // New field
    uint32_t max_connections;      // New field
    
    // Reserved expansion space
    uint32_t reserved[8];
} system_config_t;

// Compatibility configuration loading
int load_config(system_config_t *config) {
    // Read configuration file
    if(read_config_file(config) != 0) {
        return -1;
    }
    
    // Handle compatibility based on version number
    switch(config->config_version) {
        case 0x0100:  // v1.0
            // Set default values for new fields
            config->keep_alive_interval = 30;
            config->max_connections = 10;
            break;
            
        case 0x0101:  // v1.1
            // New version, use directly
            break;
            
        default:
            // Unknown version, use default configuration
            set_default_config(config);
            break;
    }
    
    return 0;
}

6. Rule Five: Performance Compatibility – Ensuring User Experience

6.1 Establishing Performance Benchmarks

// Performance monitoring framework
typedef struct {
    uint32_t boot_time_ms;        // Boot time
    uint32_t response_time_ms;    // Response time
    uint32_t memory_usage_kb;     // Memory usage
    uint32_t cpu_usage_percent;   // CPU usage
} performance_metrics_t;

// Performance benchmark check
bool check_performance_compatibility(performance_metrics_t *current) {
    static const performance_metrics_t baseline = {
        .boot_time_ms = 5000,        // Benchmark: 5 seconds to boot
        .response_time_ms = 100,     // Benchmark: 100ms response
        .memory_usage_kb = 1024,     // Benchmark: 1MB memory
        .cpu_usage_percent = 80,     // Benchmark: 80% CPU
    };
    
    // Performance cannot regress significantly
    if(current->boot_time_ms > baseline.boot_time_ms * 1.2 ||
       current->response_time_ms > baseline.response_time_ms * 1.5 ||
       current->memory_usage_kb > baseline.memory_usage_kb * 1.3) {
        return false;  // Performance regression exceeds threshold
    }
    
    return true;
}

6.2 Controlling Upgrade Time

// Upgrade progress monitoring
typedef struct {
    uint32_t total_size;     // Total size
    uint32_t current_size;   // Current progress
    uint32_t start_time;     // Start time
    uint32_t estimated_time; // Estimated time
} upgrade_progress_t;

void update_upgrade_progress(upgrade_progress_t *progress, uint32_t bytes_written) {
    progress->current_size += bytes_written;
    
    uint32_t elapsed = get_current_time() - progress->start_time;
    if(progress->current_size > 0) {
        progress->estimated_time = (elapsed * progress->total_size) / progress->current_size;
    }
    
    // If upgrade time exceeds expected, give a warning
    if(progress->estimated_time > UPGRADE_TIMEOUT_MAX) {
        log_warning("Upgrade time exceeds expected duration");
    }
}

7. Compatibility Check

// Automatic compatibility check
#define COMPAT_CHECK(condition, message) \
    do { \
        if(!(condition)) { \
            log_error("Compatibility check failed: %s", message); \
            return -1; \
        } \
    } while(0)

int system_compatibility_check(void) {
    // Check data format compatibility
    COMPAT_CHECK(check_data_format(), "Data format incompatible");
    
    // Check API version compatibility
    COMPAT_CHECK(check_api_version(), "API version incompatible");
    
    // Check system dependencies compatibility
    COMPAT_CHECK(check_system_deps(), "System dependencies incompatible");
    
    log_info("All compatibility checks passed");
    return 0;
}

Conclusion

Compatibility design is one of the most easily overlooked yet crucial aspects of embedded software development.

Key Points to Note:

  1. Data Compatibility: Protocol design should be forward-looking
  2. Interface Compatibility: API changes should be backward compatible
  3. System Compatibility: Consider cross-platform and dependency management
  4. Functional Compatibility: Maintain consistency in user experience
  5. Performance Compatibility: Ensure performance does not regress

Interaction

What other good compatibility design experiences do you want to share?

Remember to like and bookmark, so more embedded engineers can see these practical compatibility design tips!

Follow me for more embedded software architecture and design experiences!

Leave a Comment