본문으로 건너뛰기
ESP32-C3 Mastering · 9/12

ESP-IDF 빌드 시스템 분석 — 컴포넌트 구조와 CMake 통합

· Hawk · 6분 읽기

#한 줄 요약

“ESP-IDF는 컴포넌트가 단위입니다. main도 컴포넌트고, freertos도 컴포넌트고, 외부 라이브러리도 컴포넌트입니다.” 컴포넌트 경계를 잘 그으면 재사용·테스트·OTA가 자연스럽고, 못 그으면 한 덩어리의 main.c가 됩니다.

ESP-IDF v5.x의 빌드는 CMake 위의 idf.py 래퍼입니다. CMake가 실제 빌드를 하고, idf.py는 flashing·monitor·menuconfig 같은 인기 명령을 묶어 줍니다. 빌드 시스템은 컴포넌트 모델을 채택하여, 각 라이브러리·드라이버·앱이 자체 CMakeLists.txt + Kconfig를 갖습니다.

이번 장에서는 프로젝트 골격, 컴포넌트 작성, sdkconfig·menuconfig, Component Manager로 외부 의존성 가져오기, 그리고 Debug·Release 같은 빌드 flavor 분기까지 다룹니다. RISC-V 툴체인의 LTO·strip 옵션도 마지막에 정리합니다.

#프로젝트 만들기

Terminal window
# ESP-IDF 환경 활성화 (한 번만)
. $HOME/esp/esp-idf/export.sh
# 새 프로젝트
idf.py create-project --path . my_app
cd my_app
# 칩 타겟 지정 (default가 esp32라 명시 권장)
idf.py set-target esp32c3
# 빌드 / 플래시 / 모니터
idf.py build
idf.py -p /dev/ttyUSB0 flash monitor

set-target은 한 번만 실행합니다. 변경하면 build/ 디렉토리가 완전히 무효가 되어 idf.py fullclean이 필요합니다.

생성되는 구조입니다.

my_app/
├── CMakeLists.txt
├── main/
│ ├── CMakeLists.txt
│ ├── my_app.c
│ └── idf_component.yml
├── components/
├── sdkconfig
├── sdkconfig.defaults
└── partitions.csv
경로역할
CMakeLists.txt프로젝트 최상위 CMake
main/CMakeLists.txtmain 컴포넌트의 CMake
main/my_app.c사용자 코드
main/idf_component.yml외부 의존성 선언 (옵션)
components/자체 컴포넌트 (옵션)
sdkconfig빌드 설정 (gitignore 권장)
sdkconfig.defaults팀 공유 기본값 (git 커밋)
partitions.csv사용자 파티션 테이블 (옵션)

sdkconfig는 menuconfig가 생성하는 결과물입니다. 팀이 공유할 때는 *sdkconfig.defaults*에 변경한 값만 적고, sdkconfig는 .gitignore에 두는 패턴이 표준입니다.

#컴포넌트 — 빌드 단위

ESP-IDF에서 모든 코드는 컴포넌트 안에 있습니다. main도 특수 이름의 컴포넌트일 뿐, freertos, lwip, nvs_flash와 동등합니다.

컴포넌트의 최소 골격입니다.

components/
└── my_sensor/
├── CMakeLists.txt
├── include/
│ └── my_sensor.h
├── my_sensor.c
└── Kconfig # 설정 (옵션)
components/my_sensor/CMakeLists.txt
idf_component_register(
SRCS "my_sensor.c"
INCLUDE_DIRS "include"
REQUIRES driver esp_timer
PRIV_REQUIRES nvs_flash
)

