Scan to FollowLearn Embedded Together, learn and grow together

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:
- Bottom-up integration: Test the hardware abstraction layer first, then gradually integrate upwards.
- Simulated dependencies: Use hardware simulators or stub modules for early integration.
- 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.

Follow 【Learn Embedded Together】 to become better together.。
If you find this article useful, click “Share”, “Like”, or “Recommend”!