Skip to main content

xpanse_api/bus/
allocator.rs

1//! Hardware bus allocator.
2//!
3//! The allocator hands out peripherals (SPI/I2C/UART), PIO state machines and
4//! DMA channels to drivers. Constructed bus handles own their resources for the
5//! rest of the boot; the allocator is intentionally a startup-time allocator,
6//! not a hot-plug pool. The API is designed so that the most common ways to
7//! misconfigure hardware are caught at compile time:
8//!
9//! * Pin roles can't be swapped — `clk`/`mosi`/`miso` and `tx`/`rx` are
10//!   distinguished by trait bounds (`ClkPin<I>`, `MosiPin<I>`, …). A pin only
11//!   implements the role trait for its actual function, so passing `miso` where
12//!   `clk` is expected is a type error.
13//! * Pins can't be paired with the wrong peripheral instance — the role traits
14//!   are parameterized by the instance (`ClkPin<SPI0>` vs `ClkPin<SPI1>`), so an
15//!   SPI0 pin can't be used with the SPI1 peripheral.
16//! * The backend can't be confused — there are separate `create_*_hardware`,
17//!   `create_*_pio` and `create_*_bitbang` methods. There is no `bool` flag and
18//!   no silent HW→bitbang downgrade when a hardware peripheral was specifically
19//!   requested.
20//! * DMA channels are owned by the allocator — callers specify *which* typed
21//!   channel to use, and the allocator tracks availability. A channel already
22//!   handed out returns `Err(Exhausted)`.
23//! * PIO state machines are allocated soundly — the `PioManager` keeps every SM
24//!   alive, so all 12 SMs are independently available and no `(block, sm)`
25//!   combination the allocator returns is unbuild-able.
26//!
27//! Async SPI (hardware and PIO) requires DMA. Because embassy-rp models each
28//! DMA channel as a distinct type, the caller specifies the channel types
29//! (`TxDma`, `RxDma`) and provides the board's IRQ binding; the allocator
30//! dispenses the matching `Peri` tokens from its pool. PIO UART uses FIFO
31//! polling (no DMA), so [`BusAllocator::create_uart`](crate::bus::allocator::BusAllocator::create_uart) provides a PIO→BitBang fallback without
32//! DMA for baud rates supported by at least one backend.
33
34use embassy_rp::dma::{self, ChannelInstance};
35use embassy_rp::interrupt::typelevel::Binding;
36use embassy_rp::peripherals::{
37    DMA_CH0, DMA_CH1, DMA_CH2, DMA_CH3, DMA_CH4, DMA_CH5, DMA_CH6, DMA_CH7, DMA_CH8, DMA_CH9,
38    DMA_CH10, DMA_CH11, DMA_CH12, DMA_CH13, DMA_CH14, DMA_CH15, I2C0, I2C1, PIO0, PIO1, PIO2, SPI0,
39    SPI1, UART0, UART1,
40};
41use embassy_rp::pio::PioPin;
42use embassy_rp::spi::{self, ClkPin, MisoPin, MosiPin};
43use embassy_rp::{Peri, i2c, uart};
44
45use alloc::boxed::Box;
46
47use crate::bus::i2c::I2cBusHandle;
48use crate::bus::i2c_bitbang::BitBangI2cBus;
49use crate::bus::i2c_hardware::HardwareI2cBus;
50use crate::bus::pio::{PioManager, gpio_base_for_pins, spi_program_instructions};
51use crate::bus::spi::{SpiBusHandle, SpiError};
52use crate::bus::spi_bitbang::BitBangSpiBus;
53use crate::bus::spi_hardware::HardwareSpiBus;
54use crate::bus::uart::UartBusHandle;
55use crate::bus::uart_bitbang::BitBangUartBus;
56use crate::bus::uart_hardware::HardwareUartBus;
57
58#[derive(Debug, Clone, Copy, PartialEq, Eq, defmt::Format)]
59/// Startup-time resource allocation error.
60pub enum AllocatorError {
61    /// The requested hardware peripheral, DMA channel or PIO state machine is
62    /// already in use.
63    Exhausted,
64    /// The requested backend, frequency, or pin-role combination is invalid.
65    InvalidConfiguration,
66}
67
68impl From<SpiError> for AllocatorError {
69    fn from(_: SpiError) -> Self {
70        Self::InvalidConfiguration
71    }
72}
73
74#[derive(Debug, Clone, Copy, PartialEq, Eq)]
75#[repr(u8)]
76/// RP235x PIO instances
77pub enum PioBlock {
78    Block0 = 0,
79    Block1 = 1,
80    Block2 = 2,
81}
82
83impl PioBlock {
84    /// Every board PIO instance.
85    pub const ALL: [PioBlock; 3] = [PioBlock::Block0, PioBlock::Block1, PioBlock::Block2];
86}
87
88#[derive(Debug, Clone, Copy, PartialEq, Eq)]
89#[repr(u8)]
90/// RP235x PIO state machine index within a block.
91pub enum Sm {
92    /// First state machine in a block.
93    Sm0 = 0,
94    /// Second state machine in a block.
95    Sm1 = 1,
96    /// Third state machine in a block.
97    Sm2 = 2,
98    /// Fourth state machine in a block.
99    Sm3 = 3,
100}
101
102impl Sm {
103    /// All state-machine indices within a single PIO block.
104    pub const ALL: [Sm; 4] = [Sm::Sm0, Sm::Sm1, Sm::Sm2, Sm::Sm3];
105}
106
107// ── sealed instance traits ───────────────────────────────────────────────
108//
109// These let the allocator dispatch to the right typed slot for a generic
110// instance `I` *and* return a `Peri<'static, I>`, so the hardware bus can be
111// constructed with the same generic `I` as its pins — no `panic!`
112// "mismatched resource" arms, no unsafe transmutes.
113
114mod private {
115    pub trait Sealed {}
116    impl Sealed for super::SPI0 {}
117    impl Sealed for super::SPI1 {}
118    impl Sealed for super::I2C0 {}
119    impl Sealed for super::I2C1 {}
120    impl Sealed for super::UART0 {}
121    impl Sealed for super::UART1 {}
122    impl Sealed for super::DMA_CH0 {}
123    impl Sealed for super::DMA_CH1 {}
124    impl Sealed for super::DMA_CH2 {}
125    impl Sealed for super::DMA_CH3 {}
126    impl Sealed for super::DMA_CH4 {}
127    impl Sealed for super::DMA_CH5 {}
128    impl Sealed for super::DMA_CH6 {}
129    impl Sealed for super::DMA_CH7 {}
130    impl Sealed for super::DMA_CH8 {}
131    impl Sealed for super::DMA_CH9 {}
132    impl Sealed for super::DMA_CH10 {}
133    impl Sealed for super::DMA_CH11 {}
134    impl Sealed for super::DMA_CH12 {}
135    impl Sealed for super::DMA_CH13 {}
136    impl Sealed for super::DMA_CH14 {}
137    impl Sealed for super::DMA_CH15 {}
138}
139
140/// A hardware SPI instance the allocator can hand out.
141pub trait SpiHw: spi::Instance + private::Sealed + Send + 'static {
142    fn take_peri(alloc: &mut BusAllocator) -> Option<Peri<'static, Self>>;
143    fn return_peri(alloc: &mut BusAllocator, peri: Peri<'static, Self>);
144}
145
146/// A hardware I2C instance the allocator can hand out.
147pub trait I2cHw: i2c::Instance + private::Sealed + Send + 'static {
148    fn take_peri(alloc: &mut BusAllocator) -> Option<Peri<'static, Self>>;
149    fn return_peri(alloc: &mut BusAllocator, peri: Peri<'static, Self>);
150}
151
152/// A hardware UART instance the allocator can hand out.
153pub trait UartHw: uart::Instance + private::Sealed + Send + 'static {
154    fn take_peri(alloc: &mut BusAllocator) -> Option<Peri<'static, Self>>;
155    fn return_peri(alloc: &mut BusAllocator, peri: Peri<'static, Self>);
156}
157
158/// A DMA channel the allocator can hand out. Each RP235x DMA channel is a
159/// distinct type; the caller specifies which channel(s) to use, and the
160/// allocator tracks availability.
161pub trait DmaChannel: ChannelInstance + private::Sealed + 'static {
162    fn take_peri(alloc: &mut BusAllocator) -> Option<Peri<'static, Self>>;
163    fn return_peri(alloc: &mut BusAllocator, peri: Peri<'static, Self>);
164}
165
166// ── SPI peripheral impls ──
167
168impl SpiHw for SPI0 {
169    fn take_peri(alloc: &mut BusAllocator) -> Option<Peri<'static, Self>> {
170        alloc.spi0_peri.take()
171    }
172    fn return_peri(alloc: &mut BusAllocator, peri: Peri<'static, Self>) {
173        alloc.spi0_peri = Some(peri);
174    }
175}
176
177impl SpiHw for SPI1 {
178    fn take_peri(alloc: &mut BusAllocator) -> Option<Peri<'static, Self>> {
179        alloc.spi1_peri.take()
180    }
181    fn return_peri(alloc: &mut BusAllocator, peri: Peri<'static, Self>) {
182        alloc.spi1_peri = Some(peri);
183    }
184}
185
186// ── I2C peripheral impls ──
187
188impl I2cHw for I2C0 {
189    fn take_peri(alloc: &mut BusAllocator) -> Option<Peri<'static, Self>> {
190        alloc.i2c0_peri.take()
191    }
192    fn return_peri(alloc: &mut BusAllocator, peri: Peri<'static, Self>) {
193        alloc.i2c0_peri = Some(peri);
194    }
195}
196
197impl I2cHw for I2C1 {
198    fn take_peri(alloc: &mut BusAllocator) -> Option<Peri<'static, Self>> {
199        alloc.i2c1_peri.take()
200    }
201    fn return_peri(alloc: &mut BusAllocator, peri: Peri<'static, Self>) {
202        alloc.i2c1_peri = Some(peri);
203    }
204}
205
206// ── UART peripheral impls ──
207
208impl UartHw for UART0 {
209    fn take_peri(alloc: &mut BusAllocator) -> Option<Peri<'static, Self>> {
210        alloc.uart0_peri.take()
211    }
212    fn return_peri(alloc: &mut BusAllocator, peri: Peri<'static, Self>) {
213        alloc.uart0_peri = Some(peri);
214    }
215}
216
217impl UartHw for UART1 {
218    fn take_peri(alloc: &mut BusAllocator) -> Option<Peri<'static, Self>> {
219        alloc.uart1_peri.take()
220    }
221    fn return_peri(alloc: &mut BusAllocator, peri: Peri<'static, Self>) {
222        alloc.uart1_peri = Some(peri);
223    }
224}
225
226// ── DMA channel impls ──
227
228macro_rules! impl_dma_channel {
229    ($($ch:ident => $field:ident),* $(,)?) => {
230        $(
231            impl DmaChannel for $ch {
232                fn take_peri(alloc: &mut BusAllocator) -> Option<Peri<'static, Self>> {
233                    alloc.$field.take()
234                }
235                fn return_peri(alloc: &mut BusAllocator, peri: Peri<'static, Self>) {
236                    alloc.$field = Some(peri);
237                }
238            }
239        )*
240    };
241}
242
243impl_dma_channel! {
244    DMA_CH0 => dma_ch0, DMA_CH1 => dma_ch1, DMA_CH2 => dma_ch2, DMA_CH3 => dma_ch3,
245    DMA_CH4 => dma_ch4, DMA_CH5 => dma_ch5, DMA_CH6 => dma_ch6, DMA_CH7 => dma_ch7,
246    DMA_CH8 => dma_ch8, DMA_CH9 => dma_ch9, DMA_CH10 => dma_ch10, DMA_CH11 => dma_ch11,
247    DMA_CH12 => dma_ch12, DMA_CH13 => dma_ch13, DMA_CH14 => dma_ch14, DMA_CH15 => dma_ch15,
248}
249
250pub struct BusAllocator {
251    spi0_peri: Option<Peri<'static, SPI0>>,
252    spi1_peri: Option<Peri<'static, SPI1>>,
253    i2c0_peri: Option<Peri<'static, I2C0>>,
254    i2c1_peri: Option<Peri<'static, I2C1>>,
255    uart0_peri: Option<Peri<'static, UART0>>,
256    uart1_peri: Option<Peri<'static, UART1>>,
257    dma_ch0: Option<Peri<'static, DMA_CH0>>,
258    dma_ch1: Option<Peri<'static, DMA_CH1>>,
259    dma_ch2: Option<Peri<'static, DMA_CH2>>,
260    dma_ch3: Option<Peri<'static, DMA_CH3>>,
261    dma_ch4: Option<Peri<'static, DMA_CH4>>,
262    dma_ch5: Option<Peri<'static, DMA_CH5>>,
263    dma_ch6: Option<Peri<'static, DMA_CH6>>,
264    dma_ch7: Option<Peri<'static, DMA_CH7>>,
265    dma_ch8: Option<Peri<'static, DMA_CH8>>,
266    dma_ch9: Option<Peri<'static, DMA_CH9>>,
267    dma_ch10: Option<Peri<'static, DMA_CH10>>,
268    dma_ch11: Option<Peri<'static, DMA_CH11>>,
269    dma_ch12: Option<Peri<'static, DMA_CH12>>,
270    dma_ch13: Option<Peri<'static, DMA_CH13>>,
271    dma_ch14: Option<Peri<'static, DMA_CH14>>,
272    dma_ch15: Option<Peri<'static, DMA_CH15>>,
273    pio_manager: PioManager,
274}
275
276/// Set of DMA channels the board hands to the allocator. Fields left as
277/// `None` are not owned by the allocator and can't be dispensed.
278pub struct DmaPool {
279    pub ch0: Option<Peri<'static, DMA_CH0>>,
280    pub ch1: Option<Peri<'static, DMA_CH1>>,
281    pub ch2: Option<Peri<'static, DMA_CH2>>,
282    pub ch3: Option<Peri<'static, DMA_CH3>>,
283    pub ch4: Option<Peri<'static, DMA_CH4>>,
284    pub ch5: Option<Peri<'static, DMA_CH5>>,
285    pub ch6: Option<Peri<'static, DMA_CH6>>,
286    pub ch7: Option<Peri<'static, DMA_CH7>>,
287    pub ch8: Option<Peri<'static, DMA_CH8>>,
288    pub ch9: Option<Peri<'static, DMA_CH9>>,
289    pub ch10: Option<Peri<'static, DMA_CH10>>,
290    pub ch11: Option<Peri<'static, DMA_CH11>>,
291    pub ch12: Option<Peri<'static, DMA_CH12>>,
292    pub ch13: Option<Peri<'static, DMA_CH13>>,
293    pub ch14: Option<Peri<'static, DMA_CH14>>,
294    pub ch15: Option<Peri<'static, DMA_CH15>>,
295}
296
297impl DmaPool {
298    pub const fn none() -> Self {
299        Self {
300            ch0: None,
301            ch1: None,
302            ch2: None,
303            ch3: None,
304            ch4: None,
305            ch5: None,
306            ch6: None,
307            ch7: None,
308            ch8: None,
309            ch9: None,
310            ch10: None,
311            ch11: None,
312            ch12: None,
313            ch13: None,
314            ch14: None,
315            ch15: None,
316        }
317    }
318}
319
320impl BusAllocator {
321    /// Creates an allocator holding every provided peripheral.
322    ///
323    /// Supply `None` for any hardware instance the board does not want to expose
324    /// to drivers. Every PIO block is mandatory because PIO state machines back
325    /// several fallback buses.
326    ///
327    /// # Example
328    ///
329    /// ```ignore
330    /// use xpanse_api::bus::allocator::{BusAllocator, DmaPool};
331    ///
332    /// let p = embassy_rp::init(Default::default());
333    /// let buses = BusAllocator::new(
334    ///     Some(p.SPI0),
335    ///     Some(p.SPI1),
336    ///     Some(p.I2C0),
337    ///     Some(p.I2C1),
338    ///     Some(p.UART0),
339    ///     Some(p.UART1),
340    ///     DmaPool::none(),
341    ///     p.PIO0,
342    ///     p.PIO1,
343    ///     p.PIO2,
344    /// );
345    /// ```
346    pub fn new(
347        spi0: Option<Peri<'static, SPI0>>,
348        spi1: Option<Peri<'static, SPI1>>,
349        i2c0: Option<Peri<'static, I2C0>>,
350        i2c1: Option<Peri<'static, I2C1>>,
351        uart0: Option<Peri<'static, UART0>>,
352        uart1: Option<Peri<'static, UART1>>,
353        dma: DmaPool,
354        pio0: Peri<'static, PIO0>,
355        pio1: Peri<'static, PIO1>,
356        pio2: Peri<'static, PIO2>,
357    ) -> Self {
358        Self {
359            spi0_peri: spi0,
360            spi1_peri: spi1,
361            i2c0_peri: i2c0,
362            i2c1_peri: i2c1,
363            uart0_peri: uart0,
364            uart1_peri: uart1,
365            dma_ch0: dma.ch0,
366            dma_ch1: dma.ch1,
367            dma_ch2: dma.ch2,
368            dma_ch3: dma.ch3,
369            dma_ch4: dma.ch4,
370            dma_ch5: dma.ch5,
371            dma_ch6: dma.ch6,
372            dma_ch7: dma.ch7,
373            dma_ch8: dma.ch8,
374            dma_ch9: dma.ch9,
375            dma_ch10: dma.ch10,
376            dma_ch11: dma.ch11,
377            dma_ch12: dma.ch12,
378            dma_ch13: dma.ch13,
379            dma_ch14: dma.ch14,
380            dma_ch15: dma.ch15,
381            pio_manager: PioManager::new(pio0, pio1, pio2),
382        }
383    }
384
385    // ── SPI ──────────────────────────────────────────────────────────
386
387    /// Requests exclusive access to one hardware SPI peripheral.
388    ///
389    /// Returns [`AllocatorError::Exhausted`] if the peripheral has already been
390    /// handed out.
391    pub fn request_spi_hardware<I: SpiHw>(&mut self) -> Result<Peri<'static, I>, AllocatorError> {
392        I::take_peri(self).ok_or(AllocatorError::Exhausted)
393    }
394
395    /// Returns a hardware SPI peripheral to the reusable pool.
396    pub fn release_spi_hardware<I: SpiHw>(&mut self, peri: Peri<'static, I>) {
397        I::return_peri(self, peri);
398    }
399
400    /// Requests exclusive access to one DMA channel.
401    ///
402    /// Returns [`AllocatorError::Exhausted`] if the channel has already been
403    /// handed out.
404    pub fn request_dma<C: DmaChannel>(&mut self) -> Result<Peri<'static, C>, AllocatorError> {
405        C::take_peri(self).ok_or(AllocatorError::Exhausted)
406    }
407
408    /// Returns a DMA channel to the reusable pool.
409    pub fn release_dma<C: DmaChannel>(&mut self, peri: Peri<'static, C>) {
410        C::return_peri(self, peri);
411    }
412
413    /// Builds a hardware SPI bus backed by DMA (truly async). Pin roles and
414    /// instance are checked at compile time: `clk` must be a `ClkPin<I>`,
415    /// `mosi` a `MosiPin<I>`, `miso` a `MisoPin<I>`. The DMA channels are
416    /// pulled from the allocator's pool; the IRQ binding is the board's
417    /// zero-sized `bind_interrupts!` type.
418    ///
419    /// # Errors
420    ///
421    /// Returns [`AllocatorError::InvalidConfiguration`] for an unsupported SPI
422    /// clock, or if `TxDma` and `RxDma` name the same DMA channel type. Returns
423    /// [`AllocatorError::Exhausted`] if the SPI peripheral or either DMA channel
424    /// is unavailable; in that case already-acquired resources are released.
425    ///
426    /// # Example
427    ///
428    /// ```ignore
429    /// use embassy_rp::spi;
430    /// use xpanse_api::bus::allocator::BusAllocator;
431    /// use xpanse_api::reexports::embassy_rp::{peripherals::*, interrupt::typelevel::Binding};
432    ///
433    /// embassy_rp::bind_interrupts!(struct Irqs {
434    ///     DMA_CH0 => embassy_rp::dma::InterruptHandler<DMA_CH0>;
435    ///     DMA_CH1 => embassy_rp::dma::InterruptHandler<DMA_CH1>;
436    /// });
437    ///
438    /// fn make_spi(buses: &mut BusAllocator, p: SplitParts) -> SpiBusHandle {
439    ///     buses.create_spi_hardware::<SPI0, DMA_CH0, DMA_CH1, _>(
440    ///         p.gpio2, p.gpio4, p.gpio3, Irqs, spi::Config::default(),
441    ///     )
442    ///     .expect("SPI configured in range")
443    /// }
444    /// # use xpanse_api::bus::spi::SpiBusHandle;
445    /// ```
446    pub fn create_spi_hardware<I, TxDma, RxDma, Irq>(
447        &mut self,
448        clk: Peri<'static, impl ClkPin<I>>,
449        mosi: Peri<'static, impl MosiPin<I>>,
450        miso: Peri<'static, impl MisoPin<I>>,
451        irq: Irq,
452        config: spi::Config,
453    ) -> Result<SpiBusHandle, AllocatorError>
454    where
455        I: SpiHw,
456        TxDma: DmaChannel,
457        RxDma: DmaChannel,
458        Irq: Binding<TxDma::Interrupt, dma::InterruptHandler<TxDma>>
459            + Binding<RxDma::Interrupt, dma::InterruptHandler<RxDma>>
460            + 'static,
461    {
462        HardwareSpiBus::<I>::validate_config(&config)
463            .map_err(|_| AllocatorError::InvalidConfiguration)?;
464        if core::any::TypeId::of::<TxDma>() == core::any::TypeId::of::<RxDma>() {
465            return Err(AllocatorError::InvalidConfiguration);
466        }
467
468        let peri = self.request_spi_hardware::<I>()?;
469        let tx_dma = match self.request_dma::<TxDma>() {
470            Ok(tx_dma) => tx_dma,
471            Err(error) => {
472                self.release_spi_hardware(peri);
473                return Err(error);
474            }
475        };
476        let rx_dma = match self.request_dma::<RxDma>() {
477            Ok(rx_dma) => rx_dma,
478            Err(error) => {
479                self.release_dma(tx_dma);
480                self.release_spi_hardware(peri);
481                return Err(error);
482            }
483        };
484        let bus = HardwareSpiBus::new(peri, clk, mosi, miso, tx_dma, rx_dma, irq, config)
485            .expect("SPI configuration was validated before allocation");
486        Ok(SpiBusHandle::new(
487            Box::new(bus),
488            crate::bus::spi::SpiBusVersion::Hardware,
489        ))
490    }
491
492    /// Builds a PIO-backed SPI bus (async via DMA) on any free PIO state machine.
493    ///
494    /// # Errors
495    ///
496    /// Returns [`AllocatorError::InvalidConfiguration`] if the clock frequency
497    /// is unsupported, the pins span different GPIO banks, or the requested TX
498    /// and RX DMA channels are the same type. Returns
499    /// [`AllocatorError::Exhausted`] if no matching PIO state machine or DMA
500    /// channel is free; acquired DMA channels are released on failure.
501    pub fn create_spi_pio<TxDma, RxDma, Irq>(
502        &mut self,
503        clk: Peri<'static, impl PioPin>,
504        mosi: Peri<'static, impl PioPin>,
505        miso: Peri<'static, impl PioPin>,
506        irq: Irq,
507        config: spi::Config,
508    ) -> Result<SpiBusHandle, AllocatorError>
509    where
510        TxDma: DmaChannel,
511        RxDma: DmaChannel,
512        Irq: Binding<TxDma::Interrupt, dma::InterruptHandler<TxDma>>
513            + Binding<RxDma::Interrupt, dma::InterruptHandler<RxDma>>
514            + 'static,
515    {
516        crate::bus::spi_pio::PioSpiBus::<PIO0, 0>::validate_config(&config)
517            .map_err(|_| AllocatorError::InvalidConfiguration)?;
518        if core::any::TypeId::of::<TxDma>() == core::any::TypeId::of::<RxDma>() {
519            return Err(AllocatorError::InvalidConfiguration);
520        }
521
522        let gpio_base_high = gpio_base_for_pins(&[clk.pin(), mosi.pin(), miso.pin()])
523            .ok_or(AllocatorError::InvalidConfiguration)?;
524        let instructions = spi_program_instructions(&config);
525        let (block, sm) = self
526            .pio_manager
527            .find_free_sm(gpio_base_high, instructions)
528            .ok_or(AllocatorError::Exhausted)?;
529        let tx_dma = self.request_dma::<TxDma>()?;
530        let rx_dma = match self.request_dma::<RxDma>() {
531            Ok(rx_dma) => rx_dma,
532            Err(error) => {
533                self.release_dma(tx_dma);
534                return Err(error);
535            }
536        };
537        Ok(self.pio_manager.build_spi_at(
538            block,
539            sm,
540            gpio_base_high,
541            instructions,
542            clk,
543            mosi,
544            miso,
545            tx_dma,
546            rx_dma,
547            irq,
548            config,
549        ))
550    }
551
552    /// Builds a bit-banged SPI bus using only GPIO.
553    ///
554    /// # Errors
555    ///
556    /// Returns [`AllocatorError::InvalidConfiguration`] only for an SPI clock
557    /// outside the bit-bang timing range.
558    pub fn create_spi_bitbang(
559        &mut self,
560        clk: Peri<'static, impl embassy_rp::gpio::Pin>,
561        mosi: Peri<'static, impl embassy_rp::gpio::Pin>,
562        miso: Peri<'static, impl embassy_rp::gpio::Pin>,
563        config: spi::Config,
564    ) -> Result<SpiBusHandle, AllocatorError> {
565        BitBangSpiBus::validate_config(&config)
566            .map_err(|_| AllocatorError::InvalidConfiguration)?;
567        let bus = BitBangSpiBus::new(clk, mosi, miso, config)
568            .map_err(|_| AllocatorError::InvalidConfiguration)?;
569        Ok(SpiBusHandle::new(
570            Box::new(bus),
571            crate::bus::spi::SpiBusVersion::BitBang,
572        ))
573    }
574
575    /// Builds a SPI bus that doesn't use hardware, preferring PIO then bit-bang.
576    ///
577    /// If PIO cannot be built, any
578    /// PIO SM reservation is released before dropping to a bit-banged bus.
579    ///
580    /// Returns [`AllocatorError::InvalidConfiguration`] if the frequency is
581    /// unsupported anywhere, or if the requested TX and RX DMA channels are the
582    /// same type.
583    pub fn create_spi_no_hardware<TxDma, RxDma, Irq>(
584        &mut self,
585        clk: Peri<'static, impl PioPin>,
586        mosi: Peri<'static, impl PioPin>,
587        miso: Peri<'static, impl PioPin>,
588        irq: Irq,
589        config: spi::Config,
590    ) -> Result<SpiBusHandle, AllocatorError>
591    where
592        TxDma: DmaChannel,
593        RxDma: DmaChannel,
594        Irq: Binding<TxDma::Interrupt, dma::InterruptHandler<TxDma>>
595            + Binding<RxDma::Interrupt, dma::InterruptHandler<RxDma>>
596            + 'static,
597    {
598        crate::bus::spi_pio::PioSpiBus::<PIO0, 0>::validate_config(&config)
599            .map_err(|_| AllocatorError::InvalidConfiguration)?;
600        if core::any::TypeId::of::<TxDma>() == core::any::TypeId::of::<RxDma>() {
601            return Err(AllocatorError::InvalidConfiguration);
602        }
603
604        let gpio_base_high = gpio_base_for_pins(&[clk.pin(), mosi.pin(), miso.pin()])
605            .ok_or(AllocatorError::InvalidConfiguration);
606        let instructions = spi_program_instructions(&config);
607        let tx_dma = self.request_dma::<TxDma>();
608        let rx_dma = self.request_dma::<RxDma>();
609
610        match (gpio_base_high, tx_dma, rx_dma) {
611            (Ok(gpio_base_high), Ok(tx_dma), Ok(rx_dma)) => {
612                if let Ok((block, sm)) = self
613                    .pio_manager
614                    .find_free_sm(gpio_base_high, instructions)
615                    .ok_or(AllocatorError::Exhausted)
616                {
617                    return Ok(self.pio_manager.build_spi_at(
618                        block,
619                        sm,
620                        gpio_base_high,
621                        instructions,
622                        clk,
623                        mosi,
624                        miso,
625                        tx_dma,
626                        rx_dma,
627                        irq,
628                        config,
629                    ));
630                }
631            }
632            (_, tx_dma, rx_dma) => {
633                if let Ok(tx_dma) = tx_dma {
634                    self.release_dma(tx_dma);
635                }
636
637                if let Ok(rx_dma) = rx_dma {
638                    self.release_dma(rx_dma);
639                }
640            }
641        }
642
643        self.create_spi_bitbang(clk, mosi, miso, config)
644    }
645
646    /// Builds a SPI bus, preferring hardware then PIO then bit-bang.
647    ///
648    /// Pins must implement both role-checked SPI traits and `PioPin` so a
649    /// PIO fallback remains possible. If hardware SPI cannot be built, DMA
650    /// channels are released before falling back. If PIO cannot be built, any
651    /// PIO SM reservation is released before dropping to a bit-banged bus.
652    ///
653    /// Returns [`AllocatorError::InvalidConfiguration`] if the frequency is
654    /// unsupported anywhere, or if the requested TX and RX DMA channels are the
655    /// same type.
656    pub fn create_spi<I, TxDma, RxDma, Irq>(
657        &mut self,
658        clk: Peri<'static, impl ClkPin<I> + PioPin>,
659        mosi: Peri<'static, impl MosiPin<I> + PioPin>,
660        miso: Peri<'static, impl MisoPin<I> + PioPin>,
661        irq: Irq,
662        config: spi::Config,
663    ) -> Result<SpiBusHandle, AllocatorError>
664    where
665        I: SpiHw,
666        TxDma: DmaChannel,
667        RxDma: DmaChannel,
668        Irq: Binding<TxDma::Interrupt, dma::InterruptHandler<TxDma>>
669            + Binding<RxDma::Interrupt, dma::InterruptHandler<RxDma>>
670            + 'static,
671    {
672        HardwareSpiBus::<I>::validate_config(&config)
673            .map_err(|_| AllocatorError::InvalidConfiguration)?;
674        if core::any::TypeId::of::<TxDma>() == core::any::TypeId::of::<RxDma>() {
675            return Err(AllocatorError::InvalidConfiguration);
676        }
677
678        let peri = self.request_spi_hardware::<I>();
679        let tx_dma = self.request_dma::<TxDma>();
680        let rx_dma = self.request_dma::<RxDma>();
681
682        match (peri, tx_dma, rx_dma) {
683            (Ok(peri), Ok(tx_dma), Ok(rx_dma)) => {
684                let bus = HardwareSpiBus::new(peri, clk, mosi, miso, tx_dma, rx_dma, irq, config)
685                    .expect("SPI configuration was validated before allocation");
686                Ok(SpiBusHandle::new(
687                    Box::new(bus),
688                    crate::bus::spi::SpiBusVersion::Hardware,
689                ))
690            }
691            (peri, tx_dma, rx_dma) => {
692                if let Ok(peri) = peri {
693                    self.release_spi_hardware(peri);
694                }
695                if let Ok(tx_dma) = tx_dma {
696                    self.release_dma(tx_dma);
697                }
698                if let Ok(rx_dma) = rx_dma {
699                    self.release_dma(rx_dma);
700                }
701
702                self.create_spi_no_hardware::<TxDma, RxDma, Irq>(clk, mosi, miso, irq, config)
703            }
704        }
705    }
706
707    // ── I2C ──────────────────────────────────────────────────────────
708
709    /// Requests exclusive access to one hardware I2C peripheral.
710    ///
711    /// Returns [`AllocatorError::Exhausted`] if the peripheral has already been
712    /// handed out.
713    pub fn request_i2c_hardware<I: I2cHw>(&mut self) -> Result<Peri<'static, I>, AllocatorError> {
714        I::take_peri(self).ok_or(AllocatorError::Exhausted)
715    }
716
717    /// Returns a hardware I2C peripheral to the reusable pool.
718    pub fn release_i2c_hardware<I: I2cHw>(&mut self, peri: Peri<'static, I>) {
719        I::return_peri(self, peri);
720    }
721
722    /// Builds a hardware I2C bus (async, interrupt-driven — no DMA needed).
723    ///
724    /// `scl` and `sda` are role-checked against `I` at compile time, and `irq`
725    /// is the board's `bind_interrupts!` type for the I2C interrupt.
726    ///
727    /// # Errors
728    ///
729    /// Returns [`AllocatorError::InvalidConfiguration`] if the frequency or
730    /// derived clock dividers are out of range, or
731    /// [`AllocatorError::Exhausted`] if the I2C peripheral is unavailable.
732    ///
733    /// # Example
734    ///
735    /// ```ignore
736    /// use embassy_rp::i2c;
737    /// use xpanse_api::bus::allocator::BusAllocator;
738    ///
739    /// embassy_rp::bind_interrupts!(struct Irqs {
740    ///     I2C0_IRQ => embassy_rp::i2c::InterruptHandler<embassy_rp::peripherals::I2C0>;
741    /// });
742    ///
743    /// let bus = buses.create_i2c_hardware::<embassy_rp::peripherals::I2C0, _>(
744    ///     p.gpio0, p.gpio1, Irqs, i2c::Config::default(),
745    /// )
746    /// .expect("I2C configured in range");
747    /// ```
748    pub fn create_i2c_hardware<I, Irq>(
749        &mut self,
750        scl: Peri<'static, impl i2c::SclPin<I>>,
751        sda: Peri<'static, impl i2c::SdaPin<I>>,
752        irq: Irq,
753        config: i2c::Config,
754    ) -> Result<I2cBusHandle, AllocatorError>
755    where
756        I: I2cHw,
757        Irq: Binding<I::Interrupt, i2c::InterruptHandler<I>> + 'static,
758    {
759        HardwareI2cBus::<I>::validate_config(&config)
760            .map_err(|_| AllocatorError::InvalidConfiguration)?;
761        let peri = self.request_i2c_hardware::<I>()?;
762        let bus = HardwareI2cBus::new(peri, scl, sda, irq, config)
763            .expect("I2C configuration was validated before allocation");
764        Ok(I2cBusHandle::new(
765            Box::new(bus),
766            crate::bus::i2c::I2cBusVersion::Hardware,
767        ))
768    }
769
770    /// Builds a bit-banged I2C bus using two GPIO pins with open-drain
771    /// capability. Frequencies outside the timer's range are rejected.
772    /// `scl` and `sda` may be any GPIO pins; they are not role-checked.
773    ///
774    /// # Errors
775    ///
776    /// Returns [`AllocatorError::InvalidConfiguration`] for a frequency above
777    /// twice the timer tick rate or zero.
778    pub fn create_i2c_bitbang(
779        &mut self,
780        scl: Peri<'static, impl embassy_rp::gpio::Pin>,
781        sda: Peri<'static, impl embassy_rp::gpio::Pin>,
782        frequency_hz: u32,
783    ) -> Result<I2cBusHandle, AllocatorError> {
784        let bus = BitBangI2cBus::new(scl, sda, frequency_hz)
785            .map_err(|_| AllocatorError::InvalidConfiguration)?;
786        Ok(I2cBusHandle::new(
787            Box::new(bus),
788            crate::bus::i2c::I2cBusVersion::BitBang,
789        ))
790    }
791
792    // ── UART ─────────────────────────────────────────────────────────
793
794    /// Requests exclusive access to one hardware UART peripheral.
795    ///
796    /// Returns [`AllocatorError::Exhausted`] if the peripheral has already been
797    /// handed out.
798    pub fn request_uart_hardware<I: UartHw>(&mut self) -> Result<Peri<'static, I>, AllocatorError> {
799        I::take_peri(self).ok_or(AllocatorError::Exhausted)
800    }
801
802    /// Returns a hardware UART peripheral to the reusable pool.
803    pub fn release_uart_hardware<I: UartHw>(&mut self, peri: Peri<'static, I>) {
804        I::return_peri(self, peri);
805    }
806
807    /// Builds a hardware (DMA) UART bus.
808    ///
809    /// `tx` and `rx` are role-checked against `I` at compile time. The DMA
810    /// channels are pulled from the allocator's pool.
811    ///
812    /// # Errors
813    ///
814    /// Returns [`AllocatorError::InvalidConfiguration`] for an unsupported
815    /// baud rate or if `TxDma` and `RxDma` name the same channel type. Returns
816    /// [`AllocatorError::Exhausted`] if the UART peripheral or a DMA channel is
817    /// unavailable; already-acquired resources are released on failure.
818    pub fn create_uart_hardware<I, TxDma, RxDma, Irq>(
819        &mut self,
820        tx: Peri<'static, impl uart::TxPin<I>>,
821        rx: Peri<'static, impl uart::RxPin<I>>,
822        irq: Irq,
823        config: uart::Config,
824    ) -> Result<UartBusHandle, AllocatorError>
825    where
826        I: UartHw,
827        TxDma: DmaChannel,
828        RxDma: DmaChannel,
829        Irq: Binding<I::Interrupt, uart::InterruptHandler<I>>
830            + Binding<TxDma::Interrupt, dma::InterruptHandler<TxDma>>
831            + Binding<RxDma::Interrupt, dma::InterruptHandler<RxDma>>
832            + 'static,
833    {
834        HardwareUartBus::validate_config(&config)
835            .map_err(|_| AllocatorError::InvalidConfiguration)?;
836        if core::any::TypeId::of::<TxDma>() == core::any::TypeId::of::<RxDma>() {
837            return Err(AllocatorError::InvalidConfiguration);
838        }
839
840        let peri = self.request_uart_hardware::<I>()?;
841        let tx_dma = match self.request_dma::<TxDma>() {
842            Ok(tx_dma) => tx_dma,
843            Err(error) => {
844                self.release_uart_hardware(peri);
845                return Err(error);
846            }
847        };
848        let rx_dma = match self.request_dma::<RxDma>() {
849            Ok(rx_dma) => rx_dma,
850            Err(error) => {
851                self.release_dma(tx_dma);
852                self.release_uart_hardware(peri);
853                return Err(error);
854            }
855        };
856        let bus = HardwareUartBus::new(peri, tx, rx, irq, tx_dma, rx_dma, config)
857            .expect("UART configuration was validated before allocation");
858        Ok(UartBusHandle::new(
859            Box::new(bus),
860            crate::bus::uart::UartBusVersion::Hardware,
861        ))
862    }
863
864    /// Builds a PIO-backed UART bus on any two free state machines of one block.
865    ///
866    /// PIO UART uses FIFO polling and needs no DMA.
867    ///
868    /// # Errors
869    ///
870    /// Returns [`AllocatorError::InvalidConfiguration`] if the baud rate is
871    /// unsupported or `tx` and `rx` are not in the same GPIO bank. Returns
872    /// [`AllocatorError::Exhausted`] if no PIO state machine pair is free.
873    pub fn create_uart_pio(
874        &mut self,
875        tx: Peri<'static, impl PioPin>,
876        rx: Peri<'static, impl PioPin>,
877        baud_rate: u32,
878    ) -> Result<UartBusHandle, AllocatorError> {
879        crate::bus::uart_pio::PioUartBus::<PIO0, 0, 1>::validate_baud(baud_rate)
880            .map_err(|_| AllocatorError::InvalidConfiguration)?;
881        let gpio_base_high =
882            gpio_base_for_pins(&[tx.pin()]).ok_or(AllocatorError::InvalidConfiguration)?;
883        if gpio_base_for_pins(&[rx.pin()]) != Some(gpio_base_high) {
884            return Err(AllocatorError::InvalidConfiguration);
885        }
886        self.pio_manager
887            .build_uart_pio(tx, rx, baud_rate)
888            .ok_or(AllocatorError::Exhausted)
889    }
890
891    /// Builds a bit-banged UART bus using only GPIO at a representable baud.
892    ///
893    /// 8-N-1 framing, idle-high TX line, one-byte reads.
894    ///
895    /// # Errors
896    ///
897    /// Returns [`AllocatorError::InvalidConfiguration`] for a baud rate of zero
898    /// or above the timer's maximum.
899    pub fn create_uart_bitbang(
900        &mut self,
901        tx: Peri<'static, impl embassy_rp::gpio::Pin>,
902        rx: Peri<'static, impl embassy_rp::gpio::Pin>,
903        baud_rate: u32,
904    ) -> Result<UartBusHandle, AllocatorError> {
905        BitBangUartBus::validate_baud(baud_rate)
906            .map_err(|_| AllocatorError::InvalidConfiguration)?;
907        let bus = BitBangUartBus::new(tx, rx, baud_rate)
908            .map_err(|_| AllocatorError::InvalidConfiguration)?;
909        Ok(UartBusHandle::new(
910            Box::new(bus),
911            crate::bus::uart::UartBusVersion::BitBang,
912        ))
913    }
914
915    /// Builds a UART bus, preferring PIO then bit-bang.
916    ///
917    /// Hardware UART requires DMA — request it explicitly with
918    /// [`create_uart_hardware`](Self::create_uart_hardware) or
919    /// [`BusAllocator::create_uart`](crate::bus::allocator::BusAllocator::create_uart)(Self::create_uart).
920    ///
921    /// # Errors
922    ///
923    /// Returns [`AllocatorError::InvalidConfiguration`] if the baud rate is
924    /// unsupported or `tx` and `rx` are not in the same GPIO bank. Returns
925    /// [`AllocatorError::Exhausted`] if PIO state machines are free but all
926    /// fallbacks ultimately fail.
927    pub fn create_uart_no_hardware(
928        &mut self,
929        tx: Peri<'static, impl PioPin>,
930        rx: Peri<'static, impl PioPin>,
931        baud_rate: u32,
932    ) -> Result<UartBusHandle, AllocatorError> {
933        crate::bus::uart_pio::PioUartBus::<PIO0, 0, 1>::validate_baud(baud_rate)
934            .map_err(|_| AllocatorError::InvalidConfiguration)?;
935
936        let tx_base = gpio_base_for_pins(&[tx.pin()]);
937        let rx_base = gpio_base_for_pins(&[rx.pin()]);
938        if let (Some(gpio_base_high), true) = (tx_base, tx_base == rx_base)
939            && let Some((block, sm_tx, sm_rx)) = self
940                .pio_manager
941                .find_free_sm_pair(gpio_base_high, crate::bus::pio::PIO_UART_INSTRUCTIONS)
942        {
943            return Ok(self.pio_manager.build_uart_at(
944                block,
945                sm_tx,
946                sm_rx,
947                gpio_base_high,
948                crate::bus::pio::PIO_UART_INSTRUCTIONS,
949                tx,
950                rx,
951                baud_rate,
952            ));
953        }
954        self.create_uart_bitbang(tx, rx, baud_rate)
955    }
956
957    /// Builds a UART bus, preferring hardware then PIO then bit-bang.
958    ///
959    /// Pins must implement both role-checked UART traits and `PioPin` so a
960    /// PIO fallback remains possible. On the hardware failure path, any
961    /// acquired UART peripheral or DMA channel is released before the fallback
962    /// is attempted.
963    ///
964    /// # Errors
965    ///
966    /// Returns [`AllocatorError::InvalidConfiguration`] if the baud rate is
967    /// unsupported or `TxDma` and `RxDma` name the same channel type. Returns
968    /// [`AllocatorError::Exhausted`] if all three backends are unavailable.
969    pub fn create_uart<I, TxDma, RxDma, Irq>(
970        &mut self,
971        tx: Peri<'static, impl uart::TxPin<I> + PioPin>,
972        rx: Peri<'static, impl uart::RxPin<I> + PioPin>,
973        irq: Irq,
974        config: uart::Config,
975    ) -> Result<UartBusHandle, AllocatorError>
976    where
977        I: UartHw,
978        TxDma: DmaChannel,
979        RxDma: DmaChannel,
980        Irq: Binding<I::Interrupt, uart::InterruptHandler<I>>
981            + Binding<TxDma::Interrupt, dma::InterruptHandler<TxDma>>
982            + Binding<RxDma::Interrupt, dma::InterruptHandler<RxDma>>
983            + 'static,
984    {
985        HardwareUartBus::validate_config(&config)
986            .map_err(|_| AllocatorError::InvalidConfiguration)?;
987
988        if core::any::TypeId::of::<TxDma>() == core::any::TypeId::of::<RxDma>() {
989            return Err(AllocatorError::InvalidConfiguration);
990        }
991
992        let peri = self.request_uart_hardware::<I>();
993        let tx_dma = self.request_dma::<TxDma>();
994        let rx_dma = self.request_dma::<RxDma>();
995
996        match (peri, tx_dma, rx_dma) {
997            // Success path: We get direct ownership of the unwrapped values
998            (Ok(peri), Ok(tx_dma), Ok(rx_dma)) => {
999                let bus = HardwareUartBus::new(peri, tx, rx, irq, tx_dma, rx_dma, config)
1000                    .expect("UART configuration was validated before allocation");
1001
1002                Ok(UartBusHandle::new(
1003                    Box::new(bus),
1004                    crate::bus::uart::UartBusVersion::Hardware,
1005                ))
1006            }
1007
1008            // Failure path: At least one is an Err.
1009            // We re-bind the original Results to these variables and clean up.
1010            (peri, tx_dma, rx_dma) => {
1011                if let Ok(peri) = peri {
1012                    self.release_uart_hardware(peri);
1013                }
1014                if let Ok(tx_dma) = tx_dma {
1015                    self.release_dma(tx_dma);
1016                }
1017                if let Ok(rx_dma) = rx_dma {
1018                    self.release_dma(rx_dma);
1019                }
1020
1021                self.create_uart_no_hardware(tx, rx, config.baudrate)
1022            }
1023        }
1024    }
1025
1026    // ── PIO ─────────────────────────────────────────────────────────
1027
1028    /// Hands out one free PIO state machine on any block, together with the
1029    /// block's `Common` handle. Drivers that load custom PIO programs use this,
1030    /// then call [`with_pio!`](crate::with_pio!) to dispatch over the erased
1031    /// block/SM types.
1032    ///
1033    /// The `Common` borrow is only valid while the returned [`crate::bus::pio::PioAccess`] is
1034    /// alive (i.e. while you hold `&mut BusAllocator`). Programs loaded via
1035    /// `Common` produce `'static` handles, so a driver can load a program,
1036    /// configure the SM, and then keep the `LoadedProgram` + `StateMachine`
1037    /// after the borrow ends.
1038    ///
1039    /// Returns `None` if every state machine on every block is in use, the pin
1040    /// is not a PIO-capable GPIO, or 32 instructions have already been reserved.
1041    pub fn request_pio<P: PioPin>(
1042        &mut self,
1043        pin: &Peri<'static, P>,
1044    ) -> Option<crate::bus::pio::PioAccess<'_>> {
1045        self.pio_manager.request_pio(pin)
1046    }
1047}