Skip to content
Home » Software Design » Module Design in Embedded C — Structuring .h and .c Files

Module Design in Embedded C — Structuring .h and .c Files

Module Design featured image with purple background, Architecture badge, Md icon, Structuring .h and .c Files subtitle in the Software Design in C series by NerdyElectronics
Software Design Principles in C
Part 27 of 31View Full Path →

KEY TAKEAWAYS

  • The .h file is your public API contract; the .c file is your private implementation
  • Default to static — only expose what external code genuinely needs
  • Prefix all public symbols to avoid name collisions
  • Headers must be self-contained and include-guarded
  • Use opaque pointers for multi-instance modules
  • One module = one purpose = one .h/.c pair

Why Module Design Matters

In embedded C, there are no classes, namespaces, or packages. Your primary tool for organizing code is the .h / .c file pair. A well-designed module has a clean public interface in the header file and a hidden implementation in the source file. A poorly designed module leaks its internals, creates tangled dependencies, and makes changes risky.Good module design is the foundation of every other design principle — separation of concerns, encapsulation, low coupling, and high cohesion all start here.

The Anatomy of a Good Module

Header File (.h) — The Public Contract

// temperature.h — Public API only
#ifndef TEMPERATURE_H
#define TEMPERATURE_H

#include <stdbool.h>

typedef enum {
    TEMP_OK,
    TEMP_ERR_NOT_READY,
    TEMP_ERR_OUT_OF_RANGE,
} TempStatus;

// Initialize the temperature subsystem
TempStatus temp_init(int adc_channel);

// Read the current temperature in Celsius
TempStatus temp_read(float *out_celsius);

// Check if temperature exceeds the safety threshold
bool temp_is_critical(void);

// Set the critical threshold (default: 85.0 C)
void temp_set_threshold(float celsius);

#endif // TEMPERATURE_H

Source File (.c) — The Private Implementation

// temperature.c — Implementation details hidden here
#include "temperature.h"
#include "adc.h"       // Only this file knows about ADC details

// Private state — not visible outside this file
static int    s_channel = -1;
static float  s_threshold = 85.0f;
static bool   s_initialized = false;

// Private helper — static means file-scope only
static float raw_to_celsius(int raw_adc) {
    float voltage = raw_adc * 3.3f / 4096.0f;
    return (voltage - 0.5f) * 100.0f;
}

TempStatus temp_init(int adc_channel) {
    if (adc_channel  15) {
        return TEMP_ERR_OUT_OF_RANGE;
    }
    adc_configure(adc_channel);
    s_channel = adc_channel;
    s_initialized = true;
    return TEMP_OK;
}

TempStatus temp_read(float *out_celsius) {
    if (!s_initialized) return TEMP_ERR_NOT_READY;

    int raw = adc_read(s_channel);
    if (raw  4095) return TEMP_ERR_OUT_OF_RANGE;

    *out_celsius = raw_to_celsius(raw);
    return TEMP_OK;
}

bool temp_is_critical(void) {
    float t;
    if (temp_read(&t) != TEMP_OK) return false;
    return t > s_threshold;
}

void temp_set_threshold(float celsius) {
    s_threshold = celsius;
}

Module Design Rules

Rule 1: One Module, One Purpose

Each .h/.c pair should represent one logical concept. A temperature module handles temperature. A UART module handles UART. Do not create a utils.c that becomes a dumping ground for unrelated functions.

Rule 2: Prefix All Public Symbols

// BAD — pollutes global namespace
void init(void);
int read(void);

// GOOD — clear ownership
void temp_init(void);
int  temp_read(void);
C has no namespaces. Prefixing prevents name collisions when your project grows or you integrate third-party code.

Rule 3: Hide Everything That Can Be Hidden

// In .c file — static = private to this file
static int   internal_buffer[256];   // Not visible outside
static void  helper_function(void);  // Not visible outside
static const int MAX_RETRIES = 3;    // Not visible outside
Default to static. Only expose a function or variable in the header if external code genuinely needs it. When in doubt, keep it private.

Rule 4: Use Include Guards or #pragma once

// Traditional include guard
#ifndef MODULE_NAME_H
#define MODULE_NAME_H
// ... declarations ...
#endif

// Or the simpler (but non-standard) alternative
#pragma once

Rule 5: Headers Should Be Self-Contained

// BAD — assumes caller already included stdint.h
// sensor.h
void sensor_write(uint8_t *data, uint16_t len);

// GOOD — includes its own dependencies
// sensor.h
#include <stdint.h>
void sensor_write(uint8_t *data, uint16_t len);
A header should compile on its own. Include everything the declarations need — do not rely on the including file to have the right headers already.

Rule 6: Never Include .c Files

// NEVER do this
#include "temperature.c"  // Causes duplicate symbols, breaks linking

Example: Multi-Instance Module

When you need multiple instances of a module (e.g., two UART ports), use an opaque struct pattern:
// uart.h — opaque handle
typedef struct Uart Uart;

Uart *uart_create(int port, int baud);
void  uart_destroy(Uart *u);
int   uart_send(Uart *u, const uint8_t *data, int len);
int   uart_recv(Uart *u, uint8_t *buf, int max_len);

// uart.c — private struct
#include "uart.h"
#include <stdlib.h>

struct Uart {
    int port;
    int baud;
    volatile uint8_t *base_addr;
    uint8_t rx_buf[256];
    int rx_head, rx_tail;
};

Uart *uart_create(int port, int baud) {
    Uart *u = malloc(sizeof(Uart));
    u->port = port;
    u->baud = baud;
    u->base_addr = get_uart_base(port);
    u->rx_head = u->rx_tail = 0;
    configure_uart_hw(u->base_addr, baud);
    return u;
}

// Usage:
// Uart *debug  = uart_create(0, 115200);
// Uart *sensor = uart_create(1, 9600);
// uart_send(debug, msg, len);
The caller cannot access any fields of Uart directly. The struct definition is completely hidden in the .c file. This is the same pattern used by FILE * in the C standard library.

Common Module Design Mistakes

MistakeFix
Putting function bodies in .h filesOnly declarations in headers; definitions in .c files
Exposing internal structs in headersUse opaque pointers (forward declarations)
Global variables without staticMake all file-scope variables static
Creating god modules (utils.c, common.c)Split by purpose: one module, one responsibility
Circular #includes (a.h includes b.h includes a.h)Use forward declarations to break cycles
Huge headers with implementation detailsKeep headers minimal — move helpers to .c

Key Takeaways

  • The .h file is your public API contract; the .c file is your private implementation
  • Default to static — only expose what external code genuinely needs
  • Prefix all public symbols to avoid name collisions
  • Headers must be self-contained and include-guarded
  • Use opaque pointers for multi-instance modules
  • One module = one purpose = one .h/.c pair

Leave a Reply

Your email address will not be published. Required fields are marked *