i3c: add legacy I2C device support and length CCCs

attach_i2c_dev + i2c_write/read/write_read and an embedded-hal I2c impl
route legacy devices through DAT slots by static address. Add
SETMWL/SETMRL/GETMWL/GETMRL/GETMXDS, and latch target-side MWL/MRL
updates from SLV_MAX_LEN.

Signed-off-by: Steven Lee <steven_lee@aspeedtech.com>
diff --git a/target/ast10x0/peripherals/i3c/ccc.rs b/target/ast10x0/peripherals/i3c/ccc.rs
index 7800dbe..bd9bfdc 100644
--- a/target/ast10x0/peripherals/i3c/ccc.rs
+++ b/target/ast10x0/peripherals/i3c/ccc.rs
@@ -7,8 +7,9 @@
 
 use super::config::I3cConfig;
 use super::constants::{
-    I3C_CCC_GETBCR, I3C_CCC_GETDCR, I3C_CCC_GETPID, I3C_CCC_GETSTATUS, I3C_CCC_RSTDAA,
-    I3C_CCC_SETNEWDA,
+    I3C_BCR_IBI_PAYLOAD_HAS_DATA_BYTE, I3C_CCC_GETBCR, I3C_CCC_GETDCR, I3C_CCC_GETMRL,
+    I3C_CCC_GETMWL, I3C_CCC_GETMXDS, I3C_CCC_GETPID, I3C_CCC_GETSTATUS, I3C_CCC_RSTDAA,
+    I3C_CCC_SETMRL, I3C_CCC_SETMRL_BC, I3C_CCC_SETMWL, I3C_CCC_SETMWL_BC, I3C_CCC_SETNEWDA,
 };
 use super::error::{CccErrorKind, I3cError};
 use super::hardware::HardwareInterface;
@@ -393,6 +394,253 @@
         .map_err(|_| I3cError::CccError(CccErrorKind::Invalid))
 }
 
