diff --git a/.github/workflows/stm32-build.yaml b/.github/workflows/stm32-build.yaml index 894acd7715..3e51200e6a 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 + cmake --build . -t stm32_boot_test stm32_gpio_test stm32_i2c_test stm32_spi_test - name: Install Renode if: matrix.renode_platform @@ -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 diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a48932f8e..2f77d7b3f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/examples/erlang/CMakeLists.txt b/examples/erlang/CMakeLists.txt index 4c586f7106..509ea2a3cb 100644 --- a/examples/erlang/CMakeLists.txt +++ b/examples/erlang/CMakeLists.txt @@ -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) diff --git a/examples/erlang/rp2/CMakeLists.txt b/examples/erlang/rp2/CMakeLists.txt index fb6500b028..5a81d9d41f 100644 --- a/examples/erlang/rp2/CMakeLists.txt +++ b/examples/erlang/rp2/CMakeLists.txt @@ -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) diff --git a/examples/erlang/spi_flash.erl b/examples/erlang/spi_flash.erl new file mode 100644 index 0000000000..873a744b9c --- /dev/null +++ b/examples/erlang/spi_flash.erl @@ -0,0 +1,137 @@ +% +% 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 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. diff --git a/examples/erlang/spi_lis3dh.erl b/examples/erlang/spi_lis3dh.erl new file mode 100644 index 0000000000..a115f08c6e --- /dev/null +++ b/examples/erlang/spi_lis3dh.erl @@ -0,0 +1,154 @@ +% +% 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 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}. diff --git a/examples/erlang/stm32/CMakeLists.txt b/examples/erlang/stm32/CMakeLists.txt index 0cc91e19c8..5ddcf3f83c 100644 --- a/examples/erlang/stm32/CMakeLists.txt +++ b/examples/erlang/stm32/CMakeLists.txt @@ -32,3 +32,41 @@ 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) diff --git a/libs/avm_rp2/src/CMakeLists.txt b/libs/avm_rp2/src/CMakeLists.txt index ec5dff9098..c76d8b1675 100644 --- a/libs/avm_rp2/src/CMakeLists.txt +++ b/libs/avm_rp2/src/CMakeLists.txt @@ -26,6 +26,7 @@ set(ERLANG_MODULES gpio i2c pico + spi ) pack_archive(avm_rp2 DEPENDS_ON eavmlib ERLC_FLAGS +warnings_as_errors MODULES ${ERLANG_MODULES}) diff --git a/libs/avm_rp2/src/spi.erl b/libs/avm_rp2/src/spi.erl new file mode 100644 index 0000000000..155d3bd9ef --- /dev/null +++ b/libs/avm_rp2/src/spi.erl @@ -0,0 +1,638 @@ +% +% 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 SPI interface for RP2 (Pico) +%% +%% This module provides an interface to the SPI hardware on RP2 platforms. +%% +%% Two API levels are provided: +%% +%% Low-level API +%% {@link init/2}, {@link deinit/1}, {@link set_baudrate/2}, +%% {@link get_baudrate/1}, {@link set_format/4}, {@link set_slave/2}, +%% {@link is_writable/1}, {@link is_readable/1}, {@link is_busy/1}, +%% {@link write_blocking/2}, {@link read_blocking/3}, +%% {@link write_read_blocking/2}, {@link write16_blocking/2}, +%% {@link read16_blocking/3}, {@link write_read16_blocking/2}. +%% 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 (`spi_hal' behavior) +%% {@link open/1}, {@link close/1}, {@link read_at/4}, {@link write_at/5}, +%% {@link write/3}, {@link write_read/3}. +%% {@link open/1} handles pin setup and CS management automatically. +%% @end +%%----------------------------------------------------------------------------- +-module(spi). + +-behaviour(spi_hal). + +%% High-level API (spi_hal behaviour) +-export([ + open/1, + close/1, + read_at/4, + write_at/5, + write/3, + write_read/3 +]). + +%% Low-level API (Pico SDK) +-export([ + init/2, + deinit/1, + set_baudrate/2, + set_format/4, + set_slave/2, + is_writable/1, + is_readable/1, + is_busy/1, + get_baudrate/1, + write_blocking/2, + read_blocking/3, + write_read_blocking/2, + write16_blocking/2, + read16_blocking/3, + write_read16_blocking/2 +]). + +-type freq_hz() :: non_neg_integer(). +-type peripheral() :: 0 | 1. +-type spi_resource() :: reference(). +-type spi() :: pid(). +-type device_name() :: atom(). +-type address() :: non_neg_integer(). +-type transaction() :: #{ + command => integer(), + address => non_neg_integer(), + write_data => binary(), + write_bits => non_neg_integer(), + read_bits => non_neg_integer() +}. + +-export_type([spi/0, spi_resource/0, device_name/0, address/0, transaction/0]). + +-define(DEFAULT_CLOCK_SPEED_HZ, 1000000). +-define(DEFAULT_MODE, 0). +-define(DEFAULT_ADDRESS_LEN_BITS, 8). +-define(DEFAULT_COMMAND_LEN_BITS, 0). +-define(DEFAULT_PERIPHERAL, 0). + +%% --------------------------------------------------------------------------- +%% High-level API (spi_hal behaviour) +%% --------------------------------------------------------------------------- + +%%----------------------------------------------------------------------------- +%% @param Params Initialization parameters +%% @returns SPI handle (pid) +%% @doc Open a connection to the SPI driver +%% +%% This function configures the GPIO pins for SPI function, +%% initializes the SPI peripheral, and sets up CS pins for +%% software chip-select management. +%% +%% Parameters use the same format as the ESP32 SPI driver: +%%
    +%%
  • `{bus_config, BusConfig}' - bus configuration (required)
  • +%%
  • `{device_config, DeviceConfigs}' - device configurations
  • +%%
+%% +%% Bus configuration: +%%
    +%%
  • `{sclk, Pin}' - the SCLK pin number (required)
  • +%%
  • `{mosi, Pin}' - the MOSI pin number
  • +%%
  • `{miso, Pin}' - the MISO pin number
  • +%%
  • `{peripheral, 0 | 1}' - the SPI peripheral to use (default: 0)
  • +%%
+%% +%% Device configuration (keyed by device name atom): +%%
    +%%
  • `{cs, Pin}' - the CS pin number
  • +%%
  • `{clock_speed_hz, Hz}' - clock speed (default: 1000000)
  • +%%
  • `{mode, 0..3}' - SPI mode (default: 0)
  • +%%
  • `{address_len_bits, Bits}' - address width (default: 8)
  • +%%
  • `{command_len_bits, Bits}' - command width (default: 0)
  • +%%
+%% @end +%%----------------------------------------------------------------------------- +-spec open(Params :: [{atom(), term()}]) -> spi() | {error, term()}. +open(Params) -> + case get_value(bus_config, Params) of + undefined -> + {error, {missing_param, bus_config}}; + BusConfig -> + case get_value(sclk, BusConfig) of + undefined -> + {error, {missing_param, sclk}}; + SCLK -> + DeviceConfigList = get_value(device_config, Params, []), + MOSI = get_value(mosi, BusConfig, undefined), + MISO = get_value(miso, BusConfig, undefined), + Peripheral = get_value(peripheral, BusConfig, ?DEFAULT_PERIPHERAL), + gpio:set_function(SCLK, spi), + maybe_set_spi_function(MOSI), + maybe_set_spi_function(MISO), + case ?MODULE:init(Peripheral, ?DEFAULT_CLOCK_SPEED_HZ) of + {ok, {_ActualBaud, Resource}} -> + Devices = setup_devices(DeviceConfigList), + spawn_link(fun() -> loop(Resource, Devices) end); + {error, _} = Error -> + Error + end + end + end. + +%%----------------------------------------------------------------------------- +%% @param SPI SPI handle created via `open/1' +%% @returns `ok' +%% @doc Close the SPI driver and free resources. +%% @end +%%----------------------------------------------------------------------------- +-spec close(SPI :: spi()) -> ok. +close(Pid) -> + call(Pid, close). + +%%----------------------------------------------------------------------------- +%% @param SPI SPI handle created via `open/1' +%% @param DeviceName device name from configuration +%% @param Address SPI address from which to read +%% @param Len number of bits to read +%% @returns `{ok, Value}' or `{error, Reason}' +%% @doc Read a value from an address on the device. +%% +%% Sends the address (address_len_bits wide), then reads Len bits. +%% The read value is returned as a big-endian integer. +%% @end +%%----------------------------------------------------------------------------- +-spec read_at( + SPI :: spi(), DeviceName :: device_name(), Address :: address(), Len :: non_neg_integer() +) -> + {ok, integer()} | {error, term()}. +read_at(Pid, DeviceName, Address, Len) -> + call(Pid, {read_at, DeviceName, Address, Len}). + +%%----------------------------------------------------------------------------- +%% @param SPI SPI handle created via `open/1' +%% @param DeviceName device name from configuration +%% @param Address SPI address to which to write +%% @param Len number of bits to transfer +%% @param Data value to write +%% @returns `{ok, Value}' or `{error, Reason}' +%% @doc Write a value to an address on the device. +%% +%% Sends the address with bit 7 set (write flag), followed by the +%% data. Returns the value read back during the data phase. +%% @end +%%----------------------------------------------------------------------------- +-spec write_at( + SPI :: spi(), + DeviceName :: device_name(), + Address :: address(), + Len :: non_neg_integer(), + Data :: integer() +) -> + {ok, integer()} | {error, term()}. +write_at(Pid, DeviceName, Address, Len, Data) -> + call(Pid, {write_at, DeviceName, Address bor 16#80, Len, Data}). + +%%----------------------------------------------------------------------------- +%% @param SPI SPI handle created via `open/1' +%% @param DeviceName device name from configuration +%% @param Transaction transaction map +%% @returns `ok' or `{error, Reason}' +%% @doc Write data to the SPI device using a transaction. +%% +%% The transaction map may contain: `command', `address', +%% `write_data', and `write_bits'. +%% @end +%%----------------------------------------------------------------------------- +-spec write(SPI :: spi(), DeviceName :: device_name(), Transaction :: transaction()) -> + ok | {error, term()}. +write(Pid, DeviceName, Transaction) -> + call(Pid, {write, DeviceName, Transaction}). + +%%----------------------------------------------------------------------------- +%% @param SPI SPI handle created via `open/1' +%% @param DeviceName device name from configuration +%% @param Transaction transaction map +%% @returns `{ok, ReadData}' or `{error, Reason}' +%% @doc Write and simultaneously read from the SPI device. +%% +%% The transaction map may contain: `command', `address', +%% `write_data', `write_bits', and `read_bits'. +%% Returns the first `ceil(read_bits / 8)' bytes read during the +%% data phase. +%% @end +%%----------------------------------------------------------------------------- +-spec write_read(SPI :: spi(), DeviceName :: device_name(), Transaction :: transaction()) -> + {ok, binary()} | {error, term()}. +write_read(Pid, DeviceName, Transaction) -> + call(Pid, {write_read, DeviceName, Transaction}). + +%% --------------------------------------------------------------------------- +%% Low-level API (Pico SDK) +%% --------------------------------------------------------------------------- + +%%----------------------------------------------------------------------------- +%% @param Peripheral SPI peripheral number (0 or 1) +%% @param Baudrate Baudrate in Hz +%% @returns `{ok, {ActualBaudrate, Resource}}' +%% @doc Initialize the SPI 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 :: spi_resource()}} | {error, busy}. +init(_Peripheral, _Baudrate) -> + erlang:nif_error(undefined). + +%%----------------------------------------------------------------------------- +%% @param Resource SPI resource returned by `init/2' +%% @returns `ok' +%% @doc Disable the SPI HW block. +%% @end +%%----------------------------------------------------------------------------- +-spec deinit(Resource :: spi_resource()) -> ok. +deinit(_Resource) -> + erlang:nif_error(undefined). + +%%----------------------------------------------------------------------------- +%% @param Resource SPI resource returned by `init/2' +%% @param Baudrate Baudrate in Hz +%% @returns `{ok, ActualBaudrate}' +%% @doc Set SPI baudrate. +%% @end +%%----------------------------------------------------------------------------- +-spec set_baudrate(Resource :: spi_resource(), Baudrate :: freq_hz()) -> + {ok, ActualBaudrate :: freq_hz()}. +set_baudrate(_Resource, _Baudrate) -> + erlang:nif_error(undefined). + +%%----------------------------------------------------------------------------- +%% @param Resource SPI resource returned by `init/2' +%% @param DataBits Number of data bits per transfer (4..16) +%% @param CPOL Clock polarity (0 or 1) +%% @param CPHA Clock phase (0 or 1) +%% @returns `ok' +%% @doc Set SPI format. +%% +%% SPI mode mapping: mode 0 = CPOL 0, CPHA 0; mode 1 = CPOL 0, +%% CPHA 1; mode 2 = CPOL 1, CPHA 0; mode 3 = CPOL 1, CPHA 1. +%% Data order is always MSB first. +%% @end +%%----------------------------------------------------------------------------- +-spec set_format(Resource :: spi_resource(), DataBits :: 4..16, CPOL :: 0 | 1, CPHA :: 0 | 1) -> ok. +set_format(_Resource, _DataBits, _CPOL, _CPHA) -> + erlang:nif_error(undefined). + +%%----------------------------------------------------------------------------- +%% @param Resource SPI resource returned by `init/2' +%% @param Slave `true' to enable slave mode, `false' for master mode +%% @returns `ok' +%% @doc Set SPI to slave or master mode. +%% @end +%%----------------------------------------------------------------------------- +-spec set_slave(Resource :: spi_resource(), Slave :: boolean()) -> ok. +set_slave(_Resource, _Slave) -> + erlang:nif_error(undefined). + +%%----------------------------------------------------------------------------- +%% @param Resource SPI resource returned by `init/2' +%% @returns `true' if the TX FIFO is not full, `false' otherwise +%% @doc Check whether data can be written to the TX FIFO. +%% @end +%%----------------------------------------------------------------------------- +-spec is_writable(Resource :: spi_resource()) -> boolean(). +is_writable(_Resource) -> + erlang:nif_error(undefined). + +%%----------------------------------------------------------------------------- +%% @param Resource SPI resource returned by `init/2' +%% @returns `true' if the RX FIFO is not empty, `false' otherwise +%% @doc Check whether data is available in the RX FIFO. +%% @end +%%----------------------------------------------------------------------------- +-spec is_readable(Resource :: spi_resource()) -> boolean(). +is_readable(_Resource) -> + erlang:nif_error(undefined). + +%%----------------------------------------------------------------------------- +%% @param Resource SPI resource returned by `init/2' +%% @returns `true' if the SPI peripheral is busy, `false' otherwise +%% @doc Check whether the SPI peripheral is busy. +%% @end +%%----------------------------------------------------------------------------- +-spec is_busy(Resource :: spi_resource()) -> boolean(). +is_busy(_Resource) -> + erlang:nif_error(undefined). + +%%----------------------------------------------------------------------------- +%% @param Resource SPI resource returned by `init/2' +%% @returns Current baudrate in Hz +%% @doc Get the current baudrate of the SPI peripheral. +%% @end +%%----------------------------------------------------------------------------- +-spec get_baudrate(Resource :: spi_resource()) -> freq_hz(). +get_baudrate(_Resource) -> + erlang:nif_error(undefined). + +%%----------------------------------------------------------------------------- +%% @param Resource SPI resource returned by `init/2' +%% @param Data Binary data to write +%% @returns Number of bytes written +%% @doc Write to SPI, blocking. +%% @end +%%----------------------------------------------------------------------------- +-spec write_blocking(Resource :: spi_resource(), Data :: binary()) -> non_neg_integer(). +write_blocking(_Resource, _Data) -> + erlang:nif_error(undefined). + +%%----------------------------------------------------------------------------- +%% @param Resource SPI resource returned by `init/2' +%% @param RepeatedTxData Byte value to send on TX while reading +%% @param Count Number of bytes to read +%% @returns `{ok, Data}' +%% @doc Read from SPI, blocking. +%% +%% `RepeatedTxData' is repeatedly sent on TX while reading data. +%% @end +%%----------------------------------------------------------------------------- +-spec read_blocking( + Resource :: spi_resource(), RepeatedTxData :: byte(), Count :: non_neg_integer() +) -> + {ok, binary()}. +read_blocking(_Resource, _RepeatedTxData, _Count) -> + erlang:nif_error(undefined). + +%%----------------------------------------------------------------------------- +%% @param Resource SPI resource returned by `init/2' +%% @param WriteData Binary data to write +%% @returns `{ok, ReadData}' +%% @doc Simultaneous write and read from SPI, blocking. +%% +%% WriteData is sent on TX while the same number of bytes is read +%% into the returned binary. +%% @end +%%----------------------------------------------------------------------------- +-spec write_read_blocking(Resource :: spi_resource(), WriteData :: binary()) -> + {ok, ReadData :: binary()}. +write_read_blocking(_Resource, _WriteData) -> + erlang:nif_error(undefined). + +%%----------------------------------------------------------------------------- +%% @param Resource SPI resource returned by `init/2' +%% @param Data Binary data to write (must have even length, 2 bytes per halfword) +%% @returns Number of halfwords written +%% @doc Write 16-bit halfwords to SPI, blocking. +%% +%% The data format is native-endian: each pair of bytes in the +%% binary is one 16-bit halfword. +%% @end +%%----------------------------------------------------------------------------- +-spec write16_blocking(Resource :: spi_resource(), Data :: binary()) -> non_neg_integer(). +write16_blocking(_Resource, _Data) -> + erlang:nif_error(undefined). + +%%----------------------------------------------------------------------------- +%% @param Resource SPI resource returned by `init/2' +%% @param RepeatedTxData 16-bit value to send on TX while reading +%% @param Count Number of 16-bit halfwords to read +%% @returns `{ok, Data}' +%% @doc Read 16-bit halfwords from SPI, blocking. +%% +%% `RepeatedTxData' is repeatedly sent on TX while reading data. +%% The returned binary contains `Count * 2' bytes in native +%% byte order. +%% @end +%%----------------------------------------------------------------------------- +-spec read16_blocking( + Resource :: spi_resource(), RepeatedTxData :: 0..65535, Count :: non_neg_integer() +) -> + {ok, binary()}. +read16_blocking(_Resource, _RepeatedTxData, _Count) -> + erlang:nif_error(undefined). + +%%----------------------------------------------------------------------------- +%% @param Resource SPI resource returned by `init/2' +%% @param WriteData Binary data to write (must have even length, 2 bytes per halfword) +%% @returns `{ok, ReadData}' +%% @doc Simultaneous 16-bit write and read from SPI, blocking. +%% +%% WriteData is sent on TX while the same number of halfwords +%% is read into the returned binary. +%% @end +%%----------------------------------------------------------------------------- +-spec write_read16_blocking(Resource :: spi_resource(), WriteData :: binary()) -> + {ok, ReadData :: binary()}. +write_read16_blocking(_Resource, _WriteData) -> + erlang:nif_error(undefined). + +%% --------------------------------------------------------------------------- +%% Internal helpers +%% --------------------------------------------------------------------------- + +%% @private +call(Pid, Request) -> + MRef = monitor(process, Pid), + Ref = make_ref(), + Pid ! {self(), Ref, Request}, + receive + {Ref, Reply} -> + demonitor(MRef, [flush]), + Reply; + {'DOWN', MRef, process, Pid, Reason} -> + {error, {server_died, Reason}} + end. + +%% @private +maybe_set_spi_function(undefined) -> ok; +maybe_set_spi_function(Pin) -> gpio:set_function(Pin, spi). + +%% @private +get_value(Key, Map, Default) when is_map(Map) -> + maps:get(Key, Map, Default); +get_value(Key, List, Default) when is_list(List) -> + proplists:get_value(Key, List, Default). + +%% @private +get_value(Key, MapOrList) -> + get_value(Key, MapOrList, undefined). + +%% @private +setup_devices(DeviceConfigMap) when is_map(DeviceConfigMap) -> + setup_devices(maps:to_list(DeviceConfigMap)); +setup_devices(DeviceConfigList) when is_list(DeviceConfigList) -> + lists:foldl(fun setup_device/2, #{}, DeviceConfigList). + +%% @private +setup_device({Name, Config}, Acc) -> + CS = get_value(cs, Config), + ClockSpeedHz = get_value(clock_speed_hz, Config, ?DEFAULT_CLOCK_SPEED_HZ), + Mode = get_value(mode, Config, ?DEFAULT_MODE), + AddressLenBits = get_value(address_len_bits, Config, ?DEFAULT_ADDRESS_LEN_BITS), + CommandLenBits = get_value(command_len_bits, Config, ?DEFAULT_COMMAND_LEN_BITS), + case CS of + undefined -> + ok; + _ -> + gpio:set_function(CS, sio), + gpio:set_pin_mode(CS, output), + gpio:digital_write(CS, 1) + end, + DeviceInfo = #{ + cs => CS, + clock_speed_hz => ClockSpeedHz, + mode => Mode, + address_len_bits => AddressLenBits, + command_len_bits => CommandLenBits + }, + Acc#{Name => DeviceInfo}. + +%% @private +loop(Resource, Devices) -> + receive + {From, Ref, Request} -> + case handle_request(Resource, Devices, Request) of + {reply, Reply, stop} -> + From ! {Ref, Reply}; + {reply, Reply} -> + From ! {Ref, Reply}, + loop(Resource, Devices) + end + end. + +%% @private +handle_request(Resource, _Devices, close) -> + ?MODULE:deinit(Resource), + {reply, ok, stop}; +handle_request(_Resource, Devices, {Tag, DeviceName, _}) when + (Tag =:= write orelse Tag =:= write_read), not is_map_key(DeviceName, Devices) +-> + {reply, {error, {unknown_device, DeviceName}}}; +handle_request(_Resource, Devices, {Tag, DeviceName, _, _}) when + Tag =:= read_at, not is_map_key(DeviceName, Devices) +-> + {reply, {error, {unknown_device, DeviceName}}}; +handle_request(_Resource, Devices, {Tag, DeviceName, _, _, _}) when + Tag =:= write_at, not is_map_key(DeviceName, Devices) +-> + {reply, {error, {unknown_device, DeviceName}}}; +handle_request(Resource, Devices, {read_at, DeviceName, Address, LenBits}) -> + DeviceInfo = maps:get(DeviceName, Devices), + select_device(Resource, DeviceInfo), + AddressLenBits = maps:get(address_len_bits, DeviceInfo), + AddrBin = <>, + AddrLen = byte_size(AddrBin), + AddrLen = ?MODULE:write_blocking(Resource, AddrBin), + LenBytes = (LenBits + 7) div 8, + {ok, Data} = ?MODULE:read_blocking(Resource, 0, LenBytes), + deselect_device(DeviceInfo), + <> = Data, + {reply, {ok, Value}}; +handle_request(Resource, Devices, {write_at, DeviceName, Address, LenBits, Data}) -> + DeviceInfo = maps:get(DeviceName, Devices), + select_device(Resource, DeviceInfo), + AddressLenBits = maps:get(address_len_bits, DeviceInfo), + TxBin = <>, + {ok, RxBin} = ?MODULE:write_read_blocking(Resource, TxBin), + deselect_device(DeviceInfo), + <<_:AddressLenBits, Value:LenBits/big-unsigned>> = RxBin, + {reply, {ok, Value}}; +handle_request(Resource, Devices, {write, DeviceName, Transaction}) -> + DeviceInfo = maps:get(DeviceName, Devices), + select_device(Resource, DeviceInfo), + PrefixBin = build_tx_prefix(DeviceInfo, Transaction), + WriteData = maps:get(write_data, Transaction, <<>>), + WriteBits = maps:get(write_bits, Transaction, byte_size(WriteData) * 8), + WriteBytes = (WriteBits + 7) div 8, + TxData = binary:part(WriteData, 0, min(WriteBytes, byte_size(WriteData))), + TxBin = <>, + TxLen = byte_size(TxBin), + TxLen = ?MODULE:write_blocking(Resource, TxBin), + deselect_device(DeviceInfo), + {reply, ok}; +handle_request(Resource, Devices, {write_read, DeviceName, Transaction}) -> + DeviceInfo = maps:get(DeviceName, Devices), + select_device(Resource, DeviceInfo), + PrefixBin = build_tx_prefix(DeviceInfo, Transaction), + WriteData = maps:get(write_data, Transaction, <<>>), + WriteBits = maps:get(write_bits, Transaction, byte_size(WriteData) * 8), + WriteBytes = (WriteBits + 7) div 8, + ReadBits = maps:get(read_bits, Transaction, WriteBits), + ReadBytes = (ReadBits + 7) div 8, + case byte_size(PrefixBin) of + 0 -> + ok; + PrefixLen -> + PrefixLen = ?MODULE:write_blocking(Resource, PrefixBin) + end, + DataLen = max(WriteBytes, ReadBytes), + TxData = pad_binary(binary:part(WriteData, 0, min(WriteBytes, byte_size(WriteData))), DataLen), + {ok, RxBin} = ?MODULE:write_read_blocking(Resource, TxData), + deselect_device(DeviceInfo), + ReadData = binary:part(RxBin, 0, ReadBytes), + {reply, {ok, ReadData}}. + +%% @private +select_device(Resource, DeviceInfo) -> + ClockSpeedHz = maps:get(clock_speed_hz, DeviceInfo), + Mode = maps:get(mode, DeviceInfo), + {CPOL, CPHA} = mode_to_cpol_cpha(Mode), + ?MODULE:set_baudrate(Resource, ClockSpeedHz), + ?MODULE:set_format(Resource, 8, CPOL, CPHA), + case maps:get(cs, DeviceInfo) of + undefined -> ok; + CS -> gpio:digital_write(CS, 0) + end. + +%% @private +deselect_device(DeviceInfo) -> + case maps:get(cs, DeviceInfo) of + undefined -> ok; + CS -> gpio:digital_write(CS, 1) + end. + +%% @private +mode_to_cpol_cpha(0) -> {0, 0}; +mode_to_cpol_cpha(1) -> {0, 1}; +mode_to_cpol_cpha(2) -> {1, 0}; +mode_to_cpol_cpha(3) -> {1, 1}. + +%% @private +build_tx_prefix(DeviceInfo, Transaction) -> + CommandLenBits = maps:get(command_len_bits, DeviceInfo), + AddressLenBits = maps:get(address_len_bits, DeviceInfo), + Command = maps:get(command, Transaction, 0), + Address = maps:get(address, Transaction, 0), + case {CommandLenBits, AddressLenBits} of + {0, 0} -> <<>>; + {0, _} -> <>; + {_, 0} -> <>; + {_, _} -> <> + end. + +%% @private +pad_binary(Bin, Len) when byte_size(Bin) >= Len -> + Bin; +pad_binary(Bin, Len) -> + PadLen = Len - byte_size(Bin), + <>. diff --git a/libs/avm_stm32/src/CMakeLists.txt b/libs/avm_stm32/src/CMakeLists.txt index 5169705bdb..7437d2a1e9 100644 --- a/libs/avm_stm32/src/CMakeLists.txt +++ b/libs/avm_stm32/src/CMakeLists.txt @@ -25,6 +25,7 @@ include(BuildErlang) set(ERLANG_MODULES gpio i2c + spi ) pack_archive(avm_stm32 DEPENDS_ON eavmlib ERLC_FLAGS +warnings_as_errors MODULES ${ERLANG_MODULES}) diff --git a/libs/avm_stm32/src/gpio.erl b/libs/avm_stm32/src/gpio.erl index 1ab14fc403..687f5200bb 100644 --- a/libs/avm_stm32/src/gpio.erl +++ b/libs/avm_stm32/src/gpio.erl @@ -59,8 +59,10 @@ -type direction() :: input | output | output_od. %% The direction is used to set the mode of operation for a GPIO pin, either as an input, an output, or output with open drain. %% Pull mode and output_speed must be set at the same time as direction. See @type mode_config() --type mode_config() :: {direction(), pull()} | {output, pull(), output_speed()}. +-type mode_config() :: + {direction(), pull()} | {output, pull(), output_speed()} | {af, non_neg_integer()}. %% Extended mode configuration options. Default pull() is `floating', default output_speed() is `mhz_2' if options are omitted. +%% `{af, AFNumber}' configures the pin in alternate function push-pull mode with the given AF number (e.g. 5 for SPI1). -type pull() :: up | down | floating. %% Internal resistor pull mode. STM32 does not support `up_down'. -type output_speed() :: mhz_2 | mhz_25 | mhz_50 | mhz_100. diff --git a/libs/avm_stm32/src/spi.erl b/libs/avm_stm32/src/spi.erl new file mode 100644 index 0000000000..59fee4b025 --- /dev/null +++ b/libs/avm_stm32/src/spi.erl @@ -0,0 +1,575 @@ +% +% 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 SPI interface for STM32 +%% +%% This module provides an interface to the SPI hardware on STM32 platforms. +%% +%% Two API levels are provided: +%% +%% Low-level API (STM32 HAL) +%% {@link init/2}, {@link deinit/1}, {@link set_baudrate/2}, +%% {@link set_format/4}, {@link transmit/3}, {@link receive_/3}, +%% {@link transmit_receive/3}, {@link abort/1}, {@link get_state/1}, +%% {@link get_error/1}. +%% These map directly to the corresponding HAL_SPI_* functions and operate on +%% a bare resource reference returned by {@link init/2}. +%% Pin muxing must be done separately via `gpio:set_pin_mode/2' with +%% `{af, AFNumber}' mode. +%% +%% High-level API (`spi_hal' behavior) +%% {@link open/1}, {@link close/1}, {@link read_at/4}, {@link write_at/5}, +%% {@link write/3}, {@link write_read/3}. +%% {@link open/1} handles pin setup and CS management automatically. +%% @end +%%----------------------------------------------------------------------------- +-module(spi). + +-behaviour(spi_hal). + +%% High-level API (spi_hal behaviour) +-export([ + open/1, + close/1, + read_at/4, + write_at/5, + write/3, + write_read/3 +]). + +%% Low-level API (STM32 HAL) +-export([ + init/2, + deinit/1, + set_baudrate/2, + set_format/4, + transmit/3, + receive_/3, + transmit_receive/3, + abort/1, + get_state/1, + get_error/1 +]). + +-type freq_hz() :: non_neg_integer(). +-type peripheral() :: 1..6. +-type spi_resource() :: reference(). +-type spi() :: pid(). +-type device_name() :: atom(). +-type address() :: non_neg_integer(). +-type transaction() :: #{ + command => integer(), + address => non_neg_integer(), + write_data => binary(), + write_bits => non_neg_integer(), + read_bits => non_neg_integer() +}. +-type spi_state() :: reset | ready | busy | busy_tx | busy_rx | busy_tx_rx | error | abort. + +-export_type([spi/0, spi_resource/0, device_name/0, address/0, transaction/0, spi_state/0]). + +-define(DEFAULT_CLOCK_SPEED_HZ, 1000000). +-define(DEFAULT_MODE, 0). +-define(DEFAULT_ADDRESS_LEN_BITS, 8). +-define(DEFAULT_COMMAND_LEN_BITS, 0). +-define(DEFAULT_PERIPHERAL, 1). +-define(DEFAULT_AF, 5). +-define(DEFAULT_SEND_TIMEOUT_MS, 1000). + +%% --------------------------------------------------------------------------- +%% High-level API (spi_hal behaviour) +%% --------------------------------------------------------------------------- + +%%----------------------------------------------------------------------------- +%% @param Params Initialization parameters +%% @returns SPI handle (pid) +%% @doc Open a connection to the SPI driver +%% +%% This function configures the GPIO pins for SPI alternate function, +%% initializes the SPI peripheral, and sets up CS pins for +%% software chip-select management. +%% +%% Parameters follow a similar structure to other platforms: +%%
    +%%
  • `{bus_config, BusConfig}' - bus configuration (required)
  • +%%
  • `{device_config, DeviceConfigs}' - device configurations
  • +%%
+%% +%% Bus configuration: +%%
    +%%
  • `{sclk, Pin}' - the SCLK pin (required)
  • +%%
  • `{mosi, Pin}' - the MOSI pin
  • +%%
  • `{miso, Pin}' - the MISO pin
  • +%%
  • `{peripheral, 1..6}' - the SPI peripheral to use (default: 1)
  • +%%
  • `{af, N}' - GPIO alternate function number (default: 5)
  • +%%
  • `{send_timeout_ms, Ms}' - timeout for SPI transfers in +%% milliseconds (default: 1000)
  • +%%
+%% +%% Device configuration (keyed by device name atom): +%%
    +%%
  • `{cs, Pin}' - the CS pin
  • +%%
  • `{clock_speed_hz, Hz}' - clock speed (default: 1000000)
  • +%%
  • `{mode, 0..3}' - SPI mode (default: 0)
  • +%%
  • `{address_len_bits, Bits}' - address width (default: 8)
  • +%%
  • `{command_len_bits, Bits}' - command width (default: 0)
  • +%%
+%% @end +%%----------------------------------------------------------------------------- +-spec open(Params :: [{atom(), term()}]) -> spi(). +open(Params) -> + BusConfig = get_value(bus_config, Params), + DeviceConfigList = get_value(device_config, Params, []), + SCLK = get_value(sclk, BusConfig), + MOSI = get_value(mosi, BusConfig, undefined), + MISO = get_value(miso, BusConfig, undefined), + Peripheral = get_value(peripheral, BusConfig, ?DEFAULT_PERIPHERAL), + AF = get_value(af, BusConfig, ?DEFAULT_AF), + SendTimeoutMs = get_value(send_timeout_ms, BusConfig, ?DEFAULT_SEND_TIMEOUT_MS), + maybe_set_af(SCLK, AF), + maybe_set_af(MOSI, AF), + maybe_set_af(MISO, AF), + {ok, {_ActualBaud, Resource}} = ?MODULE:init(Peripheral, ?DEFAULT_CLOCK_SPEED_HZ), + Devices = setup_devices(DeviceConfigList), + spawn_link(fun() -> loop(Resource, Devices, SendTimeoutMs) end). + +%%----------------------------------------------------------------------------- +%% @param SPI SPI handle created via `open/1' +%% @returns `ok' +%% @doc Close the SPI driver and free resources. +%% @end +%%----------------------------------------------------------------------------- +-spec close(SPI :: spi()) -> ok. +close(Pid) -> + call(Pid, close). + +%%----------------------------------------------------------------------------- +%% @param SPI SPI handle created via `open/1' +%% @param DeviceName device name from configuration +%% @param Address SPI address from which to read +%% @param Len number of bits to read +%% @returns `{ok, Value}' or `{error, Reason}' +%% @doc Read a value from an address on the device. +%% +%% Sends the address (address_len_bits wide), then reads Len bits. +%% The read value is returned as a big-endian integer. +%% @end +%%----------------------------------------------------------------------------- +-spec read_at( + SPI :: spi(), DeviceName :: device_name(), Address :: address(), Len :: non_neg_integer() +) -> + {ok, integer()} | {error, term()}. +read_at(Pid, DeviceName, Address, Len) -> + call(Pid, {read_at, DeviceName, Address, Len}). + +%%----------------------------------------------------------------------------- +%% @param SPI SPI handle created via `open/1' +%% @param DeviceName device name from configuration +%% @param Address SPI address to which to write +%% @param Len number of bits to transfer +%% @param Data value to write +%% @returns `{ok, Value}' or `{error, Reason}' +%% @doc Write a value to an address on the device. +%% +%% Sends the address with bit 7 forced high (16#80 ORed). +%% This convention is used by some devices (e.g., LIS3DH) to +%% distinguish writes from reads. For devices with different +%% conventions, use {@link write/3} with a transaction map instead. +%% Returns the value read back during the data phase. +%% @end +%%----------------------------------------------------------------------------- +-spec write_at( + SPI :: spi(), + DeviceName :: device_name(), + Address :: address(), + Len :: non_neg_integer(), + Data :: integer() +) -> + {ok, integer()} | {error, term()}. +write_at(Pid, DeviceName, Address, Len, Data) -> + call(Pid, {write_at, DeviceName, Address bor 16#80, Len, Data}). + +%%----------------------------------------------------------------------------- +%% @param SPI SPI handle created via `open/1' +%% @param DeviceName device name from configuration +%% @param Transaction transaction map +%% @returns `ok' or `{error, Reason}' +%% @doc Write data to the SPI device using a transaction. +%% +%% The transaction map may contain: `command', `address', +%% `write_data', and `write_bits'. +%% @end +%%----------------------------------------------------------------------------- +-spec write(SPI :: spi(), DeviceName :: device_name(), Transaction :: transaction()) -> + ok | {error, term()}. +write(Pid, DeviceName, Transaction) -> + call(Pid, {write, DeviceName, Transaction}). + +%%----------------------------------------------------------------------------- +%% @param SPI SPI handle created via `open/1' +%% @param DeviceName device name from configuration +%% @param Transaction transaction map +%% @returns `{ok, ReadData}' or `{error, Reason}' +%% @doc Write and simultaneously read from the SPI device. +%% +%% The transaction map may contain: `command', `address', +%% `write_data', `write_bits', and `read_bits'. +%% Returns the first `ceil(read_bits / 8)' bytes read during the +%% data phase. +%% @end +%%----------------------------------------------------------------------------- +-spec write_read(SPI :: spi(), DeviceName :: device_name(), Transaction :: transaction()) -> + {ok, binary()} | {error, term()}. +write_read(Pid, DeviceName, Transaction) -> + call(Pid, {write_read, DeviceName, Transaction}). + +%% --------------------------------------------------------------------------- +%% Low-level API (STM32 HAL) +%% --------------------------------------------------------------------------- + +%%----------------------------------------------------------------------------- +%% @param Peripheral SPI peripheral number (1..6) +%% @param Baudrate Baudrate in Hz +%% @returns `{ok, {ActualBaudrate, Resource}}' +%% @doc Initialize the SPI HW block (HAL_SPI_Init). +%% +%% Pin muxing must be done separately via +%% `gpio:set_pin_mode(Pin, {af, AFNumber})'. +%% @end +%%----------------------------------------------------------------------------- +-spec init(Peripheral :: peripheral(), Baudrate :: freq_hz()) -> + {ok, {ActualBaudrate :: freq_hz(), Resource :: spi_resource()}}. +init(_Peripheral, _Baudrate) -> + erlang:nif_error(undefined). + +%%----------------------------------------------------------------------------- +%% @param Resource SPI resource returned by `init/2' +%% @returns `ok' +%% @doc Disable the SPI HW block (HAL_SPI_DeInit). +%% @end +%%----------------------------------------------------------------------------- +-spec deinit(Resource :: spi_resource()) -> ok. +deinit(_Resource) -> + erlang:nif_error(undefined). + +%%----------------------------------------------------------------------------- +%% @param Resource SPI resource returned by `init/2' +%% @param Baudrate Baudrate in Hz +%% @returns `{ok, ActualBaudrate}' +%% @doc Set SPI baudrate. +%% +%% Reinitializes the SPI peripheral with the closest achievable +%% prescaler divider. +%% @end +%%----------------------------------------------------------------------------- +-spec set_baudrate(Resource :: spi_resource(), Baudrate :: freq_hz()) -> + {ok, ActualBaudrate :: freq_hz()}. +set_baudrate(_Resource, _Baudrate) -> + erlang:nif_error(undefined). + +%%----------------------------------------------------------------------------- +%% @param Resource SPI resource returned by `init/2' +%% @param DataBits Number of data bits per transfer (8 or 16) +%% @param CPOL Clock polarity (0 or 1) +%% @param CPHA Clock phase (0 or 1) +%% @returns `ok' +%% @doc Set SPI format. +%% +%% SPI mode mapping: mode 0 = CPOL 0, CPHA 0; mode 1 = CPOL 0, +%% CPHA 1; mode 2 = CPOL 1, CPHA 0; mode 3 = CPOL 1, CPHA 1. +%% Data order is always MSB first. +%% @end +%%----------------------------------------------------------------------------- +-spec set_format(Resource :: spi_resource(), DataBits :: 8 | 16, CPOL :: 0 | 1, CPHA :: 0 | 1) -> + ok. +set_format(_Resource, _DataBits, _CPOL, _CPHA) -> + erlang:nif_error(undefined). + +%%----------------------------------------------------------------------------- +%% @param Resource SPI resource returned by `init/2' +%% @param Data Binary data to transmit +%% @param TimeoutMs Timeout in milliseconds or `infinity' +%% @returns Number of bytes written, or `{error, Reason}' +%% @doc Transmit data (HAL_SPI_Transmit). +%% @end +%%----------------------------------------------------------------------------- +-spec transmit(Resource :: spi_resource(), Data :: binary(), TimeoutMs :: timeout()) -> + non_neg_integer() | {error, term()}. +transmit(_Resource, _Data, _TimeoutMs) -> + erlang:nif_error(undefined). + +%%----------------------------------------------------------------------------- +%% @param Resource SPI resource returned by `init/2' +%% @param Count Number of bytes to receive +%% @param TimeoutMs Timeout in milliseconds or `infinity' +%% @returns `{ok, Data}' or `{error, Reason}' +%% @doc Receive data (HAL_SPI_Receive). +%% @end +%%----------------------------------------------------------------------------- +-spec receive_(Resource :: spi_resource(), Count :: non_neg_integer(), TimeoutMs :: timeout()) -> + {ok, binary()} | {error, term()}. +receive_(_Resource, _Count, _TimeoutMs) -> + erlang:nif_error(undefined). + +%%----------------------------------------------------------------------------- +%% @param Resource SPI resource returned by `init/2' +%% @param Data Binary data to transmit +%% @param TimeoutMs Timeout in milliseconds or `infinity' +%% @returns `{ok, ReadData}' or `{error, Reason}' +%% @doc Simultaneous transmit and receive (HAL_SPI_TransmitReceive). +%% +%% Data is sent on TX while the same number of bytes is read +%% into the returned binary. +%% @end +%%----------------------------------------------------------------------------- +-spec transmit_receive(Resource :: spi_resource(), Data :: binary(), TimeoutMs :: timeout()) -> + {ok, binary()} | {error, term()}. +transmit_receive(_Resource, _Data, _TimeoutMs) -> + erlang:nif_error(undefined). + +%%----------------------------------------------------------------------------- +%% @param Resource SPI resource returned by `init/2' +%% @returns `ok' or `{error, Reason}' +%% @doc Abort ongoing SPI transfer (HAL_SPI_Abort). +%% @end +%%----------------------------------------------------------------------------- +-spec abort(Resource :: spi_resource()) -> ok | {error, term()}. +abort(_Resource) -> + erlang:nif_error(undefined). + +%%----------------------------------------------------------------------------- +%% @param Resource SPI resource returned by `init/2' +%% @returns SPI state atom +%% @doc Get the SPI peripheral state (HAL_SPI_GetState). +%% +%% Returns one of: `reset', `ready', `busy', `busy_tx', `busy_rx', +%% `busy_tx_rx', `error', `abort'. +%% @end +%%----------------------------------------------------------------------------- +-spec get_state(Resource :: spi_resource()) -> spi_state(). +get_state(_Resource) -> + erlang:nif_error(undefined). + +%%----------------------------------------------------------------------------- +%% @param Resource SPI resource returned by `init/2' +%% @returns Error code as integer +%% @doc Get the SPI error code (HAL_SPI_GetError). +%% +%% Returns a bitmask of HAL_SPI_ERROR_* values. +%% 0 means no error. +%% @end +%%----------------------------------------------------------------------------- +-spec get_error(Resource :: spi_resource()) -> non_neg_integer(). +get_error(_Resource) -> + erlang:nif_error(undefined). + +%% --------------------------------------------------------------------------- +%% Internal helpers +%% --------------------------------------------------------------------------- + +%% @private +call(Pid, Request) -> + MRef = monitor(process, Pid), + Ref = make_ref(), + Pid ! {self(), Ref, Request}, + receive + {Ref, Reply} -> + demonitor(MRef, [flush]), + Reply; + {'DOWN', MRef, process, Pid, Reason} -> + {error, {server_died, Reason}} + end. + +%% @private +maybe_set_af(undefined, _AF) -> ok; +maybe_set_af(Pin, AF) -> gpio:set_pin_mode(Pin, {af, AF}). + +%% @private +get_value(Key, Map, Default) when is_map(Map) -> + maps:get(Key, Map, Default); +get_value(Key, List, Default) when is_list(List) -> + proplists:get_value(Key, List, Default). + +%% @private +get_value(Key, MapOrList) -> + get_value(Key, MapOrList, undefined). + +%% @private +setup_devices(DeviceConfigMap) when is_map(DeviceConfigMap) -> + setup_devices(maps:to_list(DeviceConfigMap)); +setup_devices(DeviceConfigList) when is_list(DeviceConfigList) -> + lists:foldl(fun setup_device/2, #{}, DeviceConfigList). + +%% @private +setup_device({Name, Config}, Acc) -> + CS = get_value(cs, Config), + ClockSpeedHz = get_value(clock_speed_hz, Config, ?DEFAULT_CLOCK_SPEED_HZ), + Mode = get_value(mode, Config, ?DEFAULT_MODE), + AddressLenBits = get_value(address_len_bits, Config, ?DEFAULT_ADDRESS_LEN_BITS), + CommandLenBits = get_value(command_len_bits, Config, ?DEFAULT_COMMAND_LEN_BITS), + case CS of + undefined -> + ok; + _ -> + gpio:set_pin_mode(CS, output), + gpio:digital_write(CS, 1) + end, + DeviceInfo = #{ + cs => CS, + clock_speed_hz => ClockSpeedHz, + mode => Mode, + address_len_bits => AddressLenBits, + command_len_bits => CommandLenBits + }, + Acc#{Name => DeviceInfo}. + +%% @private +loop(Resource, Devices, SendTimeoutMs) -> + receive + {From, Ref, Request} -> + case handle_request(Resource, Devices, SendTimeoutMs, Request) of + {reply, Reply, stop} -> + From ! {Ref, Reply}; + {reply, Reply} -> + From ! {Ref, Reply}, + loop(Resource, Devices, SendTimeoutMs) + end + end. + +%% @private +handle_request(Resource, _Devices, _SendTimeoutMs, close) -> + ?MODULE:deinit(Resource), + {reply, ok, stop}; +handle_request(Resource, Devices, SendTimeoutMs, {read_at, DeviceName, Address, LenBits}) -> + DeviceInfo = maps:get(DeviceName, Devices), + select_device(Resource, DeviceInfo), + AddressLenBits = maps:get(address_len_bits, DeviceInfo), + LenBytes = (LenBits + 7) div 8, + TxBin = <>, + case ?MODULE:transmit_receive(Resource, TxBin, SendTimeoutMs) of + {ok, RxBin} -> + deselect_device(DeviceInfo), + <<_:AddressLenBits, Value:LenBits/big-unsigned, _/bitstring>> = RxBin, + {reply, {ok, Value}}; + {error, _} = Error -> + deselect_device(DeviceInfo), + {reply, Error} + end; +handle_request(Resource, Devices, SendTimeoutMs, {write_at, DeviceName, Address, LenBits, Data}) -> + DeviceInfo = maps:get(DeviceName, Devices), + select_device(Resource, DeviceInfo), + AddressLenBits = maps:get(address_len_bits, DeviceInfo), + TxBin = <>, + case ?MODULE:transmit_receive(Resource, TxBin, SendTimeoutMs) of + {ok, RxBin} -> + deselect_device(DeviceInfo), + <<_:AddressLenBits, Value:LenBits/big-unsigned>> = RxBin, + {reply, {ok, Value}}; + {error, _} = Error -> + deselect_device(DeviceInfo), + {reply, Error} + end; +handle_request(Resource, Devices, SendTimeoutMs, {write, DeviceName, Transaction}) -> + DeviceInfo = maps:get(DeviceName, Devices), + select_device(Resource, DeviceInfo), + PrefixBin = build_tx_prefix(DeviceInfo, Transaction), + WriteData = maps:get(write_data, Transaction, <<>>), + WriteBits = maps:get(write_bits, Transaction, byte_size(WriteData) * 8), + WriteBytes = (WriteBits + 7) div 8, + TxData = binary:part(WriteData, 0, min(WriteBytes, byte_size(WriteData))), + case ?MODULE:transmit(Resource, <>, SendTimeoutMs) of + {error, _} = Error -> + deselect_device(DeviceInfo), + {reply, Error}; + _N -> + deselect_device(DeviceInfo), + {reply, ok} + end; +handle_request(Resource, Devices, SendTimeoutMs, {write_read, DeviceName, Transaction}) -> + DeviceInfo = maps:get(DeviceName, Devices), + select_device(Resource, DeviceInfo), + PrefixBin = build_tx_prefix(DeviceInfo, Transaction), + PrefixLen = byte_size(PrefixBin), + WriteData = maps:get(write_data, Transaction, <<>>), + WriteBits = maps:get(write_bits, Transaction, byte_size(WriteData) * 8), + WriteBytes = (WriteBits + 7) div 8, + ReadBits = maps:get(read_bits, Transaction, WriteBits), + ReadBytes = (ReadBits + 7) div 8, + DataLen = max(WriteBytes, ReadBytes), + TxData = pad_binary(binary:part(WriteData, 0, min(WriteBytes, byte_size(WriteData))), DataLen), + case + ?MODULE:transmit_receive( + Resource, <>, SendTimeoutMs + ) + of + {ok, RxBin} -> + deselect_device(DeviceInfo), + ReadData = binary:part(RxBin, PrefixLen, ReadBytes), + {reply, {ok, ReadData}}; + {error, _} = Error -> + deselect_device(DeviceInfo), + {reply, Error} + end. + +%% @private +select_device(Resource, DeviceInfo) -> + ClockSpeedHz = maps:get(clock_speed_hz, DeviceInfo), + Mode = maps:get(mode, DeviceInfo), + {CPOL, CPHA} = mode_to_cpol_cpha(Mode), + ?MODULE:set_baudrate(Resource, ClockSpeedHz), + ?MODULE:set_format(Resource, 8, CPOL, CPHA), + case maps:get(cs, DeviceInfo) of + undefined -> ok; + CS -> gpio:digital_write(CS, 0) + end. + +%% @private +deselect_device(DeviceInfo) -> + case maps:get(cs, DeviceInfo) of + undefined -> ok; + CS -> gpio:digital_write(CS, 1) + end. + +%% @private +mode_to_cpol_cpha(0) -> {0, 0}; +mode_to_cpol_cpha(1) -> {0, 1}; +mode_to_cpol_cpha(2) -> {1, 0}; +mode_to_cpol_cpha(3) -> {1, 1}. + +%% @private +build_tx_prefix(DeviceInfo, Transaction) -> + CommandLenBits = maps:get(command_len_bits, DeviceInfo), + AddressLenBits = maps:get(address_len_bits, DeviceInfo), + Command = maps:get(command, Transaction, 0), + Address = maps:get(address, Transaction, 0), + case {CommandLenBits, AddressLenBits} of + {0, 0} -> <<>>; + {0, _} -> <>; + {_, 0} -> <>; + {_, _} -> <> + end. + +%% @private +pad_binary(Bin, Len) when byte_size(Bin) >= Len -> + Bin; +pad_binary(Bin, Len) -> + PadLen = Len - byte_size(Bin), + <>. diff --git a/libs/eavmlib/src/spi_hal.erl b/libs/eavmlib/src/spi_hal.erl index cd037ce6db..625ae86c9c 100644 --- a/libs/eavmlib/src/spi_hal.erl +++ b/libs/eavmlib/src/spi_hal.erl @@ -25,7 +25,7 @@ %% must implement. It provides a common interface for SPI (Serial %% Peripheral Interface) operations across all supported platforms. %% -%% Currently, only ESP32 provides an SPI implementation. +%% ESP32, RP2 and STM32 provide SPI implementations. %% %%

Lifecycle

%% diff --git a/src/platforms/rp2/src/lib/CMakeLists.txt b/src/platforms/rp2/src/lib/CMakeLists.txt index 53510e8c20..7e0e535a4d 100644 --- a/src/platforms/rp2/src/lib/CMakeLists.txt +++ b/src/platforms/rp2/src/lib/CMakeLists.txt @@ -32,6 +32,7 @@ set(HEADER_FILES set(SOURCE_FILES gpiodriver.c i2cdriver.c + spidriver.c networkdriver.c otp_crypto_platform.c platform_defaultatoms.c @@ -60,6 +61,7 @@ target_link_libraries( PUBLIC hardware_gpio hardware_i2c + hardware_spi hardware_sync pico_float pico_mbedtls @@ -124,4 +126,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,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,otp_crypto_nif") diff --git a/src/platforms/rp2/src/lib/spidriver.c b/src/platforms/rp2/src/lib/spidriver.c new file mode 100644 index 0000000000..eeb3b1384e --- /dev/null +++ b/src/platforms/rp2/src/lib/spidriver.c @@ -0,0 +1,647 @@ +/* + * 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 + +#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_SPI_INSTANCES 2 + +static ErlNifResourceType *spi_resource_type; +static bool spi_in_use[NUM_SPI_INSTANCES]; + +struct SPIResource +{ + spi_inst_t *spi_inst; + int peripheral; +}; + +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_spi_resource(Context *ctx, term resource_term, struct SPIResource **rsrc_obj) +{ + void *rsrc_obj_ptr; + if (UNLIKELY(!enif_get_resource(erl_nif_env_from_context(ctx), resource_term, spi_resource_type, &rsrc_obj_ptr))) { + return false; + } + *rsrc_obj = (struct SPIResource *) rsrc_obj_ptr; + return (*rsrc_obj)->spi_inst != NULL; +} + +static term nif_spi_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_SPI_INSTANCES)) { + RAISE_ERROR(BADARG_ATOM); + } + if (UNLIKELY(spi_in_use[peripheral])) { + 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(ctx->global, ATOM_STR("\x4", "busy"))); + } + + uint baudrate = (uint) term_to_int(argv[1]); + spi_inst_t *inst = (peripheral == 0) ? spi0 : spi1; + + uint actual_baudrate = spi_init(inst, baudrate); + spi_in_use[peripheral] = true; + + struct SPIResource *rsrc_obj = enif_alloc_resource(spi_resource_type, sizeof(struct SPIResource)); + if (IS_NULL_PTR(rsrc_obj)) { + spi_deinit(inst); + spi_in_use[peripheral] = false; + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + rsrc_obj->spi_inst = inst; + rsrc_obj->peripheral = peripheral; + + if (UNLIKELY(memory_ensure_free(ctx, TERM_BOXED_RESOURCE_SIZE) != MEMORY_GC_OK)) { + spi_deinit(inst); + spi_in_use[peripheral] = false; + 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_int(actual_baudrate)); + term_put_tuple_element(inner, 1, obj); + + return create_pair(ctx, OK_ATOM, inner); +} + +static term nif_spi_deinit(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + void *rsrc_obj_ptr; + if (UNLIKELY(!enif_get_resource(erl_nif_env_from_context(ctx), argv[0], spi_resource_type, &rsrc_obj_ptr))) { + RAISE_ERROR(BADARG_ATOM); + } + struct SPIResource *rsrc_obj = (struct SPIResource *) rsrc_obj_ptr; + + if (rsrc_obj->spi_inst != NULL) { + spi_deinit(rsrc_obj->spi_inst); + rsrc_obj->spi_inst = NULL; + spi_in_use[rsrc_obj->peripheral] = false; + } + + return OK_ATOM; +} + +static term nif_spi_set_baudrate(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + struct SPIResource *rsrc_obj; + if (UNLIKELY(!get_spi_resource(ctx, argv[0], &rsrc_obj))) { + RAISE_ERROR(BADARG_ATOM); + } + VALIDATE_VALUE(argv[1], term_is_integer); + + uint baudrate = (uint) term_to_int(argv[1]); + uint actual = spi_set_baudrate(rsrc_obj->spi_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_int(actual)); +} + +static term nif_spi_set_format(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + struct SPIResource *rsrc_obj; + if (UNLIKELY(!get_spi_resource(ctx, argv[0], &rsrc_obj))) { + RAISE_ERROR(BADARG_ATOM); + } + VALIDATE_VALUE(argv[1], term_is_integer); + VALIDATE_VALUE(argv[2], term_is_integer); + VALIDATE_VALUE(argv[3], term_is_integer); + + uint data_bits = (uint) term_to_int(argv[1]); + avm_int_t cpol_val = term_to_int(argv[2]); + avm_int_t cpha_val = term_to_int(argv[3]); + + if (UNLIKELY(data_bits < 4 || data_bits > 16)) { + RAISE_ERROR(BADARG_ATOM); + } + if (UNLIKELY(cpol_val < 0 || cpol_val > 1 || cpha_val < 0 || cpha_val > 1)) { + RAISE_ERROR(BADARG_ATOM); + } + + spi_set_format(rsrc_obj->spi_inst, data_bits, (spi_cpol_t) cpol_val, (spi_cpha_t) cpha_val, SPI_MSB_FIRST); + + return OK_ATOM; +} + +static term nif_spi_write_blocking(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + struct SPIResource *rsrc_obj; + if (UNLIKELY(!get_spi_resource(ctx, argv[0], &rsrc_obj))) { + 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]); + + int ret = spi_write_blocking(rsrc_obj->spi_inst, data, len); + + return term_from_int(ret); +} + +static term nif_spi_read_blocking(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + struct SPIResource *rsrc_obj; + if (UNLIKELY(!get_spi_resource(ctx, argv[0], &rsrc_obj))) { + RAISE_ERROR(BADARG_ATOM); + } + VALIDATE_VALUE(argv[1], term_is_integer); + VALIDATE_VALUE(argv[2], term_is_integer); + + uint8_t repeated_tx_data = (uint8_t) term_to_int(argv[1]); + avm_int_t count = term_to_int(argv[2]); + + 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); + + int ret = spi_read_blocking(rsrc_obj->spi_inst, repeated_tx_data, buf, (size_t) count); + if (UNLIKELY(ret != count)) { + RAISE_ERROR(BADARG_ATOM); + } + + return create_pair(ctx, OK_ATOM, data); +} + +static term nif_spi_write_read_blocking(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + struct SPIResource *rsrc_obj; + if (UNLIKELY(!get_spi_resource(ctx, argv[0], &rsrc_obj))) { + RAISE_ERROR(BADARG_ATOM); + } + VALIDATE_VALUE(argv[1], term_is_binary); + + const uint8_t *src = (const uint8_t *) term_binary_data(argv[1]); + size_t len = term_binary_size(argv[1]); + + if (UNLIKELY(memory_ensure_free_opt(ctx, TUPLE_SIZE(2) + term_binary_heap_size(len), MEMORY_NO_GC) != MEMORY_GC_OK)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + term data = term_create_uninitialized_binary(len, &ctx->heap, ctx->global); + uint8_t *dst = (uint8_t *) term_binary_data(data); + + int ret = spi_write_read_blocking(rsrc_obj->spi_inst, src, dst, len); + if (UNLIKELY(ret != (int) len)) { + RAISE_ERROR(BADARG_ATOM); + } + + return create_pair(ctx, OK_ATOM, data); +} + +static term nif_spi_set_slave(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + struct SPIResource *rsrc_obj; + if (UNLIKELY(!get_spi_resource(ctx, argv[0], &rsrc_obj))) { + RAISE_ERROR(BADARG_ATOM); + } + if (UNLIKELY(argv[1] != TRUE_ATOM && argv[1] != FALSE_ATOM)) { + RAISE_ERROR(BADARG_ATOM); + } + + bool slave = (argv[1] == TRUE_ATOM); + spi_set_slave(rsrc_obj->spi_inst, slave); + + return OK_ATOM; +} + +static term nif_spi_is_writable(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + struct SPIResource *rsrc_obj; + if (UNLIKELY(!get_spi_resource(ctx, argv[0], &rsrc_obj))) { + RAISE_ERROR(BADARG_ATOM); + } + + return spi_is_writable(rsrc_obj->spi_inst) ? TRUE_ATOM : FALSE_ATOM; +} + +static term nif_spi_is_readable(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + struct SPIResource *rsrc_obj; + if (UNLIKELY(!get_spi_resource(ctx, argv[0], &rsrc_obj))) { + RAISE_ERROR(BADARG_ATOM); + } + + return spi_is_readable(rsrc_obj->spi_inst) ? TRUE_ATOM : FALSE_ATOM; +} + +static term nif_spi_is_busy(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + struct SPIResource *rsrc_obj; + if (UNLIKELY(!get_spi_resource(ctx, argv[0], &rsrc_obj))) { + RAISE_ERROR(BADARG_ATOM); + } + + return spi_is_busy(rsrc_obj->spi_inst) ? TRUE_ATOM : FALSE_ATOM; +} + +static term nif_spi_get_baudrate(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + struct SPIResource *rsrc_obj; + if (UNLIKELY(!get_spi_resource(ctx, argv[0], &rsrc_obj))) { + RAISE_ERROR(BADARG_ATOM); + } + + uint baudrate = spi_get_baudrate(rsrc_obj->spi_inst); + + return term_from_int(baudrate); +} + +static term nif_spi_write16_blocking(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + struct SPIResource *rsrc_obj; + if (UNLIKELY(!get_spi_resource(ctx, argv[0], &rsrc_obj))) { + 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 byte_len = term_binary_size(argv[1]); + + if (UNLIKELY(byte_len % 2 != 0)) { + RAISE_ERROR(BADARG_ATOM); + } + + size_t count = byte_len / 2; + int ret; + if (((uintptr_t) data % _Alignof(uint16_t)) != 0) { + uint16_t *tmp = malloc(byte_len); + if (IS_NULL_PTR(tmp)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + memcpy(tmp, data, byte_len); + ret = spi_write16_blocking(rsrc_obj->spi_inst, tmp, count); + free(tmp); + } else { + ret = spi_write16_blocking(rsrc_obj->spi_inst, (const uint16_t *) data, count); + } + + return term_from_int(ret); +} + +static term nif_spi_read16_blocking(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + struct SPIResource *rsrc_obj; + if (UNLIKELY(!get_spi_resource(ctx, argv[0], &rsrc_obj))) { + RAISE_ERROR(BADARG_ATOM); + } + VALIDATE_VALUE(argv[1], term_is_integer); + VALIDATE_VALUE(argv[2], term_is_integer); + + uint16_t repeated_tx_data = (uint16_t) term_to_int(argv[1]); + avm_int_t count = term_to_int(argv[2]); + + if (UNLIKELY(count < 0 || (size_t) count > SIZE_MAX / 2)) { + RAISE_ERROR(BADARG_ATOM); + } + + size_t byte_len = (size_t) count * 2; + if (UNLIKELY(memory_ensure_free_opt(ctx, TUPLE_SIZE(2) + term_binary_heap_size(byte_len), MEMORY_NO_GC) != MEMORY_GC_OK)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + term data = term_create_uninitialized_binary(byte_len, &ctx->heap, ctx->global); + uint8_t *raw_buf = (uint8_t *) term_binary_data(data); + + if (((uintptr_t) raw_buf % _Alignof(uint16_t)) != 0) { + uint16_t *tmp = malloc(byte_len); + if (IS_NULL_PTR(tmp)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + int ret = spi_read16_blocking(rsrc_obj->spi_inst, repeated_tx_data, tmp, (size_t) count); + memcpy(raw_buf, tmp, byte_len); + free(tmp); + if (UNLIKELY(ret != count)) { + RAISE_ERROR(BADARG_ATOM); + } + } else { + int ret = spi_read16_blocking(rsrc_obj->spi_inst, repeated_tx_data, (uint16_t *) raw_buf, (size_t) count); + if (UNLIKELY(ret != count)) { + RAISE_ERROR(BADARG_ATOM); + } + } + + return create_pair(ctx, OK_ATOM, data); +} + +static term nif_spi_write_read16_blocking(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + struct SPIResource *rsrc_obj; + if (UNLIKELY(!get_spi_resource(ctx, argv[0], &rsrc_obj))) { + RAISE_ERROR(BADARG_ATOM); + } + VALIDATE_VALUE(argv[1], term_is_binary); + + const uint8_t *src = (const uint8_t *) term_binary_data(argv[1]); + size_t byte_len = term_binary_size(argv[1]); + + if (UNLIKELY(byte_len % 2 != 0)) { + RAISE_ERROR(BADARG_ATOM); + } + + size_t count = byte_len / 2; + if (UNLIKELY(memory_ensure_free_opt(ctx, TUPLE_SIZE(2) + term_binary_heap_size(byte_len), MEMORY_NO_GC) != MEMORY_GC_OK)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + term data = term_create_uninitialized_binary(byte_len, &ctx->heap, ctx->global); + uint8_t *dst_raw = (uint8_t *) term_binary_data(data); + + bool src_unaligned = ((uintptr_t) src % _Alignof(uint16_t)) != 0; + bool dst_unaligned = ((uintptr_t) dst_raw % _Alignof(uint16_t)) != 0; + + if (src_unaligned || dst_unaligned) { + uint16_t *src_buf = NULL; + uint16_t *dst_buf = NULL; + const uint16_t *src_ptr; + uint16_t *dst_ptr; + + if (src_unaligned) { + src_buf = malloc(byte_len); + if (IS_NULL_PTR(src_buf)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + memcpy(src_buf, src, byte_len); + src_ptr = src_buf; + } else { + src_ptr = (const uint16_t *) src; + } + + if (dst_unaligned) { + dst_buf = malloc(byte_len); + if (IS_NULL_PTR(dst_buf)) { + free(src_buf); + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + dst_ptr = dst_buf; + } else { + dst_ptr = (uint16_t *) dst_raw; + } + + int ret = spi_write16_read16_blocking(rsrc_obj->spi_inst, src_ptr, dst_ptr, count); + + if (dst_unaligned) { + memcpy(dst_raw, dst_buf, byte_len); + free(dst_buf); + } + free(src_buf); + if (UNLIKELY(ret != (int) count)) { + RAISE_ERROR(BADARG_ATOM); + } + } else { + int ret = spi_write16_read16_blocking(rsrc_obj->spi_inst, (const uint16_t *) src, (uint16_t *) dst_raw, count); + if (UNLIKELY(ret != (int) count)) { + RAISE_ERROR(BADARG_ATOM); + } + } + + return create_pair(ctx, OK_ATOM, data); +} + +static void spi_resource_dtor(ErlNifEnv *caller_env, void *obj) +{ + UNUSED(caller_env); + struct SPIResource *rsrc_obj = (struct SPIResource *) obj; + if (!IS_NULL_PTR(rsrc_obj->spi_inst)) { + spi_deinit(rsrc_obj->spi_inst); + rsrc_obj->spi_inst = NULL; + spi_in_use[rsrc_obj->peripheral] = false; + } +} + +static const ErlNifResourceTypeInit SPIResourceTypeInit = { + .members = 1, + .dtor = spi_resource_dtor, +}; + +// +// NIF structs +// +static const struct Nif spi_init_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_spi_init +}; +static const struct Nif spi_deinit_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_spi_deinit +}; +static const struct Nif spi_set_baudrate_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_spi_set_baudrate +}; +static const struct Nif spi_set_format_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_spi_set_format +}; +static const struct Nif spi_write_blocking_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_spi_write_blocking +}; +static const struct Nif spi_read_blocking_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_spi_read_blocking +}; +static const struct Nif spi_write_read_blocking_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_spi_write_read_blocking +}; +static const struct Nif spi_set_slave_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_spi_set_slave +}; +static const struct Nif spi_is_writable_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_spi_is_writable +}; +static const struct Nif spi_is_readable_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_spi_is_readable +}; +static const struct Nif spi_is_busy_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_spi_is_busy +}; +static const struct Nif spi_get_baudrate_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_spi_get_baudrate +}; +static const struct Nif spi_write16_blocking_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_spi_write16_blocking +}; +static const struct Nif spi_read16_blocking_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_spi_read16_blocking +}; +static const struct Nif spi_write_read16_blocking_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_spi_write_read16_blocking +}; + +static void spi_nif_init(GlobalContext *global) +{ + ErlNifEnv env; + erl_nif_env_partial_init_from_globalcontext(&env, global); + spi_resource_type = enif_init_resource_type(&env, "spi_resource", &SPIResourceTypeInit, ERL_NIF_RT_CREATE, NULL); +} + +static const struct Nif *spi_nif_get_nif(const char *nifname) +{ + if (strncmp("spi:", nifname, 4) != 0) { + return NULL; + } + const char *rest = nifname + 4; + if (strcmp("init/2", rest) == 0) { + TRACE("Resolved spi nif %s ...\n", nifname); + return &spi_init_nif; + } + if (strcmp("deinit/1", rest) == 0) { + TRACE("Resolved spi nif %s ...\n", nifname); + return &spi_deinit_nif; + } + if (strcmp("set_baudrate/2", rest) == 0) { + TRACE("Resolved spi nif %s ...\n", nifname); + return &spi_set_baudrate_nif; + } + if (strcmp("set_format/4", rest) == 0) { + TRACE("Resolved spi nif %s ...\n", nifname); + return &spi_set_format_nif; + } + if (strcmp("write_blocking/2", rest) == 0) { + TRACE("Resolved spi nif %s ...\n", nifname); + return &spi_write_blocking_nif; + } + if (strcmp("read_blocking/3", rest) == 0) { + TRACE("Resolved spi nif %s ...\n", nifname); + return &spi_read_blocking_nif; + } + if (strcmp("write_read_blocking/2", rest) == 0) { + TRACE("Resolved spi nif %s ...\n", nifname); + return &spi_write_read_blocking_nif; + } + if (strcmp("set_slave/2", rest) == 0) { + TRACE("Resolved spi nif %s ...\n", nifname); + return &spi_set_slave_nif; + } + if (strcmp("is_writable/1", rest) == 0) { + TRACE("Resolved spi nif %s ...\n", nifname); + return &spi_is_writable_nif; + } + if (strcmp("is_readable/1", rest) == 0) { + TRACE("Resolved spi nif %s ...\n", nifname); + return &spi_is_readable_nif; + } + if (strcmp("is_busy/1", rest) == 0) { + TRACE("Resolved spi nif %s ...\n", nifname); + return &spi_is_busy_nif; + } + if (strcmp("get_baudrate/1", rest) == 0) { + TRACE("Resolved spi nif %s ...\n", nifname); + return &spi_get_baudrate_nif; + } + if (strcmp("write16_blocking/2", rest) == 0) { + TRACE("Resolved spi nif %s ...\n", nifname); + return &spi_write16_blocking_nif; + } + if (strcmp("read16_blocking/3", rest) == 0) { + TRACE("Resolved spi nif %s ...\n", nifname); + return &spi_read16_blocking_nif; + } + if (strcmp("write_read16_blocking/2", rest) == 0) { + TRACE("Resolved spi nif %s ...\n", nifname); + return &spi_write_read16_blocking_nif; + } + return NULL; +} + +REGISTER_NIF_COLLECTION(spi, spi_nif_init, NULL, spi_nif_get_nif) diff --git a/src/platforms/stm32/cmake/stm32_hal_conf.h.in b/src/platforms/stm32/cmake/stm32_hal_conf.h.in index 22fd0e2b6f..4f1773d2de 100644 --- a/src/platforms/stm32/cmake/stm32_hal_conf.h.in +++ b/src/platforms/stm32/cmake/stm32_hal_conf.h.in @@ -40,6 +40,7 @@ extern "C" { #define HAL_EXTI_MODULE_ENABLED #define HAL_DMA_MODULE_ENABLED #define HAL_I2C_MODULE_ENABLED +#define HAL_SPI_MODULE_ENABLED #if defined(STM32H5XX) || defined(STM32L5XX) || defined(STM32U3XX) || defined(STM32U5XX) #define HAL_ICACHE_MODULE_ENABLED #endif @@ -199,6 +200,10 @@ extern "C" { #include "@STM32_FAMILY@_hal_i2c.h" #endif +#ifdef HAL_SPI_MODULE_ENABLED +#include "@STM32_FAMILY@_hal_spi.h" +#endif + #ifdef HAL_ICACHE_MODULE_ENABLED #include "@STM32_FAMILY@_hal_icache.h" #endif diff --git a/src/platforms/stm32/src/CMakeLists.txt b/src/platforms/stm32/src/CMakeLists.txt index 9f96271202..ebfbcc4632 100644 --- a/src/platforms/stm32/src/CMakeLists.txt +++ b/src/platforms/stm32/src/CMakeLists.txt @@ -52,6 +52,14 @@ if (EXISTS "${HAL_DRIVER_SRC_DIR}/${STM32_FAMILY}_hal_i2c_ex.c") list(APPEND HAL_SOURCES ${HAL_DRIVER_SRC_DIR}/${STM32_FAMILY}_hal_i2c_ex.c) endif() +# SPI HAL +list(APPEND HAL_SOURCES + ${HAL_DRIVER_SRC_DIR}/${STM32_FAMILY}_hal_spi.c +) +if (EXISTS "${HAL_DRIVER_SRC_DIR}/${STM32_FAMILY}_hal_spi_ex.c") + list(APPEND HAL_SOURCES ${HAL_DRIVER_SRC_DIR}/${STM32_FAMILY}_hal_spi_ex.c) +endif() + # ICACHE HAL for families that have it (U5, H5) if (EXISTS "${HAL_DRIVER_SRC_DIR}/${STM32_FAMILY}_hal_icache.c") list(APPEND HAL_SOURCES ${HAL_DRIVER_SRC_DIR}/${STM32_FAMILY}_hal_icache.c) diff --git a/src/platforms/stm32/src/lib/CMakeLists.txt b/src/platforms/stm32/src/lib/CMakeLists.txt index f800e9d23c..bae8bc885a 100644 --- a/src/platforms/stm32/src/lib/CMakeLists.txt +++ b/src/platforms/stm32/src/lib/CMakeLists.txt @@ -34,6 +34,7 @@ set(HEADER_FILES set(SOURCE_FILES gpio_driver.c i2c_driver.c + spi_driver.c jit_stream_flash.c platform_nifs.c sys.c diff --git a/src/platforms/stm32/src/lib/gpio_driver.c b/src/platforms/stm32/src/lib/gpio_driver.c index 0ca541dd52..7055388573 100644 --- a/src/platforms/stm32/src/lib/gpio_driver.c +++ b/src/platforms/stm32/src/lib/gpio_driver.c @@ -384,6 +384,7 @@ static term setup_gpio_pin(Context *ctx, term gpio_pin_tuple, term mode_term) bool setup_output = false; uint32_t pull_up_down; uint32_t output_speed; + uint32_t alternate = 0; term mhz_atom = term_invalid_term(); term pull_atom = term_invalid_term(); if (term_is_tuple(mode_term)) { @@ -403,30 +404,35 @@ static term setup_gpio_pin(Context *ctx, term gpio_pin_tuple, term mode_term) setup_output = true; } - pull_atom = term_get_tuple_element(mode_term, 1); - if (UNLIKELY(!term_is_atom(pull_atom))) { - AVM_LOGE(TAG, "GPIO pull direction must be one of the following atoms: up | down | floating"); - return error_tuple_str_maybe_gc(ctx, invalid_pull_atom); - } - - pull_up_down = ((uint32_t) interop_atom_term_select_int(pull_mode_table, pull_atom, ctx->global)); - if ((setup_output) && (term_get_tuple_arity(mode_term) == 3)) { - mhz_atom = term_get_tuple_element(mode_term, 2); - if (UNLIKELY(!term_is_atom(mhz_atom))) { - AVM_LOGE(TAG, "GPIO output speed must be one of the following atoms: mhz_2 | mhz_25 | mhz_50 | mhz_100"); - error_tuple_str_maybe_gc(ctx, invalid_rate_atom); + if (gpio_mode == GPIO_MODE_AF_PP && term_is_integer(term_get_tuple_element(mode_term, 1))) { + alternate = (uint32_t) term_to_int(term_get_tuple_element(mode_term, 1)); + pull_up_down = GPIO_NOPULL; + } else { + pull_atom = term_get_tuple_element(mode_term, 1); + if (UNLIKELY(!term_is_atom(pull_atom))) { + AVM_LOGE(TAG, "GPIO pull direction must be one of the following atoms: up | down | floating"); + return error_tuple_str_maybe_gc(ctx, invalid_pull_atom); } - output_speed = (uint32_t) interop_atom_term_select_int(output_mhz_table, mhz_atom, ctx->global); - if (output_speed == INVALID_GPIO_OSPEED) { + pull_up_down = ((uint32_t) interop_atom_term_select_int(pull_mode_table, pull_atom, ctx->global)); + if ((setup_output) && (term_get_tuple_arity(mode_term) == 3)) { + mhz_atom = term_get_tuple_element(mode_term, 2); + if (UNLIKELY(!term_is_atom(mhz_atom))) { + AVM_LOGE(TAG, "GPIO output speed must be one of the following atoms: mhz_2 | mhz_25 | mhz_50 | mhz_100"); + error_tuple_str_maybe_gc(ctx, invalid_rate_atom); + } + + output_speed = (uint32_t) interop_atom_term_select_int(output_mhz_table, mhz_atom, ctx->global); + if (output_speed == INVALID_GPIO_OSPEED) { + output_speed = GPIO_SPEED_FREQ_LOW; + char *mhz_string = interop_atom_to_string(ctx, mhz_atom); + AVM_LOGW(TAG, "Invalid output speed '%s' given, falling back to 2 Mhz default.", mhz_string); + free(mhz_string); + } + } else if (setup_output) { output_speed = GPIO_SPEED_FREQ_LOW; - char *mhz_string = interop_atom_to_string(ctx, mhz_atom); - AVM_LOGW(TAG, "Invalid output speed '%s' given, falling back to 2 Mhz default.", mhz_string); - free(mhz_string); + AVM_LOGW(TAG, "No output speed given, falling back to 2 Mhz default."); } - } else if (setup_output) { - output_speed = GPIO_SPEED_FREQ_LOW; - AVM_LOGW(TAG, "No output speed given, falling back to 2 Mhz default."); } } else { mode_atom = mode_term; @@ -460,7 +466,8 @@ static term setup_gpio_pin(Context *ctx, term gpio_pin_tuple, term mode_term) AVM_LOGD(TAG, "Setup: Pin bank 0x%08lX pin bitmask 0x%04X input mode 0x%02lX, pull mode 0x%02lX", gpio_bank, gpio_pin_mask, gpio_mode, pull_up_down); } else if (gpio_mode == GPIO_MODE_AF_PP) { gpio_init.Speed = GPIO_SPEED_FREQ_HIGH; - AVM_LOGD(TAG, "Setup: Pin bank 0x%08lX pin bitmask 0x%04X AF mode", gpio_bank, gpio_pin_mask); + gpio_init.Alternate = alternate; + AVM_LOGD(TAG, "Setup: Pin bank 0x%08lX pin bitmask 0x%04X AF mode, alternate %lu", gpio_bank, gpio_pin_mask, alternate); } else if (gpio_mode == GPIO_MODE_ANALOG) { AVM_LOGD(TAG, "Setup: Pin bank 0x%08lX pin bitmask 0x%04X analog mode", gpio_bank, gpio_pin_mask); } diff --git a/src/platforms/stm32/src/lib/spi_driver.c b/src/platforms/stm32/src/lib/spi_driver.c new file mode 100644 index 0000000000..022d20c6fb --- /dev/null +++ b/src/platforms/stm32/src/lib/spi_driver.c @@ -0,0 +1,680 @@ +/* + * 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 + +// #define ENABLE_TRACE +#include + +#include "avm_log.h" +#include "stm_sys.h" + +#define TAG "spi_driver" + +static ErlNifResourceType *spi_resource_type; + +struct SPIResource +{ + SPI_HandleTypeDef handle; + int peripheral; +}; + +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_spi_resource(Context *ctx, term resource_term, struct SPIResource **rsrc_obj) +{ + void *rsrc_obj_ptr; + if (UNLIKELY(!enif_get_resource(erl_nif_env_from_context(ctx), resource_term, spi_resource_type, &rsrc_obj_ptr))) { + return false; + } + *rsrc_obj = (struct SPIResource *) 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 SPI_TypeDef *peripheral_to_instance(int peripheral) +{ + switch (peripheral) { +#ifdef SPI1 + case 1: + return SPI1; +#endif +#ifdef SPI2 + case 2: + return SPI2; +#endif +#ifdef SPI3 + case 3: + return SPI3; +#endif +#ifdef SPI4 + case 4: + return SPI4; +#endif +#ifdef SPI5 + case 5: + return SPI5; +#endif +#ifdef SPI6 + case 6: + return SPI6; +#endif + default: + return NULL; + } +} + +static void enable_spi_clock(int peripheral) +{ + switch (peripheral) { +#ifdef SPI1 + case 1: + __HAL_RCC_SPI1_CLK_ENABLE(); + break; +#endif +#ifdef SPI2 + case 2: + __HAL_RCC_SPI2_CLK_ENABLE(); + break; +#endif +#ifdef SPI3 + case 3: + __HAL_RCC_SPI3_CLK_ENABLE(); + break; +#endif +#ifdef SPI4 + case 4: + __HAL_RCC_SPI4_CLK_ENABLE(); + break; +#endif +#ifdef SPI5 + case 5: + __HAL_RCC_SPI5_CLK_ENABLE(); + break; +#endif +#ifdef SPI6 + case 6: + __HAL_RCC_SPI6_CLK_ENABLE(); + break; +#endif + default: + break; + } +} + +/* SPI2 and SPI3 are on APB1; other SPI peripherals are on APB2. + * STM32G0 has a single APB bus (PCLK1). */ +static uint32_t get_spi_apb_freq(int peripheral) +{ + (void) peripheral; +#if defined(STM32G0XX) + return HAL_RCC_GetPCLK1Freq(); +#else + switch (peripheral) { + case 2: + case 3: + return HAL_RCC_GetPCLK1Freq(); + default: + return HAL_RCC_GetPCLK2Freq(); + } +#endif +} + +static const uint32_t spi_prescaler_table[] = { + SPI_BAUDRATEPRESCALER_2, + SPI_BAUDRATEPRESCALER_4, + SPI_BAUDRATEPRESCALER_8, + SPI_BAUDRATEPRESCALER_16, + SPI_BAUDRATEPRESCALER_32, + SPI_BAUDRATEPRESCALER_64, + SPI_BAUDRATEPRESCALER_128, + SPI_BAUDRATEPRESCALER_256 +}; + +static int compute_spi_prescaler_index(uint32_t apb_freq, uint32_t target_baudrate) +{ + for (int i = 0; i < 8; i++) { + uint32_t divider = 2U << i; + if (apb_freq / divider <= target_baudrate) { + return i; + } + } + return 7; +} + +static term nif_spi_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]); + uint32_t baudrate = (uint32_t) term_to_int(argv[1]); + + SPI_TypeDef *instance = peripheral_to_instance(peripheral); + if (IS_NULL_PTR(instance)) { + AVM_LOGE(TAG, "Invalid SPI peripheral: %d", peripheral); + RAISE_ERROR(BADARG_ATOM); + } + + enable_spi_clock(peripheral); + + struct SPIResource *rsrc_obj = enif_alloc_resource(spi_resource_type, sizeof(struct SPIResource)); + if (IS_NULL_PTR(rsrc_obj)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + + memset(&rsrc_obj->handle, 0, sizeof(SPI_HandleTypeDef)); + rsrc_obj->handle.Instance = instance; + rsrc_obj->peripheral = peripheral; + + uint32_t apb_freq = get_spi_apb_freq(peripheral); + int presc_idx = compute_spi_prescaler_index(apb_freq, baudrate); + uint32_t actual_baudrate = apb_freq / (2U << presc_idx); + + rsrc_obj->handle.Init.Mode = SPI_MODE_MASTER; + rsrc_obj->handle.Init.Direction = SPI_DIRECTION_2LINES; + rsrc_obj->handle.Init.DataSize = SPI_DATASIZE_8BIT; + rsrc_obj->handle.Init.CLKPolarity = SPI_POLARITY_LOW; + rsrc_obj->handle.Init.CLKPhase = SPI_PHASE_1EDGE; + rsrc_obj->handle.Init.NSS = SPI_NSS_SOFT; + rsrc_obj->handle.Init.BaudRatePrescaler = spi_prescaler_table[presc_idx]; + rsrc_obj->handle.Init.FirstBit = SPI_FIRSTBIT_MSB; + rsrc_obj->handle.Init.TIMode = SPI_TIMODE_DISABLE; + rsrc_obj->handle.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE; + rsrc_obj->handle.Init.CRCPolynomial = 7; + + HAL_StatusTypeDef status = HAL_SPI_Init(&rsrc_obj->handle); + if (status != HAL_OK) { + enif_release_resource(rsrc_obj); + AVM_LOGE(TAG, "HAL_SPI_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(ctx->global, ATOM_STR("\x8", "spi_init"))); + } + + if (UNLIKELY(memory_ensure_free(ctx, TERM_BOXED_RESOURCE_SIZE) != MEMORY_GC_OK)) { + HAL_SPI_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); + + 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_int(actual_baudrate)); + term_put_tuple_element(inner, 1, obj); + + return create_pair(ctx, OK_ATOM, inner); +} + +static term nif_spi_deinit(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + struct SPIResource *rsrc_obj; + if (UNLIKELY(!get_spi_resource(ctx, argv[0], &rsrc_obj))) { + RAISE_ERROR(BADARG_ATOM); + } + + if (IS_NULL_PTR(rsrc_obj->handle.Instance)) { + return OK_ATOM; + } + HAL_SPI_DeInit(&rsrc_obj->handle); + rsrc_obj->handle.Instance = NULL; + + return OK_ATOM; +} + +static term nif_spi_set_baudrate(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + struct SPIResource *rsrc_obj; + if (UNLIKELY(!get_spi_resource(ctx, argv[0], &rsrc_obj))) { + RAISE_ERROR(BADARG_ATOM); + } + VALIDATE_VALUE(argv[1], term_is_integer); + + uint32_t baudrate = (uint32_t) term_to_int(argv[1]); + uint32_t apb_freq = get_spi_apb_freq(rsrc_obj->peripheral); + int presc_idx = compute_spi_prescaler_index(apb_freq, baudrate); + uint32_t actual = apb_freq / (2U << presc_idx); + + HAL_SPI_DeInit(&rsrc_obj->handle); + rsrc_obj->handle.Init.BaudRatePrescaler = spi_prescaler_table[presc_idx]; + HAL_StatusTypeDef status = HAL_SPI_Init(&rsrc_obj->handle); + if (UNLIKELY(status != HAL_OK)) { + AVM_LOGE(TAG, "HAL_SPI_Init failed in set_baudrate: %d", (int) status); + 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); + } + + 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_int(actual)); +} + +static term nif_spi_set_format(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + struct SPIResource *rsrc_obj; + if (UNLIKELY(!get_spi_resource(ctx, argv[0], &rsrc_obj))) { + RAISE_ERROR(BADARG_ATOM); + } + VALIDATE_VALUE(argv[1], term_is_integer); + VALIDATE_VALUE(argv[2], term_is_integer); + VALIDATE_VALUE(argv[3], term_is_integer); + + uint32_t data_bits = (uint32_t) term_to_int(argv[1]); + uint32_t datasize; + switch (data_bits) { + case 8: + datasize = SPI_DATASIZE_8BIT; + break; + case 16: + datasize = SPI_DATASIZE_16BIT; + break; + default: + RAISE_ERROR(BADARG_ATOM); + } + + uint32_t cpol = (term_to_int(argv[2]) == 0) ? SPI_POLARITY_LOW : SPI_POLARITY_HIGH; + uint32_t cpha = (term_to_int(argv[3]) == 0) ? SPI_PHASE_1EDGE : SPI_PHASE_2EDGE; + + HAL_SPI_DeInit(&rsrc_obj->handle); + rsrc_obj->handle.Init.DataSize = datasize; + rsrc_obj->handle.Init.CLKPolarity = cpol; + rsrc_obj->handle.Init.CLKPhase = cpha; + HAL_StatusTypeDef status = HAL_SPI_Init(&rsrc_obj->handle); + if (UNLIKELY(status != HAL_OK)) { + AVM_LOGE(TAG, "HAL_SPI_Init failed in set_format: %d", (int) status); + 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_spi_transmit(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + struct SPIResource *rsrc_obj; + if (UNLIKELY(!get_spi_resource(ctx, argv[0], &rsrc_obj))) { + 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_SPI_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_spi_receive(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + struct SPIResource *rsrc_obj; + if (UNLIKELY(!get_spi_resource(ctx, argv[0], &rsrc_obj))) { + 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_SPI_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_spi_transmit_receive(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + struct SPIResource *rsrc_obj; + if (UNLIKELY(!get_spi_resource(ctx, argv[0], &rsrc_obj))) { + RAISE_ERROR(BADARG_ATOM); + } + VALIDATE_VALUE(argv[1], term_is_binary); + + const uint8_t *src = (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); + } + + if (UNLIKELY(memory_ensure_free_opt(ctx, TUPLE_SIZE(2) + term_binary_heap_size(len), MEMORY_NO_GC) != MEMORY_GC_OK)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + term data = term_create_uninitialized_binary(len, &ctx->heap, ctx->global); + uint8_t *dst = (uint8_t *) term_binary_data(data); + + HAL_StatusTypeDef status = HAL_SPI_TransmitReceive(&rsrc_obj->handle, (uint8_t *) src, dst, (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 create_pair(ctx, OK_ATOM, data); +} + +static term nif_spi_abort(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + struct SPIResource *rsrc_obj; + if (UNLIKELY(!get_spi_resource(ctx, argv[0], &rsrc_obj))) { + RAISE_ERROR(BADARG_ATOM); + } + + HAL_StatusTypeDef status = HAL_SPI_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_spi_get_state(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + struct SPIResource *rsrc_obj; + if (UNLIKELY(!get_spi_resource(ctx, argv[0], &rsrc_obj))) { + RAISE_ERROR(BADARG_ATOM); + } + + HAL_SPI_StateTypeDef state = HAL_SPI_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_SPI_STATE_READY: + state_str = ready_str; + break; + case HAL_SPI_STATE_BUSY: + state_str = busy_str; + break; + case HAL_SPI_STATE_BUSY_TX: + state_str = busy_tx_str; + break; + case HAL_SPI_STATE_BUSY_RX: + state_str = busy_rx_str; + break; + case HAL_SPI_STATE_BUSY_TX_RX: + state_str = busy_tx_rx_str; + break; + case HAL_SPI_STATE_ERROR: + return ERROR_ATOM; + case HAL_SPI_STATE_ABORT: + return globalcontext_make_atom(ctx->global, ATOM_STR("\x5", "abort")); + default: + state_str = reset_str; + break; + } + + return globalcontext_make_atom(ctx->global, state_str); +} + +static term nif_spi_get_error(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + struct SPIResource *rsrc_obj; + if (UNLIKELY(!get_spi_resource(ctx, argv[0], &rsrc_obj))) { + RAISE_ERROR(BADARG_ATOM); + } + + uint32_t err = HAL_SPI_GetError(&rsrc_obj->handle); + + return term_from_int((int) err); +} + +static void spi_resource_dtor(ErlNifEnv *caller_env, void *obj) +{ + UNUSED(caller_env); + struct SPIResource *rsrc_obj = (struct SPIResource *) obj; + if (!IS_NULL_PTR(rsrc_obj->handle.Instance)) { + HAL_SPI_DeInit(&rsrc_obj->handle); + rsrc_obj->handle.Instance = NULL; + } +} + +static const ErlNifResourceTypeInit SPIResourceTypeInit = { + .members = 1, + .dtor = spi_resource_dtor, +}; + +// +// NIF structs +// +static const struct Nif spi_init_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_spi_init +}; +static const struct Nif spi_deinit_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_spi_deinit +}; +static const struct Nif spi_set_baudrate_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_spi_set_baudrate +}; +static const struct Nif spi_set_format_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_spi_set_format +}; +static const struct Nif spi_transmit_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_spi_transmit +}; +static const struct Nif spi_receive_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_spi_receive +}; +static const struct Nif spi_transmit_receive_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_spi_transmit_receive +}; +static const struct Nif spi_abort_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_spi_abort +}; +static const struct Nif spi_get_state_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_spi_get_state +}; +static const struct Nif spi_get_error_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_spi_get_error +}; + +static void spi_nif_init(GlobalContext *global) +{ + ErlNifEnv env; + erl_nif_env_partial_init_from_globalcontext(&env, global); + spi_resource_type = enif_init_resource_type(&env, "spi_resource", &SPIResourceTypeInit, ERL_NIF_RT_CREATE, NULL); +} + +static const struct Nif *spi_nif_get_nif(const char *nifname) +{ + if (strncmp("spi:", nifname, 4) != 0) { + return NULL; + } + const char *rest = nifname + 4; + if (strcmp("init/2", rest) == 0) { + TRACE("Resolved spi nif %s ...\n", nifname); + return &spi_init_nif; + } + if (strcmp("deinit/1", rest) == 0) { + TRACE("Resolved spi nif %s ...\n", nifname); + return &spi_deinit_nif; + } + if (strcmp("set_baudrate/2", rest) == 0) { + TRACE("Resolved spi nif %s ...\n", nifname); + return &spi_set_baudrate_nif; + } + if (strcmp("set_format/4", rest) == 0) { + TRACE("Resolved spi nif %s ...\n", nifname); + return &spi_set_format_nif; + } + if (strcmp("transmit/3", rest) == 0) { + TRACE("Resolved spi nif %s ...\n", nifname); + return &spi_transmit_nif; + } + if (strcmp("receive_/3", rest) == 0) { + TRACE("Resolved spi nif %s ...\n", nifname); + return &spi_receive_nif; + } + if (strcmp("transmit_receive/3", rest) == 0) { + TRACE("Resolved spi nif %s ...\n", nifname); + return &spi_transmit_receive_nif; + } + if (strcmp("abort/1", rest) == 0) { + TRACE("Resolved spi nif %s ...\n", nifname); + return &spi_abort_nif; + } + if (strcmp("get_state/1", rest) == 0) { + TRACE("Resolved spi nif %s ...\n", nifname); + return &spi_get_state_nif; + } + if (strcmp("get_error/1", rest) == 0) { + TRACE("Resolved spi nif %s ...\n", nifname); + return &spi_get_error_nif; + } + return NULL; +} + +REGISTER_NIF_COLLECTION(spi, spi_nif_init, NULL, spi_nif_get_nif) diff --git a/src/platforms/stm32/tests/renode/stm32_spi_test.robot b/src/platforms/stm32/tests/renode/stm32_spi_test.robot new file mode 100644 index 0000000000..5c820a9628 --- /dev/null +++ b/src/platforms/stm32/tests/renode/stm32_spi_test.robot @@ -0,0 +1,43 @@ +# +# 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 +${PLATFORM} @platforms/cpus/stm32f4.repl +${ELF} REQUIRED +${AVM} REQUIRED +${AVM_ADDRESS} 0x08060000 + +*** Test Cases *** +AtomVM Should Communicate Over SPI + Execute Command mach create + Execute Command machine LoadPlatformDescription ${PLATFORM} + Execute Command machine LoadPlatformDescriptionFromString "spiFlashMemory: Memory.MappedMemory { size: 0x100000 }; spiFlash: SPI.Micron_MT25Q @ spi1 { underlyingMemory: spiFlashMemory }" + Execute Command sysbus LoadELF ${ELF} + Execute Command sysbus LoadBinary ${AVM} ${AVM_ADDRESS} + Create Terminal Tester ${UART} + Start Emulation + Wait For Line On Uart spi_done timeout=60 diff --git a/src/platforms/stm32/tests/renode/stm32h743.repl b/src/platforms/stm32/tests/renode/stm32h743.repl index dd9aa9abc4..c39d1d96de 100644 --- a/src/platforms/stm32/tests/renode/stm32h743.repl +++ b/src/platforms/stm32/tests/renode/stm32h743.repl @@ -111,6 +111,9 @@ elif request.IsRead: request.Value = regs.get(request.Offset, 0) ''' +spi1: SPI.STM32H7_SPI @ sysbus 0x40013000 + IRQ -> nvic@35 + gpioPortA: GPIOPort.STM32_GPIOPort @ sysbus <0x58020000, +0x400> modeResetValue: 0xABFFFFFF numberOfAFs: 16 diff --git a/src/platforms/stm32/tests/test_erl_sources/CMakeLists.txt b/src/platforms/stm32/tests/test_erl_sources/CMakeLists.txt index dce1a05adc..93ecbc65b6 100644 --- a/src/platforms/stm32/tests/test_erl_sources/CMakeLists.txt +++ b/src/platforms/stm32/tests/test_erl_sources/CMakeLists.txt @@ -23,3 +23,4 @@ include(BuildErlang) 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) diff --git a/src/platforms/stm32/tests/test_erl_sources/test_spi.erl b/src/platforms/stm32/tests/test_erl_sources/test_spi.erl new file mode 100644 index 0000000000..e0180e8631 --- /dev/null +++ b/src/platforms/stm32/tests/test_erl_sources/test_spi.erl @@ -0,0 +1,39 @@ +% +% 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_spi). +-export([start/0]). + +start() -> + {ok, {_Baudrate, SPI}} = spi:init(1, 1000000), + 1 = spi:transmit(SPI, <<16#9F>>, 1000), + {ok, RxData} = spi:receive_(SPI, 3, 1000), + 3 = byte_size(RxData), + {ok, TxRxData} = spi:transmit_receive(SPI, <<16#9F, 0, 0, 0>>, 1000), + 4 = byte_size(TxRxData), + {ok, ActualBaud} = spi:set_baudrate(SPI, 500000), + true = is_integer(ActualBaud) andalso ActualBaud > 0, + ok = spi:set_format(SPI, 8, 0, 0), + ready = spi:get_state(SPI), + 0 = spi:get_error(SPI), + ok = spi:abort(SPI), + ok = spi:deinit(SPI), + ok = spi:deinit(SPI), + erlang:display(spi_done).