Cấu trúc dự án ESP-IDF: CMakeLists, component, Kconfig, menuconfig, sdkconfig cho người từ Arduino
Giải thích cấu trúc một dự án ESP-IDF qua ví dụ: ba loại CMakeLists.txt, viết component riêng, thêm menu bằng Kconfig vào menuconfig, sdkconfig và sdkconfig.defaults, các bước idf.py build.
Chuyển từ Arduino sang ESP-IDF, người mới hay bối rối vì một dự án có cả chục file: CMakeLists.txt ở nhiều nơi, thư mục main, components, file sdkconfig hàng nghìn dòng, lệnh idf.py menuconfig. Bài này giải thích từng phần đó làm gì, qua một dự án mẫu nhỏ: component main dùng một component tự viết để nháy LED, chân LED và chu kỳ nháy chỉnh được trong menuconfig. Phần cài ESP-IDF và nạp dự án đầu tiên đã có ở bài "Cài ESP-IDF trên Windows" (link ở cuối bài).
1. Cây thư mục một dự án ESP-IDF

Một dự án ESP-IDF gồm:
CMakeLists.txtở gốc: khai báo đây là một dự án ESP-IDF và đặt tên dự án.- Các component: mỗi component là một thư mục có
CMakeLists.txtriêng, chứa code của một phần chương trình.mainlà component bắt buộc, chứa hàmapp_main(). Component tự viết đặt trongcomponents/. - Cấu hình:
sdkconfigchứa mọi tùy chọn (tần số CPU, dung lượng flash, mức log…).sdkconfig.defaultschứa vài giá trị mặc định riêng của dự án. build/: thư mục kết quả, xóa đi build lại được.
Thư viện có sẵn của ESP-IDF (Wi-Fi, GPIO, FreeRTOS, NVS…) cũng là component, nằm trong thư mục cài ESP-IDF. Dự án chỉ khai báo cần dùng component nào.
2. Ba loại CMakeLists.txt
CMakeLists.txt ở gốc dự án
Chỉ có ba dòng; tên trong project() là tên file .bin sinh ra.
# PYWORLD.VN - Dự án ESP-IDF mẫu: CMakeLists.txt ở thư mục gốc dự án
# PYWORLD.VN - Ba dòng bắt buộc: phiên bản CMake, nạp hệ thống build của ESP-IDF, đặt tên dự án
# PYWORLD.VN - Tên dự án là tên file .bin sinh ra trong thư mục build
cmake_minimum_required(VERSION 3.16)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(den_nhay_kconfig)
CMakeLists.txt của component main
Hàm idf_component_register khai báo file nguồn (SRCS), thư mục chứa file .h (INCLUDE_DIRS) và component phụ thuộc (REQUIRES). Thêm file .c mới mà quên ghi vào SRCS là lỗi "undefined reference" lúc liên kết.
# PYWORLD.VN - Component main: khai báo file nguồn và component cần dùng
# PYWORLD.VN - REQUIRES ghi rõ main dùng component den_led ở thư mục components/
# PYWORLD.VN - Thêm file .c mới thì thêm tên vào SRCS
idf_component_register(SRCS "main.c"
INCLUDE_DIRS "."
REQUIRES den_led)
CMakeLists.txt của component tự viết
Component den_led dùng trình điều khiển GPIO, nhưng chỉ trong file .c, nên khai báo bằng PRIV_REQUIRES. Nếu file .h của nó có dùng kiểu dữ liệu của component khác (ví dụ gpio_num_t), phải đổi sang REQUIRES để component dùng nó cũng thấy được kiểu đó.
# PYWORLD.VN - Component den_led: một thư viện nhỏ tự viết, dùng lại được ở dự án khác
# PYWORLD.VN - INCLUDE_DIRS "include" cho component khác thấy den_led.h
# PYWORLD.VN - PRIV_REQUIRES: chỉ den_led.c dùng trình điều khiển GPIO, file .h không lộ kiểu gpio_num_t
idf_component_register(SRCS "den_led.c"
INCLUDE_DIRS "include"
PRIV_REQUIRES esp_driver_gpio)
Từ ESP-IDF 5.3, trình điều khiển được tách nhỏ: GPIO là esp_driver_gpio, UART là esp_driver_uart… Tên cũ driver vẫn dùng được nhưng kéo theo mọi trình điều khiển.
3. Viết một component: phần khai báo và phần cài đặt
File .h trong thư mục include là phần công khai: các hàm component khác được gọi.
// PYWORLD.VN - Component den_led: phần khai báo công khai (header)
// PYWORLD.VN - main.c chỉ cần #include "den_led.h", không cần biết bên trong dùng driver GPIO
// PYWORLD.VN - extern "C" để gọi được từ file C++ (main.cpp)
#pragma once
#include "esp_err.h"
#ifdef __cplusplus
extern "C" {
#endif
// Cấu hình chân làm ngõ ra và tắt LED; trả về ESP_OK nếu thành công.
esp_err_t den_led_khoi_tao(int chan);
// Đảo trạng thái LED (sáng ↔ tắt); không làm gì nếu chưa khởi tạo.
void den_led_dao(void);
#ifdef __cplusplus
}
#endif
File .c là phần cài đặt. Biến static chỉ thấy được trong file này, nên component khác không sửa nhầm được.
// PYWORLD.VN - Component den_led: phần cài đặt
// PYWORLD.VN - Dùng gpio_config() của trình điều khiển GPIO trong ESP-IDF và ESP_LOGI để ghi log
// PYWORLD.VN - Biến static chỉ thấy được trong file này
#include "den_led.h"
#include "driver/gpio.h"
#include "esp_log.h"
static const char *TAG = "den_led";
static int s_chan = -1;
static int s_muc = 0;
esp_err_t den_led_khoi_tao(int chan)
{
gpio_config_t cau_hinh = {
.pin_bit_mask = 1ULL << chan,
.mode = GPIO_MODE_OUTPUT,
.pull_up_en = GPIO_PULLUP_DISABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_DISABLE,
};
esp_err_t loi = gpio_config(&cau_hinh);
if (loi != ESP_OK) {
ESP_LOGE(TAG, "Khong cau hinh duoc GPIO%d: %s", chan, esp_err_to_name(loi));
return loi;
}
s_chan = chan;
s_muc = 0;
gpio_set_level((gpio_num_t)s_chan, s_muc);
ESP_LOGI(TAG, "Khoi tao LED o GPIO%d", chan);
return ESP_OK;
}
void den_led_dao(void)
{
if (s_chan < 0) {
return;
}
s_muc = !s_muc;
gpio_set_level((gpio_num_t)s_chan, s_muc);
}
Muốn dùng lại component ở dự án khác, chỉ cần chép cả thư mục den_led vào components/ của dự án đó. Đây là cách ESP-IDF thay cho thư mục libraries của Arduino.
4. Kconfig: thêm menu riêng vào menuconfig
File Kconfig.projbuild đặt trong một component sẽ thêm menu vào idf.py menuconfig. Mỗi mục config TEN sinh ra macro CONFIG_TEN dùng được trong code.
# PYWORLD.VN - Thêm một menu riêng vào idf.py menuconfig
# PYWORLD.VN - Mỗi "config TEN" sinh ra macro CONFIG_TEN trong sdkconfig.h
# PYWORLD.VN - Chữ hiện trong menuconfig viết không dấu cho chắc chắn hiển thị đúng
menu "Cau hinh den LED (PYWORLD)"
config DEN_LED_GPIO
int "Chan GPIO cua LED"
range 0 33
default 2
help
GPIO noi LED. ESP32 DevKit V1 co LED xanh o GPIO2.
config DEN_LED_CHU_KY_MS
int "Chu ky dao LED (ms)"
range 50 5000
default 500
help
Thoi gian giua hai lan dao trang thai LED.
endmenu

