diff --git a/CMakeModules/BuildErlang.cmake b/CMakeModules/BuildErlang.cmake
index f034411aaa..0ac37a2ba6 100644
--- a/CMakeModules/BuildErlang.cmake
+++ b/CMakeModules/BuildErlang.cmake
@@ -287,7 +287,7 @@ macro(pack_runnable avm_name main)
foreach(archive_name ${ARGN})
if(NOT ${archive_name} STREQUAL "exavmlib")
set(pack_runnable_${avm_name}_archives ${pack_runnable_${avm_name}_archives} ${CMAKE_BINARY_DIR}/libs/${archive_name}/src/${archive_name}.avm)
- if(NOT ${archive_name} MATCHES "^eavmlib|estdlib|alisp|avm_network|avm_esp32|avm_rp2|avm_stm32|avm_emscripten$")
+ if(NOT ${archive_name} MATCHES "^(eavmlib|estdlib|alisp|avm_network|avm_esp32|avm_rp2|avm_stm32|avm_emscripten)$")
set(${avm_name}_dialyzer_beams_opt ${${avm_name}_dialyzer_beams_opt} "-r" ${CMAKE_BINARY_DIR}/libs/${archive_name}/src/beams/)
endif()
else()
diff --git a/libs/avm_rp2/src/gpio.erl b/libs/avm_rp2/src/gpio.erl
index 0d8b4e4e16..deeecfca94 100644
--- a/libs/avm_rp2/src/gpio.erl
+++ b/libs/avm_rp2/src/gpio.erl
@@ -65,7 +65,7 @@
-type low_level() :: low | 0.
-type high_level() :: high | 1.
-type level() :: low_level() | high_level().
-%% Valid pin levels can be atom or binary representation.
+%% Valid pin levels can be atom or integer representation.
-type gpio() :: pid().
%% This is the pid returned by `gpio:start/0'. Unlike ESP32 and STM32, this
%% is not a real port but a process wrapping NIF calls.
@@ -118,6 +118,8 @@ close(GPIO) ->
GPIO ! {'$call', {self(), Ref}, {close}},
receive
{Ref, Result} -> Result
+ after 5000 ->
+ {error, timeout}
end.
%%-----------------------------------------------------------------------------
@@ -227,7 +229,7 @@ remove_int(_GPIO, _Pin) ->
%%-----------------------------------------------------------------------------
-spec init(Pin :: pin()) -> ok.
init(_Pin) ->
- ok.
+ erlang:nif_error(undefined).
%%-----------------------------------------------------------------------------
%% @param Pin number to deinitialize
@@ -237,7 +239,7 @@ init(_Pin) ->
%%-----------------------------------------------------------------------------
-spec deinit(Pin :: pin()) -> ok.
deinit(_Pin) ->
- ok.
+ erlang:nif_error(undefined).
%%-----------------------------------------------------------------------------
%% @param Pin number to set operational mode
diff --git a/libs/avm_stm32/src/gpio.erl b/libs/avm_stm32/src/gpio.erl
index bf9550a44b..1ab14fc403 100644
--- a/libs/avm_stm32/src/gpio.erl
+++ b/libs/avm_stm32/src/gpio.erl
@@ -56,7 +56,7 @@
%% a list of pin numbers, or the atom `all'.
-type gpio_bank() :: a | b | c | d | e | f | g | h | i | j | k.
%% STM32 gpio banks vary by board, some only break out `a' thru `h'.
--type direction() :: input | output | output_od | mode_config().
+-type direction() :: input | output | output_od.
%% The direction is used to set the mode of operation for a GPIO pin, either as an input, an output, or output with open drain.
%% Pull mode and output_speed must be set at the same time as direction. See @type mode_config()
-type mode_config() :: {direction(), pull()} | {output, pull(), output_speed()}.
@@ -68,7 +68,7 @@
-type low_level() :: low | 0.
-type high_level() :: high | 1.
-type level() :: low_level() | high_level().
-%% Valid pin levels can be atom or binary representation.
+%% Valid pin levels can be atom or integer representation.
-type gpio() :: port().
%% This is the port returned by `gpio:start/0'.
-type trigger() :: none | rising | falling | both | low | high.
@@ -79,7 +79,7 @@
%% @doc Start the GPIO driver port
%%
%% Returns the port of the active GPIO port driver, otherwise the GPIO
-%% port driver will be stared and registered as `gpio'. The use of
+%% port driver will be started and registered as `gpio'. The use of
%% `gpio:open/0' or `gpio:start/0' is required before using any functions
%% that require a GPIO port as a parameter.
%% @end
@@ -97,7 +97,7 @@ start() ->
%% @returns Port | error | {error, Reason}
%% @doc Start the GPIO driver port
%%
-%% The GPIO port driver will be stared and registered as `gpio'. If the
+%% The GPIO port driver will be started and registered as `gpio'. If the
%% port has already been started through the `gpio:open/0' or
%% `gpio:start/0' the command will fail. The use of `gpio:open/0' or
%% `gpio:start/0' is required before using any functions that require a
@@ -163,7 +163,7 @@ read(GPIO, Pin) ->
%% Pins can be used for input, output, or output with open drain.
%% @end
%%-----------------------------------------------------------------------------
--spec set_direction(GPIO :: gpio(), Pin :: pin(), Direction :: direction()) ->
+-spec set_direction(GPIO :: gpio(), Pin :: pin(), Direction :: direction() | mode_config()) ->
ok | {error, Reason :: atom()} | error.
set_direction(GPIO, Pin, Direction) ->
port:call(GPIO, {set_direction, Pin, Direction}).
@@ -274,7 +274,7 @@ deinit(_Pin) ->
%%
%% @end
%%-----------------------------------------------------------------------------
--spec set_pin_mode(Pin :: pin(), Direction :: direction()) ->
+-spec set_pin_mode(Pin :: pin(), Direction :: direction() | mode_config()) ->
ok | {error, Reason :: atom()} | error.
set_pin_mode(_Pin, _Mode) ->
erlang:nif_error(undefined).
diff --git a/libs/eavmlib/src/gpio_hal.erl b/libs/eavmlib/src/gpio_hal.erl
index 703df71789..33ef787a44 100644
--- a/libs/eavmlib/src/gpio_hal.erl
+++ b/libs/eavmlib/src/gpio_hal.erl
@@ -24,6 +24,106 @@
%% This module defines the behavior that platform-specific GPIO modules
%% must implement. It provides a common interface for basic GPIO operations
%% across all supported platforms (ESP32, RP2, STM32).
+%%
+%% There are two APIs for GPIO operations:
+%%
+%%
NIF-based API
+%%
+%% The NIF-based API provides direct access to GPIO pins without requiring
+%% a driver process or port. Functions operate directly on pin numbers.
+%%
+%%
+%% - `init/1' - Initialize a pin for GPIO use (required on RP2, no-op on
+%% ESP32 and STM32)
+%% - `deinit/1' - Release a pin from GPIO use
+%% - `set_pin_mode/2' - Configure a pin as input, output, or output with
+%% open drain
+%% - `set_pin_pull/2' - Configure internal pull resistors
+%% - `digital_write/2' - Set pin output level
+%% - `digital_read/1' - Read pin input level
+%%
+%%
+%% Port-based API
+%%
+%% The port-based API requires starting a GPIO driver using `start/0' or
+%% `open/0', which returns a handle (a port on ESP32 and STM32, a pid on
+%% RP2). This handle is passed to all subsequent operations.
+%%
+%%
+%% - `start/0' - Start the GPIO driver, or return an existing one if
+%% already started
+%% - `open/0' - Start a new GPIO driver instance
+%% - `close/1' - Stop a GPIO driver and release its resources
+%% - `stop/0' - Stop the registered GPIO driver if one is running
+%% - `read/2' - Read pin input level
+%% - `set_direction/3' - Configure a pin as input, output, or output with
+%% open drain
+%% - `set_level/3' - Set pin output level
+%% - `set_int/3' - Set a GPIO interrupt on a pin, notifications are sent
+%% to the calling process
+%% - `set_int/4' - Set a GPIO interrupt on a pin, notifications are sent
+%% to the specified process
+%% - `remove_int/2' - Remove a previously set GPIO interrupt
+%%
+%%
+%% Pin definitions
+%%
+%% Pin definitions vary by platform:
+%%
+%% - ESP32: a non-negative integer (e.g. `2', `15')
+%% - RP2: a non-negative integer, or `{wl, 0..2}' for Pico-W
+%% wireless pins
+%% - STM32: a tuple `{Bank, PinNum}' where Bank is an atom `a' through
+%% `k' and PinNum is `0..15', a list of pin numbers, or the atom
+%% `all'
+%%
+%%
+%% Platform differences
+%%
+%%
+%% - Interrupt support: ESP32 and STM32 support interrupts. RP2 does not
+%% (returns `{error, not_supported}').
+%% - Interrupt message format: on ESP32, `{gpio_interrupt, Pin}' where
+%% Pin is an integer; on STM32, `{gpio_interrupt, {Bank, Pin}}'.
+%% - Pull modes: ESP32 and RP2 support `up', `down', `up_down', and
+%% `floating'. STM32 supports `up', `down', and `floating' only.
+%% - `init/1' must be called before using a pin on RP2. On ESP32 and
+%% STM32 it is a no-op.
+%% - STM32 supports batch operations: multiple pins on the same bank
+%% can be configured or written at once.
+%%
+%%
+%% Example usage (NIF-based API)
+%%
+%% The following example configures pin 2 as an output and sets it high:
+%%
+%% ```
+%% gpio:init(2),
+%% gpio:set_pin_mode(2, output),
+%% gpio:digital_write(2, high).
+%% '''
+%%
+%% The following example configures pin 4 as an input with a pull-up
+%% resistor and reads the level:
+%%
+%% ```
+%% gpio:init(4),
+%% gpio:set_pin_mode(4, input),
+%% gpio:set_pin_pull(4, up),
+%% Level = gpio:digital_read(4).
+%% '''
+%%
+%% Example usage (Port-based API)
+%%
+%% ```
+%% GPIO = gpio:start(),
+%% gpio:set_direction(GPIO, 2, output),
+%% gpio:set_level(GPIO, 2, high),
+%% gpio:set_int(GPIO, 4, rising),
+%% receive
+%% {gpio_interrupt, 4} -> io:format("Pin 4 triggered!~n")
+%% end.
+%% '''
%% @end
%%-----------------------------------------------------------------------------
-module(gpio_hal).
@@ -34,6 +134,7 @@
-type pull() :: up | down | up_down | floating.
%% Internal resistor pull mode.
+%% Note: STM32 does not support `up_down'.
-type low_level() :: low | 0.
-type high_level() :: high | 1.
@@ -42,53 +143,130 @@
-type trigger() :: none | rising | falling | both | low | high.
%% Event type that will trigger a `gpio_interrupt'.
+%% Setting trigger to `none' disables the interrupt.
-type gpio() :: port() | pid().
%% Handle returned by `start/0' or `open/0'.
+%% On ESP32 and STM32, this is a port. On RP2, this is a pid.
-export_type([direction/0, pull/0, level/0, trigger/0, gpio/0]).
%% NIF-based API
+% Initialize a pin for GPIO use.
+%
+% This must be called before using a pin on the RP2 platform. On ESP32
+% and STM32, this function is a no-op and always returns `ok'.
-callback init(Pin :: term()) -> ok.
+% Deinitialize a pin, releasing it from GPIO use.
+%
+% Resets the pin back to its default (NULL) function.
-callback deinit(Pin :: term()) -> ok.
+% Set the operational mode of a pin.
+%
+% Configures a pin as `input', `output', or `output_od' (output with
+% open drain). The pin should be initialized with `init/1' first on
+% platforms that require it.
-callback set_pin_mode(Pin :: term(), Direction :: direction()) ->
ok | {error, Reason :: atom()} | error.
+% Configure the internal pull resistor of a pin.
+%
+% Pins can be pulled `up', `down', `up_down' (pulled in both
+% directions), or left `floating'. Not all pull modes are supported
+% on all platforms (e.g. STM32 does not support `up_down').
-callback set_pin_pull(Pin :: term(), Pull :: pull()) -> ok | error.
+% Set the digital output level of a pin.
+%
+% Sets the pin to `high' (or `1') or `low' (or `0'). The pin should
+% be configured as an output using `set_pin_mode/2' first.
-callback digital_write(Pin :: term(), Level :: level()) ->
ok | {error, Reason :: atom()} | error.
+% Read the digital input level of a pin.
+%
+% Returns `high' or `low'. The pin should be configured as an input
+% using `set_pin_mode/2' first; otherwise it may always read as `low'.
-callback digital_read(Pin :: term()) ->
high | low | {error, Reason :: atom()} | error.
%% Port-based API
+% Start the GPIO driver.
+%
+% Returns the handle of an existing GPIO driver if one is already
+% registered, or starts a new one. The returned handle is required
+% for all port-based API functions.
-callback start() -> gpio() | {error, Reason :: atom()} | error.
+% Open a new GPIO driver instance.
+%
+% Always starts a new GPIO driver instance and registers it. Fails
+% if a driver is already registered. Use `start/0' to get an existing
+% instance or start a new one.
-callback open() -> gpio() | {error, Reason :: atom()} | error.
+% Close a GPIO driver and release its resources.
+%
+% This disables any active interrupts, stops the driver, and frees
+% all associated resources.
-callback close(GPIO :: gpio()) -> ok | {error, Reason :: atom()} | error.
+% Stop the registered GPIO driver.
+%
+% If a GPIO driver is registered, it is closed and its resources are
+% freed. If no driver is registered, returns `ok'.
-callback stop() -> ok | {error, Reason :: atom()} | error.
+% Read the digital input level of a pin using the port-based API.
+%
+% Returns `high' or `low'. The pin should be configured as an input
+% using `set_direction/3' first; otherwise it may always read as `low'.
-callback read(GPIO :: gpio(), Pin :: term()) ->
high | low | {error, Reason :: atom()} | error.
+% Set the operational mode of a pin using the port-based API.
+%
+% Configures a pin as `input', `output', or `output_od' (output with
+% open drain).
-callback set_direction(GPIO :: gpio(), Pin :: term(), Direction :: direction()) ->
ok | {error, Reason :: atom()} | error.
+% Set the digital output level of a pin using the port-based API.
+%
+% Sets the pin to `high' (or `1') or `low' (or `0'). The pin should
+% be configured as an output using `set_direction/3' first.
-callback set_level(GPIO :: gpio(), Pin :: term(), Level :: level()) ->
ok | {error, Reason :: atom()} | error.
+% Set a GPIO interrupt on a pin.
+%
+% Configures the pin to trigger an interrupt on the specified condition.
+% When triggered, a message `{gpio_interrupt, Pin}' is sent to the
+% calling process (the exact format of Pin in the message depends on
+% the platform).
+%
+% Not supported on all platforms. RP2 returns `{error, not_supported}'.
-callback set_int(GPIO :: gpio(), Pin :: term(), Trigger :: trigger()) ->
ok | {error, Reason :: atom()} | error.
+% Set a GPIO interrupt on a pin, sending notifications to a
+% specific process.
+%
+% Same as `set_int/3', but the interrupt message `{gpio_interrupt, Pin}'
+% is sent to the specified `Pid' instead of the calling process.
+%
+% Not supported on all platforms. RP2 returns `{error, not_supported}'.
-callback set_int(GPIO :: gpio(), Pin :: term(), Trigger :: trigger(), Pid :: pid()) ->
ok | {error, Reason :: atom()} | error.
+% Remove a GPIO interrupt from a pin.
+%
+% Disables a previously configured interrupt on the specified pin.
+%
+% Not supported on all platforms. RP2 returns `{error, not_supported}'.
-callback remove_int(GPIO :: gpio(), Pin :: term()) ->
ok | {error, Reason :: atom()} | error.
diff --git a/libs/eavmlib/src/i2c_hal.erl b/libs/eavmlib/src/i2c_hal.erl
index 2f5c1c86de..650d77d8bf 100644
--- a/libs/eavmlib/src/i2c_hal.erl
+++ b/libs/eavmlib/src/i2c_hal.erl
@@ -22,54 +22,150 @@
%% @doc I2C Hardware Abstraction Layer behavior
%%
%% This module defines the behavior that platform-specific I2C modules
-%% must implement. It provides a common interface for I2C operations
-%% across all supported platforms.
+%% must implement. It provides a common interface for I2C (Inter-Integrated
+%% Circuit) operations across all supported platforms.
+%%
+%% Currently, only ESP32 provides an I2C implementation.
+%%
+%% Lifecycle
+%%
+%% An I2C bus is opened with `open/1' and closed with `close/1'. The
+%% returned handle is passed to all subsequent operations.
+%%
+%% Reading data
+%%
+%% Data can be read from I2C devices using `read_bytes/3' or
+%% `read_bytes/4':
+%%
+%%
+%% - `read_bytes/3' reads a number of bytes directly from a device
+%% at the given address.
+%% - `read_bytes/4' reads a number of bytes from a specific register
+%% within a device.
+%%
+%%
+%% Writing data
+%%
+%% There are two approaches for writing data:
+%%
+%% Direct writes: `write_bytes/3' and `write_bytes/4' write data
+%% to a device address, optionally specifying a register:
+%%
+%% ```
+%% i2c:write_bytes(I2C, 16#68, 16#0D, <<16#FF>>).
+%% '''
+%%
+%% Transaction-based writes: For more control, use
+%% `begin_transmission/2', followed by one or more calls to
+%% `write_byte/2' or `write_bytes/2', and finalize with
+%% `end_transmission/1':
+%%
+%% ```
+%% i2c:begin_transmission(I2C, 16#68),
+%% i2c:write_byte(I2C, 16#0D),
+%% i2c:write_bytes(I2C, <<16#FF, 16#00>>),
+%% i2c:end_transmission(I2C).
+%% '''
+%%
+%% Configuration parameters
+%%
+%% The `open/1' function accepts a proplist of configuration parameters.
+%% Common parameters include:
+%%
+%%
+%% - `{scl, pin()}' - SCL (clock) pin number
+%% - `{sda, pin()}' - SDA (data) pin number
+%% - `{clock_speed_hz, pos_integer()}' - Clock speed in Hz
+%% - `{peripheral, string() | binary()}' - I2C peripheral name (e.g.
+%% `"i2c0"', `"i2c1"')
+%%
+%%
+%% Example
+%%
+%% ```
+%% I2C = i2c:open([{scl, 22}, {sda, 21}, {clock_speed_hz, 100000}]),
+%% {ok, Data} = i2c:read_bytes(I2C, 16#68, 16#75, 1),
+%% i2c:write_bytes(I2C, 16#68, 16#6B, 0),
+%% i2c:close(I2C).
+%% '''
%% @end
%%-----------------------------------------------------------------------------
-module(i2c_hal).
-type i2c() :: port() | pid() | term().
%% Handle returned by `open/1'.
+%% On ESP32, this is either a port (port driver mode) or a resource
+%% tuple (NIF mode).
-type address() :: non_neg_integer().
-%% I2C device address.
+%% I2C device address (typically 7-bit, e.g. `16#68').
-type register() :: non_neg_integer().
%% Register address within an I2C device.
-type params() :: [term()].
%% Initialization parameters for the I2C bus.
+%% See the module documentation for common parameters.
-export_type([i2c/0, address/0, register/0, params/0]).
+% Open an I2C bus with the given configuration parameters.
+%
+% Returns a handle that must be passed to all subsequent I2C
+% operations.
-callback open(Params :: params()) -> i2c().
+% Close an I2C bus and release its resources.
-callback close(I2C :: i2c()) -> ok | {error, Reason :: term()}.
+% Begin a write transaction to a device at the given address.
+%
+% After calling this function, use `write_byte/2' or `write_bytes/2'
+% to send data, then call `end_transmission/1' to complete the
+% transaction.
-callback begin_transmission(I2C :: i2c(), Address :: address()) ->
ok | {error, Reason :: term()}.
+% Write a single byte as part of a transaction.
+%
+% Must be called between `begin_transmission/2' and
+% `end_transmission/1'.
-callback write_byte(I2C :: i2c(), Byte :: byte()) ->
ok | {error, Reason :: term()}.
+% Write binary data as part of a transaction.
+%
+% Must be called between `begin_transmission/2' and
+% `end_transmission/1'.
-callback write_bytes(I2C :: i2c(), Bytes :: binary()) ->
ok | {error, Reason :: term()}.
+% End a write transaction started with `begin_transmission/2'.
+%
+% Sends all buffered data to the device.
-callback end_transmission(I2C :: i2c()) ->
ok | {error, Reason :: term()}.
+% Read a number of bytes from a device at the given address.
-callback read_bytes(I2C :: i2c(), Address :: address(), Count :: non_neg_integer()) ->
{ok, Data :: binary()} | {error, Reason :: term()}.
+% Read a number of bytes from a specific register within a device.
-callback read_bytes(
I2C :: i2c(), Address :: address(), Register :: register(), Count :: non_neg_integer()
) ->
{ok, Data :: binary()} | {error, Reason :: term()}.
+% Write data to a device at the given address.
+%
+% BinOrInt can be a binary or a single byte value.
-callback write_bytes(I2C :: i2c(), Address :: address(), BinOrInt :: binary() | byte()) ->
ok | {error, Reason :: term()}.
+% Write data to a specific register within a device.
+%
+% BinOrInt can be a binary or a single byte value.
-callback write_bytes(
- I2C :: i2c(), Address :: address(), Register :: register(), BinOrInt :: binary() | integer()
+ I2C :: i2c(), Address :: address(), Register :: register(), BinOrInt :: binary() | byte()
) ->
ok | {error, Reason :: term()}.
diff --git a/libs/eavmlib/src/spi_hal.erl b/libs/eavmlib/src/spi_hal.erl
index 274962de9b..cd037ce6db 100644
--- a/libs/eavmlib/src/spi_hal.erl
+++ b/libs/eavmlib/src/spi_hal.erl
@@ -22,8 +22,89 @@
%% @doc SPI Hardware Abstraction Layer behavior
%%
%% This module defines the behavior that platform-specific SPI modules
-%% must implement. It provides a common interface for SPI operations
-%% across all supported platforms.
+%% must implement. It provides a common interface for SPI (Serial
+%% Peripheral Interface) operations across all supported platforms.
+%%
+%% Currently, only ESP32 provides an SPI implementation.
+%%
+%% Lifecycle
+%%
+%% An SPI bus is opened with `open/1' and closed with `close/1'. The
+%% `open/1' function takes a configuration that includes both bus
+%% parameters (pins, peripheral) and device configurations. Multiple
+%% devices can share the same SPI bus.
+%%
+%% Device names
+%%
+%% Each device on the SPI bus is identified by a `device_name()' atom.
+%% Device names are defined during `open/1' configuration and used in
+%% all subsequent read/write operations. For example, if the
+%% configuration includes `{my_sensor, [{cs, 5}]}', then `my_sensor'
+%% is used as the device name in calls like
+%% `spi:read_at(SPI, my_sensor, Address, Len)'.
+%%
+%% Simple read/write
+%%
+%% For simple address-based operations:
+%%
+%%
+%% - `read_at/4' - Read data from a device at a given address
+%% - `write_at/5' - Write data to a device at a given address
+%%
+%%
+%% Transaction-based operations
+%%
+%% For more complex operations, use transaction maps with `write/3'
+%% or `write_read/3':
+%%
+%% ```
+%% Transaction = #{
+%% command => 16#42,
+%% address => 16#00,
+%% write_data => <<1, 2, 3>>,
+%% write_bits => 24,
+%% read_bits => 8
+%% },
+%% {ok, ReadData} = spi:write_read(SPI, my_device, Transaction).
+%% '''
+%%
+%% Configuration parameters
+%%
+%% The `open/1' function accepts a proplist or map with bus and device
+%% configuration. Bus parameters include:
+%%
+%%
+%% - `{sclk, non_neg_integer()}' - SCLK (clock) pin number
+%% (required)
+%% - `{miso, non_neg_integer()}' - MISO pin number (optional)
+%% - `{mosi, non_neg_integer()}' - MOSI pin number (optional)
+%% - `{peripheral, string() | binary()}' - SPI peripheral (e.g.
+%% `"spi2"', `"spi3"', default: `"spi2"')
+%%
+%%
+%% Device parameters are specified as `{DeviceName, DeviceOpts}' entries:
+%%
+%%
+%% - `{cs, non_neg_integer()}' - Chip Select pin number
+%% - `{clock_speed_hz, non_neg_integer()}' - Clock speed (default:
+%% 1000000)
+%% - `{mode, 0..3}' - SPI mode (default: 0)
+%% - `{address_len_bits, 0..64}' - Address width in bits (default:
+%% 8)
+%% - `{command_len_bits, 0..16}' - Command width in bits (default:
+%% 0)
+%%
+%%
+%% Example
+%%
+%% ```
+%% SPI = spi:open([
+%% {sclk, 18}, {miso, 19}, {mosi, 23},
+%% {my_device, [{cs, 5}, {clock_speed_hz, 1000000}]}
+%% ]),
+%% {ok, Value} = spi:read_at(SPI, my_device, 16#0F, 8),
+%% spi:close(SPI).
+%% '''
%% @end
%%-----------------------------------------------------------------------------
-module(spi_hal).
@@ -32,13 +113,15 @@
%% Handle returned by `open/1'.
-type device_name() :: atom().
-%% Name identifying an SPI device, as specified in the device configuration.
+%% Name identifying an SPI device, as specified in the device
+%% configuration passed to `open/1'.
-type address() :: non_neg_integer().
%% SPI device address.
-type params() :: [term()] | map().
-%% Initialization parameters for the SPI bus.
+%% Initialization parameters for the SPI bus and its devices.
+%% See the module documentation for common parameters.
-type transaction() :: #{
command => integer(),
@@ -47,19 +130,34 @@
write_bits => non_neg_integer(),
read_bits => non_neg_integer()
}.
-%% SPI transaction map.
+%% SPI transaction map. Fields are all optional and depend on the
+%% operation being performed.
-export_type([spi/0, device_name/0, address/0, params/0, transaction/0]).
+% Open an SPI bus with the given configuration.
+%
+% The configuration includes both bus parameters (pins, peripheral)
+% and device configurations. Returns a handle for subsequent
+% operations.
-callback open(Params :: params()) -> spi().
+% Close the SPI bus and release its resources.
-callback close(SPI :: spi()) -> ok.
+% Read data from a device at the given address.
+%
+% Returns `{ok, Value}' where Value is an integer representing the
+% read data, or `{error, Reason}'.
-callback read_at(
SPI :: spi(), DeviceName :: device_name(), Address :: address(), Len :: non_neg_integer()
) ->
{ok, integer()} | {error, Reason :: term()}.
+% Write data to a device at the given address.
+%
+% Returns `{ok, Value}' where Value is the response from the device,
+% or `{error, Reason}'.
-callback write_at(
SPI :: spi(),
DeviceName :: device_name(),
@@ -69,8 +167,16 @@
) ->
{ok, integer()} | {error, Reason :: term()}.
+% Write data to a device using a transaction map.
+%
+% The transaction map specifies the command, address, and data to
+% write. See the `transaction()' type for details.
-callback write(SPI :: spi(), DeviceName :: device_name(), Transaction :: transaction()) ->
ok | {error, Reason :: term()}.
+% Write data to and read data from a device in a single transaction.
+%
+% Performs a simultaneous write and read operation. Returns the read
+% data as a binary.
-callback write_read(SPI :: spi(), DeviceName :: device_name(), Transaction :: transaction()) ->
{ok, ReadData :: binary()} | {error, Reason :: term()}.
diff --git a/libs/eavmlib/src/uart_hal.erl b/libs/eavmlib/src/uart_hal.erl
index 63a40b0d5c..1dc8cbf9de 100644
--- a/libs/eavmlib/src/uart_hal.erl
+++ b/libs/eavmlib/src/uart_hal.erl
@@ -22,8 +22,60 @@
%% @doc UART Hardware Abstraction Layer behavior
%%
%% This module defines the behavior that platform-specific UART modules
-%% must implement. It provides a common interface for UART operations
-%% across all supported platforms.
+%% must implement. It provides a common interface for UART (Universal
+%% Asynchronous Receiver-Transmitter) operations across all supported
+%% platforms.
+%%
+%% Currently, only ESP32 provides a UART implementation.
+%%
+%% Lifecycle
+%%
+%% A UART port is opened with `open/1' or `open/2' and closed with
+%% `close/1'. The `open/2' variant is a convenience that takes the
+%% peripheral name as a separate argument. The returned handle is
+%% passed to all subsequent operations.
+%%
+%% Reading and writing
+%%
+%%
+%% - `read/1' - Non-blocking read. Returns `{ok, Data}' if data is
+%% available, or `{error, timeout}' if no data is ready.
+%% - `read/2' - Blocking read with a timeout in milliseconds.
+%% Waits up to the specified time for data to arrive.
+%% - `write/2' - Write data to the UART port.
+%%
+%%
+%% Configuration parameters
+%%
+%% The `open/1' function accepts a proplist of configuration
+%% parameters. Common parameters include:
+%%
+%%
+%% - `{tx, integer()}' - Transmit pin number
+%% - `{rx, integer()}' - Receive pin number
+%% - `{speed, pos_integer()}' - Baud rate
+%% - `{data_bits, 5..8}' - Number of data bits (default: 8)
+%% - `{stop_bits, 1 | 2}' - Number of stop bits (default: 1)
+%% - `{parity, none | even | odd}' - Parity mode
+%% - `{flow_control, none | hardware | software}' - Flow control
+%% type
+%% - `{rts, integer()}' - RTS pin for hardware flow control
+%% - `{cts, integer()}' - CTS pin for hardware flow control
+%% - `{peripheral, string() | binary()}' - UART peripheral name
+%% (e.g. `"UART0"', `"UART1"', `"UART2"')
+%%
+%%
+%% Example
+%%
+%% ```
+%% UART = uart:open([{tx, 17}, {rx, 16}, {speed, 115200}]),
+%% uart:write(UART, <<"Hello\r\n">>),
+%% case uart:read(UART, 5000) of
+%% {ok, Data} -> io:format("Received: ~p~n", [Data]);
+%% {error, timeout} -> io:format("No response~n")
+%% end,
+%% uart:close(UART).
+%% '''
%% @end
%%-----------------------------------------------------------------------------
-module(uart_hal).
@@ -32,22 +84,40 @@
%% Handle returned by `open/1' or `open/2'.
-type peripheral() :: string() | binary().
-%% UART peripheral name.
+%% UART peripheral name (e.g. `"UART0"', `"UART1"').
-type params() :: [term()].
-%% Initialization parameters for the UART bus.
+%% Initialization parameters for the UART port.
+%% See the module documentation for common parameters.
-export_type([uart/0, peripheral/0, params/0]).
+% Open a UART port with the given configuration parameters.
+%
+% Returns a handle for subsequent read/write operations.
-callback open(Params :: params()) -> uart() | {error, Reason :: term()}.
+% Open a UART port on the specified peripheral.
+%
+% Convenience wrapper that adds `{peripheral, Name}' to the
+% parameters and calls `open/1'.
-callback open(Name :: peripheral(), Params :: params()) -> uart() | {error, Reason :: term()}.
+% Close a UART port and release its resources.
-callback close(UART :: uart()) -> ok | {error, Reason :: term()}.
+% Non-blocking read from the UART port.
+%
+% Returns `{ok, Data}' if data is available, or `{error, timeout}'
+% if no data is ready.
-callback read(UART :: uart()) -> {ok, Data :: iodata()} | {error, Reason :: term()}.
+% Blocking read from the UART port with a timeout.
+%
+% Waits up to `Timeout' milliseconds for data to arrive. Returns
+% `{error, timeout}' if no data arrives within the timeout period.
-callback read(UART :: uart(), Timeout :: pos_integer()) ->
{ok, Data :: iodata()} | {error, Reason :: term()}.
+% Write data to the UART port.
-callback write(UART :: uart(), Data :: iodata()) -> ok | {error, Reason :: term()}.