ESP32 펌웨어 기초 #2: Get Started - Blink

기운찬곰·2025년 9월 12일

ESP32 펌웨어 기초

목록 보기
2/2
post-thumbnail

ESP-IDF Examples

ESP-IDF의 샘플 프로젝트는 주로 칩의 다양한 기능을 사용하는 방법을 보여줍니다. 공식 GitHub 저장소의 examples 디렉토리에서 수많은 샘플들을 찾아볼 수 있습니다. 이 샘플들은 개발자가 자신의 프로젝트를 시작하는 데 필요한 기초적인 코드를 제공합니다.

깃허브 참고: https://github.com/espressif/esp-idf/tree/master/examples/

샘플 프로젝트 유형

ESP-IDF 샘플 프로젝트는 크게 세 가지 범주로 나눌 수 있습니다.

1. System Examples: 칩의 기본적인 기능을 다룹니다.

  • blink: LED를 깜빡이는 가장 기본적인 예제로, 칩의 GPIO를 제어하는 방법을 보여줍니다.
  • hello_world: 시리얼 포트를 통해 "Hello world"를 출력하여, 프로젝트 빌드와 플래싱이 정상적으로 작동하는지 확인하는 데 사용됩니다.
  • deep_sleep: 저전력 모드인 딥 슬립 모드를 사용하는 방법을 보여주어 배터리 구동 장치에 유용합니다.

2. Protocol Examples: 통신 프로토콜 관련 기능을 다룹니다.

  • Wi-Fi: scan, getting_started, station 등 Wi-Fi 네트워크에 연결하고 데이터를 송수신하는 방법을 보여줍니다.
  • Bluetooth: ble_scan, gatt_client, hidd_device 등 BLE(Bluetooth Low Energy)를 사용해 다른 장치와 통신하는 방법을 보여줍니다.
  • MQTT: IoT 플랫폼과 통신하기 위한 MQTT 프로토콜을 사용한 예제를 제공합니다.
  • HTTP: HTTP 서버/클라이언트 기능을 구현하는 방법을 보여줍니다.

3. Peripheral Examples: 칩에 내장된 다양한 주변 장치를 사용하는 방법을 다룹니다.

  • i2c: I2C 통신 프로토콜을 사용하여 센서와 같은 외부 장치와 통신하는 방법을 보여줍니다.
  • spi: SPI 통신을 사용하는 예제로, 주로 디스플레이나 플래시 메모리 등과 연결할 때 사용됩니다.
  • adc: 아날로그 신호를 디지털로 변환하는 ADC(Analog-to-Digital Converter)를 사용하는 방법을 보여줍니다.
  • pwm: LED 밝기 조절이나 모터 제어 등에 사용되는 PWM(Pulse Width Modulation) 기능을 다룹니다.

샘플 프로젝트 사용 방법

VS Code에서 Ctrl+Shift+P (macOS에서는 Cmd+Shift+P)를 눌러 명령 팔레트를 엽니다. 명령 팔레트에 "ESP-IDF: Show Examples Projects"를 입력하고 선택합니다.

hello_world, blink, wifi, ble 등 다양한 샘플 프로젝트 목록이 나타납니다. 원하는 샘플을 선택하고, 프로젝트를 저장할 폴더를 지정하면 자동으로 새로운 프로젝트가 생성됩니다.


가장 처음으로는 get-started 에 있는 blink 예제를 선택해서 Create project를 눌러서 프로젝트를 생성하겠습니다. 이 프로젝트는 ESP32 개발 보드에서 LED를 깜빡이는 "Blink" 예제입니다. (깃허브 참고)

프로젝트 구성은 어떤 식으로 되어 있는지, 추가적으로 어떤 설정이 필요한지 알아보도록 하겠습니다.

타겟 칩 설정

ESP-IDF에서 타겟 칩(예: ESP32, ESP32-S2, ESP32-C3)을 설정하는 방법은 CMake 기반의 빌드 시스템에서 idf.py set-target 명령어를 사용하는 것입니다.

$ idf.py set-target <target_name>

<target_name>에는 다음 중 하나를 입력합니다.

  • esp32 (가장 일반적인 ESP32 칩)
  • esp32s2, esp32s3, esp32c3, esp32c6, esp32h2 등등...

이 명령어를 실행하면, sdkconfig 파일과 CMake 빌드 시스템이 자동으로 업데이트되어 지정된 타겟 칩에 최적화된 컴파일 환경을 구성합니다.

VS Code 확장 프로그램을 설치했으면 창 하단의 상태 표시줄(파란색 막대)에서도 변경이 가능합니다. 아니면 명령 팔레트 열기(Cmd+Shift+P)를 눌러 명령 팔레트를 열고나서 "ESP-IDF: Set Espressif device target"을 입력하고 선택할 수 있습니다.