Trong menuconfig, dùng phím mũi tên để chọn, Enter để sửa, S để lưu, Q để thoát. Giá trị lưu vào file sdkconfig. Chương trình dùng hai macro này:
// PYWORLD.VN - ESP-IDF: app_main() dùng component den_led và hai tùy chọn tự thêm vào menuconfig
// PYWORLD.VN - CONFIG_DEN_LED_GPIO, CONFIG_DEN_LED_CHU_KY_MS sinh từ main/Kconfig.projbuild vào sdkconfig.h
// PYWORLD.VN - Build và xem log: idf.py build flash monitor
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#include "sdkconfig.h"
#include "den_led.h"
static const char *TAG = "main";
void app_main(void)
{
ESP_ERROR_CHECK(den_led_khoi_tao(CONFIG_DEN_LED_GPIO));
ESP_LOGI(TAG, "Chu ky dao LED: %d ms", CONFIG_DEN_LED_CHU_KY_MS);
int dem = 0;
while (1) {
den_led_dao();
dem++;
if (dem % 10 == 0) {
ESP_LOGI(TAG, "Da dao LED %d lan", dem);
}
vTaskDelay(pdMS_TO_TICKS(CONFIG_DEN_LED_CHU_KY_MS));
}
}
Lợi ích so với #define trong code: người dùng đổi chân LED, chu kỳ nháy trong menu mà không phải mở code; range chặn giá trị sai ngay từ lúc nhập.
5. sdkconfig và sdkconfig.defaults
sdkconfig được tạo ở lần build hoặc lần mở menuconfig đầu tiên, chứa toàn bộ tùy chọn với giá trị hiện tại. File này dài và đổi theo từng phiên bản ESP-IDF, nên nhiều nhóm không đưa nó vào git mà chỉ giữ sdkconfig.defaults:
# PYWORLD.VN - Giá trị mặc định của dự án, được chép vào sdkconfig khi sdkconfig chưa có
# PYWORLD.VN - Nên đưa file này vào git; sdkconfig sinh ra thì có thể bỏ qua
# PYWORLD.VN - Mỗi dòng là một tùy chọn CONFIG_... giống trong menuconfig
CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y
CONFIG_FREERTOS_HZ=1000
CONFIG_DEN_LED_CHU_KY_MS=250
Thứ tự ưu tiên khi tạo sdkconfig mới: giá trị trong sdkconfig.defaults đè lên default trong Kconfig. Ở dự án mẫu, Kconfig ghi mặc định 500 ms nhưng sdkconfig.defaults ghi 250 ms, nên menuconfig hiện 250. Lưu ý: khi sdkconfig đã có, sửa sdkconfig.defaults không có tác dụng; phải xóa sdkconfig (hoặc chạy idf.py reconfigure sau khi xóa) để tạo lại.
6. idf.py build chạy qua những bước nào