+/// Send a direct write CCC with a small fixed payload.
+fn ccc_direct_write<H>(
+    hw: &mut H,
+    config: &mut I3cConfig,
+    id: u8,
+    da: u8,
+    payload: &mut [u8],
+) -> Result<(), I3cError>
+where
+    H: HardwareInterface,
+{
+    if da == 0 {
+        return Err(I3cError::CccError(CccErrorKind::InvalidParam));
+    }
+    let tgt = CccTargetPayload {
+        addr: da,
+        rnw: false,
+        data: Some(payload),
+        num_xfer: 0,
+    };
+    let mut tgts = [tgt];
+    let mut p = CccPayload {
+        ccc: Some(Ccc {
+            id,
+            data: None,
+            num_xfer: 0,
+        }),
+        targets: Some(&mut tgts[..]),
+    };
+    hw.do_ccc(config, &mut p)
+        .map_err(|_| I3cError::CccError(CccErrorKind::Invalid))
+}
+
+/// Send a direct read CCC into a small fixed buffer.
+fn ccc_direct_read<H>(
+    hw: &mut H,
+    config: &mut I3cConfig,
+    id: u8,
+    da: u8,
+    out: &mut [u8],
+) -> Result<(), I3cError>
+where
+    H: HardwareInterface,
+{
+    if da == 0 {
+        return Err(I3cError::CccError(CccErrorKind::InvalidParam));
+    }
+    let tgt = CccTargetPayload {
+        addr: da,
+        rnw: true,
+        data: Some(out),
+        num_xfer: 0,
+    };
+    let mut tgts = [tgt];
+    let mut p = CccPayload {
+        ccc: Some(Ccc {
+            id,
+            data: None,
+            num_xfer: 0,
+        }),
+        targets: Some(&mut tgts[..]),
+    };
+    hw.do_ccc(config, &mut p)
+        .map_err(|_| I3cError::CccError(CccErrorKind::Invalid))
+}
+
+/// Set Maximum Write Length for a device (direct SETMWL); mirrors the value
+/// into the attached-device entry on success.
+pub fn ccc_setmwl<H>(hw: &mut H, config: &mut I3cConfig, da: u8, mwl: u16) -> Result<(), I3cError>
+where
+    H: HardwareInterface,
+{
+    let mut payload = mwl.to_be_bytes();
+    ccc_direct_write(hw, config, I3C_CCC_SETMWL, da, &mut payload)?;
+    if let Some(idx) = config.attached.find_dev_idx_by_addr(da)
+        && let Some(dev) = config.attached.devices.get_mut(idx)
+    {
+        dev.mwl = mwl;
+    }
+    Ok(())
+}
+
+/// Set Maximum Read Length for a device (direct SETMRL). `ibi_len` adds the
+/// optional third byte (max IBI payload size) for targets whose BCR
+/// advertises an IBI payload. Mirrors the values into the attached-device
+/// entry on success.
+pub fn ccc_setmrl<H>(
+    hw: &mut H,
+    config: &mut I3cConfig,
+    da: u8,
+    mrl: u16,
+    ibi_len: Option<u8>,
+) -> Result<(), I3cError>
+where
+    H: HardwareInterface,
+{
+    let be = mrl.to_be_bytes();
+    let mut buf3 = [be[0], be[1], 0];
+    let payload: &mut [u8] = match ibi_len {
+        Some(n) => {
+            buf3[2] = n;
+            &mut buf3[..3]
+        }
+        None => &mut buf3[..2],
+    };
+    ccc_direct_write(hw, config, I3C_CCC_SETMRL, da, payload)?;
+    if let Some(idx) = config.attached.find_dev_idx_by_addr(da)
+        && let Some(dev) = config.attached.devices.get_mut(idx)
+    {
+        dev.mrl = mrl;
+        if let Some(n) = ibi_len {
+            dev.max_ibi = n;
+        }
+    }
+    Ok(())
+}
+
+/// Broadcast SETMWL to all devices.
+pub fn ccc_setmwl_all<H>(hw: &mut H, config: &mut I3cConfig, mwl: u16) -> Result<(), I3cError>
+where
+    H: HardwareInterface,
+{
+    let mut payload = mwl.to_be_bytes();
+    hw.do_ccc(
+        config,
+        &mut CccPayload {
+            ccc: Some(Ccc {
+                id: I3C_CCC_SETMWL_BC,
+                data: Some(&mut payload[..]),
+                num_xfer: 0,
+            }),
+            targets: None,
+        },
+    )
+    .map_err(|_| I3cError::CccError(CccErrorKind::Invalid))
+}
+
+/// Broadcast SETMRL to all devices.
+pub fn ccc_setmrl_all<H>(
+    hw: &mut H,
+    config: &mut I3cConfig,
+    mrl: u16,
+    ibi_len: Option<u8>,
+) -> Result<(), I3cError>
+where
+    H: HardwareInterface,
+{
+    let be = mrl.to_be_bytes();
+    let mut buf3 = [be[0], be[1], 0];
+    let payload: &mut [u8] = match ibi_len {
+        Some(n) => {
+            buf3[2] = n;
+            &mut buf3[..3]
+        }
+        None => &mut buf3[..2],
+    };
+    hw.do_ccc(
+        config,
+        &mut CccPayload {
+            ccc: Some(Ccc {
+                id: I3C_CCC_SETMRL_BC,
+                data: Some(payload),
+                num_xfer: 0,
+            }),
+            targets: None,
+        },
+    )
+    .map_err(|_| I3cError::CccError(CccErrorKind::Invalid))
+}
+
+/// Get Maximum Write Length from a device (GETMWL); mirrors the value into
+/// the attached-device entry.
+pub fn ccc_getmwl<H>(hw: &mut H, config: &mut I3cConfig, da: u8) -> Result<u16, I3cError>
+where
+    H: HardwareInterface,
+{
+    let mut buf = [0u8; 2];
+    ccc_direct_read(hw, config, I3C_CCC_GETMWL, da, &mut buf)?;
+    let mwl = u16::from_be_bytes(buf);
+    if let Some(idx) = config.attached.find_dev_idx_by_addr(da)
+        && let Some(dev) = config.attached.devices.get_mut(idx)
+    {
+        dev.mwl = mwl;
+    }
+    Ok(mwl)
+}
+
+/// Get Maximum Read Length from a device (GETMRL).
+///
+/// Returns `(mrl, max_ibi_len)`; the third response byte is present only for
+/// targets whose BCR advertises an IBI payload (the attached entry's BCR
+/// decides how many bytes are requested). Mirrors the values into the
+/// attached-device entry.
+pub fn ccc_getmrl<H>(
+    hw: &mut H,
+    config: &mut I3cConfig,
+    da: u8,
+) -> Result<(u16, Option<u8>), I3cError>
+where
+    H: HardwareInterface,
+{
+    let has_ibi_byte = config
+        .attached
+        .find_dev_idx_by_addr(da)
+        .and_then(|idx| config.attached.devices.get(idx))
+        .map(|d| u32::from(d.bcr) & I3C_BCR_IBI_PAYLOAD_HAS_DATA_BYTE != 0)
+        .unwrap_or(false);
+
+    let mut buf = [0u8; 3];
+    let want = if has_ibi_byte { 3 } else { 2 };
+    // `want` is 2 or 3, always within the buffer.
+    let out = buf.get_mut(..want).ok_or(I3cError::Invalid)?;
+    ccc_direct_read(hw, config, I3C_CCC_GETMRL, da, out)?;
+
+    let mrl = u16::from_be_bytes([buf[0], buf[1]]);
+    let ibi_len = has_ibi_byte.then_some(buf[2]);
+    if let Some(idx) = config.attached.find_dev_idx_by_addr(da)
+        && let Some(dev) = config.attached.devices.get_mut(idx)
+    {
+        dev.mrl = mrl;
+        if let Some(n) = ibi_len {
+            dev.max_ibi = n;
+        }
+    }
+    Ok((mrl, ibi_len))
+}
+
+/// Get Max Data Speed from a device (GETMXDS format 1).
+///
+/// Returns `(max_wr, max_rd)` raw speed bytes; mirrored into the
+/// attached-device entry.
+pub fn ccc_getmxds<H>(hw: &mut H, config: &mut I3cConfig, da: u8) -> Result<(u8, u8), I3cError>
+where
+    H: HardwareInterface,
+{
+    let mut buf = [0u8; 2];
+    ccc_direct_read(hw, config, I3C_CCC_GETMXDS, da, &mut buf)?;
+    let (max_wr, max_rd) = (buf[0], buf[1]);
+    if let Some(idx) = config.attached.find_dev_idx_by_addr(da)
+        && let Some(dev) = config.attached.devices.get_mut(idx)
+    {
+        dev.maxwr = max_wr;
+        dev.maxrd = max_rd;
+    }
+    Ok((max_wr, max_rd))
+}
+
 fn bytes_to_pid(bytes: &[u8]) -> u64 {
     bytes
         .iter()
diff --git a/target/ast10x0/peripherals/i3c/config.rs b/target/ast10x0/peripherals/i3c/config.rs
index ab9f02a..2665dab 100644
--- a/target/ast10x0/peripherals/i3c/config.rs
+++ b/target/ast10x0/peripherals/i3c/config.rs
@@ -321,6 +321,22 @@
         self.pos_of(dev_idx)
     }
 