타겟을 변경한 후에는 기존 빌드 파일을 삭제하고 다시 빌드하는 것이 좋습니다.

$ idf.py fullclean
$ idf.py build

fullclean 명령어는 이전 타겟에서 생성된 모든 빌드 파일을 깨끗하게 지워, 새로운 타겟 설정에 맞춰 빌드 과정에 오류가 발생하지 않도록 합니다.


🤔 타겟 칩을 설정하는 구체적인 이유와 의미에 대해 궁금한데...?

타겟 칩에 최적화된 컴파일 환경을 구성한다는 것은, 빌드 시스템이 지정된 칩의 하드웨어 아키텍처와 내장된 기능에 맞춰서 컴파일러와 링커 설정을 변경하는 것을 의미합니다.

1. 칩 아키텍처 및 명령어 세트 최적화

모든 ESP32 칩셋이 동일한 CPU 코어를 사용하는 것은 아닙니다. 예를 들어, ESP32-S2/S3는 Tensilica Xtensa LX7 코어를 사용하고, ESP32-C3/C6는 RISC-V 코어를 사용합니다.

  • idf.py set-target esp32c3를 실행하면, ESP-IDF 빌드 시스템은 RISC-V 명령어 세트를 지원하는 툴체인(컴파일러, 어셈블러 등)을 사용하도록 설정됩니다.
  • 반면, idf.py set-target esp32s3를 실행하면 Tensilica LX7을 위한 툴체인을 사용하도록 변경됩니다.

이렇게 하면, 컴파일러가 특정 칩의 아키텍처에 맞는 효율적인 기계어 코드를 생성하여 성능을 최적화할 수 있습니다.

2. 하드웨어 기능 활성화/비활성화

각 칩은 내장된 하드웨어 주변 장치(Wi-Fi, Bluetooth, USB OTG, GPIO 등)의 구성이 다릅니다.

예를 들어, ESP32-S2는 Wi-Fi는 지원하지만 Bluetooth 기능은 없습니다. esp32s2를 타겟으로 설정하면 빌드 시스템은 Bluetooth 관련 코드가 컴파일되지 않도록 자동으로 설정합니다. 불필요한 코드를 제외함으로써 펌웨어 크기를 줄이고 메모리 사용량을 최적화할 수 있습니다.

3. 메모리 맵 및 링커 스크립트 설정

칩마다 RAM, 플래시 메모리, ROM의 주소와 크기가 다릅니다.

타겟 설정을 하면 링커가 해당 칩의 실제 메모리 맵에 맞춰 변수, 함수, 라이브러리 코드를 배치하도록 링커 스크립트를 변경합니다. 이 과정은 프로그램이 실행될 때 메모리 주소 충돌을 방지하고, 효율적으로 자원을 할당하는 데 필수적입니다.

✍️ 타겟 칩을 설정하는 것은 칩의 종류에 따라 최적의 성능을 끌어내고 불필요한 코드를 제거하며, 하드웨어에 맞는 정확한 빌드 환경을 구축하는 핵심 과정입니다.

프로젝트 구성 (menuconfig)

menuconfig는 ESP-IDF 프로젝트의 빌드 및 실행 환경, 다양한 설정 옵션을 쉽게 변경할 수 있는 메뉴 기반의 환경 설정 도구입니다. 리눅스 커널 설정과 유사한 인터페이스를 제공하며, 터미널에서 idf.py menuconfig 명령어로 실행합니다. 혹은 하단 표시줄 > 톱니바퀴 선택하면 됩니다.

여러 가지 설정 옵션이 많은데 그 중에서 Example Configuration 를 선택합니다.

Blink 예제는 단순히 LED를 켜고 끄는 것이기 때문에, 기본 설정 외에 특별히 변경할 사항은 많지 않습니다. LED가 연결된 GPIO 핀 번호를 설정하는 것이 가장 중요합니다. 이렇게 설정하면, 코드를 직접 수정하지 않고도 다른 GPIO 핀에 연결된 LED를 제어할 수 있습니다.

정확히는 menuconfig에서 GPIO 핀을 설정하면, 그 정보가 sdkconfig 파일에 저장되고, 이 파일을 기반으로 자동 생성된 헤더 파일이 코드에 포함되어 실제 핀 번호를 사용하게 됩니다.

실제로 sdkconfig 파일을 보면 위에서 설정한 내용이 적용되어 있습니다.

// sdkconfig 파일
CONFIG_BLINK_LED_GPIO=y
# CONFIG_BLINK_LED_STRIP is not set
CONFIG_BLINK_GPIO=5
CONFIG_BLINK_PERIOD=1000

참고로, 제가 가진 ESP32 개발 보드에는 내장 LED가 없고, 5V Power On LED만 존재합니다. 따라서 GPIO5에다가 LED를 연결해서 테스트를 진행했습니다. (회로도 참고)

