Skip to content

Instantly share code, notes, and snippets.

@mnishiguchi
Last active August 4, 2025 23:13
Show Gist options
  • Save mnishiguchi/6df85bcc93d62ba449dd430134d24d56 to your computer and use it in GitHub Desktop.
Save mnishiguchi/6df85bcc93d62ba449dd430134d24d56 to your computer and use it in GitHub Desktop.
AtomVM Blinky on ESP32-S3 (Debian, 2025-07-31)

AtomVM Blinky on ESP32-S3 (Debian, 2025-07-31)

Let’s run a simple Elixir “Blinky” application on the Seeed Studio XIAO ESP32-S3 using AtomVM — a lightweight virtual machine for running Elixir and Erlang on microcontrollers.


Host Environment

This setup assumes a Debian-based Linux host with the following tools installed:

$ uname
Linux

$ elixir --version
Elixir 1.17.3 (compiled with Erlang/OTP 27)

$ python --version
Python 3.12.5

$ esptool version
esptool v5.0.1

$ picocom -h | head -n1
picocom v3.1

Note: esptool.py is now esptool (without .py) and uses hyphenated options (e.g. --chip, not --chip_esp32). The older syntax still works, but is deprecated.


Target Device

We are targeting:

This board:

  • Has native USB (no need for a USB-to-serial adapter)
  • Exposes its internal LED on GPIO21 (active-low)

Install Espressif IDF

AtomVM firmware is built using Espressif’s ESP-IDF. Here’s how to install it:

For more details and official instructions:

# Install system dependencies
sudo apt install git wget flex bison gperf cmake ninja-build \
  ccache libffi-dev libssl-dev dfu-util libusb-1.0-0

# Clone the ESP-IDF source
mkdir -p $HOME/esp && cd $_
git clone --recursive https://github.com/espressif/esp-idf.git
cd esp-idf

# Checkout a specific stable version (e.g. v5.5)
git checkout v5.5
git submodule update --init --recursive

# Install tools only for esp32s3
./install.sh esp32s3

# Add IDF environment to current shell
source ./export.sh

Build AtomVM Core

Now let’s fetch and build the AtomVM virtual machine and core libraries.

AtomVM source and release info:

# Clone AtomVM source
mkdir -p $HOME/Projects/atomvm && cd $_
git clone https://github.com/atomvm/AtomVM.git
cd AtomVM

# Checkout a stable release
git checkout v0.6.6

# Build the core VM
mkdir -p build && cd build
cmake ..
make -j$(nproc)

This compiles AtomVM's core (for desktop/host use) — not the ESP32 firmware yet.


Build AtomVM Firmware for ESP32-S3

Now let’s build the firmware for the actual ESP32-S3 target:

cd $HOME/Projects/atomvm/AtomVM/src/platforms/esp32

# Set up IDF in the shell
source $HOME/esp/esp-idf/export.sh

# Configure for ESP32-S3, then build
idf.py set-target esp32s3
idf.py reconfigure
idf.py build

This builds the full AtomVM firmware for the ESP32-S3: bootloader, partition table, VM binary, and Elixir stdlib.


Flash AtomVM Firmware + Core Libraries

You can now flash AtomVM to your device.

Step 1: Erase the Flash (Recommended)

This ensures no conflicting firmware or partitions remain.

esptool --chip auto --port /dev/ttyACM0 --baud 115200 erase-flash

This wipes the entire flash memory of the ESP32-S3.

Step 2: Flash AtomVM

cd $HOME/Projects/atomvm/AtomVM/src/platforms/esp32
source $HOME/esp/esp-idf/export.sh

idf.py -p /dev/ttyACM0 flash

Build the Elixir “Blinky” Example

Let’s compile the Elixir application that will blink the onboard LED.

cd $HOME/Projects/atomvm
git clone https://github.com/atomvm/atomvm_examples.git
cd atomvm_examples/elixir/Blinky

# Install dependencies (ExAtomVM)
mix deps.get

Edit the LED Pin

To make the on-board yellow LED (GPIO21) blink on your XIAO ESP32-S3, update the pin number in the Blinky module:

  • Open lib/blinky.ex
  • Change GPIO 2 to GPIO 21

The XIAO LED is active-low, so the LED turns ON when the pin is set to 0.

