Fundamentals 17 min read

Decoupling Embedded Systems: Layers, Event‑Driven Design, and Dependency Injection

This guide explains how to achieve clean decoupling in embedded software by employing a systematic layered architecture, leveraging event‑driven and pipeline‑filter patterns for data flow, and applying dependency injection, complete with practical C code examples and best‑practice recommendations for resource‑constrained devices.

Liangxu Linux
Liangxu Linux
Liangxu Linux
Decoupling Embedded Systems: Layers, Event‑Driven Design, and Dependency Injection

1. System Layering

In embedded software development, the first step toward decoupling is a well‑defined layered architecture. A typical stack from top to bottom includes:

Application layer (business logic, UI) Middleware layer (common components) Operating‑system layer (e.g., RTOS kernel) Hardware‑abstraction layer (HAL that wraps low‑level peripherals) Hardware layer (physical components and external devices)

Layering follows three rules:

Each layer has a specific role; the application layer implements business logic without touching hardware interfaces.

Requests must travel sequentially through layers; direct cross‑layer access is prohibited.

Changes in one layer should not affect others; when unavoidable, compile‑time or runtime guards should be added.

Even on resource‑constrained MCUs, a minimal two‑layer design (hardware abstraction + business) is recommended.

2. Submodule Design

2.1 Event‑Driven Pattern

The event‑driven model separates "event producers" from "event consumers" using an event queue. An event scheduler pulls events from the queue and dispatches them to appropriate handlers, eliminating periodic polling.

1. Design an event‑filtering mechanism to drop unnecessary or duplicate events. 2. Merge high‑frequency events or defer processing to reduce load. 3. Prioritize critical events with dedicated queues. 4. Split long‑running handlers into multiple steps. 5. Use a static event pool on constrained systems; otherwise, use RTOS queues.
// Define event type
typedef struct {
    int eventType;
    void *eventData;
} Event;

// Event handler prototype
typedef void (*EventHandler)(Event *);

void registerEventHandler(int eventType, EventHandler handler);
void unregisterEventHandler(int eventType, EventHandler handler);
void publishEvent(int eventType, void *eventData);

void eventHandle_1(Event *event) { /* handle */ }
void eventHandle_2(Event *event) { /* handle */ }

int test(void) {
    registerEventHandler(EVENT_1, eventHandle_1);
    registerEventHandler(EVENT_2, eventHandle_2);
    publishEvent(EVENT_1, eventData_1);
    publishEvent(EVENT_2, eventData_2);
    return 0;
}

This code shows registration and publishing of events, achieving decoupling between producers and consumers.

2.2 Data‑Flow (Pipeline‑Filter) Pattern

For scenarios such as sensor data acquisition, a pipeline‑filter architecture splits processing into independent components linked by a data pipe. Each filter receives a standard input, transforms it, and passes it downstream.

1. Define a uniform filter interface. 2. Implement concrete filters according to requirements and order them correctly. 3. Connect filters via a linked list to form the processing chain.
typedef struct {
    char *rawData;      // NMEA raw string
    void *parsedData;   // Parsed structure
    int   flag;        // Processing flag
} NmeaDataPacket;

void receiveNmeaData(NmeaDataPacket *p) { p->rawData = readFromSerialPort(); }
void decodeNmeaData(NmeaDataPacket *p) { if (p->rawData) p->parsedData = nmeaDecode(p->rawData); }
void filterNmeaData(NmeaDataPacket *p) { if (p->parsedData && dataError) { p->parsedData = NULL; p->flag = xx; } }
void dispatchNmeaData(NmeaDataPacket *p) { if (p->parsedData) { /* forward to other modules */ } }

typedef struct FilterNode {
    void (*filter)(NmeaDataPacket *);
    struct FilterNode *next;
} FilterNode;

void addFilter(FilterNode **head, void (*newFilter)(NmeaDataPacket *)) {
    FilterNode *node = malloc(sizeof(FilterNode));
    node->filter = newFilter;
    node->next = NULL;
    if (*head == NULL) *head = node; else {
        FilterNode *cur = *head; while (cur->next) cur = cur->next; cur->next = node;
    }
}

int track_gnss_task(void) {
    FilterNode *pipeline = NULL;
    addFilter(&pipeline, receiveNmeaData);
    addFilter(&pipeline, decodeNmeaData);
    addFilter(&pipeline, filterNmeaData);
    addFilter(&pipeline, dispatchNmeaData);
    NmeaDataPacket packet;
    while (1) {
        memset(&packet, 0, sizeof(packet));
        FilterNode *cur = pipeline;
        while (cur) { cur->filter(&packet); cur = cur->next; }
    }
    return 0;
}

The pipeline can be extended by inserting additional filters without touching existing code.

2.3 Dependency Injection

Dependency injection moves the creation of a component’s dependencies outside the component, allowing the caller to supply concrete implementations. This improves testability and reuse.

typedef struct { void (*userFunction)(); } Dependency;

typedef struct { Dependency *dependency; } Component;

void useDependency(Component *c) {
    if (c->dependency && c->dependency->userFunction)
        c->dependency->userFunction();
}

void dependencyCustomerHandle(void) { /* custom logic */ }

int test(void) {
    Dependency dep = { .userFunction = dependencyCustomerHandle };
    Component comp = { .dependency = &dep };
    useDependency(&comp);
    return 0;
}

A concrete LED driver example demonstrates how the same controller can work with different hardware implementations simply by injecting a different driver interface.

typedef struct { void (*on)(void); void (*off)(void); } led_driver_interface_t;

typedef struct { led_driver_interface_t *driver; } led_controller_t;

void led_controller_init(led_controller_t *c, led_driver_interface_t *d) { if (c && d) c->driver = d; }
void led_controller_turn_on(led_controller_t *c) { if (c && c->driver && c->driver->on) c->driver->on(); }
void led_controller_turn_off(led_controller_t *c) { if (c && c->driver && c->driver->off) c->driver->off(); }

static void gpio_led_on(void) { /* GPIO toggle */ }
static void gpio_led_off(void) { /* GPIO toggle */ }

led_driver_interface_t gpio_led_driver = { .on = gpio_led_on, .off = gpio_led_off };

int test(void) {
    led_controller_t ctrl;
    led_controller_init(&ctrl, &gpio_led_driver);
    led_controller_turn_on(&ctrl);
    led_controller_turn_off(&ctrl);
    return 0;
}

3. Non‑Pattern Practices

When resources are tight, simpler techniques can also achieve decoupling:

Conditional compilation using #if defined CUSTOM_FEATURE to isolate optional code.

Hook insertion – expose empty function pointers that users can set at runtime.

Callback functions – notify other modules when specific events occur.

Scripted configuration – drive module behavior from external config files.

Object‑oriented style in C – emulate classes with structs and function pointers to improve modularity.

Version‑control best practices (Git) and code‑maintenance guidelines are recommended to keep the architecture sustainable.

4. Conclusion

For IoT and other embedded products, fragmented requirements demand a source base that can serve many scenarios. By clearly separating input, processing, and output, and by applying layered design, event‑driven or pipeline‑filter patterns, and dependency injection, developers can build flexible, maintainable firmware that scales across hardware variations.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Design PatternsSoftware Architecturedependency-injectionC programmingembedded systemsLayered Design
Liangxu Linux
Written by

Liangxu Linux

Liangxu, a self‑taught IT professional now working as a Linux development engineer at a Fortune 500 multinational, shares extensive Linux knowledge—fundamentals, applications, tools, plus Git, databases, Raspberry Pi, etc. (Reply “Linux” to receive essential resources.)

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.