sdkconfig

sdkconfig는 ESP-IDF 프로젝트의 환경 설정 파일입니다. 프로젝트의 모든 빌드 옵션과 컴포넌트 설정을 저장하는 텍스트 파일입니다.

sdkconfig의 주요 역할

  1. 빌드 옵션 저장: idf.py menuconfig를 통해 변경하는 모든 설정이 이 파일에 저장됩니다. 여기에는 칩셋(ESP32, ESP32-C3 등) 설정, Wi-Fi 및 Bluetooth 설정, GPIO 핀 번호, 파티션 테이블 레이아웃 등 수많은 옵션이 포함됩니다.
  2. 프로젝트의 일관성 유지: 팀원들이나 다른 컴퓨터에서 동일한 프로젝트를 빌드할 때, 이 파일을 공유함으로써 모두가 동일한 설정 환경에서 작업하도록 보장합니다.
  3. 코드와 설정의 분리: 코드를 직접 수정하지 않고도 menuconfig를 통해 하드웨어 관련 설정을 변경할 수 있게 합니다.

어떻게 적용이 되는지 순서대로 설명하면 다음과 같습니다.

1단계. idf.py menuconfig를 실행하고 설정을 변경한 뒤 저장하면, 프로젝트 루트 디렉토리에 있는 sdkconfig 파일이 업데이트됩니다. 이 파일은 프로젝트의 모든 설정 옵션을 키-값 쌍으로 저장하는 텍스트 파일입니다.

CONFIG_BLINK_GPIO=2

2단계. 빌드 과정이 시작될 때, CMake 빌드 시스템은 이 sdkconfig 파일의 내용을 읽어 sdkconfig.h라는 헤더 파일을 자동으로 생성합니다. 이 헤더 파일은 C/C++ 소스 코드가 사용할 수 있는 #define 매크로를 포함합니다.

#define CONFIG_BLINK_GPIO_NUM 2

3단계. Blink 예제의 C 소스 코드(main/blink.c)는 이 sdkconfig.h 파일을 #include하여 설정 값을 가져옵니다.

#include "sdkconfig.h"

...

int blink_gpio = CONFIG_BLINK_GPIO_NUM;

...

이렇게 하면 CONFIG_BLINK_GPIO_NUM이라는 매크로가 컴파일 시점에 sdkconfig 파일에서 설정한 값으로 대체되어, GPIO 제어 함수가 올바른 핀 번호를 사용하게 됩니다.

이제 blink_example_main.c 파일 소스 코드를 살펴보겠습니다.

1. 헤더 파일 포함

#include <stdio.h>

// FreeRTOS 헤더 파일, Task 관련 함수를 사용하기 위해서 선언. 여기서는 vTaskDelay() 사용
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"

// gpio 관련 함수들을 사용하기 위해 선언
#include "driver/gpio.h"

// 그 외 로그, led strip 라이브러리(사용 안함), 설정 파일
#include "esp_log.h"
#include "led_strip.h"
#include "sdkconfig.h"

🤔 근데, gpio 관련 함수들은 누가, 어디서 제공하는걸까?

driver/gpio.h는 ESP-IDF의 GPIO 드라이버 컴포넌트에서 제공하는 헤더 파일입니다. ESP-IDF 프레임워크 안에 내장되어 있으며, ESP32 칩의 GPIO 핀을 제어하는 데 필요한 함수 선언과 매크로 정의를 포함하고 있습니다. 👉 라이브러리 파일: /Users/ckstn0777/esp/v5.4.1/esp-idf/components/esp_driver_gpio/src/gpio.c

🎯 ESP-IDF 컴포넌트 구조

ESP-IDF는 여러 개의 컴포넌트(components)로 구성되어 있습니다. 각 컴포넌트는 특정 기능(예: Wi-Fi, 블루투스, GPIO, I2C 등)을 담당하는 모듈이며, 관련된 소스 코드와 헤더 파일을 가지고 있습니다.

Blink 예제가 이 헤더 파일을 사용하는 과정:

  1. #include "driver/gpio.h": Blink 예제 소스 코드에서 이 헤더 파일을 포함하면, 컴파일러는 gpio.h 파일의 내용을 불러옵니다.
  2. 빌드 시스템의 역할: ESP-IDF의 빌드 시스템(CMake) 은 자동으로 컴포넌트 경로를 설정해줍니다. 따라서 개발자는 복잡한 경로를 직접 지정할 필요 없이, <컴포넌트_이름>/<헤더_파일_이름> 형식으로 헤더 파일을 간단하게 포함할 수 있습니다.
  3. 함수 호출: 소스 코드는 이 헤더 파일에 선언된 gpio_set_direction(), gpio_set_level() 같은 함수들을 호출하여 GPIO 핀을 제어합니다.

