C Refresher

Multi-File Projects

Real projects don't fit in one file. The C preprocessor (#include, #define, #ifndef) is how you split code across multiple files and control what gets compiled.


How #include Works

#include literally copies and pastes the contents of another file at that point before compilation. Think of it as "import this file's declarations so I can use them."

c
1#include <stdio.h> // angle brackets → system/library headers
2#include "my_sensor.h" // quotes → your own project headers
SyntaxSearchesUsed For
#include <file.h>System include paths onlyStandard library, ESP-IDF headers
#include "file.h"Current directory first, then system pathsYour project's own headers

Splitting Code Across Files

Include Guards Explained

When a header is included from multiple .c files, it could be processed multiple times. This causes redefinition errors (e.g., the compiler sees sensor_data_t defined twice). Include guards prevent this:

c
1#ifndef SENSOR_H // "if SENSOR_H is NOT defined, process this file"
2#define SENSOR_H // "now SENSOR_H IS defined"
3
4// ... all your declarations ...
5
6#endif // SENSOR_H // end of guard
  • First include: SENSOR_H is not defined → process the file → define SENSOR_H.
  • Second include: SENSOR_H is already defined → skip everything between #ifndef and #endif.

#pragma once

Some compilers support #pragma once as a simpler alternative — it does the same thing in one line. However, #ifndef guards are the standard in ESP-IDF and work on all compilers.


Desktop Example: Manual GCC Compilation

If you are writing a multi-file project to run on your Mac, Windows, or Linux desktop, you must tell the compiler (gcc) about all the .c files. You do not compile the .h files.

bash
# Compile both main.c and sensor.c into a single executable named "app"
gcc main.c sensor.c -o app

# Run the resulting executable
./app

If you forget to include sensor.c in the command, the compiler will read main.c, see you calling sensor_init(), and throw a Linker Error: undefined reference to 'sensor_init'.


Microcontroller Example: The ESP-IDF Component Structure

While a standard Desktop C project might just dump all .c and .h files into one folder and compile them with a single gcc command, ESP-IDF projects follow a specific, highly modular structure enforced by the CMake build system:

text
my_esp_project/
├── main/
│   ├── CMakeLists.txt    ← tells the build system about this component
│   ├── main.c            ← app_main() entry point
│   ├── sensor.h          ← your sensor module header
│   ├── sensor.c          ← your sensor module implementation
├── components/           ← optional: reusable libraries
│   └── my_driver/
│       ├── CMakeLists.txt
│       ├── include/my_driver.h
│       └── my_driver.c
├── sdkconfig             ← generated by menuconfig (all #define configs)
└── CMakeLists.txt        ← top-level build file

1. The Header File (main/sensor.h)

Declares what functions and structs are available to the rest of the project.

c
1#ifndef SENSOR_H
2#define SENSOR_H
3
4#include "driver/i2c.h" // ESP-IDF driver header
5
6typedef struct {
7 float temperature;
8 float humidity;
9} sensor_data_t;
10
11esp_err_t sensor_init(i2c_port_t port);
12esp_err_t sensor_read(sensor_data_t *out_data); // output via pointer
13
14#endif // SENSOR_H

2. The Implementation File (main/sensor.c)

Contains the actual logic.

c
1#include "sensor.h"
2#include "esp_log.h"
3
4static const char *TAG = "SENSOR"; // ESP-IDF convention: static const for TAG
5static i2c_port_t s_i2c_port = 0; // "s_" prefix = static (file-private)
6
7esp_err_t sensor_init(i2c_port_t port) {
8 s_i2c_port = port;
9 ESP_LOGI(TAG, "Sensor initialized on I2C port %d", port);
10 return ESP_OK;
11}
12
13esp_err_t sensor_read(sensor_data_t *out_data) {
14 if (out_data == NULL) return ESP_ERR_INVALID_ARG;
15
16 // Read from I2C hardware...
17 out_data->temperature = 25.5f;
18 out_data->humidity = 60.0f;
19
20 return ESP_OK;
21}

3. The Main File (main/main.c)

Includes the header to use the module.

c
1#include "sensor.h"
2#include "esp_log.h"
3
4static const char *TAG = "MAIN";
5
6void app_main(void) {
7 ESP_ERROR_CHECK(sensor_init(I2C_NUM_0));
8
9 while (1) {
10 sensor_data_t reading = {0};
11 esp_err_t ret = sensor_read(&reading);
12
13 if (ret == ESP_OK) {
14 ESP_LOGI(TAG, "Temp: %.1f°C, Hum: %.1f%%",
15 reading.temperature, reading.humidity);
16 }
17 vTaskDelay(pdMS_TO_TICKS(1000));
18 }
19}
Previous
Memory & Keywords