+    /// Find device index by static address (legacy I2C devices)
+    #[must_use]
+    pub fn find_dev_idx_by_static_addr(&self, addr: u8) -> Option<usize> {
+        self.devices
+            .iter()
+            .position(|d| d.kind == DevKind::I2c && d.static_addr == addr)
+    }
+
+    /// Get DAT position by static address (legacy I2C devices)
+    #[must_use]
+    pub fn pos_of_static_addr(&self, addr: u8) -> Option<u8> {
+        let dev_idx = self.find_dev_idx_by_static_addr(addr)?;
+        self.pos_of(dev_idx)
+            .or_else(|| self.devices.get(dev_idx).and_then(|d| d.pos))
+    }
+
     /// Map a DAT position to a device index
     #[inline]
     pub fn map_pos(&mut self, pos: u8, idx: u8) -> bool {
diff --git a/target/ast10x0/peripherals/i3c/constants.rs b/target/ast10x0/peripherals/i3c/constants.rs
index d9739af..354aefa 100644
--- a/target/ast10x0/peripherals/i3c/constants.rs
+++ b/target/ast10x0/peripherals/i3c/constants.rs
@@ -299,14 +299,25 @@
 
 pub const I3C_CCC_RSTDAA: u8 = 0x06;
 pub const I3C_CCC_ENTDAA: u8 = 0x07;
+/// SETMWL broadcast form.
+pub const I3C_CCC_SETMWL_BC: u8 = 0x09;
+/// SETMRL broadcast form.
+pub const I3C_CCC_SETMRL_BC: u8 = 0x0A;
 pub const I3C_CCC_SETHID: u8 = 0x61;
 pub const I3C_CCC_DEVCTRL: u8 = 0x62;
 pub const I3C_CCC_SETDASA: u8 = 0x87;
 pub const I3C_CCC_SETNEWDA: u8 = 0x88;
+/// SETMWL direct form.
+pub const I3C_CCC_SETMWL: u8 = 0x89;
+/// SETMRL direct form.
+pub const I3C_CCC_SETMRL: u8 = 0x8A;
+pub const I3C_CCC_GETMWL: u8 = 0x8B;
+pub const I3C_CCC_GETMRL: u8 = 0x8C;
 pub const I3C_CCC_GETPID: u8 = 0x8D;
 pub const I3C_CCC_GETBCR: u8 = 0x8E;
 pub const I3C_CCC_GETDCR: u8 = 0x8F;
 pub const I3C_CCC_GETSTATUS: u8 = 0x90;
+pub const I3C_CCC_GETMXDS: u8 = 0x94;
 
 // CCC event bits
 pub const I3C_CCC_EVT_INTR: u8 = 1 << 0;
diff --git a/target/ast10x0/peripherals/i3c/controller.rs b/target/ast10x0/peripherals/i3c/controller.rs
index 8b086fa..4325ad2 100644
--- a/target/ast10x0/peripherals/i3c/controller.rs
+++ b/target/ast10x0/peripherals/i3c/controller.rs
@@ -54,7 +54,7 @@
 use super::constants::I3C_BROADCAST_ADDR;
 use super::error::I3cError;
 use super::hardware::HardwareInterface;
-use super::types::{DevKind, I3cIbi, I3cIbiType, I3cMsg};
+use super::types::{DevKind, I2cOp, I3cIbi, I3cIbiType, I3cMsg};
 use embedded_hal::i2c::SevenBitAddress;
 
 // =============================================================================
@@ -219,6 +219,88 @@
             .map_err(|_| I3cError::AddrInUse)
     }
 
