diff --git a/.github/workflows/build-deploy-docs.yml b/.github/workflows/build-deploy-docs.yml
index f2832f6db..7da684463 100644
--- a/.github/workflows/build-deploy-docs.yml
+++ b/.github/workflows/build-deploy-docs.yml
@@ -48,7 +48,7 @@ jobs:
- name: Build rustdoc docs
run: |
- cargo doc -p riot-rs --features bench,csprng,executor-thread,external-interrupts,hwrng,i2c,no-boards,random,threading,usb
+ cargo doc -p riot-rs --features bench,csprng,executor-thread,external-interrupts,hwrng,i2c,no-boards,random,spi,threading,usb
echo "" > target/doc/index.html
mkdir -p ./_site/dev/docs/api && mv target/doc/* ./_site/dev/docs/api
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 3c7fdb548..c40b7ddb1 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -116,19 +116,19 @@ jobs:
# TODO: we'll eventually want to enable relevant features
- name: Run crate tests
run: |
- cargo test --no-default-features --features external-interrupts,i2c,no-boards -p riot-rs -p riot-rs-embassy -p riot-rs-embassy-common -p riot-rs-runqueue -p riot-rs-threads -p riot-rs-macros
+ cargo test --no-default-features --features external-interrupts,i2c,no-boards,spi -p riot-rs -p riot-rs-embassy -p riot-rs-embassy-common -p riot-rs-runqueue -p riot-rs-threads -p riot-rs-macros
cargo test -p rbi -p ringbuffer -p coapcore
# We need to set `RUSTDOCFLAGS` as well in the following jobs, because it
# is used for doc tests.
- name: cargo test for RP
- run: RUSTDOCFLAGS='--cfg context="rp2040"' RUSTFLAGS='--cfg context="rp2040"' cargo test --features external-interrupts,i2c,embassy-rp/rp2040 -p riot-rs-rp
+ run: RUSTDOCFLAGS='--cfg context="rp2040"' RUSTFLAGS='--cfg context="rp2040"' cargo test --features external-interrupts,i2c,spi,embassy-rp/rp2040 -p riot-rs-rp
- name: cargo test for nRF
- run: RUSTDOCFLAGS='--cfg context="nrf52840"' RUSTFLAGS='--cfg context="nrf52840"' cargo test --features external-interrupts,i2c,'embassy-nrf/nrf52840' -p riot-rs-nrf
+ run: RUSTDOCFLAGS='--cfg context="nrf52840"' RUSTFLAGS='--cfg context="nrf52840"' cargo test --features external-interrupts,i2c,spi,'embassy-nrf/nrf52840' -p riot-rs-nrf
- name: cargo test for STM32
- run: RUSTDOCFLAGS='--cfg context="stm32wb55rgvx"' RUSTFLAGS='--cfg context="stm32wb55rgvx"' cargo test --features external-interrupts,i2c,'embassy-stm32/stm32wb55rg' -p riot-rs-stm32
+ run: RUSTDOCFLAGS='--cfg context="stm32wb55rgvx"' RUSTFLAGS='--cfg context="stm32wb55rgvx"' cargo test --features external-interrupts,i2c,spi,'embassy-stm32/stm32wb55rg' -p riot-rs-stm32
lint:
runs-on: ubuntu-latest
@@ -176,49 +176,49 @@ jobs:
- name: clippy
uses: clechasseur/rs-clippy-check@v3
with:
- args: --verbose --locked --features no-boards,external-interrupts -p riot-rs -p riot-rs-boards -p riot-rs-chips -p riot-rs-debug -p riot-rs-embassy -p riot-rs-embassy-common -p riot-rs-macros -p riot-rs-random -p riot-rs-rt -p riot-rs-threads -p riot-rs-utils -- --deny warnings
+ args: --verbose --locked --features no-boards,external-interrupts,spi -p riot-rs -p riot-rs-boards -p riot-rs-chips -p riot-rs-debug -p riot-rs-embassy -p riot-rs-embassy-common -p riot-rs-macros -p riot-rs-random -p riot-rs-rt -p riot-rs-threads -p riot-rs-utils -- --deny warnings
- run: echo 'RUSTFLAGS=--cfg context="esp32c6"' >> $GITHUB_ENV
- name: clippy for ESP32
uses: clechasseur/rs-clippy-check@v3
with:
- args: --verbose --locked --target=riscv32imac-unknown-none-elf --features external-interrupts,i2c,esp-hal/esp32c6,esp-hal-embassy/esp32c6 -p riot-rs-esp -- --deny warnings
+ args: --verbose --locked --target=riscv32imac-unknown-none-elf --features external-interrupts,i2c,spi,esp-hal/esp32c6,esp-hal-embassy/esp32c6 -p riot-rs-esp -- --deny warnings
- run: echo 'RUSTFLAGS=--cfg context="rp2040"' >> $GITHUB_ENV
- name: clippy for RP
uses: clechasseur/rs-clippy-check@v3
with:
- args: --verbose --locked --features external-interrupts,i2c,embassy-rp/rp2040 -p riot-rs-rp -- --deny warnings
+ args: --verbose --locked --features external-interrupts,i2c,spi,embassy-rp/rp2040 -p riot-rs-rp -- --deny warnings
- run: echo 'RUSTFLAGS=--cfg context="nrf52840"' >> $GITHUB_ENV
- name: clippy for nRF
uses: clechasseur/rs-clippy-check@v3
with:
- args: --verbose --locked --features external-interrupts,i2c,embassy-nrf/nrf52840 -p riot-rs-nrf -- --deny warnings
+ args: --verbose --locked --features external-interrupts,i2c,spi,embassy-nrf/nrf52840 -p riot-rs-nrf -- --deny warnings
- run: echo 'RUSTFLAGS=--cfg context="stm32wb55rgvx"' >> $GITHUB_ENV
- name: clippy for STM32
uses: clechasseur/rs-clippy-check@v3
with:
- args: --verbose --locked --features external-interrupts,i2c,embassy-stm32/stm32wb55rg -p riot-rs-stm32 -- --deny warnings
+ args: --verbose --locked --features external-interrupts,i2c,spi,embassy-stm32/stm32wb55rg -p riot-rs-stm32 -- --deny warnings
# Reset `RUSTFLAGS`
- run: echo 'RUSTFLAGS=' >> $GITHUB_ENV
- name: rustdoc
- run: RUSTDOCFLAGS='-D warnings' cargo doc -p riot-rs --features bench,csprng,executor-thread,external-interrupts,hwrng,i2c,no-boards,random,threading,usb
+ run: RUSTDOCFLAGS='-D warnings' cargo doc -p riot-rs --features bench,csprng,executor-thread,external-interrupts,hwrng,i2c,no-boards,random,spi,threading,usb
- name: rustdoc for ESP32
- run: RUSTDOCFLAGS='-D warnings --cfg context="esp32c6"' cargo doc --target=riscv32imac-unknown-none-elf --features external-interrupts,i2c,esp-hal/esp32c6,esp-hal-embassy/esp32c6 -p riot-rs-esp
+ run: RUSTDOCFLAGS='-D warnings --cfg context="esp32c6"' cargo doc --target=riscv32imac-unknown-none-elf --features external-interrupts,i2c,spi,esp-hal/esp32c6,esp-hal-embassy/esp32c6 -p riot-rs-esp
- name: rustdoc for RP
- run: RUSTDOCFLAGS='-D warnings --cfg context="rp2040"' cargo doc --features external-interrupts,i2c,embassy-rp/rp2040 -p riot-rs-rp
+ run: RUSTDOCFLAGS='-D warnings --cfg context="rp2040"' cargo doc --features external-interrupts,i2c,spi,embassy-rp/rp2040 -p riot-rs-rp
- name: rustdoc for nRF
- run: RUSTDOCFLAGS='-D warnings --cfg context="nrf52840"' cargo doc --features external-interrupts,i2c,embassy-nrf/nrf52840 -p riot-rs-nrf
+ run: RUSTDOCFLAGS='-D warnings --cfg context="nrf52840"' cargo doc --features external-interrupts,i2c,spi,embassy-nrf/nrf52840 -p riot-rs-nrf
- name: rustdoc for STM32
- run: RUSTDOCFLAGS='-D warnings --cfg context="stm32wb55rgvx"' cargo doc --features external-interrupts,i2c,embassy-stm32/stm32wb55rg -p riot-rs-stm32
+ run: RUSTDOCFLAGS='-D warnings --cfg context="stm32wb55rgvx"' cargo doc --features external-interrupts,i2c,spi,embassy-stm32/stm32wb55rg -p riot-rs-stm32
- name: rustfmt
run: cargo fmt --check --all
diff --git a/Cargo.lock b/Cargo.lock
index ee237e92e..6b6869644 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -3720,6 +3720,7 @@ version = "0.1.0"
dependencies = [
"cfg-if",
"defmt",
+ "embassy-embedded-hal",
"embassy-executor",
"embassy-time",
"embedded-hal 1.0.0",
@@ -3785,6 +3786,7 @@ dependencies = [
"cyw43",
"cyw43-pio",
"defmt",
+ "embassy-embedded-hal",
"embassy-executor",
"embassy-net-driver-channel",
"embassy-rp",
@@ -4162,6 +4164,32 @@ dependencies = [
"managed",
]
+[[package]]
+name = "spi-loopback"
+version = "0.1.0"
+dependencies = [
+ "embassy-executor",
+ "embassy-sync 0.6.0",
+ "embedded-hal-async",
+ "once_cell",
+ "riot-rs",
+ "riot-rs-boards",
+ "static_cell",
+]
+
+[[package]]
+name = "spi-main"
+version = "0.1.0"
+dependencies = [
+ "embassy-executor",
+ "embassy-sync 0.6.0",
+ "embedded-hal-async",
+ "once_cell",
+ "riot-rs",
+ "riot-rs-boards",
+ "static_cell",
+]
+
[[package]]
name = "spin"
version = "0.9.8"
diff --git a/Cargo.toml b/Cargo.toml
index 35c28f93c..c69916b0a 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -25,6 +25,8 @@ members = [
"tests/gpio-interrupt-nrf",
"tests/gpio-interrupt-stm32",
"tests/i2c-controller",
+ "tests/spi-loopback",
+ "tests/spi-main",
"tests/threading-dynamic-prios",
"tests/threading-lock",
]
diff --git a/book/src/support_matrix.html b/book/src/support_matrix.html
index 4b0c9df84..760e32242 100644
--- a/book/src/support_matrix.html
+++ b/book/src/support_matrix.html
@@ -4,7 +4,7 @@
Chip |
Testing Board |
- Functionality |
+ Functionality |
|
@@ -12,6 +12,7 @@
GPIO |
Debug Output |
I2C Controller Mode |
+ SPI Main Mode |
Logging |
User USB |
Wi-Fi |
@@ -27,6 +28,7 @@
✅ |
✅ |
✅ |
+ ✅ |
❌ |
☑️ |
❌ |
@@ -40,6 +42,7 @@
✅ |
✅ |
✅ |
+ ✅ |
– |
✅ |
✅ |
@@ -52,6 +55,7 @@
✅ |
✅ |
✅ |
+ ✅ |
– |
✅ |
✅ |
@@ -64,6 +68,7 @@
✅ |
✅ |
✅ |
+ ✅ |
– |
✅ |
✅ |
@@ -79,6 +84,7 @@
✅ |
✅ |
✅ |
+ ✅ |
STM32F401RETX |
@@ -86,6 +92,7 @@
✅ |
✅ |
❌ |
+ ❌ |
✅ |
– |
– |
@@ -100,6 +107,7 @@
✅ |
✅ |
✅ |
+ ✅ |
– |
❌ |
✅ |
@@ -112,6 +120,7 @@
✅ |
✅ |
✅ |
+ ✅ |
– |
✅ |
✅ |
diff --git a/clippy.toml b/clippy.toml
index 610a24382..03b57f019 100644
--- a/clippy.toml
+++ b/clippy.toml
@@ -1,2 +1,4 @@
# Require SAFETY docs, as well as a few other lints, for private items
check-private-items = true
+
+doc-valid-idents = ["MHz", "GHz", "THz", ".."]
diff --git a/doc/support_matrix.yml b/doc/support_matrix.yml
index a21da6a12..124801b3c 100644
--- a/doc/support_matrix.yml
+++ b/doc/support_matrix.yml
@@ -26,6 +26,9 @@ functionalities:
- name: i2c_controller
title: I2C Controller Mode
description: I2C in controller mode
+ - name: spi_main
+ title: SPI Main Mode
+ description: SPI in main mode
- name: logging
title: Logging
description:
@@ -51,6 +54,7 @@ chips:
debug_output: supported
hwrng: supported
i2c_controller: supported
+ spi_main: supported
logging: supported
wifi: not_available
@@ -61,6 +65,7 @@ chips:
debug_output: supported
hwrng: supported
i2c_controller: supported
+ spi_main: supported
logging: supported
wifi: not_available
@@ -71,6 +76,7 @@ chips:
debug_output: supported
hwrng: supported
i2c_controller: supported
+ spi_main: supported
logging: supported
wifi: not_available
@@ -81,6 +87,7 @@ chips:
debug_output: supported
hwrng: not_currently_supported
i2c_controller: supported
+ spi_main: supported
logging: supported
wifi: not_available
@@ -91,6 +98,7 @@ chips:
debug_output: supported
hwrng: not_available
i2c_controller: not_currently_supported
+ spi_main: not_currently_supported
logging: supported
wifi: not_available
@@ -101,6 +109,7 @@ chips:
debug_output: supported
hwrng: supported
i2c_controller: supported
+ spi_main: supported
logging: supported
wifi: not_available
@@ -111,6 +120,7 @@ chips:
debug_output: supported
hwrng: supported
i2c_controller: supported
+ spi_main: supported
logging: supported
wifi: not_available
diff --git a/src/riot-rs-embassy-common/Cargo.toml b/src/riot-rs-embassy-common/Cargo.toml
index 7058ee863..6afefbc6b 100644
--- a/src/riot-rs-embassy-common/Cargo.toml
+++ b/src/riot-rs-embassy-common/Cargo.toml
@@ -24,4 +24,7 @@ external-interrupts = []
## Enables I2C support.
i2c = ["dep:fugit"]
+## Enables SPI support.
+spi = ["dep:fugit"]
+
defmt = ["dep:defmt", "fugit?/defmt"]
diff --git a/src/riot-rs-embassy-common/src/lib.rs b/src/riot-rs-embassy-common/src/lib.rs
index 41002b5f4..819d32ada 100644
--- a/src/riot-rs-embassy-common/src/lib.rs
+++ b/src/riot-rs-embassy-common/src/lib.rs
@@ -13,6 +13,9 @@ pub mod executor_swi;
#[cfg(feature = "i2c")]
pub mod i2c;
+#[cfg(feature = "spi")]
+pub mod spi;
+
pub mod reexports {
//! Crate re-exports.
diff --git a/src/riot-rs-embassy-common/src/spi/main/mod.rs b/src/riot-rs-embassy-common/src/spi/main/mod.rs
new file mode 100644
index 000000000..7d83a5827
--- /dev/null
+++ b/src/riot-rs-embassy-common/src/spi/main/mod.rs
@@ -0,0 +1,147 @@
+//! Provides architecture-agnostic SPI-related types, for main mode.
+
+pub use fugit::KilohertzU32 as Kilohertz;
+
+// FIXME: rename this to Bitrate and use bps instead?
+/// SPI bus frequencies supported on all MCUs.
+#[derive(Copy, Clone)]
+pub enum Frequency {
+ /// 125 kHz.
+ _125k,
+ /// 250 kHz.
+ _250k,
+ /// 500 kHz.
+ _500k,
+ /// 1 MHz.
+ _1M,
+ /// 2 MHz.
+ _2M,
+ /// 4 MHz.
+ _4M,
+ /// 8 MHz.
+ _8M,
+}
+
+#[doc(hidden)]
+#[macro_export]
+macro_rules! impl_spi_from_frequency {
+ () => {
+ impl From for Frequency {
+ fn from(freq: riot_rs_embassy_common::spi::main::Frequency) -> Self {
+ match freq {
+ riot_rs_embassy_common::spi::main::Frequency::_125k => {
+ Self::F($crate::spi::main::Kilohertz::kHz(125))
+ }
+ riot_rs_embassy_common::spi::main::Frequency::_250k => {
+ Self::F($crate::spi::main::Kilohertz::kHz(250))
+ }
+ riot_rs_embassy_common::spi::main::Frequency::_500k => {
+ Self::F($crate::spi::main::Kilohertz::kHz(500))
+ }
+ riot_rs_embassy_common::spi::main::Frequency::_1M => {
+ Self::F($crate::spi::main::Kilohertz::MHz(1))
+ }
+ riot_rs_embassy_common::spi::main::Frequency::_2M => {
+ Self::F($crate::spi::main::Kilohertz::MHz(2))
+ }
+ riot_rs_embassy_common::spi::main::Frequency::_4M => {
+ Self::F($crate::spi::main::Kilohertz::MHz(4))
+ }
+ riot_rs_embassy_common::spi::main::Frequency::_8M => {
+ Self::F($crate::spi::main::Kilohertz::MHz(8))
+ }
+ }
+ }
+ }
+ };
+}
+
+#[doc(hidden)]
+#[macro_export]
+macro_rules! impl_spi_frequency_const_functions {
+ ($MAX_FREQUENCY:ident) => {
+ impl Frequency {
+ pub const fn first() -> Self {
+ Self::F(Kilohertz::kHz(1))
+ }
+
+ pub const fn last() -> Self {
+ Self::F(MAX_FREQUENCY)
+ }
+
+ pub const fn next(self) -> Option {
+ match self {
+ Self::F(kilohertz) => {
+ let khz = kilohertz.to_kHz();
+ if khz < MAX_FREQUENCY.to_kHz() {
+ Some(Self::F(Kilohertz::kHz(khz + 1)))
+ } else {
+ None
+ }
+ }
+ }
+ }
+
+ pub const fn prev(self) -> Option {
+ const MIN_FREQUENCY: Kilohertz = Kilohertz::kHz(1);
+
+ match self {
+ Self::F(kilohertz) => {
+ let khz = kilohertz.to_kHz();
+ if khz > MIN_FREQUENCY.to_kHz() {
+ Some(Self::F(Kilohertz::kHz(khz - 1)))
+ } else {
+ None
+ }
+ }
+ }
+ }
+
+ pub const fn khz(self) -> u32 {
+ match self {
+ Self::F(kilohertz) => kilohertz.to_kHz(),
+ }
+ }
+ }
+ };
+}
+
+#[doc(hidden)]
+#[macro_export]
+macro_rules! impl_async_spibus_for_driver_enum {
+ ($driver_enum:ident, $( $peripheral:ident ),*) => {
+ // The `SpiBus` trait represents exclusive ownership over the whole bus.
+ impl embedded_hal_async::spi::SpiBus for $driver_enum {
+ async fn read(&mut self, words: &mut [u8]) -> Result<(), Self::Error> {
+ match self {
+ $( Self::$peripheral(spi) => spi.spim.read(words).await, )*
+ }
+ }
+
+ async fn write(&mut self, data: &[u8]) -> Result<(), Self::Error> {
+ match self {
+ $( Self::$peripheral(spi) => spi.spim.write(data).await, )*
+ }
+ }
+
+ async fn transfer(&mut self, rx: &mut [u8], tx: &[u8]) -> Result<(), Self::Error> {
+ match self {
+ $( Self::$peripheral(spi) => spi.spim.transfer(rx, tx).await, )*
+ }
+ }
+
+ async fn transfer_in_place(&mut self, words: &mut [u8]) -> Result<(), Self::Error> {
+ match self {
+ $( Self::$peripheral(spi) => spi.spim.transfer_in_place(words).await, )*
+ }
+ }
+
+ async fn flush(&mut self) -> Result<(), Self::Error> {
+ use embedded_hal_async::spi::SpiBus;
+ match self {
+ $( Self::$peripheral(spi) => SpiBus::::flush(&mut spi.spim).await, )*
+ }
+ }
+ }
+ }
+}
diff --git a/src/riot-rs-embassy-common/src/spi/mod.rs b/src/riot-rs-embassy-common/src/spi/mod.rs
new file mode 100644
index 000000000..02c37ccec
--- /dev/null
+++ b/src/riot-rs-embassy-common/src/spi/mod.rs
@@ -0,0 +1,42 @@
+//! Provides architecture-agnostic SPI-related types.
+
+#[doc(alias = "master")]
+pub mod main;
+
+/// SPI mode.
+///
+/// - CPOL: Clock polarity.
+/// - CPHA: Clock phase.
+///
+/// See the [Wikipedia page for details](https://en.wikipedia.org/wiki/Serial_Peripheral_Interface#Mode_numbers).
+#[derive(Copy, Clone, PartialEq, Eq)]
+pub enum Mode {
+ /// CPOL = 0, CPHA = 0.
+ Mode0,
+ /// CPOL = 0, CPHA = 1.
+ Mode1,
+ /// CPOL = 1, CPHA = 0.
+ Mode2,
+ /// CPOL = 1, CPHA = 1.
+ Mode3,
+}
+
+// FIXME: should we offer configuring the bit order? (hiding from the docs for now)
+/// Order in which bits are transmitted.
+///
+/// Note: configuring the bit order is not supported on all architectures.
+// NOTE(arch): the RP2040 and RP2350 always send the MSb first
+#[doc(hidden)]
+#[derive(Copy, Clone, PartialEq, Eq)]
+pub enum BitOrder {
+ /// Most significant bit first.
+ MsbFirst,
+ /// Least significant bit first.
+ LsbFirst,
+}
+
+impl Default for BitOrder {
+ fn default() -> Self {
+ Self::MsbFirst
+ }
+}
diff --git a/src/riot-rs-embassy/Cargo.toml b/src/riot-rs-embassy/Cargo.toml
index fb7549971..9c7c70700 100644
--- a/src/riot-rs-embassy/Cargo.toml
+++ b/src/riot-rs-embassy/Cargo.toml
@@ -78,6 +78,15 @@ i2c = [
"riot-rs-rp/i2c",
"riot-rs-stm32/i2c",
]
+## Enables SPI support.
+spi = [
+ "dep:embassy-embedded-hal",
+ "riot-rs-embassy-common/spi",
+ "riot-rs-esp/spi",
+ "riot-rs-nrf/spi",
+ "riot-rs-rp/spi",
+ "riot-rs-stm32/spi",
+]
usb = [
"dep:embassy-usb",
"riot-rs-nrf/usb",
diff --git a/src/riot-rs-embassy/src/arch/mod.rs b/src/riot-rs-embassy/src/arch/mod.rs
index 32f55fa52..7ea286db3 100644
--- a/src/riot-rs-embassy/src/arch/mod.rs
+++ b/src/riot-rs-embassy/src/arch/mod.rs
@@ -15,6 +15,9 @@ pub mod hwrng;
#[cfg(feature = "i2c")]
pub mod i2c;
+#[cfg(feature = "spi")]
+pub mod spi;
+
#[cfg(feature = "usb")]
pub mod usb;
diff --git a/src/riot-rs-embassy/src/arch/spi/main/mod.rs b/src/riot-rs-embassy/src/arch/spi/main/mod.rs
new file mode 100644
index 000000000..2d1a25f49
--- /dev/null
+++ b/src/riot-rs-embassy/src/arch/spi/main/mod.rs
@@ -0,0 +1,31 @@
+//! Architecture- and MCU-specific types for SPI.
+//!
+//! This module provides a driver for each SPI peripheral, the driver name being the same as the
+//! peripheral; see the tests and examples to learn how to instantiate them.
+//! These driver instances are meant to be shared between tasks using
+//! [`SpiDevice`](crate::spi::main::SpiDevice).
+
+use riot_rs_embassy_common::spi::main::Kilohertz;
+
+const MAX_FREQUENCY: Kilohertz = Kilohertz::MHz(8);
+
+/// Peripheral-agnostic SPI driver implementing [`embedded_hal_async::spi::SpiBus`].
+///
+/// This type is not meant to be instantiated directly; instead instantiate a peripheral-specific
+/// driver provided by this module.
+// NOTE: we keep this type public because it may still required in user-written type signatures.
+pub enum Spi {
+ // Make the docs show that this enum has variants, but do not show any because they are
+ // MCU-specific.
+ #[doc(hidden)]
+ Hidden,
+}
+
+/// MCU-specific I2C bus frequency.
+#[derive(Debug, Copy, Clone, PartialEq, Eq)]
+pub enum Frequency {
+ #[doc(hidden)]
+ F(Kilohertz),
+}
+
+riot_rs_embassy_common::impl_spi_frequency_const_functions!(MAX_FREQUENCY);
diff --git a/src/riot-rs-embassy/src/arch/spi/mod.rs b/src/riot-rs-embassy/src/arch/spi/mod.rs
new file mode 100644
index 000000000..6a75b4ae3
--- /dev/null
+++ b/src/riot-rs-embassy/src/arch/spi/mod.rs
@@ -0,0 +1,8 @@
+#[doc(alias = "master")]
+pub mod main;
+
+use crate::arch;
+
+pub fn init(_peripherals: &mut arch::OptionalPeripherals) {
+ unimplemented!();
+}
diff --git a/src/riot-rs-embassy/src/lib.rs b/src/riot-rs-embassy/src/lib.rs
index 38a95ca95..1f85fea59 100644
--- a/src/riot-rs-embassy/src/lib.rs
+++ b/src/riot-rs-embassy/src/lib.rs
@@ -27,6 +27,9 @@ cfg_if::cfg_if! {
#[cfg(feature = "i2c")]
pub mod i2c;
+#[cfg(feature = "spi")]
+pub mod spi;
+
#[cfg(feature = "usb")]
pub mod usb;
@@ -47,6 +50,9 @@ pub mod api {
#[cfg(feature = "i2c")]
pub use crate::i2c;
+ #[cfg(feature = "spi")]
+ pub use crate::spi;
+
#[cfg(feature = "threading")]
pub use crate::blocker;
#[cfg(feature = "usb")]
@@ -179,6 +185,9 @@ async fn init_task(mut peripherals: arch::OptionalPeripherals) {
#[cfg(feature = "i2c")]
arch::i2c::init(&mut peripherals);
+ #[cfg(feature = "spi")]
+ arch::spi::init(&mut peripherals);
+
#[cfg(feature = "hwrng")]
arch::hwrng::construct_rng(&mut peripherals);
// Clock startup and entropy collection may lend themselves to parallelization, provided that
diff --git a/src/riot-rs-embassy/src/spi/main/mod.rs b/src/riot-rs-embassy/src/spi/main/mod.rs
new file mode 100644
index 000000000..c21a632f5
--- /dev/null
+++ b/src/riot-rs-embassy/src/spi/main/mod.rs
@@ -0,0 +1,120 @@
+//! Provides support for the SPI communication bus in main mode.
+
+use embassy_embedded_hal::shared_bus::asynch::spi::SpiDevice as InnerSpiDevice;
+use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
+
+use crate::{arch, gpio};
+
+pub use riot_rs_embassy_common::spi::main::*;
+
+/// An SPI driver implementing [`embedded_hal_async::spi::SpiDevice`].
+///
+/// Needs to be provided with an MCU-specific SPI driver tied to a specific SPI peripheral,
+/// obtainable from the [`arch::spi::main`] module.
+/// It also requires a [`gpio::Output`] for the chip select (CS) signal.
+///
+/// See [`embedded_hal::spi`] to learn more about the distinction between an
+/// [`SpiBus`](embedded_hal::spi::SpiBus) and an
+/// [`SpiDevice`](embedded_hal::spi::SpiDevice).
+///
+/// # Note
+///
+/// Despite the driver interface being `async`, it may block during operations.
+/// However, it cannot block indefinitely as a timeout is implemented, either by leveraging
+/// SPI-specific hardware capabilities or through a generic software timeout.
+// TODO: do we actually need a CriticalSectionRawMutex here?
+pub type SpiDevice =
+ InnerSpiDevice<'static, CriticalSectionRawMutex, arch::spi::main::Spi, gpio::Output>;
+
+/// Returns the highest SPI frequency available on the MCU that fits into the requested
+/// range.
+///
+/// # Examples
+///
+/// Assuming the architecture is only able to do up to 8 MHz:
+///
+/// ```
+/// # use riot_rs_embassy::{arch, spi::main::{highest_freq_in, Kilohertz}};
+/// let freq = const { highest_freq_in(Kilohertz::kHz(200)..=Kilohertz::MHz(16)) };
+/// assert_eq!(freq, arch::spi::main::Frequency::F(Kilohertz::MHz(8)));
+/// ```
+///
+/// # Panics
+///
+/// This function is only intended to be used in a `const` context.
+/// It panics if no suitable frequency can be found.
+pub const fn highest_freq_in(
+ range: core::ops::RangeInclusive,
+) -> arch::spi::main::Frequency {
+ let min = range.start().to_kHz();
+ let max = range.end().to_kHz();
+
+ assert!(max >= min);
+
+ let mut freq = arch::spi::main::Frequency::first();
+
+ loop {
+ // If not yet in the requested range
+ if freq.khz() < min {
+ if let Some(next) = freq.next() {
+ freq = next;
+ } else {
+ const_panic::concat_panic!(
+ "could not find a suitable SPI frequency: ",
+ min,
+ " kHz (minimum requested)",
+ " > ",
+ freq.khz(),
+ " kHz (highest available)"
+ );
+ }
+ } else {
+ break;
+ }
+ }
+
+ loop {
+ // If already outside of the requested range
+ if freq.khz() > max {
+ const_panic::concat_panic!(
+ "could not find a suitable SPI frequency: ",
+ max,
+ " kHz (maximum requested) < ",
+ freq.khz(),
+ " kHz (lowest available)"
+ );
+ } else if let Some(next) = freq.next() {
+ // The upper bound is inclusive.
+ if next.khz() <= max {
+ freq = next;
+ } else {
+ break;
+ }
+ } else {
+ break;
+ }
+ }
+
+ freq
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_valid_highest_freq_in() {
+ use arch::spi::main::Frequency;
+ use riot_rs_embassy_common::spi::main::Kilohertz;
+
+ const FREQ_0: Frequency = highest_freq_in(Kilohertz::kHz(50)..=Kilohertz::kHz(150));
+ const FREQ_1: Frequency = highest_freq_in(Kilohertz::kHz(100)..=Kilohertz::MHz(8));
+ const FREQ_2: Frequency = highest_freq_in(Kilohertz::MHz(8)..=Kilohertz::MHz(10));
+
+ assert_eq!(FREQ_0, Frequency::F(Kilohertz::kHz(150)));
+ assert_eq!(FREQ_1, Frequency::F(Kilohertz::MHz(8)));
+ assert_eq!(FREQ_2, Frequency::F(Kilohertz::MHz(8)));
+
+ // FIXME: add another test to check when max < min
+ }
+}
diff --git a/src/riot-rs-embassy/src/spi/mod.rs b/src/riot-rs-embassy/src/spi/mod.rs
new file mode 100644
index 000000000..23fe3e1b5
--- /dev/null
+++ b/src/riot-rs-embassy/src/spi/mod.rs
@@ -0,0 +1,7 @@
+//! Provides support for the SPI communication bus.
+#![deny(missing_docs)]
+
+#[doc(alias = "master")]
+pub mod main;
+
+pub use riot_rs_embassy_common::spi::*;
diff --git a/src/riot-rs-esp/Cargo.toml b/src/riot-rs-esp/Cargo.toml
index 3e6c31e78..d8b423097 100644
--- a/src/riot-rs-esp/Cargo.toml
+++ b/src/riot-rs-esp/Cargo.toml
@@ -12,12 +12,13 @@ workspace = true
[dependencies]
cfg-if = { workspace = true }
defmt = { workspace = true, optional = true }
+embassy-embedded-hal = { workspace = true }
embassy-executor = { workspace = true, default-features = false }
embassy-time = { workspace = true, optional = true }
embedded-hal = { workspace = true }
embedded-hal-async = { workspace = true }
-esp-hal = { workspace = true, default-features = false }
esp-alloc = { workspace = true, default-features = false, optional = true }
+esp-hal = { workspace = true, default-features = false }
esp-hal-embassy = { workspace = true, default-features = false }
esp-wifi = { workspace = true, default-features = false, features = [
"esp-alloc",
@@ -74,6 +75,9 @@ i2c = [
"embassy-executor/integrated-timers",
]
+## Enables SPI support.
+spi = ["dep:fugit", "riot-rs-embassy-common/spi"]
+
## Enables defmt support.
defmt = ["dep:defmt", "esp-wifi?/defmt", "fugit?/defmt"]
diff --git a/src/riot-rs-esp/src/lib.rs b/src/riot-rs-esp/src/lib.rs
index b0e444c30..67d42a4ac 100644
--- a/src/riot-rs-esp/src/lib.rs
+++ b/src/riot-rs-esp/src/lib.rs
@@ -8,6 +8,9 @@ pub mod gpio;
#[cfg(feature = "i2c")]
pub mod i2c;
+#[cfg(feature = "spi")]
+pub mod spi;
+
#[cfg(feature = "wifi")]
pub mod wifi;
diff --git a/src/riot-rs-esp/src/spi/main/mod.rs b/src/riot-rs-esp/src/spi/main/mod.rs
new file mode 100644
index 000000000..b45577a32
--- /dev/null
+++ b/src/riot-rs-esp/src/spi/main/mod.rs
@@ -0,0 +1,124 @@
+use embassy_embedded_hal::adapter::{BlockingAsync, YieldingAsync};
+use esp_hal::{
+ gpio::{self, InputPin, OutputPin},
+ peripheral::Peripheral,
+ peripherals,
+ spi::{master::Spi as InnerSpi, FullDuplexMode},
+};
+use riot_rs_embassy_common::{
+ impl_async_spibus_for_driver_enum,
+ spi::{main::Kilohertz, BitOrder, Mode},
+};
+
+// TODO: we could consider making this `pub`
+// NOTE(arch): values from the datasheets.
+#[cfg(any(context = "esp32c3", context = "esp32c6"))]
+const MAX_FREQUENCY: Kilohertz = Kilohertz::MHz(80);
+
+#[derive(Clone)]
+#[non_exhaustive]
+pub struct Config {
+ pub frequency: Frequency,
+ pub mode: Mode,
+ pub bit_order: BitOrder,
+}
+
+impl Default for Config {
+ fn default() -> Self {
+ Self {
+ frequency: Frequency::F(Kilohertz::MHz(80)),
+ mode: Mode::Mode0,
+ bit_order: BitOrder::default(),
+ }
+ }
+}
+
+// Possible values are copied from embassy-nrf
+#[derive(Debug, Copy, Clone, PartialEq, Eq)]
+#[cfg_attr(feature = "defmt", derive(defmt::Format))]
+#[repr(u32)]
+pub enum Frequency {
+ F(Kilohertz),
+}
+
+riot_rs_embassy_common::impl_spi_from_frequency!();
+riot_rs_embassy_common::impl_spi_frequency_const_functions!(MAX_FREQUENCY);
+
+impl From for fugit::HertzU32 {
+ fn from(freq: Frequency) -> Self {
+ match freq {
+ Frequency::F(kilohertz) => fugit::HertzU32::kHz(kilohertz.to_kHz()),
+ }
+ }
+}
+
+macro_rules! define_spi_drivers {
+ ($( $peripheral:ident ),* $(,)?) => {
+ $(
+ /// Peripheral-specific SPI driver.
+ pub struct $peripheral {
+ spim: YieldingAsync>>,
+ }
+
+ impl $peripheral {
+ #[expect(clippy::new_ret_no_self)]
+ #[must_use]
+ pub fn new(
+ sck_pin: impl Peripheral + 'static,
+ miso_pin: impl Peripheral + 'static,
+ mosi_pin: impl Peripheral + 'static,
+ config: Config,
+ ) -> Spi {
+ let frequency = config.frequency.into();
+
+ // Make this struct a compile-time-enforced singleton: having multiple statics
+ // defined with the same name would result in a compile-time error.
+ paste::paste! {
+ #[allow(dead_code)]
+ static []: () = ();
+ }
+
+ // FIXME(safety): enforce that the init code indeed has run
+ // SAFETY: this struct being a singleton prevents us from stealing the
+ // peripheral multiple times.
+ let spi_peripheral = unsafe { peripherals::$peripheral::steal() };
+
+ let spi = esp_hal::spi::master::Spi::new(
+ spi_peripheral,
+ frequency,
+ crate::spi::from_mode(config.mode),
+ );
+ let spi = spi.with_bit_order(
+ crate::spi::from_bit_order(config.bit_order), // Read order
+ crate::spi::from_bit_order(config.bit_order), // Write order
+ );
+ // The order of MOSI/MISO pins is inverted.
+ let spi = spi.with_pins(
+ sck_pin,
+ mosi_pin,
+ miso_pin,
+ gpio::NoPin, // The CS pin is managed separately
+ );
+
+ Spi::$peripheral(Self { spim: YieldingAsync::new(BlockingAsync::new(spi)) })
+ }
+ }
+ )*
+
+ /// Peripheral-agnostic driver.
+ pub enum Spi {
+ $( $peripheral($peripheral) ),*
+ }
+
+ impl embedded_hal_async::spi::ErrorType for Spi {
+ type Error = esp_hal::spi::Error;
+ }
+
+ impl_async_spibus_for_driver_enum!(Spi, $( $peripheral ),*);
+ };
+}
+
+// FIXME: there seems to be an SPI3 on ESP32-S2 and ESP32-S3
+// Define a driver per peripheral
+#[cfg(context = "esp32c6")]
+define_spi_drivers!(SPI2);
diff --git a/src/riot-rs-esp/src/spi/mod.rs b/src/riot-rs-esp/src/spi/mod.rs
new file mode 100644
index 000000000..f89d8ed5e
--- /dev/null
+++ b/src/riot-rs-esp/src/spi/mod.rs
@@ -0,0 +1,31 @@
+#[doc(alias = "master")]
+pub mod main;
+
+use riot_rs_embassy_common::spi::{BitOrder, Mode};
+
+fn from_mode(mode: Mode) -> esp_hal::spi::SpiMode {
+ match mode {
+ Mode::Mode0 => esp_hal::spi::SpiMode::Mode0,
+ Mode::Mode1 => esp_hal::spi::SpiMode::Mode1,
+ Mode::Mode2 => esp_hal::spi::SpiMode::Mode2,
+ Mode::Mode3 => esp_hal::spi::SpiMode::Mode3,
+ }
+}
+
+fn from_bit_order(bit_order: BitOrder) -> esp_hal::spi::SpiBitOrder {
+ match bit_order {
+ BitOrder::MsbFirst => esp_hal::spi::SpiBitOrder::MSBFirst,
+ BitOrder::LsbFirst => esp_hal::spi::SpiBitOrder::LSBFirst,
+ }
+}
+
+pub fn init(peripherals: &mut crate::OptionalPeripherals) {
+ // Take all SPI peripherals and do nothing with them.
+ cfg_if::cfg_if! {
+ if #[cfg(context = "esp32c6")] {
+ let _ = peripherals.SPI2.take().unwrap();
+ } else {
+ compile_error!("this ESP32 chip is not supported");
+ }
+ }
+}
diff --git a/src/riot-rs-nrf/Cargo.toml b/src/riot-rs-nrf/Cargo.toml
index 821c0fd3c..e57134245 100644
--- a/src/riot-rs-nrf/Cargo.toml
+++ b/src/riot-rs-nrf/Cargo.toml
@@ -54,6 +54,9 @@ hwrng = ["dep:riot-rs-random"]
## Enables I2C support.
i2c = ["riot-rs-embassy-common/i2c", "embassy-executor/integrated-timers"]
+## Enables SPI support.
+spi = ["riot-rs-embassy-common/spi", "embassy-executor/integrated-timers"]
+
## Enables USB support.
usb = []
diff --git a/src/riot-rs-nrf/src/lib.rs b/src/riot-rs-nrf/src/lib.rs
index 94a139915..6cba6ae62 100644
--- a/src/riot-rs-nrf/src/lib.rs
+++ b/src/riot-rs-nrf/src/lib.rs
@@ -16,6 +16,9 @@ pub mod hwrng;
#[cfg(feature = "i2c")]
pub mod i2c;
+#[cfg(feature = "spi")]
+pub mod spi;
+
#[cfg(feature = "usb")]
pub mod usb;
diff --git a/src/riot-rs-nrf/src/spi/main/mod.rs b/src/riot-rs-nrf/src/spi/main/mod.rs
new file mode 100644
index 000000000..b1cedc93c
--- /dev/null
+++ b/src/riot-rs-nrf/src/spi/main/mod.rs
@@ -0,0 +1,206 @@
+use embassy_nrf::{
+ bind_interrupts,
+ gpio::Pin as GpioPin,
+ peripherals,
+ spim::{InterruptHandler, Spim},
+ Peripheral,
+};
+use riot_rs_embassy_common::{
+ impl_async_spibus_for_driver_enum,
+ spi::{BitOrder, Mode},
+};
+
+#[derive(Clone)]
+#[non_exhaustive]
+pub struct Config {
+ pub frequency: Frequency,
+ pub mode: Mode,
+ pub bit_order: BitOrder,
+}
+
+impl Default for Config {
+ fn default() -> Self {
+ Self {
+ frequency: Frequency::_1M,
+ mode: Mode::Mode0,
+ bit_order: BitOrder::default(),
+ }
+ }
+}
+
+// NOTE(arch): limited set of frequencies available.
+#[derive(Debug, Copy, Clone, PartialEq, Eq)]
+#[cfg_attr(feature = "defmt", derive(defmt::Format))]
+#[repr(u32)]
+pub enum Frequency {
+ _125k,
+ _250k,
+ _500k,
+ _1M,
+ _2M,
+ _4M,
+ _8M,
+ // FIXME(embassy): these frequencies are supported by hardware but do not seem supported by
+ // Embassy.
+ // #[cfg(context = "nrf5340")]
+ // _16M,
+ // #[cfg(context = "nrf5340")]
+ // _32M,
+}
+
+impl Frequency {
+ pub const fn first() -> Self {
+ Self::_125k
+ }
+
+ pub const fn last() -> Self {
+ Self::_8M
+ }
+
+ pub const fn next(self) -> Option {
+ match self {
+ Self::_125k => Some(Self::_250k),
+ Self::_250k => Some(Self::_500k),
+ Self::_500k => Some(Self::_1M),
+ Self::_1M => Some(Self::_2M),
+ Self::_2M => Some(Self::_4M),
+ Self::_4M => Some(Self::_8M),
+ Self::_8M => None,
+ }
+ }
+
+ pub const fn prev(self) -> Option {
+ match self {
+ Self::_125k => None,
+ Self::_250k => Some(Self::_125k),
+ Self::_500k => Some(Self::_250k),
+ Self::_1M => Some(Self::_500k),
+ Self::_2M => Some(Self::_1M),
+ Self::_4M => Some(Self::_2M),
+ Self::_8M => Some(Self::_4M),
+ }
+ }
+
+ pub const fn khz(self) -> u32 {
+ match self {
+ Self::_125k => 125,
+ Self::_250k => 250,
+ Self::_500k => 500,
+ Self::_1M => 1000,
+ Self::_2M => 2000,
+ Self::_4M => 4000,
+ Self::_8M => 8000,
+ }
+ }
+}
+
+impl From for Frequency {
+ fn from(freq: riot_rs_embassy_common::spi::main::Frequency) -> Self {
+ match freq {
+ riot_rs_embassy_common::spi::main::Frequency::_125k => Self::_125k,
+ riot_rs_embassy_common::spi::main::Frequency::_250k => Self::_250k,
+ riot_rs_embassy_common::spi::main::Frequency::_500k => Self::_500k,
+ riot_rs_embassy_common::spi::main::Frequency::_1M => Self::_1M,
+ riot_rs_embassy_common::spi::main::Frequency::_2M => Self::_2M,
+ riot_rs_embassy_common::spi::main::Frequency::_4M => Self::_4M,
+ riot_rs_embassy_common::spi::main::Frequency::_8M => Self::_8M,
+ }
+ }
+}
+
+impl From for embassy_nrf::spim::Frequency {
+ fn from(freq: Frequency) -> Self {
+ match freq {
+ Frequency::_125k => embassy_nrf::spim::Frequency::K125,
+ Frequency::_250k => embassy_nrf::spim::Frequency::K250,
+ Frequency::_500k => embassy_nrf::spim::Frequency::K500,
+ Frequency::_1M => embassy_nrf::spim::Frequency::M1,
+ Frequency::_2M => embassy_nrf::spim::Frequency::M2,
+ Frequency::_4M => embassy_nrf::spim::Frequency::M4,
+ Frequency::_8M => embassy_nrf::spim::Frequency::M8,
+ }
+ }
+}
+
+macro_rules! define_spi_drivers {
+ ($( $interrupt:ident => $peripheral:ident ),* $(,)?) => {
+ $(
+ /// Peripheral-specific SPI driver.
+ pub struct $peripheral {
+ spim: Spim<'static, peripherals::$peripheral>,
+ }
+
+ impl $peripheral {
+ #[expect(clippy::new_ret_no_self)]
+ #[must_use]
+ pub fn new(
+ sck_pin: impl Peripheral + 'static,
+ miso_pin: impl Peripheral + 'static,
+ mosi_pin: impl Peripheral + 'static,
+ config: Config,
+ ) -> Spi {
+ let mut spi_config = embassy_nrf::spim::Config::default();
+ spi_config.frequency = config.frequency.into();
+ spi_config.mode = crate::spi::from_mode(config.mode);
+ spi_config.bit_order = crate::spi::from_bit_order(config.bit_order);
+
+ bind_interrupts!(
+ struct Irqs {
+ $interrupt => InterruptHandler;
+ }
+ );
+
+ // Make this struct a compile-time-enforced singleton: having multiple statics
+ // defined with the same name would result in a compile-time error.
+ paste::paste! {
+ #[allow(dead_code)]
+ static []: () = ();
+ }
+
+ // FIXME(safety): enforce that the init code indeed has run
+ // SAFETY: this struct being a singleton prevents us from stealing the
+ // peripheral multiple times.
+ let spim_peripheral = unsafe { peripherals::$peripheral::steal() };
+
+ let spim = Spim::new(
+ spim_peripheral,
+ Irqs,
+ sck_pin,
+ miso_pin,
+ mosi_pin,
+ spi_config,
+ );
+
+ Spi::$peripheral(Self { spim })
+ }
+ }
+ )*
+
+ /// Peripheral-agnostic driver.
+ pub enum Spi {
+ $( $peripheral($peripheral) ),*
+ }
+
+ impl embedded_hal_async::spi::ErrorType for Spi {
+ type Error = embassy_nrf::spim::Error;
+ }
+
+ impl_async_spibus_for_driver_enum!(Spi, $( $peripheral ),*);
+ };
+}
+
+// Define a driver per peripheral
+#[cfg(context = "nrf52840")]
+define_spi_drivers!(
+ // FIXME: arbitrary selected peripherals
+ // SPIM0_SPIS0_TWIM0_TWIS0_SPI0_TWI0 => TWISPI0,
+ // SPIM1_SPIS1_TWIM1_TWIS1_SPI1_TWI1 => TWISPI1,
+ // SPIM2_SPIS2_SPI2 => SPI2,
+ SPIM3 => SPI3,
+);
+// FIXME: arbitrary selected peripherals
+#[cfg(context = "nrf5340")]
+define_spi_drivers!(
+ SERIAL2 => SERIAL2,
+ SERIAL3 => SERIAL3,
+);
diff --git a/src/riot-rs-nrf/src/spi/mod.rs b/src/riot-rs-nrf/src/spi/mod.rs
new file mode 100644
index 000000000..dbef7bcd4
--- /dev/null
+++ b/src/riot-rs-nrf/src/spi/mod.rs
@@ -0,0 +1,35 @@
+#[doc(alias = "master")]
+pub mod main;
+
+use riot_rs_embassy_common::spi::{BitOrder, Mode};
+
+fn from_mode(mode: Mode) -> embassy_nrf::spim::Mode {
+ match mode {
+ Mode::Mode0 => embassy_nrf::spim::MODE_0,
+ Mode::Mode1 => embassy_nrf::spim::MODE_1,
+ Mode::Mode2 => embassy_nrf::spim::MODE_2,
+ Mode::Mode3 => embassy_nrf::spim::MODE_3,
+ }
+}
+
+fn from_bit_order(bit_order: BitOrder) -> embassy_nrf::spim::BitOrder {
+ match bit_order {
+ BitOrder::MsbFirst => embassy_nrf::spim::BitOrder::MSB_FIRST,
+ BitOrder::LsbFirst => embassy_nrf::spim::BitOrder::LSB_FIRST,
+ }
+}
+
+pub fn init(peripherals: &mut crate::OptionalPeripherals) {
+ // Take all SPI peripherals and do nothing with them.
+ cfg_if::cfg_if! {
+ if #[cfg(context = "nrf52840")] {
+ let _ = peripherals.SPI2.take().unwrap();
+ let _ = peripherals.SPI3.take().unwrap();
+ } else if #[cfg(context = "nrf5340")] {
+ let _ = peripherals.SERIAL2.take().unwrap();
+ let _ = peripherals.SERIAL3.take().unwrap();
+ } else {
+ compile_error!("this nRF chip is not supported");
+ }
+ }
+}
diff --git a/src/riot-rs-rp/Cargo.toml b/src/riot-rs-rp/Cargo.toml
index 4351b3a80..5b7a2bf20 100644
--- a/src/riot-rs-rp/Cargo.toml
+++ b/src/riot-rs-rp/Cargo.toml
@@ -12,6 +12,7 @@ workspace = true
[dependencies]
cfg-if = { workspace = true }
defmt = { workspace = true, optional = true }
+embassy-embedded-hal = { workspace = true }
embassy-net-driver-channel = { workspace = true, optional = true }
embassy-rp = { workspace = true, default-features = false, features = [
"rt",
@@ -51,6 +52,9 @@ hwrng = ["dep:riot-rs-random"]
## Enables I2C support.
i2c = ["riot-rs-embassy-common/i2c", "embassy-executor/integrated-timers"]
+## Enables SPI support.
+spi = ["riot-rs-embassy-common/spi", "embassy-executor/integrated-timers"]
+
## Enables USB support.
usb = []
diff --git a/src/riot-rs-rp/src/lib.rs b/src/riot-rs-rp/src/lib.rs
index bb1d5b879..66cc96b28 100644
--- a/src/riot-rs-rp/src/lib.rs
+++ b/src/riot-rs-rp/src/lib.rs
@@ -21,6 +21,9 @@ pub mod hwrng;
#[cfg(feature = "i2c")]
pub mod i2c;
+#[cfg(feature = "spi")]
+pub mod spi;
+
#[cfg(feature = "usb")]
pub mod usb;
diff --git a/src/riot-rs-rp/src/spi/main/mod.rs b/src/riot-rs-rp/src/spi/main/mod.rs
new file mode 100644
index 000000000..86e000925
--- /dev/null
+++ b/src/riot-rs-rp/src/spi/main/mod.rs
@@ -0,0 +1,115 @@
+use embassy_embedded_hal::adapter::{BlockingAsync, YieldingAsync};
+use embassy_rp::{
+ peripherals,
+ spi::{Blocking, ClkPin, MisoPin, MosiPin, Spi as InnerSpi},
+ Peripheral,
+};
+use riot_rs_embassy_common::{
+ impl_async_spibus_for_driver_enum,
+ spi::{main::Kilohertz, Mode},
+};
+
+// TODO: we could consider making this `pub`
+// NOTE(arch): values from the datasheets.
+#[cfg(context = "rp2040")]
+const MAX_FREQUENCY: Kilohertz = Kilohertz::kHz(62_500);
+
+#[derive(Clone)]
+#[non_exhaustive]
+pub struct Config {
+ pub frequency: Frequency,
+ pub mode: Mode,
+}
+
+impl Default for Config {
+ fn default() -> Self {
+ Self {
+ frequency: Frequency::F(Kilohertz::MHz(1)),
+ mode: Mode::Mode0,
+ }
+ }
+}
+
+#[derive(Debug, Copy, Clone, PartialEq, Eq)]
+#[cfg_attr(feature = "defmt", derive(defmt::Format))]
+#[repr(u32)]
+pub enum Frequency {
+ F(Kilohertz),
+}
+
+riot_rs_embassy_common::impl_spi_from_frequency!();
+riot_rs_embassy_common::impl_spi_frequency_const_functions!(MAX_FREQUENCY);
+
+impl Frequency {
+ fn as_hz(&self) -> u32 {
+ match self {
+ Self::F(kilohertz) => kilohertz.to_Hz(),
+ }
+ }
+}
+
+macro_rules! define_spi_drivers {
+ ($( $peripheral:ident ),* $(,)?) => {
+ $(
+ /// Peripheral-specific SPI driver.
+ pub struct $peripheral {
+ spim: YieldingAsync>>,
+ }
+
+ impl $peripheral {
+ #[expect(clippy::new_ret_no_self)]
+ #[must_use]
+ pub fn new(
+ sck_pin: impl Peripheral> + 'static,
+ miso_pin: impl Peripheral> + 'static,
+ mosi_pin: impl Peripheral> + 'static,
+ config: Config,
+ ) -> Spi {
+ let (pol, phase) = crate::spi::from_mode(config.mode);
+
+ let mut spi_config = embassy_rp::spi::Config::default();
+ spi_config.frequency = config.frequency.as_hz();
+ spi_config.polarity = pol;
+ spi_config.phase = phase;
+
+ // Make this struct a compile-time-enforced singleton: having multiple statics
+ // defined with the same name would result in a compile-time error.
+ paste::paste! {
+ #[allow(dead_code)]
+ static []: () = ();
+ }
+
+ // FIXME(safety): enforce that the init code indeed has run
+ // SAFETY: this struct being a singleton prevents us from stealing the
+ // peripheral multiple times.
+ let spi_peripheral = unsafe { peripherals::$peripheral::steal() };
+
+ // The order of MOSI/MISO pins is inverted.
+ let spi = InnerSpi::new_blocking(
+ spi_peripheral,
+ sck_pin,
+ mosi_pin,
+ miso_pin,
+ spi_config,
+ );
+
+ Spi::$peripheral(Self { spim: YieldingAsync::new(BlockingAsync::new(spi)) })
+ }
+ }
+ )*
+
+ /// Peripheral-agnostic driver.
+ pub enum Spi {
+ $( $peripheral($peripheral) ),*
+ }
+
+ impl embedded_hal_async::spi::ErrorType for Spi {
+ type Error = embassy_rp::spi::Error;
+ }
+
+ impl_async_spibus_for_driver_enum!(Spi, $( $peripheral ),*);
+ };
+}
+
+// Define a driver per peripheral
+define_spi_drivers!(SPI0, SPI1);
diff --git a/src/riot-rs-rp/src/spi/mod.rs b/src/riot-rs-rp/src/spi/mod.rs
new file mode 100644
index 000000000..450abcc7b
--- /dev/null
+++ b/src/riot-rs-rp/src/spi/mod.rs
@@ -0,0 +1,26 @@
+#[doc(alias = "master")]
+pub mod main;
+
+use embassy_rp::spi::{Phase, Polarity};
+use riot_rs_embassy_common::spi::Mode;
+
+fn from_mode(mode: Mode) -> (Polarity, Phase) {
+ match mode {
+ Mode::Mode0 => (Polarity::IdleLow, Phase::CaptureOnFirstTransition),
+ Mode::Mode1 => (Polarity::IdleLow, Phase::CaptureOnSecondTransition),
+ Mode::Mode2 => (Polarity::IdleHigh, Phase::CaptureOnFirstTransition),
+ Mode::Mode3 => (Polarity::IdleHigh, Phase::CaptureOnSecondTransition),
+ }
+}
+
+pub fn init(peripherals: &mut crate::OptionalPeripherals) {
+ // Take all SPI peripherals and do nothing with them.
+ cfg_if::cfg_if! {
+ if #[cfg(context = "rp2040")] {
+ let _ = peripherals.SPI0.take().unwrap();
+ let _ = peripherals.SPI1.take().unwrap();
+ } else {
+ compile_error!("this RP chip is not supported");
+ }
+ }
+}
diff --git a/src/riot-rs-stm32/Cargo.toml b/src/riot-rs-stm32/Cargo.toml
index 0c72132e0..b7b4a459d 100644
--- a/src/riot-rs-stm32/Cargo.toml
+++ b/src/riot-rs-stm32/Cargo.toml
@@ -53,6 +53,9 @@ i2c = [
"embassy-executor/integrated-timers",
]
+## Enables SPI support.
+spi = ["riot-rs-embassy-common/spi", "embassy-executor/integrated-timers"]
+
## Enables USB support.
usb = []
# These are chosen automatically by riot-rs-boards and select the correct stm32
diff --git a/src/riot-rs-stm32/src/lib.rs b/src/riot-rs-stm32/src/lib.rs
index 207bd5596..723b64ef5 100644
--- a/src/riot-rs-stm32/src/lib.rs
+++ b/src/riot-rs-stm32/src/lib.rs
@@ -14,6 +14,9 @@ pub mod extint_registry;
#[cfg(feature = "i2c")]
pub mod i2c;
+#[cfg(feature = "spi")]
+pub mod spi;
+
use embassy_stm32::Config;
pub use embassy_stm32::{interrupt, peripherals, OptionalPeripherals, Peripherals};
@@ -103,7 +106,8 @@ fn board_config(config: &mut Config) {
prediv: PllPreDiv::DIV4,
mul: PllMul::MUL50,
divp: Some(PllDiv::DIV2),
- divq: None,
+ // Required for SPI (configured by `spi123sel`)
+ divq: Some(PllDiv::DIV16), // FIXME: adjust this divider
divr: None,
});
config.rcc.sys = Sysclk::PLL1_P; // 400 Mhz
@@ -116,6 +120,9 @@ fn board_config(config: &mut Config) {
// Set SMPS power config otherwise MCU will not powered after next power-off
config.rcc.supply_config = SupplyConfig::DirectSMPS;
config.rcc.mux.usbsel = mux::Usbsel::HSI48;
+ // Select the clock signal used for SPI1, SPI2, and SPI3.
+ // FIXME: what to do about SPI4, SPI5, and SPI6?
+ config.rcc.mux.spi123sel = mux::Saisel::PLL1_Q; // Reset value
}
// mark used
diff --git a/src/riot-rs-stm32/src/spi/main/mod.rs b/src/riot-rs-stm32/src/spi/main/mod.rs
new file mode 100644
index 000000000..e38762911
--- /dev/null
+++ b/src/riot-rs-stm32/src/spi/main/mod.rs
@@ -0,0 +1,143 @@
+use embassy_embedded_hal::adapter::{BlockingAsync, YieldingAsync};
+use embassy_stm32::{
+ gpio,
+ mode::Blocking,
+ peripherals,
+ spi::{MisoPin, MosiPin, SckPin, Spi as InnerSpi},
+ time::Hertz,
+ Peripheral,
+};
+use riot_rs_embassy_common::{
+ impl_async_spibus_for_driver_enum,
+ spi::{main::Kilohertz, BitOrder, Mode},
+};
+
+// TODO: we could consider making this `pub`
+// NOTE(arch): values from the datasheets.
+// When peripherals support different frequencies, the smallest one is used.
+#[cfg(context = "stm32f401retx")]
+const MAX_FREQUENCY: Kilohertz = Kilohertz::MHz(21);
+#[cfg(context = "stm32h755zitx")]
+const MAX_FREQUENCY: Kilohertz = Kilohertz::MHz(150);
+#[cfg(context = "stm32wb55rgvx")]
+const MAX_FREQUENCY: Kilohertz = Kilohertz::MHz(32);
+
+#[derive(Clone)]
+#[non_exhaustive]
+pub struct Config {
+ pub frequency: Frequency,
+ pub mode: Mode,
+ pub bit_order: BitOrder,
+}
+
+impl Default for Config {
+ fn default() -> Self {
+ Self {
+ frequency: Frequency::F(Kilohertz::MHz(1)),
+ mode: Mode::Mode0,
+ bit_order: BitOrder::default(),
+ }
+ }
+}
+
+#[derive(Debug, Copy, Clone, PartialEq, Eq)]
+#[cfg_attr(feature = "defmt", derive(defmt::Format))]
+#[repr(u32)]
+pub enum Frequency {
+ F(Kilohertz),
+}
+
+impl From for Hertz {
+ fn from(freq: Frequency) -> Self {
+ match freq {
+ Frequency::F(kilohertz) => Hertz::khz(kilohertz.to_kHz()),
+ }
+ }
+}
+
+riot_rs_embassy_common::impl_spi_from_frequency!();
+riot_rs_embassy_common::impl_spi_frequency_const_functions!(MAX_FREQUENCY);
+
+macro_rules! define_spi_drivers {
+ ($( $interrupt:ident => $peripheral:ident ),* $(,)?) => {
+ $(
+ /// Peripheral-specific SPI driver.
+ pub struct $peripheral {
+ spim: YieldingAsync>>,
+ }
+
+ impl $peripheral {
+ #[expect(clippy::new_ret_no_self)]
+ #[must_use]
+ pub fn new(
+ sck_pin: impl Peripheral> + 'static,
+ miso_pin: impl Peripheral> + 'static,
+ mosi_pin: impl Peripheral> + 'static,
+ config: Config,
+ ) -> Spi {
+ let mut spi_config = embassy_stm32::spi::Config::default();
+ spi_config.frequency = config.frequency.into();
+ spi_config.mode = crate::spi::from_mode(config.mode);
+ spi_config.bit_order = crate::spi::from_bit_order(config.bit_order);
+ spi_config.miso_pull = gpio::Pull::None;
+
+ // Make this struct a compile-time-enforced singleton: having multiple statics
+ // defined with the same name would result in a compile-time error.
+ paste::paste! {
+ #[allow(dead_code)]
+ static []: () = ();
+ }
+
+ // FIXME(safety): enforce that the init code indeed has run
+ // SAFETY: this struct being a singleton prevents us from stealing the
+ // peripheral multiple times.
+ let spim_peripheral = unsafe { peripherals::$peripheral::steal() };
+
+ // The order of MOSI/MISO pins is inverted.
+ let spim = InnerSpi::new_blocking(
+ spim_peripheral,
+ sck_pin,
+ mosi_pin,
+ miso_pin,
+ spi_config,
+ );
+
+ Spi::$peripheral(Self { spim: YieldingAsync::new(BlockingAsync::new(spim)) })
+ }
+ }
+ )*
+
+ /// Peripheral-agnostic driver.
+ pub enum Spi {
+ $( $peripheral($peripheral) ),*
+ }
+
+ impl embedded_hal_async::spi::ErrorType for Spi {
+ type Error = embassy_stm32::spi::Error;
+ }
+
+ impl_async_spibus_for_driver_enum!(Spi, $( $peripheral ),*);
+ };
+}
+
+// Define a driver per peripheral
+#[cfg(context = "stm32f401retx")]
+define_spi_drivers!(
+ SPI1 => SPI1,
+ SPI2 => SPI2,
+ SPI3 => SPI3,
+);
+#[cfg(context = "stm32h755zitx")]
+define_spi_drivers!(
+ SPI1 => SPI1,
+ SPI2 => SPI2,
+ SPI3 => SPI3,
+ SPI4 => SPI4,
+ SPI5 => SPI5,
+ SPI6 => SPI6,
+);
+#[cfg(context = "stm32wb55rgvx")]
+define_spi_drivers!(
+ SPI1 => SPI1,
+ SPI2 => SPI2,
+);
diff --git a/src/riot-rs-stm32/src/spi/mod.rs b/src/riot-rs-stm32/src/spi/mod.rs
new file mode 100644
index 000000000..f7f9ee5a2
--- /dev/null
+++ b/src/riot-rs-stm32/src/spi/mod.rs
@@ -0,0 +1,44 @@
+#[doc(alias = "master")]
+pub mod main;
+
+use riot_rs_embassy_common::spi::{BitOrder, Mode};
+
+fn from_mode(mode: Mode) -> embassy_stm32::spi::Mode {
+ match mode {
+ Mode::Mode0 => embassy_stm32::spi::MODE_0,
+ Mode::Mode1 => embassy_stm32::spi::MODE_1,
+ Mode::Mode2 => embassy_stm32::spi::MODE_2,
+ Mode::Mode3 => embassy_stm32::spi::MODE_3,
+ }
+}
+
+fn from_bit_order(bit_order: BitOrder) -> embassy_stm32::spi::BitOrder {
+ match bit_order {
+ BitOrder::MsbFirst => embassy_stm32::spi::BitOrder::MsbFirst,
+ BitOrder::LsbFirst => embassy_stm32::spi::BitOrder::LsbFirst,
+ }
+}
+
+pub fn init(peripherals: &mut crate::OptionalPeripherals) {
+ // This macro has to be defined in this function so that the `peripherals` variables exists.
+ macro_rules! take_all_spi_peripherals {
+ ($peripherals:ident, $( $peripheral:ident ),*) => {
+ $(
+ let _ = peripherals.$peripheral.take().unwrap();
+ )*
+ }
+ }
+
+ // Take all SPI peripherals and do nothing with them.
+ cfg_if::cfg_if! {
+ if #[cfg(context = "stm32f401retx")] {
+ take_all_spi_peripherals!(Peripherals, SPI1, SPI2, SPI3);
+ } else if #[cfg(context = "stm32h755zitx")] {
+ take_all_spi_peripherals!(Peripherals, SPI1, SPI2, SPI3, SPI4, SPI5, SPI6);
+ } else if #[cfg(context = "stm32wb55rgvx")] {
+ take_all_spi_peripherals!(Peripherals, SPI1, SPI2);
+ } else {
+ compile_error!("this STM32 chip is not supported");
+ }
+ }
+}
diff --git a/src/riot-rs/Cargo.toml b/src/riot-rs/Cargo.toml
index da6ffc527..a14e60511 100644
--- a/src/riot-rs/Cargo.toml
+++ b/src/riot-rs/Cargo.toml
@@ -47,6 +47,8 @@ hwrng = ["riot-rs-embassy/hwrng"]
#! ## Serial communication
## Enables I2C support.
i2c = ["riot-rs-embassy/i2c"]
+## Enables SPI support.
+spi = ["riot-rs-embassy/spi"]
## Enables USB support.
usb = ["riot-rs-embassy/usb"]
diff --git a/tests/laze.yml b/tests/laze.yml
index c8792e3ff..d7491046a 100644
--- a/tests/laze.yml
+++ b/tests/laze.yml
@@ -4,5 +4,7 @@ subdirs:
- gpio-interrupt-nrf
- gpio-interrupt-stm32
- i2c-controller
+ - spi-loopback
+ - spi-main
- threading-dynamic-prios
- threading-lock
diff --git a/tests/spi-loopback/Cargo.toml b/tests/spi-loopback/Cargo.toml
new file mode 100644
index 000000000..6121f799c
--- /dev/null
+++ b/tests/spi-loopback/Cargo.toml
@@ -0,0 +1,19 @@
+[package]
+name = "spi-loopback"
+version.workspace = true
+authors.workspace = true
+license.workspace = true
+edition.workspace = true
+repository.workspace = true
+
+[lints]
+workspace = true
+
+[dependencies]
+embassy-executor = { workspace = true }
+embassy-sync = { workspace = true }
+embedded-hal-async = { workspace = true }
+once_cell = { workspace = true }
+riot-rs = { path = "../../src/riot-rs", features = ["spi"] }
+riot-rs-boards = { path = "../../src/riot-rs-boards" }
+static_cell = { workspace = true }
diff --git a/tests/spi-loopback/README.md b/tests/spi-loopback/README.md
new file mode 100644
index 000000000..0c52ef950
--- /dev/null
+++ b/tests/spi-loopback/README.md
@@ -0,0 +1,15 @@
+# spi-loopback
+
+## About
+
+This application is testing raw SPI bus loopback in RIOT-rs.
+
+## How to run
+
+In this folder, run
+
+ laze build -b nrf52840dk run
+
+This test requires MISO/MOSI directly connected for the SPI port defined in the
+`pins` module.
+It attempts to do a transfer and compares if what was sent has been read back.
diff --git a/tests/spi-loopback/laze.yml b/tests/spi-loopback/laze.yml
new file mode 100644
index 000000000..644682f16
--- /dev/null
+++ b/tests/spi-loopback/laze.yml
@@ -0,0 +1,16 @@
+apps:
+ - name: spi-loopback
+ env:
+ global:
+ CARGO_ENV:
+ - CONFIG_ISR_STACKSIZE=16384
+ context:
+ - espressif-esp32-c6-devkitc-1
+ - nrf52840
+ - nrf5340
+ - rp2040
+ - st-nucleo-f401re
+ - st-nucleo-h755zi-q
+ - st-nucleo-wb55
+ selects:
+ - ?release
diff --git a/tests/spi-loopback/src/main.rs b/tests/spi-loopback/src/main.rs
new file mode 100644
index 000000000..0ead83b43
--- /dev/null
+++ b/tests/spi-loopback/src/main.rs
@@ -0,0 +1,68 @@
+//! This example is merely to illustrate and test raw bus usage.
+//!
+//! Please use [`riot_rs::sensors`] instead for a high-level sensor abstraction that is
+//! architecture-agnostic.
+#![no_main]
+#![no_std]
+#![feature(type_alias_impl_trait)]
+#![feature(used_with_arg)]
+#![feature(impl_trait_in_assoc_type)]
+
+mod pins;
+
+use embassy_sync::mutex::Mutex;
+use embedded_hal_async::spi::SpiDevice as _;
+use riot_rs::{
+ arch,
+ debug::{
+ exit,
+ log::{debug, info},
+ EXIT_SUCCESS,
+ },
+ gpio,
+ spi::{
+ main::{highest_freq_in, Kilohertz, SpiDevice},
+ Mode,
+ },
+};
+
+pub static SPI_BUS: once_cell::sync::OnceCell<
+ Mutex,
+> = once_cell::sync::OnceCell::new();
+
+#[riot_rs::task(autostart, peripherals)]
+async fn main(peripherals: pins::Peripherals) {
+ let mut spi_config = arch::spi::main::Config::default();
+ spi_config.frequency = const { highest_freq_in(Kilohertz::kHz(1000)..=Kilohertz::kHz(2000)) };
+ debug!("Selected frequency: {}", spi_config.frequency);
+ spi_config.mode = if !cfg!(context = "esp") {
+ Mode::Mode3
+ } else {
+ // FIXME: the sensor datasheet does say SPI mode 3, not mode 0
+ Mode::Mode0
+ };
+
+ let spi_bus = pins::SensorSpi::new(
+ peripherals.spi_sck,
+ peripherals.spi_miso,
+ peripherals.spi_mosi,
+ spi_config,
+ );
+
+ let _ = SPI_BUS.set(Mutex::new(spi_bus));
+
+ let cs_output = gpio::Output::new(peripherals.spi_cs, gpio::Level::High);
+ let mut spi_device = SpiDevice::new(SPI_BUS.get().unwrap(), cs_output);
+
+ let out = [0u8, 1, 2, 3, 4, 5, 6, 7];
+ let mut in_ = [0u8; 8];
+ spi_device.transfer(&mut in_, &out).await.unwrap();
+
+ info!("got 0x{:x}", &in_);
+
+ assert_eq!(out, in_);
+
+ info!("Test passed!");
+
+ exit(EXIT_SUCCESS);
+}
diff --git a/tests/spi-loopback/src/pins.rs b/tests/spi-loopback/src/pins.rs
new file mode 100644
index 000000000..f1f4314db
--- /dev/null
+++ b/tests/spi-loopback/src/pins.rs
@@ -0,0 +1,76 @@
+use riot_rs::arch::{peripherals, spi};
+
+#[cfg(context = "esp")]
+pub type SensorSpi = spi::main::SPI2;
+#[cfg(context = "esp")]
+riot_rs::define_peripherals!(Peripherals {
+ spi_sck: GPIO_0,
+ spi_miso: GPIO_1,
+ spi_mosi: GPIO_2,
+ spi_cs: GPIO_3,
+});
+
+// Side SPI of Arduino v3 connector
+#[cfg(context = "nrf52840")]
+pub type SensorSpi = spi::main::SPI3;
+#[cfg(context = "nrf52840")]
+riot_rs::define_peripherals!(Peripherals {
+ spi_sck: P1_15,
+ spi_miso: P1_14,
+ spi_mosi: P1_13,
+ spi_cs: P1_12,
+});
+
+// Side SPI of Arduino v3 connector
+#[cfg(context = "nrf5340")]
+pub type SensorSpi = spi::main::SERIAL2;
+#[cfg(context = "nrf5340")]
+riot_rs::define_peripherals!(Peripherals {
+ spi_sck: P1_15,
+ spi_miso: P1_14,
+ spi_mosi: P1_13,
+ spi_cs: P1_12,
+});
+
+#[cfg(context = "rp")]
+pub type SensorSpi = spi::main::SPI0;
+#[cfg(context = "rp")]
+riot_rs::define_peripherals!(Peripherals {
+ spi_sck: PIN_18,
+ spi_miso: PIN_16,
+ spi_mosi: PIN_19,
+ spi_cs: PIN_17,
+});
+
+// Side SPI of Arduino v3 connector
+#[cfg(context = "stm32h755zitx")]
+pub type SensorSpi = spi::main::SPI1;
+#[cfg(context = "stm32h755zitx")]
+riot_rs::define_peripherals!(Peripherals {
+ spi_sck: PA5,
+ spi_miso: PA6,
+ spi_mosi: PB5,
+ spi_cs: PD14,
+});
+
+// Side SPI of Arduino v3 connector
+#[cfg(context = "stm32wb55rgvx")]
+pub type SensorSpi = spi::main::SPI1;
+#[cfg(context = "stm32wb55rgvx")]
+riot_rs::define_peripherals!(Peripherals {
+ spi_sck: PA5,
+ spi_miso: PA6,
+ spi_mosi: PA7,
+ spi_cs: PA4,
+});
+
+// Side SPI of Arduino v3 connector
+#[cfg(context = "stm32f401retx")]
+pub type SensorSpi = spi::main::SPI1;
+#[cfg(context = "stm32f401retx")]
+riot_rs::define_peripherals!(Peripherals {
+ spi_sck: PA5,
+ spi_miso: PA6,
+ spi_mosi: PA7,
+ spi_cs: PA4,
+});
diff --git a/tests/spi-main/Cargo.toml b/tests/spi-main/Cargo.toml
new file mode 100644
index 000000000..4ce204674
--- /dev/null
+++ b/tests/spi-main/Cargo.toml
@@ -0,0 +1,19 @@
+[package]
+name = "spi-main"
+version.workspace = true
+authors.workspace = true
+license.workspace = true
+edition.workspace = true
+repository.workspace = true
+
+[lints]
+workspace = true
+
+[dependencies]
+embassy-executor = { workspace = true }
+embassy-sync = { workspace = true }
+embedded-hal-async = { workspace = true }
+once_cell = { workspace = true }
+riot-rs = { path = "../../src/riot-rs", features = ["spi"] }
+riot-rs-boards = { path = "../../src/riot-rs-boards" }
+static_cell = { workspace = true }
diff --git a/tests/spi-main/README.md b/tests/spi-main/README.md
new file mode 100644
index 000000000..623b78205
--- /dev/null
+++ b/tests/spi-main/README.md
@@ -0,0 +1,15 @@
+# spi-main
+
+## About
+
+This application is testing raw SPI bus usage in RIOT-rs.
+
+## How to run
+
+In this folder, run
+
+ laze build -b nrf52840dk run
+
+This test requires an LIS3DH sensor (3-axis accelerometer) attached to the pins configured in the
+`pins` module.
+It attempts to read the `WHO_AM_I` register and checks the received value against the expected id.
diff --git a/tests/spi-main/laze.yml b/tests/spi-main/laze.yml
new file mode 100644
index 000000000..8aa02a73e
--- /dev/null
+++ b/tests/spi-main/laze.yml
@@ -0,0 +1,15 @@
+apps:
+ - name: spi-main
+ env:
+ global:
+ CARGO_ENV:
+ - CONFIG_ISR_STACKSIZE=16384
+ context:
+ - espressif-esp32-c6-devkitc-1
+ - nrf52840
+ - nrf5340
+ - rp2040
+ - st-nucleo-h755zi-q
+ - st-nucleo-wb55
+ selects:
+ - ?release
diff --git a/tests/spi-main/src/main.rs b/tests/spi-main/src/main.rs
new file mode 100644
index 000000000..734fcdb8e
--- /dev/null
+++ b/tests/spi-main/src/main.rs
@@ -0,0 +1,82 @@
+//! This example is merely to illustrate and test raw bus usage.
+//!
+//! Please use [`riot_rs::sensors`] instead for a high-level sensor abstraction that is
+//! architecture-agnostic.
+//!
+//! This example requires a LIS3DH sensor (3-axis accelerometer).
+#![no_main]
+#![no_std]
+#![feature(type_alias_impl_trait)]
+#![feature(used_with_arg)]
+#![feature(impl_trait_in_assoc_type)]
+
+mod pins;
+
+use embassy_sync::mutex::Mutex;
+use embedded_hal_async::spi::{Operation, SpiDevice as _};
+use riot_rs::{
+ arch,
+ debug::{
+ exit,
+ log::{debug, info},
+ EXIT_SUCCESS,
+ },
+ gpio,
+ spi::{
+ main::{highest_freq_in, Kilohertz, SpiDevice},
+ Mode,
+ },
+};
+
+// WHO_AM_I register of the LIS3DH sensor
+const WHO_AM_I_REG_ADDR: u8 = 0x0f;
+
+pub static SPI_BUS: once_cell::sync::OnceCell<
+ Mutex,
+> = once_cell::sync::OnceCell::new();
+
+#[riot_rs::task(autostart, peripherals)]
+async fn main(peripherals: pins::Peripherals) {
+ let mut spi_config = arch::spi::main::Config::default();
+ spi_config.frequency = const { highest_freq_in(Kilohertz::kHz(1000)..=Kilohertz::kHz(2000)) };
+ debug!("Selected frequency: {}", spi_config.frequency);
+ spi_config.mode = if !cfg!(context = "esp") {
+ Mode::Mode3
+ } else {
+ // FIXME: the sensor datasheet does say SPI mode 3, not mode 0
+ Mode::Mode0
+ };
+
+ let spi_bus = pins::SensorSpi::new(
+ peripherals.spi_sck,
+ peripherals.spi_miso,
+ peripherals.spi_mosi,
+ spi_config,
+ );
+
+ let _ = SPI_BUS.set(Mutex::new(spi_bus));
+
+ let cs_output = gpio::Output::new(peripherals.spi_cs, gpio::Level::High);
+ let mut spi_device = SpiDevice::new(SPI_BUS.get().unwrap(), cs_output);
+
+ let mut id = [0];
+ spi_device
+ .transaction(&mut [
+ Operation::Write(&[get_spi_read_command(WHO_AM_I_REG_ADDR)]),
+ Operation::TransferInPlace(&mut id),
+ ])
+ .await
+ .unwrap();
+
+ let who_am_i = id[0];
+ info!("LIS3DH WHO_AM_I_COMMAND register value: 0x{:x}", who_am_i);
+ assert_eq!(who_am_i, 0x33);
+
+ info!("Test passed!");
+
+ exit(EXIT_SUCCESS);
+}
+
+fn get_spi_read_command(addr: u8) -> u8 {
+ addr | 0x80
+}
diff --git a/tests/spi-main/src/pins.rs b/tests/spi-main/src/pins.rs
new file mode 100644
index 000000000..02473fb6a
--- /dev/null
+++ b/tests/spi-main/src/pins.rs
@@ -0,0 +1,65 @@
+use riot_rs::arch::{peripherals, spi};
+
+#[cfg(context = "esp")]
+pub type SensorSpi = spi::main::SPI2;
+#[cfg(context = "esp")]
+riot_rs::define_peripherals!(Peripherals {
+ spi_sck: GPIO_0,
+ spi_miso: GPIO_1,
+ spi_mosi: GPIO_2,
+ spi_cs: GPIO_3,
+});
+
+// Side SPI of Arduino v3 connector
+#[cfg(context = "nrf52840")]
+pub type SensorSpi = spi::main::SPI3;
+#[cfg(context = "nrf52840")]
+riot_rs::define_peripherals!(Peripherals {
+ spi_sck: P1_15,
+ spi_miso: P1_14,
+ spi_mosi: P1_13,
+ spi_cs: P1_12,
+});
+
+// Side SPI of Arduino v3 connector
+#[cfg(context = "nrf5340")]
+pub type SensorSpi = spi::main::SERIAL2;
+#[cfg(context = "nrf5340")]
+riot_rs::define_peripherals!(Peripherals {
+ spi_sck: P1_15,
+ spi_miso: P1_14,
+ spi_mosi: P1_13,
+ spi_cs: P1_12,
+});
+
+#[cfg(context = "rp")]
+pub type SensorSpi = spi::main::SPI0;
+#[cfg(context = "rp")]
+riot_rs::define_peripherals!(Peripherals {
+ spi_sck: PIN_18,
+ spi_miso: PIN_16,
+ spi_mosi: PIN_19,
+ spi_cs: PIN_17,
+});
+
+// Side SPI of Arduino v3 connector
+#[cfg(context = "stm32h755zitx")]
+pub type SensorSpi = spi::main::SPI1;
+#[cfg(context = "stm32h755zitx")]
+riot_rs::define_peripherals!(Peripherals {
+ spi_sck: PA5,
+ spi_miso: PA6,
+ spi_mosi: PB5,
+ spi_cs: PD14,
+});
+
+// Side SPI of Arduino v3 connector
+#[cfg(context = "stm32wb55rgvx")]
+pub type SensorSpi = spi::main::SPI1;
+#[cfg(context = "stm32wb55rgvx")]
+riot_rs::define_peripherals!(Peripherals {
+ spi_sck: PA5,
+ spi_miso: PA6,
+ spi_mosi: PA7,
+ spi_cs: PA4,
+});