Modular Design in Embedded Software Development

Scan to FollowLearn Embedded Together, learn and grow together

Modular Design in Embedded Software Development

Embedded system development is becoming increasingly complex, evolving from simple microcontroller control to today’s smart devices that integrate various sensors, communication protocols, and complex algorithms. Modular programming has become the core methodology in embedded software development.

Modular programming refers to dividing a software system into a series of highly cohesive, loosely coupled functional modules, with each module focusing on completing specific functions or tasks.

In the embedded field, modularity is not only a programming style but also a necessary strategy to address the challenges of developing in resource-constrained environments.

Advantages

1. Improved Code Maintainability

  • Localized Modifications: When a specific function needs to be modified, developers only need to focus on the relevant module without having to read through the entire codebase.
  • Error Isolation: Clear boundaries between modules can prevent errors from spreading, making problem identification faster and more accurate.
  • Simplified Version Management: Version control and updates can be performed on individual modules without affecting other parts of the system.

2. Enhanced Code Reusability

  • Cross-Project Reuse: Well-designed modules (such as drivers and protocol stacks) can be reused across different projects.
  • Product Line Development: Different modules can be combined to quickly build product variants, significantly shortening development cycles.
  • Utilization of Open Source Community: Modular architecture makes it easier to integrate third-party open-source components.

3. Improved Team Collaboration Efficiency

  • Parallel Development: Different team members can develop different modules simultaneously, reducing code conflicts.
  • Clear Responsibility Boundaries: Each module has a clear interface definition and responsible person, lowering communication costs.
  • Knowledge Encapsulation: Experts can focus on module development in specific areas (such as RF, motor control).

4. Enhanced System Reliability

  • Feasibility of Unit Testing: Modularity makes unit testing for individual functions more feasible and thorough.
  • Interface Validation: Clear module interfaces facilitate interface testing and contract validation.
  • Simplified Static Analysis: Smaller code units are easier to perform static code analysis and formal verification.

5. Optimized Resource Management

  • Controlled Memory Usage: Modular design helps to precisely control the memory usage of each function.
  • Refined Power Management: Fine-tuned low-power strategies can be implemented for different modules.
  • Guaranteed Real-Time Performance: Critical modules can receive guaranteed execution time and resource allocation.

Modular Design Methods

1. Module Partitioning Principles

Function Aggregation Criteria:

  • Each module should focus on a single function or a closely related set of functions.
  • For example: Combine sensor acquisition, filtering algorithms, and calibration functions into a “Sensor Processing” module.

Information Hiding Principle:

  • Modules should only expose necessary interfaces, with internal implementation details hidden from the outside.
  • For example: The communication module only provides send/receive interfaces, hiding the details of the underlying protocol.

Interface Minimization Principle:

  • Interactions between modules should occur through precisely defined interfaces to avoid implicit coupling.
  • Keep interfaces stable while allowing internal implementations to change freely.

2. Hierarchical Architecture Design

A typical modular hierarchical structure for embedded systems:

Application Layer
   ├── Business Logic Module
   ├── User Interface Module
   └── System Management Module
Service Layer
   ├── Communication Protocol Stack
   ├── Data Storage Module
   └── Security Service Module
Hardware Abstraction Layer
   ├── Device Driver Module
   ├── Board Support Package (BSP)
   └── Real-Time Operating System (RTOS) Interface

3. Interface Design Specifications

Function Interface Design:

// Good module interface example
typedef struct 
{
    uint32_t sample_rate;
    uint8_t resolution;
} adc_config_t;

adc_status_t adc_init(const adc_config_t* config);
adc_status_t adc_read_channel(uint8_t channel, int16_t* value);
void adc_deinit(void);

Message Interface Design:

// Event-driven message interface
typedef struct 
{
    uint16_t event_id;
    uint32_t timestamp;
    union 
    {
        int32_t  i_value;
        float    f_value;
        void*    pointer;
    } data;
} system_event_t;

bool event_queue_post(const system_event_t* event);
bool event_queue_get(system_event_t* event, uint32_t timeout_ms);

Implementation Examples

1. Modular Implementation Based on C Language

Header File Specification:

// sensor.h - Module interface declaration
#ifndef SENSOR_H
#define SENSOR_H

#include <stdint.h>

#ifdef __cplusplus
extern "C" {
#endif

typedef enum 
{
    SENSOR_OK,
    SENSOR_ERR_INIT,
    SENSOR_ERR_IO
} sensor_status_t;

sensor_status_t sensor_init(void);
float sensor_read_temperature(void);
void sensor_set_calibration(float offset, float gain);

#ifdef __cplusplus
}
#endif

#endif // SENSOR_H

Source File Implementation:

// sensor.c - Module internal implementation
#include "sensor.h"
#include "hal_i2c.h"

static float calibration_offset = 0.0f;
static float calibration_gain = 1.0f;

sensor_status_t sensor_init(void) 
{
    if(hal_i2c_init() != HAL_OK) 
    {
        return SENSOR_ERR_INIT;
    }
    // More initialization code...
    return SENSOR_OK;
}

float sensor_read_temperature(void) 
{
    uint16_t raw_value;
    hal_i2c_read(0x48, &raw_value, 2);
    
    float temperature = (raw_value * 0.02f) * calibration_gain + calibration_offset;
    return temperature;
}

void sensor_set_calibration(float offset, float gain) 
{
    calibration_offset = offset;
    calibration_gain = gain;
}

2. Object-Oriented Implementation Based on C++

Class Encapsulation Example:

