# Programmers Guide This guide is intended for programmers who develop applications targeted for AtomVM. As an implementation of the Erlang virtual machine, AtomVM is designed to execute unmodified byte-code instructions compiled into BEAM files, either by the Erlang or Elixir compilers. This allow developers to write programs in their BEAM programming language of choice, and to use the common Erlang community tool-chains specific to their language platform, and to then deploy those applications onto the various devices that AtomVM supports. This document describes the development workflow when writing AtomVM applications, as well as a high-level overview of the various APIs that are supported by AtomVM. With an understanding of this guide, you should be able to design, implement, and deploy applications onto a device running the AtomVM virtual machine. ## AtomVM Features Currently, AtomVM implements a strict subset of the BEAM instruction set, as of Erlang/OTP R21. Previous versions of Erlang/OTP are not supported. A high level overview of the supported language features include: * All the major Erlang types, including * integers (with size limits) * limited support for floats (not supported on all platforms) * tuples * lists * binaries * maps * support for many Erlang BIFs and guard expressions to support the above types * pattern matching (case statements, function clause heads, etc) * `try ... catch ... finally` constructs * anonymous functions * process `spawn` and `spawn_link` * send (`!`) and `receive` messages * bit syntax (with some restrictions) * reference counted binaries In addition, several features are supported specifically for integration with micro-controllers, including: * Wifi networking (`network`) * UDP and TCP/IP support (`inet`, `gen_tcp` and `gen_udp`) * Peripheral and system support on micro-controllers, including * GPIO, including pins reads, writes, and interrupts * I2C interface * SPI interface * UART interface * LEDC (PWM) * non-volatile storage (NVS) * deep sleep ### Limitations While the list of supported features is long and growing, the currently unsupported Erlang/OTP and BEAM features include (but are not limited to): * Bingnums. Integer values are restricted to 64-bit values. * SMP support. The AtomVM VM is currently a single-threaded process. * The `epmd` and the `disterl` protocols are not supported. * There is no support for code hot swapping. * There is no support for a Read-Eval-Print-Loop. (REPL) * Numerous modules and functions from Erlang/OTP standard libraries (`kernel`, `stdlib`, `sasl`, etc) are not implemented. AtomVM bit syntax is restricted to alignment on 8-bit boundaries. Little-endian and signed insertion and extraction of integer values is restricted to 8, 16, and 32-bit values. Only unsigned big and little endian 64-bit values can be inserted into or extracted from binaries. It is highly unlikely that an existing Erlang program targeted for Erlang/OTP will run unmodified on AtomVM. And indeed, even as AtomVM matures and additional features are added, it is more likely than not that Erlang applications will need to targeted specifically for the AtomVM platform. The intended target environment (small, cheap micro-controllers) differs enough from desktop or server-class systems in both scale and APIs that special care and attention is needed to target applications for such embedded environments. That being said, many of the features of the BEAM are supported and provide a rich and compelling development environment for embedded devices, which Erlang and Elixir developers will find natural and productive. ## AtomVM Development This section describes the typical development environment and workflow most AtomVM developers are most likely to use. ### Development Environment In general, for most development purposes, you should be able to get away with an Erlang/OTP development environment (OTP21 or later), and for Elixir developers, and Elixir version TODO development environment. We assume most development will take place on some UNIX-like environment (e.g., Linux, FreeBSD, or MacOS). Consult your local package manager for installation of these development environments. Developers will want to make use of common Erlang or Elixir development tools, such as `rebar3` for Erlang developers or `mix` for Elixir developers. Developers will need to make use of some AtomVM tooling. Fortunately, there are several choices for developers to use: 1. AtomVM `PackBEAM` executable (described below) 1. [`atomvm_rebar3_plugin`](https://github.com/atomvm/atomvm_rebar3_plugin), for Erlang development using [`rebar3`](https://rebar3.readme.io). 1. [`ExAtomVM`](https://github.com/atomvm/ExAtomVM) Mix plugin, Elixir development using [`Mix`](https://elixir-lang.org/getting-started/mix-otp/introduction-to-mix.html). Some testing can be performed on UNIX-like systems, using the `AtomVM` executable that is suitable for your development environment. AtomVM applications that do not make use of platform-specific APIs are suitable for such tests. Deployment and testing on micro-controllers is slightly more involved, as these platforms require additional hardware and software, described below. #### ESP32 Deployment Requirements In order to deploy AtomVM applications to and test on the ESP32 platform, developers will need: * A computer running MacOS or Linux (Windows support is TBD); * An ESP32 module with a USB/UART connector (typically part of an ESP32 development board); * A USB cable capable of connecting the ESP32 module or board to your development machine (laptop or PC); * The [`esptool`](https://github.com/espressif/esptool) program, for flashing the AtomVM image and AtomVM programs; * (Optional, but recommended) A serial console program, such as `minicom` or `screen`, so that you can view console output from your AtomVM application. #### STM32 Deployment Requirements TODO ### Development Workflow For the majority of users, AtomVM applications are written in the Erlang or Elixir programming language. These applications are compiled to BEAM (`.beam`) files using standard Erlang or Elixir compiler tool chains (`erlc`, `rebar`, `mix`, etc). The generated BEAM files contain byte-code that can be executed by the Erlang/OTP runtime, or by the AtomVM virtual machine. > Note. In a small number of cases, it may be useful to write parts of an application in the C programming language, as AtomVM nifs or ports. However, writing AtomVM nifs and ports is outside of the scope of this document. Once Erlang and/or Elixir files are compiled to BEAM files, AtomVM provides tooling for processing and aggregating BEAM files into AtomVM Packbeam (`.avm`) files, using AtomVM tooling, distributed as part of AtomVM, or as provided through the AtomVM community. AtomVM packbeam files are the applications and libraries that run on the AtomVM virtual machine. For micro-controller devices, they are "flashed" or uploaded to the device; for command-line use of AtomVM (e.g., on Linux, FreeBSD, or MacOS), they are supplied as the first parameter to the AtomVM command. The following diagram illustrates the typical development workflow, starting from Erlang or Elixir source code, and resulting in a deployed Packbeam file: *.erl or *.ex *.beam +-------+ +-------+ | |+ | |+ | ||+ | ||+ | ||| --------> | ||| | ||| Erlang/Elixir | ||| +-------+|| Compiler +-------+|| +-------+| +-------+| +-------+ +-------+ ^ | | | packbeam | | | v | +-------+ | | | | test | | | debug | | | fix | | | +-------+ | app.avm | | | | flash/upload | | | v +-------------------- Micro-controller device The typical compile-test-debug cycle can be summarized in the following steps: 1. Deploy the AtomVM virtual machine to your device 1. Develop an AtomVM application in Erlang or Elixir 1. Write application 1. Deploy application to device 1. Test/Debug/Fix application 1. Repeat Deployment of the AtomVM virtual machine and an AtomVM application currently require a USB serial connection. There is currently no support for over-the-air (OTA) updates. For more information about deploying the AtomVM image and AtomVM applications to your device, see the [Getting Started Guide](getting-started-guide.md) ## Applications An AtomVM application is a collection of BEAM files, aggregated into an AtomVM "Packbeam" (`.avm`) file, and typically deployed (flashed) to some device. These BEAM files be be compiled from Erlang, Elixir, or any other language that targets the Erlang VM. > Note. The return value from the `start/0` function is ignored. Here, for example is one of the smallest AtomVM applications you can write: %% erlang -module(myapp). -export([start/0]). start() -> ok. This particular application doesn't do much, of course. The application will start and immediately terminate, with a return value of `ok`. Typical AtomVM applications will be more complex than this one, and the AVM file that contains the application BEAM files will be considerably larger and more complex than the above program. Most applications will spawn processes, send and receive messages between processes, and wait for certain conditions to apply before terminating, if they terminate at all. For applications that spawn processes and run forever, you may need to add an empty `receive ... end` block, to prevent the AtomVM from terminating prematurely, e.g., %% erlang wait_forever() -> receive X -> X end. ### Packbeam files AtomVM applications are packaged into Packbeam (`.avm`) files, which contain collections of files, typically BEAM (`.beam`) files that have been generated by the Erlang or Elixir compiler. At least one BEAM module in this file must contain an exported `start/0` function. The first module in a Packbeam file that contain this function is the entry-point of your application and will be executed when the AtomVM virtual machine starts. Not all files in a Packbeam need to be BEAM modules -- you can embed any type of file in a Packbeam file, for consumption by your AtomVM application. > Note. The Packbeam format is described in more detail in the AtomVM [PackBEAM format](packbeam-format.md). The AtomVM community has provided several tools for simplifying your experience, as a developer. These tools allow you to use standard Erlang and Elixir tooling (such as `rebar3` and `mix`) to build Packbeam files and deploy then to your device of choice. ### `PackBEAM` tool The `PackBEAM` tool is a command-line application that can be used to create Packbeam files from a collection of input files: shell$ PackBEAM -h Usage: PackBEAM [-h] [-l] [] -h Print this help menu. -l List the contents of an AVM file. [-a] + Create an AVM file (archive if -a specified). To create a packbeam file, specify the name of the AVM file to created (by convention, ending in `.avm`), followed by a list of BEAM files: shell$ PackBEAM foo.avm path/to/foo.beam path/to/bar.beam You can also specify another AVM file to include. Thus, for example, to add to BEAM file to an existing AVM file, you might enter: shell$ PackBEAM foo.avm foo.avm path/to/gnu.beam To list the contents of an AVM file, use the `-l` flag: shell% PackBEAM -l foo.avm foo.beam * bar.beam gnu.beam Any BEAM files that export a `start/0` function will contain an asterisk (`*`) in the AVM file contents. ### Running AtomVM AtomVM is executed in different ways, depending on the platform. On most microcontrollers (e.g., the ESP32), the VM starts when the device is powered on. On UNIX platforms, the VM is started from the command-line using the `AtomVM` executable. AtomVM will use the first module in the supplied AVM file that exports a `start/0` function as the entrypoint for the application. #### `AtomVM` program syntax On UNIX platforms, you can specify a BEAM file or AVM file as the first argument to the executable, e.g., shell$ AtomVM foo.avm > Note. If you start the `AtomVM` executable with a BEAM file, then the corresponding module may not make any calls to external function in other modules, with the exception of built-in functions and Nifs that are included in the VM. ## Core APIs The AtomVM virtual machine provides a set of Erlang built-in functions (BIFs) and native functions (NIFs), as well as a collection of Erlang and Elixir libraries that can be used from your applications. This section provides an overview of these APIs. For more detailed information about specific APIs, please consult the [API reference documentation](api-reference-documentation.md). ### Standard Libraries AtomVM provides a limited implementations of standard library modules, including: * `base64` * `gen_server` * `gen_statem` * `io` and `io_lib` * `lists` * `maps` * `proplists` * `supervisor` * `timer` In addition AtomVM provides limited implementations of standard Elixir modules, including: * `List` * `Tuple` * `Enum` * `Kernel` * `Module` * `Process` * `Console` For detailed information about these functions, please consult the [API reference documentation](api-reference-documentation.md). These modules provide a strict subset of functionality from their Erlang/OTP counterparts. However, they aim to be API-compatible with the Erlang/OTP interfaces, at least for the subset of provided functionality. ### Console Output There are several mechanisms for writing data to the console. For common debugging, many users will find `erlang:display/1` sufficient for debugging: %% erlang erlang:display({foo, [{bar, tapas}]}). The output parameter is any Erlang term, and a newline will be appended automatically. Users may prefer using the `io:format/1,2` functions for more controlled output: %% erlang io:format("The ~p did a ~p~n", [friddle, frop]). Note that the `io_lib` module can be used to format string data, as well. > Note. Formatting parameters are currently limited to `~p`, `~s`, and `~n`. ### Process Management You can obtain a list of all processes in the system via `erlang:processes/0`: %% erlang Pids = erlang:processes(). And for each process, you can get detailed process information via the `erlang:process_info/1` function: %% erlang [io:format("Process info for Pid ~p: ~p~n", [Pid, erlang:process_info(Pid)]) || Pid <- Pids]. The return value is a property list containing values for `heap_size`, `stack_size`, `message_queue_len`,and `memory` consumed by the process. ### System APIs You can obtain system information about the AtomVM virtual machine via the `erlang:system_info/1` function, which takes an atom parameter designating the desired datum. Allowable parameters include * `process_count` The number of processes running in the system. * `port_count` The number of ports running in the system. * `atom_count` The number of atoms allocated in the system. * `word_size` The word size (in bytes) on the current platform (typically 4 or 8). For example, %% erlang io:format("Atom Count: ~p~n", [erlang:system_info(atom_count)]). > Note. Additional platform-specific information is supported, depending on the platform type. See below. Use the `atomvm:platform/0` to obtain the system platform on which your code is running. The return value of this function is an atom who's value will depend on the platform on which your application is running. %% erlang case atomvm:platform() of esp32 -> io:format("I am running on an ESP32!~n"); stm32 -> io:format("I am running on an STM32!~n"); generic_unix -> io:format("I am running on a UNIX box!~n") end. Use `erlang:garbage_collect/0` or `erlang:garbage_collect/1` to force the AtomVM garbage collector to run on a give process. Garbage collection will in general happen automatically when additional free space is needed and is rarely needed to be called explicitly. The 0-arity version of this function will run the garbage collector on the currently executing process. %% erlang Pid = ... %% get a reference to some pid ok = erlang:garbage_collect(Pid). ### System Time AtomVM supports numerous function for accessing the current time on the device. Use `erlang:timestamp/0` to get the current time since the UNIX epoch (Midnight, Jan 1, 1970, UTC), at microsecond granularity, expressed as a triple (mega-seconds, seconds, and micro-seconds): %% erlang {MegaSecs, Secs, MicroSecs} = erlang:timestamp(). User `erlang:system_time/1` to obtain the seconds or milliseconds since the UNIX epoch (Midnight, Jan 1, 1970, UTC): %% erlang Seconds = erlang:system_time(second). MilliSeconds = erlang:system_time(millisecond). Use `erlang:universaltime/0` to get the current time at second resolution, to obtain the year, month, day, hour, minute, and second: %% erlang {{Year, Month, Day}, {Hour, Minute, Second}} = erlang:universaltime(). > Note. Setting the system time is done in a platform-specific manner. For information about how to set system time on the ESP32, see the [Network Programming Guide](network-programming-guide.md). ### Miscellaneous Use the `erlang:md5/1` function to compute the MD5 hash of an input binary. The output is a fixed-length binary () %% erlang Hash = erlang:md5(<>). Use `atomvm:random/0` to generate a random unsigned 32-bit integer in the range `0..4294967295`: %% erlang RandomInetger = atomvm:random(). Use `atomvm:random_bytes/1` to return a randomly populated binary of a specified size: %% erlang RandomBinary = erlang:random_bytes(32). Use `base64:encode/1` and `base64:decode/1` to encode to and decode from Base64 format. The input value to these functions may be a binary or string. The output value from these functions is an Erlang binary. %% erlang Encoded = base64:encode(<<"foo">>). <<"foo">> = base64:decode(Encoded). You can Use `base64:encode_to_string/1` and `base64:decode_to_string/1` to perform the same encoding, but to return values as Erlang list structures, instead of as binaries. ## ESP32-specific APIs Certain APIs are specific to and only supported on the ESP32 platform. This section describes these APIs. ### System-Level APIs As noted above, the `erlang:system_info/1` function can be used to obtain system-specific information about the platform on which your application is deployed. You can request ESP32-specific information using using the following input atoms: * `esp_free_heap_size` Returns the available free space in the ESP32 heap * `esp_chip_info` Returns 4-tuple of the form `{esp32, Features, Cores, Revision}`, where `Features` is a bit mask of features enabled in the chip, `Cores` is the number of CPU cores on the chip, and `Revision` is the chip version. * `esp_idf_version` Return the IDF SDK version, as a string. For example, %% erlang FreeHeapSize = erlang:system_info(esp_free_heap_size). ### Non-volatile Storage AtomVM provides functions for setting, retrieving, and deleting key-value data in binary form in non-volatile storage (NVS) on an ESP device. Entries in NVS survive reboots of the ESP device, and can be used a limited "persistent store" for key-value data. > Note. NVS storage is limited in size, and NVS keys are restricted to 15 characters. Try to avoid writing frequently to NVS storage, as the flash storage may degrade more rapidly with repeated writes to the medium. NVS entries are stored under a namespace and key, both of which are expressed as atoms. AtomVM uses the namespace `atomvm` for entries under its control. Applications may read from and write to the `atomvm` namespace, but they are strongly discouraged from doing so, except when explicitly stated otherwise. To set a value in non-volatile storage, use the `esp:set_binary/3` function, and specify a namespace, key, and value: %% erlang Namespace = <<"my-namespace">>, Key = <<"my-key">>, esp:set_binary(Namespace, Key, <<"some-value">>). To retrieve a value in non-volatile storage, use the `esp:get_binary/2` function, and specify a namespace and key. You can optionally specify a default value (of any desired type), if an entry does not exist in non-volatile storage: %% erlang Value = esp:get_binary(Namespace, Key, <<"default-value">>). To delete an entry, use the `esp:erase_key/2` function, and specify a namespace and key: %% erlang ok = esp:erase_key(Namespace, Key). You can delete all entries in a namespace via the `esp:erase_all/1` function: %% erlang ok = esp:erase_all(Namespace). Finally, you can delete all entries in all namespaces on the NVS partition via the `esp:reformat/0` function: %% erlang ok = esp:reformat(). Applications should use the `esp:reformat/0` function with caution, in case other applications are making using the non-volatile storage. > Note. NVS entries are currently stored in plaintext and are not encrypted. Applications should exercise caution if sensitive security information, such as account passwords, are stored in NVS storage. ### Restart and Deep Sleep You can use the `esp:restart/0` function to immediately restart the ESP32 device. This function does not return a value. %% erlang esp:restart(). Use the `esp:reset_reason/0` function to obtain the reason for the ESP32 restart. Possible values include: * `esp_rst_unknown` * `esp_rst_poweron` * `esp_rst_ext` * `esp_rst_sw` * `esp_rst_panic` * `esp_rst_int_wdt` * `esp_rst_task_wdt` * `esp_rst_wdt` * `esp_rst_deepsleep` * `esp_rst_brownout` * `esp_rst_sdio` Use the `esp:deep_sleep/1` function to put the ESP device into deep sleep for a specified number of milliseconds. Be sure to safely stop any critical processes running before this function is called, as it will cause an immediate shutdown of the device. %% erlang esp:deep_sleep(60*1000). Use the `esp:sleep_get_wakeup_cause/0` function can be used to inspect the reason for a wakeup. Currently, the only supported return value is the atom `undefined` or `sleep_wakeup_timer`. %% erlang case esp:sleep_get_wakeup_cause() of sleep_wakeup_timer -> io:format("Woke up from a timer~n"); _ -> io:format("Woke up for some other reason~n") end. ### Miscellaneous The `freq_hz` function can be used to retrieve the clock frequency of the chip. * `esp:freq_hz/0` ## Peripherals The AtomVM virtual machine and libraries support APIs for interfacing with peripheral devices connected to the ESP32. This section provides information about these APIs. ### GPIO You can read and write digital values on GPIO pins using the `gpio` module, using the `digital_read/1` and `digital_write/2` functions. You must first set the direction of the pin using the `gpio:set_direction/2` function, using `input` or `output` as the direction parameter. To read the value of a GPIO pin (`high` or `low`), use `gpio:digital_read/1`: %% erlang Pin = 2, gpio:set_direction(Pin, input), case gpio:digital_read(Pin) of high -> io:format("Pin ~p is high ~n", [Pin]); low -> io:format("Pin ~p is low ~n", [Pin]) end. To set the value of a GPIO pin (`high` or `low`), use `gpio:digital_write/2`: %% erlang Pin = 2, gpio:set_direction(Pin, output), gpio:digital_write(Pin, low). #### Interrupt Handling You can get notified of changes in the state of a GPIO pin by using the `gpio:set_int/2` function. This function takes a reference to a GPIO Pin and a trigger. Allowable triggers are `rising`, `falling`, `both`, `low`, `high`, and `none` (to disable an interrupt). When a trigger event occurs, such as a pin rising in voltage, a tuple will be delivered to the process containing the atom `gpio_interrupt` and the pin. %% erlang Pin = 2, gpio:set_direction(Pin, input), GPIO = gpio:open(), ok = gpio:set_int(GPIO, Pin, rising), receive {gpio_interrupt, Pin} -> io:format("Pin ~p is rising ~n", [Pin]) end. Interrupts can be removed by using the `gpio:remove_int/2` function. ### I2C The `i2c` module encapsulates functionality associated with the 2-wire Inter-Integrated Circuit (I2C) interface. > Note. Information about the ESP32 I2C interface can be found in the IDF SDK [I2C Documentation](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/peripherals/i2c.html). The AtomVM I2C implementation uses the AtomVM Port mechanism and must be initialized using the `i2c:open/1` function. The single parameter contains a properties list, with the following elements: | Key | Value Type | Required | Description | |-----|------------|----------|---| | `scl_io_num` | `integer()` | yes | I2C clock pin (SCL) | | `sda_io_num` | `integer()` | yes | I2C data pin (SDA) | | `i2c_clock_hz` | `integer()` | yes | I2C clock frequency (in hertz) | For example, %% erlang I2C = i2c:open([ {scl_io_num, 21}, {sda_io_num, 22}, {i2c_clock_hz, 40000} ]) Once the port is opened, you can use the returned `I2C` instance to read and write bytes to the attached device. Both read and write operations require the I2C bus address from which data is read or to which data is written. A devices address is typically hard-wired for the specific device type, or in some cases may be changed by the addition or removal of a resistor. In addition, you may optionally specify a register to read from or write to, as some devices require specification of a register value. Consult your device's data sheet for more information and the device's I2C bus address and registers, if applicable. There are two patterns for writing data to an I2C device: 1. Queuing `i2c:qwrite_bytes/2,3` write operations between calls to `i2c:begin_transmission/1` and `i2c:end_transmission/1`. In this case, write operations are queued locally and dispatched to the target device when the `i2c:end_transmission/1` operation is called; 1. Writing a byte or sequence of bytes in one `i2c:write_bytes/2,3` operation. The choice of which pattern to use will depend on the device being communicated with. For example, some devices require a sequence of write operations to be queued and written in one atomic write, in which case the first pattern is appropriate. E.g., %% erlang ok = i2c:begin_transmission(I2C), ok = i2c:qwrite_bytes(I2C, DeviceAddress, Register1, <<"some sequence of bytes">>), ok = i2c:qwrite_bytes(I2C, DeviceAddress, Register2, <<"some other of bytes">>), ok = i2c:end_transmission(I2C), In other cases, you may just need to write a byte or sequence of bytes in one operation to the device: %% erlang ok = i2c:write_bytes(I2C, DeviceAddress, Register1, <<"write it all in one go">>), Reading bytes is more straightforward. Simply use `i2c:read_bytes/3,4`, specifying the port instance, device address, optionally a register, and the number of bytes to read: %% erlang BinaryData = i2c:read_bytes(I2C, DeviceAddress, Register, Len) ### SPI The `spi` module encapsulates functionality associated with the 4-wire Serial Peripheral Interface (SPI) in leader mode. > Note. Information about the ESP32 SPI leader mode interface can be found in the IDF SDK [SPI Documentation](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/peripherals/spi_master.html). The AtomVM SPI implementation uses the AtomVM Port mechanism and must be initialized using the `spi:open/1` function. The single parameter to this function is a properties list containing two elements: * `bus_config` -- a properties list containing entries for the SPI bus * `device_config` -- a properties list containing entries for the device The `bus_config` properties list contains the following entries: | Key | Value Type | Required | Description | |-----|------------|----------|---| | `miso_io_num` | `integer()` | yes | SPI leader-in, follower-out pin (MOSI) | | `mosi_io_num` | `integer()` | yes | SPI leader-out, follower-in pin (MISO) | | `sclk_io_num` | `integer()` | yes | SPI clock pin (SCLK) | The `device_config` -- a properties list containing entries for the device properties list contains the following entries: | Key | Value Type | Required | Description | |-----|------------|----------|---| | `spi_clock_hz` | `integer()` | yes | SPI clock frequency (in hertz) | | `spi_mode` | `integer()` | yes | SPI mode | | `spi_cs_io_num` | `integer()` | yes | SPI chip select pin (CS) | | `address_len_bits` | `integer()` | yes | number of bits in a read/write operation (for example, 8, to read and write single bytes at a time) | For example, %% erlang SPIConfig = [ {bus_config, [ {miso_io_num, 12}, {mosi_io_num, 13}, {sclk_io_num, 14} ]}, {device_config, [ {spi_clock_hz, 1000000}, {spi_mode, 0}, {spi_cs_io_num, 18}, {address_len_bits, 8} ]} ], SPI = spi:open(SPIConfig), ... Once the port is opened, you can use the returned `SPI` instance to read and write bytes to the attached device. To read a byte at a given address on the device, use the `spi:read_at/3` function: %% erlang {ok, Byte} = spi:read_at(SPI, Address, 8) To write a byte at a given address on the device, use the `spi_write_at/4` function: %% erlang write_at(SPI, Address, 8, Byte) > Note. The `spi:write_at/4` takes integer values as inputs and the `spi:read_at/3` returns integer values. You may read and write up to 32-bit integer values via these functions. Consult your local device data sheet for information about various device addresses to read from or write to, and their semantics. ### UART The `uart` module encapsulates functionality associated with the Universal Asynchronous Receiver/Transmitter (UART) interface supported on ESP32 devices. Some devices, such as NMEA GPS receivers, make use of this interface for communicating with an ESP32. > Note. Information about the ESP32 UART interface can be found in the IDF SDK [UART Documentation](https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/peripherals/uart.html). The AtomVM UART implementation uses the AtomVM Port mechanism and must be initialized using the `uart:open/2` function. The first parameter indicates the ESP32 UART hardware interface. Legal values are: "UART0" | "UART1" | "UART2" The selection of the hardware interface dictates the default RX and TX pins on the ESP32: | Port | RX pin | TX pin | |-------------|--------|-------| | `UART0` | GPIO_3 | GPIO_1 | | `UART1` | GPIO_9 | GPIO_10 | | `UART2` | GPIO_16 | GPIO_17 | The second parameter is a properties list, containing the following elements: | Key | Value Type | Required | Default Value | Description | |-----|------------|----------|---------------|-------------| | `speed` | `integer()` | no | 115200 | UART baud rate (bits/sec) | | `data_bits` | `5 \| 6 \| 7 \| 8` | no | 8 | UART data bits | | `stop_bits` | `1 \| 2` | no | 1 | UART stop bits | | `flow_control` | `hardware \| software \| none` | no | `none` | Flow control | | `parity` | `even \| odd \| none` | no | `none` | UART parity check | For example, %% erlang UART = uart:open("UART0", [{speed, 9600}]) Once the port is opened, you can use the returned `UART` instance to read and write bytes to the attached device. To read data from the UART channel, use the `uart:read/1` function. The return value from this function is a binary: %% erlang Bin = uart:read(UART) To write data to the UART channel, use the `uart_write/2` function. The input data is any Erlang I/O list: %% erlang uart:write(UART, [<<"any">>, $d, $a, $t, $a, "goes", <<"here">>]) Consult your local device data sheet for information about the format of data to be read from or written to the UART channel. ### LED Control The LED Control API can be used to drive LEDs, as well as generate PWM signals on GPIO pins. The LEDC API is encapsulated in the `ledc` module and is a direct translation of the IDF SDK LEDC API, with a natural mapping into Erlang. This API is intended for users with complex use-cases, and who require low-level access to the LEDC APIs. The `ledc.hrl` module should be used for common modes, channels, duty cycle resolutions, and so forth. %% erlang -include("ledc.hrl"). ... %% create a 5khz timer SpeedMode = ?LEDC_HIGH_SPEED_MODE, Channel = ?LEDC_CHANNEL_0, ledc:timer_config([ {duty_resolution, ?LEDC_TIMER_13_BIT}, {freq_hz, 5000}, {speed_mode, ?LEDC_HIGH_SPEED_MODE}, {timer_num, ?LEDC_TIMER_0} ]). %% bind pin 2 to this timer in a channel ledc:channel_config([ {channel, Channel}, {duty, 0}, {gpio_num, 2}, {speed_mode, ?LEDC_HIGH_SPEED_MODE}, {hpoint, 0}, {timer_sel, ?LEDC_TIMER_0} ]). %% set the duty cycle to 0, and fade up to 16000 over 5 seconds ledc:set_duty(SpeedMode, Channel, 0). ledc:update_duty(SpeedMode, Channel). TargetDuty = 16000. FadeMs = 5000. ok = ledc:set_fade_with_time(SpeedMode, Channel, TargetDuty, FadeMs). ## Protocols AtomVM supports network programming on devices that support it, specifically the ESP32 platform, with its built-in support for WIFI networking, and of course on the UNIX platform. This section describes the network programming APIs available on AtomVM. ### Network (ESP32 only) The ESP32 supports WiFi connectivity as part of the built-in WiFi and Bluetooth radio (and in most modules, an integrated antenna). The WIFI radio on an ESP32 can operate in several modes: * STA (Station) mode, whereby it acts as a member of an existing WiFi network; * AP (Access Point) mode, whereby the ESP32 acts as an access point for other devices; or * AP+STA mode, whereby the ESP32 behaves both as a member of an existing WiFi network and as an access point for other devices. AtomVM supports these modes of operation via the `network` module, which is used to initialize the network and allow applications to respond to events within the network, such as a network disconnect or reconnect, or a connection to the ESP32 from another device. > Note. Establishment and maintenance of network connections on roaming devices is a complex and subtle art, and the AtomVM `network` module is designed to accommodate as many IoT scenarios as possible. This section of the programmer's guide is deliberately brief and only addresses the most basic scenarios. For a more detailed explanation of the AtomVM `network` module and its many use-cases, please refer to the [AtomVM Network Programming Guide](network-programming-guide.md). #### STA mode To connect your ESP32 to an existing WiFi network, use the `network:wait_for_sta/1,2` convenience function, which abstracts away some of the more complex details of ESP32 STA mode. This function takes a station mode configuration, as a properties list, and optionally a timeout (in milliseconds) before connecting to the network should fail. The default timeout, if unspecified, is 15 seconds. The station mode configuration supports the following options: | Key | Value Type | Required | Default Value | Description | |-----|------------|----------|---------------|-------------| | `ssid` | `string() \| binary()` | yes | - | WiFi AP SSID | | `psk` | `string() \| binary()` | yes, if network is encrypted | - | WiFi AP password | | `dhcp_hostname` | `string() \| binary()` | no | `atomvm-` where `` is the factory-assigned MAC-address of the device | DHCP hostname for the connecting device | > Note. The WiFi network to which you are connecting must support DHCP and IPv4. IPv6 addressing is not yet supported on AtomVM. If the ESP32 device connects to the specified network successfully, the device's assigned address, netmask, and gateway address will be returned in an `{ok, ...}` tuple; otherwise, an error is returned. For example: %% erlang Config = [ {ssid, <<"myssid">>}, {psk, <<"mypsk">>}, {dhcp_hostname, <<"mydevice">>} ], case network:wait_for_sta(Config, 15000) of {ok, {Address, _Netmask, _Gateway}} -> io:format("Acquired IP address: ~p~n", [Address]); {error, Reason} -> io:format("Network initialization failed: ~p~n", [Reason]) end Once connected to a WiFi network, you may begin TCP or UDP networking, as described in more detail below. For information about how to handle disconnections and reconnections to a WiFi network, see the [AtomVM Network Programming Guide](network-programming-guide.md). #### AP mode To turn your ESP32 into an access point for other devices, you can use the `network:wait_for_ap/1,2` convenience function, which abstracts away some of the more complex details of ESP32 AP mode. When the network is started, the ESP32 device will assign itself the `192.168.4.1` address. Any devices that connect to the ESP32 will take addresses in the `192.168.4/24` network. This function takes an access point mode configuration, as a properties list, and optionally a timeout (in milliseconds) before starting the network should fail. The default timeout, if unspecified, is 15 seconds. The access point mode configuration supports the following options: | Key | Value Type | Required | Default Value | Description | |-----|------------|----------|---------------|-------------| | `ssid` | `string() \| binary()` | no | `atomvm-` where `` is the factory-assigned MAC-address of the device | WiFi AP SSID | | `ssid_hidden` | `boolean()` | no | `false` | Whether the AP SSID should be hidden (i.e., not broadcast) | | `psk` | `string() \| binary()` | yes, if network is encrypted | - | WiFi AP password. Warning: If this option is not specified, the network will be an open network, to which anyone who knows the SSID can connect and which is not encrypted. | | `ap_max_connections` | `non_neg_integer()` | no | `4` | Maximum number of devices that can be connected to this AP | If the ESP32 device starts the AP network successfully, the `ok` atom is returned; otherwise, an error is returned. For example: %% erlang Config = [ {psk, <<"mypsk">>} ], case network:wait_for_ap(Config, 15000) of ok -> io:format("AP network started at 192.168.4.1~n"); {error, Reason} -> io:format("Network initialization failed: ~p~n", [Reason]) end Once the WiFi network is started, you may begin TCP or UDP networking, as described in more detail below. For information about how to handle connections and disconnections from attached devices, see the [AtomVM Network Programming Guide](network-programming-guide.md). #### STA+AP mode For information about how to run the AtomVM network in STA and AP mode simultaneously, see the [AtomVM Network Programming Guide](network-programming-guide.md). ### UDP AtomVM supports network programming using the User Datagram Protocol (UDP) via the `gen_udp` module. This modules obeys the syntax and semantics of the Erlang/OTP [`gen_udp`](https://erlang.org/doc/man/gen_udp.html) interface. > Note. Not all of the Erlang/OTP `gen_udp` functionality is implemented in AtomVM. For details, consults the AtomVM API documentation. To open a UDP port, use the `gen_udp:open/1,2` function. Supply a port number, and if your application plans to receive UDP messages, specify that the port is active via the `{active, true}` property in the optional properties list. For example: %% erlang Port = 44404, case gen_udp:open(Port, [{active, true}]) of {ok, Socket} -> {ok, SockName} = inet:sockname(Socket) io:format("Opened UDP socket on ~p.~n", [SockName]) Error -> io:format("An error occurred opening UDP socket: ~p~n", [Error]) end If the port is active, you can receive UDP messages in your application. They will be delivered as a 5-tuple, starting with the `udp` atom, and containing the socket, address and port from which the message was sent, as well as the datagram packet, itself, as a binary. %% erlang receive {udp, _Socket, Address, Port, Packet} -> io:format("Received UDP packet ~p from address ~p port ~p~n", [Packet, Address, Port)]) end, With a reference to a UDP `Socket`, you can send messages to a target UDP endpoint using the `gen_udp:send/4` function. Specify the UDP socket returned from `gen_udp:open/1,2`, the address (as a 4-tuple of octets), port number, and the datagram packet to send: Packet = <<":アトムVM">>, Address = {192, 168, 1, 101}, Port = 44404, case gen_udp:send(Socket, Address, Port, Packet) of ok -> io:format("Sent ~p~n", [Packet]); Error -> io:format("An error occurred sending a packet: ~p~n", [Error]) end > Note. IPv6 networking is not currently supported in AtomVM. ### TCP AtomVM supports network programming using the Transport Connection Protocol (TCP) via the `gen_tcp` module. This modules obeys the syntax and semantics of the Erlang/OTP [`gen_tcp`](https://erlang.org/doc/man/gen_tcp.html) interface. > Note. Not all of the Erlang/OTP `gen_tcp` functionality is implemented in AtomVM. For details, consults the AtomVM API documentation. #### Server-side TCP Server side TCP requires opening a listening socket, and then waiting to accept connections from remote clients. Once a connection is established, the application may then use a combination of sending and receiving packets over the established connection to or from the remote client. > Note. Programming TCP on the server-side using the `gen_tcp` interface is a subtle art, and this portion of the documentation will not go into all of the design choices available when designing a TCP application. Start by opening a listening socket using the `gen_tcp:listen/2` function. Specify the port number on which the TCP server should be listening: %% erlang case gen_tcp:listen(44405, []) of {ok, ListenSocket} -> {ok, SockName} = inet:sockname(Socket), io:format("Listening for connections at address ~p.~n", [SockName]), spawn(fun() -> accept(ListenSocket) end); Error -> io:format("An error occurred listening: ~p~n", [Error]) end. In this particular example, the server will spawn a new process to wait to accept a connection from a remote client, by calling the `gen_tcp:accept/1` function, passing in a reference to the listening socket. This function will block until a client has established a connection with the server. When a client connects, the function will return a tuple `{ok, Socket}`, where `Socket` is a reference to the connection between the client and server: %% erlang accept(ListenSocket) -> io:format("Waiting to accept connection...~n"), case gen_tcp:accept(ListenSocket) of {ok, Socket} -> {ok, SockName} = inet:sockname(Socket), {ok, Peername} = inet:peername(Socket), io:format("Accepted connection. local: ~p peer: ~p~n", [SockName, Peername]), spawn(fun() -> accept(ListenSocket) end), echo(); Error -> io:format("An error occurred accepting connection: ~p~n", [Error]) end. > Note that immediately after accepting a connection, this example code will spawn a new process to accept any new connections from other clients. The socket returned from `gen_tcp:accept/1` can then be used to send and receive messages to the connected client: %% erlang echo() -> io:format("Waiting to receive data...~n"), receive {tcp_closed, _Socket} -> io:format("Connection closed.~n"), ok; {tcp, Socket, Packet} -> {ok, Peername} = inet:peername(Socket), io:format("Received packet ~p from ~p. Echoing back...~n", [Packet, Peername]), gen_tcp:send(Socket, Packet), echo() end. In this case, the server program will continuosuly echo the received input back to the client, until the client closes the connection. For more information about the `gen_tcp` server interface, consult the AtomVM [API Reference Documentation](api-reference-documentation.md). #### Client-side TCP Client side TCP requires establishing a connection with an endpoint, and then using a combination of sending and receiving packets over the established connection. Start by opening a connection to another TCP endpoint using the `gen_tcp:connect/3` function. Supply the address and port of the TCP endpoint. For example: %% erlang Address = {192, 168, 1, 101}, Port = 44405, case gen_tcp:connect(Address, Port, []) of {ok, Socket} -> {ok, SockName} = inet:sockname(Socket), {ok, Peername} = inet:peername(Socket), io:format("Connected to ~p from ~p~n", [Peername, SockName]); Error -> io:format("An error occurred connecting: ~p~n", [Error]) end Once a connection is established, you can use a combination of %% erlang SendPacket = <<":アトムVM">>, case gen_tcp:send(Socket, SendPacket) of ok -> receive {tcp_closed, _Socket} -> io:format("Connection closed.~n"), ok; {tcp, _Socket, ReceivedPacket} -> {ok, Peername} = inet:peername(Socket), io:format("Received ~p from ~p~n", [ReceivedPacket, Peername]) end; Error -> io:format("An error occurred sending a packet: ~p~n", [Error]) end. For more information about the `gen_tcp` client interface, consults the AtomVM API documentation.