Skip to content
Home » Embedded Systems » Embedded C » Separate Header Files in C

Separate Header Files in C

Separate Header Files in C featured image with dark blue background, C FOUNDATIONS badge, .h .c icon in teal circle, and Code Organization and Best Practices subtitle by nerdyelectronics.com
Embedded Systems Learning Path
Part 27 of 129View Full Path →

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
In embedded systems projects, proper use of header files is critical. A typical firmware project may have dozens of source files for drivers, middleware, and application code. Without header files, managing dependencies becomes impossible.

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_H
uart.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 guard
How it works:
  1. First time uart.h is included: UART_H is not defined, so the preprocessor enters the block, defines UART_H, and processes the content.
  2. If uart.h is included again (directly or indirectly): UART_H is already defined, so the preprocessor skips the entire block.
Naming convention: Use the filename in uppercase with underscores: 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 constantsStatic (private) helper functions
Extern variable declarationsVariable definitions
Inline function definitionsInternal logic
Golden rule: A header file should contain declarations (what exists), not definitions (how it works). If you put a function definition in a header and include it in two source files, you get a “multiple definition” linker error.

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.h
sensor.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_H
sensor.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

If a.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 static for private functions and variables that should not be visible outside the module
  • Keep headers minimal: only expose what other modules need
This pattern keeps your code modular, testable, and maintainable, even as your project grows to hundreds of files.

Leave a Reply

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