+    /// 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> {
+        let (hw, config) = self.parts();
+        if static_addr == 0 || static_addr >= I3C_BROADCAST_ADDR {
+            return Err(I3cError::InvalidArgs);
+        }
+        if usize::from(slot) >= super::constants::MAX_DEVICES_PER_BUS {
+            return Err(I3cError::InvalidArgs);
+        }
+        if config
+            .attached
+            .by_pos
+            .get(usize::from(slot))
+            .copied()
+            .flatten()
+            .is_some()
+        {
+            return Err(I3cError::DevAlreadyAttached);
+        }
+
+        let mut dev = DeviceEntry::new_i2c(static_addr);
+        dev.pos = Some(slot);
+        let idx = config
+            .attached
+            .attach(dev)
+            .map_err(|_| I3cError::NoFreeSlot)?;
+        config
+            .attached
+            .map_pos(slot, u8::try_from(idx).map_err(|_| I3cError::InvalidArgs)?);
+        // The static address occupies the same 7-bit space as dynamic ones.
+        config.addrbook.mark_use(static_addr, true);
+
+        hw.attach_i2c_dev(slot.into(), static_addr)
+    }
+
+    /// Write to a legacy I2C device (by static address).
+    pub fn i2c_write(&mut self, static_addr: u8, data: &[u8]) -> Result<(), I3cError> {
+        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> {
+        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> {
+        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) {
         let (hw, config) = self.parts();
@@ -355,6 +437,15 @@
             .or_else(|| self.config.target_config.as_ref().and_then(|t| t.addr))
     }
 
+    /// Max read/write lengths `(mrl, mwl)` the bus master pushed to this
+    /// target via SETMRL/SETMWL, if any update was observed (latched by the
+    /// ISR from `SLV_MAX_LEN`). Target mode only.
+    #[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> {
         let (hw, config) = self.parts();
@@ -777,6 +868,50 @@
     }
 }
 
