diff --git a/.github/workflows/stm32-build.yaml b/.github/workflows/stm32-build.yaml index 33488e02bd..de3c7cfb7f 100644 --- a/.github/workflows/stm32-build.yaml +++ b/.github/workflows/stm32-build.yaml @@ -64,21 +64,21 @@ jobs: # FinishTransmission() on the I2C slave when a STOP condition # occurs, causing the BME280 sensor to get stuck in Reading # state and ignore subsequent writes. - skip_i2c_test: true + tests: "boot gpio spi crypto uart" - device: stm32f411ceu6 max_size: 393216 renode_platform: stm32f4.repl avm_address: "0x08060000" - skip_i2c_test: true # No RNG peripheral on F411, so the firmware has no crypto NIFs # (mbedTLS is excluded by STM32_HAS_RNG in CMake). - skip_crypto_test: true + tests: "boot gpio spi uart" - device: stm32f429zit6 max_size: 524288 - device: stm32h743vit6 max_size: 524288 renode_platform: stm32h743.repl avm_address: "0x08080000" + tests: "boot gpio i2c spi crypto uart" - device: stm32h743zit6 max_size: 524288 - device: stm32u585ait6q @@ -91,6 +91,7 @@ jobs: max_size: 524288 renode_platform: stm32f746.repl avm_address: "0x08080000" + tests: "boot gpio i2c spi crypto uart" - device: stm32g474ret6 max_size: 393216 - device: stm32l476rgt6 @@ -102,10 +103,9 @@ jobs: # Renode's built-in stm32l552.repl uses STM32F4_I2C (legacy I2C # register layout) but the L5 HAL uses the newer I2C registers # (TIMINGR, ISR, etc.), causing a complete register mismatch. - skip_i2c_test: true # 512 KB flash with avm_address=0x08060000 leaves only 128 KB, # but the crypto AVM is 207 KB and gets truncated. - skip_crypto_test: true + tests: "boot gpio spi uart" - device: stm32f207zgt6 max_size: 524288 - device: stm32u375rgt6 @@ -117,7 +117,7 @@ jobs: # No RNG peripheral on G0B1 (only G041/G061/G081/G0C1 have one), # so the firmware has no crypto NIFs (mbedTLS is excluded by # STM32_HAS_RNG in CMake). - skip_crypto_test: true + tests: "boot gpio i2c spi uart" steps: - uses: erlef/setup-beam@v1 @@ -195,7 +195,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 stm32_crypto_test + cmake --build . -t stm32_boot_test stm32_gpio_test stm32_i2c_test stm32_spi_test stm32_crypto_test stm32_uart_test - name: Install Renode if: matrix.renode_platform @@ -207,7 +207,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 }}" @@ -216,71 +216,10 @@ 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" - else - PLATFORM="@platforms/cpus/${{ matrix.renode_platform }}" - 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 - - - name: Run Renode crypto test - # Devices without RNG hardware (F411 / G0) don't ship mbedTLS at all; - # L562 has only 512 KB of flash and the 207 KB crypto AVM is truncated - # at AVM_ADDRESS=0x08060000, which kills kernel boot. - if: matrix.renode_platform && !matrix.skip_crypto_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_crypto_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_crypto_test.avm \ - --variable AVM_ADDRESS:${{ matrix.avm_address }} \ - --variable PLATFORM:$PLATFORM + for TEST in ${{ matrix.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 36d08f92d4..b227f981ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,7 @@ 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 `proc_lib:init_fail/2,3` - Added UART API to rp2 platform diff --git a/examples/erlang/CMakeLists.txt b/examples/erlang/CMakeLists.txt index 882d07463b..2c10ab5a02 100644 --- a/examples/erlang/CMakeLists.txt +++ b/examples/erlang/CMakeLists.txt @@ -48,4 +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) +pack_runnable(sim800l sim800l eavmlib estdlib DIALYZE_AGAINST avm_esp32 avm_rp2 avm_stm32) diff --git a/examples/erlang/sim800l.erl b/examples/erlang/sim800l.erl index 7c0b1eb303..427c96b36a 100644 --- a/examples/erlang/sim800l.erl +++ b/examples/erlang/sim800l.erl @@ -34,6 +34,7 @@ %% 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 @@ -46,13 +47,8 @@ -define(AT_TIMEOUT, 2000). start() -> - {TX, RX} = default_pins(), - io:format("Opening UART1 on TX=~B RX=~B~n", [TX, RX]), - UART = uart:open("UART1", [ - {tx, TX}, - {rx, RX}, - {speed, 115200} - ]), + 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 -> @@ -156,13 +152,33 @@ drain(UART) -> end. %%----------------------------------------------------------------------------- -%% Platform-specific default pins +%% Platform-specific UART operations %%----------------------------------------------------------------------------- -default_pins() -> - default_pins(atomvm:platform()). -%% {TX, RX} +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() -> 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_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..980129db26 --- /dev/null +++ b/libs/avm_stm32/src/uart.erl @@ -0,0 +1,396 @@ +% +% 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 peripheral_name() :: string() | binary(). +-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: +%% +%% @end +%%----------------------------------------------------------------------------- +-spec open(Opts :: [{atom(), term()}]) -> uart(). +open(Opts) -> + Config = parse_opts(Opts), + case ?MODULE:init(Config) of + {ok, Resource} -> + spawn_link(fun() -> loop(Resource) end); + {error, Reason} -> + error(Reason) + end. + +%%----------------------------------------------------------------------------- +%% @param Name UART peripheral name +%% @param Opts UART configuration options +%% @returns UART handle (pid) +%% @doc Open a UART connection with the given options. +%% @end +%%----------------------------------------------------------------------------- +-spec open(Name :: peripheral_name(), Opts :: [{atom(), term()}]) -> uart(). +open(Name, Opts) -> + open([{peripheral, Name} | 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 currently available data from the UART. +%% @end +%%----------------------------------------------------------------------------- +-spec read(UART :: uart()) -> {ok, binary()} | {error, term()}. +read(Pid) when is_pid(Pid) -> + call(Pid, read). + +%%----------------------------------------------------------------------------- +%% @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: +%% +%% @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) -> + case ?MODULE:read(Resource, 1, 0) of + {ok, Byte} -> + {reply, {ok, read_available(Resource, [Byte])}}; + {error, _} = Error -> + {reply, Error} + end; +handle_request(Resource, {read, Timeout}) -> + case ?MODULE:read(Resource, 1, Timeout) of + {ok, Byte} -> + {reply, {ok, read_available(Resource, [Byte])}}; + {error, _} = Error -> + {reply, Error} + end; +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. + +read_available(Resource, Acc) -> + case ?MODULE:read(Resource, 1, 0) of + {ok, Byte} -> + read_available(Resource, [Byte | Acc]); + {error, timeout} -> + erlang:iolist_to_binary(lists:reverse(Acc)); + {error, _} -> + erlang:iolist_to_binary(lists:reverse(Acc)) + 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 = normalize_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. + +normalize_peripheral(N) when is_integer(N) -> N; +normalize_peripheral([$U, $A, $R, $T | N]) -> list_to_integer(N); +normalize_peripheral(<<"UART", N/binary>>) -> binary_to_integer(N). diff --git a/libs/eavmlib/src/uart_hal.erl b/libs/eavmlib/src/uart_hal.erl index 35bc72f50f..5078af001d 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. %% -%% ESP32 and RP2 platforms provide UART implementations. +%% ESP32, RP2, STM32 and generic unix platforms provide UART implementations. %% %%

Lifecycle

%% diff --git a/src/platforms/stm32/src/lib/CMakeLists.txt b/src/platforms/stm32/src/lib/CMakeLists.txt index 98cf713f85..b9830c60b9 100644 --- a/src/platforms/stm32/src/lib/CMakeLists.txt +++ b/src/platforms/stm32/src/lib/CMakeLists.txt @@ -55,6 +55,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 6e2d7d2238..05f1ba2d7c 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 fd2ac4f920..92d2593e41 100644 --- a/src/platforms/stm32/tests/test_erl_sources/CMakeLists.txt +++ b/src/platforms/stm32/tests/test_erl_sources/CMakeLists.txt @@ -25,3 +25,4 @@ 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_crypto_test test_crypto estdlib) +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).