따라서 driver/gpio.h는 ESP-IDF가 제공하는 드라이버 라이브러리의 일부이며, 사용자가 직접 가져올 필요 없이 프레임워크 내에서 자동으로 관리됩니다.

2. GPIO 핀 정의, 상태 변수

#define BLINK_GPIO CONFIG_BLINK_GPIO

static uint8_t s_led_state = 0;

menuconfig에서 설정한 CONFIG_BLINK_GPIO_NUM 매크로 값을 BLINK_GPIO라는 더 직관적인 이름으로 다시 정의합니다.

참고로, static 키워드는 변수의 유효 범위를 현재 파일로 제한하는 역할을 합니다. static으로 선언된 변수는 이 변수가 선언된 .c 파일 내에서만 접근 가능하며, 다른 파일에서는 이 변수를 사용할 수 없습니다. 코드의 모듈성을 높이고, 안정성과 유지보수성을 향상시키는 데 매우 중요한 역할을 합니다.

3. LED 함수 구현

#ifdef CONFIG_BLINK_LED_STRIP

...(실행 안됨)...

#elif CONFIG_BLINK_LED_GPIO

static void blink_led(void)
{
    // led_state에 따라 GPIO 핀의 전압 레벨 설정
    gpio_set_level(BLINK_GPIO, s_led_state); 
}

static void configure_led(void)
{
    ESP_LOGI(TAG, "Example configured to blink GPIO LED!");
    
    // 사용할 GPIO 핀 초기화
    gpio_reset_pin(BLINK_GPIO);  
    
    // 해당 핀의 모드를 출력으로 설정
    gpio_set_direction(BLINK_GPIO, GPIO_MODE_OUTPUT); 
}
#else
#error "unsupported LED type"
#endif

#ifdef, #elif, #else, #endif 전처리기를 사용하여 조건부 컴파일을 수행합니다. 이는 menuconfig에서 사용자가 어떤 LED 타입을 선택했는지에 따라 컴파일될 코드를 결정합니다. 저희는 LED_GPIO 타입을 사용하기 때문에 LED_STRIP 관련 코드 부분은 실행되지 않습니다.

4. 메인 애플리케이션 함수

void app_main(void)
{
    /* Configure the peripheral according to the LED type */
    configure_led();

    while (1) {
        ESP_LOGI(TAG, "Turning the LED %s!", s_led_state == true ? "ON" : "OFF");
        blink_led();
        /* Toggle the LED state */
        s_led_state = !s_led_state;
        vTaskDelay(CONFIG_BLINK_PERIOD / portTICK_PERIOD_MS); // 1초 동안 딜레이
    }
}

CMake

CMake는 C, C++와 같은 소스 코드를 컴파일하고 실행 파일을 만드는 과정을 제어하는 크로스 플랫폼 빌드 시스템 생성기 입니다. 다양한 운영 체제와 개발 환경에서 일관된 방식으로 프로젝트를 빌드할 수 있도록 돕습니다.

CMake의 역할

CMake는 직접 소스 코드를 컴파일하는 컴파일러가 아니라, 컴파일을 위한 빌드 스크립트(Makefile, Visual Studio 프로젝트 파일 등)를 생성하는 도구입니다.

  1. 빌드 시스템 생성: 개발자가 작성한 CMakeLists.txt 파일을 읽어들여, 프로젝트에 필요한 빌드 규칙을 분석합니다.
  2. 환경 독립적: 분석된 규칙을 기반으로, 사용자의 운영 체제에 맞는 네이티브 빌드 파일을 만듭니다. Linux/macOS에서는 make 명령어가 사용하는 Makefile을 생성합니다. Windows에서는 Visual Studio의 .sln 파일을 생성합니다.
  3. 컴파일 실행: 사용자는 생성된 빌드 파일(예: Makefile)을 사용하여 최종 실행 파일을 만듭니다.

ESP-IDF에서 CMake를 사용하는 이유

ESP-IDF는 CMake를 사용하여 복잡한 임베디드 프로젝트의 빌드 과정을 단순화하고, 개발 환경에 구애받지 않는 유연성을 제공합니다.

  • 컴포넌트 기반 빌드: ESP-IDF는 각 기능(예: Wi-Fi, GPIO, FreeRTOS)을 독립적인 컴포넌트로 구성합니다. CMake는 각 컴포넌트의 CMakeLists.txt를 읽고, 종속성을 자동으로 해결하여 필요한 컴포넌트만 빌드에 포함시킵니다.
  • 크로스 플랫폼 지원: Windows, macOS, Linux 등 어떤 운영 체제에서도 cmake 명령어를 통해 동일한 빌드 환경을 구축할 수 있습니다.
  • 구성 관리: menuconfig에서 설정한 내용(예: 타겟 칩, GPIO 핀)이 sdkconfig 파일에 저장되면, CMake는 이 정보를 읽어 최적화된 빌드 환경을 자동으로 구성합니다.

