| // Licensed under the Apache-2.0 license |
| // SPDX-License-Identifier: Apache-2.0 |
| |
| //! I3C Controller |
| //! |
| //! Main hardware abstraction for I3C bus controller. |
| //! |
| //! # Lifecycle |
| //! |
| //! Two states, matching the SMC peripheral's `Uninitialized -> Ready` |
| //! precedent: |
| //! |
| //! | State | Entered by | Available operations | |
| //! |-------|-----------|----------------------| |
| //! | [`Uninitialized`] | [`I3cController::new`] | [`start()`](I3cController::start) | |
| //! | [`Ready`] | `start()` (IRQ trampoline claimed + hardware programmed) | bus operations | |
| //! |
| //! After `start()` the integration layer unmasks the NVIC line it owns (it |
| //! selected the bus, so it knows the matching platform interrupt line); |
| //! the driver never touches the NVIC. |
| //! |
| //! # ISR decoupling |
| //! |
| //! The ISR shares **no** `&mut` state with this controller: at `start()` the |
| //! driver parks an ISR-owned register handle plus the role flag in the |
| //! per-bus registry (`hardware::register_i3c_irq_handler`), and the ISR |
| //! communicates back exclusively through per-bus atomics and the global IBI |
| //! work rings (the SMC flag-and-defer model). The controller is therefore a |
| //! plain owned value — no pinning, no `'static` storage, no raw context |
| //! pointer. |
| //! |
| //! # Example |
| //! |
| //! ```rust,ignore |
| //! // === BOOT/INIT CODE (runs once) === |
| //! // Platform init first (clocks, resets - not part of i3c_core) |
| //! scu.enable_i3c_clock(bus); |
| //! scu.deassert_i3c_reset(bus); |
| //! |
| //! let hw = unsafe { Ast1060I3c::new(bus, yield_fn) }.ok_or(...)?; |
| //! let mut ctrl = I3cController::new(hw, &mut config) |
| //! .start()?; // register ISR ctx (single-shot) + program hardware |
| //! |
| //! // Integration layer owns the NVIC line; unmask it now. |
| //! unsafe { NVIC::unmask(integration_owned_irq_line) }; |
| //! |
| //! ctrl.priv_write(pid, &mut data)?; |
| //! ``` |
| |
| use core::marker::PhantomData; |
| |
| use super::ccc; |
| use super::config::{DaState, DeviceEntry, I3cConfig, I3cTargetConfig, IbiState}; |
| use super::constants::{I3C_BROADCAST_ADDR, MAX_PRIV_XFER_CMDS, MAX_XFER_DATA_LEN}; |
| use super::error::I3cError; |
| use super::hardware::HardwareInterface; |
| use super::types::{DevKind, I2cOp, I3cIbi, I3cIbiType, I3cMsg}; |
| use embedded_hal::i2c::SevenBitAddress; |
| |
| // ============================================================================= |
| // Lifecycle states |
| // ============================================================================= |
| |
| /// Initial state: nothing registered, no I/O done. |
| pub struct Uninitialized; |
| /// IRQ trampoline claimed and hardware programmed; bus operations available. |
| /// The integration layer unmasks the NVIC line it owns after entering this |
| /// state. |
| pub struct Ready; |
| |
| // ============================================================================= |
| // Controller shell |
| // ============================================================================= |
| |
| /// I3C controller: a plain owned value over the hardware driver, borrowing |
| /// the caller's configuration. No pinning or `'static` storage is required — |
| /// the ISR never holds a pointer into this object (see the module docs). |
| /// |
| /// The configuration is **borrowed** (`&'c mut I3cConfig`), not owned: the |
| /// config embeds the device tables (~0.5 KiB), and the typestate transition |
| /// (`start(self) -> Self<Ready>`) moves the controller by value — owning the |
| /// config would transiently stack two copies inside one frame, which the |
| /// 2 KiB kernel bootstrap stack cannot afford. Borrowing keeps exactly one |
| /// config alive, wherever the caller placed it. |
| pub struct I3cController<'c, H: HardwareInterface, S = Uninitialized> { |
| hw: H, |
| config: &'c mut I3cConfig, |
| _state: PhantomData<S>, |
| } |
| |
| impl<'c, H: HardwareInterface, S> I3cController<'c, H, S> { |
| /// Split-borrow helper for operations that drive `hw` with `config`. |
| #[inline] |
| fn parts(&mut self) -> (&mut H, &mut I3cConfig) { |
| (&mut self.hw, &mut *self.config) |
| } |
| |
| /// Return this controller's bus number. |
| #[inline] |
| #[must_use] |
| pub fn bus_num(&self) -> u8 { |
| self.hw.bus_num() |
| } |
| } |
| |
| impl<'c, H: HardwareInterface> I3cController<'c, H, Uninitialized> { |
| /// Bundle hardware and a borrowed configuration. No I/O, no registration. |
| #[must_use] |
| pub fn new(hw: H, config: &'c mut I3cConfig) -> Self { |
| Self { |
| hw, |
| config, |
| _state: PhantomData, |
| } |
| } |
| |
| /// Bring the controller up: park this bus's ISR context in the registry |
| /// (single-shot per bus) and program the hardware. |
| /// |
| /// The target/kernel owns the top-level interrupt vector; its ISR calls |
| /// [`dispatch_i3c_irq`](super::hardware::dispatch_i3c_irq), which services |
| /// the bus through the registered context. On return the device may |
| /// assert its IRQ line; nothing is delivered until the integration layer |
| /// unmasks the NVIC line it owns. |
| /// |
| /// Returns [`I3cError::Busy`] if the bus's IRQ slot was already claimed by |
| /// another controller, or [`I3cError::Timeout`] if the hardware's initial |
| /// queue-reset poll timed out. |
| pub fn start(mut self) -> Result<I3cController<'c, H, Ready>, I3cError> { |
| // Reject invalid timing before programming hardware. |
| self.config.validate_clock()?; |
| if let Some(static_addr) = self |
| .config |
| .target_config |
| .as_ref() |
| .and_then(|target| target.static_addr) |
| && (static_addr >= I3C_BROADCAST_ADDR || self.config.addrbook.is_reserved(static_addr)) |
| { |
| return Err(I3cError::InvalidArgs); |
| } |
| |
| let bus = self.hw.bus_num() as usize; |
| let ctx = self.hw.isr_ctx(self.config.is_secondary); |
| if !super::hardware::register_i3c_irq_handler(bus, ctx) { |
| return Err(I3cError::Busy); |
| } |
| |
| if let Err(e) = self.hw.init(self.config) { |
| // Release the just-claimed slot, or every retry of `start()` |
| // would fail with `Busy` against a controller that never came up. |
| super::hardware::unregister_i3c_irq_handler(bus); |
| return Err(e); |
| } |
| // Memory barrier so init writes are visible before the integration |
| // layer unmasks the IRQ line. |
| cortex_m::asm::dmb(); |
| |
| Ok(I3cController { |
| hw: self.hw, |
| config: self.config, |
| _state: PhantomData, |
| }) |
| } |
| } |
| |
| // ============================================================================= |
| // Bus operations (Ready) |
| // ============================================================================= |
| |
| impl<'c, H: HardwareInterface> I3cController<'c, H, Ready> { |
| #[inline] |
| fn ensure_primary(&self) -> Result<(), I3cError> { |
| if self.config.is_secondary { |
| Err(I3cError::Access) |
| } else { |
| Ok(()) |
| } |
| } |
| |
| // ========================================================================= |
| // Device Management |
| // ========================================================================= |
| |
| /// Attach an I3C device to the bus |
| /// |
| /// # Arguments |
| /// * `pid` - Provisional ID of the device |
| /// * `desired_da` - Desired dynamic address |
| /// * `slot` - DAT slot to use |
| pub fn attach_i3c_dev(&mut self, pid: u64, desired_da: u8, slot: u8) -> Result<(), I3cError> { |
| self.ensure_primary()?; |
| let (hw, config) = self.parts(); |
| if desired_da == 0 || desired_da >= I3C_BROADCAST_ADDR { |
| return Err(I3cError::InvalidArgs); |
| } |
| // GETPID returns a 48-bit value. |
| if pid >= (1u64 << 48) { |
| return Err(I3cError::InvalidArgs); |
| } |
| // Enforce both software and hardware DAT limits. |
| if usize::from(slot) >= super::constants::MAX_DEVICES_PER_BUS |
| || u16::from(slot) >= config.maxdevs |
| { |
| return Err(I3cError::InvalidArgs); |
| } |
| if config |
| .attached |
| .by_pos |
| .get(usize::from(slot)) |
| .copied() |
| .flatten() |
| .is_some() |
| { |
| return Err(I3cError::DevAlreadyAttached); |
| } |
| if config.addrbook.is_reserved(desired_da) { |
| return Err(I3cError::InvalidArgs); |
| } |
| if !config.addrbook.is_free(desired_da) { |
| return Err(I3cError::AddrInUse); |
| } |
| if config |
| .attached |
| .devices |
| .iter() |
| .any(|d| d.dyn_addr == desired_da) |
| { |
| return Err(I3cError::AddrInUse); |
| } |
| // PID lookups require uniqueness. |
| if config.attached.devices.iter().any(|d| d.pid == Some(pid)) { |
| return Err(I3cError::DevAlreadyAttached); |
| } |
| |
| // Program hardware before committing bookkeeping. |
| hw.attach_i3c_dev(slot.into(), desired_da) |
| .map_err(|_| I3cError::AddrInUse)?; |
| |
| let dev = DeviceEntry { |
| kind: DevKind::I3c, |
| pid: Some(pid), |
| static_addr: 0, |
| dyn_addr: desired_da, |
| desired_da, |
| bcr: 0, |
| dcr: 0, |
| maxrd: 0, |
| maxwr: 0, |
| mrl: 0, |
| mwl: 0, |
| max_ibi: 0, |
| ibi_state: IbiState::Disabled, |
| pos: Some(slot), |
| da_state: DaState::Unassigned, |
| }; |
| |
| let idx = match config.attached.attach(dev) { |
| Ok(idx) => idx, |
| Err(_) => { |
| hw.detach_i3c_dev(slot.into()); |
| return Err(I3cError::NoFreeSlot); |
| } |
| }; |
| let idx_u8 = match u8::try_from(idx) { |
| Ok(idx) => idx, |
| Err(_) => { |
| config.attached.detach(idx); |
| hw.detach_i3c_dev(slot.into()); |
| return Err(I3cError::InvalidArgs); |
| } |
| }; |
| if !config.attached.map_pos(slot, idx_u8) { |
| config.attached.detach(idx); |
| hw.detach_i3c_dev(slot.into()); |
| return Err(I3cError::NoDatPos); |
| } |
| config.addrbook.mark_use(desired_da, true); |
| Ok(()) |
| } |
| |
| /// Attach a legacy I2C device to the bus. |
| /// |
| /// The DAT slot is programmed with the device's static address and the |
| /// legacy-I2C marker; transfers then go through |
| /// [`i2c_write`](Self::i2c_write)/[`i2c_read`](Self::i2c_read)/ |
| /// [`i2c_write_read`](Self::i2c_write_read) or the |
| /// `embedded_hal::i2c::I2c` impl. Detach with |
| /// [`detach_i3c_dev`](Self::detach_i3c_dev) (by slot) or |
| /// [`detach_i3c_dev_by_idx`](Self::detach_i3c_dev_by_idx). |
| pub fn attach_i2c_dev(&mut self, static_addr: u8, slot: u8) -> Result<(), I3cError> { |
| self.ensure_primary()?; |
| let (hw, config) = self.parts(); |
| if static_addr == 0 || static_addr >= I3C_BROADCAST_ADDR { |
| return Err(I3cError::InvalidArgs); |
| } |
| // Reject positions outside the implemented DAT. |
| if usize::from(slot) >= super::constants::MAX_DEVICES_PER_BUS |
| || u16::from(slot) >= config.maxdevs |
| { |
| return Err(I3cError::InvalidArgs); |
| } |
| if config |
| .attached |
| .by_pos |
| .get(usize::from(slot)) |
| .copied() |
| .flatten() |
| .is_some() |
| { |
| return Err(I3cError::DevAlreadyAttached); |
| } |
| if config.i2c_scl_hz == 0 { |
| return Err(I3cError::InvalidParam); |
| } |
| if !config.addrbook.is_free(static_addr) { |
| return Err(if config.addrbook.is_reserved(static_addr) { |
| I3cError::InvalidArgs |
| } else { |
| I3cError::AddrInUse |
| }); |
| } |
| if config |
| .attached |
| .devices |
| .iter() |
| .any(|d| d.dyn_addr == static_addr) |
| { |
| return Err(I3cError::AddrInUse); |
| } |
| |
| // Program hardware before committing bookkeeping. |
| hw.attach_i2c_dev(slot.into(), static_addr)?; |
| |
| let mut dev = DeviceEntry::new_i2c(static_addr); |
| dev.pos = Some(slot); |
| let idx = match config.attached.attach(dev) { |
| Ok(idx) => idx, |
| Err(e) => { |
| hw.detach_i3c_dev(slot.into()); |
| return Err(e); |
| } |
| }; |
| let idx_u8 = match u8::try_from(idx) { |
| Ok(idx) => idx, |
| Err(_) => { |
| config.attached.detach(idx); |
| hw.detach_i3c_dev(slot.into()); |
| return Err(I3cError::InvalidArgs); |
| } |
| }; |
| if !config.attached.map_pos(slot, idx_u8) { |
| config.attached.detach(idx); |
| hw.detach_i3c_dev(slot.into()); |
| return Err(I3cError::NoDatPos); |
| } |
| // The static address occupies the same 7-bit space as dynamic ones. |
| config.addrbook.mark_use(static_addr, true); |
| Ok(()) |
| } |
| |
| /// Write to a legacy I2C device (by static address). |
| pub fn i2c_write(&mut self, static_addr: u8, data: &[u8]) -> Result<(), I3cError> { |
| self.ensure_primary()?; |
| let (hw, config) = self.parts(); |
| let pos = config |
| .attached |
| .pos_of_static_addr(static_addr) |
| .ok_or(I3cError::NoSuchDev)?; |
| let mut ops = [I2cOp::Write(data)]; |
| hw.i2c_priv_xfer(config, pos, &mut ops) |
| } |
| |
| /// Read from a legacy I2C device (by static address). `out` is filled |
| /// completely on success. |
| pub fn i2c_read(&mut self, static_addr: u8, out: &mut [u8]) -> Result<(), I3cError> { |
| self.ensure_primary()?; |
| let (hw, config) = self.parts(); |
| let pos = config |
| .attached |
| .pos_of_static_addr(static_addr) |
| .ok_or(I3cError::NoSuchDev)?; |
| let mut ops = [I2cOp::Read(out)]; |
| hw.i2c_priv_xfer(config, pos, &mut ops) |
| } |
| |
| /// Write then read (repeated START between) on a legacy I2C device. |
| pub fn i2c_write_read( |
| &mut self, |
| static_addr: u8, |
| data: &[u8], |
| out: &mut [u8], |
| ) -> Result<(), I3cError> { |
| self.ensure_primary()?; |
| let (hw, config) = self.parts(); |
| let pos = config |
| .attached |
| .pos_of_static_addr(static_addr) |
| .ok_or(I3cError::NoSuchDev)?; |
| let mut ops = [I2cOp::Write(data), I2cOp::Read(out)]; |
| hw.i2c_priv_xfer(config, pos, &mut ops) |
| } |
| |
| /// Detach an I3C device by DAT position |
| pub fn detach_i3c_dev(&mut self, pos: usize) -> Result<(), I3cError> { |
| self.ensure_primary()?; |
| // Never touch an aliased or unimplemented DAT position. |
| if pos >= super::constants::MAX_DEVICES_PER_BUS || pos >= usize::from(self.config.maxdevs) { |
| return Ok(()); |
| } |
| let (hw, config) = self.parts(); |
| // Release the dynamic address (parity with `detach_i3c_dev_by_idx`), |
| // or detaching by position would leak it in the address book forever. |
| let da = config |
| .attached |
| .by_pos |
| .get(pos) |
| .copied() |
| .flatten() |
| .and_then(|idx| config.attached.devices.get(usize::from(idx))) |
| .map(|dev| (dev.dyn_addr, dev.da_state)); |
| if da.is_some_and(|(_, state)| state == DaState::Unknown) { |
| return Err(I3cError::Busy); |
| } |
| if let Some((da, _)) = da |
| && da != 0 |
| { |
| config.addrbook.mark_use(da, false); |
| } |
| config.attached.detach_by_pos(pos); |
| hw.detach_i3c_dev(pos); |
| Ok(()) |
| } |
| |
| /// Detach an I3C device by device index |
| pub fn detach_i3c_dev_by_idx(&mut self, dev_idx: usize) -> Result<(), I3cError> { |
| self.ensure_primary()?; |
| let (hw, config) = self.parts(); |
| // `get` (not `[dev_idx]`) keeps this panic-free for the `no_panics` |
| // analysis; an out-of-range index is simply a no-op. |
| let Some(dev) = config.attached.devices.get(dev_idx) else { |
| return Ok(()); |
| }; |
| if dev.da_state == DaState::Unknown { |
| return Err(I3cError::Busy); |
| } |
| |
| if dev.dyn_addr != 0 { |
| let dyn_addr = dev.dyn_addr; |
| config.addrbook.mark_use(dyn_addr, false); |
| } |
| |
| let dev_pos = config.attached.devices.get(dev_idx).and_then(|dev| dev.pos); |
| if let Some(pos) = dev_pos { |
| hw.detach_i3c_dev(pos.into()); |
| } |
| |
| config.attached.detach(dev_idx); |
| Ok(()) |
| } |
| |
| // ========================================================================= |
| // Bus Recovery |
| // ========================================================================= |
| |
| /// Recover the I3C bus from a stuck state |
| /// |
| /// Performs bus recovery sequence: |
| /// 1. Enter software (bit-bang) mode |
| /// 2. Toggle SCL to clear stuck slaves |
| /// 3. Generate STOP condition |
| /// 4. Exit software mode |
| /// |
| /// # Arguments |
| /// * `scl_toggles` - Number of SCL toggles (typically 9 to clear a stuck byte) |
| /// |
| /// # When to Use |
| /// |
| /// - Bus appears hung (transfers timing out) |
| /// - Device not responding after partial transfer |
| /// - After detecting SDA stuck low |
| /// |
| /// # Example |
| /// |
| /// ```rust,ignore |
| /// // Standard recovery with 9 SCL clocks |
| /// ctrl.recover_bus(9); |
| /// |
| /// // More aggressive recovery |
| /// ctrl.recover_bus(18); |
| /// ``` |
| pub fn recover_bus(&mut self, scl_toggles: u32) -> Result<(), I3cError> { |
| self.ensure_primary()?; |
| let (hw, _) = self.parts(); |
| hw.enter_sw_mode(); |
| hw.i3c_toggle_scl_in(scl_toggles); |
| hw.gen_internal_stop(); |
| hw.exit_sw_mode(); |
| Ok(()) |
| } |
| |
| /// Perform full bus recovery with controller reset |
| /// |
| /// More aggressive recovery that also resets controller FIFOs: |
| /// 1. Bus recovery (SCL toggle + STOP) |
| /// 2. Reset TX/RX FIFOs |
| /// 3. Reset command queue |
| /// |
| /// # Arguments |
| /// * `reset_mask` - Controller components to reset (use `RESET_CTRL_*` constants) |
| /// |
| /// # Example |
| /// |
| /// ```rust,ignore |
| /// use aspeed_rust::i3c_core::{RESET_CTRL_RX_FIFO, RESET_CTRL_TX_FIFO, RESET_CTRL_CMD_QUEUE}; |
| /// |
| /// // Full recovery with FIFO reset |
| /// let reset = RESET_CTRL_RX_FIFO | RESET_CTRL_TX_FIFO | RESET_CTRL_CMD_QUEUE; |
| /// ctrl.recover_bus_full(reset)?; |
| /// ``` |
| /// |
| /// # Errors |
| /// |
| /// [`I3cError::Timeout`] if the controller reset bits did not self-clear — |
| /// the engine is wedged beyond what software recovery can fix. |
| pub fn recover_bus_full(&mut self, reset_mask: u32) -> Result<(), I3cError> { |
| self.recover_bus(8)?; |
| let (hw, _) = self.parts(); |
| let result = hw.reset_ctrl(reset_mask); |
| if result.is_err() { |
| // Block transfers until a successful init. |
| hw.mark_xfer_faulted(); |
| } |
| result |
| } |
| |
| // ========================================================================= |
| // Accessors |
| // ========================================================================= |
| |
| /// Allocate a dynamic address from `start_addr`. |
| #[inline] |
| pub fn alloc_dynamic_address_from(&mut self, start_addr: u8) -> Option<u8> { |
| if self.ensure_primary().is_err() { |
| return None; |
| } |
| let (_, config) = self.parts(); |
| config.addrbook.alloc_from(start_addr) |
| } |
| |
| /// Return the currently assigned target dynamic address, if any. |
| /// |
| /// The address is assigned by the bus master and latched by the ISR into |
| /// the per-bus event block; the locally configured address (if any) is |
| /// the fallback. |
| #[inline] |
| #[must_use] |
| pub fn target_dynamic_address(&self) -> Option<u8> { |
| let addr = super::hardware::isr_events(self.hw.bus_num() as usize) |
| .dyn_addr() |
| .or_else(|| self.config.target_config.as_ref().and_then(|t| t.addr))?; |
| // Filter stale or reserved values. |
| (addr != 0 && !self.config.addrbook.is_reserved(addr)).then_some(addr) |
| } |
| |
| /// Max read/write lengths `(mrl, mwl)` the bus master pushed to this |
| /// target via SETMRL/SETMWL, if any update was observed. |
| #[inline] |
| #[must_use] |
| pub fn target_max_lengths(&self) -> Option<(u16, u16)> { |
| super::hardware::isr_events(self.hw.bus_num() as usize).max_len() |
| } |
| |
| /// Set the device's IBI mandatory data byte and enable IBI delivery for `addr`. |
| pub fn enable_ibi(&mut self, addr: u8, mdb: u8) -> Result<(), I3cError> { |
| self.ensure_primary()?; |
| let (hw, config) = self.parts(); |
| hw.set_ibi_mdb(mdb); |
| let result = hw.ibi_enable(config, addr); |
| if let Some(dev) = config |
| .attached |
| .devices |
| .iter_mut() |
| .find(|d| d.dyn_addr == addr) |
| { |
| dev.ibi_state = match result { |
| Ok(()) => IbiState::Enabled, |
| Err(I3cError::Timeout | I3cError::RespError) => IbiState::Unknown, |
| Err(_) => dev.ibi_state, |
| }; |
| } |
| result |
| } |
| |
| /// Disable IBI delivery for `addr` (DISEC + reject its SIRs). |
| pub fn disable_ibi(&mut self, addr: u8) -> Result<(), I3cError> { |
| self.ensure_primary()?; |
| let (hw, config) = self.parts(); |
| let result = hw.ibi_disable(config, addr); |
| if let Some(dev) = config |
| .attached |
| .devices |
| .iter_mut() |
| .find(|d| d.dyn_addr == addr) |
| { |
| dev.ibi_state = match result { |
| Ok(()) => IbiState::Disabled, |
| Err(I3cError::Timeout | I3cError::RespError | I3cError::AddressNack) => { |
| IbiState::Unknown |
| } |
| Err(_) => dev.ibi_state, |
| }; |
| } |
| result |
| } |
| |
| /// Re-run full hardware initialization and rebuild attached DAT entries. |
| pub fn reinit(&mut self) -> Result<(), I3cError> { |
| let (hw, config) = self.parts(); |
| hw.init(config)?; |
| for dev in &mut config.attached.devices { |
| dev.ibi_state = IbiState::Disabled; |
| } |
| for i in 0..config.attached.devices.len() { |
| let Some(dev) = config.attached.devices.get(i) else { |
| continue; |
| }; |
| if let Some(pos) = dev.pos { |
| let result = match dev.kind { |
| DevKind::I3c => hw.attach_i3c_dev(pos.into(), dev.dyn_addr), |
| DevKind::I2c => hw.attach_i2c_dev(pos.into(), dev.static_addr), |
| }; |
| if result.is_err() { |
| hw.mark_xfer_faulted(); |
| } |
| result?; |
| } |
| } |
| cortex_m::asm::dmb(); |
| Ok(()) |
| } |
| |
| /// Issue a private read to `pid`, returning the number of received bytes. |
| pub fn priv_read(&mut self, pid: u64, out: &mut [u8]) -> Result<u32, I3cError> { |
| self.ensure_primary()?; |
| let (hw, config) = self.parts(); |
| let actual_len = u32::try_from(out.len()).map_err(|_| I3cError::InvalidArgs)?; |
| let mut msgs = [I3cMsg { |
| buf: Some(out), |
| actual_len, |
| num_xfer: 0, |
| flags: super::constants::I3C_MSG_READ | super::constants::I3C_MSG_STOP, |
| hdr_mode: 0, |
| hdr_cmd_mode: 0, |
| }]; |
| hw.priv_xfer(config, pid, &mut msgs)?; |
| Ok(msgs[0].actual_len) |
| } |
| |
| /// Issue a private write to `pid`. |
| pub fn priv_write(&mut self, pid: u64, data: &mut [u8]) -> Result<(), I3cError> { |
| self.ensure_primary()?; |
| let (hw, config) = self.parts(); |
| let actual_len = u32::try_from(data.len()).map_err(|_| I3cError::InvalidArgs)?; |
| let mut msgs = [I3cMsg { |
| buf: Some(data), |
| actual_len, |
| num_xfer: 0, |
| flags: super::constants::I3C_MSG_WRITE | super::constants::I3C_MSG_STOP, |
| hdr_mode: 0, |
| hdr_cmd_mode: 0, |
| }]; |
| hw.priv_xfer(config, pid, &mut msgs) |
| } |
| |
| /// Raise a hot-join request from the target side. |
| pub fn target_raise_hot_join(&mut self) -> Result<(), I3cError> { |
| let (hw, config) = self.parts(); |
| hw.target_ibi_raise_hj(config) |
| } |
| |
| // ========================================================================= |
| // Master / Target operations (Delta D1) |
| // ========================================================================= |
| // |
| // The reference exposed these through `proposed_traits::i3c_master::I3c` |
| // and the `proposed_traits` target traits (`aspeed-rust/src/i3c/hal_impl.rs`). |
| // That crate is unavailable in openprot and embedded-hal 1.0 defines no I3C |
| // trait, so — as the I2C port did for `proposed_traits::i2c_target` — the |
| // logic is preserved verbatim here as **inherent methods**. The only change |
| // is that `ErrorKind`-mapped errors become direct `I3cError` variants |
| // (`DynamicAddressConflict` -> `AddrInUse`, `InvalidCcc` -> `Invalid`). |
| |
| /// Assign a dynamic address to the device at `static_address` via ENTDAA, |
| /// then read back PID/BCR and enable IBI. Returns the assigned address. |
| pub fn assign_dynamic_address( |
| &mut self, |
| static_address: SevenBitAddress, |
| ) -> Result<SevenBitAddress, I3cError> { |
| self.ensure_primary()?; |
| let (hw, config) = self.parts(); |
| let slot = config |
| .attached |
| .pos_of_addr(static_address) |
| .ok_or(I3cError::AddrInUse)?; |
| |
| let dev_idx = config |
| .attached |
| .find_dev_idx_by_addr(static_address) |
| .ok_or(I3cError::Other)?; |
| |
| // Do not repeat ENTDAA for an address that may already be claimed. |
| let already_claimed = config |
| .attached |
| .devices |
| .get(dev_idx) |
| .is_some_and(|d| d.da_state != DaState::Unassigned); |
| if !already_claimed { |
| match hw.do_entdaa(config, slot.into()) { |
| Ok(()) => config.mark_da_unknown(dev_idx, None), |
| Err(e @ (I3cError::Timeout | I3cError::RespError)) => { |
| // The target may have latched the DAT address before the |
| // controller lost completion. |
| config.mark_da_unknown(dev_idx, None); |
| return Err(e); |
| } |
| Err(e) => return Err(e), |
| } |
| } |
| |
| let pid = ccc::ccc_getpid(hw, config, static_address)?; |
| |
| let old_pid = config |
| .attached |
| .devices |
| .get(dev_idx) |
| .ok_or(I3cError::Other)? |
| .pid; |
| |
| if let Some(op) = old_pid |
| && pid != op |
| { |
| // Preserve the claimed state after a PID mismatch. |
| if let Some(dev) = config.attached.devices.get_mut(dev_idx) { |
| dev.da_state = DaState::Unknown; |
| } |
| return Err(I3cError::Other); |
| } |
| |
| let bcr = ccc::ccc_getbcr(hw, config, static_address)?; |
| // DCR is optional; only an address NACK is benign. |
| let dcr = match ccc::ccc_getdcr(hw, config, static_address) { |
| Ok(dcr) => dcr, |
| Err(I3cError::AddressNack) => 0, |
| Err(e) => return Err(e), |
| }; |
| |
| { |
| let dev = config |
| .attached |
| .devices |
| .get_mut(dev_idx) |
| .ok_or(I3cError::Other)?; |
| |
| dev.pid = Some(pid); |
| dev.bcr = bcr; |
| dev.dcr = dcr; |
| dev.da_state = DaState::Verified; |
| } |
| |
| let dyn_addr: SevenBitAddress = config |
| .attached |
| .devices |
| .get(dev_idx) |
| .ok_or(I3cError::Other)? |
| .dyn_addr; |
| |
| let ibi_result = hw.ibi_enable(config, dyn_addr); |
| if let Some(dev) = config.attached.devices.get_mut(dev_idx) { |
| dev.ibi_state = match ibi_result { |
| Ok(()) => IbiState::Enabled, |
| Err(I3cError::Timeout | I3cError::RespError) => IbiState::Unknown, |
| Err(_) => dev.ibi_state, |
| }; |
| } |
| ibi_result?; |
| |
| Ok(dyn_addr) |
| } |
| |
| /// Run dynamic address assignment for every attached I3C device. |
| /// |
| /// Multi-device ENTDAA orchestration ported from the vendor C driver's |
| /// `aspeed_i3c_do_daa`. Walks the DAT slots that still need a verified |
| /// assignment, lets one device win each ENTDAA, reads its PID back and |
| /// corrects the two failure shapes the single-device |
| /// [`assign_dynamic_address`](Self::assign_dynamic_address) cannot: |
| /// |
| /// - **Mis-assignment**: with several unassigned targets on the bus, any |
| /// of them may answer the ENTDAA issued for another device's slot (bus |
| /// arbitration picks the winner). The winner is moved to its own |
| /// expected address via SETNEWDA and the slot is retried for its |
| /// intended owner. |
| /// - **Unsolicited device**: a target whose PID matches no attached entry |
| /// is parked on a freshly allocated address so it stops answering |
| /// subsequent ENTDAAs. |
| /// |
| /// Returns the number of devices verified in this run, but only when every |
| /// attached device that needed an address was verified. If ENTDAA reports |
| /// no more responders while entries are still pending, returns |
| /// [`I3cError::DaaNack`] instead of reporting a partial success. IBIs are |
| /// not enabled here — call [`enable_ibi`](Self::enable_ibi) per device |
| /// afterwards. |
| pub fn bus_daa(&mut self) -> Result<u32, I3cError> { |
| self.ensure_primary()?; |
| let (hw, config) = self.parts(); |
| let ndevs = config.attached.by_pos.len(); |
| |
| // DAT positions still needing a verified assignment. |
| let mut need: u32 = 0; |
| for idx in 0..config.attached.devices.len() { |
| let Some(dev) = config.attached.devices.get(idx) else { |
| continue; |
| }; |
| if dev.kind == DevKind::I3c |
| && dev.pid.is_some() |
| && dev.da_state == DaState::Unassigned |
| && let Some(pos) = dev.pos |
| { |
| // pos < 8 enforced by attach_i3c_dev. |
| need |= 1u32 << pos; |
| } |
| } |
| |
| let mut verified = 0u32; |
| let mut pos = 0usize; |
| let mut normal_nack = false; |
| // Hang guard only: every lap either clears a `need` bit, parks an |
| // unsolicited device (finite), or exits via the ENTDAA break below. |
| let mut budget = 8 * (ndevs as u32); |
| while need != 0 && budget != 0 { |
| budget -= 1; |
| |
| if need & (1u32 << pos) == 0 { |
| pos = (pos + 1) % ndevs; |
| continue; |
| } |
| |
| // The address the ENTDAA winner will latch: the DAT slot was |
| // programmed with its owner's desired address at attach time. |
| let Some(addr) = config |
| .attached |
| .by_pos |
| .get(pos) |
| .copied() |
| .flatten() |
| .and_then(|idx| config.attached.devices.get(usize::from(idx))) |
| .map(|d| d.desired_da) |
| else { |
| // Stale bit with no mapped device — drop it. |
| need &= !(1u32 << pos); |
| pos = (pos + 1) % ndevs; |
| continue; |
| }; |
| |
| let provisional_idx = config |
| .attached |
| .by_pos |
| .get(pos) |
| .copied() |
| .flatten() |
| .map(usize::from); |
| |
| match hw.do_entdaa(config, pos as u32) { |
| Ok(()) => {} |
| // Address NACK normally ends DAA. |
| Err(I3cError::DaaNack) => { |
| normal_nack = true; |
| break; |
| } |
| Err(e @ (I3cError::Timeout | I3cError::RespError)) => { |
| // ENTDAA may have assigned this slot before completion was |
| // lost. Never retry it as unassigned. |
| if let Some(idx) = provisional_idx { |
| config.mark_da_unknown(idx, None); |
| } |
| return Err(e); |
| } |
| Err(e) => return Err(e), |
| } |
| |
| // Mark the claimed address unknown until GETPID identifies it. |
| if let Some(idx) = provisional_idx { |
| config.mark_da_unknown(idx, None); |
| } |
| |
| let pid = match ccc::ccc_getpid(hw, config, addr) { |
| Ok(pid) => pid, |
| // The address is claimed; retrying ENTDAA could collide. |
| Err(e) => { |
| // Only RSTDAA can safely clear this unknown owner. |
| if let Some(dev) = |
| provisional_idx.and_then(|idx| config.attached.devices.get_mut(idx)) |
| { |
| dev.da_state = DaState::Unknown; |
| } |
| return Err(e); |
| } |
| }; |
| |
| let owner = config |
| .attached |
| .devices |
| .iter() |
| .position(|d| d.pid == Some(pid)); |
| match owner { |
| Some(idx) => { |
| let expected = config |
| .attached |
| .devices |
| .get(idx) |
| .map_or(addr, |d| d.desired_da); |
| if expected == addr { |
| // The intended device answered its own slot. |
| let bcr = ccc::ccc_getbcr(hw, config, addr)?; |
| let dcr = match ccc::ccc_getdcr(hw, config, addr) { |
| Ok(dcr) => dcr, |
| Err(I3cError::AddressNack) => 0, |
| Err(e) => return Err(e), |
| }; |
| if let Some(dev) = config.attached.devices.get_mut(idx) { |
| dev.bcr = bcr; |
| dev.dcr = dcr; |
| dev.da_state = DaState::Verified; |
| } |
| need &= !(1u32 << pos); |
| verified += 1; |
| } else { |
| let setnewda_result = |
| ccc::ccc_setnewda_bus_only(hw, config, addr, expected); |
| // Probe ambiguous moves before freeing the old slot. |
| let move_ambiguous = matches!( |
| setnewda_result, |
| Err(I3cError::Timeout | I3cError::RespError) |
| ); |
| if setnewda_result.is_ok() || move_ambiguous { |
| if setnewda_result.is_ok() |
| && let Some(dev) = |
| provisional_idx.and_then(|i| config.attached.devices.get_mut(i)) |
| { |
| dev.da_state = DaState::Unassigned; |
| } |
| if let Some(dev) = config.attached.devices.get_mut(idx) { |
| dev.da_state = DaState::Unknown; |
| } |
| // Verify the winner at its expected address. |
| let bcr = ccc::ccc_getbcr(hw, config, expected)?; |
| let dcr = match ccc::ccc_getdcr(hw, config, expected) { |
| Ok(dcr) => dcr, |
| Err(I3cError::AddressNack) => 0, |
| Err(e) => return Err(e), |
| }; |
| if move_ambiguous |
| && let Some(dev) = |
| provisional_idx.and_then(|i| config.attached.devices.get_mut(i)) |
| { |
| // The probe confirmed the move. |
| dev.da_state = DaState::Unassigned; |
| } |
| if let Some(dev) = config.attached.devices.get_mut(idx) { |
| dev.bcr = bcr; |
| dev.dcr = dcr; |
| dev.da_state = DaState::Verified; |
| } |
| if let Some(own_pos) = config |
| .attached |
| .pos_of(idx) |
| .or_else(|| config.attached.devices.get(idx).and_then(|d| d.pos)) |
| { |
| need &= !(1u32 << u32::from(own_pos)); |
| } |
| verified += 1; |
| // Retry this slot for its intended owner. |
| } else { |
| // A definite move failure leaves the slot occupied. |
| return Err(setnewda_result.err().unwrap_or(I3cError::Other)); |
| } |
| } |
| } |
| None => { |
| // Unknown PID: park it on a fresh address so it stops |
| // answering ENTDAA for slots it does not own. |
| let Some(park) = config.addrbook.alloc_from(8) else { |
| return Err(I3cError::AddrExhausted); |
| }; |
| config.addrbook.mark_use(park, true); |
| match ccc::ccc_setnewda_bus_only(hw, config, addr, park) { |
| Ok(()) => { |
| if let Some(dev) = |
| provisional_idx.and_then(|i| config.attached.devices.get_mut(i)) |
| { |
| dev.da_state = DaState::Unassigned; |
| } |
| } |
| Err(e @ (I3cError::Timeout | I3cError::RespError)) => { |
| // Keep an ambiguously adopted parking address reserved. |
| if let Some(idx) = provisional_idx { |
| config.mark_da_unknown(idx, Some(park)); |
| } |
| return Err(e); |
| } |
| Err(e) => { |
| // Release a definitely unused parking address. |
| config.addrbook.mark_use(park, false); |
| return Err(e); |
| } |
| } |
| // Retry this slot without advancing. |
| continue; |
| } |
| } |
| |
| pos = (pos + 1) % ndevs; |
| } |
| |
| if need == 0 { |
| Ok(verified) |
| } else if normal_nack { |
| // A normal ENTDAA NACK ends discovery, but attached devices are |
| // still pending. Do not report a partial assignment as success. |
| Err(I3cError::DaaNack) |
| } else { |
| // The retry budget expired with assignments pending. |
| Err(I3cError::AddrExhausted) |
| } |
| } |
| |
| /// Acknowledge an IBI from `address` (validates the device is known). |
| pub fn acknowledge_ibi(&mut self, address: SevenBitAddress) -> Result<(), I3cError> { |
| self.ensure_primary()?; |
| let (_, config) = self.parts(); |
| let dev_idx = config |
| .attached |
| .find_dev_idx_by_addr(address) |
| .ok_or(I3cError::Other)?; |
| |
| // `get` (not `[dev_idx]`) keeps this panic-free for the `no_panics` |
| // analysis; `find_dev_idx_by_addr` already returns a valid index. |
| let dev = config |
| .attached |
| .devices |
| .get(dev_idx) |
| .ok_or(I3cError::Other)?; |
| if dev.pid.is_none() || dev.da_state != DaState::Verified { |
| return Err(I3cError::Other); |
| } |
| |
| Ok(()) |
| } |
| |
| /// Hot-join handler hook. Call [`assign_dynamic_address`](Self::assign_dynamic_address) |
| /// after receiving a hot-join IBI; nothing else is required here. |
| #[allow(clippy::unused_self)] |
| pub fn handle_hot_join(&mut self) -> Result<(), I3cError> { |
| self.ensure_primary()?; |
| Ok(()) |
| } |
| |
| /// Bus speed is fixed on the AST1060 controller; this is a no-op. |
| #[allow(clippy::unused_self)] |
| pub fn set_bus_speed(&mut self) -> Result<(), I3cError> { |
| self.ensure_primary()?; |
| Ok(()) |
| } |
| |
| /// Multi-master mode is unsupported. |
| #[allow(clippy::unused_self)] |
| pub fn request_mastership(&mut self) -> Result<(), I3cError> { |
| Err(I3cError::Access) |
| } |
| |
| // --- Target (secondary) mode callbacks --- |
| |
| /// Initialize target-mode software state with `own_addr`. |
| pub fn target_init(&mut self, own_addr: u8) { |
| let (_, config) = self.parts(); |
| if let Some(target) = config.target_config.as_mut() { |
| if target.addr.is_none() { |
| target.addr = Some(own_addr); |
| } |
| } else { |
| let mut target = I3cTargetConfig::new(0, Some(own_addr), /* mdb */ 0xae); |
| target.addr = Some(own_addr); |
| config.target_config = Some(target); |
| } |
| } |
| |
| /// Returns `true` if `addr` matches this target's assigned address. |
| #[must_use] |
| pub fn target_on_address_match(&self, addr: u8) -> bool { |
| self.target_dynamic_address() == Some(addr) |
| } |
| |
| /// Record that the controller assigned this target a dynamic address; SIRs |
| /// are then permitted by software. Also syncs the ISR-latched address into |
| /// the thread-owned target config. |
| /// |
| /// **Timing caveat (vendor C driver parity):** the C driver delays this |
| /// permission by one second after the DA assignment (`target_worker`), |
| /// because a controller that has not yet finished ENTDAA/DISEC sequencing |
| /// can be confused by an immediate SIR. This port has no timer, so the |
| /// caller owns that delay — wait ~1 s after the `TargetDaAssignment` work |
| /// item before calling this if the bus master is slow to settle. |
| pub fn target_on_dynamic_address_assigned(&mut self) { |
| // Require an ISR-confirmed dynamic address. |
| let Some(da) = super::hardware::isr_events(self.hw.bus_num() as usize).dyn_addr() else { |
| return; |
| }; |
| if let Some(tc) = self.config.target_config.as_mut() { |
| tc.addr = Some(da); |
| } |
| self.config.sir_allowed_by_sw = true; |
| } |
| |
| /// This target always wants to raise IBIs when it has data. |
| #[must_use] |
| #[allow(clippy::unused_self)] |
| pub fn target_wants_ibi(&self) -> bool { |
| true |
| } |
| |
| /// Build and submit the IBI payload `[mdb, crc8_ccitt(addr_rnw, mdb)]` for a |
| /// pending target read, returning the number of bytes made available. |
| pub fn target_get_ibi_payload(&mut self, buffer: &mut [u8]) -> Result<usize, I3cError> { |
| let (hw, config) = self.parts(); |
| let (da, mdb) = match config.target_config.as_ref() { |
| Some(t) => ( |
| match t.addr { |
| Some(da) => da, |
| None => return Ok(0), |
| }, |
| t.mdb, |
| ), |
| None => return Ok(0), |
| }; |
| |
| let addr_rnw = (da << 1) | 0x1; |
| let mut crc = crc8_ccitt(0, &[addr_rnw]); |
| crc = crc8_ccitt(crc, &[mdb]); |
| |
| let payload = [mdb, crc]; |
| let mut ibi = I3cIbi { |
| ibi_type: I3cIbiType::TargetIntr, |
| payload: Some(&payload), |
| }; |
| let rc = hw.target_pending_read_notify(config, buffer, &mut ibi); |
| |
| match rc { |
| Ok(()) => Ok(buffer.len() + payload.len()), |
| Err(e) => Err(e), |
| } |
| } |
| } |
| |
| // ============================================================================= |
| // embedded-hal I2C bus implementation (legacy I2C devices on the I3C bus) |
| // ============================================================================= |
| |
| impl<'c, H: HardwareInterface> embedded_hal::i2c::ErrorType for I3cController<'c, H, Ready> { |
| type Error = I3cError; |
| } |
| |
| impl<'c, H: HardwareInterface> embedded_hal::i2c::I2c for I3cController<'c, H, Ready> { |
| /// Execute an I2C transaction against an attached legacy I2C device. |
| /// |
| /// The device must have been attached with |
| /// [`attach_i2c_dev`](Self::attach_i2c_dev) first (the controller |
| /// addresses devices through DAT slots, not free-form). Consecutive |
| /// operations are joined by repeated START; the last one ends with STOP. |
| fn transaction( |
| &mut self, |
| address: SevenBitAddress, |
| operations: &mut [embedded_hal::i2c::Operation<'_>], |
| ) -> Result<(), I3cError> { |
| self.ensure_primary()?; |
| let (hw, config) = self.parts(); |
| let pos = config |
| .attached |
| .pos_of_static_addr(address) |
| .ok_or(I3cError::NoSuchDev)?; |
| |
| if operations.is_empty() { |
| return Ok(()); |
| } |
| if operations.len() > MAX_PRIV_XFER_CMDS { |
| return Err(I3cError::TooManyMsgs); |
| } |
| // Validate before borrowing caller buffers. |
| for op in operations.iter() { |
| let len = match op { |
| embedded_hal::i2c::Operation::Write(buf) => buf.len(), |
| embedded_hal::i2c::Operation::Read(buf) => buf.len(), |
| }; |
| if len == 0 || len > MAX_XFER_DATA_LEN { |
| return Err(I3cError::InvalidArgs); |
| } |
| } |
| |
| let mut ops: heapless::Vec<I2cOp<'_>, MAX_PRIV_XFER_CMDS> = heapless::Vec::new(); |
| for op in operations.iter_mut() { |
| let mapped = match op { |
| embedded_hal::i2c::Operation::Write(buf) => I2cOp::Write(&**buf), |
| embedded_hal::i2c::Operation::Read(buf) => I2cOp::Read(&mut **buf), |
| }; |
| ops.push(mapped).map_err(|_| I3cError::TooManyMsgs)?; |
| } |
| |
| hw.i2c_priv_xfer(config, pos, ops.as_mut_slice()) |
| } |
| } |
| |
| /// CRC-8 CCITT calculation (ported from `hal_impl.rs`). |
| #[inline] |
| fn crc8_ccitt(mut crc: u8, data: &[u8]) -> u8 { |
| for &b in data { |
| let mut x = crc ^ b; |
| for _ in 0..8 { |
| x = if (x & 0x80) != 0 { |
| (x << 1) ^ 0x07 |
| } else { |
| x << 1 |
| }; |
| } |
| crc = x; |
| } |
| crc |
| } |