Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 12 additions & 35 deletions .github/workflows/stm32-build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
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
Expand All @@ -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 }}"
Expand All @@ -206,38 +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"
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_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
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added Erlang distribution over serial (uart)
- Added WASM32 JIT backend for Emscripten platform
- Added `network:wifi_scan/0,1` to ESP32 network driver to scan available APs when in sta or sta+ap mode.
- Added UART API to rp2 platform
- Added I2C, SPI and UART APIs to stm32 platform

### Changed
- Updated network type db() to dbm() to reflect the actual representation of the type
Expand Down
5 changes: 3 additions & 2 deletions examples/erlang/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -46,5 +46,6 @@ pack_runnable(disterl disterl estdlib)
pack_runnable(serial_disterl serial_disterl eavmlib estdlib DIALYZE_AGAINST avm_esp32 avm_unix)
pack_runnable(i2c_scanner i2c_scanner eavmlib estdlib DIALYZE_AGAINST avm_esp32 avm_rp2 avm_stm32)
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)
pack_runnable(spi_lis3dh spi_lis3dh eavmlib estdlib DIALYZE_AGAINST avm_esp32 avm_rp2)
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)
10 changes: 10 additions & 0 deletions examples/erlang/rp2/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
197 changes: 197 additions & 0 deletions examples/erlang/sim800l.erl
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
%
% This file is part of AtomVM.
%
% Copyright 2026 Paul Guyot <pguyot@kallisys.net>
%
% 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}.
2 changes: 2 additions & 0 deletions examples/erlang/spi_flash.erl
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
%% Default pins are auto-detected from the platform and chip model:
%%
%% Pico (SPI0): SCK=GP2, MOSI=GP3, MISO=GP4, CS=GP5
%% STM32 (SPI1): SCK=PA5, MOSI=PA7, MISO=PA6, CS=PA4
%% ESP32/S2/S3 (SPI2): SCK=18, MOSI=23, MISO=19, CS=5
%% ESP32-C2/C3/C5 (SPI2): SCK=6, MOSI=7, MISO=2, CS=10
%% ESP32-C6/C61 (SPI2): SCK=6, MOSI=7, MISO=2, CS=16
Expand Down Expand Up @@ -108,6 +109,7 @@ default_pins() ->

%% {SCK, MOSI, MISO, CS}
default_pins(pico) -> {2, 3, 4, 5};
default_pins(stm32) -> {{a, 5}, {a, 7}, {a, 6}, {a, 4}};
default_pins(esp32) -> esp32_default_pins().

esp32_default_pins() ->
Expand Down
2 changes: 2 additions & 0 deletions examples/erlang/spi_lis3dh.erl
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
%% Default pins are auto-detected from the platform and chip model:
%%
%% Pico (SPI0): SCK=GP2, MOSI=GP3, MISO=GP4, CS=GP5
%% STM32 (SPI1): SCK=PA5, MOSI=PA7, MISO=PA6, CS=PA4
%% ESP32/S2/S3 (SPI2): SCK=18, MOSI=23, MISO=19, CS=5
%% ESP32-C2/C3/C5 (SPI2): SCK=6, MOSI=7, MISO=2, CS=10
%% ESP32-C6/C61 (SPI2): SCK=6, MOSI=7, MISO=2, CS=16
Expand Down Expand Up @@ -134,6 +135,7 @@ default_pins() ->

%% {SCK, MOSI, MISO, CS}
default_pins(pico) -> {2, 3, 4, 5};
default_pins(stm32) -> {{a, 5}, {a, 7}, {a, 6}, {a, 4}};
default_pins(esp32) -> esp32_default_pins().

esp32_default_pins() ->
Expand Down
54 changes: 54 additions & 0 deletions examples/erlang/stm32/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,57 @@ pack_runnable(blink_weact_studio_u585 blink_weact_studio_u585 eavmlib avm_stm32)
pack_runnable(blink_weact_studio_wb55 blink_weact_studio_wb55 eavmlib avm_stm32)
pack_runnable(blink_nucleo64 blink_nucleo64 eavmlib avm_stm32)
pack_runnable(blink_nucleo144 blink_nucleo144 eavmlib avm_stm32)

if(AVM_RELEASE)
set(INCLUDE_LINES "--remove_lines")
else()
set(INCLUDE_LINES "")
endif()

set(SPI_LIS3DH_BEAM ${CMAKE_BINARY_DIR}/examples/erlang/spi_lis3dh.beam)
add_custom_command(
OUTPUT stm32_spi_lis3dh.avm
DEPENDS spi_lis3dh_main ${SPI_LIS3DH_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_spi_lis3dh.avm
${SPI_LIS3DH_BEAM}
${CMAKE_BINARY_DIR}/libs/estdlib/src/estdlib.avm
${CMAKE_BINARY_DIR}/libs/avm_stm32/src/avm_stm32.avm
COMMENT "Packing runnable stm32_spi_lis3dh.avm"
VERBATIM
)
add_custom_target(stm32_spi_lis3dh ALL DEPENDS stm32_spi_lis3dh.avm)

set(SPI_FLASH_BEAM ${CMAKE_BINARY_DIR}/examples/erlang/spi_flash.beam)
add_custom_command(
OUTPUT stm32_spi_flash.avm
DEPENDS spi_flash_main ${SPI_FLASH_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_spi_flash.avm
${SPI_FLASH_BEAM}
${CMAKE_BINARY_DIR}/libs/estdlib/src/estdlib.avm
${CMAKE_BINARY_DIR}/libs/avm_stm32/src/avm_stm32.avm
COMMENT "Packing runnable stm32_spi_flash.avm"
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)
1 change: 1 addition & 0 deletions libs/avm_rp2/src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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})
Expand Down
Loading
Loading