blob: c5489affef327685e40e42899de95f6a3136a391 [file]
// 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::{DeviceEntry, I3cConfig, I3cTargetConfig};
use super::constants::I3C_BROADCAST_ADDR;
use super::error::I3cError;
use super::hardware::HardwareInterface;
use super::types::{DevKind, 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.
pub fn start(mut self) -> Result<I3cController<'c, H, Ready>, I3cError> {
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);
}
self.hw.init(self.config);
// 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> {
// =========================================================================
// 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> {
let (hw, config) = self.parts();
if desired_da == 0 || desired_da >= I3C_BROADCAST_ADDR {
return Err(I3cError::InvalidArgs);
}
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_en: false,
pos: Some(slot),
};
let idx = config
.attached
.attach(dev)
.map_err(|_| I3cError::AddrInUse)?;
config
.attached
.map_pos(slot, u8::try_from(idx).map_err(|_| I3cError::InvalidArgs)?);
config.addrbook.mark_use(desired_da, true);
hw.attach_i3c_dev(slot.into(), desired_da)
.map_err(|_| I3cError::AddrInUse)
}
/// Detach an I3C device by DAT position
pub fn detach_i3c_dev(&mut self, pos: usize) {
let (hw, config) = self.parts();
config.attached.detach_by_pos(pos);
hw.detach_i3c_dev(pos);
}
/// Detach an I3C device by device index
pub fn detach_i3c_dev_by_idx(&mut self, dev_idx: usize) {
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;
};
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);
}
// =========================================================================
// 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) {
let (hw, _) = self.parts();
hw.enter_sw_mode();
hw.i3c_toggle_scl_in(scl_toggles);
hw.gen_internal_stop();
hw.exit_sw_mode();
}
/// 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);
/// ```
pub fn recover_bus_full(&mut self, reset_mask: u32) {
self.recover_bus(8);
let (hw, _) = self.parts();
hw.reset_ctrl(reset_mask);
}
// =========================================================================
// Accessors
// =========================================================================
/// Allocate a dynamic address from `start_addr`.
#[inline]
pub fn alloc_dynamic_address_from(&mut self, start_addr: u8) -> Option<u8> {
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> {
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))
}
/// 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> {
let (hw, config) = self.parts();
hw.set_ibi_mdb(mdb);
hw.ibi_enable(config, addr)
}
/// 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> {
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> {
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> {
let (hw, config) = self.parts();
let slot = config
.attached
.pos_of_addr(static_address)
.ok_or(I3cError::AddrInUse)?;
hw.do_entdaa(config, slot.into())
.map_err(|_| I3cError::AddrInUse)?;
let pid = ccc::ccc_getpid(hw, config, static_address).map_err(|_| I3cError::Invalid)?;
let dev_idx = config
.attached
.find_dev_idx_by_addr(static_address)
.ok_or(I3cError::Other)?;
let old_pid = config
.attached
.devices
.get(dev_idx)
.ok_or(I3cError::Other)?
.pid;
if let Some(op) = old_pid
&& pid != op
{
return Err(I3cError::Other);
}
let bcr = ccc::ccc_getbcr(hw, config, static_address).map_err(|_| I3cError::Invalid)?;
{
let dev = config
.attached
.devices
.get_mut(dev_idx)
.ok_or(I3cError::Other)?;
dev.pid = Some(pid);
dev.bcr = bcr;
}
let dyn_addr: SevenBitAddress = config
.attached
.devices
.get(dev_idx)
.ok_or(I3cError::Other)?
.dyn_addr;
hw.ibi_enable(config, dyn_addr)
.map_err(|_| I3cError::Other)?;
Ok(dyn_addr)
}
/// Acknowledge an IBI from `address` (validates the device is known).
pub fn acknowledge_ibi(&mut self, address: SevenBitAddress) -> Result<(), I3cError> {
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() {
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> {
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> {
Ok(())
}
/// The AST1060 controller does not support multi-master; this is a no-op.
#[allow(clippy::unused_self)]
pub fn request_mastership(&mut self) -> Result<(), I3cError> {
Ok(())
}
// --- Target (secondary) mode callbacks ---
/// Initialize target mode with `own_addr` (sets the static/target address).
pub fn target_init(&mut self, own_addr: u8) {
let (_, config) = self.parts();
if let Some(t) = config.target_config.as_mut() {
if t.addr.is_none() {
t.addr = Some(own_addr);
}
} else {
config.target_config =
Some(I3cTargetConfig::new(0, Some(own_addr), /* mdb */ 0xae));
}
}
/// 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.
pub fn target_on_dynamic_address_assigned(&mut self) {
let da = super::hardware::isr_events(self.hw.bus_num() as usize).dyn_addr();
if let (Some(da), Some(tc)) = (da, 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()),
_ => Ok(0),
}
}
}
/// 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
}