ESP-IDFを使ってみる

条件付きコンパイルとコンパイルオプション


esp-idfではCmakeが標準のツールとして使われます。
Cmakeは非常に高機能なツールですが、あまりにも高機能すぎて使いこなせまん。
今回はよく使う条件付きコンパイルと、コンパイルオプションの設定方法を紹介ます。
ソースは以下の様に簡単なものです。
#include <stdio.h>

void app_main()
{
        printf("app_main\n");
#ifdef ENABLE1
        printf("enable1\n");
#endif
#ifdef ENABLE2
        printf("enable2\n");
#endif
}

このソースをそのままコンパイルすると、[app_main] だけが表示されます。
ENABLE1を有効にするには、プロジェクトトップのCMakeLists.txtに以下を定義します。
これでENABLE1のマクロ定義が有効となります。
# The following lines of boilerplate have to be in your project's
# CMakeLists in this exact order for cmake to work correctly
cmake_minimum_required(VERSION 3.5)

include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(version)

idf_build_set_property(COMPILE_OPTIONS "-DENABLE1" APPEND)


マクロ定義は、mainディレクトリのCMakeLists.txtに定義することもできますが、書式が変わります。
set(srcs "main.c")

set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -D ENABLE1")

idf_component_register(SRCS "${srcs}"
                       INCLUDE_DIRS ".")



esp-idfのコンパイルは標準で[-Werror=maybe-uninitialized]のオプションが有効になっています。
初期値を設定していない変数を使うと以下の様にコンパイルエラーとなります。
error: 'rc' may be used uninitialized in this function [-Werror=maybe-uninitialized]

本来は初期値を設定するのが定石ですが、github上のライブラリをコンポーネントとして取り込む時などは、ソースを直すのは面倒です。
この様なときも、プロジェクトトップのCMakeLists.txtに以下を定義すればエラーを回避することができます。
# The following lines of boilerplate have to be in your project's
# CMakeLists in this exact order for cmake to work correctly
cmake_minimum_required(VERSION 3.5)

include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(version)

idf_build_set_property(COMPILE_OPTIONS "-Wno-maybe-uninitialized" APPEND)




esp-idf v5.0-dev-4037 辺りからデフォルトのコンパイルオプションが変更になり、今まで通っていたソースで、以下のコンパイルエラーが出るようになりました。
error: argument 2 of type 'uint8_t[]' {aka 'unsigned char[]'} with mismatched bound [-Werror=array-parameter=]

本来は変数の型を修正するのが定石ですが、uint8_t と unsigned char は実質同じ8ビットの符号なし整数値です。
また、github上のライブラリをコンポーネントとして取り込む時などは、ソースを直すのは面倒です。
この様なときも、プロジェクトトップレベルのCMakeLists.txtに以下を定義すればエラーを回避することができます。
# The following lines of boilerplate have to be in your project's
# CMakeLists in this exact order for cmake to work correctly
cmake_minimum_required(VERSION 3.5)

include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(version)

idf_build_set_property(COMPILE_OPTIONS "-Wno-array-parameter" APPEND)




esp-idfのコンパイルは標準で[-Og]のオプティマイズが使用されます。
ESP32は通常の分岐命令の他に[zero-overhead loop]と呼ばれる命令を持っています。
これを有効にするには、プロジェクトトップのCMakeLists.txtにオプティマイズオプションを変更します。
[-O2]オプションでビルドすると、かなり高速になります。
# The following lines of boilerplate have to be in your project's
# CMakeLists in this exact order for cmake to work correctly
cmake_minimum_required(VERSION 3.5)

include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(version)

idf_build_set_property(COMPILE_OPTIONS "-O2" APPEND)




ESP32シリーズの中で、USBをサポートしているのはesp32s2/s3だけです。
また、clasic bluetoothをサポートしているのはesp32だけです。
以下の様に指定することで、特定のターゲット以外のビルドを抑制することができます。
idf_component_register(SRCS "main.c"
                       INCLUDE_DIRS "."
                       REQUIRED_IDF_TARGETS esp32s2 esp32s3)

この状態で、例えばesp32c3でビルドすると以下のエラーとなります。
CMake Error at /home/nop/esp-idf/tools/cmake/component.cmake:310 (message):
  Component main only supports targets: esp32s2;esp32s3
Call Stack (most recent call first):
  /home/nop/esp-idf/tools/cmake/component.cmake:428 (__component_check_target)
  main/CMakeLists.txt:1 (idf_component_register)

