Entorno de Desarrollo

En esta sección se mostrará cómo configurar el entorno de desarrollo y compilar AtomVM para ser ejecutado en la ESP32 utilizando un sistema operativo Linux.

Linux/MacOS

Se recomienda usar Devenv para gestionar dependencias del sistema. El siguiente archivo devenv.nix define todas las dependencias necesarias para compilar un proyecto Elixir a avm, y luego flashear el resultado a un ESP32:

devenv.nix
{ lib, pkgs, ... }:

{
  languages.elixir.enable = true;
  packages = [
    pkgs.inotify-tools

    pkgs.esptool
    pkgs.picocom
  ] ++
  lib.optionals pkgs.stdenv.isDarwin [
    # for ExUnit notifier
    pkgs.terminal-notifier

    # for package - file_system
    pkgs.darwin.apple_sdk.frameworks.CoreFoundation
    pkgs.darwin.apple_sdk.frameworks.CoreServices
  ];

  scripts.flash.exec = ''
    mix atomvm.packbeam && sudo esptool --chip auto --port /dev/ttyUSB0 --baud 921600 \
      write-flash 0x250000 serial_test.avm
  '';

  scripts.monitor.exec = ''
    sudo picocom -b 115200 /dev/ttyUSB0
  '';

  scripts.format.exec = ''
    sudo esptool --chip auto --port /dev/ttyUSB0 --baud 921600 erase-flash
  '';
}

El entorno también incluye un alias para flashear el programa (flash) y un alias para monitorear la salida serial (monitor). Estos alias se pueden ejectuar directamente a través de la shell. Para entrar a la shell de devenv, se debe ejecutar el comando devenv shell.

Se asume que la entrada de ESP32 se encuentra en /dev/ttyUSB0. En caso de no encontrarse ahí (se puede verificar con ls /dev/ttyUSB*), se debe cambiar en la definición de los scripts.

Mise

También es una opción el utilizar la herramienta mise para la instalación de esptool.

$ mise use -g pipx
$ pipx --version
$ mise use -g pipx:esptool
$ esptool version

Windows

Para Windows se recomienda la instalación de una máquina virtual con Linux o utilizar WSL.

Preparar ESP32

Para correr programas Elixir compilados a avm, es necesario primero flashear la imagen de AtomVM (con soporte para Elixir) en la ESP32.

# eliminar memoria flash
sudo esptool --chip auto --port /dev/ttyUSB0 --baud 921600 erase-flash

# flashear AtomVM (descarga en https://github.com/atomvm/AtomVM/releases)

sudo esptool \
  --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 \
  ./AtomVM-esp32-elixir-v0.6.6.img

Encender un LED con AtomVM

mix.exs
defmodule LedTest.MixProject do
  use Mix.Project

  def project do
    [
      app: :led_test,
      version: "0.1.0",
      elixir: "~> 1.18",
      start_permanent: Mix.env() == :prod,
      deps: deps(),
      atomvm: [
        start: LedTest,
        flash_offset: 0x250000
      ]
    ]
  end

  # Run "mix help compile.app" to learn about applications.
  def application do
    [
      extra_applications: [:logger]
    ]
  end

  # Run "mix help deps" to learn about dependencies.
  defp deps do
    [
      {:exatomvm, git: "https://github.com/atomvm/ExAtomVM/"}
    ]
  end