✍️ CMake는 개발자가 각기 다른 환경의 복잡한 빌드 시스템을 직접 다룰 필요 없이, 하나의 CMakeLists.txt 파일만으로 프로젝트의 빌드 과정을 통일하고 자동화할 수 있게 해주는 핵심 도구입니다.

Make와 CMake 차이

이미지 출처: https://earthly.dev/blog/cmake-vs-make-diff/

Make는 프로그램의 소스 파일로부터 실행 파일과 기타 비(非) 소스 파일을 생성하는 과정을 제어하는 도구입니다. 이 도구는 Makefile이라는 파일로부터 프로그램을 빌드하는 방법을 지시받습니다.

반면, CMake는 CMakeLists.txt 파일이 필요하며, 크로스 플랫폼(cross-platform)을 지원하는 Make입니다. 즉, 여러 운영 체제에서 작동합니다. 이를 통해 컴파일러에 독립적인 빌드, 테스트, 패키징 및 소프트웨어 설치가 가능합니다.

중요한 점은 CMake가 다른 시스템을 위한 빌드 파일을 생성할 뿐, 그 자체가 빌드 시스템은 아니라는 것입니다. CMake는 Makefile을 생성할 수 있으며, 생성된 Makefile은 작업 중인 플랫폼에서 Make와 함께 사용될 수 있습니다.

CMakeLists.txt 사용법

CMakeLists.txt는 CMake가 프로젝트를 빌드하는 데 사용하는 스크립트 파일입니다. 이 파일을 작성함으로써 소스 파일, 헤더 파일, 라이브러리 등을 어떻게 컴파일하고 링크할지 CMake에 지시할 수 있습니다.

기본적인 CMakeLists.txt 파일 구성 요소는 다음과 같습니다.

1. 최소 CMake 버전 지정

cmake_minimum_required() 명령은 이 프로젝트를 빌드하는 데 필요한 최소 CMake 버전을 지정합니다. 이는 하위 호환성을 보장하는 데 중요합니다.

cmake_minimum_required(VERSION 3.16)

ESP-IDF의 경우, 각 버전별로 요구하는 CMake의 최소 버전이 다릅니다. 따라서 idf.py가 자동으로 생성하는 CMakeLists.txt에는 이 명령어가 항상 포함되어 있습니다.

2. 프로젝트 정의: project() 명령어를 사용하여 프로젝트 이름과 언어를 정의합니다.

# 프로젝트 이름과 언어 정의
project(MyProject C)

3. 실행 파일 생성: add_executable() 명령어를 사용하여 소스 파일들을 묶어 실행 파일을 만듭니다.

# my_executable이라는 실행 파일 생성
# 소스 파일: main.c, my_module.c
add_executable(my_executable main.c my_module.c)

4. 라이브러리 연결: target_link_libraries() 명령어를 사용하여 생성된 실행 파일에 필요한 라이브러리를 연결합니다. → 타겟에 라이브러리 링크

# my_executable에 math 라이브러리 연결
target_link_libraries(my_executable PRIVATE math)

그 외 주요 CMake 명령어 정리

  • add_library(name src1 src2 ...) → 라이브러리 생성
  • include_directories(path1 path2 ...) → 헤더 파일 경로 추가
  • idf_component_register(...) → ESP-IDF 전용, 컴포넌트 등록

ESP-IDF는 일반 CMake 프로젝트보다 살짝 구조가 다릅니다. 기본적으로 프로젝트 단위 CMakeLists.txt와 컴포넌트 단위 CMakeLists.txt가 있어요.

1. 프로젝트 최상위 CMakeLists.txt

이 파일은 전체 프로젝트의 빌드 방법을 정의합니다. 모든 ESP-IDF 프로젝트의 최상위 CMakeLists.txt는 다음의 필수적인 내용을 포함해야 합니다.

# 최상위 CMakeLists.txt (프로젝트 루트)
cmake_minimum_required(VERSION 3.16)

# IDF_PATH: ESP-IDF가 설치된 경로를 가리키는 환경 변수
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(my_app)
  • 최소 CMake 버전 지정: 프로젝트 빌드에 필요한 최소 CMake 버전을 지정합니다.
  • ESP-IDF 빌드 시스템 포함: IDF_PATH 환경 변수를 사용하여 ESP-IDF의 빌드 시스템을 가져옵니다.
  • 프로젝트 이름 설정: 프로젝트의 이름을 정의합니다. 이 이름은 최종 바이너리 파일(예: my_app.elf)에 사용됩니다.

2. 컴포넌트 단위 CMakeLists.txt

ESP-IDF에서는 컴포넌트 기반의 빌드 시스템을 사용하기 때문에, 각 컴포넌트 폴더에 CMakeLists.txt 파일을 두어 독립적으로 관리합니다. idf_component_register() 명령어를 사용하여 소스 파일, 헤더 파일 경로, 그리고 다른 컴포넌트에 대한 종속성을 정의합니다.