esp32s2だけはbleをサポートしていません。
REQUIRED_IDF_TARGETSは、リストされたターゲットだけを許可する指定ですが、
残念ながら、リストされたターゲットだけを不許可にする指定は有りません。
許可するターゲットを列挙するしかありませんが、サポートするターゲットが増えた時は修正する必要があります。
ソースコードの中でターゲットを判定することで、特定のSoCの動作を抑制する事ができます。
#include <stdio.h>

void app_main()
{
#ifdef CONFIG_IDF_TARGET_ESP32S2
#error Unsupported SoC.
#endif
      printf("Hello\n");
}



esp-idfでは、esp32/esp32s2/esp32s3/esp32h2/esp32c2/esp32c3などのSoCをサポートして いま す。
今後もesp32c5/esp32c6などの追加が予定されています。
それぞれのSoCで微妙に挙動が違うことが有り、そのようなときは、ソースコードをSoCに応じて振り分ける必要が有ります。
ソースコードの中でターゲットを判定することで、SoCごとにコードを振り分けることができます。
#include <stdio.h>
#include <inttypes.h>
#include "rom/ets_sys.h" // ets_get_cpu_frequency()

void app_main()
{
#ifdef CONFIG_IDF_TARGET_ESP32
    printf("Core is ESP32@%"PRIu32"Mhz\n", ets_get_cpu_frequency());
#elif defined CONFIG_IDF_TARGET_ESP32S2
    printf("Core is ESP32S2@%"PRIu32"Mhz\n", ets_get_cpu_frequency());
#elif defined CONFIG_IDF_TARGET_ESP32S3
    printf("Core is ESP32S3@%"PRIu32"Mhz\n", ets_get_cpu_frequency());
#elif defined CONFIG_IDF_TARGET_ESP32C2
    printf("Core is ESP32C2@%"PRIu32"Mhz\n", ets_get_cpu_frequency());
#elif defined CONFIG_IDF_TARGET_ESP32C3
    printf("Core is ESP32C3@%"PRIu32"Mhz\n", ets_get_cpu_frequency());
#endif
}

関数にすることで、大きな違いにも対応することができます。
#include <stdio.h>
#include <inttypes.h>
#include "rom/ets_sys.h" // ets_get_cpu_frequency()

#ifdef CONFIG_IDF_TARGET_ESP32
void esp32_hoge(void)
{
    printf("Core is ESP32@%"PRIu32"Mhz\n", ets_get_cpu_frequency());
}
#elif defined CONFIG_IDF_TARGET_ESP32S2
void esp32s2_hoge(void)
{
    printf("Core is ESP32S2@%"PRIu32"Mhz\n", ets_get_cpu_frequency());
}
#elif defined CONFIG_IDF_TARGET_ESP32S4
void esp32s3_hoge(void)
{
    printf("Core is ESP32S3@%"PRIu32"Mhz\n", ets_get_cpu_frequency());
}
#elif defined CONFIG_IDF_TARGET_ESP32C2
void esp32c2_hoge(void)
{
    printf("Core is ESP32C2@%"PRIu32"Mhz\n", ets_get_cpu_frequency());
}
#elif defined CONFIG_IDF_TARGET_ESP32C3
void esp32c3_hoge(void)
{
    printf("Core is ESP32C3@%"PRIu32"Mhz\n", ets_get_cpu_frequency());
}
#endif

void app_main()
{
#ifdef CONFIG_IDF_TARGET_ESP32
    esp32_hoge();
#elif defined CONFIG_IDF_TARGET_ESP32S2
    esp32s2_hoge();
#elif defined CONFIG_IDF_TARGET_ESP32S3
    esp32s3_hoge();
#elif defined CONFIG_IDF_TARGET_ESP32C2
    esp32c2_hoge();
#elif defined CONFIG_IDF_TARGET_ESP32C3
    esp32c3_hoge();
#endif
}

さらにCMakeLists.txtの中でターゲットを判定することができます。
Cmake時のターゲットの判定は、こ ちらのサンプルで多用されていて、SoC毎にリンクするファイルを変えています。
$ cat CMakeLists.txt
set(srcs "main.c")

if(IDF_TARGET STREQUAL "esp32")
    list(APPEND srcs "hoge_esp32.c")
elseif(IDF_TARGET STREQUAL "esp32s2")
    list(APPEND srcs "hoge_esp32s2.c")
elseif(IDF_TARGET STREQUAL "esp32s3")
    list(APPEND srcs "hoge_esp32s3.c")
elseif(IDF_TARGET STREQUAL "esp32c2")
    list(APPEND srcs "hoge_esp32c2.c")
elseif(IDF_TARGET STREQUAL "esp32c3")
    list(APPEND srcs "hoge_esp32c3.c")
endif()

idf_component_register(SRCS "${srcs}" INCLUDE_DIRS ".")

SoC毎にリンクするファイルを変えると、ソースコードの中でSoCを判定する必要は無くなり、ソースコードは単純になります。
但し、慎重に使わないと間違いの元にもなります。
#include <stdio.h>
void hoge(void);

void app_main()
{
    hoge();// リンクされているソースはSoC毎に異なる
}

hoge() fuga() piyo() の3つの関数の内容をSoC毎に分けるときも、コードは簡単になります。
#include <stdio.h>
void hoge(void);

void app_main()
{
    hoge();
    fuga();
    piyo();
}

CMakeLists.txtの中では、CONFIG値を判定することができます。
CONFIG値の判定はこ ちらのサンプルで多用されています。



Kconfig.projbuildのchoiceに応じて、リンクするソースを変えるときは以下の様になります。
まず、Kconfig.projbuildで以下のchoiceを定義します。
menu "Application Configuration"

    choice TYPE
        prompt "Application Type"
        default TYPE1
        help
            Select Application Type.
        config TYPE1
            bool "Type1"
            help
                Type1.
        config TYPE2
            bool "Type2"
            help
                Type2.
    endchoice

endmenu

menuconfigを実行すると、以下のいずれかのbool変数が有効になります。
#
# Application Configuration
#
CONFIG_TYPE1=y
# CONFIG_TYPE2 is not set
# end of Application Configuration

CMakeLists.txtでは有効になっているbool変数を判定し、srcs変数にソースファイルを追加します。
set(srcs "main.c")

if (CONFIG_TYPE1)
    list(APPEND srcs "type1.c")
elseif (CONFIG_TYPE2)
    list(APPEND srcs "type2.c")
endif()

idf_component_register(SRCS "${srcs}" INCLUDE_DIRS ".")

main.cでは単純に1つのタスクを起動します。
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"

void task(void *pvParameter);

void app_main()
{
    xTaskCreate(&task, "TASK", 2048, NULL, 2, NULL);
}

type1.c type2.c を用意します。
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"

void task(void *pvParameter)
{
    printf("This is type1\n");
    vTaskDelete( NULL );
}

これで、choiceに応じたタスク(type1.c/type2.c)が実行されます。
I (333) main_task: Calling app_main()
This is type1
I (333) main_task: Returned from app_main()



CMakeLists.txtの中でターゲットを判定して、SoCごとにデファイン定義を変えることができます。
$ cat CMakeLists.txt
set(srcs "main.c")

if(IDF_TARGET STREQUAL "esp32")
    list(APPEND srcs "esp32.c")
    set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -D ESP32")
elseif(IDF_TARGET STREQUAL "esp32s2")
    list(APPEND srcs "esp32s2.c")
    set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -D ESP32S")
elseif(IDF_TARGET STREQUAL "esp32s3")
    list(APPEND srcs "esp32s3.c")
    set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -D ESP32S")
elseif(IDF_TARGET STREQUAL "esp32c2")
    list(APPEND srcs "esp32c2.c")
    set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -D ESP32C")
elseif(IDF_TARGET STREQUAL "esp32c3")
    list(APPEND srcs "esp32c3.c")
    set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -D ESP32C")
endif()

idf_component_register(SRCS "${srcs}" INCLUDE_DIRS ".")

この様にするとソースコードの中で、デファイン定義を判定することができます。
#include <stdio.h>
void hoge(void);

void app_main()
{
#if defined(ESP32)
    esp32();
#elif defined(ESP32S)
    esp32s();
#elif defined(ESP32C)
    esp32c();
#endif
}

ESP32S2/S3だけがUSBをサポートしているので、以下の様にESP32S2/S3のときだけ、ENABLE_USBを定義することがで きます。
$ cat CMakeLists.txt
set(srcs "main.c")

if(IDF_TARGET STREQUAL "esp32s2")
    set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -D ENABLE_USB")
elseif(IDF_TARGET STREQUAL "esp32s3")
    set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -D ENABLE_USB")
