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
17 changes: 16 additions & 1 deletion .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

- name: Install Renode
if: matrix.renode_platform
Expand Down Expand Up @@ -241,3 +241,18 @@ jobs:
--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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.7.0] - Unreleased

### Added
- Added I2C and SPI APIs to rp2 platform
- Added I2C and SPI APIs to stm32 platform

## [0.7.0-alpha.0] - 2026-03-20

### Added
Expand Down
2 changes: 2 additions & 0 deletions examples/erlang/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,5 @@ pack_runnable(http_client http_client estdlib eavmlib avm_network)
pack_runnable(disterl disterl estdlib)
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 avm_stm32)
pack_runnable(spi_lis3dh spi_lis3dh eavmlib estdlib DIALYZE_AGAINST avm_esp32 avm_rp2 avm_stm32)
22 changes: 22 additions & 0 deletions examples/erlang/rp2/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,28 @@ add_custom_command(
add_custom_target(i2c_lis3dh_uf2 ALL DEPENDS i2c_lis3dh.uf2)
add_dependencies(i2c_lis3dh_uf2 i2c_lis3dh)

set(SPI_FLASH_AVM ${CMAKE_BINARY_DIR}/examples/erlang/spi_flash.avm)
add_custom_command(
OUTPUT spi_flash.uf2
DEPENDS ${SPI_FLASH_AVM} UF2Tool
COMMAND ${CMAKE_BINARY_DIR}/tools/uf2tool/uf2tool create -o spi_flash.uf2 -f universal -s 0x10180000 ${SPI_FLASH_AVM}
COMMENT "Creating UF2 file spi_flash.uf2"
VERBATIM
)
add_custom_target(spi_flash_uf2 ALL DEPENDS spi_flash.uf2)
add_dependencies(spi_flash_uf2 spi_flash)

set(SPI_LIS3DH_AVM ${CMAKE_BINARY_DIR}/examples/erlang/spi_lis3dh.avm)
add_custom_command(
OUTPUT spi_lis3dh.uf2
DEPENDS ${SPI_LIS3DH_AVM} UF2Tool
COMMAND ${CMAKE_BINARY_DIR}/tools/uf2tool/uf2tool create -o spi_lis3dh.uf2 -f universal -s 0x10180000 ${SPI_LIS3DH_AVM}
COMMENT "Creating UF2 file spi_lis3dh.uf2"
VERBATIM
)
add_custom_target(spi_lis3dh_uf2 ALL DEPENDS spi_lis3dh.uf2)
add_dependencies(spi_lis3dh_uf2 spi_lis3dh)

pack_uf2(picow_blink picow_blink)
pack_uf2(picow_wifi_sta picow_wifi_sta)
pack_uf2(picow_wifi_ap picow_wifi_ap)
Expand Down
137 changes: 137 additions & 0 deletions examples/erlang/spi_flash.erl
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
%
% 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 SPI flash JEDEC ID reader example.
%%
%% Reads the JEDEC ID and Status Register 1 from a standard SPI flash chip
%% (W25Qxx or similar) and prints the results every 5 seconds.
%%
%% 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
%%
%% Note: some breakout boards label pins using QSPI convention where
%% D0 is MISO and D1 is MOSI (opposite of DI/DO). If you read all
%% zeros, try swapping D0 and D1.
%%
%% The flash command byte is sent as the "address" parameter of read_at/4,
%% using address_len_bits => 8 and command_len_bits => 0.
%% @end
%%-----------------------------------------------------------------------------
-module(spi_flash).
-export([start/0]).

%% SPI flash commands
-define(CMD_JEDEC_ID, 16#9F).
-define(CMD_READ_STATUS1, 16#05).

start() ->
{SCK, MOSI, MISO, CS} = default_pins(),
SPI = spi:open([
{bus_config, [
{sclk, SCK},
{mosi, MOSI},
{miso, MISO}
]},
{device_config, [
{flash, [
{cs, CS},
{clock_speed_hz, 1000000},
{mode, 0},
{address_len_bits, 8},
{command_len_bits, 0}
]}
]}
]),
loop(SPI).

loop(SPI) ->
read_jedec_id(SPI),
read_status(SPI),
timer:sleep(5000),
loop(SPI).

read_jedec_id(SPI) ->
case spi:read_at(SPI, flash, ?CMD_JEDEC_ID, 24) of
{ok, Value} ->
Manufacturer = (Value bsr 16) band 16#FF,
MemType = (Value bsr 8) band 16#FF,
Capacity = Value band 16#FF,
io:format(
"JEDEC ID: manufacturer=0x~2.16.0B mem_type=0x~2.16.0B capacity=0x~2.16.0B~n", [
Manufacturer, MemType, Capacity
]
),
case manufacturer_name(Manufacturer) of
unknown -> ok;
Name -> io:format(" Manufacturer: ~s~n", [Name])
end;
{error, Reason} ->
io:format("JEDEC ID read error: ~p~n", [Reason])
end.

read_status(SPI) ->
case spi:read_at(SPI, flash, ?CMD_READ_STATUS1, 8) of
{ok, Status} ->
Busy = Status band 1,
Wel = (Status bsr 1) band 1,
io:format("Status Register 1: 0x~2.16.0B (BUSY=~B WEL=~B)~n", [
Status, Busy, Wel
]);
{error, Reason} ->
io:format("Status read error: ~p~n", [Reason])
end.

default_pins() ->
default_pins(atomvm:platform()).

%% {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() ->
#{model := Model} = erlang:system_info(esp32_chip_info),
esp32_default_pins(Model).

%% {SCK, MOSI, MISO, CS}
esp32_default_pins(esp32) -> {18, 23, 19, 5};
esp32_default_pins(esp32_s2) -> {18, 23, 19, 5};
esp32_default_pins(esp32_s3) -> {18, 23, 19, 5};
esp32_default_pins(esp32_c2) -> {6, 7, 2, 10};
esp32_default_pins(esp32_c3) -> {6, 7, 2, 10};
esp32_default_pins(esp32_c5) -> {6, 7, 2, 10};
esp32_default_pins(esp32_c6) -> {6, 7, 2, 16};
esp32_default_pins(esp32_c61) -> {6, 7, 2, 16};
esp32_default_pins(_) -> {18, 23, 19, 5}.

manufacturer_name(16#EF) -> "Winbond";
manufacturer_name(16#C8) -> "GigaDevice";
manufacturer_name(16#20) -> "Micron";
manufacturer_name(16#01) -> "Spansion/Cypress";
manufacturer_name(16#1F) -> "Adesto/Atmel";
manufacturer_name(16#BF) -> "Microchip/SST";
manufacturer_name(16#9D) -> "ISSI";
manufacturer_name(_) -> unknown.
154 changes: 154 additions & 0 deletions examples/erlang/spi_lis3dh.erl
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
%
% 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 LIS3DH accelerometer SPI example.
%%
%% Reads X, Y, Z acceleration from a LIS3DH connected via SPI and prints
%% the values every second.
%%
%% 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
%%
%% LIS3DH SPI protocol: bit 7 = R/W (1=read, 0=write),
%% bit 6 = MS (1=auto-increment).
%% @end
%%-----------------------------------------------------------------------------
-module(spi_lis3dh).
-export([start/0]).

%% LIS3DH registers
-define(WHO_AM_I, 16#0F).
-define(CTRL_REG1, 16#20).
-define(CTRL_REG4, 16#23).
-define(OUT_X_L, 16#28).

%% Expected WHO_AM_I response
-define(LIS3DH_ID, 16#33).

%% SPI address bits: bit 7 = read, bit 6 = auto-increment
-define(READ_BIT, 16#80).
-define(MS_BIT, 16#40).

start() ->
{SCK, MOSI, MISO, CS} = default_pins(),
SPI = spi:open([
{bus_config, [
{sclk, SCK},
{mosi, MOSI},
{miso, MISO}
]},
{device_config, [
{lis3dh, [
{cs, CS},
{clock_speed_hz, 1000000},
{mode, 0},
{address_len_bits, 8},
{command_len_bits, 0}
]}
]}
]),
case check_who_am_i(SPI) of
ok ->
configure(SPI),
loop(SPI);
{error, Reason} ->
io:format("LIS3DH not found: ~p~n", [Reason])
end.

check_who_am_i(SPI) ->
case spi:read_at(SPI, lis3dh, ?READ_BIT bor ?WHO_AM_I, 8) of
{ok, ?LIS3DH_ID} ->
io:format("LIS3DH detected (SPI)~n"),
ok;
{ok, Other} ->
io:format("Unexpected WHO_AM_I: ~.16B~n", [Other]),
{error, unexpected_id};
{error, _} = Error ->
Error
end.

configure(SPI) ->
%% CTRL_REG1: 50 Hz ODR, normal mode, X/Y/Z enabled
%% Bits: ODR=0100 LPen=0 Zen=1 Yen=1 Xen=1 -> 0x47
ok = spi:write(SPI, lis3dh, #{address => ?CTRL_REG1, write_data => <<16#47:8>>}),
%% CTRL_REG4: +/- 2g full scale, high resolution
%% Bits: BDU=1 BLE=0 FS=00 HR=1 ST=00 SIM=0 -> 0x88
ok = spi:write(SPI, lis3dh, #{address => ?CTRL_REG4, write_data => <<16#88:8>>}).

loop(SPI) ->
case read_acceleration(SPI) of
{ok, {X, Y, Z}} ->
io:format("X=~B Y=~B Z=~B~n", [X, Y, Z]);
{error, Reason} ->
io:format("Read error: ~p~n", [Reason])
end,
timer:sleep(1000),
loop(SPI).

read_acceleration(SPI) ->
%% Read 6 bytes starting at OUT_X_L with auto-increment (MS bit set)
case spi:read_at(SPI, lis3dh, ?READ_BIT bor ?MS_BIT bor ?OUT_X_L, 48) of
{ok, Value} ->
XL = (Value bsr 40) band 16#FF,
XH = (Value bsr 32) band 16#FF,
YL = (Value bsr 24) band 16#FF,
YH = (Value bsr 16) band 16#FF,
ZL = (Value bsr 8) band 16#FF,
ZH = Value band 16#FF,
%% 12-bit left-justified in high-resolution mode: shift right by 4
X = sign_extend_12(((XH bsl 8) bor XL) bsr 4),
Y = sign_extend_12(((YH bsl 8) bor YL) bsr 4),
Z = sign_extend_12(((ZH bsl 8) bor ZL) bsr 4),
{ok, {X, Y, Z}};
{error, _} = Error ->
Error
end.

sign_extend_12(V) when V >= 16#800 -> V - 16#1000;
sign_extend_12(V) -> V.

default_pins() ->
default_pins(atomvm:platform()).

%% {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() ->
#{model := Model} = erlang:system_info(esp32_chip_info),
esp32_default_pins(Model).

%% {SCK, MOSI, MISO, CS}
esp32_default_pins(esp32) -> {18, 23, 19, 5};
esp32_default_pins(esp32_s2) -> {18, 23, 19, 5};
esp32_default_pins(esp32_s3) -> {18, 23, 19, 5};
esp32_default_pins(esp32_c2) -> {6, 7, 2, 10};
esp32_default_pins(esp32_c3) -> {6, 7, 2, 10};
esp32_default_pins(esp32_c5) -> {6, 7, 2, 10};
esp32_default_pins(esp32_c6) -> {6, 7, 2, 16};
esp32_default_pins(esp32_c61) -> {6, 7, 2, 16};
esp32_default_pins(_) -> {18, 23, 19, 5}.
Loading
Loading