end
lib/led_test.ex
defmodule LedTest do
  @test_duty 3000
  @test_fade_time 2000

  @led_1 2 # led de ESP32

  def start do
    ledc_hs_timer = [
      {:duty_resolution, 13},
      {:freq_hz, 5000},
      {:speed_mode, LEDC.high_speed_mode()},
      {:timer_num, @high_speed_timer}
    ]

    :ok = LEDC.timer_config(ledc_hs_timer)

    ledc_channel = [
      [
        {:channel, 0},
        {:duty, 0},
        {:gpio_num, @led_1},
        {:speed_mode, LEDC.high_speed_mode()},
        {:hpoint, 0},
        {:timer_sel, @high_speed_timer}
      ]
    ]

    Enum.each(ledc_channel, fn channel_config -> :ok = LEDC.channel_config(channel_config) end)
    :ok = LEDC.fade_func_install(0)
    loop(ledc_channel)
  end

  def loop(ledc_channel) do
    :io.format('1. LEDC fade up to duty = ~p~n', [@test_duty])
    Enum.each(ledc_channel, fn channel_config -> do_stage_1(channel_config) end)
    Process.sleep(@test_fade_time)

    :io.format('2. LEDC fade down to duty = 0~n')
    Enum.each(ledc_channel, fn channel_config -> do_stage_2(channel_config) end)
    Process.sleep(@test_fade_time)

    :io.format('3. LEDC set duty = ~p without fade~n', [@test_duty])
    Enum.each(ledc_channel, fn channel_config -> do_stage_3(channel_config) end)
    Process.sleep(@test_fade_time)

    :io.format('4. LEDC set duty = 0 without fade~n')
    Enum.each(ledc_channel, fn channel_config -> do_stage_4(channel_config) end)
    Process.sleep(@test_fade_time)

    loop(ledc_channel)
  end

  defp do_stage_1(channel_config) do
    speed_mode = :proplists.get_value(:speed_mode, channel_config)
    channel = :proplists.get_value(:channel, channel_config)
    :ok = LEDC.set_fade_with_time(speed_mode, channel, @test_duty, @test_fade_time)
    :ok = LEDC.fade_start(speed_mode, channel, LEDC.fade_no_wait())
  end

  defp do_stage_2(channel_config) do
    speed_mode = :proplists.get_value(:speed_mode, channel_config)
    channel = :proplists.get_value(:channel, channel_config)
    :ok = LEDC.set_fade_with_time(speed_mode, channel, 0, @test_fade_time)
    :ok = LEDC.fade_start(speed_mode, channel, LEDC.fade_no_wait())
  end

  defp do_stage_3(channel_config) do
    speed_mode = :proplists.get_value(:speed_mode, channel_config)
    channel = :proplists.get_value(:channel, channel_config)
    :ok = LEDC.set_duty(speed_mode, channel, @test_duty)
    :ok = LEDC.update_duty(speed_mode, channel)
  end

  defp do_stage_4(channel_config) do
    speed_mode = :proplists.get_value(:speed_mode, channel_config)
    channel = :proplists.get_value(:channel, channel_config)
    :ok = LEDC.set_duty(speed_mode, channel, 0)
    :ok = LEDC.update_duty(speed_mode, channel)
  end
end

Luego, en una shell con devenv:

flash

Si logras ejecutar éste código y el Led prende. Exitosamente has configurado el entorno de desarrollo para AtomVM.

Compilar AtomVM

También es opción compilar un ejecutable de AtomVM que permita probar el código dentro de un computador Linux o MacOS.

Para esto se necesitan las siguientes dependencias:

  • cmake

  • gperf

  • rebar3

Estas pueden ser instaladas en Linux (Debian) por ejemplo:

$ sudo apt install cmake
$ sudo apt install gperf
$ sudo apt install rebar3

Y en MacOS utilizando MacPorts por ejemplo:

$ sudo port install cmake
$ sudo port install gperf
$ sudo port install rebar3

rebar3 necesita erlang. Por lo que si ya tienes erlang instalado solo debes descargar el archivo.

$ curl https://s3.amazonaws.com/rebar3/rebar3 -o rebar3
$ chmod +x rebar3
$ sudo mv rebar3 /usr/local/bin
$ rebar3 --version
rebar 3.25.1 on Erlang/OTP 26 Erts 14.2.5.9

Luego se puede compilar:

$ git clone https://github.com/atomvm/AtomVM.git
$ cd AtomVM/
$ mkdir build
$ cd build
$ cmake ..
$ make -j 16

Esto generará un ejecutable que se puede crear un script como avm. El ejecutable está dentro del directorio build/src.

$ mkdir /usr/local/bin/atomvm
$ cp src/AtomVM /usr/local/bin/atomvm
$ cp lib/atomvmlib.avm /usr/local/bin/atomvm
$ touch /usr/local/bin/avm
$ chmod +x /usr/local/bin/avm
/usr/local/bin/avm
#!/usr/bin/env bash
/usr/local/bin/atomvm/AtomVM /usr/local/bin/atomvm/atomvmlib.avm "$@"

Y se puede usar de la siguiente forma

$ avm ./examples/erlang/hello_world.avm
Hello World
Return value: ok

Elixir

Para tener soporte de Elixir se debe seguir el siguiente ejemplo:

$ git clone https://github.com/atomvm/atomvm_examples.git
$ cd atomvm_examples/elixir/HelloWorld
$ mix deps.get
$ mix atomvm.packbeam
$ avm hello_world.avm
Hello World
Return value: ok

Muchas bibliotecas de Elixir como Enum y Regex, entre otras no están disponibles en AtomVM debido a que es una máquina virtual muy pequeña. Se debe tener en consideración sobre qué dependencias utilizar en los proyectos, muchas veces se preferirá código Erlang para suplir lo que haga falta.