endif()

idf_component_register(SRCS "${srcs}" INCLUDE_DIRS ".")

ESP32S2だけがbleをサポートしていないので、否定を使って、ESP32S2以外の時だけ、ENABLE_BLEを定義することができま す。
$ cat CMakeLists.txt
set(srcs "main.c")

if(NOT IDF_TARGET STREQUAL "esp32s2")
    set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -D ENABLE_BLE")
endif()

idf_component_register(SRCS "${srcs}" INCLUDE_DIRS ".")



ESP32はXTENSAシリーズとRISCVシリーズで全く挙動が違うことが有ります。
XTENSAでは、i2cとspiのポートはどちらも2つ使うことができますが、RISCVではどちらも1つしかありません。
CMakeLists.txtの中で CONFIG_IDF_TARGET_ARCH_RISCV/CONFIG_IDF_TARGET_ARCH_XTENSAにより、これらを区別 することができます。
以下の様にするとXTENSAのESP32だけビルドすることができます。
if(CONFIG_IDF_TARGET_ARCH_RISCV)
    message(FATAL_ERROR "RISCV targets not supported")
endif()

RISCVのESP32をターゲットとしてビルドすると以下のエラーとなります。
CMake Error at main/CMakeLists.txt:4 (message):
  RISCV targets not supported

ソースコードの中でこの定義を使うこともできます。
#include <stdio.h>

#ifdef CONFIG_IDF_TARGET_ARCH_RISCV
#error RISCV targets not supported
#endif


RISCVのESP32をターゲットとしてビルドすると以下のエラーとなります。
/home/nop/rtos/test/main/main.c:9:2: error: #error RISCV targets not supported
    9 | #error RISCV targets not supported



Cmakeではディレクトリの作成やファイルのダウンロードを行うことができます。
以下の様にするとspiffsディレクトリを作成し、指定されたURLからファイルをダウンロードすることができます。
このテクニックはこ ちらで使われています。
Cmakeでの主要なファイル操作は、こちらに 公開されています。
# Download the example font into a directory "spiffs" in the build directory
set(URL "https://github.com/espressif/esp-docs/raw/f036a337d8bee5d1a93b2264ecd29255baec4260/src/esp_docs/fonts/DejaVuSans.ttf")
set(FILE "DejaVuSans.ttf")
set(SPIFFS_DIR "../spiffs")
file(MAKE_DIRECTORY ${SPIFFS_DIR})
file(DOWNLOAD ${URL} ${SPIFFS_DIR}/${FILE} SHOW_PROGRESS)

Cmakeの中ではいくつかのディレクトリを示す変数を使うことができます。
こちらに その変数の一覧と使い方が紹介されています。



esp-idf v4.xとv5.xでは、様々なAPIや定数が変わりました。
バージョンを判定してソース内のコードを振り分けることもできますが、 ADC関連のAPIなどは全く別物になり、v4.xとv5.xではほとんど別のコードになります。
以下の様に、CMakeLists.txtで、v4.xとv5.xではリンクするファイルを分けた方が簡単な場合も有ります。
set(srcs "main.c")

if (IDF_VERSION_MAJOR STREQUAL "4")
    list(APPEND srcs "ver4.c")
elseif (IDF_VERSION_MAJOR STREQUAL "5")
    list(APPEND srcs "ver5.c")
endif()

idf_component_register(SRCS ${srcs} INCLUDE_DIRS ".")

esp-idfのマイナーバージョンまで判断してソースを切り替える場合、以下の様にidf_version変数にメジャー番号とマ イナー番号を設定すると
マイナーバージョンでリンクするファイルを分けることができます。
このテクニックはこ ちらのプロジェクトで使われています。
VERSION_GREATER_EQUALはメジャーバージョン、マイナーバージョンだけでなく、パッチバージョンまで判定することができま す。
こ ちらにパッチバージョンまで判定している例が公開されていますが、パッチバージョンまで判定することは稀です。
# get IDF version for comparison
set(idf_version "${IDF_VERSION_MAJOR}.${IDF_VERSION_MINOR}")

set(srcs "main.c")

if(idf_version VERSION_GREATER_EQUAL "5.2")
    list(APPEND srcs "ver5.2.c")
else()
    list(APPEND srcs "original.c")
endif()

idf_component_register(SRCS "${srcs}" INCLUDE_DIRS ".")

続く....