diff --git a/.github/workflows/stm32-build.yaml b/.github/workflows/stm32-build.yaml index 97cf7a09fe..84a822e5f9 100644 --- a/.github/workflows/stm32-build.yaml +++ b/.github/workflows/stm32-build.yaml @@ -185,7 +185,7 @@ jobs: mkdir build-host cd build-host cmake .. -G Ninja - cmake --build . -t stm32_boot_test stm32_gpio_test stm32_i2c_test stm32_spi_test + cmake --build . -t stm32_boot_test stm32_gpio_test stm32_i2c_test stm32_spi_test stm32_uart_test - name: Install Renode if: matrix.renode_platform @@ -197,7 +197,7 @@ jobs: echo "$PWD/renode-portable" >> $GITHUB_PATH pip install -r renode-portable/tests/requirements.txt - - name: Run Renode boot test + - name: Run Renode tests if: matrix.renode_platform run: | LOCAL_REPL="src/platforms/stm32/tests/renode/${{ matrix.renode_platform }}" @@ -206,53 +206,15 @@ jobs: else PLATFORM="@platforms/cpus/${{ matrix.renode_platform }}" fi - renode-test src/platforms/stm32/tests/renode/stm32_boot_test.robot \ - --variable ELF:@$PWD/src/platforms/stm32/build/AtomVM-${{ matrix.device }}.elf \ - --variable AVM:@$PWD/build-host/src/platforms/stm32/tests/test_erl_sources/stm32_boot_test.avm \ - --variable AVM_ADDRESS:${{ matrix.avm_address }} \ - --variable PLATFORM:$PLATFORM - - - name: Run Renode GPIO test - if: matrix.renode_platform - run: | - LOCAL_REPL="src/platforms/stm32/tests/renode/${{ matrix.renode_platform }}" - if [ -f "$LOCAL_REPL" ]; then - PLATFORM="@$PWD/$LOCAL_REPL" - else - PLATFORM="@platforms/cpus/${{ matrix.renode_platform }}" - fi - renode-test src/platforms/stm32/tests/renode/stm32_gpio_test.robot \ - --variable ELF:@$PWD/src/platforms/stm32/build/AtomVM-${{ matrix.device }}.elf \ - --variable AVM:@$PWD/build-host/src/platforms/stm32/tests/test_erl_sources/stm32_gpio_test.avm \ - --variable AVM_ADDRESS:${{ matrix.avm_address }} \ - --variable PLATFORM:$PLATFORM - - - name: Run Renode I2C test - if: matrix.renode_platform && !matrix.skip_i2c_test - run: | - LOCAL_REPL="src/platforms/stm32/tests/renode/${{ matrix.renode_platform }}" - if [ -f "$LOCAL_REPL" ]; then - PLATFORM="@$PWD/$LOCAL_REPL" - else - PLATFORM="@platforms/cpus/${{ matrix.renode_platform }}" - fi - renode-test src/platforms/stm32/tests/renode/stm32_i2c_test.robot \ - --variable ELF:@$PWD/src/platforms/stm32/build/AtomVM-${{ matrix.device }}.elf \ - --variable AVM:@$PWD/build-host/src/platforms/stm32/tests/test_erl_sources/stm32_i2c_test.avm \ - --variable AVM_ADDRESS:${{ matrix.avm_address }} \ - --variable PLATFORM:$PLATFORM - - - name: Run Renode SPI test - if: matrix.renode_platform - run: | - LOCAL_REPL="src/platforms/stm32/tests/renode/${{ matrix.renode_platform }}" - if [ -f "$LOCAL_REPL" ]; then - PLATFORM="@$PWD/$LOCAL_REPL" + if [ "${{ matrix.skip_i2c_test }}" = "true" ]; then + TESTS="boot gpio spi uart" else - PLATFORM="@platforms/cpus/${{ matrix.renode_platform }}" + TESTS="boot gpio i2c spi uart" fi - renode-test src/platforms/stm32/tests/renode/stm32_spi_test.robot \ - --variable ELF:@$PWD/src/platforms/stm32/build/AtomVM-${{ matrix.device }}.elf \ - --variable AVM:@$PWD/build-host/src/platforms/stm32/tests/test_erl_sources/stm32_spi_test.avm \ - --variable AVM_ADDRESS:${{ matrix.avm_address }} \ - --variable PLATFORM:$PLATFORM + for TEST in $TESTS; do + renode-test "src/platforms/stm32/tests/renode/stm32_${TEST}_test.robot" \ + --variable ELF:@$PWD/src/platforms/stm32/build/AtomVM-${{ matrix.device }}.elf \ + --variable AVM:@$PWD/build-host/src/platforms/stm32/tests/test_erl_sources/stm32_${TEST}_test.avm \ + --variable AVM_ADDRESS:${{ matrix.avm_address }} \ + --variable PLATFORM:$PLATFORM + done diff --git a/CHANGELOG.md b/CHANGELOG.md index 6dfad7498d..ed0acae80d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,8 +13,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added support for `nif_start`, `executable_line` and `debug_line` opcodes - Added named variable debugging support in DWARF when modules are compiled with `beam_debug_info` - Added more reset reasons and ensured `esp:reset_reason/0` doesn't return `undefined` -- Added I2C and SPI APIs to stm32 platform +- Added I2C, SPI and UART APIs to stm32 platform - Added `Transfer-Encoding: chunked` response support to `ahttp_client`, including HTTP trailers +- Added UART API to rp2 platform ### Changed - Updated network type db() to dbm() to reflect the actual representation of the type diff --git a/examples/erlang/CMakeLists.txt b/examples/erlang/CMakeLists.txt index 5a205e2dfb..2c10ab5a02 100644 --- a/examples/erlang/CMakeLists.txt +++ b/examples/erlang/CMakeLists.txt @@ -48,3 +48,4 @@ pack_runnable(i2c_scanner i2c_scanner eavmlib estdlib DIALYZE_AGAINST avm_esp32 pack_runnable(i2c_lis3dh i2c_lis3dh eavmlib estdlib DIALYZE_AGAINST avm_esp32 avm_rp2 avm_stm32) pack_runnable(spi_flash spi_flash eavmlib estdlib DIALYZE_AGAINST avm_esp32 avm_rp2 avm_stm32) pack_runnable(spi_lis3dh spi_lis3dh eavmlib estdlib DIALYZE_AGAINST avm_esp32 avm_rp2 avm_stm32) +pack_runnable(sim800l sim800l eavmlib estdlib DIALYZE_AGAINST avm_esp32 avm_rp2 avm_stm32) diff --git a/examples/erlang/rp2/CMakeLists.txt b/examples/erlang/rp2/CMakeLists.txt index 5a81d9d41f..82ca2b1ce7 100644 --- a/examples/erlang/rp2/CMakeLists.txt +++ b/examples/erlang/rp2/CMakeLists.txt @@ -70,6 +70,16 @@ add_custom_command( add_custom_target(spi_lis3dh_uf2 ALL DEPENDS spi_lis3dh.uf2) add_dependencies(spi_lis3dh_uf2 spi_lis3dh) +set(SIM800L_AVM ${CMAKE_BINARY_DIR}/examples/erlang/sim800l.avm) +add_custom_command( + OUTPUT sim800l.uf2 + DEPENDS ${SIM800L_AVM} UF2Tool + COMMAND ${CMAKE_BINARY_DIR}/tools/uf2tool/uf2tool create -o sim800l.uf2 -f universal -s 0x10180000 ${SIM800L_AVM} + COMMENT "Creating UF2 file sim800l.uf2" + VERBATIM +) +add_custom_target(sim800l_uf2 ALL DEPENDS sim800l.uf2) +add_dependencies(sim800l_uf2 sim800l) pack_uf2(picow_blink picow_blink) pack_uf2(picow_wifi_sta picow_wifi_sta) pack_uf2(picow_wifi_ap picow_wifi_ap) diff --git a/examples/erlang/sim800l.erl b/examples/erlang/sim800l.erl new file mode 100644 index 0000000000..427c96b36a --- /dev/null +++ b/examples/erlang/sim800l.erl @@ -0,0 +1,197 @@ +% +% This file is part of AtomVM. +% +% Copyright 2026 Paul Guyot +% +% Licensed under the Apache License, Version 2.0 (the "License"); +% you may not use this file except in compliance with the License. +% You may obtain a copy of the License at +% +% http://www.apache.org/licenses/LICENSE-2.0 +% +% Unless required by applicable law or agreed to in writing, software +% distributed under the License is distributed on an "AS IS" BASIS, +% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +% See the License for the specific language governing permissions and +% limitations under the License. +% +% SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later +% + +%%----------------------------------------------------------------------------- +%% @doc SIM800L AT command demo. +%% +%% Opens a UART connection to a SIM800L GSM module and verifies it responds +%% to basic AT commands. Prints firmware identification and signal quality +%% every 10 seconds. +%% +%% Be careful: SIM800L boards can draw up to 2A and shouldn't be powered by +%% the 3.3V of the usual Pico / ESP32 boards. It's ok for this demo but +%% do not put a SIM card in them to avoid damaging your board. +%% +%% The SIM800L communicates at 115200 baud (8N1) by default. +%% +%% Default pins are auto-detected from the platform and chip model: +%% +%% Pico (UART1): TX=GP4, RX=GP5 +%% STM32 (USART1): TX=PA9, RX=PA10, AF=7 +%% ESP32/S2/S3 (UART1): TX=17, RX=16 +%% ESP32-C2 (UART1): TX=4, RX=5 +%% ESP32-C3/C5 (UART1): TX=4, RX=5 +%% ESP32-C6/C61 (UART1): TX=4, RX=5 +%% @end +%%----------------------------------------------------------------------------- +-module(sim800l). +-export([start/0]). + +-define(AT_TIMEOUT, 2000). + +start() -> + Platform = atomvm:platform(), + UART = open_uart(Platform), + %% SIM800L takes 3-5 seconds to boot after power-on + case wait_for_module(UART, 5) of + ok -> + io:format("SIM800L responding to AT commands~n"), + at_identify(UART), + loop(UART); + error -> + io:format("SIM800L not responding, giving up~n"), + uart:close(UART) + end. + +loop(UART) -> + at_signal_quality(UART), + timer:sleep(10000), + loop(UART). + +wait_for_module(_UART, 0) -> + error; +wait_for_module(UART, Retries) -> + drain(UART), + case at_command(UART, <<"AT">>) of + {ok, _} -> + ok; + {error, _} -> + timer:sleep(1000), + wait_for_module(UART, Retries - 1) + end. + +at_identify(UART) -> + case at_command(UART, <<"ATI">>) of + {ok, Response} -> + io:format("Module info: ~s~n", [Response]); + {error, Reason} -> + io:format("ATI failed: ~p~n", [Reason]) + end. + +at_signal_quality(UART) -> + case at_command(UART, <<"AT+CSQ">>) of + {ok, Response} -> + io:format("Signal quality: ~s~n", [Response]); + {error, Reason} -> + io:format("AT+CSQ failed: ~p~n", [Reason]) + end. + +%%----------------------------------------------------------------------------- +%% @private Send an AT command and collect the response until OK or ERROR. +%%----------------------------------------------------------------------------- +at_command(UART, Command) -> + uart:write(UART, [Command, <<"\r\n">>]), + collect_response(UART, []). + +collect_response(UART, Acc) -> + case uart:read(UART, ?AT_TIMEOUT) of + {ok, Data} -> + NewAcc = [Data | Acc], + Combined = erlang:iolist_to_binary(lists:reverse(NewAcc)), + case parse_response(Combined) of + {ok, Body} -> {ok, Body}; + error -> {error, Combined}; + incomplete -> collect_response(UART, NewAcc) + end; + {error, timeout} when Acc =/= [] -> + Combined = erlang:iolist_to_binary(lists:reverse(Acc)), + case parse_response(Combined) of + {ok, Body} -> {ok, Body}; + _ -> {error, {partial, Combined}} + end; + {error, timeout} -> + {error, timeout} + end. + +%% Look for OK or ERROR in the accumulated response +parse_response(Data) -> + case binary:match(Data, <<"\r\nOK\r\n">>) of + {_Pos, _Len} -> + Body = strip_status(Data), + {ok, Body}; + nomatch -> + case binary:match(Data, <<"\r\nERROR\r\n">>) of + {_Pos2, _Len2} -> error; + nomatch -> incomplete + end + end. + +%% Extract body between echo/first CRLF and final status line +strip_status(Data) -> + Trimmed = trim_leading_crlf(Data), + case binary:match(Trimmed, <<"\r\nOK\r\n">>) of + {Pos, _} -> binary:part(Trimmed, 0, Pos); + nomatch -> Trimmed + end. + +trim_leading_crlf(<<"\r\n", Rest/binary>>) -> trim_leading_crlf(Rest); +trim_leading_crlf(Data) -> Data. + +%% Discard any pending data in the UART buffer +drain(UART) -> + case uart:read(UART, 100) of + {ok, _} -> drain(UART); + {error, timeout} -> ok + end. + +%%----------------------------------------------------------------------------- +%% Platform-specific UART operations +%%----------------------------------------------------------------------------- + +open_uart(stm32) -> + {TX, RX} = default_pins(stm32), + io:format("Opening USART1 on TX=~p RX=~p~n", [TX, RX]), + uart:open([ + {peripheral, 1}, + {tx, TX}, + {rx, RX}, + {af, 7}, + {speed, 115200} + ]); +open_uart(Platform) -> + {TX, RX} = default_pins(Platform), + io:format("Opening UART1 on TX=~p RX=~p~n", [TX, RX]), + uart:open("UART1", [ + {tx, TX}, + {rx, RX}, + {speed, 115200} + ]). + +%%----------------------------------------------------------------------------- +%% Platform-specific default pins +%%----------------------------------------------------------------------------- +default_pins(pico) -> {4, 5}; +default_pins(stm32) -> {{a, 9}, {a, 10}}; +default_pins(esp32) -> esp32_default_pins(). + +esp32_default_pins() -> + #{model := Model} = erlang:system_info(esp32_chip_info), + esp32_default_pins(Model). + +%% {TX, RX} +esp32_default_pins(esp32) -> {17, 16}; +esp32_default_pins(esp32_s2) -> {17, 16}; +esp32_default_pins(esp32_s3) -> {17, 16}; +esp32_default_pins(esp32_c2) -> {4, 5}; +esp32_default_pins(esp32_c3) -> {4, 5}; +esp32_default_pins(esp32_c5) -> {4, 5}; +esp32_default_pins(esp32_c6) -> {4, 5}; +esp32_default_pins(esp32_c61) -> {4, 5}; +esp32_default_pins(_) -> {17, 16}. diff --git a/examples/erlang/stm32/CMakeLists.txt b/examples/erlang/stm32/CMakeLists.txt index 5ddcf3f83c..8ecbec5c14 100644 --- a/examples/erlang/stm32/CMakeLists.txt +++ b/examples/erlang/stm32/CMakeLists.txt @@ -70,3 +70,19 @@ add_custom_command( VERBATIM ) add_custom_target(stm32_spi_flash ALL DEPENDS stm32_spi_flash.avm) + +set(SIM800L_BEAM ${CMAKE_BINARY_DIR}/examples/erlang/sim800l.beam) +add_custom_command( + OUTPUT stm32_sim800l.avm + DEPENDS sim800l_main ${SIM800L_BEAM} + ${CMAKE_BINARY_DIR}/libs/estdlib/src/estdlib.avm estdlib + ${CMAKE_BINARY_DIR}/libs/avm_stm32/src/avm_stm32.avm avm_stm32 + PackBEAM + COMMAND ${CMAKE_BINARY_DIR}/tools/packbeam/packbeam create ${INCLUDE_LINES} stm32_sim800l.avm + ${SIM800L_BEAM} + ${CMAKE_BINARY_DIR}/libs/estdlib/src/estdlib.avm + ${CMAKE_BINARY_DIR}/libs/avm_stm32/src/avm_stm32.avm + COMMENT "Packing runnable stm32_sim800l.avm" + VERBATIM +) +add_custom_target(stm32_sim800l ALL DEPENDS stm32_sim800l.avm) diff --git a/libs/avm_rp2/src/CMakeLists.txt b/libs/avm_rp2/src/CMakeLists.txt index c76d8b1675..dce842b829 100644 --- a/libs/avm_rp2/src/CMakeLists.txt +++ b/libs/avm_rp2/src/CMakeLists.txt @@ -27,6 +27,7 @@ set(ERLANG_MODULES i2c pico spi + uart ) pack_archive(avm_rp2 DEPENDS_ON eavmlib ERLC_FLAGS +warnings_as_errors MODULES ${ERLANG_MODULES}) diff --git a/libs/avm_rp2/src/uart.erl b/libs/avm_rp2/src/uart.erl new file mode 100644 index 0000000000..a0e9da3bda --- /dev/null +++ b/libs/avm_rp2/src/uart.erl @@ -0,0 +1,540 @@ +% +% This file is part of AtomVM. +% +% Copyright 2026 Paul Guyot +% +% Licensed under the Apache License, Version 2.0 (the "License"); +% you may not use this file except in compliance with the License. +% You may obtain a copy of the License at +% +% http://www.apache.org/licenses/LICENSE-2.0 +% +% Unless required by applicable law or agreed to in writing, software +% distributed under the License is distributed on an "AS IS" BASIS, +% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +% See the License for the specific language governing permissions and +% limitations under the License. +% +% SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later +% + +%%----------------------------------------------------------------------------- +%% @doc AtomVM UART interface for RP2 (Pico) +%% +%% This module provides an interface to the UART hardware on RP2 platforms. +%% +%% Two API levels are provided: +%% +%% Low-level API +%% {@link init/2}, {@link deinit/1}, {@link set_baudrate/2}, +%% {@link set_format/4}, {@link set_hw_flow/3}, {@link set_fifo_enabled/2}, +%% {@link set_break/2}, {@link is_writable/1}, {@link tx_wait_blocking/1}, +%% {@link write_blocking/2}, {@link read_blocking/2}, +%% {@link is_readable/1}, {@link is_readable_within_us/2}, +%% {@link putc/2}, {@link putc_raw/2}, {@link puts/2}, {@link getc/1}. +%% These operate on a bare resource reference returned by {@link init/2}. +%% Pin muxing must be done separately via `gpio:set_function/2'. +%% +%% High-level API (`uart_hal' behavior) +%% {@link open/1}, {@link open/2}, {@link close/1}, +%% {@link read/1}, {@link read/2}, {@link write/2}. +%% {@link open/1} handles pin setup automatically. +%% @end +%%----------------------------------------------------------------------------- +-module(uart). + +-behaviour(uart_hal). + +%% High-level API (uart_hal behaviour) +-export([ + open/1, open/2, + close/1, + read/1, read/2, + write/2 +]). + +%% Low-level API (Pico SDK) +-export([ + init/2, + deinit/1, + set_baudrate/2, + set_format/4, + set_hw_flow/3, + set_fifo_enabled/2, + set_break/2, + is_writable/1, + tx_wait_blocking/1, + write_blocking/2, + read_blocking/2, + is_readable/1, + is_readable_within_us/2, + putc/2, + putc_raw/2, + puts/2, + getc/1 +]). + +-type pin() :: non_neg_integer(). +-type freq_hz() :: non_neg_integer(). +-type peripheral() :: 0 | 1. +-type param() :: + {tx, pin()} + | {rx, pin()} + | {rts, pin()} + | {cts, pin()} + | {speed, pos_integer()} + | {data_bits, 5..8} + | {stop_bits, 1 | 2} + | {parity, none | even | odd} + | {flow_control, none | hardware} + | {peripheral, peripheral() | string() | binary()}. +-type params() :: [param()]. +-type uart_resource() :: reference(). +-type uart() :: pid(). + +-export_type([uart/0, uart_resource/0]). + +-define(DEFAULT_SPEED, 115200). +-define(DEFAULT_DATA_BITS, 8). +-define(DEFAULT_STOP_BITS, 1). +-define(DEFAULT_PARITY, none). +-define(DEFAULT_FLOW_CONTROL, none). +-define(DEFAULT_PERIPHERAL, 0). + +%% --------------------------------------------------------------------------- +%% High-level API (uart_hal behaviour) +%% --------------------------------------------------------------------------- + +%%----------------------------------------------------------------------------- +%% @param Name UART peripheral name (`"UART0"' or `"UART1"') +%% @param Params Initialization parameters +%% @returns UART handle (pid) +%% @doc Open a connection to the UART driver +%% +%% This function provides compatibility with the ESP32 UART driver +%% interface. The `Name' parameter is converted to a peripheral +%% number and prepended to the parameters. +%% @end +%%----------------------------------------------------------------------------- +-spec open(Name :: string() | binary(), Params :: params()) -> uart(). +open(Name, Params) -> + open([{peripheral, Name} | Params]). + +%%----------------------------------------------------------------------------- +%% @param Params Initialization parameters +%% @returns UART handle (pid) +%% @doc Open a connection to the UART driver +%% +%% This function configures the GPIO pins for UART function, +%% initializes the UART peripheral, and configures data format +%% and flow control. +%% +%% Supported parameters: +%% +%% @end +%%----------------------------------------------------------------------------- +-spec open(Params :: params()) -> uart(). +open(Params) -> + TX = proplists:get_value(tx, Params), + RX = proplists:get_value(rx, Params), + TX =:= undefined andalso error({missing_required_param, tx}), + RX =:= undefined andalso error({missing_required_param, rx}), + RTS = proplists:get_value(rts, Params, undefined), + CTS = proplists:get_value(cts, Params, undefined), + Speed = proplists:get_value(speed, Params, ?DEFAULT_SPEED), + DataBits = proplists:get_value(data_bits, Params, ?DEFAULT_DATA_BITS), + StopBits = proplists:get_value(stop_bits, Params, ?DEFAULT_STOP_BITS), + Parity = proplists:get_value(parity, Params, ?DEFAULT_PARITY), + FlowControl = proplists:get_value(flow_control, Params, ?DEFAULT_FLOW_CONTROL), + PeripheralParam = proplists:get_value(peripheral, Params, ?DEFAULT_PERIPHERAL), + Peripheral = normalize_peripheral(PeripheralParam), + gpio:set_function(TX, uart), + gpio:set_function(RX, uart), + maybe_set_uart_function(RTS), + maybe_set_uart_function(CTS), + case FlowControl of + hardware when RTS =:= undefined andalso CTS =:= undefined -> + error({missing_required_param, rts_or_cts}); + _ -> + ok + end, + {ok, {_ActualBaudrate, Resource}} = ?MODULE:init(Peripheral, Speed), + ?MODULE:set_format(Resource, DataBits, StopBits, Parity), + case FlowControl of + hardware -> + ?MODULE:set_hw_flow(Resource, CTS =/= undefined, RTS =/= undefined); + none -> + ok + end, + spawn_link(fun() -> loop(Resource) end). + +%%----------------------------------------------------------------------------- +%% @param UART UART handle created via `open/1' +%% @returns `ok' +%% @doc Close the connection to the UART driver and free resources. +%% @end +%%----------------------------------------------------------------------------- +-spec close(UART :: uart()) -> ok | {error, Reason :: term()}. +close(Pid) -> + call(Pid, close). + +%%----------------------------------------------------------------------------- +%% @param UART UART handle created via `open/1' +%% @returns `{ok, Data}' or `{error, timeout}' +%% @doc Read currently available data from the UART FIFO. +%% +%% Returns `{error, timeout}' immediately if no data is available. +%% @end +%%----------------------------------------------------------------------------- +-spec read(UART :: uart()) -> {ok, binary()} | {error, Reason :: term()}. +read(Pid) -> + call(Pid, read). + +%%----------------------------------------------------------------------------- +%% @param UART UART handle created via `open/1' +%% @param Timeout Timeout in milliseconds +%% @returns `{ok, Data}' or `{error, timeout}' +%% @doc Read data from the UART with a timeout. +%% +%% Waits up to `Timeout' milliseconds for data to become available. +%% Once data arrives, reads all currently available bytes from the +%% FIFO. +%% @end +%%----------------------------------------------------------------------------- +-spec read(UART :: uart(), Timeout :: pos_integer()) -> + {ok, binary()} | {error, Reason :: term()}. +read(Pid, Timeout) -> + call(Pid, {read, Timeout}). + +%%----------------------------------------------------------------------------- +%% @param UART UART handle created via `open/1' +%% @param Data iodata to write +%% @returns `ok' +%% @doc Write data to the UART. +%% @end +%%----------------------------------------------------------------------------- +-spec write(UART :: uart(), Data :: iodata()) -> ok | {error, Reason :: term()}. +write(Pid, Data) -> + case is_iolist(Data) of + true -> call(Pid, {write, Data}); + false -> error(badarg) + end. + +%% --------------------------------------------------------------------------- +%% Low-level API (Pico SDK) +%% --------------------------------------------------------------------------- + +%%----------------------------------------------------------------------------- +%% @param Peripheral UART peripheral number (0 or 1) +%% @param Baudrate Baudrate in Hz (e.g. 115200) +%% @returns `{ok, {ActualBaudrate, Resource}}' +%% @doc Initialize the UART HW block. +%% +%% Pin muxing must be done separately via `gpio:set_function/2'. +%% @end +%%----------------------------------------------------------------------------- +-spec init(Peripheral :: peripheral(), Baudrate :: freq_hz()) -> + {ok, {ActualBaudrate :: freq_hz(), Resource :: uart_resource()}}. +init(_Peripheral, _Baudrate) -> + erlang:nif_error(undefined). + +%%----------------------------------------------------------------------------- +%% @param Resource UART resource returned by `init/2' +%% @returns `ok' +%% @doc Disable the UART HW block. +%% @end +%%----------------------------------------------------------------------------- +-spec deinit(Resource :: uart_resource()) -> ok. +deinit(_Resource) -> + erlang:nif_error(undefined). + +%%----------------------------------------------------------------------------- +%% @param Resource UART resource returned by `init/2' +%% @param Baudrate Baudrate in Hz +%% @returns `{ok, ActualBaudrate}' +%% @doc Set UART baudrate. +%% @end +%%----------------------------------------------------------------------------- +-spec set_baudrate(Resource :: uart_resource(), Baudrate :: freq_hz()) -> + {ok, ActualBaudrate :: freq_hz()}. +set_baudrate(_Resource, _Baudrate) -> + erlang:nif_error(undefined). + +%%----------------------------------------------------------------------------- +%% @param Resource UART resource returned by `init/2' +%% @param DataBits Number of data bits per character (5..8) +%% @param StopBits Number of stop bits (1 or 2) +%% @param Parity Parity setting (`none', `even', or `odd') +%% @returns `ok' +%% @doc Set UART data format. +%% @end +%%----------------------------------------------------------------------------- +-spec set_format( + Resource :: uart_resource(), DataBits :: 5..8, StopBits :: 1 | 2, Parity :: none | even | odd +) -> ok. +set_format(_Resource, _DataBits, _StopBits, _Parity) -> + erlang:nif_error(undefined). + +%%----------------------------------------------------------------------------- +%% @param Resource UART resource returned by `init/2' +%% @param CTS Enable CTS flow control +%% @param RTS Enable RTS flow control +%% @returns `ok' +%% @doc Set UART hardware flow control. +%% +%% The corresponding CTS/RTS pins must have been set to UART +%% function via `gpio:set_function/2' before enabling. +%% @end +%%----------------------------------------------------------------------------- +-spec set_hw_flow(Resource :: uart_resource(), CTS :: boolean(), RTS :: boolean()) -> ok. +set_hw_flow(_Resource, _CTS, _RTS) -> + erlang:nif_error(undefined). + +%%----------------------------------------------------------------------------- +%% @param Resource UART resource returned by `init/2' +%% @param Data Binary data to write +%% @returns `ok' +%% @doc Write to UART, blocking until all data is sent. +%% @end +%%----------------------------------------------------------------------------- +-spec write_blocking(Resource :: uart_resource(), Data :: binary()) -> ok. +write_blocking(_Resource, _Data) -> + erlang:nif_error(undefined). + +%%----------------------------------------------------------------------------- +%% @param Resource UART resource returned by `init/2' +%% @param Count Number of bytes to read +%% @returns `{ok, Data}' +%% @doc Read from UART, blocking until `Count' bytes have been received. +%% @end +%%----------------------------------------------------------------------------- +-spec read_blocking(Resource :: uart_resource(), Count :: non_neg_integer()) -> + {ok, binary()}. +read_blocking(_Resource, _Count) -> + erlang:nif_error(undefined). + +%%----------------------------------------------------------------------------- +%% @param Resource UART resource returned by `init/2' +%% @returns `true' if data is available, `false' otherwise +%% @doc Check if UART has data available to read. +%% @end +%%----------------------------------------------------------------------------- +-spec is_readable(Resource :: uart_resource()) -> boolean(). +is_readable(_Resource) -> + erlang:nif_error(undefined). + +%%----------------------------------------------------------------------------- +%% @param Resource UART resource returned by `init/2' +%% @param Us Maximum wait time in microseconds +%% @returns `true' if data became available, `false' on timeout +%% @doc Wait for UART data with a microsecond timeout. +%% @end +%%----------------------------------------------------------------------------- +-spec is_readable_within_us(Resource :: uart_resource(), Us :: non_neg_integer()) -> boolean(). +is_readable_within_us(_Resource, _Us) -> + erlang:nif_error(undefined). + +%%----------------------------------------------------------------------------- +%% @param Resource UART resource returned by `init/2' +%% @returns `true' if space is available in TX FIFO, `false' otherwise +%% @doc Check if UART TX FIFO has space available. +%% @end +%%----------------------------------------------------------------------------- +-spec is_writable(Resource :: uart_resource()) -> boolean(). +is_writable(_Resource) -> + erlang:nif_error(undefined). + +%%----------------------------------------------------------------------------- +%% @param Resource UART resource returned by `init/2' +%% @returns `ok' +%% @doc Wait for the UART TX FIFO to be drained. +%% @end +%%----------------------------------------------------------------------------- +-spec tx_wait_blocking(Resource :: uart_resource()) -> ok. +tx_wait_blocking(_Resource) -> + erlang:nif_error(undefined). + +%%----------------------------------------------------------------------------- +%% @param Resource UART resource returned by `init/2' +%% @param Enabled `true' to enable FIFOs (default), `false' to disable +%% @returns `ok' +%% @doc Enable or disable the UART FIFOs. +%% @end +%%----------------------------------------------------------------------------- +-spec set_fifo_enabled(Resource :: uart_resource(), Enabled :: boolean()) -> ok. +set_fifo_enabled(_Resource, _Enabled) -> + erlang:nif_error(undefined). + +%%----------------------------------------------------------------------------- +%% @param Resource UART resource returned by `init/2' +%% @param Enable `true' to assert break (TX held low), `false' to clear +%% @returns `ok' +%% @doc Assert or clear a break condition on UART transmission. +%% @end +%%----------------------------------------------------------------------------- +-spec set_break(Resource :: uart_resource(), Enable :: boolean()) -> ok. +set_break(_Resource, _Enable) -> + erlang:nif_error(undefined). + +%%----------------------------------------------------------------------------- +%% @param Resource UART resource returned by `init/2' +%% @param Char Character to send (0..255) +%% @returns `ok' +%% @doc Write a single character with optional CR/LF conversion. +%% @end +%%----------------------------------------------------------------------------- +-spec putc(Resource :: uart_resource(), Char :: byte()) -> ok. +putc(_Resource, _Char) -> + erlang:nif_error(undefined). + +%%----------------------------------------------------------------------------- +%% @param Resource UART resource returned by `init/2' +%% @param Char Character to send (0..255) +%% @returns `ok' +%% @doc Write a single character without CR/LF conversion. +%% @end +%%----------------------------------------------------------------------------- +-spec putc_raw(Resource :: uart_resource(), Char :: byte()) -> ok. +putc_raw(_Resource, _Char) -> + erlang:nif_error(undefined). + +%%----------------------------------------------------------------------------- +%% @param Resource UART resource returned by `init/2' +%% @param String Binary string to send +%% @returns `ok' +%% @doc Write a string with CR/LF conversion. +%% @end +%%----------------------------------------------------------------------------- +-spec puts(Resource :: uart_resource(), String :: binary()) -> ok. +puts(_Resource, _String) -> + erlang:nif_error(undefined). + +%%----------------------------------------------------------------------------- +%% @param Resource UART resource returned by `init/2' +%% @returns `{ok, Char}' where Char is 0..255 +%% @doc Read a single character, blocking until one is available. +%% @end +%%----------------------------------------------------------------------------- +-spec getc(Resource :: uart_resource()) -> {ok, byte()}. +getc(_Resource) -> + erlang:nif_error(undefined). + +%% --------------------------------------------------------------------------- +%% Internal helpers +%% --------------------------------------------------------------------------- + +%% @private +call(Pid, Request) -> + Ref = erlang:monitor(process, Pid), + Pid ! {self(), Ref, Request}, + receive + {Ref, Reply} -> + erlang:demonitor(Ref, [flush]), + Reply; + {'DOWN', Ref, process, Pid, Reason} -> + error({uart_server_down, Reason}) + end. + +%% @private +loop(Resource) -> + receive + {From, Ref, Request} -> + case handle_request(Resource, Request) of + {reply, Reply, stop} -> + From ! {Ref, Reply}; + {reply, Reply} -> + From ! {Ref, Reply}, + loop(Resource) + end + end. + +%% @private +handle_request(Resource, close) -> + ?MODULE:deinit(Resource), + {reply, ok, stop}; +handle_request(Resource, read) -> + case ?MODULE:is_readable(Resource) of + true -> + Data = read_available(Resource), + {reply, {ok, Data}}; + false -> + {reply, {error, timeout}} + end; +handle_request(Resource, {read, Timeout}) -> + Deadline = erlang:system_time(millisecond) + Timeout, + case poll_readable(Resource, Deadline) of + true -> + Data = read_available(Resource), + {reply, {ok, Data}}; + false -> + {reply, {error, timeout}} + end; +handle_request(Resource, {write, Data}) -> + Bin = erlang:iolist_to_binary(Data), + ?MODULE:write_blocking(Resource, Bin), + {reply, ok}. + +%% @private +read_available(Resource) -> + read_available(Resource, []). + +%% @private +read_available(Resource, Acc) -> + {ok, Byte} = ?MODULE:read_blocking(Resource, 1), + case ?MODULE:is_readable(Resource) of + true -> + read_available(Resource, [Byte | Acc]); + false -> + erlang:iolist_to_binary(lists:reverse([Byte | Acc])) + end. + +%% @private +poll_readable(Resource, Deadline) -> + case ?MODULE:is_readable_within_us(Resource, 1000) of + true -> + true; + false -> + case erlang:system_time(millisecond) >= Deadline of + true -> false; + false -> poll_readable(Resource, Deadline) + end + end. + +%% @private +normalize_peripheral(N) when is_integer(N) -> N; +normalize_peripheral("UART0") -> 0; +normalize_peripheral("UART1") -> 1; +normalize_peripheral(<<"UART0">>) -> 0; +normalize_peripheral(<<"UART1">>) -> 1. + +%% @private +maybe_set_uart_function(undefined) -> ok; +maybe_set_uart_function(Pin) -> gpio:set_function(Pin, uart). + +%% @private +is_iolist([]) -> + true; +is_iolist(B) when is_binary(B) -> + true; +is_iolist(I) when is_integer(I) andalso 0 =< I andalso I =< 255 -> + true; +is_iolist([H | T]) -> + case is_iolist(H) of + true -> + is_iolist(T); + false -> + false + end; +is_iolist(_) -> + false. diff --git a/libs/avm_stm32/src/CMakeLists.txt b/libs/avm_stm32/src/CMakeLists.txt index 7437d2a1e9..0267964b7f 100644 --- a/libs/avm_stm32/src/CMakeLists.txt +++ b/libs/avm_stm32/src/CMakeLists.txt @@ -26,6 +26,7 @@ set(ERLANG_MODULES gpio i2c spi + uart ) pack_archive(avm_stm32 DEPENDS_ON eavmlib ERLC_FLAGS +warnings_as_errors MODULES ${ERLANG_MODULES}) diff --git a/libs/avm_stm32/src/uart.erl b/libs/avm_stm32/src/uart.erl new file mode 100644 index 0000000000..5621b5a267 --- /dev/null +++ b/libs/avm_stm32/src/uart.erl @@ -0,0 +1,370 @@ +% +% This file is part of AtomVM. +% +% Copyright 2026 Paul Guyot +% +% Licensed under the Apache License, Version 2.0 (the "License"); +% you may not use this file except in compliance with the License. +% You may obtain a copy of the License at +% +% http://www.apache.org/licenses/LICENSE-2.0 +% +% Unless required by applicable law or agreed to in writing, software +% distributed under the License is distributed on an "AS IS" BASIS, +% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +% See the License for the specific language governing permissions and +% limitations under the License. +% +% SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later +% +%%----------------------------------------------------------------------------- +%% @doc AtomVM UART interface for STM32 +%% +%% This module provides an interface to the UART hardware on STM32 platforms. +%% +%% Two API levels are provided: +%% +%% Low-level API (STM32 HAL) +%% {@link init/1}, {@link deinit/1}, {@link write/3}, {@link read/3}, +%% {@link abort/1}, {@link get_state/1}, {@link get_error/1}, +%% {@link halfduplex_init/1}, {@link halfduplex_enable_tx/1}, +%% {@link halfduplex_enable_rx/1}. +%% These map directly to the corresponding HAL_UART_* functions. +%% +%% High-level API (`uart_hal' behavior) +%% {@link open/1}, {@link open/2}, {@link close/1}, {@link read/1}, +%% {@link read/2}, {@link write/2}. +%% @end +%%----------------------------------------------------------------------------- +-module(uart). +-behaviour(uart_hal). + +%% High-level API (uart_hal behaviour) +-export([open/1, open/2, close/1, read/1, read/2, write/2]). + +%% Low-level API (STM32 HAL) +-export([ + init/1, + deinit/1, + write/3, + read/3, + abort/1, + get_state/1, + get_error/1, + halfduplex_init/1, + halfduplex_enable_tx/1, + halfduplex_enable_rx/1 +]). + +-type peripheral() :: 1..9. +-type uart_resource() :: reference(). +-type uart() :: pid(). +-type uart_state() :: reset | ready | busy | busy_tx | busy_rx | busy_tx_rx | error | timeout. +-type pin() :: {Bank :: atom(), PinNum :: 0..15}. + +-type uart_config() :: #{ + peripheral => peripheral(), + tx => pin(), + rx => pin(), + speed => pos_integer(), + data_bits => 7 | 8 | 9, + stop_bits => 1 | 2, + parity => none | odd | even, + af => non_neg_integer() +}. + +-type halfduplex_config() :: #{ + peripheral => peripheral(), + tx => pin(), + speed => pos_integer(), + data_bits => 7 | 8 | 9, + stop_bits => 1 | 2, + parity => none | odd | even, + af => non_neg_integer() +}. + +-export_type([ + uart/0, uart_resource/0, peripheral/0, uart_state/0, pin/0, uart_config/0, halfduplex_config/0 +]). + +-define(DEFAULT_SPEED, 115200). +-define(DEFAULT_DATA_BITS, 8). +-define(DEFAULT_STOP_BITS, 1). +-define(DEFAULT_PARITY, none). +-define(DEFAULT_AF, 7). +-define(DEFAULT_PERIPHERAL, 1). +-define(DEFAULT_TIMEOUT_MS, 5000). + +%%----------------------------------------------------------------------------- +%% High-level API (uart_hal behaviour) +%%----------------------------------------------------------------------------- + +%%----------------------------------------------------------------------------- +%% @param Opts UART configuration options +%% @returns UART handle (pid) +%% @doc Open a UART connection with the given options. +%% +%% Options: +%%
    +%%
  • `{tx, Pin}' - TX pin (required for full-duplex)
  • +%%
  • `{rx, Pin}' - RX pin (required for full-duplex)
  • +%%
  • `{speed, 115200}' - Baud rate (default: 115200)
  • +%%
  • `{data_bits, 8}' - Data bits: 7, 8, or 9 (default: 8)
  • +%%
  • `{stop_bits, 1}' - Stop bits: 1 or 2 (default: 1)
  • +%%
  • `{parity, none}' - Parity: none, odd, or even (default: none)
  • +%%
  • `{peripheral, 1}' - UART peripheral number (default: 1)
  • +%%
  • `{af, 7}' - Alternate function number (default: 7)
  • +%%
