はじめに
これまで主に Nerves を使って Elixir での応用例を積み重ねてきました。AtomVM はウォッチしている程度でしたが、【名古屋】Davide BettioさんとAtomVMを囲む会に参加する機会があって、ようやく使い始めてみました。例によってハマりました。ここでは ESP32 に AtomVM で Lチカと Hello world をさせた経験を記録しておきます。
背景
AtomVM は資源の乏しいマイコンクラスの計算機でもいごく Elixir 処理系です。通常の Elixir がしっかりした Linux で、Nerves はコンパクトと言っても組込み用 Linux の buildroot ベースの規模ですから、AtomVM は大変コンパクトな実行バイナリを吐くということです。
組込み界隈で IoT や産業用途の IIoT にkochi.ex では Nerves を勧めてます。しかしながら、組込み屋さんの一部にはマイコンでいごくのが必須という需要もあります。そういう向きに Elixir を勧めるにはAtomVM が向いていそうです。
AtomVM についての最も古い Qiita の記事は @takasehideki さんの ElixirでIoT#3.1:ESP32やSTM32でElixirが動く!AtomVMという選択肢 と思います。だいぶ古くからありますね。このころはまだドキュメントが少なく、当時の公式ドキュメントも開発ガチ勢じゃないとハードル高そうな印象でした。
それがこのところドキュメントもかなり整備されてきて、Qiita だけでもそこそこ記事が出てきてます。前出の会合直前の晩に「まあこれぐらい公式ドキュメントと日本語記事があればまあLチカぐらいは半日ぐらいで出来るんじゃね」とチャレンジしたところ8時間溶かしてもできない… 公式ドキュメント通りでも AtomVM: Elixir で ESP32-S3 の LED を光らせる (2025年8月) などを読んでもできない。何なら ChatGPT5.2 と Gemini3 に聞いてもうまく行きません。Hello World ができたのが会合の終わった深夜の名古屋のホテルで、Lチカができたのがその翌日というありさまでした。
出来上がった後でわかったのは、少なくともこの時点では公式ドキュメントと出回ってる記事だけではすんなりとはLチカに到達できなくて、ちょっとしたスパイスが必要ということです。この状況はたまたま今だけなのかも知れません。後日になれば余計な雑音かもしれませんので、読んでトライする方は注意をお願いします。
ハードウェア
ESP32 はどのチップ (SoC) か、どのモジュールか、どのボードかで無数のバリエーションがあって混乱します。ESP32 以外の他のアーキテクチャでもそうなんですが、特に ESP32 は複雑に感じます。(一緒によく出てくる STM32 も SoC がやたらと多いですが、手に入るのが限られてたりして ESP32 よりひどくない気がします)
今回用いたボード(モジュール)は ESP32DevKitC (ESP32-WROOM-32D) で、搭載している SoC は ESP32(ESP32-D0WD)という古典的なものです。あと、ESP32DevKitC は開発用のブレッドボード SideBBForESP32DevKitC を使いました。これちょっと工作するには神ボードで、残念ながら記事を書いてる時点では在庫無しです。

