ariel_os_esp/spi/main/
mod.rs

1//! Provides support for the SPI communication bus in main mode.
2
3#![expect(unsafe_code)]
4
5use ariel_os_embassy_common::{
6    impl_async_spibus_for_driver_enum,
7    spi::{BitOrder, Mode, main::Kilohertz},
8};
9use embassy_embedded_hal::adapter::{BlockingAsync, YieldingAsync};
10use esp_hal::{
11    gpio::{
12        self,
13        interconnect::{PeripheralInput, PeripheralOutput},
14    },
15    peripherals,
16    spi::master::Spi as InnerSpi,
17};
18
19// TODO: we could consider making this `pub`
20// NOTE(hal): values from the datasheets.
21#[cfg(any(
22    context = "esp32",
23    context = "esp32c3",
24    context = "esp32c6",
25    context = "esp32s2",
26    context = "esp32s3"
27))]
28const MAX_FREQUENCY: Kilohertz = Kilohertz::MHz(80);
29
30/// SPI bus configuration.
31#[derive(Clone)]
32#[non_exhaustive]
33pub struct Config {
34    /// The frequency at which the bus should operate.
35    pub frequency: Frequency,
36    /// The SPI mode to use.
37    pub mode: Mode,
38    #[doc(hidden)]
39    pub bit_order: BitOrder,
40}
41
42impl Default for Config {
43    fn default() -> Self {
44        Self {
45            frequency: Frequency::F(Kilohertz::MHz(80)),
46            mode: Mode::Mode0,
47            bit_order: BitOrder::default(),
48        }
49    }
50}
51
52/// SPI bus frequency.
53// Possible values are copied from embassy-nrf
54#[derive(Debug, Copy, Clone, PartialEq, Eq)]
55#[cfg_attr(feature = "defmt", derive(defmt::Format))]
56#[repr(u32)]
57pub enum Frequency {
58    /// Arbitrary frequency.
59    F(Kilohertz),
60}
61
62ariel_os_embassy_common::impl_spi_from_frequency!();
63ariel_os_embassy_common::impl_spi_frequency_const_functions!(MAX_FREQUENCY);
64
65impl From<Frequency> for esp_hal::time::Rate {
66    fn from(freq: Frequency) -> Self {
67        match freq {
68            Frequency::F(kilohertz) => esp_hal::time::Rate::from_khz(kilohertz.raw()),
69        }
70    }
71}
72
73macro_rules! define_spi_drivers {
74    ($( $peripheral:ident ),* $(,)?) => {
75        $(
76            /// Peripheral-specific SPI driver.
77            pub struct $peripheral {
78                spim: YieldingAsync<BlockingAsync<InnerSpi<'static, esp_hal::Blocking>>>,
79            }
80
81            impl $peripheral {
82                /// Returns a driver implementing [`embedded_hal_async::spi::SpiBus`] for this SPI
83                /// peripheral.
84                #[expect(clippy::new_ret_no_self)]
85                #[must_use]
86                pub fn new<
87                    SCK: PeripheralOutput<'static>,
88                    MISO: PeripheralInput<'static>,
89                    MOSI: PeripheralOutput<'static>,
90                >(
91                    sck_pin: impl $crate::IntoPeripheral<'static, SCK>,
92                    miso_pin: impl $crate::IntoPeripheral<'static, MISO>,
93                    mosi_pin: impl $crate::IntoPeripheral<'static, MOSI>,
94                    config: Config,
95                ) -> Spi {
96                    // Make this struct a compile-time-enforced singleton: having multiple statics
97                    // defined with the same name would result in a compile-time error.
98                    paste::paste! {
99                        #[allow(dead_code)]
100                        static [<PREVENT_MULTIPLE_ $peripheral>]: () = ();
101                    }
102
103                    let spi_config = esp_hal::spi::master::Config::default()
104                        .with_frequency(config.frequency.into())
105                        .with_mode(crate::spi::from_mode(config.mode))
106                        .with_read_bit_order(crate::spi::from_bit_order(config.bit_order))
107                        .with_write_bit_order(crate::spi::from_bit_order(config.bit_order));
108
109                    // FIXME(safety): enforce that the init code indeed has run
110                    // SAFETY: this struct being a singleton prevents us from stealing the
111                    // peripheral multiple times.
112                    let spi_peripheral = unsafe { peripherals::$peripheral::steal() };
113
114                    let spi = esp_hal::spi::master::Spi::new(
115                        spi_peripheral,
116                        spi_config,
117                    )
118                        .unwrap()
119                        .with_sck(sck_pin.into_hal_peripheral())
120                        .with_mosi(mosi_pin.into_hal_peripheral())
121                        .with_miso(miso_pin.into_hal_peripheral())
122                        .with_cs(gpio::NoPin); // The CS pin is managed separately
123
124                    Spi::$peripheral(Self { spim: YieldingAsync::new(BlockingAsync::new(spi)) })
125                }
126            }
127        )*
128
129        /// Peripheral-agnostic driver.
130        pub enum Spi {
131            $(
132                #[doc = concat!(stringify!($peripheral), " peripheral.")]
133                $peripheral($peripheral)
134            ),*
135        }
136
137        impl embedded_hal_async::spi::ErrorType for Spi {
138            type Error = esp_hal::spi::Error;
139        }
140
141        impl_async_spibus_for_driver_enum!(Spi, $( $peripheral ),*);
142    };
143}
144
145// Define a driver per peripheral
146// SPI0 and SPI1 exist but are not general-purpose SPI peripherals.
147#[cfg(context = "esp32")]
148define_spi_drivers!(SPI2, SPI3);
149#[cfg(context = "esp32c3")]
150define_spi_drivers!(SPI2);
151#[cfg(context = "esp32c6")]
152define_spi_drivers!(SPI2);
153#[cfg(context = "esp32s2")]
154define_spi_drivers!(SPI2, SPI3);
155#[cfg(context = "esp32s3")]
156define_spi_drivers!(SPI2, SPI3);