defmodule SampleApp do
  @pin 21

  def start() do
    :gpio.set_pin_mode(@pin, :output)
    loop(@pin, :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

Compile and Pack

Now compile the Elixir code and package it into a .avm file:

mix atomvm.packbeam

ls -l *.avm

You should see something like Blinky.avm in the current directory.


Flash the “Blinky” App

Use the AtomVM Mix task to flash your .avm bundle to the ESP32-S3:

mix atomvm.esp32.flash --port /dev/ttyACM0 --baud 115200

You can repeat this step every time you modify your app — no need to reflash the firmware.


Monitor Serial Output

You can view AtomVM logs and Elixir output using either:

Option 1: idf.py monitor

cd $HOME/Projects/atomvm/AtomVM/src/platforms/esp32
source $HOME/esp/esp-idf/export.sh

idf.py --port /dev/ttyACM0 monitor

Press Ctrl+] to exit.

Option 2: picocom (simpler & works anywhere)

picocom /dev/ttyACM0 --baud 115200

To exit: Ctrl+A, then Ctrl+X


Quick Development Cycle

For future updates:

# Make changes to Elixir code
mix atomvm.packbeam
mix atomvm.esp32.flash --port /dev/ttyACM0

No need to rebuild or reflash the firmware unless you update AtomVM itself.

@mnishiguchi
Copy link
Author

mnishiguchi commented Jul 31, 2025

atomvm-blinky_0835~3
atomvm-blinky 2025-08-01 12-04

@mnishiguchi
Copy link
Author

mnishiguchi commented Aug 4, 2025

#!/usr/bin/env bash
#
# reliably source ESP-IDF, cd into AtomVM ESP32 dir, and run `idf.py monitor`
#

set -euo pipefail

# ————————————————
# Colored output helpers
# ————————————————
echo_heading() { printf "\n\033[34m%s\033[0m\n" "$*"; }
echo_success() { printf " \033[32m✔ %s\033[0m\n" "$*"; }
echo_warning() { printf " \033[33m⚠ %s\033[0m\n" "$*"; }
echo_failure() { printf " \033[31m✖ %s\033[0m\n" "$*"; }

# ————————————————
# Configuration
# ————————————————

# https://stackoverflow.com/a/246128/3837223
this_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &> /dev/null && pwd)"

ESP_IDF_DIR="${HOME}/esp/esp-idf"
ESP_IDF_EXPORT="${ESP_IDF_DIR}/export.sh"

ATOMVM_ESP32_DIR="${this_dir}/../AtomVM/src/platforms/esp32"
DEFAULT_PORT="/dev/ttyACM0"

# ————————————————
# Usage
# ————————————————
usage() {
  cat <<EOF

Usage: $(basename "$0") [OPTIONS] [PORT]

Description:
  Source the ESP-IDF environment, cd into AtomVM's ESP32 platform folder,
  and invoke \`idf.py --port PORT monitor\`.

Options:
  -h, --help     Show this help and exit
  PORT           Serial port (default: ${DEFAULT_PORT})

EOF
}

# ————————————————
# Check & source ESP-IDF
# ————————————————
setup_esp_idf() {
  echo_heading "Configuring ESP-IDF environment"
  if [[ ! -f "${ESP_IDF_EXPORT}" ]]; then
    echo_failure "ESP-IDF export script not found: ${ESP_IDF_EXPORT}"
    exit 1
  fi

  # shellcheck source=/dev/null
  source "${ESP_IDF_EXPORT}"
  echo_success "Sourced ESP-IDF from ${ESP_IDF_DIR}"
}

# ————————————————
# Change to AtomVM ESP32 dir
# ————————————————
cd_to_atomvm() {
  echo_heading "Entering AtomVM ESP32 platform directory"
  if [[ ! -d "${ATOMVM_ESP32_DIR}" ]]; then
    echo_failure "Directory not found: ${ATOMVM_ESP32_DIR}"
    exit 1
  fi
  cd "${ATOMVM_ESP32_DIR}"
  echo_success "Now in $(pwd)"
}

# ————————————————
# Run idf.py monitor
# ————————————————
run_monitor() {
  local port="$1"
  echo_heading "Launching \`idf.py --port ${port} monitor\`"
  exec idf.py --port "${port}" monitor
}

# ————————————————
# Main
# ————————————————
main() {
  # Parse flags
  local port="${DEFAULT_PORT}"
  while [[ $# -gt 0 ]]; do
    case "$1" in
    -h | --help)
      usage
      exit 0
      ;;
    *)
      port="$1"
      shift
      ;;
    esac
  done

  echo_heading "AtomVM ESP32 Monitor Script"
  setup_esp_idf
  cd_to_atomvm
  run_monitor "${port}"
}

main "$@"

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment