| // Licensed under the Apache-2.0 license |
| // SPDX-License-Identifier: Apache-2.0 |
| |
| //! I3C In-Band Interrupt (IBI) Work Queue |
| //! |
| //! Handles IBI events including Hot-Join, SIR (Slave Interrupt Request), |
| //! and target dynamic address assignment. |
| //! |
| //! Ported from `aspeed-rust/src/i3c/ibi.rs` @ ce3b567. |
| //! |
| //! **Porting delta (queue mechanism).** The reference uses `heapless::spsc` |
| //! `Producer`/`Consumer` handles, split once and parked in a global |
| //! `Mutex<UnsafeCell<..>>`. On this target (heapless 0.9 + this toolchain) we |
| //! observed unstable behavior when those handles were stored in a `static` and |
| //! later re-accessed across separate critical sections: a split that read back |
| //! `prod=Some, cons=Some` in-place would, after the consumer was taken in a |
| //! later critical section, read back `prod=None, cons=Some`. The root cause was |
| //! not fully isolated, so this port uses a simpler fixed-size ring buffer whose |
| //! aliasing and lifetime rules are easier to audit. |
| //! |
| //! So the SPSC split is replaced by a plain fixed-size ring buffer of |
| //! `Option<IbiWork>` (`IbiWork` is `Copy`, no niche pointers), guarded by the |
| //! same `critical_section`. The process-global queue/handler design (goal.md |
| //! ADR-3) is preserved — an ISR still cannot borrow a stack-owned device, so |
| //! the IBI plane stays global and uses one consumer per bus. |
| |
| use core::cell::UnsafeCell; |
| use core::sync::atomic::{AtomicBool, Ordering}; |
| use critical_section::Mutex; |
| |
| /// IBI queue depth |
| const IBIQ_DEPTH: usize = 16; |
| /// Maximum IBI payload data size |
| pub const IBI_DATA_MAX: u8 = 16; |
| /// Maximum private-write payload captured per [`IbiWork::TargetMasterWrite`]. |
| /// |
| /// The vendor C driver delivers the full write (heap-allocated per response); |
| /// this port has no heap, so the work item carries an inline buffer instead. |
| /// Writes longer than this are truncated (`len` reports the captured length); |
| /// the ISR drains the excess so the FIFO stays aligned. |
| /// |
| /// **Sizing is ISR-stack-bound, not RAM-bound.** `IbiWork` is passed by value |
| /// through the enqueue path, so the ISR transiently stacks roughly |
| /// `2 * (IBI_MWR_DATA_MAX + 8)` bytes per enqueue on top of the ISR's own |
| /// bounce buffer (also this size, see `isr_target_responses`). 128 was |
| /// empirically enough to HardFault the kernel handler stack on AST1060; 64 |
| /// keeps the total below the original 256-byte-bounce-buffer footprint while |
| /// quadrupling the old 16-byte payload cap. Static cost of the rings is |
| /// `(IBI_MWR_DATA_MAX + 8) * IBIQ_DEPTH * 4 buses` (~4.6 KiB at 64). |
| pub const IBI_MWR_DATA_MAX: usize = 64; |
| |
| // ============================================================================= |
| // IBI Work Item |
| // ============================================================================= |
| |
| /// IBI work item representing an interrupt event |
| #[derive(Debug, Clone, Copy)] |
| pub enum IbiWork { |
| /// One or more older work items were dropped because the queue was full. |
| Overflow, |
| /// Hot-Join request from a device |
| HotJoin, |
| /// Slave Interrupt Request |
| Sirq { |
| /// Address of requesting device |
| addr: u8, |
| /// Length of payload data |
| len: u8, |
| /// Payload data |
| data: [u8; IBI_DATA_MAX as usize], |
| }, |
| /// Target dynamic address assignment notification |
| TargetDaAssignment, |
| /// Private write received by this target from the controller. |
| TargetMasterWrite { |
| /// Number of received bytes captured in `data`. |
| len: u8, |
| /// Received data, truncated to [`IBI_MWR_DATA_MAX`]. |
| data: [u8; IBI_MWR_DATA_MAX], |
| }, |
| } |
| |
| // ============================================================================= |
| // Static Ring-Buffer Storage |
| // ============================================================================= |
| |
| /// Fixed-size single-producer/single-consumer ring of IBI work items. |
| /// |
| /// All access is serialized by the per-bus `critical_section::Mutex`, so the |
| /// indices need no atomics; the producer is the I3C ISR and the consumer is the |
| /// owning test/driver loop. |
| struct IbiRing { |
| buf: [Option<IbiWork>; IBIQ_DEPTH], |
| head: usize, |
| len: usize, |
| overflowed: bool, |
| } |
| |
| impl IbiRing { |
| const fn new() -> Self { |
| Self { |
| buf: [None; IBIQ_DEPTH], |
| head: 0, |
| len: 0, |
| overflowed: false, |
| } |
| } |
| |
| fn push(&mut self, work: IbiWork) -> bool { |
| self.head %= IBIQ_DEPTH; |
| if self.len >= IBIQ_DEPTH { |
| // Keep the newest IRQ information when the consumer falls behind. |
| self.head = (self.head + 1) % IBIQ_DEPTH; |
| self.len = IBIQ_DEPTH - 1; |
| self.overflowed = true; |
| } |
| let idx = (self.head + self.len) % IBIQ_DEPTH; |
| let Some(slot) = self.buf.get_mut(idx) else { |
| return false; |
| }; |
| *slot = Some(work); |
| self.len += 1; |
| true |
| } |
| |
| fn pop(&mut self) -> Option<IbiWork> { |
| if self.overflowed { |
| self.overflowed = false; |
| return Some(IbiWork::Overflow); |
| } |
| self.head %= IBIQ_DEPTH; |
| if self.len == 0 || self.len > IBIQ_DEPTH { |
| // Empty, or a corrupt length — treat as empty (panic-free). |
| return None; |
| } |
| let work = self.buf.get_mut(self.head).and_then(Option::take); |
| self.head = (self.head + 1) % IBIQ_DEPTH; |
| self.len -= 1; |
| work |
| } |
| } |
| |
| // INTENTIONAL EXCEPTION to borrow-arbitrated exclusivity (goal.md ADR-3): |
| // the IBI plane is process-global mutable state because the producer is the |
| // ISR, which cannot borrow a stack-owned device. Bounded (one fixed-depth |
| // ring per bus) and serialized by the critical section; access is via the |
| // leaf `ring_push`/`ring_pop` helpers only. |
| static IBI_RINGS: [Mutex<UnsafeCell<IbiRing>>; 4] = [ |
| Mutex::new(UnsafeCell::new(IbiRing::new())), |
| Mutex::new(UnsafeCell::new(IbiRing::new())), |
| Mutex::new(UnsafeCell::new(IbiRing::new())), |
| Mutex::new(UnsafeCell::new(IbiRing::new())), |
| ]; |
| |
| /// Enforce the single-consumer side of the per-bus SPSC queue. |
| static IBI_CONSUMER_CLAIMED: [AtomicBool; 4] = [ |
| AtomicBool::new(false), |
| AtomicBool::new(false), |
| AtomicBool::new(false), |
| AtomicBool::new(false), |
| ]; |
| |
| /// Push work; a full ring drops the oldest item and records an overflow event. |
| /// |
| /// The `&mut IbiRing` is confined to this leaf function — no caller-provided |
| /// code runs while it is live — so the exclusive borrow cannot be re-entered. |
| fn ring_push(bus: usize, work: IbiWork) -> bool { |
| let Some(workq) = IBI_RINGS.get(bus) else { |
| return false; |
| }; |
| critical_section::with(|cs| { |
| // SAFETY: the critical section excludes ISR/thread concurrency, and |
| // the `&mut IbiRing` never escapes this function (the ring is only |
| // reachable via `ring_push`/`ring_pop`, neither of which calls back |
| // into caller code), so this is the only live reference. |
| let ring: &mut IbiRing = unsafe { &mut *workq.borrow(cs).get() }; |
| ring.push(work) |
| }) |
| } |
| |
| /// Pop the next work item from the ring for `bus`, if any. |
| /// |
| /// Same confinement argument as [`ring_push`]. |
| fn ring_pop(bus: usize) -> Option<IbiWork> { |
| let workq = IBI_RINGS.get(bus)?; |
| critical_section::with(|cs| { |
| // SAFETY: see `ring_push` — critical section + leaf confinement make |
| // this the only live reference to the ring. |
| let ring: &mut IbiRing = unsafe { &mut *workq.borrow(cs).get() }; |
| ring.pop() |
| }) |
| } |
| |
| /// Discard pre-reset work for a bus. |
| pub fn i3c_ibi_workq_clear(bus: usize) { |
| while ring_pop(bus).is_some() {} |
| } |
| |
| // ============================================================================= |
| // Consumer Handle |
| // ============================================================================= |
| |
| /// Consumer handle for a bus's IBI work queue. |
| /// |
| /// Holds no state beyond the bus index; dequeuing reads the shared ring under |
| /// the critical section. Returned by [`i3c_ibi_workq_consumer`]. |
| pub struct IbiConsumer { |
| bus: usize, |
| } |
| |
| impl Drop for IbiConsumer { |
| fn drop(&mut self) { |
| if let Some(claimed) = IBI_CONSUMER_CLAIMED.get(self.bus) { |
| claimed.store(false, Ordering::Release); |
| } |
| } |
| } |
| |
| impl IbiConsumer { |
| /// Dequeue the next IBI work item, if any. |
| #[must_use] |
| pub fn dequeue(&mut self) -> Option<IbiWork> { |
| ring_pop(self.bus) |
| } |
| } |
| |
| /// Get the IBI work queue consumer for a bus. |
| /// |
| /// Returns `None` if the bus index is out of range. |
| #[must_use] |
| pub fn i3c_ibi_workq_consumer(bus: usize) -> Option<IbiConsumer> { |
| let claimed = IBI_CONSUMER_CLAIMED.get(bus)?; |
| claimed |
| .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire) |
| .ok()?; |
| Some(IbiConsumer { bus }) |
| } |
| |
| // ============================================================================= |
| // Enqueue Functions |
| // ============================================================================= |
| |
| /// Enqueue a target dynamic address assignment notification |
| #[must_use] |
| pub fn i3c_ibi_work_enqueue_target_da_assignment(bus: usize) -> bool { |
| ring_push(bus, IbiWork::TargetDaAssignment) |
| } |
| |
| /// Enqueue a Hot-Join notification |
| #[must_use] |
| pub fn i3c_ibi_work_enqueue_hotjoin(bus: usize) -> bool { |
| ring_push(bus, IbiWork::HotJoin) |
| } |
| |
| /// Enqueue an SIR. |
| #[must_use] |
| pub fn i3c_ibi_work_enqueue_target_irq(bus: usize, addr: u8, data: &[u8]) -> bool { |
| let mut ibi_buf = [0u8; IBI_DATA_MAX as usize]; |
| let take = core::cmp::min(IBI_DATA_MAX as usize, data.len()); |
| ibi_buf[..take].copy_from_slice(&data[..take]); |
| let work = IbiWork::Sirq { |
| addr, |
| len: u8::try_from(take).unwrap_or(IBI_DATA_MAX), |
| data: ibi_buf, |
| }; |
| ring_push(bus, work) |
| } |
| |
| /// Enqueue a private write received by this target from the controller. |
| #[must_use] |
| pub fn i3c_ibi_work_enqueue_target_master_write(bus: usize, data: &[u8]) -> bool { |
| let mut buf = [0u8; IBI_MWR_DATA_MAX]; |
| let take = core::cmp::min(IBI_MWR_DATA_MAX, data.len()); |
| buf[..take].copy_from_slice(&data[..take]); |
| let work = IbiWork::TargetMasterWrite { |
| len: u8::try_from(take).unwrap_or(u8::MAX), |
| data: buf, |
| }; |
| ring_push(bus, work) |
| } |