예시 1: main 컴포넌트의 CMakeLists.txt (main 컴포넌트는 프로젝트의 시작점인 app_main() 함수를 포함하는 특수한 컴포넌트입니다.)

# main 컴포넌트의 소스 파일과 포함 디렉터리를 등록합니다.
idf_component_register(SRCS "main.c"
                       INCLUDE_DIRS ".")

예시 2: 사용자 정의 컴포넌트의 CMakeLists.txt

components 디렉터리에 my_component라는 컴포넌트를 추가할 경우, my_component 디렉터리 내부에 CMakeLists.txt를 생성합니다.

# 프로젝트 구조
myProject/
├── CMakeLists.txt         # 최상위 프로젝트 CMakeLists.txt
├── main/
│   ├── CMakeLists.txt     # main 컴포넌트의 CMakeLists.txt
│   └── main.c
├── components/
│   └── my_component/
│       ├── CMakeLists.txt # 사용자 정의 컴포넌트의 CMakeLists.txt
│       ├── my_component.c
│       └── my_component.h
└── build/

my_component/CMakeLists.txt

# my_component의 소스 파일과 포함 디렉터리를 등록합니다.
idf_component_register(SRCS "my_component.c"
                       INCLUDE_DIRS ".")

main/CMakeLists.txt

# main 컴포넌트가 my_component를 사용하려면 종속성을 추가해야 합니다.
idf_component_register(SRCS "main.c"
                       INCLUDE_DIRS "."
                       REQUIRES my_component)

🤔 include($ENV{IDF_PATH}/tools/cmake/project.cmake) 구체적 의미

이 명령어는 ESP-IDF 프로젝트의 CMake 빌드 시스템을 로드하는 핵심 명령입니다. 이 한 줄을 통해 ESP-IDF의 모든 빌드 규칙과 환경 설정이 활성화됩니다. /tools/cmake/project.cmake 는 ESP-IDF의 빌드 시스템과 관련된 모든 핵심 함수와 매크로가 정의된 파일의 경로입니다.

project.cmake 파일에는 다음과 같은 중요한 내용이 담겨 있습니다.

  1. 컴포넌트 빌드 규칙: idf_component_register()와 같은 ESP-IDF 전용 CMake 함수들이 정의되어 있습니다. 이 함수들은 컴포넌트의 소스 파일을 찾고, 종속성을 해결하며, 최종적으로 실행 파일을 만드는 복잡한 과정을 자동화합니다.
  2. 환경 설정: 타겟 칩, SDK 구성(예: sdkconfig 파일 읽기), 툴체인 경로 설정 등 ESP-IDF 프로젝트 빌드에 필요한 모든 환경 설정이 이 파일에서 이루어집니다.
  3. ESP-IDF 빌드 시스템 활성화: 이 한 줄을 포함함으로써, idf.py 명령어가 동작하는 데 필요한 모든 기반이 마련됩니다.

결론적으로, 이 명령어는 ESP-IDF가 제공하는 특별한 CMake 빌드 기능을 가져와 프로젝트에 적용하는 역할을 합니다. 이 한 줄이 없으면 ESP-IDF의 편리한 빌드 시스템을 전혀 사용할 수 없게 됩니다.

덕분에 사용자는 간단한 CMakeLists.txt만 작성해도 복잡한 빌드 환경을 쉽게 사용할 수 있습니다.


🤔 CMake의 의존성 관리

REQUIRES는 ESP-IDF의 idf_component_register() 함수에서 사용되며, 한 컴포넌트가 다른 컴포넌트에 의존함을 선언하는 데 사용됩니다. 이 의존성은 CMake가 빌드 과정을 자동화하고 종속성 문제를 해결하는 핵심 메커니즘입니다.

아래 예시는 main 컴포넌트가 driver와 freertos 컴포넌트에 의존함을 CMake에게 알려줍니다.

idf_component_register(
    SRCS "main.c"
    REQUIRES "driver" "freertos"
)

CMake는 프로젝트의 모든 CMakeLists.txt 파일을 분석하여 종속성 트리를 구축합니다. 이 트리는 "A는 B를 필요로 하고, B는 C를 필요로 한다"와 같은 관계를 파악합니다. REQUIRES를 통해 명시된 모든 컴포넌트와 그들의 종속성을 재귀적으로 탐색하여 최종적인 컴포넌트 목록을 확정합니다.

종속성이 선언되면, CMake는 의존하는 컴포넌트의 헤더 파일 경로를 자동으로 컴파일러에 전달합니다. 따라서, main 컴포넌트의 소스 코드에서 <driver/gpio.h>와 같은 헤더 파일을 #include해도 컴파일러가 해당 파일을 찾을 수 있습니다.


