lilkaoxide/
spi.rs

1#[cfg(all(feature = "blocking", any(feature = "RefCellBus", feature = "CriticalSectionBus")))]
2use core::cell::RefCell;
3#[cfg(all(feature = "async", feature = "AsyncBus"))]
4use embassy_embedded_hal::shared_bus::asynch::spi::SpiDevice as EmbassySpiDevice;
5#[cfg(all(feature = "async", feature = "AsyncBus"))]
6use embassy_sync::blocking_mutex::raw::NoopRawMutex;
7#[cfg(all(feature = "async", feature = "AsyncBus"))]
8use embassy_sync::mutex::Mutex as AsyncMutex;
9#[cfg(feature = "AtomicBus")]
10use embedded_hal_bus::{spi::AtomicDevice, util::AtomicCell};
11use esp_hal::gpio::Output;
12use esp_hal::gpio::interconnect::{PeripheralInput, PeripheralOutput};
13use esp_hal::peripherals::SPI2;
14use esp_hal::spi::master::{Config as SpiConfig, Spi};
15use esp_hal::time::Rate;
16use static_cell::StaticCell;
17
18// ── Compile-time feature exclusivity guards ────────────────────────────────
19
20#[cfg(all(feature = "async", feature = "blocking"))]
21compile_error!("'async' і 'blocking' — взаємовиключні features");
22#[cfg(not(any(feature = "async", feature = "blocking")))]
23compile_error!("Either 'async' or 'blocking' feature must be enabled");
24#[cfg(all(feature = "RefCellBus", feature = "CriticalSectionBus"))]
25compile_error!("'RefCellBus' і 'CriticalSectionBus' — взаємовиключні");
26#[cfg(all(feature = "RefCellBus", feature = "AtomicBus"))]
27compile_error!("'RefCellBus' і 'AtomicBus' — взаємовиключні");
28#[cfg(all(feature = "CriticalSectionBus", feature = "AtomicBus"))]
29compile_error!("'CriticalSectionBus' і 'AtomicBus' — взаємовиключні");
30#[cfg(all(feature = "AsyncBus", feature = "RefCellBus"))]
31compile_error!("'AsyncBus' і 'RefCellBus' — взаємовиключні");
32#[cfg(all(feature = "AsyncBus", feature = "CriticalSectionBus"))]
33compile_error!("'AsyncBus' і 'CriticalSectionBus' — взаємовиключні");
34#[cfg(all(feature = "AsyncBus", feature = "AtomicBus"))]
35compile_error!("'AsyncBus' і 'AtomicBus' — взаємовиключні");
36#[cfg(all(feature = "async", feature = "RefCellBus"))]
37compile_error!("'RefCellBus' дозволено лише з feature 'blocking'");
38#[cfg(all(feature = "blocking", feature = "AsyncBus"))]
39compile_error!("'AsyncBus' дозволено лише з feature 'async'");
40#[cfg(all(feature = "async", not(feature = "AsyncBus")))]
41compile_error!("Для 'async' потрібно ввімкнути 'AsyncBus'");
42#[cfg(all(
43    feature = "blocking",
44    not(any(feature = "RefCellBus", feature = "CriticalSectionBus", feature = "AtomicBus"))
45))]
46compile_error!("Для 'blocking' потрібно ввімкнути 'RefCellBus', 'CriticalSectionBus' або 'AtomicBus'");
47
48// ── Type aliases ───────────────────────────────────────────────────────────
49
50/// Marker type for the SPI peripheral drive mode, selected by feature flag.
51///
52/// * `blocking` → `esp_hal::Blocking`
53/// * `async`    → `esp_hal::Async`
54#[cfg(feature = "blocking")]
55pub type SpiMode = esp_hal::Blocking;
56#[cfg(feature = "async")]
57pub type SpiMode = esp_hal::Async;
58
59/// The raw SPI2 master bus type.  Lifetime is `'static` because it lives in
60/// a [`StaticCell`] after [`init_bus`] is called.
61pub type SpiBusInner = Spi<'static, SpiMode>;
62
63/// The delay type used by the SPI bus sharing wrappers.
64///
65/// * `blocking` → `esp_hal::delay::Delay`
66/// * `async`    → `embassy_time::Delay`
67#[cfg(feature = "blocking")]
68pub type InnerDelay = esp_hal::delay::Delay;
69#[cfg(feature = "async")]
70pub type InnerDelay = embassy_time::Delay;
71
72/// Shared SPI bus wrapper type, selected by the active bus-sharing feature.
73///
74/// This type wraps [`SpiBusInner`] in a concurrency-safe container so that
75/// both the display and the SD card can share the same SPI2 peripheral:
76///
77/// | Feature | Wrapper | Thread safety |
78/// |---|---|---|
79/// | `RefCellBus` | `RefCell<SpiBusInner>` | Single-core, no ISRs. |
80/// | `CriticalSectionBus` | `Mutex<RefCell<SpiBusInner>>` | Safe with ISRs. |
81/// | `AtomicBus` | `AtomicCell<SpiBusInner>` | Lock-free (blocking only). |
82/// | `AsyncBus` | `embassy_sync::Mutex<…>` | Async-safe. |
83///
84/// The wrapper lives in a `'static` [`StaticCell`]; you receive a `&'static SpiBusWrapper`
85/// from [`init_bus`] and pass it to [`make_device`] for each peripheral.
86#[cfg(all(feature = "blocking", feature = "RefCellBus"))]
87pub type SpiBusWrapper = RefCell<SpiBusInner>;
88#[cfg(all(feature = "blocking", feature = "CriticalSectionBus"))]
89pub type SpiBusWrapper = critical_section::Mutex<RefCell<SpiBusInner>>;
90#[cfg(all(feature = "blocking", feature = "AtomicBus"))]
91pub type SpiBusWrapper = AtomicCell<SpiBusInner>;
92#[cfg(all(feature = "async", feature = "AsyncBus"))]
93pub type SpiBusWrapper = AsyncMutex<NoopRawMutex, SpiBusInner>;
94
95/// A virtual SPI device (chip-select + delay) created from a shared [`SpiBusWrapper`].
96///
97/// The concrete type is selected by the active feature combination:
98///
99/// | Features | Type |
100/// |---|---|
101/// | `blocking` + `RefCellBus` | `embedded_hal_bus::spi::RefCellDevice<…>` |
102/// | `blocking` + `CriticalSectionBus` | `embedded_hal_bus::spi::CriticalSectionDevice<…>` |
103/// | `blocking` + `AtomicBus` | `embedded_hal_bus::spi::AtomicDevice<…>` |
104/// | `async` + `AsyncBus` | `embassy_embedded_hal::shared_bus::asynch::spi::SpiDevice<…>` |
105#[cfg(all(feature = "blocking", feature = "RefCellBus"))]
106pub type SpiDev<'a> =
107    embedded_hal_bus::spi::RefCellDevice<'a, SpiBusInner, Output<'a>, InnerDelay>;
108#[cfg(all(feature = "blocking", feature = "CriticalSectionBus"))]
109pub type SpiDev<'a> =
110    embedded_hal_bus::spi::CriticalSectionDevice<'a, SpiBusInner, Output<'a>, InnerDelay>;
111#[cfg(all(feature = "blocking", feature = "AtomicBus"))]
112pub type SpiDev<'a> = AtomicDevice<'a, SpiBusInner, Output<'a>, InnerDelay>;
113#[cfg(all(feature = "async", feature = "AsyncBus"))]
114pub type SpiDev<'a> = EmbassySpiDevice<'a, NoopRawMutex, SpiBusInner, Output<'a>>;
115
116static SPI_BUS: StaticCell<SpiBusWrapper> = StaticCell::new();
117
118/// Error returned by SPI bus or device initialisation functions.
119///
120/// Propagated as [`crate::InitError::Spi`] from [`crate::Lilka::init`].
121#[derive(Debug, Clone, Copy, PartialEq, Eq)]
122pub enum SpiInitError {
123    /// Creating a per-device [`SpiDev`] from the shared bus failed (CS pin conflict, etc.).
124    DeviceInitFailed,
125    /// Initialising the raw SPI2 master bus failed (unsupported frequency, pin conflict, etc.).
126    BusInitFailed,
127}
128
129impl SpiInitError {
130    /// Returns a short, `'static` error description suitable for `defmt` logging.
131    pub const fn as_str(self) -> &'static str {
132        match self {
133            Self::DeviceInitFailed => "SPI device init failed",
134            Self::BusInitFailed => "SPI bus init failed",
135        }
136    }
137}
138
139
140
141/// Places the raw SPI bus into a `'static` shared wrapper and returns a `'static` reference to it.
142///
143/// This is called once by [`crate::Lilka::init`] after [`init_spi_master`].
144/// The specific wrapper type depends on the active bus-sharing feature
145/// (see [`SpiBusWrapper`]).
146///
147/// # Panics
148///
149/// Panics if called more than once (the internal `StaticCell` can only be
150/// initialised once).
151///
152/// # Example
153///
154/// ```rust,no_run
155/// # #[cfg(all(feature = "blocking", feature = "RefCellBus"))]
156/// # fn example(spi: lilkaoxide::LilkaSpiBus) {
157/// use lilkaoxide::{init_bus, init_spi_master};
158///
159/// // spi comes from init_spi_master(...)
160/// let bus: &'static _ = init_bus(spi);
161/// # }
162/// ```
163#[cfg(all(feature = "blocking", feature = "RefCellBus"))]
164pub fn init_bus(spi: SpiBusInner) -> &'static SpiBusWrapper {
165    SPI_BUS.init(RefCell::new(spi))
166}
167#[cfg(all(feature = "blocking", feature = "CriticalSectionBus"))]
168pub fn init_bus(spi: SpiBusInner) -> &'static SpiBusWrapper {
169    use critical_section::Mutex;
170    SPI_BUS.init(Mutex::new(RefCell::new(spi)))
171}
172#[cfg(all(feature = "blocking", feature = "AtomicBus"))]
173pub fn init_bus(spi: SpiBusInner) -> &'static SpiBusWrapper {
174    SPI_BUS.init(AtomicCell::new(spi))
175}
176#[cfg(all(feature = "async", feature = "AsyncBus"))]
177pub fn init_bus(spi: SpiBusInner) -> &'static SpiBusWrapper {
178    SPI_BUS.init(AsyncMutex::new(spi))
179}
180
181/// Creates a virtual SPI device from a shared bus wrapper, a chip-select pin, and a delay.
182///
183/// This is the low-level function used internally to give the display and SD card their
184/// own `embedded-hal` SPI device views of the shared SPI2 bus.
185///
186/// # Parameters
187///
188/// * `bus` — `'static` reference to the shared [`SpiBusWrapper`] created by [`init_bus`].
189/// * `cs` — Output pin to use as the chip-select.  Must be driven by the caller.
190/// * `delay` — Delay implementation for CS assertion timing (unused in `AsyncBus` mode).
191///
192/// # Returns
193///
194/// `Ok(`[`SpiDev`]`)` on success, or `Err(`[`SpiInitError::DeviceInitFailed`]`)` if
195/// the bus sharing wrapper rejects the device (e.g. duplicate CS pin).
196///
197/// # Example
198///
199/// ```rust,no_run
200/// # #[cfg(all(feature = "blocking", feature = "RefCellBus"))]
201/// # fn example(bus: &'static lilkaoxide::SpiBusWrapper, cs: esp_hal::gpio::Output<'static>, delay: lilkaoxide::LilkaDelay) {
202/// use lilkaoxide::{make_device, SpiDev};
203///
204/// let dev: SpiDev<'static> = make_device(bus, cs, delay).expect("device init failed");
205/// # }
206/// ```
207pub fn make_device<'a>(
208    bus: &'a SpiBusWrapper,
209    cs: Output<'a>,
210    delay: InnerDelay,
211) -> Result<SpiDev<'a>, SpiInitError> {
212    #[cfg(feature = "blocking")]
213    {
214        #[cfg(feature = "RefCellBus")]
215        {
216            embedded_hal_bus::spi::RefCellDevice::new(bus, cs, delay)
217                .map_err(|_| SpiInitError::DeviceInitFailed)
218        }
219        #[cfg(feature = "CriticalSectionBus")]
220        {
221            embedded_hal_bus::spi::CriticalSectionDevice::new(bus, cs, delay)
222                .map_err(|_| SpiInitError::DeviceInitFailed)
223        }
224        #[cfg(feature = "AtomicBus")]
225        {
226            AtomicDevice::new(bus, cs, delay).map_err(|_| SpiInitError::DeviceInitFailed)
227        }
228    }
229    #[cfg(all(feature = "async", feature = "AsyncBus"))]
230    {
231        let _ = delay;
232        Ok(EmbassySpiDevice::new(bus, cs))
233    }
234}
235
236/// Initialises the ESP32-S3 SPI2 master bus at the requested frequency.
237///
238/// This is called once during [`crate::Lilka::init`].  The SPI2 peripheral is
239/// hardwired to three specific GPIOs on the Lilka PCB:
240///
241/// | Signal | GPIO |
242/// |---|---|
243/// | SCK (clock) | GPIO18 |
244/// | MOSI (data out) | GPIO17 |
245/// | MISO (data in) | GPIO8 |
246///
247/// In `async` builds the returned bus is automatically converted to async mode
248/// via `Spi::into_async()`.
249///
250/// # Parameters
251///
252/// * `spi2` — Ownership of the `SPI2` peripheral token.
253/// * `sck` / `mosi` / `miso` — GPIO peripheral tokens (consumed permanently).
254/// * `frequency` — Desired bus clock frequency.  The hardware rounds to the nearest
255///   achievable value.  Use values ≤ 80 MHz; the ST7789 display supports up to ~80 MHz
256///   but SD cards max out at 25 MHz in standard speed mode.
257///
258/// # Returns
259///
260/// `Ok(`[`SpiBusInner`]`)` on success, or `Err(`[`SpiInitError::BusInitFailed`]`)`.
261///
262/// # Example
263///
264/// ```rust,no_run
265/// use lilkaoxide::{init_spi_master, LilkaSpiBus};
266/// use fugit::RateExtU32;
267///
268/// # fn example(peripherals: esp_hal::peripherals::Peripherals) {
269/// let spi: LilkaSpiBus = init_spi_master(
270///     peripherals.SPI2,
271///     peripherals.GPIO18,
272///     peripherals.GPIO17,
273///     peripherals.GPIO8,
274///     60.MHz(),
275/// ).expect("SPI init failed");
276/// # }
277/// ```
278pub fn init_spi_master(
279    spi2: SPI2<'static>,
280    sck: impl PeripheralOutput<'static>,
281    mosi: impl PeripheralOutput<'static>,
282    miso: impl PeripheralInput<'static>,
283    frequency: fugit::HertzU32,
284) -> Result<SpiBusInner, SpiInitError> {
285    let spi = Spi::new(
286        spi2,
287        SpiConfig::default().with_frequency(Rate::from_hz(frequency.raw())),
288    )
289    .map_err(|_| SpiInitError::BusInitFailed)?
290    .with_sck(sck)
291    .with_mosi(mosi)
292    .with_miso(miso);
293
294    #[cfg(feature = "async")]
295    let spi = spi.into_async();
296
297    Ok(spi)
298}