idf.py build (hình minh họa).- CMake đọc
CMakeLists.txtở gốc, tìm mọi component (của dự án và của ESP-IDF) cần dùng. - Gom file Kconfig của các component đó, kết hợp với
sdkconfigđể sinhbuild/config/sdkconfig.h. - Biên dịch từng component thành thư viện, liên kết thành file
.elf, rồi tạo file.bincùng bootloader và bảng phân vùng.
Chạy cả ba việc build, nạp, xem log bằng một lệnh: idf.py -p COM5 build flash monitor (đổi COM5 theo cổng của mạch). Thoát monitor bằng Ctrl+].

idf.py monitor (hình minh họa).Mỗi dòng log có mức (I là thông tin, W là cảnh báo, E là lỗi), thời gian tính từ lúc khởi động theo mili giây, tên TAG và nội dung. Mức log hiển thị chỉnh trong menuconfig: Component config → Log → Default log verbosity.
7. Từ Arduino sang ESP-IDF: đối chiếu nhanh

Lõi Arduino cho ESP32 thật ra được xây trên ESP-IDF, nên mọi hàm ESP-IDF đều gọi được từ sketch Arduino. Chuyển sang ESP-IDF đáng công khi cần cấu hình sâu (bảng phân vùng, mức log, tùy chọn Wi-Fi, bảo mật flash), cần build lại nhanh khi dự án lớn, hoặc làm sản phẩm cần kiểm soát phiên bản từng thư viện.
8. Lỗi hay gặp
| Thông báo | Nguyên nhân | Cách xử lý |
|---|---|---|
fatal error: den_led.h: No such file or directory | Thiếu REQUIRES den_led hoặc sai INCLUDE_DIRS | Kiểm tra CMakeLists.txt của cả hai component |
undefined reference to 'den_led_dao' | File .c không có trong SRCS | Thêm tên file vào SRCS |
'CONFIG_DEN_LED_GPIO' undeclared | Kconfig.projbuild sai tên hoặc đặt sai thư mục; thiếu #include "sdkconfig.h" | Đặt file trong thư mục component, chạy lại menuconfig |
| Sửa sdkconfig.defaults không thấy tác dụng | sdkconfig đã có từ trước | Xóa sdkconfig rồi build lại |
Failed to resolve component 'driver' trên bản cũ / mới | Tên component khác nhau giữa các phiên bản ESP-IDF | Dùng tên đúng với phiên bản đang cài |
Xem thêm