2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Windows/WSLでESP32向けAtomVM開発環境をさっと作る 2026Ver

2
Posted at

環境設定

WSL2

今回利用するESP-IDF/AtomVMの環境ではLinuxが必要なためWSL2を用いる.

usb / usbipd

WSL2でマイコンに接続するためusbipd(https://github.com/dorssel/usbipd-win)を利用.
GUIツール(https://github.com/nickbeth/wsl-usb-manager)が便利.

デフォルトではUSBシリアルデバイスがルート権限になっており,都度chmodが必要でめんどい.
ESP32シリーズの主要なUSBシリアルチップをざっくり登録してしまう.
またユーザをdialoutグループに追加しておく.

sudo usermod -aG dialout $USER
sudo nano /etc/udev/rules.d/99-esp32.rules
/etc/udev/rules.d/99-esp32.rules
# CP210x (多くのESP32開発ボード)
SUBSYSTEMS=="usb", ATTRS{idVendor}=="10c4", ATTRS{idProduct}=="ea60", MODE="0666", GROUP="dialout"

# CH340 / CH341
SUBSYSTEMS=="usb", ATTRS{idVendor}=="1a86", ATTRS{idProduct}=="7523", MODE="0666", GROUP="dialout"
SUBSYSTEMS=="usb", ATTRS{idVendor}=="1a86", ATTRS{idProduct}=="55d4", MODE="0666", GROUP="dialout"

# FTDI FT232
SUBSYSTEMS=="usb", ATTRS{idVendor}=="0403", ATTRS{idProduct}=="6001", MODE="0666", GROUP="dialout"
SUBSYSTEMS=="usb", ATTRS{idVendor}=="0403", ATTRS{idProduct}=="6010", MODE="0666", GROUP="dialout"

# ttyUSB / ttyACM 全般(上記に漏れがあった場合の保険)
KERNEL=="ttyUSB[0-9]*", MODE="0666", GROUP="dialout"

設定を反映するためWSLを再起動しておく.PS等でwsl --shutdown

ESP-IDF

EspressifのESPIDFインストーラのeimを入れ,5.x系の最新版をインストール

echo "deb [trusted=yes] https://dl.espressif.com/dl/eim/apt/ stable main" | sudo tee /etc/apt/sources.list.d/espressif.list
sudo apt update
sudo apt install eim-cli
eim install -i v5.5.4

activateコマンドが長くて面倒なので,bashrc等に適当なaliasを作っておく.(以降getidfとする)

alias getidf='source /home/hosoai/.espressif/tools/activate_idf_v5.5.4.sh'

VSCode

WSL内から起動し,適当なプロファイルを作って,Espressifの拡張を入れておく.お好みのエージェントも入れておく.

Elixir / Erlang / Reber3

いつの間にかasdfではなくmiseというツールに変わったらしい

curl https://mise.run | sh

インストール後の指示に従いbash等に追加しとく.

順番が前後するが,以降のAtomVMをCloneしたフォルダにmise.tomlを作成して,インストールする.
なお,AtomVMの現時点(2026/06/06)でサポートされている最新環境を指定している.AtomVMのバージョンが上がっていたら適宜修正を.

mise.toml
[tools]
erlang = '27'
elixir = '1.17'
rebar = '3.27.0'
mise install

AtomVM

まずはソースをClone

git clone https://github.com/atomvm/AtomVM

Unix/Tool build

AtomVMのコマンド類やもろもろのベースとなるものをビルド,インストール

cd AtomVM
mkdir build
cd build
cmake ..
make -j 8
sudo make install

ESP32向けFirmwareのビルド

とりあえずデフォルトのままビルドしてみる

cd src/platform/esp32
getidf
(->envに入る)
idf.py set-target esp32s3
idf.py reconfigure
idf.py build

出来上がったイメージを書き込み,シリアルコンソールで接続する.
今回はAtomS3で試している.AtomS3を繋ぎ,usbipdで認識したポートをbind/attachしておく.
以下で書き込みし,シリアルで繋ぐ.書き込みできない場合はWSL内で/dev/ttyACM* or ttyUSB*があるか,権限が合っているか確認する.

idf.py flash
idf.py monitor

AtomVMのロゴが出てればとりあえずおk,メインのアプリは入ってないのでたぶんなんも動かん.

AtomVMのプラットフォームイメージのカスタマイズ

上まではメモをベースにパッと書いたものだが,以降はClaudeさんがGitのコミットログからいい感じに仕上げてくれたんで,そのまま使う.今回はAtomVM上でZenoh-picoを動作させることを目的としている.
お試しで作成した環境はこちら https://github.com/s-hosoai/AtomVM_Zenoh

AtomVM に外部ライブラリを統合する手順

以下の手順は Zenoh-pico (C) と M5Unified (C++) の2例から抽出した一般的なパターンです。

ステップ 0:前提環境の確認

# ツールバージョン管理 (mise を推奨)
# プロジェクトルートに mise.toml を置く
[tools]
erlang = '27'
elixir = '1.17'
rebar = '3.27.0'
gleam = '1.17.0'   # Gleam を使う場合

mise install

# ESP-IDF (v5.4 以上推奨)
source ~/.espressif/tools/activate_idf_v5.x.x.sh
idf.py --version

ステップ 1: ライブラリを third_party/ またはサブモジュールとして追加

# git submodule として追加する場合
git submodule add https://github.com/<ライブラリ>/repo.git \
  src/platforms/esp32/third_party/<libname>
git submodule update --init --recursive

.gitmodules に自動追記されます。

ライブラリが ESP-IDF 公式コンポーネント(idf_component.yml で管理)の場合は、
コンポーネント側の idf_component.yml に依存を書くだけでサブモジュール不要です(M5Unified の例)。

ステップ 2: ESP-IDF コンポーネントディレクトリを作成

src/platforms/esp32/components/<コンポーネント名>/
├── CMakeLists.txt        ← 必須
├── Kconfig               ← 有効化フラグ(任意だが推奨)
├── <nif_impl>.c          ← NIF 実装
└── [追加ファイル]

CMakeLists.txt の基本形(純 C ライブラリの場合)

# COMPONENT_DIR は ESP-IDF が自動設定するコンポーネントの絶対パス
# CMake の requirements フェーズでも正しく解決される唯一の変数
set(LIB_DIR "${COMPONENT_DIR}/../../third_party/<libname>")

# ライブラリのソースを列挙
file(GLOB_RECURSE LIB_SRCS "${LIB_DIR}/src/**/*.c")

idf_component_register(
    SRCS "<nif_impl>.c" ${LIB_SRCS}
    INCLUDE_DIRS "${LIB_DIR}/include"
    PRIV_REQUIRES "libatomvm" "avm_sys" "esp_system" "driver"
    WHOLE_ARCHIVE   # IDF v5+ では NIF 登録に必要
)

target_compile_definitions(${COMPONENT_LIB} PRIVATE
    SOME_BACKEND_FLAG=1
)

⚠️ CMake パス注意: requirements フェーズ中は CMAKE_CURRENT_SOURCE_DIR
CMAKE_SOURCE_DIRビルドツリーのパスに解決されて誤動作します。
サードパーティライブラリのパス解決には必ず COMPONENT_DIR を使ってください。

CMakeLists.txt の基本形(C++ ライブラリの場合)

idf_component_register(
    SRCS
        "nif_impl.cpp"   # C++ ラッパー(AtomVM ヘッダを include しない)
        "nif_reg.c"      # NIF 登録(AtomVM ヘッダを include する)
    PRIV_REQUIRES
        "libatomvm"
        "avm_sys"
        "<cpp_library_component>"
    WHOLE_ARCHIVE
)

target_compile_features(${COMPONENT_LIB} INTERFACE cxx_std_17)

⚠️ C++ NIF の分離パターン: AtomVM の term.h#ifndef __cplusplus ガードがあり
C++ ファイルから直接 include できません。NIF 実装を .cpp(C++ ライブラリ呼び出し)と
.c(AtomVM ヘッダ + NIF テーブル登録)の2ファイルに分けてください。

Kconfig(有効化フラグ)

menu "AtomVM <LibName> Component"

config AVM_ENABLE_<LIBNAME>_NIFS
    bool "Enable <LibName> NIFs"
    default y
    help
        Enable NIFs for <LibName>.

endmenu

ステップ 3: NIF 実装を書く

基本テンプレート(C NIF)

#include <sdkconfig.h>
#ifdef CONFIG_AVM_ENABLE_<LIBNAME>_NIFS

#include "context.h"
#include "defaultatoms.h"
#include "erl_nif.h"
#include "erl_nif_priv.h"
#include "interop.h"
#include "memory.h"
#include "nifs.h"
#include "portnifloader.h"
#include "term.h"

// --- NIF リソース型の定義 ---
typedef struct {
    SomeLibHandle handle;
    bool is_open;
} MyLibResource;

static ErlNifResourceType *my_lib_resource_type;

// デストラクタ(GC 時に自動呼び出し)
static void my_lib_resource_dtor(ErlNifEnv *env, void *obj)
{
    UNUSED(env);
    MyLibResource *res = (MyLibResource *) obj;
    if (res->is_open) {
        some_lib_close(res->handle);
        res->is_open = false;
    }
}

static const ErlNifResourceTypeInit MyLibResourceTypeInit = {
    .members = 1,
    .dtor = my_lib_resource_dtor,
};

// --- NIF 関数 ---
static term nif_my_lib_open(Context *ctx, int argc, term argv[])
{
    UNUSED(argc);
    // 引数チェック
    if (!term_is_binary(argv[0])) {
        RAISE_ERROR(BADARG_ATOM);
    }

    // リソースを AtomVM ヒープに確保
    MyLibResource *res = enif_alloc_resource(my_lib_resource_type, sizeof(MyLibResource));
    if (!res) {
        RAISE_ERROR(OUT_OF_MEMORY_ATOM);
    }

    // ライブラリ初期化
    res->handle = some_lib_open(...);
    res->is_open = true;

    // {ok, Resource} を返す
    term resource_term = enif_make_resource(ctx->global->erl_nif_env, res);
    enif_release_resource(res);

    term ok_tuple[2] = { OK_ATOM, resource_term };
    return term_alloc_tuple(2, &ctx->heap);
    // ※ 実際は memory.h の API でタプルを構築
}

// --- NIF テーブル登録 ---
static const struct Nif my_lib_open_nif = {
    .base.type = NIFFunctionType,
    .nif_ptr = nif_my_lib_open
};

static const struct Nif *my_lib_nif_get_nif(const char *nifname)
{
    if (strcmp("my_lib:open/1", nifname) == 0) { return &my_lib_open_nif; }
    // ... 他の NIF を追加
    return NULL;
}

REGISTER_NIF_COLLECTION(my_lib, NULL, NULL, my_lib_nif_get_nif)

#endif // CONFIG_AVM_ENABLE_<LIBNAME>_NIFS

よく使う AtomVM API

用途 API
バイナリ引数をチェック term_is_binary(argv[N])
整数引数をチェック term_is_integer(argv[N])
バイナリを C 文字列に変換 interop_binary_to_string(term) ※ free() 必要
整数値を取得 term_to_int(term)
ok アトムを返す return OK_ATOM
エラーを発生させる RAISE_ERROR(BADARG_ATOM)
リソース確保 enif_alloc_resource(type, size)
リソース解放(参照カウント) enif_release_resource(res)
リソース → term enif_make_resource(env, res)

ステップ 4: Elixir/Gleam ラッパーを追加

Elixir ラッパー (libs/exavmlib/lib/MyLib.ex)

defmodule MyLib do
  @compile {:no_warn_undefined, [:my_lib]}

  @doc "セッションを開く"
  def open(config) when is_binary(config), do: :my_lib.open(config)

  @doc "データを送信する"
  def send(session, key, payload) when is_binary(key) and is_binary(payload) do
    :my_lib.send(session, key, payload)
  end

  @doc "セッションを閉じる"
  def close(session), do: :my_lib.close(session)
end

libs/exavmlib/lib/CMakeLists.txt に追記:

target_sources(exavmlib INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/MyLib.ex)

Gleam バインディング (libs/gleam_avm/src/gleam_avm/my_lib.gleam)

pub type Session

@external(erlang, "my_lib", "open")
pub fn open(config: String) -> Result(Session, Nil)

@external(erlang, "my_lib", "send")
pub fn send(session: Session, key: String, payload: BitArray) -> Result(Nil, Nil)

@external(erlang, "my_lib", "close")
pub fn close(session: Session) -> Nil

ステップ 5: ビルド設定の確認

sdkconfig / パーティション

フラッシュ容量・スタックサイズ・ヒープの調整が必要な場合:

# src/platforms/esp32/sdkconfig.defaults に追記
CONFIG_ESP32S3_DEFAULT_CPU_FREQ_240=y
CONFIG_SPIRAM_USE_MALLOC=y
# ... ライブラリ固有の設定

フラッシュレイアウト (partitions.csv):

パーティション オフセット 用途
factory 0x10000 AtomVM VM 本体
boot.avm 0x1D0000 ブート AVM
main.avm 0x250000 ユーザーアプリ ← ここに書き込む

ビルド & フラッシュ

cd src/platforms/esp32
getidf                    # ESP-IDF 環境を有効化

idf.py set-target esp32s3
idf.py reconfigure        # コンポーネント変更後は必須
idf.py build
idf.py flash

# アプリのビルド(プロジェクトルートの build/ で)
make MyApp

# アプリのフラッシュ(アドレスは 0x250000 固定)
esptool.py --chip esp32s3 --port /dev/ttyACM0 \
  write_flash 0x250000 build/examples/elixir/esp32/MyApp.avm

3. Zenoh 統合の補足事項

ハマりやすい落とし穴

raweth スタブ問題

zenoh-pico はRaw Ethernet機能を無効にしても、tx.c から _z_raweth_send_* シンボルを無条件参照します。ライブラリ側に #else スタブがないため、raweth_stubs.c でスタブを提供する必要があります。

// raweth_stubs.c
#include "zenoh-pico/transport/raweth/tx.h"
#include "zenoh-pico/utils/result.h"

#if Z_FEATURE_RAWETH_TRANSPORT != 1
z_result_t _z_raweth_send_t_msg(...) { return _Z_ERR_TRANSPORT_NOT_AVAILABLE; }
z_result_t _z_raweth_send_n_msg(...) { return _Z_ERR_TRANSPORT_NOT_AVAILABLE; }
#endif

AtomVM GC によるセッション解放問題

末尾再帰ループでセッション変数を引数として渡すとき、コンパイラがセッション参照を最適化で消去することがあります。GC がセッションをデストラクトし、次の通信でクラッシュします。

解決策: セッションをループの引数として明示的に渡し続ける。

# NG: session が GC される可能性がある
defp loop(pub, count) do
  Zenoh.publisher_put(pub, "data")
  loop(pub, count + 1)  # session への参照がない
end

# OK: session も引数に含める
defp loop(session, pub, count) do
  Zenoh.publisher_put(pub, "data")
  loop(session, pub, count + 1)  # session への参照を維持
end

スタック上の大きな構造体によるクラッシュ

AtomVM のスケジューラタスクはスタックサイズが限られています(約 4KB 程度)。ZenohMessage のような大きな構造体(keyexpr 256 + payload 4096 = ~4.4KB)をスタックに置くと MMU エントリフォルトでクラッシュします。

解決策: malloc() でヒープに確保し、free() で解放。

// NG: スタックに確保(クラッシュの原因)
ZenohMessage msg;

// OK: ヒープに確保
ZenohMessage *msg = malloc(sizeof(ZenohMessage));
if (!msg) { /* エラー処理 */ }
// ... 使用後 ...
free(msg);

UDP マルチキャスト/IPv6 の無効化

ESP-IDF の LwIP はデフォルトで UDP マルチキャストと IPv6 が制限されています。zenoh_generic_config.h でこれらを明示的に無効化します。

#define Z_FEATURE_RAWETH_TRANSPORT 0
#define Z_FEATURE_LINK_TCP 1
#define Z_FEATURE_LINK_BLUETOOTH 0

動作確認手順

# PC 側で Zenoh ルーターを起動
docker run --network host eclipse/zenoh

# ESP32 側でサブスクライバーを起動してモニタ
esptool.py ... write_flash 0x250000 ZenohSub.avm
idf.py monitor

# PC 側からパブリッシュして受信確認
# (z_pub や zenoh-cli などのツールを使用)

4. ディレクトリ構成まとめ

src/platforms/esp32/
├── components/
│   ├── avm_m5unified/          ← M5Unified NIF (C++ + C 分離パターン)
│   │   ├── CMakeLists.txt
│   │   ├── Kconfig
│   │   ├── idf_component.yml   ← idf_component_manager 依存
│   │   ├── m5unified_nif.cpp   ← C++ ラッパー(AtomVM ヘッダ不使用)
│   │   └── m5unified_reg.c     ← NIF 登録(AtomVM ヘッダ使用)
│   └── avm_zenoh/              ← Zenoh NIF (純 C + submodule パターン)
│       ├── CMakeLists.txt
│       ├── Kconfig
│       ├── zenoh_nif.c
│       ├── raweth_stubs.c      ← 未定義シンボルスタブ
│       └── zenoh_config/
│           └── zenoh_generic_config.h  ← カスタム設定
└── third_party/
    └── zenoh-pico/             ← git submodule

libs/
├── exavmlib/lib/
│   ├── M5.ex                   ← Elixir ラッパー
│   └── Zenoh.ex
└── gleam_avm/src/gleam_avm/
    ├── m5.gleam                ← Gleam バインディング
    └── zenoh.gleam

examples/elixir/esp32/
├── M5Blink.ex
├── ZenohPub.ex
└── ZenohSub.ex

なお,サンプルを利用する場合は,Zenoh RouterのIPアドレス,WiFiのSSID/Passwordを書き換えてからビルドして利用してください.ほとんどコード書いてないけど,いつの間にかAtomVM上からZenoh-picoを叩いてPub/Subすることができました.

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?