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

Cây thư mục dự án den_nhay_kconfig: CMakeLists.txt, sdkconfig.defaults, sdkconfig, thư mục main có CMakeLists.txt, Kconfig.projbuild, main.c; thư mục components có den_led với CMakeLists.txt, include/den_led.h, den_led.c; thư mục build
Cây thư mục của dự án mẫu trong bài.

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.txt riêng, chứa code của một phần chương trình. main là component bắt buộc, chứa hàm app_main(). Component tự viết đặt trong components/.
  • Cấu hình: sdkconfig chứa mọi tùy chọn (tần số CPU, dung lượng flash, mức log…). sdkconfig.defaults chứ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
Giao diện chữ nền xanh của idf.py menuconfig đang mở menu Cau hinh den LED PYWORLD với hai mục Chan GPIO cua LED bằng 2 và Chu ky dao LED bằng 250 ms
Menu tự thêm trong menuconfig (hình minh họa).

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

Sơ đồ khối: idf.py build, CMake đọc CMakeLists.txt, gom Kconfig mọi component, sdkconfig thành sdkconfig.h, biên dịch từng component, liên kết thành .elf, tạo .bin và bảng phân vùng, idf.py flash nạp vào chip
Các bước của idf.py build (hình minh họa).
  1. CMake đọc CMakeLists.txt ở gốc, tìm mọi component (của dự án và của ESP-IDF) cần dùng.
  2. Gom file Kconfig của các component đó, kết hợp với sdkconfig để sinh build/config/sdkconfig.h.
  3. 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 .bin cù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+].

Cửa sổ idf.py monitor với các dòng I (302) den_led: Khoi tao LED o GPIO2, I (302) main: Chu ky dao LED 250 ms và các dòng Da dao LED 10, 20, 30 lan
Log của dự án mẫu trên 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

Bảng đối chiếu: setup và loop ứng với app_main và task FreeRTOS; thư viện ứng với component; menu Tools ứng với menuconfig và sdkconfig; define ứng với Kconfig; Partition Scheme ứng với partitions.csv; Serial.print ứng với ESP_LOGI; delay ứng với vTaskDelay; Library Manager ứng với idf.py add-dependency
Khái niệm Arduino và khái niệm tương ứng trong ESP-IDF.

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áoNguyên nhânCách xử lý
fatal error: den_led.h: No such file or directoryThiếu REQUIRES den_led hoặc sai INCLUDE_DIRSKiểm tra CMakeLists.txt của cả hai component
undefined reference to 'den_led_dao'File .c không có trong SRCSThêm tên file vào SRCS
'CONFIG_DEN_LED_GPIO' undeclaredKconfig.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ụngsdkconfig đã có từ trướcXóa sdkconfig rồi build lại
Failed to resolve component 'driver' trên bản cũ / mớiTên component khác nhau giữa các phiên bản ESP-IDFDùng tên đúng với phiên bản đang cài

9. Xem thêm


Tin tức liên quan

SPI là gì: MOSI, MISO, SCK, CS, bốn chế độ và cách dùng trên ESP32
SPI là gì: MOSI, MISO, SCK, CS, bốn chế độ và cách dùng trên ESP32

Giải thích giao tiếp SPI: bốn dây MOSI, MISO, SCK, CS, truyền hai chiều cùng lúc và bốn chế độ clock. Kèm hai chương trình ESP32: tự gửi tự nhận để kiểm tra bus và gửi lệnh thô cho thẻ nhớ.

Tìm lỗi mạch điện tử từng bước: quan sát, đo nguồn, chia đôi mạch
Tìm lỗi mạch điện tử từng bước: quan sát, đo nguồn, chia đôi mạch

Trình tự 6 bước tìm lỗi mạch: mô tả triệu chứng, quan sát mối hàn và breadboard, đo nguồn đúng chỗ, chia đôi mạch, đo linh kiện khi tắt điện, lỗi hay gặp với ESP32 và bảng tra theo triệu chứng.

Hiệu chuẩn cờ lê lực theo ISO 6789: phân loại, dung sai ±4 %, trình tự đo và lỗi khi sử dụng
Hiệu chuẩn cờ lê lực theo ISO 6789: phân loại, dung sai ±4 %, trình tự đo và lỗi khi sử dụng

Cờ lê lực theo ISO 6789-1 và ISO 6789-2: kiểu chỉ thị và kiểu cài đặt, dung sai ±4 %, trình tự hiệu chuẩn ba điểm mười lần đo, ảnh hưởng điểm đặt tay, đầu nối dài và cách bảo quản.

Lập trình nuôi dưỡng tư duy sáng tạo của trẻ: bài toán nhiều lời giải, dự án mở, vòng học sáng tạo
Lập trình nuôi dưỡng tư duy sáng tạo của trẻ: bài toán nhiều lời giải, dự án mở, vòng học sáng tạo

Vì sao học lập trình nuôi dưỡng tư duy sáng tạo của trẻ: bằng chứng nghiên cứu, bài toán nhiều lời giải, học qua làm sản phẩm, vòng học sáng tạo, bốn tiêu chí và thử thách mở.

Cách đấu jack Type-C tích hợp mạch sạc với pin 18650, Li-po: sơ đồ, đèn báo và lỗi thường gặp
Cách đấu jack Type-C tích hợp mạch sạc với pin 18650, Li-po: sơ đồ, đèn báo và lỗi thường gặp

Hướng dẫn đấu jack Type-C tích hợp mạch sạc với pin 18650 hoặc Li-po một cell: dây đỏ, dây đen nối vào đâu, tải nối thế nào, khi nào cần mạch bảo vệ, cách đọc đèn đỏ xanh và bảng tra lỗi thường gặp.

Tư duy máy tính (tư duy lập trình) là gì? 4 kỹ năng với ví dụ đời thường cho trẻ
Tư duy máy tính (tư duy lập trình) là gì? 4 kỹ năng với ví dụ đời thường cho trẻ

Tư duy máy tính, hay tư duy lập trình, gồm bốn kỹ năng: phân rã, nhận dạng mẫu, trừu tượng hóa và thuật toán. Bài giải thích bằng ví dụ đời thường và gợi ý hoạt động cho trẻ.

Độ không đảm bảo đo theo GUM: sáu bước tính, loại A, loại B và ví dụ ngân sách hiệu chuẩn áp kế
Độ không đảm bảo đo theo GUM: sáu bước tính, loại A, loại B và ví dụ ngân sách hiệu chuẩn áp kế

Cách tính độ không đảm bảo đo theo GUM: sáu bước, đánh giá loại A và loại B, phân bố chuẩn, chữ nhật, tam giác, ví dụ ngân sách hiệu chuẩn áp kế, cách làm tròn và lỗi hay gặp.

Jack DC 5.5×2.1 và 5.5×2.5: phân biệt, cực tính, chọn jack gắn hộp và dây nguồn DC
Jack DC 5.5×2.1 và 5.5×2.5: phân biệt, cực tính, chọn jack gắn hộp và dây nguồn DC

Jack DC 5.5×2.1 và 5.5×2.5 khác nhau ở lỗ trong: cách phân biệt khi không có nhãn, ký hiệu tâm dương, đo cực tính adapter, lắp jack gắn hộp và nối nguồn vào mạch an toàn.


Bình luận
Đã thêm vào giỏ hàng