Table of Contents
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_HSource 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 autils.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 outsideDefault 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
| Mistake | Fix |
|---|---|
| Putting function bodies in .h files | Only declarations in headers; definitions in .c files |
| Exposing internal structs in headers | Use opaque pointers (forward declarations) |
| Global variables without static | Make 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 details | Keep 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

Vivek Bhageria — Lead Firmware R&D Engineer, 12+ years. Ex-Bosch (automotive powertrain), MusicTribe (real-time audio), medical devices. M.Tech BITS Pilani. I write at NerdyElectronics — practical, register-level embedded systems for engineers who want to understand what’s actually happening under the hood.







