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 control✅ Correct 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 registers✅ Correct Approach: Access hardware through HAL interfaces
6. Summary and Recommendations
Core Points
- 1. Layered Architecture: Separation of responsibilities between HAL layer, middleware layer, and application layer
- 2. Interface Abstraction: Depend on interfaces rather than specific implementations
- 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!