// motor_controller.hpp
class MotorController 
{
public:
    enum class Status 
    {
        OK,
        OVERCURRENT,
        TIMEOUT
    };
    
    MotorController(PWMDriver& pwm, ADCDriver& adc);
    
    Status setSpeed(float rpm);
    float getCurrent() const;
    void emergencyStop();
    
private:
    PWMDriver& pwm_;
    ADCDriver& adc_;
    float current_limit_;
    
    void applyPWM(float duty);
    float readCurrent();
};

3. Task Modularization Based on RTOS

FreeRTOS Task Example:

// comm_task.c
void comm_task(void* params) 
{
    comm_config_t config = *(comm_config_t*)params;
    comm_init(&config);
    
    while(1) 
    {
        comm_message_t msg;
        if(comm_receive(&msg, pdMS_TO_TICKS(100))) 
        {
            process_message(&msg);
        }
        
        check_connection_status();
        vTaskDelay(pdMS_TO_TICKS(10));
    }
}

// Create task during system initialization
xTaskCreate(comm_task, "Comm", 512, &comm_config, 3, NULL);

General Development Process

1. Requirement Analysis and Module Planning

Module Identification Matrix Example:

Functional Requirement Related Hardware Expected Complexity Recommended Module
Temperature Acquisition ADC+I2C Medium sensor_temp
Wireless Communication (LoRa) SPI+RF Chip High comm_lora
Data Encryption Software Implementation High security_aes
User Button Handling GPIO Low input_button

2. Interface Design

Module Interface Document Template:

# ADC Module Document

## Function Overview
Provides multi-channel ADC acquisition services, supporting hardware filtering and calibration

## Interface Functions
### adc_init
```c
adc_status_t adc_init(const adc_config_t* config);
```
<p>Initializes the ADC hardware and module internal state</p><p>Parameters:</p><ul><li><span>config: Contains configuration information such as sample rate and resolution</span></li></ul><p>Return Values:</p><ul><li><span>ADC_OK: Initialization successful</span></li><li><span>ADC_ERR_HW: Hardware initialization failed</span></li></ul><p><strong><span>Usage Example</span></strong></p><pre><code class="language-c">adc_config_t cfg = 
{
    .sample_rate = 1000,
    .resolution = 12
};

if(adc_init(&cfg) != ADC_OK) 
{
    // Error handling
}

3. Independent Module Development and Testing

Unit Testing Framework Example:

// test_sensor.c
void test_temperature_calibration(void) 
{
    sensor_init();
    sensor_set_calibration(1.5f, 1.1f);
    
    float temp = sensor_read_temperature();
    TEST_ASSERT_FLOAT_WITHIN(0.1, 25.0, temp);
    
    // Simulate ADC reading
    hal_i2c_set_mock_value(0x48, 1250);
    temp = sensor_read_temperature();
    TEST_ASSERT_FLOAT_WITHIN(0.1, (1250*0.02*1.1 + 1.5), temp);
}

4. Module Integration and System Testing

Integration Testing Strategy:

  1. Bottom-up integration: Test the hardware abstraction layer first, then gradually integrate upwards.
  2. Simulated dependencies: Use hardware simulators or stub modules for early integration.
  3. Continuous integration: Automated build and testing framework.

Advanced Topics

1. Plugin Architecture

Dynamic Loading Interface:

// plugin_interface.h
typedef struct 
{
    const char* name;
    int version;
    void (*init)(void* config);
    void (*process)(void* data);
    void (*cleanup)(void);
} plugin_descriptor_t;

// The main system calls plugin functions through a function pointer table

2. Decoupling Based on Message Bus

Message Bus Implementation:

// message_bus.h
typedef struct 
{
    uint16_t msg_id;
    void* sender;
    void* data;
    size_t data_size;
} message_t;

void message_bus_init(void);
bool message_subscribe(uint16_t msg_id, void (*handler)(message_t*));
bool message_publish(message_t* msg);

3. Configuration Driver Modularization

Runtime Configuration Example:

// config.json
{
    "modules": 
    {
        "sensor": 
        {
            "type": "bme280",
            "update_interval": 1000
        },
        "communication": 
        {
            "protocol": "lora",
            "frequency": 868000000
        }
    }
}

// Dynamically load configured modules during system initialization

Common Questions

1. Modularization in Memory-Constrained Environments

Optimization Strategies:

  • Selective Loading: Only load the modules currently needed.
  • Module Compression: Compress modules stored in memory and decompress at runtime.
  • Memory Pool Sharing: Share pre-allocated memory pools between modules.

2. Balancing Real-Time Requirements

Guarantee Measures:

  • Fixed priority for critical modules.
  • Minimize cross-module calls on time-critical paths.
  • Use lock-free designs to reduce blocking between modules.

3. Cross-Platform Compatibility

Implementation Methods:

  • Standardization of Hardware Abstraction Layer (HAL).
  • Conditional compilation to handle platform differences.
  • Keep module interfaces platform-independent.

Conclusion

By rationally partitioning modules, clearly defining interfaces, and systematically designing architecture, development teams can build more reliable, maintainable, and adaptable embedded systems for future changes.

Implementing modularization requires an initial investment of design effort, but over the project lifecycle, this investment will lead to significant improvements in maintainability, team efficiency, and product quality.

It is recommended to gradually introduce modular practices starting from existing projects, continuously accumulating experience, and ultimately forming an embedded modular development system that suits the characteristics of the organization.

Modular Design in Embedded Software Development

Follow 【Learn Embedded Together】 to become better together.

If you find this article useful, click “Share”, “Like”, or “Recommend”!

Leave a Comment