Skip to main content

xpanse_api/bus/
pio.rs

1//! PIO resource manager.
2//!
3//! Owns the three PIO blocks and hands out state machines on demand. Unlike the
4//! previous design, a `PioSlot` keeps the `Common` handle, all four state
5//! machines and the IRQ handles alive for the lifetime of the slot, so SMs are
6//! never orphaned and can be allocated independently up to the full 12 SMs.
7
8use alloc::boxed::Box;
9
10use embassy_rp::Peri;
11use embassy_rp::dma::{self, ChannelInstance};
12use embassy_rp::interrupt::typelevel::Binding;
13use embassy_rp::peripherals::{PIO0, PIO1, PIO2};
14use embassy_rp::pio::{Common, Instance, Irq, IrqFlags, Pio, PioPin, StateMachine};
15use embassy_rp::spi;
16
17use crate::bus::allocator::{PioBlock, Sm};
18use crate::bus::spi::{DynSpiBusCombined, SpiBusHandle, SpiBusVersion};
19use crate::bus::spi_pio::PioSpiBus;
20use crate::bus::uart::{DynUartBus, UartBusHandle, UartBusVersion};
21use crate::bus::uart_pio::PioUartBus;
22
23pub(crate) const PIO_UART_INSTRUCTIONS: u8 = 14;
24
25pub(crate) fn spi_program_instructions(config: &spi::Config) -> u8 {
26    match config.phase {
27        spi::Phase::CaptureOnFirstTransition => 2,
28        spi::Phase::CaptureOnSecondTransition => 3,
29    }
30}
31
32pub(crate) fn gpio_base_for_pins(pins: &[u8]) -> Option<bool> {
33    gpio_base_for_pin_numbers(pins.iter().copied())
34}
35
36fn gpio_base_for_pin_numbers(pins: impl IntoIterator<Item = u8>) -> Option<bool> {
37    let mut has_pins = false;
38    let mut all_low = true;
39    let mut all_high = true;
40    for pin in pins {
41        has_pins = true;
42        all_low &= pin < 32;
43        all_high &= (16..48).contains(&pin);
44    }
45
46    if !has_pins {
47        None
48    } else if all_low {
49        Some(false)
50    } else if all_high {
51        Some(true)
52    } else {
53        None
54    }
55}
56
57/// Type-erased view of a PIO-capable pin used to select a PIO GPIO window.
58///
59/// This is implemented for every typed Embassy [`Peri`] containing a
60/// [`PioPin`], allowing heterogeneous pins to be passed to
61/// [`BusAllocator::request_pio`](crate::bus::allocator::BusAllocator::request_pio).
62pub trait PioProgramPin {
63    #[doc(hidden)]
64    fn pio_pin_number(&self) -> u8;
65}
66
67impl<P: PioPin> PioProgramPin for Peri<'static, P> {
68    fn pio_pin_number(&self) -> u8 {
69        self.pin()
70    }
71}
72
73/// A `StateMachine` with the SM number erased at runtime.
74///
75/// Drivers that load custom PIO programs match on this enum to recover the
76/// const-generic `StateMachine<'static, PIO, N>` they need.
77pub enum AnyStateMachine<'d, PIO: Instance + 'static> {
78    Sm0(StateMachine<'d, PIO, 0>),
79    Sm1(StateMachine<'d, PIO, 1>),
80    Sm2(StateMachine<'d, PIO, 2>),
81    Sm3(StateMachine<'d, PIO, 3>),
82}
83
84/// A handle returned by [`BusAllocator::request_pio`](crate::bus::allocator::BusAllocator::request_pio)
85/// that gives a driver temporary access to one block's `Common` handle plus a
86/// free `StateMachine`.
87///
88/// The `Common` borrow is only valid for the lifetime of the handle (which is
89/// tied to `&mut BusAllocator`).  Programs loaded via `Common` return handles
90/// with `'static` lifetime (because the `Common` inside `BusAllocator` is
91/// `Common<'static, PIO>`), so a driver can load a program, configure the SM,
92/// and then keep the `LoadedProgram` + `StateMachine` after the borrow ends.
93#[must_use = "custom PIO access reserves its block for the rest of the boot"]
94pub enum PioAccess<'a> {
95    Block0 {
96        common: &'a mut Common<'static, PIO0>,
97        sm: AnyStateMachine<'static, PIO0>,
98    },
99    Block1 {
100        common: &'a mut Common<'static, PIO1>,
101        sm: AnyStateMachine<'static, PIO1>,
102    },
103    Block2 {
104        common: &'a mut Common<'static, PIO2>,
105        sm: AnyStateMachine<'static, PIO2>,
106    },
107}
108
109embassy_rp::bind_interrupts!(struct PioIrqs {
110    PIO0_IRQ_0 => embassy_rp::pio::InterruptHandler<PIO0>;
111    PIO1_IRQ_0 => embassy_rp::pio::InterruptHandler<PIO1>;
112    PIO2_IRQ_0 => embassy_rp::pio::InterruptHandler<PIO2>;
113});
114
115/// A single PIO block: the shared `Common` handle plus all four state machines
116/// and IRQ handles, kept alive together so dropping one piece never decrements
117/// the PIO's user counter prematurely.
118struct PioSlot<PIO: Instance + 'static> {
119    common: Common<'static, PIO>,
120    sm0: Option<StateMachine<'static, PIO, 0>>,
121    sm1: Option<StateMachine<'static, PIO, 1>>,
122    sm2: Option<StateMachine<'static, PIO, 2>>,
123    sm3: Option<StateMachine<'static, PIO, 3>>,
124    _irq_flags: IrqFlags<'static, PIO>,
125    _irq0: Irq<'static, PIO, 0>,
126    _irq1: Irq<'static, PIO, 1>,
127    _irq2: Irq<'static, PIO, 2>,
128    _irq3: Irq<'static, PIO, 3>,
129    gpio_base_high: Option<bool>,
130    reserved_instructions: u8,
131}
132
133impl<PIO: Instance + Send + 'static> PioSlot<PIO> {
134    fn new(pio: Pio<'static, PIO>) -> Self {
135        let Pio {
136            common,
137            irq_flags,
138            irq0,
139            irq1,
140            irq2,
141            irq3,
142            sm0,
143            sm1,
144            sm2,
145            sm3,
146            ..
147        } = pio;
148        Self {
149            common,
150            sm0: Some(sm0),
151            sm1: Some(sm1),
152            sm2: Some(sm2),
153            sm3: Some(sm3),
154            _irq_flags: irq_flags,
155            _irq0: irq0,
156            _irq1: irq1,
157            _irq2: irq2,
158            _irq3: irq3,
159            gpio_base_high: None,
160            reserved_instructions: 0,
161        }
162    }
163
164    fn can_reserve(&self, gpio_base_high: bool, instructions: u8) -> bool {
165        self.gpio_base_high
166            .is_none_or(|current| current == gpio_base_high)
167            && self.reserved_instructions.saturating_add(instructions) <= 32
168    }
169
170    fn reserve(&mut self, gpio_base_high: bool, instructions: u8) {
171        debug_assert!(self.can_reserve(gpio_base_high, instructions));
172        self.gpio_base_high = Some(gpio_base_high);
173        self.reserved_instructions += instructions;
174    }
175
176    fn has_sm(&self, sm: Sm) -> bool {
177        match sm {
178            Sm::Sm0 => self.sm0.is_some(),
179            Sm::Sm1 => self.sm1.is_some(),
180            Sm::Sm2 => self.sm2.is_some(),
181            Sm::Sm3 => self.sm3.is_some(),
182        }
183    }
184
185    fn take_any_sm(&mut self, sm: Sm) -> Option<AnyStateMachine<'static, PIO>> {
186        match sm {
187            Sm::Sm0 => self.sm0.take().map(AnyStateMachine::Sm0),
188            Sm::Sm1 => self.sm1.take().map(AnyStateMachine::Sm1),
189            Sm::Sm2 => self.sm2.take().map(AnyStateMachine::Sm2),
190            Sm::Sm3 => self.sm3.take().map(AnyStateMachine::Sm3),
191        }
192    }
193
194    fn build_spi<TxDma, RxDma, Irq>(
195        &mut self,
196        sm: Sm,
197        gpio_base_high: bool,
198        instructions: u8,
199        clk: Peri<'static, impl PioPin>,
200        mosi: Peri<'static, impl PioPin>,
201        miso: Peri<'static, impl PioPin>,
202        tx_dma: Peri<'static, TxDma>,
203        rx_dma: Peri<'static, RxDma>,
204        irq: Irq,
205        config: spi::Config,
206    ) -> Option<Box<dyn DynSpiBusCombined>>
207    where
208        TxDma: ChannelInstance,
209        RxDma: ChannelInstance,
210        Irq: Binding<TxDma::Interrupt, dma::InterruptHandler<TxDma>>
211            + Binding<RxDma::Interrupt, dma::InterruptHandler<RxDma>>
212            + 'static,
213    {
214        macro_rules! take {
215            ($field:ident) => {
216                match self.$field.take() {
217                    Some(sm) => {
218                        self.reserve(gpio_base_high, instructions);
219                        Some(Box::new(
220                            PioSpiBus::new(
221                                &mut self.common,
222                                sm,
223                                clk,
224                                mosi,
225                                miso,
226                                tx_dma,
227                                rx_dma,
228                                irq,
229                                config,
230                            )
231                            .expect("PIO SPI configuration was validated before allocation"),
232                        ) as Box<dyn DynSpiBusCombined>)
233                    }
234                    None => None,
235                }
236            };
237        }
238
239        match sm {
240            Sm::Sm0 => take!(sm0),
241            Sm::Sm1 => take!(sm1),
242            Sm::Sm2 => take!(sm2),
243            Sm::Sm3 => take!(sm3),
244        }
245    }
246
247    fn build_uart(
248        &mut self,
249        sm_tx: Sm,
250        sm_rx: Sm,
251        gpio_base_high: bool,
252        instructions: u8,
253        tx_pin: Peri<'static, impl PioPin>,
254        rx_pin: Peri<'static, impl PioPin>,
255        baud_rate: u32,
256    ) -> Option<Box<dyn DynUartBus>> {
257        macro_rules! pair {
258            ($txf:ident, $rxf:ident) => {
259                match (self.$txf.take(), self.$rxf.take()) {
260                    (Some(t), Some(r)) => {
261                        self.reserve(gpio_base_high, instructions);
262                        Some(Box::new(
263                            PioUartBus::new(&mut self.common, t, r, tx_pin, rx_pin, baud_rate)
264                                .expect("PIO UART baud was validated before allocation"),
265                        ) as Box<dyn DynUartBus>)
266                    }
267                    (Some(t), None) => {
268                        self.$txf = Some(t);
269                        None
270                    }
271                    (None, _) => None,
272                }
273            };
274        }
275
276        match (sm_tx, sm_rx) {
277            (Sm::Sm0, Sm::Sm1) => pair!(sm0, sm1),
278            (Sm::Sm0, Sm::Sm2) => pair!(sm0, sm2),
279            (Sm::Sm0, Sm::Sm3) => pair!(sm0, sm3),
280            (Sm::Sm1, Sm::Sm0) => pair!(sm1, sm0),
281            (Sm::Sm1, Sm::Sm2) => pair!(sm1, sm2),
282            (Sm::Sm1, Sm::Sm3) => pair!(sm1, sm3),
283            (Sm::Sm2, Sm::Sm0) => pair!(sm2, sm0),
284            (Sm::Sm2, Sm::Sm1) => pair!(sm2, sm1),
285            (Sm::Sm2, Sm::Sm3) => pair!(sm2, sm3),
286            (Sm::Sm3, Sm::Sm0) => pair!(sm3, sm0),
287            (Sm::Sm3, Sm::Sm1) => pair!(sm3, sm1),
288            (Sm::Sm3, Sm::Sm2) => pair!(sm3, sm2),
289            // Equal pairs are never produced by the allocator (it picks two
290            // *distinct* free SMs), so this arm is unreachable in correct use.
291            _ => None,
292        }
293    }
294}
295
296pub struct PioManager {
297    pio0: Option<PioSlot<PIO0>>,
298    pio1: Option<PioSlot<PIO1>>,
299    pio2: Option<PioSlot<PIO2>>,
300}
301
302impl PioManager {
303    pub fn new(
304        pio0: Peri<'static, PIO0>,
305        pio1: Peri<'static, PIO1>,
306        pio2: Peri<'static, PIO2>,
307    ) -> Self {
308        Self {
309            pio0: Some(PioSlot::new(Pio::new(pio0, PioIrqs))),
310            pio1: Some(PioSlot::new(Pio::new(pio1, PioIrqs))),
311            pio2: Some(PioSlot::new(Pio::new(pio2, PioIrqs))),
312        }
313    }
314
315    /// Build a PIO SPI bus on a specific (block, sm). The SM must be free —
316    /// i.e. obtained from [`find_free_sm`] in the same synchronous sequence.
317    pub(crate) fn build_spi_at<TxDma, RxDma, Irq>(
318        &mut self,
319        block: PioBlock,
320        sm: Sm,
321        gpio_base_high: bool,
322        instructions: u8,
323        clk: Peri<'static, impl PioPin>,
324        mosi: Peri<'static, impl PioPin>,
325        miso: Peri<'static, impl PioPin>,
326        tx_dma: Peri<'static, TxDma>,
327        rx_dma: Peri<'static, RxDma>,
328        irq: Irq,
329        config: spi::Config,
330    ) -> SpiBusHandle
331    where
332        TxDma: ChannelInstance,
333        RxDma: ChannelInstance,
334        Irq: Binding<TxDma::Interrupt, dma::InterruptHandler<TxDma>>
335            + Binding<RxDma::Interrupt, dma::InterruptHandler<RxDma>>
336            + 'static,
337    {
338        let bus = match block {
339            PioBlock::Block0 => self
340                .pio0
341                .as_mut()
342                .and_then(|slot| {
343                    slot.build_spi(
344                        sm,
345                        gpio_base_high,
346                        instructions,
347                        clk,
348                        mosi,
349                        miso,
350                        tx_dma,
351                        rx_dma,
352                        irq,
353                        config,
354                    )
355                })
356                .expect("PIO SM allocation invariant: SM was not free"),
357            PioBlock::Block1 => self
358                .pio1
359                .as_mut()
360                .and_then(|slot| {
361                    slot.build_spi(
362                        sm,
363                        gpio_base_high,
364                        instructions,
365                        clk,
366                        mosi,
367                        miso,
368                        tx_dma,
369                        rx_dma,
370                        irq,
371                        config,
372                    )
373                })
374                .expect("PIO SM allocation invariant: SM was not free"),
375            PioBlock::Block2 => self
376                .pio2
377                .as_mut()
378                .and_then(|slot| {
379                    slot.build_spi(
380                        sm,
381                        gpio_base_high,
382                        instructions,
383                        clk,
384                        mosi,
385                        miso,
386                        tx_dma,
387                        rx_dma,
388                        irq,
389                        config,
390                    )
391                })
392                .expect("PIO SM allocation invariant: SM was not free"),
393        };
394        SpiBusHandle::new(bus, SpiBusVersion::Pio)
395    }
396
397    /// Build a PIO UART bus on a specific (block, sm_tx, sm_rx). The SMs must be
398    /// free — obtained from [`find_free_sm_pair`] in the same sequence.
399    pub(crate) fn build_uart_at(
400        &mut self,
401        block: PioBlock,
402        sm_tx: Sm,
403        sm_rx: Sm,
404        gpio_base_high: bool,
405        instructions: u8,
406        tx_pin: Peri<'static, impl PioPin>,
407        rx_pin: Peri<'static, impl PioPin>,
408        baud_rate: u32,
409    ) -> UartBusHandle {
410        let bus = match block {
411            PioBlock::Block0 => self
412                .pio0
413                .as_mut()
414                .and_then(|slot| {
415                    slot.build_uart(
416                        sm_tx,
417                        sm_rx,
418                        gpio_base_high,
419                        instructions,
420                        tx_pin,
421                        rx_pin,
422                        baud_rate,
423                    )
424                })
425                .expect("PIO SM allocation invariant: SM pair was not free"),
426            PioBlock::Block1 => self
427                .pio1
428                .as_mut()
429                .and_then(|slot| {
430                    slot.build_uart(
431                        sm_tx,
432                        sm_rx,
433                        gpio_base_high,
434                        instructions,
435                        tx_pin,
436                        rx_pin,
437                        baud_rate,
438                    )
439                })
440                .expect("PIO SM allocation invariant: SM pair was not free"),
441            PioBlock::Block2 => self
442                .pio2
443                .as_mut()
444                .and_then(|slot| {
445                    slot.build_uart(
446                        sm_tx,
447                        sm_rx,
448                        gpio_base_high,
449                        instructions,
450                        tx_pin,
451                        rx_pin,
452                        baud_rate,
453                    )
454                })
455                .expect("PIO SM allocation invariant: SM pair was not free"),
456        };
457        UartBusHandle::new(bus, UartBusVersion::Pio)
458    }
459
460    /// Build a PIO-backed UART bus on any two free state machines of a single
461    /// block (one for TX, one for RX).
462    pub fn build_uart_pio(
463        &mut self,
464        tx_pin: Peri<'static, impl PioPin>,
465        rx_pin: Peri<'static, impl PioPin>,
466        baud_rate: u32,
467    ) -> Option<UartBusHandle> {
468        let gpio_base_high = gpio_base_for_pins(&[tx_pin.pin()])?;
469        if gpio_base_for_pins(&[rx_pin.pin()])? != gpio_base_high {
470            return None;
471        }
472        let (block, sm_tx, sm_rx) =
473            self.find_free_sm_pair(gpio_base_high, PIO_UART_INSTRUCTIONS)?;
474        Some(self.build_uart_at(
475            block,
476            sm_tx,
477            sm_rx,
478            gpio_base_high,
479            PIO_UART_INSTRUCTIONS,
480            tx_pin,
481            rx_pin,
482            baud_rate,
483        ))
484    }
485
486    /// Hand out one free state machine on any block, together with the block's
487    /// `Common` handle. Drivers use this to load custom PIO programs. Every pin
488    /// used by the program must be included so the selected GPIO window covers
489    /// the complete pin set.
490    ///
491    /// # Returns
492    ///
493    /// Returns `None` if `pins` is empty, if the pins cannot all fit in either
494    /// GPIO window (0-31 or 16-47), or if no PIO block has both a free state
495    /// machine and all 32 instruction slots available. Custom PIO access
496    /// reserves the selected block's full instruction memory for the rest of
497    /// the boot.
498    pub fn request_pio(&mut self, pins: &[&dyn PioProgramPin]) -> Option<PioAccess<'_>> {
499        let gpio_base_high =
500            gpio_base_for_pin_numbers(pins.iter().map(|pin| pin.pio_pin_number()))?;
501        let (block, sm) = self.find_free_sm(gpio_base_high, 32)?;
502        match block {
503            PioBlock::Block0 => {
504                let slot = self.pio0.as_mut()?;
505                let sm = slot.take_any_sm(sm)?;
506                slot.reserve(gpio_base_high, 32);
507                Some(PioAccess::Block0 {
508                    common: &mut slot.common,
509                    sm,
510                })
511            }
512            PioBlock::Block1 => {
513                let slot = self.pio1.as_mut()?;
514                let sm = slot.take_any_sm(sm)?;
515                slot.reserve(gpio_base_high, 32);
516                Some(PioAccess::Block1 {
517                    common: &mut slot.common,
518                    sm,
519                })
520            }
521            PioBlock::Block2 => {
522                let slot = self.pio2.as_mut()?;
523                let sm = slot.take_any_sm(sm)?;
524                slot.reserve(gpio_base_high, 32);
525                Some(PioAccess::Block2 {
526                    common: &mut slot.common,
527                    sm,
528                })
529            }
530        }
531    }
532
533    pub(crate) fn find_free_sm(
534        &self,
535        gpio_base_high: bool,
536        instructions: u8,
537    ) -> Option<(PioBlock, Sm)> {
538        if let Some(slot) = &self.pio0 {
539            for sm in Sm::ALL {
540                if slot.has_sm(sm) && slot.can_reserve(gpio_base_high, instructions) {
541                    return Some((PioBlock::Block0, sm));
542                }
543            }
544        }
545        if let Some(slot) = &self.pio1 {
546            for sm in Sm::ALL {
547                if slot.has_sm(sm) && slot.can_reserve(gpio_base_high, instructions) {
548                    return Some((PioBlock::Block1, sm));
549                }
550            }
551        }
552        if let Some(slot) = &self.pio2 {
553            for sm in Sm::ALL {
554                if slot.has_sm(sm) && slot.can_reserve(gpio_base_high, instructions) {
555                    return Some((PioBlock::Block2, sm));
556                }
557            }
558        }
559        None
560    }
561
562    pub(crate) fn find_free_sm_pair(
563        &self,
564        gpio_base_high: bool,
565        instructions: u8,
566    ) -> Option<(PioBlock, Sm, Sm)> {
567        if let Some(slot) = &self.pio0
568            && slot.can_reserve(gpio_base_high, instructions)
569            && let Some((a, b)) = two_free_sms(slot)
570        {
571            return Some((PioBlock::Block0, a, b));
572        }
573        if let Some(slot) = &self.pio1
574            && slot.can_reserve(gpio_base_high, instructions)
575            && let Some((a, b)) = two_free_sms(slot)
576        {
577            return Some((PioBlock::Block1, a, b));
578        }
579        if let Some(slot) = &self.pio2
580            && slot.can_reserve(gpio_base_high, instructions)
581            && let Some((a, b)) = two_free_sms(slot)
582        {
583            return Some((PioBlock::Block2, a, b));
584        }
585        None
586    }
587}
588
589fn two_free_sms<PIO: Instance + Send + 'static>(slot: &PioSlot<PIO>) -> Option<(Sm, Sm)> {
590    let mut free = Sm::ALL.into_iter().filter(|sm| slot.has_sm(*sm));
591    match (free.next(), free.next()) {
592        (Some(a), Some(b)) => Some((a, b)),
593        _ => None,
594    }
595}
596
597/// Dispatch over a `PioAccess` handle, recovering the typed `Common` and
598/// `StateMachine` so a driver can load a custom PIO program.
599///
600/// The driver provides a **generic** function/closure that works for any
601/// `PIO: Instance` and `const N: usize`.  The macro matches all 12
602/// (block × SM) arms and calls the function in the one that matches.
603///
604/// # Example
605///
606/// ```ignore
607/// use embassy_rp::pio::{Common, Instance, StateMachine};
608///
609/// fn my_init<PIO: Instance, const N: usize>(
610///     common: &mut Common<'static, PIO>,
611///     sm: StateMachine<'static, PIO, N>,
612/// ) -> MyDriver<PIO, N> {
613///     let program = MyProgram::new(common);
614///     MyDriver::new(common, sm, &program)
615/// }
616///
617/// let handle = allocator.request_pio(&[&data_pin, &clock_pin])?;
618/// let driver = with_pio!(handle, common, sm, my_init(common, sm));
619/// ```
620#[macro_export]
621macro_rules! with_pio {
622    ($handle:expr, $common:ident, $sm:ident, $body:expr) => {
623        match $handle {
624            $crate::bus::pio::PioAccess::Block0 { common, sm } => {
625                let $common = common;
626                match sm {
627                    $crate::bus::pio::AnyStateMachine::Sm0($sm) => $body,
628                    $crate::bus::pio::AnyStateMachine::Sm1($sm) => $body,
629                    $crate::bus::pio::AnyStateMachine::Sm2($sm) => $body,
630                    $crate::bus::pio::AnyStateMachine::Sm3($sm) => $body,
631                }
632            }
633            $crate::bus::pio::PioAccess::Block1 { common, sm } => {
634                let $common = common;
635                match sm {
636                    $crate::bus::pio::AnyStateMachine::Sm0($sm) => $body,
637                    $crate::bus::pio::AnyStateMachine::Sm1($sm) => $body,
638                    $crate::bus::pio::AnyStateMachine::Sm2($sm) => $body,
639                    $crate::bus::pio::AnyStateMachine::Sm3($sm) => $body,
640                }
641            }
642            $crate::bus::pio::PioAccess::Block2 { common, sm } => {
643                let $common = common;
644                match sm {
645                    $crate::bus::pio::AnyStateMachine::Sm0($sm) => $body,
646                    $crate::bus::pio::AnyStateMachine::Sm1($sm) => $body,
647                    $crate::bus::pio::AnyStateMachine::Sm2($sm) => $body,
648                    $crate::bus::pio::AnyStateMachine::Sm3($sm) => $body,
649                }
650            }
651        }
652    };
653}