🤔 근데 Blink 예시에서 mian 폴더의 CMakeLists.txt 파일을 보면 REQUIRES 이 없습니다. 그런데 어떻게 main.c 에서 driver/gpio, esp_log 등을 불러와서 사용할 수 있는 걸까요?

idf_component_register(SRCS "blink_example_main.c"
                       INCLUDE_DIRS ".")

main 컴포넌트는 사실상 루트 실행 대상이라, 특별히 의존성을 안 적어도 빌드 시스템이 기본 컴포넌트 세트를 자동으로 연결해줍니다.

ESP-IDF는 project() 호출 시, 내부적으로 freertos, esp_system, esp_common, esp_event, driver, esp_log 등 기본 컴포넌트를 항상 로드합니다. 즉, gpio.h (driver 컴포넌트)나 esp_log.h (log 컴포넌트)는 이미 기본 dependency chain에 들어 있기 때문에, REQUIRES에 따로 안 써줘도 그냥 불러올 수 있는 거예요.

ESP-IDF 컴포넌트 시스템

ESP-IDF 컴포넌트 시스템은 재사용 가능한 코드 블록을 체계적으로 구성하고 관리하는 핵심적인 빌드 개념입니다. 각 컴포넌트는 자체 소스 코드, 헤더 파일, 그리고 컴포넌트의 빌드 방법을 정의하는 CMakeLists.txt 파일을 포함하는 독립적인 디렉터리입니다.

프로젝트/
├── main/                    # 메인 애플리케이션 컴포넌트
│   ├── CMakeLists.txt       # 컴포넌트 설정
│   └── blink_example_main.c
├── components/              # 사용자 정의 컴포넌트들
│   ├── my_sensor/
│   │   ├── CMakeLists.txt
│   │   ├── include/
│   │   │   └── my_sensor.h
│   │   └── my_sensor.c
│   └── my_display/
│       ├── CMakeLists.txt
│       └── my_display.c
└── managed_components/      # 의존성 관리자로 설치된 컴포넌트 (외부 컴포넌트)
    └── espressif__led_strip/

그리고 ESP-IDF 내장 컴포넌트도 있습니다.

esp-idf/components/
├── esp_driver_gpio/         # GPIO 드라이버
├── esp_common/              # 공통 라이브러리
├── freertos/                # FreeRTOS 운영체제
├── newlib/                  # C 표준 라이브러리
├── esp_wifi/                # Wi-Fi 스택
├── esp_netif/               # 네트워크 인터페이스
└── ...

외부 컴포넌트(managed_components)도 있습니다.

# main/idf_component.yml
dependencies:
  espressif/led_strip: "^2.4.1"  # 자동 다운로드

ESP-IDF는 다음 순서로 컴포넌트를 찾습니다:

  1. main/: 메인 애플리케이션
  2. components/: 사용자 정의 컴포넌트
  3. managed_components/: 의존성 관리자 컴포넌트
  4. $IDF_PATH/components/: ESP-IDF 내장 컴포넌트

✍️ 이처럼 ESP-IDF 컴포넌트 시스템은 프로젝트 코드를 논리적인 모듈로 나누어 재사용성과 유지보수성을 크게 향상시킵니다.

컴파일(빌드)

하단 작업표시줄에 빌드 아이콘을 선택해서 빌드를 진행해보겠습니다. 빌드는 말그대로 소스 코드를 ESP32가 실행할 수 있는 기계어로 변환하는 과정입니다. (컴파일, 링크, 최적화, 바이너리 생성)

  1. idf.py build 명령을 실행하면 먼저 build 디렉터리가 생성됩니다.
  2. CMake가 실행되며, CMake는 CMakeLists.txt 파일들을 기반으로 ESP-IDF, 프로젝트 등 지정된 위치에서 모든 컴포넌트들을 찾습니다.
  3. sdkconfig 파일을 읽고, 컴포넌트별 의존성을 분석하여 컴파일에 필요한 중간 빌드 파일(예: sdkconfig.cmake, sdkconfig.h)과 Ninja 빌드 스크립트들을 build 디렉터리에 생성합니다.
  4. 빌드 도구 실행: ninja와 같은 빌드 도구를 호출합니다.
  5. 컴포넌트 빌드: ninja는 각 컴포넌트의 소스 파일(*.c, *.cpp)을 컴파일하여 개별 라이브러리 파일(*.a)을 생성합니다.
  6. 프로젝트 빌드: 모든 컴포넌트 라이브러리가 빌드되면, main 컴포넌트의 코드와 함께 이들을 링킹하여 최종 실행 파일(project.elf)을 만듭니다.
  7. 보조 파일 생성: 이 과정에서 부트로더 이미지, 파티션 테이블 등의 보조 바이너리 파일도 생성됩니다.

🤔 Ninja 가 뭐지?

