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."
1#include <stdio.h> // angle brackets → system/library headers2#include "my_sensor.h" // quotes → your own project headers| Syntax | Searches | Used For |
|---|---|---|
#include <file.h> | System include paths only | Standard library, ESP-IDF headers |
#include "file.h" | Current directory first, then system paths | Your 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:
1#ifndef SENSOR_H // "if SENSOR_H is NOT defined, process this file"2#define SENSOR_H // "now SENSOR_H IS defined"34// ... all your declarations ...56#endif // SENSOR_H // end of guard- First include:
SENSOR_His not defined → process the file → defineSENSOR_H. - Second include:
SENSOR_His already defined → skip everything between#ifndefand#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.
# 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:
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.
1#ifndef SENSOR_H2#define SENSOR_H34#include "driver/i2c.h" // ESP-IDF driver header56typedef struct {7 float temperature;8 float humidity;9} sensor_data_t;1011esp_err_t sensor_init(i2c_port_t port); 12esp_err_t sensor_read(sensor_data_t *out_data); // output via pointer 1314#endif // SENSOR_H2. The Implementation File (main/sensor.c)
Contains the actual logic.
1#include "sensor.h"2#include "esp_log.h"34static const char *TAG = "SENSOR"; // ESP-IDF convention: static const for TAG5static i2c_port_t s_i2c_port = 0; // "s_" prefix = static (file-private)67esp_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}1213esp_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.
1#include "sensor.h"2#include "esp_log.h"34static const char *TAG = "MAIN";56void app_main(void) {7 ESP_ERROR_CHECK(sensor_init(I2C_NUM_0));89 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}
