I previously tried using AtomVM, a lightweight virtual machine that lets you run Elixir (and Erlang) applications directly on microcontrollers like the ESP32-S3, but honestly, I rushed through the process without fully understanding the underlying details — especially around firmware setup.
This time around, I wanted to go back and properly understand how to set things up, focusing specifically on getting started with AtomVM using a prebuilt firmware image. The goal is to simplify the onboarding experience without building anything from source.
This post doesn’t dig into advanced features. Instead, it’s a hands-on guide to get your ESP32-S3 board up and running with AtomVM, so you can start running Elixir code on real hardware as quickly as possible.
Originally published in Japanese: AtomVM 入門: ビルド済みイメージを活用した環境構築 (2025年9月)
Target Environment and Devices
Here's what I used for this setup.
Target Board
-
Seeed Studio XIAO ESP32-S3
- Onboard LED on GPIO 21 (active-low)
Host Machine
- Linux (Debian-based — LMDE6)
- Elixir 1.17 with Erlang/OTP 27
- AtomVM v0.6.6 (prebuilt firmware)
- esptool 5.0 — official flashing tool for ESP32
- picocom 3.1 — for serial monitoring
- Python 3.13 — required by esptool
Notes
- esptool is a Python tool and requires Python 3.x.
- Since we're using a prebuilt AtomVM image, you do not need to install ESP-IDF.
- For compatibility info (e.g. which Elixir & OTP versions are supported by AtomVM), refer to the official release notes.
Installing esptool
esptool is the official command-line utility for flashing firmware to ESP32 boards.
We'll install it using pip
, which makes it easy to manage and update.
pip install "esptool>=5,<6"
- Version 5.x introduces hyphenated options instead of older underscore syntax.
- Make sure Python 3.x is installed on your system.
Checking the Serial Port
When you connect your ESP32-S3 via USB, Linux will create a new serial device (e.g., /dev/ttyACM0
). You’ll need this port name when flashing firmware or uploading .avm
files.
On Linux, you can run:
dmesg | grep tty
Output might look like:
[12345.678901] cdc_acm 1-1.4:1.0: ttyACM0: USB ACM device
In this case, use /dev/ttyACM0
.
You’re typically looking for something like /dev/ttyACM0
or /dev/ttyUSB0
, depending on your board and USB-to-serial chip.
Verifying esptool
and Board Connection
Once esptool
is installed, you can check that:
- It's correctly installed
- It can communicate with your ESP32-S3 board
# Check version
esptool.py version
# Query flash chip information
esptool.py -p /dev/ttyACM0 flash-id
If this succeeds, you're ready to start flashing firmware.
Downloading and Flashing AtomVM Firmware
We’ll be using a prebuilt binary from the AtomVM GitHub Releases, so there’s no need to build anything from source.
We’ll use the official prebuilt AtomVM image (v0.6.6) for ESP32-S3.
Download the image
# Destination folder is arbitrary
mkdir -p $HOME/Projects/atomvm && cd $_
curl -LO https://github.com/atomvm/AtomVM/releases/download/v0.6.6/AtomVM-esp32s3-elixir-v0.6.6.img
Erase flash (recommended)
esptool.py --chip auto --port /dev/ttyACM0 --baud 921600 erase-flash
Write AtomVM firmware
esptool.py --chip auto --port /dev/ttyACM0 --baud 921600 write_flash 0x0 $HOME/Projects/atomvm/AtomVM-esp32s3-elixir-v0.6.6.img
According to the AtomVM docs, the bootloader offset for ESP32-S3 is 0x0
.
Building and Flashing an Elixir Application
With AtomVM running on your ESP32-S3, you can now deploy an actual Elixir application to it!
In this section, we'll use the classic Blinky
sample to flash an .avm
(AtomVM bytecode) file that blinks the onboard LED.
Get the sample project
Clone the official atomvm_examples repo:
cd ~/Projects/atomvm # Or any directory you like
git clone https://github.com/atomvm/atomvm_examples.git
cd atomvm_examples/elixir/Blinky
Install dependencies (mainly exatomvm):
mix deps.get
Adjust GPIO pin for XIAO ESP32-S3
By default, the sample blinks GPIO 2
— but XIAO’s onboard LED is wired to GPIO 21
(active low). Update lib/blinky.ex
accordingly:
defmodule Blinky do
@pin 21
def start() do
:gpio.set_pin_mode(@pin, :output)
loop(@pin, :low) # LOW = LED ON (active-low)
end
defp loop(pin, level) do
:io.format(~c"Setting pin ~p ~p~n", [pin, level])
:gpio.digital_write(pin, level)
Process.sleep(1000)
loop(pin, toggle(level))
end
defp toggle(:high), do: :low
defp toggle(:low), do: :high
end
Configure mix.exs
with flash offset
You must set the .avm
flash offset to 0x250000
. Without this, the ESP32-S3 won’t recognize your app at boot.
def atomvm do
[
start: SampleApp,
flash_offset: 0x250000 # important!
]
end
Package the app
Build the .avm
files using:
mix atomvm.packbeam
This generates .avm
files such as:
- Blinky.avm
- deps.avm
- priv.avm
Flash the app to ESP32-S3
Finally, push your app:
mix atomvm.esp32.flash --port /dev/ttyACM0
This will write the .avm
files to the right spot, without touching the AtomVM firmware.
You should now see the LED start blinking 🎉
Viewing Logs over Serial
Your Blinky app doesn't just blink the LED — it also prints log messages via serial.
To view these logs in real time, you'll need a serial monitor. We'll use picocom — a simple, lightweight tool that works great on Linux.
To start monitoring logs from your ESP32-S3 board:
picocom /dev/ttyACM0 --baud 115200
This opens the serial console. You should immediately start seeing output like:
These messages come from your Elixir code (:io.format
), showing the pin toggling every second.
To exit picocom, press: Ctrl+A
then Ctrl+X
.
Wrapping Up
In this post, we walked through the full process of setting up an ESP32-S3 board to run Elixir code using AtomVM — no custom firmware builds required.
This setup makes it easy to start tinkering with embedded Elixir, even without diving deep into ESP-IDF or AtomVM internals.
Next steps might include integrating with sensors, displaying data, or connecting to the cloud via MQTT.
Hopefully, this gave you a smooth start with AtomVM on ESP32-S3!
Links
- AtomVM GitHub
- AtomVM Getting Started Guide
- AtomVM Release Notes
- Blinky Example
- Seeed Studio XIAO ESP32-S3
- Elixir Docs
Top comments (0)