+// =============================================================================
+// 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> {
+        let (hw, config) = self.parts();
+        let pos = config
+            .attached
+            .pos_of_static_addr(address)
+            .ok_or(I3cError::NoSuchDev)?;
+
+        if operations.is_empty() {
+            return Ok(());
+        }
+
+        let mut ops: heapless::Vec<I2cOp<'_>, { super::constants::MAX_PRIV_XFER_CMDS }> =
+            heapless::Vec::new();
+        for op in operations.iter_mut() {
+            let mapped = match op {
+                embedded_hal::i2c::Operation::Write(b) => I2cOp::Write(b),
+                embedded_hal::i2c::Operation::Read(b) => I2cOp::Read(core::mem::take(b)),
+            };
+            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 {
diff --git a/target/ast10x0/peripherals/i3c/hardware.rs b/target/ast10x0/peripherals/i3c/hardware.rs
index 75344a9..2d34cd4 100644
--- a/target/ast10x0/peripherals/i3c/hardware.rs
+++ b/target/ast10x0/peripherals/i3c/hardware.rs
@@ -37,6 +37,7 @@
     COMMAND_PORT_CMD, COMMAND_PORT_CP, COMMAND_PORT_DBP, COMMAND_PORT_DEV_COUNT,
     COMMAND_PORT_DEV_INDEX, COMMAND_PORT_READ_TRANSFER, COMMAND_PORT_ROC, COMMAND_PORT_SPEED,
     COMMAND_PORT_TID, COMMAND_PORT_TOC, DEV_ADDR_TABLE_IBI_MDB, DEV_ADDR_TABLE_IBI_PEC,
+    DEV_ADDR_TABLE_LEGACY_I2C_DEV, DEV_ADDR_TABLE_MR_REJECT, DEV_ADDR_TABLE_STATIC_ADDR,
     DEV_ADDR_TABLE_SIR_REJECT, I3CG_REG1_SCL_IN_SW_MODE_EN, I3CG_REG1_SCL_IN_SW_MODE_VAL,
     I3CG_REG1_SDA_IN_SW_MODE_EN, I3CG_REG1_SDA_IN_SW_MODE_VAL, I3C_AST10X0_MIPI_MANUF_ID,
     I3C_BCR_IBI_PAYLOAD_HAS_DATA_BYTE, I3C_BUS_FREE_TIMING_RESET, I3C_BUS_I2C_FMP_TF_MAX_NS,
@@ -48,6 +49,7 @@
     I3C_DEFAULT_STATIC_ADDR, I3C_GLOBAL_RESET_DEASSERT_MASK, I3C_IBI_DATA_THRESHOLD_MAX,
     I3C_INIT_POLL_DELAY_NS, I3C_INTR_STATUS_ALL_BITS, I3C_MSG_READ, I3C_OP_TIMEOUT_US,
     I3C_POLL_MAX_ITERS, IBIQ_STATUS_IBI_DATA_LEN, IBIQ_STATUS_IBI_DATA_LEN_SHIFT,
+    SLV_EVENT_CTRL_MRL_UPD, SLV_EVENT_CTRL_MWL_UPD,
     IBIQ_STATUS_IBI_ID, IBIQ_STATUS_IBI_ID_SHIFT, INTR_CCC_UPDATED_STAT, INTR_DYN_ADDR_ASSGN_STAT,
     INTR_IBI_THLD_STAT, INTR_RESP_READY_STAT, INTR_TRANSFER_ABORT_STAT, INTR_TRANSFER_ERR_STAT,
     MAX_CMDS, MAX_PRIV_XFER_CMDS, MAX_XFER_DATA_LEN, NSEC_PER_SEC, RESET_CTRL_ALL,
@@ -59,7 +61,7 @@
 use super::error::I3cError as I3cDrvError;
 use super::error::I3cError;
 use super::ibi as ibi_workq;
-use super::types::{Completion, I3cCmd, I3cIbi, I3cMsg, I3cXfer, SpeedI3c, Tid};
+use super::types::{Completion, I2cOp, I3cCmd, I3cIbi, I3cMsg, I3cXfer, SpeedI2c, SpeedI3c, Tid};
 
 use super::registers::I3cRegisters;
 use core::sync::atomic::{AtomicBool, AtomicU32, Ordering};
@@ -125,6 +127,11 @@
     pending: AtomicU32,
     /// Dynamic address assigned by the bus master; bit 8 = valid.
     dyn_addr: AtomicU32,
+    /// Raw `SLV_MAX_LEN` (MRL in bits 31:16, MWL in bits 15:0) latched by the
+    /// ISR when the bus master updates it via SETMRL/SETMWL.
+    slv_max_len: AtomicU32,
+    /// `slv_max_len` holds a master-written value (not reset state).
+    slv_max_len_valid: AtomicBool,
     /// A deferred fault: the ISR observed a halted/errored engine and left
     /// recovery (halt/resume sequencing needs the wait policy) to the thread.
     fault: AtomicBool,
@@ -139,6 +146,8 @@
         Self {
             pending: AtomicU32::new(0),
             dyn_addr: AtomicU32::new(0),
+            slv_max_len: AtomicU32::new(0),
+            slv_max_len_valid: AtomicBool::new(false),
             fault: AtomicBool::new(false),
             target_ibi_done: Completion::new(),
             target_data_done: Completion::new(),
@@ -164,6 +173,16 @@
             None
         }
     }
+
+    /// Max read/write lengths `(mrl, mwl)` the bus master set via
+    /// SETMRL/SETMWL, if any update was observed.
+    pub(crate) fn max_len(&self) -> Option<(u16, u16)> {
+        if !self.slv_max_len_valid.load(Ordering::Acquire) {
+            return None;
+        }
+        let v = self.slv_max_len.load(Ordering::Acquire);
+        Some(((v >> 16) as u16, (v & 0xffff) as u16))
+    }
 }
 
 static ISR_EVENTS: [IsrEvents; 4] = [
@@ -293,6 +312,14 @@
             // Read-and-clear the event; if the engine halted, defer the
             // resume sequencing (it needs the wait policy) to the thread.
             let event = regs.read_slv_event_ctrl();
+            // Latch SETMRL/SETMWL updates before the write-back clears the
+            // update flags (the thread reads them via `max_len`).
+            if event & (SLV_EVENT_CTRL_MRL_UPD | SLV_EVENT_CTRL_MWL_UPD) != 0 {
+                events
+                    .slv_max_len
+                    .store(regs.read_slv_max_len(), Ordering::Release);
+                events.slv_max_len_valid.store(true, Ordering::Release);
+            }
             regs.write_slv_event_ctrl(event);
             if regs.xfer_status() == CM_TFR_STS_TARGET_HALT {
                 events.fault.store(true, Ordering::Release);
@@ -553,6 +580,23 @@
     /// Attach a device to a DAT position
     fn attach_i3c_dev(&mut self, pos: usize, addr: u8) -> Result<(), I3cError>;
 
+    /// Attach a legacy I2C device to a DAT position (static address,
+    /// `LEGACY_I2C_DEV` marked, SIR/MR rejected).
+    fn attach_i2c_dev(&mut self, pos: usize, static_addr: u8) -> Result<(), I3cError>;
+
+    /// Execute a legacy-I2C transaction against the device at DAT `pos`.
+    ///
+    /// Consecutive operations are joined by repeated START; the last ends
+    /// with STOP. **Consumes each `Read` buffer** (left empty in the slice,
+    /// same contract as [`priv_xfer`](HardwareTransfer::priv_xfer)); the data
+    /// lands in the caller-owned memory the reborrow came from.
+    fn i2c_priv_xfer<'a>(
+        &mut self,
+        config: &mut I3cConfig,
+        pos: u8,
+        ops: &mut [I2cOp<'a>],
+    ) -> Result<(), I3cError>;
+
     /// Execute a CCC
     fn do_ccc(&mut self, config: &mut I3cConfig, ccc: &mut CccPayload) -> Result<(), I3cError>;
 
@@ -1499,6 +1543,106 @@
         Ok(())
     }
 
+    fn attach_i2c_dev(&mut self, pos: usize, static_addr: u8) -> Result<(), I3cDrvError> {
+        // Legacy I2C entry: static address in the low field, the LEGACY bit
+        // routes transfers through the controller's I2C engine; SIR/MR stay
+        // rejected (an I2C device cannot raise them).
+        let raw = DEV_ADDR_TABLE_LEGACY_I2C_DEV
+            | DEV_ADDR_TABLE_SIR_REJECT
+            | DEV_ADDR_TABLE_MR_REJECT
+            | field_prep(DEV_ADDR_TABLE_STATIC_ADDR, u32::from(static_addr));
+        self.regs.dat_write_raw(pos, raw);
+        Ok(())
+    }
+
+    fn i2c_priv_xfer<'a>(
+        &mut self,
+        config: &mut I3cConfig,
+        pos: u8,
+        ops: &mut [I2cOp<'a>],
+    ) -> Result<(), I3cDrvError> {
+        if ops.is_empty() {
+            return Ok(());
+        }
+        // Same TID-width bound as private I3C transfers.
+        if ops.len() > MAX_PRIV_XFER_CMDS {
+            return Err(I3cDrvError::TooManyMsgs);
+        }
+        // Pre-validate every length before consuming any buffer
+        // (all-or-nothing, mirroring priv_xfer_build_cmds).
+        for op in ops.iter() {
+            let len = match op {
+                I2cOp::Write(b) => b.len(),
+                I2cOp::Read(b) => b.len(),
+            };
+            if len == 0 || len > MAX_XFER_DATA_LEN {
+                return Err(I3cDrvError::Invalid);
+            }
+        }
+
+        // The DAT entry marks the device as legacy I2C, so the SPEED field
+        // selects between the I2C timing sets programmed by init_clock.
+        let speed = if config.i2c_scl_hz > 400_000 {
+            SpeedI2c::Fmp
+        } else {
+            SpeedI2c::Fm
+        } as u32;
+
+        let mut cmds: heapless::Vec<I3cCmd<'a>, MAX_CMDS> = heapless::Vec::new();
+        let nops = ops.len();
+        for (i, op) in ops.iter_mut().enumerate() {
+            let mut cmd = I3cCmd::new();
+            let len = match op {
+                I2cOp::Write(b) => b.len(),
+                I2cOp::Read(b) => b.len(),
+            };
+            cmd.cmd_hi = field_prep(COMMAND_PORT_ATTR, COMMAND_ATTR_XFER_ARG)
+                | field_prep(
+                    COMMAND_PORT_ARG_DATA_LEN,
+                    u32::try_from(len).map_err(|_| I3cDrvError::Invalid)?,
+                );
+            cmd.cmd_lo = field_prep(
+                COMMAND_PORT_TID,
+                u32::try_from(i).map_err(|_| I3cDrvError::Invalid)?,
+            ) | field_prep(COMMAND_PORT_DEV_INDEX, u32::from(pos))
+                | field_prep(COMMAND_PORT_SPEED, speed)
+                | COMMAND_PORT_ROC;
+
+            match op {
+                I2cOp::Write(b) => {
+                    cmd.tx = Some(*b);
+                    cmd.tx_len = u32::try_from(len).map_err(|_| I3cDrvError::Invalid)?;
+                }
+                I2cOp::Read(b) => {
+                    // Move the caller's reborrow into the command (same
+                    // consume contract as priv_xfer); `take` leaves an empty
+                    // slice behind.
+                    let buf: &'a mut [u8] = core::mem::take(b);
+                    cmd.rx = Some(buf);
+                    cmd.rx_len = u32::try_from(len).map_err(|_| I3cDrvError::Invalid)?;
+                    cmd.cmd_lo |= COMMAND_PORT_READ_TRANSFER;
+                }
+            }
+
+            if i + 1 == nops {
+                cmd.cmd_lo |= COMMAND_PORT_TOC;
+            }
+            cmds.push(cmd).map_err(|_| I3cDrvError::TooManyMsgs)?;
+        }
+
+        let mut xfer = I3cXfer::new(cmds.as_mut_slice());
+        self.start_xfer(config, &mut xfer);
+
+        if !self.wait_xfer_complete(config, &mut xfer, I3C_OP_TIMEOUT_US) {
+            return Err(I3cDrvError::Timeout);
+        }
+
+        match xfer.ret {
+            0 => Ok(()),
+            _ => Err(I3cDrvError::IoError),
+        }
+    }
+
     #[allow(clippy::too_many_lines)]
     fn do_ccc(
         &mut self,
diff --git a/target/ast10x0/peripherals/i3c/mod.rs b/target/ast10x0/peripherals/i3c/mod.rs
index 59417a7..a915d80 100644
--- a/target/ast10x0/peripherals/i3c/mod.rs
+++ b/target/ast10x0/peripherals/i3c/mod.rs
@@ -62,8 +62,8 @@
 
 // Core types
 pub use types::{
-    Completion, DevKind, I3cCmd, I3cDeviceId, I3cIbi, I3cIbiType, I3cMsg, I3cPid, I3cStatus,
-    I3cXfer, SpeedI2c, SpeedI3c, Tid,
+    Completion, DevKind, I2cOp, I3cCmd, I3cDeviceId, I3cIbi, I3cIbiType, I3cMsg, I3cPid,
+    I3cStatus, I3cXfer, SpeedI2c, SpeedI3c, Tid,
 };
 
 // Hardware interface
@@ -77,8 +77,9 @@
 
 // CCC operations
 pub use ccc::{
-    ccc_events_all_set, ccc_events_set, ccc_getbcr, ccc_getdcr, ccc_getpid, ccc_getstatus,
-    ccc_getstatus_fmt1, ccc_rstact_all, ccc_rstdaa_all, ccc_setnewda, Ccc, CccPayload,
+    ccc_events_all_set, ccc_events_set, ccc_getbcr, ccc_getdcr, ccc_getmrl, ccc_getmwl,
+    ccc_getmxds, ccc_getpid, ccc_getstatus, ccc_getstatus_fmt1, ccc_rstact_all, ccc_rstdaa_all,
+    ccc_setmrl, ccc_setmrl_all, ccc_setmwl, ccc_setmwl_all, ccc_setnewda, Ccc, CccPayload,
     CccRstActDefByte, CccTargetPayload, GetStatusDefByte, GetStatusFormat, GetStatusResp,
 };
 
diff --git a/target/ast10x0/peripherals/i3c/registers.rs b/target/ast10x0/peripherals/i3c/registers.rs
index fdb61db..3379977 100644
--- a/target/ast10x0/peripherals/i3c/registers.rs
+++ b/target/ast10x0/peripherals/i3c/registers.rs
@@ -764,6 +764,12 @@
         self.i3c().i3cd078().read().bits()
     }
 
+    /// I3CD07C: read the max write/read length register (MRL in bits 31:16,
+    /// MWL in bits 15:0; updated by the bus master via SETMRL/SETMWL).
+    pub(crate) fn read_slv_max_len(&self) -> u32 {
+        self.i3c().i3cd07c().read().bits()
+    }
+
     /// I3CD078: write the slave characteristics register.
     pub(crate) fn write_slv_char_ctrl(&self, val: u32) {
         self.i3c().i3cd078().write(|w| unsafe { w.bits(val) });
diff --git a/target/ast10x0/peripherals/i3c/types.rs b/target/ast10x0/peripherals/i3c/types.rs
index d208dc0..201fa97 100644
--- a/target/ast10x0/peripherals/i3c/types.rs
+++ b/target/ast10x0/peripherals/i3c/types.rs
@@ -205,6 +205,22 @@
 }
 
 // =============================================================================
+// Legacy I2C Operations
+// =============================================================================
+
+/// One leg of a legacy-I2C transaction on the I3C bus.
+///
+/// Mirrors `embedded_hal::i2c::Operation` without pulling that type into the
+/// hardware trait. Consecutive operations are joined by repeated START; the
+/// last one ends with STOP.
+pub enum I2cOp<'a> {
+    /// Write the bytes to the device.
+    Write(&'a [u8]),
+    /// Read into the buffer (filled completely on success).
+    Read(&'a mut [u8]),
+}
+
+// =============================================================================
 // Device Identification
 // =============================================================================