では公式ドキュメントの Getting Started on the ESP32 platformを読みなが進めます。
環境を整える
ESP32で開発する条件が ESP32 Requirements にあります。
今回母艦は Macbook Pro (Apple M1 チップ) の MacOS 26.2 を用いました。書いてある大概の物はすでにありました。esptool と screen を brew で入れてあります。あと Elixir/Erlang は mise で入れてあります。
mise ls
Tool Version Source Requested
elixir 1.19.5-otp-27 ~/.config/mise/config.toml latest
erlang 27.3.4.6 ~/.config/mise/config.toml 27.3.4
今回用いた AtomVM は安定版の最新である 0.6.6 です。これが対象とする OTP は 27 です。最新の 28 だとうまく行きません。 ハマりどころその1です1。
バイナリの位置
つぎに ESP32 Deployment Overview を読んで全体像を掴みます。
このドキュメントにははっきり書かれていませんが、どちらも ESP32 のアーキテクチャでアドレスが変わることに注意してください。今回用いたボード ESP32DevKitC のモジュール ESP32 では以下のアドレスになります。特に後者は 0x210000 と間違えやすいです。ハマりどころその2です。
- 0x1000: AtomVM binary image
- 0x250000: Erlang/Elixir application
USBケーブル
ESP32 へのソフトウェアのインストールは USBケーブルを使います。これはドキュメント Connecting the ESP32 device にごく簡潔に書いてあります。
ESP32DevKitC の場合は mini-USB のコネクタが必要ですので注意です。母艦側はマシンに合わせて USB-A/USB-C を用意します。この USB ケーブルは(Nerves のときとか組込み一般にそうですが)トラブルのもとです。電源しか繋がってないとか通信しか繋がってないとかあります。ハマりどころその3です。
慣れてないときは異なる種類のケーブルを数本用意して、うまく行かないときは替えてみてください。そしてうまく行かないケーブルは二度と間違いが起こらないように捨ててしまいましょう。
LEDをつなぐ
ESP32DevKitC のLEDは電源ONしかありません。GPIO出力にLEDを繋ぐ必要があります。
回路は Blijnky Application にあります。
+-----------+
| | 330 ohm
| IO2 o--- \/\/\/\ ---+
| | resistor |
| | |
| | |
| | |
| GND o------|<-------+
+-----------+ LED
ESP32
これは ESP32 の GPIO2 の出力で直接LEDを点灯する回路です。電圧3.3Vの場合は電流制限用に 220Ω〜1kΩ の抵抗を入れるように指示があります。回路図では330Ωを入れてます。今回は写真のような回路にしてます。全く同じにしたつもりでしたが、抵抗の入る位置とLEDは逆になってますね。
ソフトウェア
ESP32 Deployment Overview には、インストールするソフトウェアが以下の2つであることが示されています。
- AtomVM binary image
- Erlang/Elixir application
AtomVM 本体のインストール
ハードウェアの準備ができたら Deploying the ESP32 AtomVM virtual machine を見ながら Elixir/Erlang を実行する AtomVM 本体を ESP32 にインストールします。
まず ESP32 をきれいにする
Flashing a binary image to ESP32 に We recommend first erasing any existing applications on the ESP32 device. とありますので「要らねんじゃね?」とか思いつつ念のためフラッシュを消去しておきます。
マニュアルには以下の例があります。
esptool.py --chip auto --port /dev/ttyUSB0 --baud 921600 erase_flash
まず、/dev/ttyUSB0 とあるのは自分の母艦に合わせて変更する必要があります。
私の MBP の場合は以下で探しました。これ、USBなら何でも同じではなく、つなぎ先のデバイスで変わるので要注意です。ハマりどころその4です
% ls /dev/tty.usb*
/dev/tty.usbserial-56980063271
次に、USBのボーレートが 921600bps とか見たこともないハイスピードが指示されてます。これ直後の note にあるように遅くしないとつながりませんでした。私の場合は 115200bps でうまく来ました。ハマりどころその5です
あと、esptool.py を使うと「それはもう古い」とか warning が出たりしてあまり気持ちが良くないので esptool としたります。それでもコマンドオプションの指定が古いとか言われますが、warning なんでスルーしましょう。
% esptool --chip auto --port /dev/tty.usbserial-56980063271 --baud 115200 erase_flash
Warning: Deprecated: Command 'erase_flash' is deprecated. Use 'erase-flash' instead.
esptool v5.1.0
Connected to ESP32 on /dev/tty.usbserial-56980063271:
Chip type: ESP32-D0WD-V3 (revision v3.1)
Features: Wi-Fi, BT, Dual Core + LP Core, 240MHz, Vref calibration in eFuse, Coding Scheme None
Crystal frequency: 40MHz
MAC: 64:b7:08:ce:b3:74
Stub flasher running.
Flash memory erased successfully in 12.9 seconds.
Hard resetting via RTS pin...
処理系をダウンロードして焼く
AtomVM の処理系はコンパイルされているバイナリが用意されてますのでそれを使います。
今回は releases の v0.6.6 を用いました。これを下に見ていくと Assets とあるところにバイナリが66個おいてあります。アーキテクチャごとにバイナリが違うことに注意してください。ハマりどころその6です
今回は AtomVM-esp32-elixir-v0.6.6.img を用いました。これを適当なところにダウンロードします。
ダウンロードしたファイルを ESP32 に書き込みます。ここで焼きますとか言うとそれっぽいです。公式ドキュメントには以下が書いてあります。
esptool.py \
--chip auto \
--port /dev/ttyUSB0 --baud 921600 \
--before default_reset --after hard_reset \
write_flash -u \
--flash_mode dio --flash_freq 40m --flash_size detect \
0x1000 \
/path/to/Atomvm-esp32-v0.6.0.img
先程同様に以下を施して実行します。
- コマンド名を変える(必須ではない)
- ポートを自分の母艦に合わせる
- 最後の行は自分が置いたバイナリの位置に合わせる
-
deprecatedとか warning が出ても我慢する
あと ESP32 のアーキテクチャごとに先頭アドレスが変わります。ESP32は 0x1000 で、ESP32-S2, ESP32-S3, ESP32-C2, ESP32-C3, ESP32-C5, ESP32-C6, ESP32-C61, ESP32-H2, ESP32-P4 については公式ドキュメントのこの直後に書いてあります。ハマりどころその7です
今回は以下で焼きました。
% esptool \
--chip auto \
--port /dev/tty.usbserial-56980063271 --baud 115200 \
--before default_reset --after hard_reset \
write_flash -u \
--flash_mode dio --flash_freq 40m --flash_size detect \
0x1000 \
~/tmp/AtomVM-esp32-elixir-v0.6.6.img
これは数分かかります。
# 略
esptool v5.1.0
Connected to ESP32 on /dev/tty.usbserial-56980063271:
Chip type: ESP32-D0WD-V3 (revision v3.1)です**
ざっくりいうと以下が書いてあります。
-
.avmというのが仮想コードだからそれを焼く - リッチな Elixir アプリケーションは 0x250000 から焼く
- そこが
start/0って関数の先頭でブートして準備が終わったらそこから実行する
この後は STM32 の場合はどうするこうするが始まるので「これだけでどうするんじゃー」という気分になります。以下、ここから先はわりと公式ドキュメントを無視してこうしたらできましたの世界です。
アプリケーションの作成と実行
アプリケーションを ESP32 上で実行するのに
- プログラムを作る
- ライブラリを持ってくる
- コンパイル・リンク(パック)して実行形式を作る
- それを ESP32 に焼く
を行います。
サンプルプログラムをダウンロードする
今回はピンから作らずに、サンプルプログラム集 atomvm_examples をダウンロードします。github なのでお好きな clone の方法でどうぞ。私は code の download zip で落としました。以下では clone された zip ファイルを解凍したディレクトリが `~/src/elixir/AtomVM/atomvm_examples-master としてあるものとします。
ダウンロードしたサンプルプログラムを掘っていくと Elixir 版の Lチカである Blinky が出てきます。そこに行きます。
% cd ~/src/elixir/AtomVM
% ls
atomvm_examples-master/
% cd atomvm_examples-master
% ls
build.sh* demos/ elixir/ erlang/ LICENSES/ README.md
% cd elixir
% ls
Blinky/ HelloWorld/ LEDC_Example/ README.md Wifi/
% cd Blinky
% ls
lib/ mix.exs README.md
ライブラリを仕込む
ここで唐突ですが、AtomVM 本体をダウンロードしたリポジトリにおいてある atomvmlib-v0.6.6.avm をダウンロードして avm_deps なるディレクトリに置きます。これ詳しくは後で説明します。ハマりどころその8です
% mkdir avm_deps
# ダウンロードする(ダウンロードした先を ~/tmp とする)
# mv ~/tmp/
mv ~/tmp/atomvmlib-v0.6.6.avm avm_deps
mix.exs の扱い
mix.exs は変更無しで大丈夫です。中に以下の依存関係の記述があるのを確認してください。
defp deps do
[
{:exatomvm, git: "https://github.com/atomvm/ExAtomVM/"}
]
end
ソースコードから実行可能形式を作る
Lチカの本体は lib/Blinky.ex です。これもこのままでやります。
ではコンパイルしましょう。まずは依存関係を解決します。
% mix deps.get
* Getting exatomvm (https://github.com/atomvm/ExAtomVM/)
remote: Enumerating objects: 578, done.
remote: Counting objects: 100% (140/140), done.
remote: Compressing objects: 100% (71/71), done.
remote: Total 578 (delta 94), reused 72 (delta 68), pack-reused 438 (from 2)
'origin/HEAD' is unchanged and points to 'main'
Resolving Hex dependencies...
Resolution completed in 0.009s
New:
uf2tool 1.1.0
* Getting uf2tool (Hex package)
つぎにコンパイルします。
% mix compile
Compiling 1 file (.ex)
#
# ここでたっぷり warning が出る
#
Generated Blinky app
ぎょっとする数の warning が出ます。特に「GPIO モジュールと :atomvm モジュールの関数が定義されてない」という警告は致命的に見えますが、結論から言うと放置してよいです。ハマりどころその9です
これで出来上がった .beam ファイルから .avm ファイルを生成します。
% mix atomvm.packbeam
% ls
_build/ Blinky.avm deps.avm mix.exs priv.avm
avm_deps/ deps/ lib/ mix.lock README.md
この3つの .avm が ESP32 に積むべきアプリケーションです。
アプリケーションを ESP32 に焼いて実行
出来上がった .avm ファイルを mix atomvm.esp32.flash コマンドで ESP32 に焼きます。母艦のシリアルポートを指定する必要があります。
% mix atomvm.esp32.flash --port /dev/tty.usbserial-56980063271
Flashing using esptool..
# Warning を省略
esptool v5.1.0
Connected to ESP32 on /dev/tty.usbserial-56980063271:
Chip type: ESP32-D0WD-V3 (revision v3.1)
Features: Wi-Fi, BT, Dual Core + LP Core, 240MHz, Vref calibration in eFuse, Coding Scheme None
Crystal frequency: 40MHz
MAC: 64:b7:08:ce:b3:74
Stub flasher running.
Configuring flash size...
Auto-detected flash size: 4MB
Flash will be erased from 0x00250000 to 0x002a2fff...
# 書込みの様子を省略
Wrote 344064 bytes at 0x00250000 in 33.6 seconds (81.9 kbit/s).
Hash of data verified.
Hard resetting via RTS pin...
これで最後に自動的にリセットが行われ、Lチカが開始されます。
mix コマンドの一連の動作
mix compile mix atomvm.packbeam mix atomvm.esp32.flash は別々に実行しましたが、mix atomvm.esp32.flash だけで compile も packbeam も実行します。ただ何か異常があったときにどこで何があったのかわかりにくいという欠点があります。
コンパイル時の warning を止める
mix compile で恐ろしい量の warning が出ます。それも GPIO や :atomvm なんてどこにあるかわかりませんので途方にくれます。あらためて ハマりどころその9 です
これなんとか消そうとしますが require, import, Alias, use を使っても解決しないです。mix.exs で依存関係を書いたら引っ張ってきてくれるということもありません。これがわからなくて凄まじい時間を溶かしました。
これはライブラリが avm_deps/atomvmlib-v0.6.6.avm にあるのですが、コンパイル時には解決できなくて warning がでます。出ますが packbeam するときにリンクされて最終的にはちゃんとした実行形式になるという、なんともわかりにくい状態になってます。
その他も warning が出ますので、コンパイル時に静かにしてもらうためには以下の変更をします。
- 見つからないモジュールを使っててもコンパイラに無視させる(必須)
@compile {:no_warn_undefined, [GPIO, :atomvm]}
- warning を減らす
- シングルクォートの記法
:io.format('Setting pin ~p ~p~n', [pin, level])が怒られるので:io.format("Setting pin ~p ~p~n", [pin, level])とする(推奨)
- シングルクォートの記法
オリジナルのファイルに対する差分が以下です。
defmodule Blinky do
+ @compile {:no_warn_undefined, [GPIO, :atomvm]}
# Pin 2 works on any pico device with an external LED
@pin 2
# Comment out above and uncomment below to use Pico W onboard LED
# @pin {:wl, 0}
def start() do
platform_gpio_setup()
loop(pin(), :low)
end
defp loop(pin, level) do
- :io.format('Setting pin ~p ~p~n', [pin, level])
+ :io.format(~c"Setting pin ~p ~p~n", [pin, level])
GPIO.digital_write(pin, level)
Process.sleep(1000)
loop(pin, toggle(level))
おまけ: Hello World! も出してみる
Lチカに加えて起動時に文字列を出してみましょう。
def start() do
+ :io.format(~c"Hello World!~n")
platform_gpio_setup()
loop(pin(), :low)
end
起動してすぐのLチカを始める直前にターミナルに文字列が出てきます。これ screen コマンド等でコンソールを繋がないとわかりません。母艦のシリアルポートと通信速度を指定して接続します。
% screen /dev/tty.usbserial-56980063271 115200
もうLチカモードで走っているのでおそらく起動時の状態は見られません。ESP32には2つのボタンがあります。そのうち EN を押すとCPUリセットがかかります。
rst:0x1 (POWERON_RESET),boot:0x13 (SPI_FAST_FLASH_BOOT)
configsip: 0, SPIWP:0xee
clk_drv:0x00,q_drv:0x00,d_drv:0x00,cs0_drv:0x00,hd_drv:0x00,wp_drv:0x00
mode:DIO, clock div:2
load:0x3fff0030,len:5172
load:0x40078000,len:15512
load:0x40080400,len:4
load:0x40080404,len:3788
entry 0x40080624
I (29) boot: ESP-IDF v5.4.1 2nd stage bootloader
からブートに関するメッセージが続いて
###########################################################
### ######## ####### ## ## ## ## ## ##
## ## ## ## ## ### ### ## ## ### ###
## ## ## ## ## #### #### ## ## #### ####
## ## ## ## ## ## ### ## ## ## ## ### ##
######### ## ## ## ## ## ## ## ## ##
## ## ## ## ## ## ## ## ## ## ##
## ## ## ####### ## ## ### ## ##
###########################################################
I (784) AtomVM: Starting AtomVM revision 0.6.6
と AtomVM の処理系が動き出したのがわかります。その後もメッセージが続いて…
AtomVM init.
I (844) sys: Loaded BEAM partition main.avm at address 0x250000 (size=1048576 bytes)
Starting application...
Hello World!
が出てきます。パチパチパチ。続いて GPIO 2番ピンを high/low に繰り返す様子が出力されます。
Setting pin 2 low
Setting pin 2 high
Setting pin 2 low
Setting pin 2 high
まとめ
ESP32上のAtomVMでLチカをやってみました。ドキュメントに沿ってやってもスムーズにはいきませんでしたがなんとかできました。ハマりどころが以下のようにありました。
- AtomVM 0.6.x が使うのは OTP27 まで
- Elixir の application の先頭アドレスは 0x250000
- USBケーブルに気をつけろ
- 母艦のシリアルポートは相手デバイスで変わりうる
- シリアルのボーレートは標準的な速さで
- コンパイル済みの処理系はアーキテクチャごとにファイルが違う
- ESP32シリーズのアーキテクチャごとにロードするメモリアドレスが異なる
- 明示的に言われてなくても
atomvmlibを準備しよう - 関数が定義されてない場合でも後で
atomvmlibにリンクされる場合がある
開発がどんどん続いていて、処理系やドキュメントもどんどん変わってます。これからやる方は最新の情報に当たってください。あと生成AIは古い情報と新しい情報とをごっちゃにして提供するので、このような開発が進んでいる最中では怪しげな話を連発するのでっそのつもりで使いましょう。
謝辞
【名古屋】AtomVM入門〜大須散策を添えて〜と【名古屋】Davide BettioさんとAtomVMを囲む会を準備してくださった @nako_sleep_9h と @mnishiguchi に感謝します。
参考文献
-
AtomVM
- AtomVM: Elixir で ESP32-S3 の LED を光らせる (2025年8月)
- AtomVM: how to run Elixir code on a 3 $ microcontroller 作者の Davide Bettio さんの2018年の記事
- ElixirでIoT#3.1:ESP32やSTM32でElixirが動く!AtomVMという選択肢 これも古いので初学者は新しいドキュメントを
- Piyopiyo.ex
- @kikuyuta の Elixir 関連ドキュメント集
-
作者の Davide Bettio さんによると今年中に出る AtomVM 0.7 系では OTP28に対応するとのことです。 ↩
