Decoupling Embedded Software Modules: Three Key Methods from Chaos to Clarity

Changing a sensor requires modifying 100 lines of code? This article teaches you three methods to completely solve the module coupling problem.

1. Have You Encountered These Problems?

As an embedded engineer, the most painful experiences include:

🔥 Hardware Change Nightmare: The project needs to replace the DHT11 temperature and humidity sensor with the SHT30, only to find that the data collection, processing, display, and upload modules all need changes, resulting in over 100 lines of code modified and three days of testing that introduced new bugs.

🔥 One Change Affects All: Just wanting to optimize the serial port driver, but discovering that protocol parsing, application logic, and UI display are all affected, leading to issues with other functionalities after the changes.

🔥 Team Collaboration Difficulties: Three people developing different functionalities but all modifying the same main.c file, making every code merge a nightmare.

The root cause of these problems is one: high coupling between modules!

2. What is Module Decoupling?

Decoupling is about reducing the dependencies between modules, allowing each module to work as independently as possible.

To understand this with a real-life analogy:

  • High Coupling: Like gluing parts together with super glue, changing one part requires dismantling everything
  • Low Coupling: Like connecting with screws, you can change whichever part you want without affecting others

A simple judgment criterion:

  • • ✅ Low Coupling: Modifying one module does not require changes to other modules
  • • ❌ High Coupling: Change one place, errors pop up everywhere

3. Three Core Decoupling Methods

Method 1: Layered Architecture

Divide the system into three layers based on responsibilities:

Application Layer (Application)  ← Business Logic, User Interaction
   ↓
Middleware Layer (Middleware)   ← Protocol Processing, Algorithm Implementation
   ↓
Hardware Layer (HAL)          ← Driver Encapsulation, Hardware Operations

Key Principle: The upper layer calls the lower layer, and the lower layer does not depend on the upper layer.

Code Example:

// ❌ Incorrect: All logic mixed together
void main_task(void) {
    uint8_t data = DHT11_REG;  // Directly read register
    float temp = data * 0.1;    // Directly process
    LCD_Write(temp);            // Directly display
    UART_Send(temp);            // Directly upload
}

// ✅ Correct: Decoupled through layers
void main_task(void) {
    sensor_data_t data = sensor_read();  // Middleware layer interface
    display_show(data);                  // Display interface
    comm_upload(data);                   // Communication interface
}

Value: Changing the sensor only requires modifying the sensor module, and the application layer code remains unchanged.

Method 2: Interface Abstraction

Code depends on abstract interfaces rather than specific implementations.

Implementation Steps:

1. Define a Unified Interface

typedef struct {
    void          (*init)(void);
    sensor_data_t (*read)(void);
    void          (*sleep)(void);
} sensor_ops_t;

extern const sensor_ops_t *g_sensor;

2. Implement Interfaces for Different Hardware

// DHT11 Implementation
static void dht11_init(void) { /* DHT11 Initialization */ }
static sensor_data_t dht11_read(void) { /* DHT11 Read */ }

const sensor_ops_t dht11_ops = {
    .init  = dht11_init,
    .read  = dht11_read,
    .sleep = dht11_sleep
};

// SHT30 Implementation
const sensor_ops_t sht30_ops = {
    .init  = sht30_init,
    .read  = sht30_read,
    .sleep = sht30_sleep
};

3. Use in Application Layer

void app_init(void) {
    #ifdef USE_DHT11
        g_sensor = &dht11_ops;  // Use DHT11
    #else
        g_sensor = &sht30_ops;  // Use SHT30
    #endif
    g_sensor->init();
}

void app_task(void) {
    sensor_data_t data = g_sensor->read();  // No concern for specific sensor
}

Value: The application layer code does not change a line and can support new hardware!

Method 3: Message Queue

Tasks communicate through a message queue, implementing a producer-consumer model.

Implementation Example:

// Define Message
typedef struct {
    uint8_t  msg_type;
    float    value;
} sensor_msg_t;

QueueHandle_t msg_queue;

// Producer: Collect Data
void sensor_task(void *param) {
    sensor_msg_t msg;
    while(1) {
        msg.value = read_sensor();
        xQueueSend(msg_queue, &msg, 0);  // Send to queue
        vTaskDelay(1000);
    }
}

// Consumer: Process Data
void process_task(void *param) {
    sensor_msg_t msg;
    while(1) {
        if (xQueueReceive(msg_queue, &msg, portMAX_DELAY) == pdPASS) {
            process_data(msg.value);  // Process data
        }
    }
}

Value:

  • • Two tasks are completely decoupled and do not depend on each other
  • • The queue provides buffering, smoothing peak loads
  • • Thread-safe, RTOS automatically protects

4. Practical Case: Temperature and Humidity System Refactoring

Before Refactoring (High Coupling)

void main(void) {
    while(1) {
        // All functionalities mixed together
        uint8_t data = DHT11_ReadByte();
        float temp = data * 0.1 - 40;

        if (temp > 30) {
            GPIO_SetBits(ALARM_PIN);  // Alarm
        }
        LCD_ShowTemp(temp);           // Display
        UART_Send(temp);              // Upload

        delay_ms(1000);
    }
}

After Refactoring (Decoupled Architecture)

void app_init(void) {
    sensor_manager_init(&dht11_ops);  // Easily switch to &sht30_ops
    display_init();
    alarm_init();
    comm_init();
}

void app_task(void) {
    while(1) {
        sensor_data_t data = sensor_read();  // Abstract interface

        if (data.is_valid) {
            display_show(data);       // Display module
            alarm_check(data);        // Alarm module
            comm_upload(data);        // Communication module
        }

        delay_ms(1000);
    }
}

Effect Comparison

Comparison Item Before Refactoring After Refactoring
Changing Sensor Modify 100+ Modify 1 file
Independent Testing Requires complete hardware Can use simulation implementation
Team Collaboration Frequent conflicts Each responsible for their module

5. Avoid These Misconceptions

Misconception 1: Over-Design – Designing a 5-layer architecture for simple LED controlCorrect Approach: Appropriately layer based on complexity (2 layers for small projects, 3 layers for medium projects)

Misconception 2: Unclear Interfaces<span>process(1, 2, 0)</span><span> meaning of parameters unclear</span><span>✅ </span><strong><span>Correct Approach</span></strong><span>:</span><code><span>sensor_set_mode(CONTINUOUS, 1000)</span><span> clear semantics</span>

Misconception 3: Breaking Layers – Application layer directly manipulating registersCorrect Approach: Access hardware through HAL interfaces

6. Summary and Recommendations

Core Points

  1. 1. Layered Architecture: Separation of responsibilities between HAL layer, middleware layer, and application layer
  2. 2. Interface Abstraction: Depend on interfaces rather than specific implementations
  3. 3. Message Queues: Loose coupling communication between tasks

Learning Path

🎯 Step 1: Start with a small module (like LED driver), try layered design🎯 Step 2: Learn STM32 HAL library, FreeRTOS source code🎯 Step 3: Adopt decoupled architecture from the design phase in new projects

Take Action Now

✅ Choose a small module in your project and start refactoring today✅ Establish code templates for direct reuse next time✅ Share with the team to unify design standards

Remember: A good architecture evolves, it is not perfectly designed from the start. Start small, continuously improve, and your code will become more elegant!

💬 What coupling issues have you encountered in your projects? How did you solve them? Feel free to leave a comment for discussion!

👍 If you find this useful, please like, collect, and share with friends who need it!

Leave a Comment