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.

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.