+%% @end +%%----------------------------------------------------------------------------- +-spec open(Opts :: [{atom(), term()}]) -> uart() | {error, term()}. +open(Opts) -> + Config = parse_opts(Opts), + case ?MODULE:init(Config) of + {ok, Resource} -> + spawn_link(fun() -> loop(Resource) end); + {error, _} = Error -> + Error + end. + +%%----------------------------------------------------------------------------- +%% @param Name UART peripheral name (unused, for compatibility) +%% @param Opts UART configuration options +%% @returns UART handle (pid) +%% @doc Open a UART connection with the given options. +%% @end +%%----------------------------------------------------------------------------- +-spec open(Name :: string() | binary(), Opts :: [{atom(), term()}]) -> uart() | {error, term()}. +open(_Name, Opts) -> + open(Opts). + +%%----------------------------------------------------------------------------- +%% @param UART UART handle created via `open/1' or `open/2' +%% @returns `ok' +%% @doc Close the UART connection. +%% @end +%%----------------------------------------------------------------------------- +-spec close(UART :: uart()) -> ok. +close(Pid) when is_pid(Pid) -> + call(Pid, close). + +%%----------------------------------------------------------------------------- +%% @param UART UART handle created via `open/1' or `open/2' +%% @returns `{ok, Data}' or `{error, Reason}' +%% @doc Read data from the UART with default timeout. +%% @end +%%----------------------------------------------------------------------------- +-spec read(UART :: uart()) -> {ok, binary()} | {error, term()}. +read(Pid) when is_pid(Pid) -> + call(Pid, {read, ?DEFAULT_TIMEOUT_MS}). + +%%----------------------------------------------------------------------------- +%% @param UART UART handle created via `open/1' or `open/2' +%% @param Timeout Timeout in milliseconds +%% @returns `{ok, Data}' or `{error, Reason}' +%% @doc Read data from the UART with the specified timeout. +%% @end +%%----------------------------------------------------------------------------- +-spec read(UART :: uart(), Timeout :: pos_integer()) -> {ok, binary()} | {error, term()}. +read(Pid, Timeout) when is_pid(Pid), is_integer(Timeout), Timeout > 0 -> + call(Pid, {read, Timeout}). + +%%----------------------------------------------------------------------------- +%% @param UART UART handle created via `open/1' or `open/2' +%% @param Data Data to write (binary or iolist) +%% @returns `ok' or `{error, Reason}' +%% @doc Write data to the UART. +%% @end +%%----------------------------------------------------------------------------- +-spec write(UART :: uart(), Data :: iodata()) -> ok | {error, term()}. +write(Pid, Data) when is_pid(Pid) -> + call(Pid, {write, Data, ?DEFAULT_TIMEOUT_MS}). + +%%----------------------------------------------------------------------------- +%% Low-level API (STM32 HAL) +%%----------------------------------------------------------------------------- + +%%----------------------------------------------------------------------------- +%% @param Config UART configuration map +%% @returns `{ok, Resource}' +%% @doc Initialize the UART HW block (HAL_UART_Init). +%% +%% The Config map should contain: +%%
    +%%
  • `{tx, Pin}' - TX pin (required)
  • +%%
  • `{rx, Pin}' - RX pin (required)
  • +%%
  • `{peripheral, 1}' - UART peripheral number (default: 1)
  • +%%
  • `{speed, 115200}' - Baud rate (default: 115200)
  • +%%
  • `{data_bits, 8}' - Data bits: 7, 8, or 9 (default: 8)
  • +%%
  • `{stop_bits, 1}' - Stop bits: 1 or 2 (default: 1)
  • +%%
  • `{parity, none}' - Parity: none, odd, or even (default: none)
  • +%%
  • `{af, 7}' - Alternate function number (default: 7)
  • +%%
+%% @end +%%----------------------------------------------------------------------------- +-spec init(Config :: uart_config() | [{atom(), term()}]) -> {ok, uart_resource()}. +init(_Config) -> + erlang:nif_error(undefined). + +%%----------------------------------------------------------------- +%% @param Resource UART resource returned by `init/1' +%% @returns `ok' +%% @doc Disable the UART HW block (HAL_UART_DeInit). +%% @end +%%----------------------------------------------------------------- +-spec deinit(Resource :: uart_resource()) -> ok. +deinit(_Resource) -> + erlang:nif_error(undefined). + +%%----------------------------------------------------------------- +%% @param Resource UART resource returned by `init/1' +%% @param Data Data to write +%% @param TimeoutMs Timeout in milliseconds or `infinity' +%% @returns Number of bytes written or `{error, Reason}' +%% @doc Write data (HAL_UART_Transmit). +%% @end +%%----------------------------------------------------------------- +-spec write(Resource :: uart_resource(), Data :: binary(), TimeoutMs :: timeout()) -> + non_neg_integer() | {error, term()}. +write(_Resource, _Data, _TimeoutMs) -> + erlang:nif_error(undefined). + +%%----------------------------------------------------------------- +%% @param Resource UART resource returned by `init/1' +%% @param Count Number of bytes to read +%% @param TimeoutMs Timeout in milliseconds or `infinity' +%% @returns `{ok, Data}' or `{error, Reason}' +%% @doc Read data (HAL_UART_Receive). +%% @end +%%----------------------------------------------------------------- +-spec read(Resource :: uart_resource(), Count :: non_neg_integer(), TimeoutMs :: timeout()) -> + {ok, binary()} | {error, term()}. +read(_Resource, _Count, _TimeoutMs) -> + erlang:nif_error(undefined). + +%%----------------------------------------------------------------- +%% @param Resource UART resource returned by `init/1' +%% @returns `ok' or `{error, Reason}' +%% @doc Abort ongoing UART transfer (HAL_UART_Abort). +%% @end +%%----------------------------------------------------------------- +-spec abort(Resource :: uart_resource()) -> ok | {error, term()}. +abort(_Resource) -> + erlang:nif_error(undefined). + +%%----------------------------------------------------------------- +%% @param Resource UART resource returned by `init/1' +%% @returns UART state atom +%% @doc Get the UART state (HAL_UART_GetState). +%% @end +%%----------------------------------------------------------------- +-spec get_state(Resource :: uart_resource()) -> uart_state(). +get_state(_Resource) -> + erlang:nif_error(undefined). + +%%----------------------------------------------------------------- +%% @param Resource UART resource returned by `init/1' +%% @returns Error code as integer +%% @doc Get the UART error (HAL_UART_GetError). +%% @end +%%----------------------------------------------------------------- +-spec get_error(Resource :: uart_resource()) -> non_neg_integer(). +get_error(_Resource) -> + erlang:nif_error(undefined). + +%%----------------------------------------------------------------- +%% @param Config Half-duplex UART configuration +%% @returns `{ok, Resource}' +%% @doc Initialize UART in half-duplex mode (HAL_HalfDuplex_Init). +%% @end +%%----------------------------------------------------------------- +-spec halfduplex_init(Config :: halfduplex_config() | [{atom(), term()}]) -> {ok, uart_resource()}. +halfduplex_init(_Config) -> + erlang:nif_error(undefined). + +%%----------------------------------------------------------------- +%% @param Resource UART resource from `halfduplex_init/1' +%% @returns `ok' or `error' +%% @doc Enable transmitter (HAL_HalfDuplex_EnableTransmitter). +%% @end +%%----------------------------------------------------------------- +-spec halfduplex_enable_tx(Resource :: uart_resource()) -> ok | error. +halfduplex_enable_tx(_Resource) -> + erlang:nif_error(undefined). + +%%----------------------------------------------------------------- +%% @param Resource UART resource from `halfduplex_init/1' +%% @returns `ok' or `error' +%% @doc Enable receiver (HAL_HalfDuplex_EnableReceiver). +%% @end +%%----------------------------------------------------------------- +-spec halfduplex_enable_rx(Resource :: uart_resource()) -> ok | error. +halfduplex_enable_rx(_Resource) -> + erlang:nif_error(undefined). + +%%----------------------------------------------------------------- +%% Internal helpers +%%----------------------------------------------------------------- + +call(Pid, Request) -> + MRef = erlang:monitor(process, Pid), + Ref = make_ref(), + Pid ! {self(), Ref, Request}, + receive + {Ref, Reply} -> + erlang:demonitor(MRef, [flush]), + Reply; + {'DOWN', MRef, process, Pid, Reason} -> + {error, {server_died, Reason}} + end. + +loop(Resource) -> + receive + {From, Ref, Request} -> + case handle_request(Resource, Request) of + {reply, Reply, stop} -> + From ! {Ref, Reply}; + {reply, Reply} -> + From ! {Ref, Reply}, + loop(Resource) + end + end. + +handle_request(Resource, close) -> + ?MODULE:deinit(Resource), + {reply, ok, stop}; +handle_request(Resource, {read, Timeout}) -> + Result = ?MODULE:read(Resource, 1, Timeout), + {reply, Result}; +handle_request(Resource, {write, Data, Timeout}) -> + Bin = iolist_to_binary(Data), + case ?MODULE:write(Resource, Bin, Timeout) of + N when is_integer(N) -> {reply, ok}; + {error, _} = Error -> {reply, Error} + end. + +parse_opts(Opts) -> + Tx = proplists:get_value(tx, Opts), + Rx = proplists:get_value(rx, Opts), + Tx =:= undefined andalso error({missing_required_option, tx}), + Rx =:= undefined andalso error({missing_required_option, rx}), + Speed = proplists:get_value(speed, Opts, ?DEFAULT_SPEED), + DataBits = proplists:get_value(data_bits, Opts, ?DEFAULT_DATA_BITS), + StopBits = proplists:get_value(stop_bits, Opts, ?DEFAULT_STOP_BITS), + Parity = proplists:get_value(parity, Opts, ?DEFAULT_PARITY), + Peripheral = proplists:get_value(peripheral, Opts, ?DEFAULT_PERIPHERAL), + AF = proplists:get_value(af, Opts, ?DEFAULT_AF), + [ + {peripheral, Peripheral}, + {tx, Tx}, + {rx, Rx}, + {af, AF}, + {speed, Speed}, + {data_bits, DataBits}, + {stop_bits, StopBits}, + {parity, parity_to_int(Parity)} + ]. + +parity_to_int(none) -> 0; +parity_to_int(odd) -> 1; +parity_to_int(even) -> 2. diff --git a/libs/eavmlib/src/uart_hal.erl b/libs/eavmlib/src/uart_hal.erl index 1dc8cbf9de..7c09c792ee 100644 --- a/libs/eavmlib/src/uart_hal.erl +++ b/libs/eavmlib/src/uart_hal.erl @@ -26,7 +26,7 @@ %% Asynchronous Receiver-Transmitter) operations across all supported %% platforms. %% -%% Currently, only ESP32 provides a UART implementation. +%% ESP32, RP2, STM32 and generic unix platforms provide UART implementations. %% %%

Lifecycle

%% diff --git a/src/platforms/rp2/src/lib/CMakeLists.txt b/src/platforms/rp2/src/lib/CMakeLists.txt index 7e0e535a4d..7410586f04 100644 --- a/src/platforms/rp2/src/lib/CMakeLists.txt +++ b/src/platforms/rp2/src/lib/CMakeLists.txt @@ -33,6 +33,7 @@ set(SOURCE_FILES gpiodriver.c i2cdriver.c spidriver.c + uartdriver.c networkdriver.c otp_crypto_platform.c platform_defaultatoms.c @@ -62,6 +63,7 @@ target_link_libraries( hardware_gpio hardware_i2c hardware_spi + hardware_uart hardware_sync pico_float pico_mbedtls @@ -126,4 +128,4 @@ if (NOT AVM_DISABLE_JIT) target_link_options(libAtomVM${PLATFORM_LIB_SUFFIX} PUBLIC "SHELL:-Wl,-u -Wl,jit_stream_flash_get_nif") endif() -target_link_options(libAtomVM${PLATFORM_LIB_SUFFIX} PUBLIC "SHELL:-Wl,-u -Wl,gpio_nif -Wl,-u -Wl,i2c_nif -Wl,-u -Wl,spi_nif -Wl,-u -Wl,otp_crypto_nif") +target_link_options(libAtomVM${PLATFORM_LIB_SUFFIX} PUBLIC "SHELL:-Wl,-u -Wl,gpio_nif -Wl,-u -Wl,i2c_nif -Wl,-u -Wl,spi_nif -Wl,-u -Wl,uart_nif -Wl,-u -Wl,otp_crypto_nif") diff --git a/src/platforms/rp2/src/lib/uartdriver.c b/src/platforms/rp2/src/lib/uartdriver.c new file mode 100644 index 0000000000..75364d77d0 --- /dev/null +++ b/src/platforms/rp2/src/lib/uartdriver.c @@ -0,0 +1,645 @@ +/* + * This file is part of AtomVM. + * + * Copyright 2026 Paul Guyot + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later + */ + +#include +#include +#include + +#include + +#include "context.h" +#include "defaultatoms.h" +#include "erl_nif.h" +#include "erl_nif_priv.h" +#include "globalcontext.h" +#include "interop.h" +#include "memory.h" +#include "nifs.h" +#include "rp2_sys.h" +#include "term.h" + +// #define ENABLE_TRACE +#include "trace.h" + +#define NUM_UART_INSTANCES 2 + +static ErlNifResourceType *uart_resource_type; + +struct UARTResource +{ + uart_inst_t *uart_inst; +}; + +static term create_pair(Context *ctx, term term1, term term2) +{ + term ret = term_alloc_tuple(2, &ctx->heap); + term_put_tuple_element(ret, 0, term1); + term_put_tuple_element(ret, 1, term2); + return ret; +} + +static bool get_uart_resource(Context *ctx, term resource_term, struct UARTResource **rsrc_obj) +{ + void *rsrc_obj_ptr; + if (UNLIKELY(!enif_get_resource(erl_nif_env_from_context(ctx), resource_term, uart_resource_type, &rsrc_obj_ptr))) { + return false; + } + *rsrc_obj = (struct UARTResource *) rsrc_obj_ptr; + return true; +} + +static term nif_uart_init(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + VALIDATE_VALUE(argv[0], term_is_integer); + VALIDATE_VALUE(argv[1], term_is_integer); + + int peripheral = term_to_int(argv[0]); + if (UNLIKELY(peripheral < 0 || peripheral >= NUM_UART_INSTANCES)) { + RAISE_ERROR(BADARG_ATOM); + } + + int baudrate_val = term_to_int(argv[1]); + if (UNLIKELY(baudrate_val <= 0)) { + RAISE_ERROR(BADARG_ATOM); + } + uint baudrate = (uint) baudrate_val; + uart_inst_t *inst = uart_get_instance((uint) peripheral); + + if (UNLIKELY(uart_is_enabled(inst))) { + RAISE_ERROR(BADARG_ATOM); + } + + uint actual_baudrate = uart_init(inst, baudrate); + + struct UARTResource *rsrc_obj = enif_alloc_resource(uart_resource_type, sizeof(struct UARTResource)); + if (IS_NULL_PTR(rsrc_obj)) { + uart_deinit(inst); + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + rsrc_obj->uart_inst = inst; + + if (UNLIKELY(memory_ensure_free(ctx, TERM_BOXED_RESOURCE_SIZE) != MEMORY_GC_OK)) { + uart_deinit(inst); + enif_release_resource(rsrc_obj); + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + term obj = term_from_resource(rsrc_obj, &ctx->heap); + enif_release_resource(rsrc_obj); + + size_t requested_size = TUPLE_SIZE(2) + TUPLE_SIZE(2); + if (UNLIKELY(memory_ensure_free_with_roots(ctx, requested_size, 1, &obj, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + + term inner = term_alloc_tuple(2, &ctx->heap); + term_put_tuple_element(inner, 0, term_from_int28((int32_t) actual_baudrate)); + term_put_tuple_element(inner, 1, obj); + + return create_pair(ctx, OK_ATOM, inner); +} + +static term nif_uart_deinit(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + struct UARTResource *rsrc_obj; + if (UNLIKELY(!get_uart_resource(ctx, argv[0], &rsrc_obj))) { + RAISE_ERROR(BADARG_ATOM); + } + + if (IS_NULL_PTR(rsrc_obj->uart_inst)) { + RAISE_ERROR(BADARG_ATOM); + } + uart_deinit(rsrc_obj->uart_inst); + rsrc_obj->uart_inst = NULL; + + return OK_ATOM; +} + +static term nif_uart_set_baudrate(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + struct UARTResource *rsrc_obj; + if (UNLIKELY(!get_uart_resource(ctx, argv[0], &rsrc_obj))) { + RAISE_ERROR(BADARG_ATOM); + } + if (IS_NULL_PTR(rsrc_obj->uart_inst)) { + RAISE_ERROR(BADARG_ATOM); + } + VALIDATE_VALUE(argv[1], term_is_integer); + + int baudrate_val = term_to_int(argv[1]); + if (UNLIKELY(baudrate_val <= 0)) { + RAISE_ERROR(BADARG_ATOM); + } + uint baudrate = (uint) baudrate_val; + uint actual = uart_set_baudrate(rsrc_obj->uart_inst, baudrate); + + if (UNLIKELY(memory_ensure_free(ctx, TUPLE_SIZE(2)) != MEMORY_GC_OK)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + return create_pair(ctx, OK_ATOM, term_from_int28((int32_t) actual)); +} + +static term nif_uart_set_format(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + struct UARTResource *rsrc_obj; + if (UNLIKELY(!get_uart_resource(ctx, argv[0], &rsrc_obj))) { + RAISE_ERROR(BADARG_ATOM); + } + if (IS_NULL_PTR(rsrc_obj->uart_inst)) { + RAISE_ERROR(BADARG_ATOM); + } + VALIDATE_VALUE(argv[1], term_is_integer); + VALIDATE_VALUE(argv[2], term_is_integer); + VALIDATE_VALUE(argv[3], term_is_atom); + + int data_bits = term_to_int(argv[1]); + int stop_bits = term_to_int(argv[2]); + if (data_bits < 5 || data_bits > 8) { + RAISE_ERROR(BADARG_ATOM); + } + if (stop_bits != 1 && stop_bits != 2) { + RAISE_ERROR(BADARG_ATOM); + } + + term parity_term = argv[3]; + uart_parity_t parity; + if (parity_term == globalcontext_make_atom(ctx->global, ATOM_STR("\x4", "none"))) { + parity = UART_PARITY_NONE; + } else if (parity_term == globalcontext_make_atom(ctx->global, ATOM_STR("\x4", "even"))) { + parity = UART_PARITY_EVEN; + } else if (parity_term == globalcontext_make_atom(ctx->global, ATOM_STR("\x3", "odd"))) { + parity = UART_PARITY_ODD; + } else { + RAISE_ERROR(BADARG_ATOM); + } + + uart_set_format(rsrc_obj->uart_inst, (uint) data_bits, (uint) stop_bits, parity); + + return OK_ATOM; +} + +static term nif_uart_set_hw_flow(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + struct UARTResource *rsrc_obj; + if (UNLIKELY(!get_uart_resource(ctx, argv[0], &rsrc_obj))) { + RAISE_ERROR(BADARG_ATOM); + } + if (IS_NULL_PTR(rsrc_obj->uart_inst)) { + RAISE_ERROR(BADARG_ATOM); + } + VALIDATE_VALUE(argv[1], term_is_atom); + VALIDATE_VALUE(argv[2], term_is_atom); + + bool cts = (argv[1] == TRUE_ATOM); + bool rts = (argv[2] == TRUE_ATOM); + + uart_set_hw_flow(rsrc_obj->uart_inst, cts, rts); + + return OK_ATOM; +} + +static term nif_uart_write_blocking(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + struct UARTResource *rsrc_obj; + if (UNLIKELY(!get_uart_resource(ctx, argv[0], &rsrc_obj))) { + RAISE_ERROR(BADARG_ATOM); + } + if (IS_NULL_PTR(rsrc_obj->uart_inst)) { + RAISE_ERROR(BADARG_ATOM); + } + VALIDATE_VALUE(argv[1], term_is_binary); + + const uint8_t *data = (const uint8_t *) term_binary_data(argv[1]); + size_t len = term_binary_size(argv[1]); + + uart_write_blocking(rsrc_obj->uart_inst, data, len); + + return OK_ATOM; +} + +static term nif_uart_is_readable(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + struct UARTResource *rsrc_obj; + if (UNLIKELY(!get_uart_resource(ctx, argv[0], &rsrc_obj))) { + RAISE_ERROR(BADARG_ATOM); + } + if (IS_NULL_PTR(rsrc_obj->uart_inst)) { + RAISE_ERROR(BADARG_ATOM); + } + + bool readable = uart_is_readable(rsrc_obj->uart_inst); + return readable ? TRUE_ATOM : FALSE_ATOM; +} + +static term nif_uart_read_blocking(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + struct UARTResource *rsrc_obj; + if (UNLIKELY(!get_uart_resource(ctx, argv[0], &rsrc_obj))) { + RAISE_ERROR(BADARG_ATOM); + } + if (IS_NULL_PTR(rsrc_obj->uart_inst)) { + RAISE_ERROR(BADARG_ATOM); + } + VALIDATE_VALUE(argv[1], term_is_integer); + + avm_int_t count = term_to_int(argv[1]); + if (UNLIKELY(count < 0)) { + RAISE_ERROR(BADARG_ATOM); + } + + if (UNLIKELY(memory_ensure_free_opt(ctx, TUPLE_SIZE(2) + term_binary_heap_size(count), MEMORY_NO_GC) != MEMORY_GC_OK)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + term data = term_create_uninitialized_binary(count, &ctx->heap, ctx->global); + uint8_t *buf = (uint8_t *) term_binary_data(data); + + uart_read_blocking(rsrc_obj->uart_inst, buf, (size_t) count); + + return create_pair(ctx, OK_ATOM, data); +} + +static term nif_uart_is_writable(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + struct UARTResource *rsrc_obj; + if (UNLIKELY(!get_uart_resource(ctx, argv[0], &rsrc_obj))) { + RAISE_ERROR(BADARG_ATOM); + } + if (IS_NULL_PTR(rsrc_obj->uart_inst)) { + RAISE_ERROR(BADARG_ATOM); + } + + bool writable = uart_is_writable(rsrc_obj->uart_inst); + return writable ? TRUE_ATOM : FALSE_ATOM; +} + +static term nif_uart_tx_wait_blocking(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + struct UARTResource *rsrc_obj; + if (UNLIKELY(!get_uart_resource(ctx, argv[0], &rsrc_obj))) { + RAISE_ERROR(BADARG_ATOM); + } + if (IS_NULL_PTR(rsrc_obj->uart_inst)) { + RAISE_ERROR(BADARG_ATOM); + } + + uart_tx_wait_blocking(rsrc_obj->uart_inst); + + return OK_ATOM; +} + +static term nif_uart_set_fifo_enabled(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + struct UARTResource *rsrc_obj; + if (UNLIKELY(!get_uart_resource(ctx, argv[0], &rsrc_obj))) { + RAISE_ERROR(BADARG_ATOM); + } + if (IS_NULL_PTR(rsrc_obj->uart_inst)) { + RAISE_ERROR(BADARG_ATOM); + } + VALIDATE_VALUE(argv[1], term_is_atom); + + bool enabled = (argv[1] == TRUE_ATOM); + uart_set_fifo_enabled(rsrc_obj->uart_inst, enabled); + + return OK_ATOM; +} + +static term nif_uart_set_break(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + struct UARTResource *rsrc_obj; + if (UNLIKELY(!get_uart_resource(ctx, argv[0], &rsrc_obj))) { + RAISE_ERROR(BADARG_ATOM); + } + if (IS_NULL_PTR(rsrc_obj->uart_inst)) { + RAISE_ERROR(BADARG_ATOM); + } + VALIDATE_VALUE(argv[1], term_is_atom); + + bool en = (argv[1] == TRUE_ATOM); + uart_set_break(rsrc_obj->uart_inst, en); + + return OK_ATOM; +} + +static term nif_uart_putc_raw(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + struct UARTResource *rsrc_obj; + if (UNLIKELY(!get_uart_resource(ctx, argv[0], &rsrc_obj))) { + RAISE_ERROR(BADARG_ATOM); + } + if (IS_NULL_PTR(rsrc_obj->uart_inst)) { + RAISE_ERROR(BADARG_ATOM); + } + VALIDATE_VALUE(argv[1], term_is_integer); + + int c_val = term_to_int(argv[1]); + if (UNLIKELY(c_val < 0 || c_val > 255)) { + RAISE_ERROR(BADARG_ATOM); + } + char c = (char) c_val; + uart_putc_raw(rsrc_obj->uart_inst, c); + + return OK_ATOM; +} + +static term nif_uart_putc(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + struct UARTResource *rsrc_obj; + if (UNLIKELY(!get_uart_resource(ctx, argv[0], &rsrc_obj))) { + RAISE_ERROR(BADARG_ATOM); + } + if (IS_NULL_PTR(rsrc_obj->uart_inst)) { + RAISE_ERROR(BADARG_ATOM); + } + VALIDATE_VALUE(argv[1], term_is_integer); + + int c_val = term_to_int(argv[1]); + if (UNLIKELY(c_val < 0 || c_val > 255)) { + RAISE_ERROR(BADARG_ATOM); + } + char c = (char) c_val; + uart_putc(rsrc_obj->uart_inst, c); + + return OK_ATOM; +} + +static term nif_uart_puts(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + struct UARTResource *rsrc_obj; + if (UNLIKELY(!get_uart_resource(ctx, argv[0], &rsrc_obj))) { + RAISE_ERROR(BADARG_ATOM); + } + if (IS_NULL_PTR(rsrc_obj->uart_inst)) { + RAISE_ERROR(BADARG_ATOM); + } + VALIDATE_VALUE(argv[1], term_is_binary); + + size_t len = term_binary_size(argv[1]); + const char *data = term_binary_data(argv[1]); + for (size_t i = 0; i < len; i++) { + uart_putc(rsrc_obj->uart_inst, data[i]); + } + + return OK_ATOM; +} + +static term nif_uart_getc(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + struct UARTResource *rsrc_obj; + if (UNLIKELY(!get_uart_resource(ctx, argv[0], &rsrc_obj))) { + RAISE_ERROR(BADARG_ATOM); + } + if (IS_NULL_PTR(rsrc_obj->uart_inst)) { + RAISE_ERROR(BADARG_ATOM); + } + + char c = uart_getc(rsrc_obj->uart_inst); + + if (UNLIKELY(memory_ensure_free(ctx, TUPLE_SIZE(2)) != MEMORY_GC_OK)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + return create_pair(ctx, OK_ATOM, term_from_int11((int16_t) (uint8_t) c)); +} + +static term nif_uart_is_readable_within_us(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + struct UARTResource *rsrc_obj; + if (UNLIKELY(!get_uart_resource(ctx, argv[0], &rsrc_obj))) { + RAISE_ERROR(BADARG_ATOM); + } + if (IS_NULL_PTR(rsrc_obj->uart_inst)) { + RAISE_ERROR(BADARG_ATOM); + } + VALIDATE_VALUE(argv[1], term_is_integer); + + int us_val = term_to_int(argv[1]); + if (UNLIKELY(us_val < 0)) { + RAISE_ERROR(BADARG_ATOM); + } + uint32_t us = (uint32_t) us_val; + bool readable = uart_is_readable_within_us(rsrc_obj->uart_inst, us); + return readable ? TRUE_ATOM : FALSE_ATOM; +} + +static void uart_resource_dtor(ErlNifEnv *caller_env, void *obj) +{ + UNUSED(caller_env); + struct UARTResource *rsrc_obj = (struct UARTResource *) obj; + if (!IS_NULL_PTR(rsrc_obj->uart_inst)) { + uart_deinit(rsrc_obj->uart_inst); + rsrc_obj->uart_inst = NULL; + } +} + +static const ErlNifResourceTypeInit UARTResourceTypeInit = { + .members = 1, + .dtor = uart_resource_dtor, +}; + +// +// NIF structs +// +static const struct Nif uart_init_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_uart_init +}; +static const struct Nif uart_deinit_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_uart_deinit +}; +static const struct Nif uart_set_baudrate_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_uart_set_baudrate +}; +static const struct Nif uart_set_format_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_uart_set_format +}; +static const struct Nif uart_set_hw_flow_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_uart_set_hw_flow +}; +static const struct Nif uart_write_blocking_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_uart_write_blocking +}; +static const struct Nif uart_is_readable_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_uart_is_readable +}; +static const struct Nif uart_read_blocking_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_uart_read_blocking +}; +static const struct Nif uart_is_writable_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_uart_is_writable +}; +static const struct Nif uart_tx_wait_blocking_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_uart_tx_wait_blocking +}; +static const struct Nif uart_set_fifo_enabled_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_uart_set_fifo_enabled +}; +static const struct Nif uart_set_break_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_uart_set_break +}; +static const struct Nif uart_putc_raw_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_uart_putc_raw +}; +static const struct Nif uart_putc_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_uart_putc +}; +static const struct Nif uart_puts_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_uart_puts +}; +static const struct Nif uart_getc_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_uart_getc +}; +static const struct Nif uart_is_readable_within_us_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_uart_is_readable_within_us +}; + +static void uart_nif_init(GlobalContext *global) +{ + ErlNifEnv env; + erl_nif_env_partial_init_from_globalcontext(&env, global); + uart_resource_type = enif_init_resource_type(&env, "uart_resource", &UARTResourceTypeInit, ERL_NIF_RT_CREATE, NULL); +} + +static const struct Nif *uart_nif_get_nif(const char *nifname) +{ + if (strncmp("uart:", nifname, 5) != 0) { + return NULL; + } + const char *rest = nifname + 5; + if (strcmp("init/2", rest) == 0) { + TRACE("Resolved uart nif %s ...\n", nifname); + return &uart_init_nif; + } + if (strcmp("deinit/1", rest) == 0) { + TRACE("Resolved uart nif %s ...\n", nifname); + return &uart_deinit_nif; + } + if (strcmp("set_baudrate/2", rest) == 0) { + TRACE("Resolved uart nif %s ...\n", nifname); + return &uart_set_baudrate_nif; + } + if (strcmp("set_format/4", rest) == 0) { + TRACE("Resolved uart nif %s ...\n", nifname); + return &uart_set_format_nif; + } + if (strcmp("set_hw_flow/3", rest) == 0) { + TRACE("Resolved uart nif %s ...\n", nifname); + return &uart_set_hw_flow_nif; + } + if (strcmp("write_blocking/2", rest) == 0) { + TRACE("Resolved uart nif %s ...\n", nifname); + return &uart_write_blocking_nif; + } + if (strcmp("is_readable/1", rest) == 0) { + TRACE("Resolved uart nif %s ...\n", nifname); + return &uart_is_readable_nif; + } + if (strcmp("read_blocking/2", rest) == 0) { + TRACE("Resolved uart nif %s ...\n", nifname); + return &uart_read_blocking_nif; + } + if (strcmp("is_writable/1", rest) == 0) { + TRACE("Resolved uart nif %s ...\n", nifname); + return &uart_is_writable_nif; + } + if (strcmp("tx_wait_blocking/1", rest) == 0) { + TRACE("Resolved uart nif %s ...\n", nifname); + return &uart_tx_wait_blocking_nif; + } + if (strcmp("set_fifo_enabled/2", rest) == 0) { + TRACE("Resolved uart nif %s ...\n", nifname); + return &uart_set_fifo_enabled_nif; + } + if (strcmp("set_break/2", rest) == 0) { + TRACE("Resolved uart nif %s ...\n", nifname); + return &uart_set_break_nif; + } + if (strcmp("putc_raw/2", rest) == 0) { + TRACE("Resolved uart nif %s ...\n", nifname); + return &uart_putc_raw_nif; + } + if (strcmp("putc/2", rest) == 0) { + TRACE("Resolved uart nif %s ...\n", nifname); + return &uart_putc_nif; + } + if (strcmp("puts/2", rest) == 0) { + TRACE("Resolved uart nif %s ...\n", nifname); + return &uart_puts_nif; + } + if (strcmp("getc/1", rest) == 0) { + TRACE("Resolved uart nif %s ...\n", nifname); + return &uart_getc_nif; + } + if (strcmp("is_readable_within_us/2", rest) == 0) { + TRACE("Resolved uart nif %s ...\n", nifname); + return &uart_is_readable_within_us_nif; + } + return NULL; +} + +REGISTER_NIF_COLLECTION(uart, uart_nif_init, NULL, uart_nif_get_nif) diff --git a/src/platforms/stm32/src/lib/CMakeLists.txt b/src/platforms/stm32/src/lib/CMakeLists.txt index bae8bc885a..4c738e1625 100644 --- a/src/platforms/stm32/src/lib/CMakeLists.txt +++ b/src/platforms/stm32/src/lib/CMakeLists.txt @@ -35,6 +35,7 @@ set(SOURCE_FILES gpio_driver.c i2c_driver.c spi_driver.c + uart_driver.c jit_stream_flash.c platform_nifs.c sys.c diff --git a/src/platforms/stm32/src/lib/uart_driver.c b/src/platforms/stm32/src/lib/uart_driver.c new file mode 100644 index 0000000000..41f68c8e0c --- /dev/null +++ b/src/platforms/stm32/src/lib/uart_driver.c @@ -0,0 +1,892 @@ +/* + * This file is part of AtomVM. + * + * Copyright 2026 Paul Guyot + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later + */ + +#include +#include +#include + +#include "stm32_hal_platform.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// #define ENABLE_TRACE +#include + +#include "avm_log.h" +#include "stm_sys.h" + +#define TAG "uart_driver" + +static ErlNifResourceType *uart_resource_type; + +struct UARTResource +{ + UART_HandleTypeDef handle; + bool is_half_duplex; +}; + +static const AtomStringIntPair gpio_bank_table[] = { + { ATOM_STR("\x1", "a"), (int) GPIOA }, + { ATOM_STR("\x1", "b"), (int) GPIOB }, + { ATOM_STR("\x1", "c"), (int) GPIOC }, + { ATOM_STR("\x1", "d"), (int) GPIOD }, + { ATOM_STR("\x1", "e"), (int) GPIOE }, +#ifdef GPIOF + { ATOM_STR("\x1", "f"), (int) GPIOF }, +#endif +#ifdef GPIOG + { ATOM_STR("\x1", "g"), (int) GPIOG }, +#endif +#ifdef GPIOH + { ATOM_STR("\x1", "h"), (int) GPIOH }, +#endif +#ifdef GPIOI + { ATOM_STR("\x1", "i"), (int) GPIOI }, +#endif +#ifdef GPIOJ + { ATOM_STR("\x1", "j"), (int) GPIOJ }, +#endif +#ifdef GPIOK + { ATOM_STR("\x1", "k"), (int) GPIOK }, +#endif + SELECT_INT_DEFAULT(0) +}; + +static term create_pair(Context *ctx, term term1, term term2) +{ + term ret = term_alloc_tuple(2, &ctx->heap); + term_put_tuple_element(ret, 0, term1); + term_put_tuple_element(ret, 1, term2); + return ret; +} + +static bool get_uart_resource(Context *ctx, term resource_term, struct UARTResource **rsrc_obj) +{ + void *rsrc_obj_ptr; + if (UNLIKELY(!enif_get_resource(erl_nif_env_from_context(ctx), resource_term, uart_resource_type, &rsrc_obj_ptr))) { + return false; + } + *rsrc_obj = (struct UARTResource *) rsrc_obj_ptr; + return true; +} + +static bool get_timeout_ms(term timeout_term, uint32_t *out) +{ + if (term_is_atom(timeout_term)) { + if (timeout_term == INFINITY_ATOM) { + *out = HAL_MAX_DELAY; + return true; + } + return false; + } + if (!term_is_integer(timeout_term)) { + return false; + } + avm_int_t val = term_to_int(timeout_term); + if (val < 0) { + return false; + } + *out = (uint32_t) val; + return true; +} + +static term hal_status_to_error(Context *ctx, HAL_StatusTypeDef status) +{ + switch (status) { + case HAL_TIMEOUT: + return create_pair(ctx, ERROR_ATOM, TIMEOUT_ATOM); + case HAL_BUSY: + return create_pair(ctx, ERROR_ATOM, globalcontext_make_atom(ctx->global, ATOM_STR("\x4", "busy"))); + default: + return create_pair(ctx, ERROR_ATOM, globalcontext_make_atom(ctx->global, ATOM_STR("\x3", "eio"))); + } +} + +static USART_TypeDef *peripheral_to_instance(int peripheral) +{ + switch (peripheral) { +#ifdef USART1 + case 1: + return USART1; +#endif +#ifdef USART2 + case 2: + return USART2; +#endif +#ifdef USART3 + case 3: + return USART3; +#endif +#ifdef UART4 + case 4: + return UART4; +#endif +#ifdef UART5 + case 5: + return UART5; +#endif +#ifdef USART6 + case 6: + return USART6; +#endif +#ifdef UART7 + case 7: + return UART7; +#endif +#ifdef UART8 + case 8: + return UART8; +#endif +#ifdef LPUART1 + case 9: + return LPUART1; +#endif + default: + return NULL; + } +} + +static void enable_uart_clock(int peripheral) +{ + switch (peripheral) { +#ifdef USART1 + case 1: + __HAL_RCC_USART1_CLK_ENABLE(); + break; +#endif +#ifdef USART2 + case 2: + __HAL_RCC_USART2_CLK_ENABLE(); + break; +#endif +#ifdef USART3 + case 3: + __HAL_RCC_USART3_CLK_ENABLE(); + break; +#endif +#ifdef UART4 + case 4: + __HAL_RCC_UART4_CLK_ENABLE(); + break; +#endif +#ifdef UART5 + case 5: + __HAL_RCC_UART5_CLK_ENABLE(); + break; +#endif +#ifdef USART6 + case 6: + __HAL_RCC_USART6_CLK_ENABLE(); + break; +#endif +#ifdef UART7 + case 7: + __HAL_RCC_UART7_CLK_ENABLE(); + break; +#endif +#ifdef UART8 + case 8: + __HAL_RCC_UART8_CLK_ENABLE(); + break; +#endif +#ifdef LPUART1 + case 9: + __HAL_RCC_LPUART1_CLK_ENABLE(); + break; +#endif + default: + break; + } +} + +static bool parse_pin(GlobalContext *glb, term pin_term, GPIO_TypeDef **port, uint16_t *pin_mask) +{ + if (!term_is_tuple(pin_term) || term_get_tuple_arity(pin_term) != 2) { + return false; + } + term bank_atom = term_get_tuple_element(pin_term, 0); + if (!term_is_atom(bank_atom)) { + return false; + } + uint32_t gpio_bank = (uint32_t) interop_atom_term_select_int(gpio_bank_table, bank_atom, glb); + if (gpio_bank == 0) { + return false; + } + *port = (GPIO_TypeDef *) gpio_bank; + term pin_num_term = term_get_tuple_element(pin_term, 1); + if (!term_is_integer(pin_num_term)) { + return false; + } + int pin_num = term_to_int(pin_num_term); + if (pin_num < 0 || pin_num > 15) { + return false; + } + *pin_mask = (uint16_t) (1U << pin_num); + return true; +} + +static term nif_uart_init(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + term opts = argv[0]; + VALIDATE_VALUE(opts, term_is_list); + + GlobalContext *glb = ctx->global; + + static const char *const peripheral_str = ATOM_STR("\xA", "peripheral"); + static const char *const tx_str = ATOM_STR("\x2", "tx"); + static const char *const rx_str = ATOM_STR("\x2", "rx"); + static const char *const speed_str = ATOM_STR("\x5", "speed"); + static const char *const data_bits_str = ATOM_STR("\x9", "data_bits"); + static const char *const stop_bits_str = ATOM_STR("\x9", "stop_bits"); + static const char *const parity_str = ATOM_STR("\x6", "parity"); + static const char *const af_str = ATOM_STR("\x2", "af"); + + term peripheral_term = interop_kv_get_value_default(opts, peripheral_str, term_from_int(1), glb); + term tx_term = interop_kv_get_value(opts, tx_str, glb); + term rx_term = interop_kv_get_value(opts, rx_str, glb); + term speed_term = interop_kv_get_value_default(opts, speed_str, term_from_int(115200), glb); + term data_bits_term = interop_kv_get_value_default(opts, data_bits_str, term_from_int(8), glb); + term stop_bits_term = interop_kv_get_value_default(opts, stop_bits_str, term_from_int(1), glb); + term parity_term = interop_kv_get_value_default(opts, parity_str, term_from_int(0), glb); + term af_term = interop_kv_get_value_default(opts, af_str, term_from_int(7), glb); + + if (term_is_invalid_term(tx_term) || term_is_invalid_term(rx_term)) { + AVM_LOGE(TAG, "tx and rx pins are required"); + RAISE_ERROR(BADARG_ATOM); + } + + VALIDATE_VALUE(peripheral_term, term_is_integer); + VALIDATE_VALUE(speed_term, term_is_integer); + VALIDATE_VALUE(data_bits_term, term_is_integer); + VALIDATE_VALUE(stop_bits_term, term_is_integer); + VALIDATE_VALUE(parity_term, term_is_integer); + VALIDATE_VALUE(af_term, term_is_integer); + + int peripheral = term_to_int(peripheral_term); + uint32_t speed = (uint32_t) term_to_int(speed_term); + int data_bits = term_to_int(data_bits_term); + int stop_bits = term_to_int(stop_bits_term); + int parity = term_to_int(parity_term); + uint32_t af = (uint32_t) term_to_int(af_term); + + USART_TypeDef *instance = peripheral_to_instance(peripheral); + if (IS_NULL_PTR(instance)) { + AVM_LOGE(TAG, "Invalid UART peripheral: %d", peripheral); + RAISE_ERROR(BADARG_ATOM); + } + + GPIO_TypeDef *tx_port; + uint16_t tx_pin; + if (!parse_pin(glb, tx_term, &tx_port, &tx_pin)) { + AVM_LOGE(TAG, "Invalid TX pin"); + RAISE_ERROR(BADARG_ATOM); + } + + GPIO_TypeDef *rx_port; + uint16_t rx_pin; + if (!parse_pin(glb, rx_term, &rx_port, &rx_pin)) { + AVM_LOGE(TAG, "Invalid RX pin"); + RAISE_ERROR(BADARG_ATOM); + } + + enable_uart_clock(peripheral); + + GPIO_InitTypeDef gpio_init = { + .Mode = GPIO_MODE_AF_PP, + .Pull = GPIO_PULLUP, + .Speed = GPIO_SPEED_FREQ_HIGH, + .Alternate = af, + }; + gpio_init.Pin = tx_pin; + HAL_GPIO_Init(tx_port, &gpio_init); + gpio_init.Pin = rx_pin; + gpio_init.Mode = GPIO_MODE_AF_PP; + HAL_GPIO_Init(rx_port, &gpio_init); + + struct UARTResource *rsrc_obj = enif_alloc_resource(uart_resource_type, sizeof(struct UARTResource)); + if (IS_NULL_PTR(rsrc_obj)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + + memset(&rsrc_obj->handle, 0, sizeof(UART_HandleTypeDef)); + rsrc_obj->handle.Instance = instance; + rsrc_obj->is_half_duplex = false; + + rsrc_obj->handle.Init.BaudRate = speed; + + switch (data_bits) { +#ifdef UART_WORDLENGTH_7B + case 7: + rsrc_obj->handle.Init.WordLength = UART_WORDLENGTH_7B; + break; +#endif + case 8: + rsrc_obj->handle.Init.WordLength = UART_WORDLENGTH_8B; + break; + case 9: + rsrc_obj->handle.Init.WordLength = UART_WORDLENGTH_9B; + break; + default: + rsrc_obj->handle.Init.WordLength = UART_WORDLENGTH_8B; + break; + } + + switch (stop_bits) { + case 1: + rsrc_obj->handle.Init.StopBits = UART_STOPBITS_1; + break; + case 2: + rsrc_obj->handle.Init.StopBits = UART_STOPBITS_2; + break; + default: + rsrc_obj->handle.Init.StopBits = UART_STOPBITS_1; + break; + } + + switch (parity) { + case 0: + rsrc_obj->handle.Init.Parity = UART_PARITY_NONE; + break; + case 1: + rsrc_obj->handle.Init.Parity = UART_PARITY_ODD; + break; + case 2: + rsrc_obj->handle.Init.Parity = UART_PARITY_EVEN; + break; + default: + rsrc_obj->handle.Init.Parity = UART_PARITY_NONE; + break; + } + + rsrc_obj->handle.Init.Mode = UART_MODE_TX_RX; + rsrc_obj->handle.Init.HwFlowCtl = UART_HWCONTROL_NONE; + rsrc_obj->handle.Init.OverSampling = UART_OVERSAMPLING_16; + + HAL_StatusTypeDef status = HAL_UART_Init(&rsrc_obj->handle); + if (status != HAL_OK) { + enif_release_resource(rsrc_obj); + AVM_LOGE(TAG, "HAL_UART_Init failed: %d", (int) status); + if (UNLIKELY(memory_ensure_free(ctx, TUPLE_SIZE(2)) != MEMORY_GC_OK)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + return create_pair(ctx, ERROR_ATOM, globalcontext_make_atom(glb, ATOM_STR("\x9", "uart_init"))); + } + + if (UNLIKELY(memory_ensure_free(ctx, TERM_BOXED_RESOURCE_SIZE) != MEMORY_GC_OK)) { + HAL_UART_DeInit(&rsrc_obj->handle); + enif_release_resource(rsrc_obj); + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + + term obj = term_from_resource(rsrc_obj, &ctx->heap); + enif_release_resource(rsrc_obj); + + if (UNLIKELY(memory_ensure_free_with_roots(ctx, TUPLE_SIZE(2), 1, &obj, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + + return create_pair(ctx, OK_ATOM, obj); +} + +static term nif_uart_deinit(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + struct UARTResource *rsrc_obj; + if (UNLIKELY(!get_uart_resource(ctx, argv[0], &rsrc_obj))) { + RAISE_ERROR(BADARG_ATOM); + } + if (IS_NULL_PTR(rsrc_obj->handle.Instance)) { + return OK_ATOM; + } + HAL_UART_DeInit(&rsrc_obj->handle); + rsrc_obj->handle.Instance = NULL; + rsrc_obj->is_half_duplex = false; + return OK_ATOM; +} + +static term nif_uart_write(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + struct UARTResource *rsrc_obj; + if (UNLIKELY(!get_uart_resource(ctx, argv[0], &rsrc_obj))) { + RAISE_ERROR(BADARG_ATOM); + } + if (IS_NULL_PTR(rsrc_obj->handle.Instance)) { + RAISE_ERROR(BADARG_ATOM); + } + VALIDATE_VALUE(argv[1], term_is_binary); + + const uint8_t *data = (const uint8_t *) term_binary_data(argv[1]); + size_t len = term_binary_size(argv[1]); + uint32_t timeout_ms; + if (UNLIKELY(!get_timeout_ms(argv[2], &timeout_ms))) { + RAISE_ERROR(BADARG_ATOM); + } + if (UNLIKELY(len > UINT16_MAX)) { + RAISE_ERROR(BADARG_ATOM); + } + + HAL_StatusTypeDef status = HAL_UART_Transmit(&rsrc_obj->handle, (uint8_t *) data, (uint16_t) len, timeout_ms); + if (UNLIKELY(status != HAL_OK)) { + if (UNLIKELY(memory_ensure_free(ctx, TUPLE_SIZE(2)) != MEMORY_GC_OK)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + return hal_status_to_error(ctx, status); + } + return term_from_int((int) len); +} + +static term nif_uart_read(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + struct UARTResource *rsrc_obj; + if (UNLIKELY(!get_uart_resource(ctx, argv[0], &rsrc_obj))) { + RAISE_ERROR(BADARG_ATOM); + } + if (IS_NULL_PTR(rsrc_obj->handle.Instance)) { + RAISE_ERROR(BADARG_ATOM); + } + VALIDATE_VALUE(argv[1], term_is_integer); + avm_int_t count = term_to_int(argv[1]); + uint32_t timeout_ms; + if (UNLIKELY(!get_timeout_ms(argv[2], &timeout_ms))) { + RAISE_ERROR(BADARG_ATOM); + } + if (UNLIKELY(count < 0 || count > UINT16_MAX)) { + RAISE_ERROR(BADARG_ATOM); + } + + if (UNLIKELY(memory_ensure_free_opt(ctx, TUPLE_SIZE(2) + term_binary_heap_size(count), MEMORY_NO_GC) != MEMORY_GC_OK)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + + term data = term_create_uninitialized_binary(count, &ctx->heap, ctx->global); + uint8_t *buf = (uint8_t *) term_binary_data(data); + + HAL_StatusTypeDef status = HAL_UART_Receive(&rsrc_obj->handle, buf, (uint16_t) count, timeout_ms); + if (UNLIKELY(status != HAL_OK)) { + if (UNLIKELY(memory_ensure_free(ctx, TUPLE_SIZE(2)) != MEMORY_GC_OK)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + return hal_status_to_error(ctx, status); + } + + return create_pair(ctx, OK_ATOM, data); +} + +static term nif_uart_abort(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + struct UARTResource *rsrc_obj; + if (UNLIKELY(!get_uart_resource(ctx, argv[0], &rsrc_obj))) { + RAISE_ERROR(BADARG_ATOM); + } + if (IS_NULL_PTR(rsrc_obj->handle.Instance)) { + RAISE_ERROR(BADARG_ATOM); + } + HAL_StatusTypeDef status = HAL_UART_Abort(&rsrc_obj->handle); + if (UNLIKELY(status != HAL_OK)) { + if (UNLIKELY(memory_ensure_free(ctx, TUPLE_SIZE(2)) != MEMORY_GC_OK)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + return hal_status_to_error(ctx, status); + } + return OK_ATOM; +} + +static term nif_uart_get_state(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + struct UARTResource *rsrc_obj; + if (UNLIKELY(!get_uart_resource(ctx, argv[0], &rsrc_obj))) { + RAISE_ERROR(BADARG_ATOM); + } + if (IS_NULL_PTR(rsrc_obj->handle.Instance)) { + RAISE_ERROR(BADARG_ATOM); + } + + HAL_UART_StateTypeDef state = HAL_UART_GetState(&rsrc_obj->handle); + + static const char *const ready_str = ATOM_STR("\x5", "ready"); + static const char *const busy_str = ATOM_STR("\x4", "busy"); + static const char *const busy_tx_str = ATOM_STR("\x7", "busy_tx"); + static const char *const busy_rx_str = ATOM_STR("\x7", "busy_rx"); + static const char *const busy_tx_rx_str = ATOM_STR("\xA", "busy_tx_rx"); + static const char *const reset_str = ATOM_STR("\x5", "reset"); + + const char *state_str; + switch (state) { + case HAL_UART_STATE_READY: + state_str = ready_str; + break; + case HAL_UART_STATE_BUSY: + state_str = busy_str; + break; + case HAL_UART_STATE_BUSY_TX: + state_str = busy_tx_str; + break; + case HAL_UART_STATE_BUSY_RX: + state_str = busy_rx_str; + break; + case HAL_UART_STATE_BUSY_TX_RX: + state_str = busy_tx_rx_str; + break; + case HAL_UART_STATE_ERROR: + return ERROR_ATOM; + case HAL_UART_STATE_TIMEOUT: + return TIMEOUT_ATOM; + default: + state_str = reset_str; + break; + } + return globalcontext_make_atom(ctx->global, state_str); +} + +static term nif_uart_get_error(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + struct UARTResource *rsrc_obj; + if (UNLIKELY(!get_uart_resource(ctx, argv[0], &rsrc_obj))) { + RAISE_ERROR(BADARG_ATOM); + } + if (IS_NULL_PTR(rsrc_obj->handle.Instance)) { + RAISE_ERROR(BADARG_ATOM); + } + uint32_t err = HAL_UART_GetError(&rsrc_obj->handle); + return term_from_int((int) err); +} + +static term nif_uart_halfduplex_init(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + term opts = argv[0]; + VALIDATE_VALUE(opts, term_is_list); + + GlobalContext *glb = ctx->global; + + static const char *const peripheral_str = ATOM_STR("\xA", "peripheral"); + static const char *const tx_str = ATOM_STR("\x2", "tx"); + static const char *const speed_str = ATOM_STR("\x5", "speed"); + static const char *const data_bits_str = ATOM_STR("\x9", "data_bits"); + static const char *const stop_bits_str = ATOM_STR("\x9", "stop_bits"); + static const char *const parity_str = ATOM_STR("\x6", "parity"); + static const char *const af_str = ATOM_STR("\x2", "af"); + + term peripheral_term = interop_kv_get_value_default(opts, peripheral_str, term_from_int(1), glb); + term tx_term = interop_kv_get_value(opts, tx_str, glb); + term speed_term = interop_kv_get_value_default(opts, speed_str, term_from_int(115200), glb); + term data_bits_term = interop_kv_get_value_default(opts, data_bits_str, term_from_int(8), glb); + term stop_bits_term = interop_kv_get_value_default(opts, stop_bits_str, term_from_int(1), glb); + term parity_term = interop_kv_get_value_default(opts, parity_str, term_from_int(0), glb); + term af_term = interop_kv_get_value_default(opts, af_str, term_from_int(7), glb); + + if (term_is_invalid_term(tx_term)) { + AVM_LOGE(TAG, "tx pin is required for half-duplex"); + RAISE_ERROR(BADARG_ATOM); + } + + VALIDATE_VALUE(peripheral_term, term_is_integer); + VALIDATE_VALUE(speed_term, term_is_integer); + VALIDATE_VALUE(data_bits_term, term_is_integer); + VALIDATE_VALUE(stop_bits_term, term_is_integer); + VALIDATE_VALUE(parity_term, term_is_integer); + VALIDATE_VALUE(af_term, term_is_integer); + + int peripheral = term_to_int(peripheral_term); + uint32_t speed = (uint32_t) term_to_int(speed_term); + int data_bits = term_to_int(data_bits_term); + int stop_bits = term_to_int(stop_bits_term); + int parity = term_to_int(parity_term); + uint32_t af = (uint32_t) term_to_int(af_term); + + USART_TypeDef *instance = peripheral_to_instance(peripheral); + if (IS_NULL_PTR(instance)) { + AVM_LOGE(TAG, "Invalid UART peripheral: %d", peripheral); + RAISE_ERROR(BADARG_ATOM); + } + + GPIO_TypeDef *tx_port; + uint16_t tx_pin; + if (!parse_pin(glb, tx_term, &tx_port, &tx_pin)) { + AVM_LOGE(TAG, "Invalid TX pin"); + RAISE_ERROR(BADARG_ATOM); + } + + enable_uart_clock(peripheral); + + GPIO_InitTypeDef gpio_init = { + .Mode = GPIO_MODE_AF_OD, + .Pull = GPIO_NOPULL, + .Speed = GPIO_SPEED_FREQ_HIGH, + .Alternate = af, + }; + gpio_init.Pin = tx_pin; + HAL_GPIO_Init(tx_port, &gpio_init); + + struct UARTResource *rsrc_obj = enif_alloc_resource(uart_resource_type, sizeof(struct UARTResource)); + if (IS_NULL_PTR(rsrc_obj)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + + memset(&rsrc_obj->handle, 0, sizeof(UART_HandleTypeDef)); + rsrc_obj->handle.Instance = instance; + rsrc_obj->is_half_duplex = true; + + rsrc_obj->handle.Init.BaudRate = speed; + + switch (data_bits) { +#ifdef UART_WORDLENGTH_7B + case 7: + rsrc_obj->handle.Init.WordLength = UART_WORDLENGTH_7B; + break; +#endif + case 8: + rsrc_obj->handle.Init.WordLength = UART_WORDLENGTH_8B; + break; + case 9: + rsrc_obj->handle.Init.WordLength = UART_WORDLENGTH_9B; + break; + default: + rsrc_obj->handle.Init.WordLength = UART_WORDLENGTH_8B; + break; + } + + switch (stop_bits) { + case 1: + rsrc_obj->handle.Init.StopBits = UART_STOPBITS_1; + break; + case 2: + rsrc_obj->handle.Init.StopBits = UART_STOPBITS_2; + break; + default: + rsrc_obj->handle.Init.StopBits = UART_STOPBITS_1; + break; + } + + switch (parity) { + case 0: + rsrc_obj->handle.Init.Parity = UART_PARITY_NONE; + break; + case 1: + rsrc_obj->handle.Init.Parity = UART_PARITY_ODD; + break; + case 2: + rsrc_obj->handle.Init.Parity = UART_PARITY_EVEN; + break; + default: + rsrc_obj->handle.Init.Parity = UART_PARITY_NONE; + break; + } + + rsrc_obj->handle.Init.Mode = UART_MODE_TX_RX; + rsrc_obj->handle.Init.HwFlowCtl = UART_HWCONTROL_NONE; + rsrc_obj->handle.Init.OverSampling = UART_OVERSAMPLING_16; + + HAL_StatusTypeDef status = HAL_HalfDuplex_Init(&rsrc_obj->handle); + if (status != HAL_OK) { + enif_release_resource(rsrc_obj); + AVM_LOGE(TAG, "HAL_HalfDuplex_Init failed: %d", (int) status); + if (UNLIKELY(memory_ensure_free(ctx, TUPLE_SIZE(2)) != MEMORY_GC_OK)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + return create_pair(ctx, ERROR_ATOM, globalcontext_make_atom(glb, ATOM_STR("\xf", "halfduplex_init"))); + } + + if (UNLIKELY(memory_ensure_free(ctx, TERM_BOXED_RESOURCE_SIZE) != MEMORY_GC_OK)) { + HAL_UART_DeInit(&rsrc_obj->handle); + enif_release_resource(rsrc_obj); + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + + term obj = term_from_resource(rsrc_obj, &ctx->heap); + enif_release_resource(rsrc_obj); + + if (UNLIKELY(memory_ensure_free_with_roots(ctx, TUPLE_SIZE(2), 1, &obj, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + + return create_pair(ctx, OK_ATOM, obj); +} + +static term nif_uart_halfduplex_enable_tx(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + struct UARTResource *rsrc_obj; + if (UNLIKELY(!get_uart_resource(ctx, argv[0], &rsrc_obj))) { + RAISE_ERROR(BADARG_ATOM); + } + if (IS_NULL_PTR(rsrc_obj->handle.Instance)) { + RAISE_ERROR(BADARG_ATOM); + } + if (!rsrc_obj->is_half_duplex) { + return ERROR_ATOM; + } + HAL_StatusTypeDef status = HAL_HalfDuplex_EnableTransmitter(&rsrc_obj->handle); + return (status == HAL_OK) ? OK_ATOM : ERROR_ATOM; +} + +static term nif_uart_halfduplex_enable_rx(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + struct UARTResource *rsrc_obj; + if (UNLIKELY(!get_uart_resource(ctx, argv[0], &rsrc_obj))) { + RAISE_ERROR(BADARG_ATOM); + } + if (IS_NULL_PTR(rsrc_obj->handle.Instance)) { + RAISE_ERROR(BADARG_ATOM); + } + if (!rsrc_obj->is_half_duplex) { + return ERROR_ATOM; + } + HAL_StatusTypeDef status = HAL_HalfDuplex_EnableReceiver(&rsrc_obj->handle); + return (status == HAL_OK) ? OK_ATOM : ERROR_ATOM; +} + +static void uart_resource_dtor(ErlNifEnv *caller_env, void *obj) +{ + UNUSED(caller_env); + struct UARTResource *rsrc_obj = (struct UARTResource *) obj; + if (!IS_NULL_PTR(rsrc_obj->handle.Instance)) { + HAL_UART_DeInit(&rsrc_obj->handle); + rsrc_obj->handle.Instance = NULL; + } +} + +static const ErlNifResourceTypeInit UARTResourceTypeInit = { + .members = 1, + .dtor = uart_resource_dtor, +}; + +static const struct Nif uart_init_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_uart_init +}; + +static const struct Nif uart_deinit_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_uart_deinit +}; + +static const struct Nif uart_write_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_uart_write +}; + +static const struct Nif uart_read_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_uart_read +}; + +static const struct Nif uart_abort_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_uart_abort +}; + +static const struct Nif uart_get_state_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_uart_get_state +}; + +static const struct Nif uart_get_error_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_uart_get_error +}; + +static const struct Nif uart_halfduplex_init_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_uart_halfduplex_init +}; + +static const struct Nif uart_halfduplex_enable_tx_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_uart_halfduplex_enable_tx +}; + +static const struct Nif uart_halfduplex_enable_rx_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_uart_halfduplex_enable_rx +}; + +static void uart_nif_init(GlobalContext *global) +{ + ErlNifEnv env; + erl_nif_env_partial_init_from_globalcontext(&env, global); + uart_resource_type = enif_init_resource_type(&env, "uart_resource", &UARTResourceTypeInit, ERL_NIF_RT_CREATE, NULL); +} + +static const struct Nif *uart_nif_get_nif(const char *nifname) +{ + if (strncmp("uart:", nifname, 5) != 0) { + return NULL; + } + const char *rest = nifname + 5; + if (strcmp("init/1", rest) == 0) { + TRACE("Resolved uart nif %s ...\n", nifname); + return &uart_init_nif; + } + if (strcmp("deinit/1", rest) == 0) { + TRACE("Resolved uart nif %s ...\n", nifname); + return &uart_deinit_nif; + } + if (strcmp("write/3", rest) == 0) { + TRACE("Resolved uart nif %s ...\n", nifname); + return &uart_write_nif; + } + if (strcmp("read/3", rest) == 0) { + TRACE("Resolved uart nif %s ...\n", nifname); + return &uart_read_nif; + } + if (strcmp("abort/1", rest) == 0) { + TRACE("Resolved uart nif %s ...\n", nifname); + return &uart_abort_nif; + } + if (strcmp("get_state/1", rest) == 0) { + TRACE("Resolved uart nif %s ...\n", nifname); + return &uart_get_state_nif; + } + if (strcmp("get_error/1", rest) == 0) { + TRACE("Resolved uart nif %s ...\n", nifname); + return &uart_get_error_nif; + } + if (strcmp("halfduplex_init/1", rest) == 0) { + TRACE("Resolved uart nif %s ...\n", nifname); + return &uart_halfduplex_init_nif; + } + if (strcmp("halfduplex_enable_tx/1", rest) == 0) { + TRACE("Resolved uart nif %s ...\n", nifname); + return &uart_halfduplex_enable_tx_nif; + } + if (strcmp("halfduplex_enable_rx/1", rest) == 0) { + TRACE("Resolved uart nif %s ...\n", nifname); + return &uart_halfduplex_enable_rx_nif; + } + return NULL; +} + +REGISTER_NIF_COLLECTION(uart, uart_nif_init, NULL, uart_nif_get_nif) diff --git a/src/platforms/stm32/tests/renode/stm32_uart_test.robot b/src/platforms/stm32/tests/renode/stm32_uart_test.robot new file mode 100644 index 0000000000..a8ec875293 --- /dev/null +++ b/src/platforms/stm32/tests/renode/stm32_uart_test.robot @@ -0,0 +1,51 @@ +# +# This file is part of AtomVM. +# +# Copyright 2026 Paul Guyot +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later +# + +*** Settings *** +Suite Setup Setup +Suite Teardown Teardown +Test Setup Reset Emulation +Resource ${RENODEKEYWORDS} + +*** Variables *** +${UART} sysbus.usart1 +${TEST_UART} sysbus.usart2 +${PLATFORM} @platforms/cpus/stm32f4.repl +${ELF} REQUIRED +${AVM} REQUIRED +${AVM_ADDRESS} 0x08060000 + +*** Test Cases *** +AtomVM Should Communicate Over UART + Execute Command mach create + Execute Command machine LoadPlatformDescription ${PLATFORM} + Execute Command sysbus LoadELF ${ELF} + Execute Command sysbus LoadBinary ${AVM} ${AVM_ADDRESS} + Create Terminal Tester ${UART} + Start Emulation + # Wait for the Erlang test to signal it is ready to receive, then inject + # "hello" (104 101 108 108 111) into USART2 one byte at a time. + Wait For Line On Uart uart_waiting timeout=30 + Execute Command ${TEST_UART} WriteChar 104 + Execute Command ${TEST_UART} WriteChar 101 + Execute Command ${TEST_UART} WriteChar 108 + Execute Command ${TEST_UART} WriteChar 108 + Execute Command ${TEST_UART} WriteChar 111 + Wait For Line On Uart uart_done timeout=30 diff --git a/src/platforms/stm32/tests/renode/stm32h743.repl b/src/platforms/stm32/tests/renode/stm32h743.repl index c39d1d96de..3b5c47641e 100644 --- a/src/platforms/stm32/tests/renode/stm32h743.repl +++ b/src/platforms/stm32/tests/renode/stm32h743.repl @@ -40,6 +40,11 @@ usart1: UART.STM32F7_USART @ sysbus <0x40011000, +0x100> frequency: 120000000 IRQ -> nvic@37 +// USART2 for loopback testing (PA2/PA3) +usart2: UART.STM32F7_USART @ sysbus <0x40004400, +0x100> + frequency: 120000000 + IRQ -> nvic@38 + // RCC Python peripheral at 0x58024400. // H7 RCC register layout: // CR (0x00): HSION[0], HSIRDY[2], CSION[7], CSIRDY[8], diff --git a/src/platforms/stm32/tests/test_erl_sources/CMakeLists.txt b/src/platforms/stm32/tests/test_erl_sources/CMakeLists.txt index 93ecbc65b6..860e49f6e3 100644 --- a/src/platforms/stm32/tests/test_erl_sources/CMakeLists.txt +++ b/src/platforms/stm32/tests/test_erl_sources/CMakeLists.txt @@ -24,3 +24,4 @@ pack_runnable(stm32_boot_test test_boot) pack_runnable(stm32_gpio_test test_gpio eavmlib avm_stm32) pack_runnable(stm32_i2c_test test_i2c eavmlib avm_stm32) pack_runnable(stm32_spi_test test_spi eavmlib avm_stm32) +pack_runnable(stm32_uart_test test_uart eavmlib avm_stm32) diff --git a/src/platforms/stm32/tests/test_erl_sources/test_uart.erl b/src/platforms/stm32/tests/test_erl_sources/test_uart.erl new file mode 100644 index 0000000000..543287b61c --- /dev/null +++ b/src/platforms/stm32/tests/test_erl_sources/test_uart.erl @@ -0,0 +1,46 @@ +% +% This file is part of AtomVM. +% +% Copyright 2026 Paul Guyot +% +% Licensed under the Apache License, Version 2.0 (the "License"); +% you may not use this file except in compliance with the License. +% You may obtain a copy of the License at +% +% http://www.apache.org/licenses/LICENSE-2.0 +% +% Unless required by applicable law or agreed to in writing, software +% distributed under the License is distributed on an "AS IS" BASIS, +% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +% See the License for the specific language governing permissions and +% limitations under the License. +% +% SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later +% + +-module(test_uart). + +-export([start/0]). + +start() -> + erlang:display(test_uart_start), + % Initialize USART2: PA2 (TX), PA3 (RX), AF7, 115200 baud + % All test platforms use USART2 on PA2/PA3 with AF7 (AF is not checked by Renode) + {ok, UART} = uart:init([ + {peripheral, 2}, + {tx, {a, 2}}, + {rx, {a, 3}}, + {af, 7}, + {speed, 115200} + ]), + % Test TX: write "hello" and verify byte count + 5 = uart:write(UART, <<"hello">>, 5000), + ready = uart:get_state(UART), + 0 = uart:get_error(UART), + % Signal the Renode test that we are ready to receive. + % The robot will inject 5 bytes into USART2 upon seeing this. + erlang:display(uart_waiting), + % Test RX: read back the 5 bytes injected by Renode + {ok, <<"hello">>} = uart:read(UART, 5, 5000), + ok = uart:deinit(UART), + erlang:display(uart_done).