diff --git a/.github/workflows/build-deploy-docs.yml b/.github/workflows/build-deploy-docs.yml index f2832f6d..7da68446 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 d9f3c736..c1393be6 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -110,19 +110,19 @@ jobs: # TODO: we'll eventually want to enable relevant features - name: Run crate tests run: | - cargo test --no-default-features --features i2c,no-boards -p riot-rs -p riot-rs-embassy -p riot-rs-runqueue -p riot-rs-threads -p riot-rs-macros + cargo test --no-default-features --features i2c,no-boards,spi -p riot-rs -p riot-rs-embassy -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 @@ -170,49 +170,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-macros -p riot-rs-random -p riot-rs-rt -p riot-rs-threads -p riot-rs-utils + 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 - 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 + args: --verbose --locked --target=riscv32imac-unknown-none-elf --features external-interrupts,i2c,spi,esp-hal/esp32c6,esp-hal-embassy/esp32c6 -p riot-rs-esp - 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 + args: --verbose --locked --features external-interrupts,i2c,spi,embassy-rp/rp2040 -p riot-rs-rp - 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 + args: --verbose --locked --features external-interrupts,i2c,spi,embassy-nrf/nrf52840 -p riot-rs-nrf - 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 + args: --verbose --locked --features external-interrupts,i2c,spi,embassy-stm32/stm32wb55rg -p riot-rs-stm32 # 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 5c02c9a5..558b0d34 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1770,6 +1770,7 @@ dependencies = [ "embassy-usb-driver", "embassy-usb-synopsys-otg", "embedded-can", + "embedded-hal 0.2.7", "embedded-hal 1.0.0", "embedded-hal-async", "embedded-hal-nb", @@ -3742,6 +3743,7 @@ version = "0.1.0" dependencies = [ "cfg-if", "defmt", + "embassy-embedded-hal", "embassy-executor", "embassy-time", "embedded-hal 1.0.0", @@ -3806,6 +3808,7 @@ dependencies = [ "cyw43", "cyw43-pio", "defmt", + "embassy-embedded-hal", "embassy-executor", "embassy-net-driver-channel", "embassy-rp", @@ -4178,6 +4181,19 @@ dependencies = [ "managed", ] +[[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 a514da89..7cc5ce59 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,6 +24,7 @@ members = [ "tests/gpio-interrupt-nrf", "tests/gpio-interrupt-stm32", "tests/i2c-controller", + "tests/spi-main", "tests/threading-lock", ] diff --git a/book/src/support_matrix.html b/book/src/support_matrix.html index 4b0c9df8..760e3224 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 610a2438..03b57f01 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 a21da6a1..124801b3 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 7058ee86..6afefbc6 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 41002b5f..819d32ad 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 00000000..7d83a582 --- /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 00000000..02c37cce --- /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 b515bf02..9abb92af 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 32f55fa5..7ea286db 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 00000000..2ed907c8 --- /dev/null +++ b/src/riot-rs-embassy/src/arch/spi/main/mod.rs @@ -0,0 +1,34 @@ +//! 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. +#[expect( + clippy::manual_non_exhaustive, + reason = "this struct is consumed by this crate, not produced; we do want the 'some variants omitted' mention in the docs" +)] +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum Frequency { + 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 00000000..6a75b4ae --- /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 a7b72c6c..a5a38f60 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 00000000..c21a632f --- /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 00000000..23fe3e1b --- /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 64ab6c5a..7fc4b76b 100644 --- a/src/riot-rs-esp/Cargo.toml +++ b/src/riot-rs-esp/Cargo.toml @@ -12,6 +12,7 @@ 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 } @@ -19,6 +20,7 @@ embedded-hal-async = { workspace = true } esp-hal = { workspace = true, default-features = false, features = [ "async", "embedded-hal", + "embedded-hal-02", # TODO: make do without this ] } esp-hal-embassy = { workspace = true, default-features = false } esp-wifi = { workspace = true, default-features = false, features = [ @@ -67,6 +69,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 0deca2be..46af81ee 100644 --- a/src/riot-rs-esp/src/lib.rs +++ b/src/riot-rs-esp/src/lib.rs @@ -11,6 +11,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 00000000..63864033 --- /dev/null +++ b/src/riot-rs-esp/src/spi/main/mod.rs @@ -0,0 +1,125 @@ +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 { + #[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(); + let clocks = crate::CLOCKS.get().unwrap(); + + // 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), + clocks, + ); + 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( + Some(sck_pin), + Some(mosi_pin), + Some(miso_pin), + gpio::NO_PIN, // 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 00000000..f89d8ed5 --- /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 0070f5f4..04410d34 100644 --- a/src/riot-rs-nrf/Cargo.toml +++ b/src/riot-rs-nrf/Cargo.toml @@ -50,6 +50,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 94a13991..6cba6ae6 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 00000000..4ac7e0ca --- /dev/null +++ b/src/riot-rs-nrf/src/spi/main/mod.rs @@ -0,0 +1,205 @@ +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 { + #[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 00000000..dbef7bcd --- /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 4351b3a8..5b7a2bf2 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 bb1d5b87..66cc96b2 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 00000000..406c7fcb --- /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 { + #[expect(non_snake_case, reason = "consistency with fugit")] + fn to_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 { + #[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.to_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 00000000..450abcc7 --- /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 0c72132e..b7b4a459 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 207bd559..723b64ef 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 00000000..dc64e1a7 --- /dev/null +++ b/src/riot-rs-stm32/src/spi/main/mod.rs @@ -0,0 +1,142 @@ +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 { + #[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 00000000..f7f9ee5a --- /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 da6ffc52..a14e6051 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 9e73d211..e5153ef9 100644 --- a/tests/laze.yml +++ b/tests/laze.yml @@ -4,4 +4,5 @@ subdirs: - gpio-interrupt-nrf - gpio-interrupt-stm32 - i2c-controller + - spi-main - threading-lock diff --git a/tests/spi-main/Cargo.toml b/tests/spi-main/Cargo.toml new file mode 100644 index 00000000..d3aa40e4 --- /dev/null +++ b/tests/spi-main/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "spi-main" +version.workspace = true +authors.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 00000000..623b7820 --- /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 00000000..8aa02a73 --- /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 00000000..734fcdb8 --- /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 00000000..819428af --- /dev/null +++ b/tests/spi-main/src/pins.rs @@ -0,0 +1,61 @@ +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, +}); + +#[cfg(context = "nrf52840")] +pub type SensorSpi = spi::main::SPI3; +#[cfg(context = "nrf52840")] +riot_rs::define_peripherals!(Peripherals { + spi_sck: P0_28, + spi_miso: P0_30, + spi_mosi: P0_29, + spi_cs: P0_31, +}); + +#[cfg(context = "nrf5340")] +pub type SensorSpi = spi::main::SERIAL2; +#[cfg(context = "nrf5340")] +riot_rs::define_peripherals!(Peripherals { + spi_sck: P0_06, + spi_miso: P0_25, + spi_mosi: P0_07, + spi_cs: P0_26, +}); + +#[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, +}); + +#[cfg(context = "stm32h755zitx")] +pub type SensorSpi = spi::main::SPI2; +#[cfg(context = "stm32h755zitx")] +riot_rs::define_peripherals!(Peripherals { + spi_sck: PB10, + spi_miso: PC2, + spi_mosi: PC3, + spi_cs: PB12, +}); + +#[cfg(context = "stm32wb55rgvx")] +pub type SensorSpi = spi::main::SPI2; +#[cfg(context = "stm32wb55rgvx")] +riot_rs::define_peripherals!(Peripherals { + spi_sck: PA9, + spi_miso: PC2, + spi_mosi: PC1, + spi_cs: PC0, +});