REQUIRES는 public 의존성(이 컴포넌트의 헤더가 해당 컴포넌트의 헤더를 #include함). PRIV_REQUIRES는 private 의존성(.c 파일 안에서만 씀). 분리하면 헤더 포함 그래프가 명확해지고 순환 의존을 막습니다.

main 컴포넌트의 CMake도 같은 구조입니다.

main/CMakeLists.txt
idf_component_register(
SRCS "my_app.c"
INCLUDE_DIRS "."
REQUIRES my_sensor esp_wifi nvs_flash
)

#Kconfig — 컴포넌트 설정 메뉴

각 컴포넌트는 자체 Kconfig로 설정을 노출합니다. idf.py menuconfig에서 사용자가 체크박스·정수·문자열로 수정합니다.

# components/my_sensor/Kconfig
menu "My Sensor"
config MY_SENSOR_ENABLE
bool "Enable my sensor driver"
default y
config MY_SENSOR_SAMPLE_RATE_HZ
int "Sample rate (Hz)"
depends on MY_SENSOR_ENABLE
range 1 1000
default 100
help
Sample rate Hz. Higher rate increases CPU load.
config MY_SENSOR_LOG_LEVEL
int "Log level"
default 3
range 0 5
endmenu

C 코드에서는 생성된 매크로를 씁니다.

#include "sdkconfig.h"
#if CONFIG_MY_SENSOR_ENABLE
int rate = CONFIG_MY_SENSOR_SAMPLE_RATE_HZ;
ESP_LOGI(TAG, "sensor sample rate: %d Hz", rate);
#endif

Kconfig.projbuild라는 변형 파일명은 프로젝트 루트 메뉴에 직접 항목을 추가합니다. 보통은 그냥 Kconfig를 쓰고, 별도 메뉴를 최상위로 띄우고 싶을 때만 Kconfig.projbuild를 씁니다.

Terminal window
idf.py menuconfig

ncurses TUI가 뜹니다. 주요 메뉴입니다.

─[Sdkconfig editor]──────────────────────
Serial flasher config --->
Partition Table --->
Compiler options --->
Component config --->
Bluetooth --->
Wi-Fi --->
FreeRTOS --->
ESP System Settings --->
Example Configuration --->
My Sensor ---> ← 우리가 추가한 메뉴

저장하면 *프로젝트 루트의 sdkconfig*에 반영됩니다. 팀 공유는 변경된 값만을 sdkconfig.defaults에 옮기는 것이 깔끔합니다.

팀 공유 기본값은 sdkconfig.defaults에 적습니다.

CONFIG_IDF_TARGET="esp32c3"
CONFIG_ESP_MAIN_TASK_STACK_SIZE=8192
CONFIG_FREERTOS_HZ=1000
CONFIG_COMPILER_OPTIMIZATION_SIZE=y
CONFIG_BT_ENABLED=y
CONFIG_BT_NIMBLE_ENABLED=y
CONFIG_MY_SENSOR_ENABLE=y
CONFIG_MY_SENSOR_SAMPLE_RATE_HZ=200

새 clone에서 idf.py build를 처음 돌리면 defaults가 sdkconfig로 자동 변환됩니다.

#Component Manager — 외부 의존성

ESP-IDF 4.4 이후 Component Manager가 도입되었습니다. NPM 같은 컴포넌트 레지스트리에서 라이브러리를 가져옵니다. 레지스트리는 https://components.espressif.com 입니다.

main/idf_component.yml
dependencies:
idf:
version: ">=5.1"
espressif/cmake_utilities: "^0.5"
espressif/led_strip: "^2.5"
espressif/mdns: "^1.2"
joltwallet/littlefs: "^1.10"

idf.py build 첫 호출에서 자동으로 다운로드되어 managed_components/에 풀립니다. .gitignore에 두는 것이 표준입니다(lock 파일인 dependencies.lock만 커밋).

자체 git repo의 컴포넌트도 가능합니다.

dependencies:
my_private_lib:
git: "https://github.com/myorg/my_private_lib.git"
version: "v1.2.3"

#파티션 테이블

플래시는 파티션으로 나뉩니다. 기본은 factory + nvs + otadata + ota_0 + ota_1 같은 형태이지만, 펌웨어 크기·OTA·파일시스템에 따라 맞춰 잘라야 합니다.

partitions.csv
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x9000, 0x6000,
phy_init, data, phy, 0xf000, 0x1000,
factory, app, factory, 0x10000, 1M,
storage, data, spiffs, , 1M,

OTA 슬롯 두 개를 두려면 factory를 빼고 ota_0, ota_1을 둡니다.

nvs, data, nvs, 0x9000, 0x6000,
otadata, data, ota, 0xf000, 0x2000,
ota_0, app, ota_0, 0x20000, 1500K,
ota_1, app, ota_1, , 1500K,
storage, data, spiffs, , 500K,
Terminal window
idf.py menuconfig
# Partition Table → Custom partition table CSV → partitions.csv

플래시 사이즈 mismatch는 흔한 빌드 실패 원인입니다. menuconfig의 Serial flasher config → Flash size와 실제 모듈의 플래시 사이즈가 같아야 합니다. ESP32-C3-WROOM-02는 4 MB가 표준이고, 일부 -N8 모델은 8 MB입니다.

#빌드 플레이버 — Debug·Release 분리

여러 sdkconfig.defaults 파일을 두면 플레이버가 만들어집니다.

my_app/
├── sdkconfig.defaults # 공통
├── sdkconfig.defaults.debug # Debug 추가
├── sdkconfig.defaults.release # Release 추가
└── ...
Terminal window
# Debug 빌드
idf.py -DSDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.defaults.debug" build
# Release 빌드 (별도 build dir)
idf.py -B build-release \
-DSDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.defaults.release" \
build

Debug용 추가 옵션 예시입니다.

sdkconfig.defaults.debug
CONFIG_COMPILER_OPTIMIZATION_DEBUG=y
CONFIG_BOOTLOADER_LOG_LEVEL_DEBUG=y
CONFIG_LOG_DEFAULT_LEVEL_DEBUG=y
CONFIG_ESP_SYSTEM_PANIC_PRINT_HALT=y
CONFIG_FREERTOS_USE_TRACE_FACILITY=y
CONFIG_FREERTOS_GENERATE_RUN_TIME_STATS=y
sdkconfig.defaults.release
CONFIG_COMPILER_OPTIMIZATION_SIZE=y
CONFIG_BOOTLOADER_LOG_LEVEL_WARN=y
CONFIG_LOG_DEFAULT_LEVEL_INFO=y
CONFIG_ESP_SYSTEM_PANIC_SILENT_REBOOT=y
CONFIG_COMPILER_CXX_EXCEPTIONS=n
CONFIG_NEWLIB_NANO_FORMAT=y

#툴체인 — riscv32-esp-elf-gcc

ESP32-C3는 RV32IMC 코어라 RISC-V 툴체인을 씁니다.

~/.espressif/tools/riscv32-esp-elf/esp-13.2.0_20230928/riscv32-esp-elf/bin/riscv32-esp-elf-gcc
which riscv32-esp-elf-gcc
riscv32-esp-elf-gcc --version
# riscv32-esp-elf-gcc (crosstool-NG esp-13.2.0_20230928) 13.2.0

ESP-IDF v5.x는 GCC 13 기반입니다. C++23, C17 일부 기능이 사용 가능합니다. *LTO(Link-Time Optimization)*를 켜면 코드 크기가 5~10% 줄고 cross-translation-unit inlining이 이뤄집니다.

# sdkconfig
CONFIG_COMPILER_OPTIMIZATION_LTO=y

단, LTO는 빌드 시간이 2~3배로 늘고, 링커 에러 메시지가 모호해집니다. 운영 release 빌드에만 켜고 Debug에는 꺼 두는 것이 보통입니다.

#흔한 함정과 troubleshooting

증상원인해결
”esp32 target not supported”set-target 이 esp32 (기본)idf.py set-target esp32c3
sdkconfig diff 폭주sdkconfig를 git commitsdkconfig.defaults만 커밋, sdkconfig 제외
ESP_ERROR_CHECK fail = 0x103flash size mismatchmenuconfig Flash size 모듈과 일치
component header not foundREQUIRES 누락해당 컴포넌트를 REQUIRES에 추가
링커 에러: undefined referencePRIV_REQUIRES만 있고 헤더 노출 시도REQUIRES로 승격
OTA가 두 슬롯 인식 못 함partitions.csv에 otadata 누락otadata 파티션 추가
managed_components가 매번 다운로드.gitignore에 못 들어감dependencies.lock 커밋, dir은 ignore
빌드는 되는데 부팅 panic파티션 offset 미정렬offset을 0x1000 배수로

가장 자주 보는 함정은 플래시 사이즈 mismatch입니다. 4 MB로 설정해 빌드했는데 모듈이 2 MB라면, 부트로더가 파티션 테이블 위치를 못 찾고 즉시 reset 루프에 빠집니다. esptool.py flash_id로 실제 칩 정보를 먼저 확인합니다.

Terminal window
esptool.py --port /dev/ttyUSB0 flash_id
# Manufacturer: ef
# Device: 4016
# Detected flash size: 4MB

#정리

  • ESP-IDF는 컴포넌트가 빌드 단위입니다. main, freertos, 외부 라이브러리가 동등합니다.
  • 컴포넌트는 CMakeLists.txt + Kconfig 두 파일로 정의됩니다. idf_component_register가 핵심 호출입니다.
  • REQUIRES(public)와 PRIV_REQUIRES(private)를 분리하면 헤더 그래프가 명확해집니다.
  • sdkconfig는 gitignore, sdkconfig.defaults는 git 커밋이 표준입니다.
  • Component Manager(idf_component.yml)는 컴포넌트 NPM입니다. 공식 레지스트리와 git URL 모두 지원합니다.
  • 파티션 테이블의 flash size는 실제 모듈과 반드시 일치해야 합니다. mismatch는 부팅 panic의 1순위 원인입니다.
  • 빌드 플레이버는 sdkconfig.defaults.debug / .release로 분리합니다. Debug는 풍부한 로그, Release는 size 최적화 + silent reboot.
  • 툴체인은 riscv32-esp-elf-gcc 13입니다. LTO는 release 빌드에서만 켜는 것이 보통입니다.

#다음 편

Ch 10: FreeRTOS on ESP32-C3에서는 빌드한 펌웨어 안에서 어떻게 태스크가 돌아가는지를 봅니다. Espressif fork의 단일 코어 동작, tickless idle, watchdog까지 풉니다.

#관련 항목