Table of Contents
KEY TAKEAWAYS
- Header files (
.h) contain declarations (prototypes, types, macros) shared across source files - Source files (
.c) contain function implementations and private data - Include guards (
#ifndef/#define/#endif) prevent multiple inclusion errors - Proper header file organization is the foundation of modular, maintainable C projects
Why Separate Header Files?
As your C program grows beyond a single file, you need a way to share function declarations, type definitions, and constants across multiple source files. This is exactly what header files (.h files) are for.Header files let you:- Declare functions and variables that are defined in other source files
- Share type definitions (structs, enums, typedefs) across your project
- Define constants and macros in a single place
- Create a clear interface for each module of your program
The Basic Pattern
Every module in your project typically consists of two files:- module.h – The header file containing declarations (the interface)
- module.c – The source file containing definitions (the implementation)
Example: A UART Driver Module
uart.h – The interface:#ifndef UART_H
#define UART_H
#include <stdint.h>
// Constants
#define UART_BAUD_9600 9600
#define UART_BAUD_115200 115200
// Type definitions
typedef enum {
UART_OK,
UART_ERROR,
UART_BUSY,
UART_TIMEOUT
} UartStatus;
// Function declarations (prototypes)
UartStatus uart_init(uint32_t baud_rate);
UartStatus uart_send_byte(uint8_t byte);
UartStatus uart_send_string(const char *str);
uint8_t uart_receive_byte(void);
int uart_data_available(void);
#endif // UART_Huart.c – The implementation:#include "uart.h"
// Private (static) variables - not visible outside this file
static volatile uint8_t rx_buffer[64];
static volatile uint8_t rx_head = 0;
static volatile uint8_t rx_tail = 0;
UartStatus uart_init(uint32_t baud_rate) {
// Configure UART hardware registers
// Set baud rate, 8N1 format, enable TX and RX
UART_BRR = SYSTEM_CLOCK / baud_rate;
UART_CR1 |= UART_CR1_TE | UART_CR1_RE | UART_CR1_UE;
return UART_OK;
}
UartStatus uart_send_byte(uint8_t byte) {
while (!(UART_SR & UART_SR_TXE)); // Wait for TX empty
UART_DR = byte;
return UART_OK;
}
UartStatus uart_send_string(const char *str) {
while (*str) {
UartStatus status = uart_send_byte(*str++);
if (status != UART_OK) return status;
}
return UART_OK;
}
uint8_t uart_receive_byte(void) {
while (!uart_data_available());
uint8_t data = rx_buffer[rx_tail];
rx_tail = (rx_tail + 1) % 64;
return data;
}
int uart_data_available(void) {
return rx_head != rx_tail;
}main.c – Using the module:#include "uart.h"
int main(void) {
uart_init(UART_BAUD_115200);
uart_send_string("Hello from embedded system!rn");
while (1) {
if (uart_data_available()) {
uint8_t data = uart_receive_byte();
uart_send_byte(data); // Echo back
}
}
return 0;
}Include Guards
The#ifndef / #define / #endif pattern at the top and bottom of every header file is called an include guard. It prevents the same header from being included multiple times, which would cause compilation errors due to duplicate definitions.#ifndef UART_H // If UART_H is NOT defined... #define UART_H // Define it now // ... header content ... #endif // UART_H // End of the guardHow it works:
- First time
uart.his included:UART_His not defined, so the preprocessor enters the block, definesUART_H, and processes the content. - If
uart.his included again (directly or indirectly):UART_His already defined, so the preprocessor skips the entire block.
my_driver.h becomes MY_DRIVER_H.An alternative is #pragma once, which is supported by most modern compilers:#pragma once // ... header content ...
What Goes in a Header File vs Source File
| Put in Header (.h) | Put in Source (.c) |
|---|---|
| Function prototypes (declarations) | Function definitions (implementations) |
| Type definitions (struct, enum, typedef) | Static (private) variables |
| #define macros and constants | Static (private) helper functions |
| Extern variable declarations | Variable definitions |
| Inline function definitions | Internal logic |
A Multi-Module Embedded Project
Here is a realistic project structure for a temperature monitoring system:project/ |-- main.c |-- uart.h / uart.c |-- spi.h / spi.c |-- sensor.h / sensor.c |-- display.h / display.c |-- config.hsensor.h:
#ifndef SENSOR_H
#define SENSOR_H
#include <stdint.h>
typedef struct {
float temperature;
float humidity;
uint8_t valid;
} SensorData;
void sensor_init(void);
SensorData sensor_read(void);
#endif // SENSOR_Hsensor.c:#include "sensor.h"
#include "spi.h" // sensor uses SPI to communicate
// Private function - only used within this file
static uint16_t read_raw_temperature(void) {
uint8_t cmd = 0xAA;
spi_transmit(&cmd, 1);
uint8_t response[2];
spi_receive(response, 2);
return (response[0] << 8) | response[1];
}
void sensor_init(void) {
spi_init();
// Send configuration commands to sensor
}
SensorData sensor_read(void) {
SensorData data;
uint16_t raw = read_raw_temperature();
data.temperature = raw * 0.0625f;
data.humidity = 0; // This sensor does not measure humidity
data.valid = 1;
return data;
}main.c:#include "uart.h"
#include "sensor.h"
#include "display.h"
#include <stdio.h>
int main(void) {
uart_init(115200);
sensor_init();
display_init();
char buffer[50];
while (1) {
SensorData data = sensor_read();
if (data.valid) {
sprintf(buffer, "Temp: %.1f Crn", data.temperature);
uart_send_string(buffer);
display_show_temperature(data.temperature);
}
}
return 0;
}Notice how main.c does not need to know anything about SPI or raw register reads. It only uses the clean interface defined in sensor.h.Common Mistakes
1. Defining Functions in Headers
// BAD - in my_module.h
int add(int a, int b) {
return a + b; // This is a DEFINITION, not a declaration
}
// GOOD - in my_module.h
int add(int a, int b); // Declaration only
// GOOD - in my_module.c
int add(int a, int b) {
return a + b; // Definition in the source file
}2. Missing Include Guards
Without include guards, if file A includes both file B and file C, and file C also includes file B, then file B gets included twice, causing duplicate definition errors.3. Including .c Files
// WRONG #include "uart.c" // CORRECT #include "uart.h"
4. Circular Dependencies
Ifa.h includes b.h and b.h includes a.h, you have a circular dependency. Use forward declarations to break the cycle:// In b.h, instead of #include "a.h" struct SomeType; // Forward declaration void func(struct SomeType *ptr);
Summary
Properly organizing your code into header and source file pairs is a fundamental skill in C programming, especially for embedded systems:- Each module gets a
.h(interface) and.c(implementation) file - Always use include guards (
#ifndef) or#pragma once - Headers contain declarations; source files contain definitions
- Use
staticfor private functions and variables that should not be visible outside the module - Keep headers minimal: only expose what other modules need

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.