Ninja는 빠르고 효율적인 빌드 시스템으로, CMake와 같은 상위 빌드 시스템에서 생성된 빌드 파일을 실행하는 데 최적화되어 있습니다. ESP-IDF 빌드 과정에서 CMake가 프로젝트를 구성하고 빌드 스크립트를 생성하면, Ninja가 이 스크립트를 실제로 실행하여 프로젝트를 컴파일하고 링크합니다.

CMakeLists.txt → CMake → Ninja → 실제 빌드

Ninja의 주요 특징

  • 속도 중심 설계: Ninja는 특히 증분 빌드(incremental builds)에서 빠른 속도를 자랑합니다. 증분 빌드는 소스 파일 일부만 변경되었을 때, 변경된 파일과 그에 의존하는 파일만 다시 빌드하는 것을 의미합니다.
  • 하위 수준의 빌드 도구: Make와 달리, Ninja는 고수준의 기능을 포함하지 않습니다. 이는 CMake와 같은 다른 도구가 생성한 단순한 빌드 파일(예: .ninja)을 빠르고 효율적으로 처리하도록 설계되었기 때문입니다.
  • 자동 병렬 빌드: Ninja는 시스템의 CPU 코어 수를 자동으로 감지하여 여러 빌드 작업을 동시에 실행합니다. 이를 통해 대규모 프로젝트의 빌드 시간을 크게 단축할 수 있습니다.

펌웨어 플래시

플래시는 빌드된 결과물을 보드에 반영하는 단계입니다. 플래시 방법에는 UART, JTAG, DFU를 지원하며, 각 방법은 사용 편의성, 속도, 디버깅 기능 등에서 차이가 있습니다.

1. UART (시리얼 통신) 플래싱

가장 일반적이고 기본적인 플래싱 방법입니다. USB-UART 변환 칩(예: CP210x, FTDI)이 내장된 개발 보드를 사용합니다. idf.py flash 명령어를 실행하면, esptool.py가 빌드된 펌웨어를 시리얼 포트를 통해 칩에 업로드합니다.

대부분의 최신 ESP32 보드는 자동 플래싱 회로를 내장하여 사용자가 부트 버튼을 누를 필요 없이 자동으로 플래싱 모드로 진입합니다.

별도의 하드웨어 없이 USB 케이블만으로 플래싱이 가능하다는 장점이 있습니다. 다만 다른 방법에 비해 속도가 느릴 수 있꼬, 플래싱 후 칩의 동작을 직접 디버깅할 수 없습니다.

2. JTAG 플래싱

JTAG 디버그 어댑터를 사용하여 펌웨어를 플래시하고, 하드웨어 디버깅을 수행합니다. OpenOCD와 JTAG 디버거(예: ESP-PROG)를 사용하여 플래시합니다. idf.py flash 또는 openocd 명령어를 직접 사용합니다.

중단점(breakpoint) 설정, 변수 검사 등 고급 디버깅 기능이 가능하며, 시리얼 통신보다 안정적인 플래싱을 제공합니다. 다만, 전용 JTAG 어댑터(예: J-Link, ESP-Prog)가 필요하고 OpenOCD를 설정하는 과정이 필요합니다.

3. DFU (Device Firmware Update) 플래싱

USB-OTG 기능을 지원하는 칩(ESP32-S2, ESP32-S3)에서 USB 케이블을 통해 직접 펌웨어를 업데이트하는 방식입니다. idf.py dfu-flash 명령을 사용하며, 칩의 USB-OTG 포트에 직접 연결합니다.

UART보다 훨씬 빠른 속도로 플래싱이 가능하며, 별도의 USB-UART 변환 칩이 필요 없습니다. 다만 ESP32-S2, ESP32-S3 등 특정 칩에서만 지원되며 플래시 암호화(flash encryption) 또는 보안 부팅(secure boot)이 활성화된 경우 DFU 기능이 비활성화될 수 있습니다.


저는 UART (시리얼 통신)를 선택해서 펌웨어 플래시를 진행하겠습니다. 그리고 시리얼 포트랑 타켓 보드를 잘 지정했는지 확인합니다. 작업 표시줄에 이렇게 있고, 변경이 가능합니다.

모니터링

모니터는 ESP32에서 실행 중인 프로그램의 출력을 실시간으로 확인하는 도구입니다. printf 출력, 로그 메시지, 에러 메시지, 디버그 정보를 볼 수 있습니다.

LED 깜빡임 실행, GPIO 설정 성공, LED 깜빡임 패턴이 정확히 출력되는 것을 알 수 있습니다.


참고로, 빌드, 플래시 디바이스, 모니터 디바이스를 한번에 실행할 수 있는 명령이 있는데 다음과 같습니다.

profile
행동하는 바보가 돼라. 생각을 즉시 행동으로 옮기는 사람이 되어라

0개의 댓글