Skip to main content

Struct-Based Function Encapsulation in Embedded C

·526 words·3 mins
Embedded C Embedded Systems Firmware Design HAL
Table of Contents

🧩 Why Use Struct-Based Function Encapsulation?
#

In embedded systems, struct-based function encapsulation uses function pointers inside structures to bind behavior with data.
This pattern emulates Object-Oriented Programming (OOP) concepts in plain C while remaining lightweight, deterministic, and portable.

Instead of scattering related functions across files, a struct becomes a self-contained driver object that exposes a clean and consistent interface to the application layer.


🧠 Core Concept: Behavior + State
#

Traditional C structs package only data. Embedded C extends this idea by embedding function pointers alongside configuration or runtime state.

This creates a logical unit that:

  • Owns its internal state
  • Exposes a defined API
  • Hides hardware-specific implementation details

The caller interacts with what the object does, not how it does it.


🎯 Key Design Advantages
#

  • Modularization
    Related data and functions live together, improving reuse and readability.

  • Abstraction
    Hardware details are hidden behind function pointers, enabling clean APIs.

  • Polymorphism
    Function pointers can be swapped at runtime, allowing different implementations under the same interface.

  • Scalability
    New features can be added without breaking existing code paths.


🏗️ Common Embedded Use Cases
#

Struct-based function encapsulation is widely used in:

  • Hardware Abstraction Layers (HAL)
    Unified interfaces for SPI, I2C, UART, GPIO, and timers.

  • Callback Systems
    Passing behavior into drivers, ISRs, or middleware.

  • State Machines
    Each state contains a pointer to its handler function.

  • Task Management
    Lightweight task objects with their own execution logic.


🔌 Example: SPI Hardware Interface
#

Encapsulating SPI operations allows the application to remain completely unaware of register-level details.

typedef struct {
    void (*init)(void);
    void (*write)(uint8_t data);
    uint8_t (*read)(void);
} spi_t;

/* Implementation functions */
void spi_init(void) { /* Register initialization */ }
void spi_write(uint8_t data) { /* TX logic */ }
uint8_t spi_read(void) { return 0; }

int main(void) {
    spi_t my_spi = {
        .init = spi_init,
        .write = spi_write,
        .read = spi_read
    };

    my_spi.init();
    my_spi.write(0xAA);
    uint8_t data = my_spi.read();

    return 0;
}

This pattern enables easy replacement of the SPI backend without touching application code.


💡 Example: PWM Control for LEDs
#

Using structs allows multiple PWM-controlled devices to share the same logic with different configurations.

typedef struct {
    uint8_t duty_cycle;
    void (*set_duty)(uint8_t duty);
    void (*start)(void);
    void (*stop)(void);
} pwm_control_t;

void set_led_duty(uint8_t duty) { /* Timer compare update */ }
void start_led_pwm(void) { /* Enable PWM timer */ }

int main(void) {
    pwm_control_t led_red = {
        .duty_cycle = 50,
        .set_duty = set_led_duty,
        .start = start_led_pwm
    };

    led_red.set_duty(led_red.duty_cycle);
    led_red.start();
}

Each instance acts as an independent “object” with shared behavior.


🧰 Maintenance and Long-Term Benefits
#

  • Information Hiding The application depends only on the interface, not the implementation.

  • Safe Extension New functionality can be added by extending the struct definition.

  • Reduced Global State State is localized, minimizing unintended side effects.

  • Improved Testability Mock implementations can replace real hardware functions during testing.


🧪 When This Pattern Shines
#

Struct-based function encapsulation is ideal when:

  • Hardware varies across platforms
  • Drivers must be reused across projects
  • Runtime flexibility is required
  • Full C++ is unavailable or undesirable

It strikes a practical balance between bare-metal efficiency and architectural clarity.


Bottom line: This pattern gives you OOP-style structure, polymorphism, and abstraction—without sacrificing the predictability and control that embedded C demands.

Related

C Structs Explained: From Basics to Embedded Bit-Fields
·572 words·3 mins
C Programming Embedded Systems Data Structures Low-Level Programming
From Code to Binary: Understanding the C Compilation Pipeline
·510 words·3 mins
C Language Compilation Toolchain Embedded Systems
Embedded C Memory Allocation: Heap vs Variable-Length Arrays
·550 words·3 mins
Embedded Systems Embedded C Memory Management RTOS MISRA C