Chapter 285: Implementing Custom Components
Chapter Objectives
By the end of this chapter, you will be able to:
- Understand the purpose and structure of an ESP-IDF component.
- Create a new, self-contained component from scratch.
- Write a
CMakeLists.txt
file to register a component with the build system. - Define a public API for a component using header files.
- Add custom configuration options using a
Kconfig
file. - Use a custom component within a main application.
- Manage dependencies between different components.
Introduction
As your projects grow in complexity, a single, monolithic main.c
file becomes unmanageable. Good software engineering practice, in any domain, relies on modularity: breaking a large problem down into smaller, self-contained, and reusable pieces. In the ESP-IDF ecosystem, the mechanism for achieving this is the component.
Think of a component as a library or module. It could be a driver for a specific sensor (like a BME280), a helper library for a communication protocol (like Modbus), or a collection of utility functions you use across many projects. By encapsulating logic within a component, you create a “black box” with a well-defined public interface. This simplifies your main application logic, promotes code reuse, and makes collaboration and testing far easier. This chapter will teach you how to build these fundamental blocks of an ESP-IDF application.
Theory
1. The Anatomy of a Component
At its core, a component is simply a directory containing source code and a build script. The ESP-IDF build system (based on CMake) automatically discovers and integrates these components into your final application.
A typical component has the following structure:
my_project/
└── components/
└── my_component/
├── CMakeLists.txt // (Required) The build script for this component.
├── Kconfig // (Optional) Defines configuration options for menuconfig.
├── include/
│ └── my_component.h // (Recommended) Public header file defining the API.
└── my_component.c // Source file(s) containing the implementation.
CMakeLists.txt
: This is the only mandatory file. It tells the build system what source files are part of the component and what other components it depends on.include/
directory: This directory is special. Any header file placed here is considered public. The build system automatically adds this directory to the global include path for your entire project, meaningmain.c
or any other component can include it directly (e.g.,#include "my_component.h"
).- Source Files: Your
.c
or.cpp
files contain the component’s internal logic and implementation of the public API. Kconfig
: This file allows you to add custom options for your component that will appear in themenuconfig
interface, making your component configurable by the user.
graph TD subgraph "Your Project" A[main_app.c] --> B(Custom: Sensor Driver); A --> C(Custom: Utils Library); B --> D(ESP-IDF: I2C Driver); B --> E(ESP-IDF: esp_log); C --> E; end %% Styling style A fill:#EDE9FE,stroke:#5B21B6,stroke-width:2px,color:#5B21B6 style B fill:#DBEAFE,stroke:#2563EB,stroke-width:1px,color:#1E40AF style C fill:#DBEAFE,stroke:#2563EB,stroke-width:1px,color:#1E40AF style D fill:#D1FAE5,stroke:#059669,stroke-width:1px,color:#065F46 style E fill:#D1FAE5,stroke:#059669,stroke-width:1px,color:#065F46 linkStyle 0 stroke:#5B21B6,stroke-width:1.5px; linkStyle 1 stroke:#5B21B6,stroke-width:1.5px; linkStyle 2 stroke:#2563EB,stroke-width:1.5px; linkStyle 3 stroke:#2563EB,stroke-width:1.5px; linkStyle 4 stroke:#2563EB,stroke-width:1.5px;
2. The Component Build Script: CMakeLists.txt
The heart of the component is its CMakeLists.txt
file. The primary function used here is idf_component_register
.
# CMakeLists.txt for my_component
idf_component_register(SRCS "my_component.c"
INCLUDE_DIRS "."
REQUIRES "esp_log")
Let’s break down the parameters:
Parameter | Purpose | Example |
---|---|---|
SRCS | Lists all source files (.c, .cpp, .S) to be compiled as part of this component. | SRCS “driver.c” “helpers.c” |
INCLUDE_DIRS | Lists directories containing private header files, used only by this component’s sources. | INCLUDE_DIRS “private_include” |
REQUIRES | Lists other components this component depends on. This is a public dependency. | REQUIRES “esp_log” “driver” |
PRIV_REQUIRES | Lists other components this component depends on, but this dependency is private (not exposed to other components). | PRIV_REQUIRES “json_parser” |
SRCS
: A list of all source files (.c
,.cpp
,.S
) that belong to this component.INCLUDE_DIRS
: A list of directories to be added to the component’s private include path. This is for headers used only internally by the component’s source files. The publicinclude
directory is handled automatically.REQUIRES
: A list of other components that this component depends on. This is a public dependency. It tells the build system two things:- This component needs to be linked against the
esp_log
component. - The public include directories of
esp_log
will be available to any other component that usesmy_component
.
- This component needs to be linked against the
PRIV_REQUIRES
: This defines a private dependency. The component will be linked against the required component, but the dependency is not propagated to components that depend on this one. It’s an internal implementation detail.
3. Making a Component Configurable with Kconfig
To make your component flexible, you can add options to menuconfig
. This is done by creating a Kconfig
file in the component’s root directory.
A simple Kconfig
file might look like this:
# Kconfig for my_component
menu "My Component Settings"
config MY_COMPONENT_ENABLE_FEATURE_X
bool "Enable Feature X"
default y
help
Enable this to add support for the amazing Feature X.
config MY_COMPONENT_RETRY_COUNT
int "Number of Retries"
range 0 10
default 3
help
Specifies how many times to retry an operation before failing.
endmenu
Keyword | Purpose | Example |
---|---|---|
menu / endmenu | Creates a submenu in the `menuconfig` interface to group related options. | menu “My Component Settings” |
config | Defines a new configuration option. The name is automatically prefixed with CONFIG_. | config MY_COMPONENT_TIMEOUT |
bool / int / string | Specifies the data type of the configuration option. | bool “Enable Feature” |
default | Sets the default value for the option if the user doesn’t change it. | default 1000 |
range | Constrains the allowable values for an int option. | range 0 100 |
help | Provides descriptive help text that appears in `menuconfig`. | help Sets the timeout in milliseconds. |
To use this in your C code, you simply reference the generated macro:
#if CONFIG_MY_COMPONENT_ENABLE_FEATURE_X
// Code for Feature X is compiled
#endif
void some_function() {
for (int i = 0; i < CONFIG_MY_COMPONENT_RETRY_COUNT; i++) {
// ...
}
}
Practical Example: Creating a Reusable Utilities Component
Let’s build a simple component named math_utils
that provides a function to calculate the average of an integer array.
1. Set Up the Project Structure
First, create a standard project. Then, create a components
directory, and inside it, the directory for our new component.
my_project/
├── main/
│ ├── main.c
│ └── CMakeLists.txt
├── components/
│ └── math_utils/
│ ├── include/
│ │ └── math_utils.h
│ ├── math_utils.c
│ └── CMakeLists.txt
└── CMakeLists.txt
2. Tell the Project Where to Find Components
Edit the top-level CMakeLists.txt
file in your project root (my_project/CMakeLists.txt
) and add the following line near the top, after cmake_minimum_required
:
# my_project/CMakeLists.txt
...
set(EXTRA_COMPONENT_DIRS components)
This tells the build system to look for components in the components
directory.
3. Write the Component Code
components/math_utils/include/math_utils.h
(Public API):
#ifndef MATH_UTILS_H
#define MATH_UTILS_H
#include <stdint.h>
/**
* @brief Calculates the average of an array of integers.
*
* @param data Pointer to the array of integers.
* @param len The number of elements in the array.
* @return The calculated average as a float. Returns 0 if len is 0.
*/
float average_int_array(const int32_t *data, uint32_t len);
#endif // MATH_UTILS_H
components/math_utils/math_utils.c
(Implementation):
#include "math_utils.h"
#include <stddef.h>
float average_int_array(const int32_t *data, uint32_t len) {
if (data == NULL || len == 0) {
return 0.0f;
}
int64_t sum = 0;
for (uint32_t i = 0; i < len; i++) {
sum += data[i];
}
return (float)sum / len;
}
4. Create the Component’s Build Script
components/math_utils/CMakeLists.txt
:
# Register the component with the build system.
idf_component_register(SRCS "math_utils.c"
INCLUDE_DIRS "."
REQUIRES "")
We list the source file. We don’t have any dependencies for this simple component, so REQUIRES
is empty.
5. Use the Component in main
Now, let’s use our new component in the main application.
main/main.c
:
#include <stdio.h>
#include "math_utils.h" // Include the public header of our component
#include "esp_log.h"
static const char *TAG = "MAIN_APP";
void app_main(void) {
int32_t my_data[] = {10, 20, 30, 40, 55};
uint32_t data_len = sizeof(my_data) / sizeof(my_data[0]);
float avg = average_int_array(my_data, data_len);
ESP_LOGI(TAG, "The average of the array is: %.2f", avg);
}
Notice how we can directly include math_utils.h
.
6. Build and Run
- Save all files.
- Run
idf.py build flash monitor
.
Observe
The output on the serial monitor should be:
I (XXX) MAIN_APP: The average of the array is: 31.00
You have successfully created, integrated, and used a custom component!
Variant Notes
The component mechanism itself is a feature of the ESP-IDF build system and is identical across all ESP32 variants. The code inside your component, however, may need to be variant-aware.
For example, if you were writing a peripheral driver component, you might need to handle differences in pin numbers or register addresses. The standard way to do this is with C preprocessor directives that check the target configuration.
// Inside a component's source file
#include "soc/soc_caps.h" // Provides hardware capability macros
#if CONFIG_IDF_TARGET_ESP32S3
// Code specific to ESP32-S3
#define MY_SENSOR_I2C_PORT I2C_NUM_0
#elif CONFIG_IDF_TARGET_ESP32C3
// Code specific to ESP32-C3
#define MY_SENSOR_I2C_PORT I2C_NUM_0
#elif CONFIG_IDF_TARGET_ESP32
// Code specific to the original ESP32
#define MY_SENSOR_I2C_PORT I2C_NUM_1
#else
#error "This component does not support the selected target"
#endif
Common Mistakes & Troubleshooting Tips
Mistake / Issue | Symptom(s) | Troubleshooting / Solution |
---|---|---|
“fatal error: my_component.h: No such file or directory” | The build fails when trying to compile `main.c`. | The public header is in the wrong place OR the component directory isn’t registered. Solution: Ensure the header is in components/my_component/include/ and that the main CMakeLists.txt has set(EXTRA_COMPONENT_DIRS components). |
“undefined reference to `my_function`” | The build compiles but fails at the final linking stage. | The component that uses your function is missing the dependency. Solution: In the `CMakeLists.txt` of the component calling the function (e.g., `main`), add your component to the REQUIRES list. |
“Circular dependency detected” | The build fails with an explicit error about a circular dependency. | Component A requires B, and component B requires A. This is a design flaw. Solution: Refactor your code. The component with lower-level logic (e.g., `led_driver`) should not depend on a higher-level one (`status_indicator`). |
Kconfig options don’t appear in `menuconfig`. | You run `menuconfig` but your new component settings menu is not there. | The build system did not find your `Kconfig` file. Solution: Ensure the file is named exactly Kconfig (case-sensitive) and is in the root of your component directory, e.g., components/my_component/Kconfig. |
Exercises
- LED Driver Component: Create a component named
led_driver
. It should have a public API (led_driver.h
) with three functions:led_driver_init(gpio_num_t pin)
,led_driver_on()
, andled_driver_off()
. Theinit
function should configure the GPIO pin, and the other two should control the LED state. Use this component inapp_main
to blink an LED.I - Configurable Blink Component: Extend the
led_driver
component from Exercise 1. Add aKconfig
file with an integer option calledCONFIG_LED_BLINK_INTERVAL_MS
with a default value of 500. Add a new function to your component,led_driver_blink()
, which blinks the LED in a loop using the interval frommenuconfig
. - Dependent Components: Create a new component called
status_indicator
. This component should depend on yourled_driver
component (useREQUIRES led_driver
in itsCMakeLists.txt
). It should have a functionstatus_indicator_set_error(bool is_error)
. Ifis_error
is true, it should turn the LED on solid; otherwise, it should turn it off. Yourmain
application should only interact with thestatus_indicator
component, not theled_driver
component directly.
graph TD A[main.c] -- "Public API Call<br><i>#include \status_indicator.h\</i>" --> B(status_indicator); B -- "Public API Call<br><i>#include \led_driver.h\</i>" --> C(led_driver); C -- "IDF API Call" --> D[ESP-IDF<br>GPIO Driver]; subgraph "Your Application" A end subgraph "Custom Components" B C end subgraph "ESP-IDF" D end %% Styling style A fill:#EDE9FE,stroke:#5B21B6,stroke-width:2px style B fill:#DBEAFE,stroke:#2563EB,stroke-width:1.5px style C fill:#DBEAFE,stroke:#2563EB,stroke-width:1.5px style D fill:#D1FAE5,stroke:#059669,stroke-width:1.5px
Summary
- Components are the key to modularity and code reuse in ESP-IDF.
- A component’s structure typically includes source files, a public
include
directory, aCMakeLists.txt
file, and an optionalKconfig
file. - The project’s root
CMakeLists.txt
must point to your custom components directory viaset(EXTRA_COMPONENT_DIRS ...)
. idf_component_register
is the core function for defining a component’s sources and dependencies in itsCMakeLists.txt
.- Public APIs are defined in headers placed in the component’s
include/
directory. Kconfig
files make your components flexible and configurable through themenuconfig
system.- The component architecture is a powerful tool for building clean, maintainable, and scalable embedded applications.
Further Reading
- ESP-IDF Documentation: Build System:
- ESP-IDF Documentation: Component
CMakeLists.txt
Files: