Add i2c peripheral to the ast10x0 peripheral crate
diff --git a/target/ast10x0/peripherals/BUILD.bazel b/target/ast10x0/peripherals/BUILD.bazel
index 254e9d4..ee911c5 100644
--- a/target/ast10x0/peripherals/BUILD.bazel
+++ b/target/ast10x0/peripherals/BUILD.bazel
@@ -10,6 +10,19 @@
     name = "peripherals",
     srcs = [
         "lib.rs",
+        "i2c/constants.rs",
+        "i2c/controller.rs",
+        "i2c/error.rs",
+        "i2c/global.rs",
+        "i2c/hal_impl.rs",
+        "i2c/master.rs",
+        "i2c/mod.rs",
+        "i2c/recovery.rs",
+        "i2c/slave.rs",
+        "i2c/target_adapter.rs",
+        "i2c/timing.rs",
+        "i2c/transfer.rs",
+        "i2c/types.rs",
         "scu/mod.rs",
         "scu/registers.rs",
         "scu/types.rs",
@@ -26,6 +39,7 @@
     deps = [
         "@ast1060_pac",
         "@rust_crates//:bitflags",
+        "@rust_crates//:embedded-hal",
         "@rust_crates//:embedded-hal-nb",
         "@rust_crates//:embedded-io",
         "@rust_crates//:nb",
diff --git a/target/ast10x0/peripherals/i2c/constants.rs b/target/ast10x0/peripherals/i2c/constants.rs
new file mode 100644
index 0000000..67dd863
--- /dev/null
+++ b/target/ast10x0/peripherals/i2c/constants.rs
@@ -0,0 +1,178 @@
+// Licensed under the Apache-2.0 license
+
+//! Hardware constants for AST1060 I2C controller
+//!
+//! # Reference Implementation
+//!
+//! Constants derived from the original working code:
+//! - **aspeed-rust/src/i2c/ast1060_i2c.rs** lines 55-100
+//!
+//! # Register Map (New Register Definition Mode)
+//!
+//! | Offset | Register | Description |
+//! |--------|----------|-------------|
+//! | 0x00   | I2CC00   | Function Control Register |
+//! | 0x04   | I2CC04   | AC Timing Register |
+//! | 0x08   | I2CC08   | Byte Buffer / Line Status Register |
+//! | 0x0C   | I2CC0C   | Pool Buffer Control Register |
+//! | 0x10   | I2CM10   | Master Interrupt Control Register |
+//! | 0x14   | I2CM14   | Master Interrupt Status Register (write-to-clear) |
+//! | 0x18   | I2CM18   | Master Command Register |
+//! | 0x1C   | I2CM1C   | Master DMA Length Register |
+//!
+//! # Critical Register Notes
+//!
+//! - **i2cm18**: Command register - write all command bits here
+//! - **i2cm14**: Status register - read status, write to clear interrupts
+//! - **i2cc08**: Byte data register for byte mode (`tx_byte_buffer`, `rx_byte_buffer`)
+//! - **i2cc0c**: Buffer size register for buffer mode (`tx_data_byte_count`, `rx_pool_buffer_size`)
+
+/// HPLL frequency (1 `GHz`)
+pub const HPLL_FREQ: u32 = 1_000_000_000;
+
+/// ASPEED I2C bus clock (100 `MHz` typical)
+pub const ASPEED_I2C_BUS_CLK_HZ: u32 = 100_000_000;
+
+/// Standard mode speed (100 kHz)
+pub const I2C_STANDARD_MODE_HZ: u32 = 100_000;
+
+/// Fast mode speed (400 kHz)
+pub const I2C_FAST_MODE_HZ: u32 = 400_000;
+
+/// Fast-plus mode speed (1 `MHz`)
+pub const I2C_FAST_PLUS_MODE_HZ: u32 = 1_000_000;
+
+/// Buffer mode maximum size (32 bytes)
+pub const BUFFER_MODE_SIZE: usize = 32;
+
+/// DMA mode maximum transfer size (4096 bytes, hardware limit)
+pub const DMA_MODE_MAX_SIZE: usize = 4096;
+
+/// I2C buffer size register value
+/// Reference: `ast1060_i2c.rs:97`
+pub const I2C_BUF_SIZE: u8 = 0x20;
+
+/// Default timeout in microseconds
+pub const DEFAULT_TIMEOUT_US: u32 = 1_000_000;
+
+/// Maximum retry attempts for operations
+pub const MAX_RETRY_ATTEMPTS: u32 = 3;
+
+// ============================================================================
+// Register bit definitions
+// Reference: aspeed-rust/src/i2c/ast1060_i2c.rs lines 55-100
+// ============================================================================
+
+// Function Control Register (I2CC00)
+/// Enable slave function
+pub const AST_I2CC_SLAVE_EN: u32 = 1 << 1;
+/// Enable master function
+pub const AST_I2CC_MASTER_EN: u32 = 1 << 0;
+/// Disable multi-master capability
+pub const AST_I2CC_MULTI_MASTER_DIS: u32 = 1 << 15;
+/// Enable save address byte into buffer for Slave Packet mode receive command (I2CC00 bit 20)
+/// Per AST1060 datasheet section 13.3.1, when this bit is set, the slave address byte
+/// is saved into the receive buffer in slave packet mode.
+pub const AST_I2CC_SLAVE_PKT_SAVE_ADDR: u32 = 1 << 20;
+
+// Master Command Register (I2CM18) bit definitions
+// Reference: ast1060_i2c.rs:58-69
+/// Enable packet mode
+pub const AST_I2CM_PKT_EN: u32 = 1 << 16;
+/// Enable RX DMA mode
+pub const AST_I2CM_RX_DMA_EN: u32 = 1 << 9;
+/// Enable TX DMA mode
+pub const AST_I2CM_TX_DMA_EN: u32 = 1 << 8;
+/// Enable RX buffer mode
+pub const AST_I2CM_RX_BUFF_EN: u32 = 1 << 7;
+/// Enable TX buffer mode
+pub const AST_I2CM_TX_BUFF_EN: u32 = 1 << 6;
+/// Send STOP condition
+pub const AST_I2CM_STOP_CMD: u32 = 1 << 5;
+/// RX command with NACK (last byte)
+pub const AST_I2CM_RX_CMD_LAST: u32 = 1 << 4;
+/// RX command with ACK
+pub const AST_I2CM_RX_CMD: u32 = 1 << 3;
+/// TX command
+pub const AST_I2CM_TX_CMD: u32 = 1 << 1;
+/// Send START condition
+pub const AST_I2CM_START_CMD: u32 = 1 << 0;
+
+// Master Interrupt Status Register (I2CM14) bit definitions
+// Reference: ast1060_i2c.rs:71-77
+// NOTE: These bits overlap with command bits but have different meanings when read from i2cm14
+/// SCL low timeout
+pub const AST_I2CM_SCL_LOW_TO: u32 = 1 << 6;
+/// Abnormal STOP condition
+pub const AST_I2CM_ABNORMAL: u32 = 1 << 5;
+/// Normal STOP condition
+pub const AST_I2CM_NORMAL_STOP: u32 = 1 << 4;
+/// Arbitration loss
+pub const AST_I2CM_ARBIT_LOSS: u32 = 1 << 3;
+/// RX transfer done
+pub const AST_I2CM_RX_DONE: u32 = 1 << 2;
+/// TX received NACK
+pub const AST_I2CM_TX_NAK: u32 = 1 << 1;
+/// TX received ACK
+pub const AST_I2CM_TX_ACK: u32 = 1 << 0;
+
+// Packet mode status (I2CM14 bits 12-17)
+// Reference: ast1060_i2c.rs:86-92
+/// Packet mode error
+pub const AST_I2CM_PKT_ERROR: u32 = 1 << 17;
+/// Packet mode done
+pub const AST_I2CM_PKT_DONE: u32 = 1 << 16;
+/// Bus recovery failed
+pub const AST_I2CM_BUS_RECOVER_FAIL: u32 = 1 << 15;
+/// SDA data line timeout
+pub const AST_I2CM_SDA_DL_TO: u32 = 1 << 14;
+/// Bus recovery done
+pub const AST_I2CM_BUS_RECOVER: u32 = 1 << 13;
+/// `SMBus` alert
+pub const AST_I2CM_SMBUS_ALT: u32 = 1 << 12;
+
+// Slave mode constants
+// Reference: ast1060_i2c.rs:83-84 and 99-125
+/// Enable slave packet mode
+pub const AST_I2CS_PKT_MODE_EN: u32 = 1 << 16;
+/// Active on all addresses
+pub const AST_I2CS_ACTIVE_ALL: u32 = 0x3 << 17;
+/// Enable slave RX DMA
+pub const AST_I2CS_RX_DMA_EN: u32 = 1 << 9;
+/// Enable slave TX DMA
+pub const AST_I2CS_TX_DMA_EN: u32 = 1 << 8;
+/// Enable slave RX buffer
+pub const AST_I2CS_RX_BUFF_EN: u32 = 1 << 7;
+/// Enable slave TX buffer
+pub const AST_I2CS_TX_BUFF_EN: u32 = 1 << 6;
+/// Slave TX command
+pub const AST_I2CS_TX_CMD: u32 = 1 << 2;
+/// Slave address matched
+pub const AST_I2CS_SLAVE_MATCH: u32 = 1 << 7;
+/// STOP condition received
+pub const AST_I2CS_STOP: u32 = 1 << 4;
+/// Slave RX done NACK
+pub const AST_I2CS_RX_DONE_NAK: u32 = 1 << 3;
+/// Slave RX done
+pub const AST_I2CS_RX_DONE: u32 = 1 << 2;
+/// Slave TX got NACK
+pub const AST_I2CS_TX_NAK: u32 = 1 << 1;
+/// Slave TX got ACK
+pub const AST_I2CS_TX_ACK: u32 = 1 << 0;
+/// Slave inactive timeout
+pub const AST_I2CS_INACTIVE_TO: u32 = 1 << 15;
+/// Slave packet mode done
+pub const AST_I2CS_PKT_DONE: u32 = 1 << 16;
+/// Slave packet mode error
+pub const AST_I2CS_PKT_ERROR: u32 = 1 << 17;
+/// Waiting for TX DMA
+pub const AST_I2CS_WAIT_TX_DMA: u32 = 1 << 25;
+/// Waiting for RX DMA
+pub const AST_I2CS_WAIT_RX_DMA: u32 = 1 << 24;
+
+/// Helper to build packet mode address field
+#[inline]
+#[must_use]
+pub fn ast_i2cm_pkt_addr(addr: u8) -> u32 {
+    u32::from(addr & 0x7F) << 24
+}
diff --git a/target/ast10x0/peripherals/i2c/controller.rs b/target/ast10x0/peripherals/i2c/controller.rs
new file mode 100644
index 0000000..4a6d48f
--- /dev/null
+++ b/target/ast10x0/peripherals/i2c/controller.rs
@@ -0,0 +1,288 @@
+// Licensed under the Apache-2.0 license
+
+//! AST1060 I2C controller implementation
+
+use core::cell::UnsafeCell;
+use core::marker::PhantomData;
+
+use super::timing::configure_timing;
+use super::types::{I2cConfig, I2cXferMode};
+use super::{constants, error::I2cError};
+use ast1060_pac::{i2c::RegisterBlock, i2cbuff::RegisterBlock as BuffRegisterBlock};
+
+/// Main I2C hardware abstraction.
+///
+/// Wraps a raw `*const RegisterBlock` and `*const BuffRegisterBlock` pair
+/// for one AST1060 I2C controller. Mirrors the [`Usart`](super::super::uart::Usart)
+/// pattern: the constructor is `unsafe`; ownership/coordination is the
+/// caller's responsibility.
+///
+/// `Y` is the caller-supplied yield closure. [`wait_completion`] calls
+/// it as `(yield_ns)(100_000)` between status polls so the runtime can
+/// hand the CPU back to a scheduler instead of busy-looping. For a
+/// pure busy-wait, pass `|_| core::hint::spin_loop()`.
+#[allow(clippy::struct_excessive_bools)]
+pub struct Ast1060I2c<'a, Y: FnMut(u32)> {
+    regs: *const RegisterBlock,
+    buff_regs: *const BuffRegisterBlock,
+
+    /// Transfer mode (visible to other modules for optimization decisions)
+    pub(crate) xfer_mode: I2cXferMode,
+    multi_master: bool,
+    smbus_alert: bool,
+    #[allow(dead_code)]
+    bus_recover: bool,
+
+    // Transfer state (visible to transfer/master modules)
+    /// Current device address being communicated with
+    pub(crate) current_addr: u8,
+    /// Current transfer length
+    pub(crate) current_len: u32,
+    /// Bytes transferred so far
+    pub(crate) current_xfer_cnt: u32,
+    /// Completion flag for synchronous operations
+    pub(crate) completion: bool,
+    /// DMA buffer for DMA mode (non-cached SRAM, caller-owned)
+    pub(crate) dma_buf: Option<&'a mut [u8]>,
+    /// Cooperative yield invoked between status polls in
+    /// [`wait_completion`]. Argument is the suggested wait window in
+    /// nanoseconds.
+    pub(crate) yield_ns: Y,
+
+    /// Makes `Ast1060I2c` `!Sync` so the raw register pointers can't be
+    /// shared across threads without explicit synchronization.
+    _not_sync: PhantomData<UnsafeCell<()>>,
+}
+
+impl<'a, Y: FnMut(u32)> Ast1060I2c<'a, Y> {
+    /// Create a new I2C instance and initialize hardware.
+    ///
+    /// Performs full hardware init (controller reset, multi-master,
+    /// timing, interrupts). Use [`from_initialized`] when the bus has
+    /// already been brought up by application code or a previous
+    /// `new()` call.
+    ///
+    /// # Safety
+    ///
+    /// - `regs` and `buff_regs` must be valid, non-null pointers to the
+    ///   AST1060 I2C register block and its companion buffer block for
+    ///   the **same** controller instance.
+    /// - The pointed register blocks must remain valid for the lifetime
+    ///   of this `Ast1060I2c`.
+    /// - Caller must enforce global ownership/coordination so concurrent
+    ///   mutable access does not occur through other code paths.
+    pub unsafe fn new(
+        regs: *const RegisterBlock,
+        buff_regs: *const BuffRegisterBlock,
+        config: &I2cConfig,
+        yield_ns: Y,
+    ) -> Result<Self, I2cError> {
+        let mut i2c = unsafe { Self::from_initialized(regs, buff_regs, config, yield_ns) };
+        i2c.init_hardware(&config)?;
+        Ok(i2c)
+    }
+
+    /// Create I2C instance from pre-initialized hardware (NO hardware init).
+    ///
+    /// Lightweight constructor that wraps register pointers without
+    /// writing to hardware. Use when:
+    /// - Hardware was already initialized by app `main.rs` before kernel start
+    /// - Hardware was initialized by a previous `new()` call
+    /// - You want to avoid redundant re-initialization overhead
+    ///
+    /// # Safety
+    ///
+    /// Same contract as [`new`]. Additionally, the caller must ensure
+    /// hardware is already configured: I2C global registers (I2CG0C,
+    /// I2CG10) are set (call [`super::global::init_i2c_global`] ONCE in
+    /// the app before use); controller is enabled (I2CC00); timing is
+    /// configured; pin mux is configured.
+    #[must_use]
+    pub unsafe fn from_initialized(
+        regs: *const RegisterBlock,
+        buff_regs: *const BuffRegisterBlock,
+        config: &I2cConfig,
+        yield_ns: Y,
+    ) -> Self {
+        Self {
+            regs,
+            buff_regs,
+            xfer_mode: config.xfer_mode,
+            multi_master: config.multi_master,
+            smbus_alert: config.smbus_alert,
+            bus_recover: true,
+            current_addr: 0,
+            current_len: 0,
+            current_xfer_cnt: 0,
+            completion: false,
+            dma_buf: None,
+            yield_ns,
+            _not_sync: PhantomData,
+        }
+    }
+
+    /// Create I2C instance with DMA mode support.
+    ///
+    /// Like [`new`] but also attaches a DMA buffer for use when
+    /// `xfer_mode == I2cXferMode::DmaMode`. The buffer must reside in
+    /// non-cached SRAM (e.g. `#[link_section = ".ram_nc"]`) so that the
+    /// DMA engine and CPU see the same data without cache maintenance.
+    ///
+    /// # Safety
+    ///
+    /// Same contract as [`new`]. Additionally `dma_buf` must remain valid
+    /// for the lifetime of this `Ast1060I2c` and must be in a memory
+    /// region the DMA engine can address coherently with the CPU.
+    pub unsafe fn new_with_dma(
+        regs: *const RegisterBlock,
+        buff_regs: *const BuffRegisterBlock,
+        config: &I2cConfig,
+        dma_buf: &'a mut [u8],
+        yield_ns: Y,
+    ) -> Result<Self, I2cError> {
+        let mut i2c = unsafe { Self::from_initialized(regs, buff_regs, config, yield_ns) };
+        i2c.dma_buf = Some(dma_buf);
+        i2c.init_hardware(&config)?;
+        Ok(i2c)
+    }
+
+    /// Create I2C instance from pre-initialized hardware with DMA buffer
+    /// (NO hardware init).
+    ///
+    /// Like [`from_initialized`] but attaches a DMA buffer for use when
+    /// `xfer_mode == I2cXferMode::DmaMode`. Use this per-operation after
+    /// the bus has already been initialized via [`new_with_dma`], to
+    /// avoid the overhead of re-running hardware initialization.
+    ///
+    /// The buffer must reside in non-cached SRAM (e.g. `#[link_section = ".ram_nc"]`).
+    ///
+    /// # Safety
+    ///
+    /// Same contract as [`from_initialized`] and [`new_with_dma`].
+    #[must_use]
+    pub unsafe fn from_initialized_with_dma(
+        regs: *const RegisterBlock,
+        buff_regs: *const BuffRegisterBlock,
+        config: &I2cConfig,
+        dma_buf: &'a mut [u8],
+        yield_ns: Y,
+    ) -> Self {
+        let mut i2c = unsafe { Self::from_initialized(regs, buff_regs, config, yield_ns) };
+        i2c.dma_buf = Some(dma_buf);
+        i2c
+    }
+
+    /// Get access to registers (visible to other modules)
+    #[inline]
+    pub(crate) fn regs(&self) -> &RegisterBlock {
+        // SAFETY: `Ast1060I2c` construction is `unsafe`, so the caller
+        // upholds pointer validity, non-nullness, and ownership.
+        unsafe { &*self.regs }
+    }
+
+    /// Get access to buffer registers (visible to other modules)
+    #[inline]
+    pub(crate) fn buff_regs(&self) -> &BuffRegisterBlock {
+        // SAFETY: see `regs`.
+        unsafe { &*self.buff_regs }
+    }
+
+    /// Initialize hardware
+    #[inline(never)]
+    pub fn init_hardware(&mut self, config: &I2cConfig) -> Result<(), I2cError> {
+        // PRECONDITION: I2C global registers must be initialized by app before use.
+        // See: super::global::init_i2c_global().
+
+        // Reset I2C controller
+        unsafe {
+            self.regs().i2cc00().write(|w| w.bits(0));
+        }
+
+        // Configure multi-master
+        if !self.multi_master {
+            self.regs()
+                .i2cc00()
+                .modify(|_, w| w.dis_multimaster_capability_for_master_fn_only().set_bit());
+        }
+
+        // Enable master function and bus auto-release
+        self.regs().i2cc00().modify(|_, w| {
+            w.enbl_bus_autorelease_when_scllow_sdalow_or_slave_mode_inactive_timeout()
+                .set_bit()
+                .enbl_master_fn()
+                .set_bit()
+        });
+
+        // Configure timing
+        configure_timing(self.regs(), config)?;
+
+        // Clear all interrupts
+        unsafe {
+            self.regs().i2cm14().write(|w| w.bits(0xffff_ffff));
+        }
+
+        // Enable interrupts for packet mode
+        self.regs().i2cm10().modify(|_, w| {
+            w.enbl_pkt_cmd_done_int()
+                .set_bit()
+                .enbl_bus_recover_done_int()
+                .set_bit()
+        });
+
+        if self.smbus_alert {
+            self.regs()
+                .i2cm10()
+                .modify(|_, w| w.enbl_smbus_dev_alert_int().set_bit());
+        }
+
+        Ok(())
+    }
+
+    /// Enable interrupts
+    pub fn enable_interrupts(&mut self, mask: u32) {
+        unsafe {
+            self.regs().i2cm10().write(|w| w.bits(mask));
+        }
+    }
+
+    /// Clear interrupts
+    pub fn clear_interrupts(&mut self, mask: u32) {
+        unsafe {
+            self.regs().i2cm14().write(|w| w.bits(mask));
+        }
+    }
+
+    /// Check if bus is busy
+    ///
+    /// Checks if any I2C transfer is currently active by examining status register bits.
+    #[must_use]
+    pub fn is_bus_busy(&self) -> bool {
+        let status = self.regs().i2cm14().read().bits();
+        // Bus is busy if any transfer command bits are set
+        (status
+            & (constants::AST_I2CM_TX_CMD
+                | constants::AST_I2CM_RX_CMD
+                | constants::AST_I2CM_START_CMD))
+            != 0
+    }
+
+    /// Wait for completion with timeout (visible to master/transfer modules).
+    /// Calls the caller-supplied `yield_ns` closure between status polls.
+    pub(crate) fn wait_completion(&mut self, timeout_us: u32) -> Result<(), I2cError> {
+        let mut timeout = timeout_us;
+        self.completion = false;
+
+        while timeout > 0 && !self.completion {
+            self.handle_interrupt()?;
+            timeout = timeout.saturating_sub(1);
+            (self.yield_ns)(100_000);
+        }
+
+        if self.completion {
+            Ok(())
+        } else {
+            Err(I2cError::Timeout)
+        }
+    }
+}
+
diff --git a/target/ast10x0/peripherals/i2c/error.rs b/target/ast10x0/peripherals/i2c/error.rs
new file mode 100644
index 0000000..304f921
--- /dev/null
+++ b/target/ast10x0/peripherals/i2c/error.rs
@@ -0,0 +1,30 @@
+// Licensed under the Apache-2.0 license
+
+//! Core error types without OS dependencies
+
+/// I2C error type
+#[derive(Debug, Copy, Clone, PartialEq, Eq)]
+pub enum I2cError {
+    /// Data overrun
+    Overrun,
+    /// No acknowledge from device
+    NoAcknowledge,
+    /// Operation timeout
+    Timeout,
+    /// Bus recovery failed
+    BusRecoveryFailed,
+    /// Bus error
+    Bus,
+    /// Bus busy
+    Busy,
+    /// Invalid parameter
+    Invalid,
+    /// Abnormal condition
+    Abnormal,
+    /// Arbitration loss (multi-master)
+    ArbitrationLoss,
+    /// Slave mode error
+    SlaveError,
+    /// Invalid address
+    InvalidAddress,
+}
diff --git a/target/ast10x0/peripherals/i2c/global.rs b/target/ast10x0/peripherals/i2c/global.rs
new file mode 100644
index 0000000..3b0e0d5
--- /dev/null
+++ b/target/ast10x0/peripherals/i2c/global.rs
@@ -0,0 +1,79 @@
+// Licensed under the Apache-2.0 license
+
+//! I2C global hardware initialization utility
+//!
+//! Configures I2C global registers (i2cg0c, i2cg10).
+//! This must be called once after SCU has been configured by the board layer.
+//!
+//! # Initialization Order
+//!
+//! The board layer must call init functions in this order:
+//! 1. Enable I2C clock via SCU
+//! 2. Assert I2C reset via SCU
+//! 3. Delay
+//! 4. Deassert I2C reset via SCU
+//! 5. Delay
+//! 6. Call `init_i2c_global()` to configure I2C registers
+//!
+//! Example in board crate:
+//! ```ignore
+//! let scu = unsafe { ScuRegisters::new_global() };
+//! scu.ungate_clock_mask(ClockRegisterHalf::Lower, 1 << 2);
+//! scu.assert_reset_mask(ScuRegisterHalf::Upper, 1 << 2);
+//! delay_us(1000);
+//! scu.deassert_reset_mask(ScuRegisterHalf::Upper, 1 << 2);
+//! delay_us(1000);
+//! unsafe { i2c::init_i2c_global() };
+//! ```
+//!
+//! # Register Details
+//!
+//! ## I2CG0C - I2C Global Control Register
+//! - `clk_divider_mode_sel`: Enable new clock divider mode
+//! - `reg_definition_sel`: Select new register definition
+//! - `select_the_action_when_slave_pkt_mode_rxbuf_empty`: RX buffer empty action
+//!
+//! ## I2CG10 - I2C Global Clock Divider Register
+//! Value: `0x6222_0803`
+//! - Bits [31:24] = 0x62: Base clk4 for auto recovery timeout
+//! - Bits [23:16] = 0x22: Base clk3 for Standard-mode (100kHz), tBuf=5.76us
+//! - Bits [15:8]  = 0x08: Base clk2 for Fast-mode (400kHz), tBuf=1.6us
+//! - Bits [7:0]   = 0x03: Base clk1 for Fast-mode Plus (1MHz), tBuf=0.8us
+//!
+//! Based on APB clock = 50MHz:
+//! ```text
+//! div  : scl       : baseclk [APB/((div/2) + 1)] : tBuf [1/bclk * 16]
+//! 0x03 : 1MHz      : 20MHz                       : 0.8us  (Fast-mode Plus)
+//! 0x08 : 400kHz    : 10MHz                       : 1.6us  (Fast-mode)
+//! 0x22 : 99.21kHz  : 2.77MHz                     : 5.76us (Standard-mode)
+//! ```
+
+use ast1060_pac;
+
+/// Configure I2C global registers (one-time init, after SCU setup).
+///
+/// # Safety
+/// - Must be called only once, after SCU has enabled I2C clock and deasserted reset.
+/// - Not thread-safe; caller must ensure single invocation.
+/// - Assumes I2C controller clock is already enabled by the board layer.
+pub unsafe fn init_i2c_global() {
+    let i2cg = unsafe { &*ast1060_pac::I2cglobal::ptr() };
+
+    // Configure global settings
+    i2cg.i2cg0c().write(|w| {
+        w.clk_divider_mode_sel()
+            .set_bit()
+            .reg_definition_sel()
+            .set_bit()
+            .select_the_action_when_slave_pkt_mode_rxbuf_empty()
+            .set_bit()
+    });
+
+    // Set base clock dividers for different speeds
+    // APB clk: 50MHz
+    // I2CG10[31:24] = 0x62: base clk4 for i2c auto recovery timeout counter
+    // I2CG10[23:16] = 0x22: base clk3 for Standard-mode (100kHz) min tBuf 4.7us
+    // I2CG10[15:8]  = 0x08: base clk2 for Fast-mode (400kHz) min tBuf 1.3us
+    // I2CG10[7:0]   = 0x03: base clk1 for Fast-mode Plus (1MHz) min tBuf 0.5us
+    unsafe { i2cg.i2cg10().write(|w| w.bits(0x6222_0803)) };
+}
diff --git a/target/ast10x0/peripherals/i2c/hal_impl.rs b/target/ast10x0/peripherals/i2c/hal_impl.rs
new file mode 100644
index 0000000..604bcd2
--- /dev/null
+++ b/target/ast10x0/peripherals/i2c/hal_impl.rs
@@ -0,0 +1,269 @@
+// Licensed under the Apache-2.0 license
+
+//! embedded-hal trait implementations for AST1060 I2C driver
+//!
+//! This module provides [embedded-hal](https://docs.rs/embedded-hal) trait implementations
+//! for the AST1060 I2C controller, enabling compatibility with the broader Rust embedded
+//! ecosystem.
+//!
+//! # Implementation Status
+//!
+//! Currently implements:
+//! - [`embedded_hal::i2c::I2c`] - Master mode I2C operations
+//! - [`embedded_hal::i2c::Error`] - Standard error type mapping
+//!
+//! This allows the AST1060 I2C driver to be used with any crate that accepts embedded-hal
+//! I2C traits, such as device drivers, protocols, and middleware.
+//!
+//! # Hardware Features
+//!
+//! The AST1060 I2C controller supports:
+//! - **14 independent I2C controllers**
+//! - **Buffer mode with 32-byte FIFO** for efficient transfers
+//! - **Multi-master capability** with arbitration
+//! - **Standard (100 kHz) and Fast (400 kHz) modes**
+//! - **Slave/target mode** with hardware packet mode and interrupts
+//! - **`SMBus` protocol support**
+//! - **Automatic bus recovery**
+//!
+//! # Slave Mode API
+//!
+//! Slave functionality is provided through hardware-specific methods:
+//! - [`configure_slave()`](super::controller::Ast1060I2c::configure_slave) - Configure slave address and mode
+//! - [`slave_read()`](super::controller::Ast1060I2c::slave_read) - Read data received from master
+//! - [`slave_write()`](super::controller::Ast1060I2c::slave_write) - Write response data for master
+//! - [`handle_slave_interrupt()`](super::controller::Ast1060I2c::handle_slave_interrupt) - Process slave events
+//!
+//! These methods are more ergonomic and hardware-aware than generic HAL traits,
+//! exposing AST1060-specific features like packet mode and structured interrupt events.
+//!
+//! # Integration
+//!
+//! For Hubris RTOS integration, see the `drv-ast1060-i2c-refactor` crate which
+//! wraps this driver to implement the `drv-i2c-types::I2cHardware` trait.
+//!
+//! # Example Usage
+//!
+//! ```no_run
+//! use embedded_hal::i2c::I2c;
+//! use aspeed_ddk::i2c_core::controller::Ast1060I2c;
+//! use aspeed_ddk::i2c_core::types::{I2cConfig, I2cSpeed};
+//!
+//! // Initialize I2C controller 0 at 400 kHz
+//! let config = I2cConfig::new(I2cSpeed::Fast);
+//! let mut i2c = unsafe { Ast1060I2c::new(0) };
+//! i2c.init_hardware(&config);
+//!
+//! // Use embedded-hal I2c trait methods
+//! i2c.write(0x50, &[0x10, 0x20])?;
+//! let mut buffer = [0u8; 4];
+//! i2c.read(0x51, &mut buffer)?;
+//! i2c.write_read(0x52, &[0x00], &mut buffer)?;
+//! # Ok::<(), aspeed_ddk::i2c_core::error::I2cError>(())
+//! ```
+
+use super::controller::Ast1060I2c;
+use super::error::I2cError;
+use embedded_hal::i2c::{
+    Error, ErrorKind, ErrorType, I2c, NoAcknowledgeSource, Operation, SevenBitAddress,
+};
+
+// ============================================================================
+// embedded-hal Error Trait Implementation
+// ============================================================================
+
+/// Implements the embedded-hal Error trait for `I2cError`.
+///
+/// Maps AST1060-specific I2C errors to standard embedded-hal `ErrorKind` values.
+/// This enables interoperability with any code using the embedded-hal I2C traits.
+///
+/// # Error Mappings
+///
+/// - `NoAcknowledge` → `ErrorKind::NoAcknowledge` - Slave did not acknowledge
+/// - `ArbitrationLoss` → `ErrorKind::ArbitrationLoss` - Lost bus arbitration
+/// - `Bus`, `BusRecoveryFailed` → `ErrorKind::Bus` - Bus errors
+/// - `Overrun` → `ErrorKind::Overrun` - Data overrun/underrun
+/// - All others → `ErrorKind::Other` - Hardware-specific errors
+impl Error for I2cError {
+    fn kind(&self) -> ErrorKind {
+        match self {
+            I2cError::NoAcknowledge => ErrorKind::NoAcknowledge(NoAcknowledgeSource::Unknown),
+            I2cError::ArbitrationLoss => ErrorKind::ArbitrationLoss,
+            I2cError::Bus | I2cError::BusRecoveryFailed => ErrorKind::Bus,
+            I2cError::Overrun => ErrorKind::Overrun,
+            I2cError::Timeout
+            | I2cError::Busy
+            | I2cError::Invalid
+            | I2cError::Abnormal
+            | I2cError::SlaveError
+            | I2cError::InvalidAddress => ErrorKind::Other,
+        }
+    }
+}
+
+/// Associates the `I2cError` type with `Ast1060I2c` for embedded-hal trait implementations.
+impl<Y: FnMut(u32)> ErrorType for Ast1060I2c<'_, Y> {
+    type Error = I2cError;
+}
+
+// ============================================================================
+// embedded-hal I2c Trait Implementation (Master Mode)
+// ============================================================================
+
+/// Implements the embedded-hal I2c trait for `Ast1060I2c`.
+///
+/// Provides standard I2C master mode operations compatible with the embedded-hal ecosystem.
+/// This implementation uses the AST1060's buffer mode with a 32-byte FIFO for efficient
+/// data transfer.
+///
+/// # Supported Operations
+///
+/// - **write**: Send data to a slave device
+/// - **read**: Receive data from a slave device  
+/// - **`write_read`**: Write then read without releasing the bus (combined transaction)
+/// - **transaction**: Execute a sequence of read/write operations
+///
+/// # Buffer Mode Limitations
+///
+/// The AST1060 hardware uses a 32-byte FIFO buffer. For transfers larger than 32 bytes,
+/// the driver automatically splits them into multiple hardware transactions while
+/// maintaining the logical transaction semantics.
+///
+/// # Example
+///
+/// ```no_run
+/// use embedded_hal::i2c::I2c;
+/// # use aspeed_ddk::i2c_core::controller::Ast1060I2c;
+/// # let mut i2c = unsafe { Ast1060I2c::new(0) };
+///
+/// // Write data to slave at address 0x50
+/// i2c.write(0x50, &[0x00, 0x01, 0x02])?;
+///
+/// // Read data from slave at address 0x51
+/// let mut buffer = [0u8; 4];
+/// i2c.read(0x51, &mut buffer)?;
+///
+/// // Combined write-read transaction
+/// i2c.write_read(0x52, &[0xA0], &mut buffer)?;
+/// # Ok::<(), aspeed_ddk::i2c_core::error::I2cError>(())
+/// ```
+impl<Y: FnMut(u32)> I2c<SevenBitAddress> for Ast1060I2c<'_, Y> {
+    /// Writes data to an I2C slave device.
+    ///
+    /// # Arguments
+    ///
+    /// * `address` - 7-bit I2C slave address
+    /// * `bytes` - Data bytes to write
+    ///
+    /// # Errors
+    ///
+    /// Returns an error if:
+    /// - The slave does not acknowledge (`NoAcknowledge`)
+    /// - Bus arbitration is lost (`ArbitrationLoss`)
+    /// - A bus error occurs (`Bus`)
+    /// - The operation times out (`Timeout`)
+    fn write(&mut self, address: SevenBitAddress, bytes: &[u8]) -> Result<(), Self::Error> {
+        Ast1060I2c::write(self, address, bytes)
+    }
+
+    /// Reads data from an I2C slave device.
+    ///
+    /// # Arguments
+    ///
+    /// * `address` - 7-bit I2C slave address
+    /// * `buffer` - Buffer to store received data
+    ///
+    /// # Errors
+    ///
+    /// Returns an error if:
+    /// - The slave does not acknowledge (`NoAcknowledge`)
+    /// - Bus arbitration is lost (`ArbitrationLoss`)
+    /// - A bus error occurs (`Bus`)
+    /// - The operation times out (`Timeout`)
+    fn read(&mut self, address: SevenBitAddress, buffer: &mut [u8]) -> Result<(), Self::Error> {
+        Ast1060I2c::read(self, address, buffer)
+    }
+
+    /// Performs a combined write-read transaction without releasing the bus.
+    ///
+    /// This is commonly used to write a register address followed by reading
+    /// the register value in a single atomic transaction.
+    ///
+    /// # Arguments
+    ///
+    /// * `address` - 7-bit I2C slave address
+    /// * `bytes` - Data bytes to write first
+    /// * `buffer` - Buffer to store read data
+    ///
+    /// # Errors
+    ///
+    /// Returns an error if:
+    /// - The slave does not acknowledge (`NoAcknowledge`)
+    /// - Bus arbitration is lost (`ArbitrationLoss`)
+    /// - A bus error occurs (`Bus`)
+    /// - The operation times out (`Timeout`)
+    ///
+    /// # Example
+    ///
+    /// ```no_run
+    /// # use embedded_hal::i2c::I2c;
+    /// # use aspeed_ddk::i2c_core::controller::Ast1060I2c;
+    /// # let mut i2c = unsafe { Ast1060I2c::new(0) };
+    /// // Read from register 0x10 at slave address 0x50
+    /// let mut data = [0u8; 4];
+    /// i2c.write_read(0x50, &[0x10], &mut data)?;
+    /// # Ok::<(), aspeed_ddk::i2c_core::error::I2cError>(())
+    /// ```
+    fn write_read(
+        &mut self,
+        address: SevenBitAddress,
+        bytes: &[u8],
+        buffer: &mut [u8],
+    ) -> Result<(), Self::Error> {
+        Ast1060I2c::write_read(self, address, bytes, buffer)
+    }
+
+    /// Executes a sequence of I2C operations as a single transaction.
+    ///
+    /// Each operation is executed in order. The bus is held between operations
+    /// and only released after all operations complete or an error occurs.
+    ///
+    /// # Arguments
+    ///
+    /// * `address` - 7-bit I2C slave address for all operations
+    /// * `operations` - Slice of read/write operations to execute
+    ///
+    /// # Errors
+    ///
+    /// Returns an error if any operation fails. Subsequent operations are not
+    /// executed if an error occurs.
+    ///
+    /// # Example
+    ///
+    /// ```no_run
+    /// # use embedded_hal::i2c::{I2c, Operation};
+    /// # use aspeed_ddk::i2c_core::controller::Ast1060I2c;
+    /// # let mut i2c = unsafe { Ast1060I2c::new(0) };
+    /// let mut read_buf = [0u8; 4];
+    /// let mut ops = [
+    ///     Operation::Write(&[0x10]),        // Write register address
+    ///     Operation::Read(&mut read_buf),   // Read register value
+    /// ];
+    /// i2c.transaction(0x50, &mut ops)?;
+    /// # Ok::<(), aspeed_ddk::i2c_core::error::I2cError>(())
+    /// ```
+    fn transaction(
+        &mut self,
+        address: SevenBitAddress,
+        operations: &mut [Operation<'_>],
+    ) -> Result<(), Self::Error> {
+        // Execute each operation in sequence
+        for op in operations {
+            match op {
+                Operation::Read(buf) => self.read(address, buf)?,
+                Operation::Write(data) => self.write(address, data)?,
+            }
+        }
+        Ok(())
+    }
+}
diff --git a/target/ast10x0/peripherals/i2c/master.rs b/target/ast10x0/peripherals/i2c/master.rs
new file mode 100644
index 0000000..d473981
--- /dev/null
+++ b/target/ast10x0/peripherals/i2c/master.rs
@@ -0,0 +1,598 @@
+// Licensed under the Apache-2.0 license
+
+//! Master mode operations
+//!
+//! # Reference Implementation
+//!
+//! This follows the transfer logic from the original working code:
+//! - **aspeed-rust/src/i2c/ast1060_i2c.rs** lines 960-1120
+//!   - `aspeed_i2c_read()` - RX command building for byte/buffer/DMA modes
+//!   - `aspeed_i2c_write()` - TX command building for byte/buffer/DMA modes
+//!   - `i2c_aspeed_transfer()` - Main transfer entry point
+//!
+//! # Key Register Usage
+//!
+//! - **i2cm18** (Master Command Register): All command bits written here
+//!   - Command: `PKT_EN | pkt_addr(addr) | START_CMD | TX/RX_CMD | BUFF_EN | STOP_CMD`
+//!   - Reference: `ast1060_i2c.rs:1024` and `ast1060_i2c.rs:1107`
+//!
+//! - **i2cc08** (Byte Buffer Register): TX/RX byte data for byte mode
+//!   - `tx_byte_buffer()`: Write byte to transmit (`ast1060_i2c.rs:1101`)
+//!   - `rx_byte_buffer()`: Read received byte (`ast1060_i2c.rs:790`)
+//!
+//! - **i2cc0c** (Buffer Size Register): Buffer sizes for buffer mode
+//!   - `tx_data_byte_count()`: Set TX count (`ast1060_i2c.rs:1089`)
+//!   - `rx_pool_buffer_size()`: Set RX size (`ast1060_i2c.rs:1011`)
+//!
+//! - **i2cm14** (Interrupt Status Register): Read status, write-to-clear
+//!   - Reference: `ast1060_i2c.rs:849-870` (`aspeed_i2c_master_irq`)
+
+use super::{constants, controller::Ast1060I2c, error::I2cError, types::I2cXferMode};
+
+impl<Y: FnMut(u32)> Ast1060I2c<'_, Y> {
+    /// Write bytes to an I2C device
+    pub fn write(&mut self, addr: u8, bytes: &[u8]) -> Result<(), I2cError> {
+        if bytes.is_empty() {
+            return Ok(());
+        }
+
+        match self.xfer_mode {
+            I2cXferMode::ByteMode => self.write_byte_mode(addr, bytes),
+            I2cXferMode::BufferMode => self.write_buffer_mode(addr, bytes),
+            I2cXferMode::DmaMode => self.write_dma_mode(addr, bytes),
+        }
+    }
+
+    /// Read bytes from an I2C device
+    pub fn read(&mut self, addr: u8, buffer: &mut [u8]) -> Result<(), I2cError> {
+        if buffer.is_empty() {
+            return Ok(());
+        }
+
+        match self.xfer_mode {
+            I2cXferMode::ByteMode => self.read_byte_mode(addr, buffer),
+            I2cXferMode::BufferMode => self.read_buffer_mode(addr, buffer),
+            I2cXferMode::DmaMode => self.read_dma_mode(addr, buffer),
+        }
+    }
+
+    /// Write then read (combined transaction)
+    pub fn write_read(
+        &mut self,
+        addr: u8,
+        bytes: &[u8],
+        buffer: &mut [u8],
+    ) -> Result<(), I2cError> {
+        // Write phase
+        self.write(addr, bytes)?;
+
+        // Read phase
+        self.read(addr, buffer)?;
+
+        Ok(())
+    }
+
+    /// Write in byte mode (for small transfers)
+    ///
+    /// Uses i2cc08 for TX byte data buffer, i2cm18 for commands.
+    /// Only sends START on first byte, STOP on last byte.
+    fn write_byte_mode(&mut self, addr: u8, bytes: &[u8]) -> Result<(), I2cError> {
+        let msg_len = bytes.len();
+
+        // Initialize transfer state
+        self.current_addr = addr;
+        #[allow(clippy::cast_possible_truncation)]
+        {
+            self.current_len = msg_len as u32;
+        }
+        self.current_xfer_cnt = 0;
+        self.completion = false;
+
+        // Clear any previous status
+        self.clear_interrupts(0xffff_ffff);
+
+        for (i, &byte) in bytes.iter().enumerate() {
+            let is_first = i == 0;
+            let is_last = i == msg_len - 1;
+
+            // Write data byte to TX byte buffer (i2cc08)
+            self.regs()
+                .i2cc08()
+                .modify(|_, w| unsafe { w.tx_byte_buffer().bits(byte) });
+
+            // Build command
+            let mut cmd = constants::AST_I2CM_PKT_EN | constants::AST_I2CM_TX_CMD;
+
+            // Only send START and address on first byte
+            if is_first {
+                cmd |= constants::ast_i2cm_pkt_addr(addr) | constants::AST_I2CM_START_CMD;
+            }
+
+            // Send STOP on last byte
+            if is_last {
+                cmd |= constants::AST_I2CM_STOP_CMD;
+            }
+
+            // Issue command to i2cm18
+            self.regs().i2cm18().write(|w| unsafe { w.bits(cmd) });
+
+            // Wait for completion
+            self.completion = false;
+            self.wait_completion(constants::DEFAULT_TIMEOUT_US)?;
+
+            // Check for errors (read from i2cm14 - interrupt status register)
+            let status = self.regs().i2cm14().read().bits();
+            if status & constants::AST_I2CM_TX_NAK != 0 {
+                return Err(I2cError::NoAcknowledge);
+            }
+
+            self.current_xfer_cnt += 1;
+        }
+
+        Ok(())
+    }
+
+    /// Read in byte mode
+    ///
+    /// Uses i2cc08 for RX byte data buffer, i2cm18 for commands.
+    /// Only sends START on first byte, NACK+STOP on last byte.
+    fn read_byte_mode(&mut self, addr: u8, buffer: &mut [u8]) -> Result<(), I2cError> {
+        let msg_len = buffer.len();
+
+        // Initialize transfer state
+        self.current_addr = addr;
+        #[allow(clippy::cast_possible_truncation)]
+        {
+            self.current_len = msg_len as u32;
+        }
+        self.current_xfer_cnt = 0;
+        self.completion = false;
+
+        // Clear any previous status
+        self.clear_interrupts(0xffff_ffff);
+
+        for (i, byte) in buffer.iter_mut().enumerate() {
+            let is_first = i == 0;
+            let is_last = i == msg_len - 1;
+
+            // Build command
+            let mut cmd = constants::AST_I2CM_PKT_EN | constants::AST_I2CM_RX_CMD;
+
+            // Only send START and address on first byte
+            if is_first {
+                cmd |= constants::ast_i2cm_pkt_addr(addr) | constants::AST_I2CM_START_CMD;
+            }
+
+            // Send NACK and STOP on last byte
+            if is_last {
+                cmd |= constants::AST_I2CM_RX_CMD_LAST | constants::AST_I2CM_STOP_CMD;
+            }
+
+            // Issue command to i2cm18
+            self.regs().i2cm18().write(|w| unsafe { w.bits(cmd) });
+
+            // Wait for completion
+            self.completion = false;
+            self.wait_completion(constants::DEFAULT_TIMEOUT_US)?;
+
+            // Read data from RX byte buffer (i2cc08)
+            *byte = self.regs().i2cc08().read().rx_byte_buffer().bits();
+
+            // Check status (read from i2cm14 - interrupt status register)
+            let status = self.regs().i2cm14().read().bits();
+            if status & constants::AST_I2CM_TX_NAK != 0 {
+                return Err(I2cError::NoAcknowledge);
+            }
+
+            self.current_xfer_cnt += 1;
+        }
+
+        Ok(())
+    }
+
+    /// Write in buffer mode (optimal for 2-32 bytes)
+    ///
+    /// Uses hardware buffer for efficient multi-byte transfers.
+    /// Single transaction model: START+addr on first chunk only,
+    /// subsequent chunks continue the transaction without re-addressing.
+    /// Reference: `ast1060_i2c.rs` `do_i2cm_tx()` continuation logic
+    fn write_buffer_mode(&mut self, addr: u8, bytes: &[u8]) -> Result<(), I2cError> {
+        let total_len = bytes.len();
+        let mut offset = 0;
+
+        // Initialize transfer state
+        self.current_addr = addr;
+        #[allow(clippy::cast_possible_truncation)]
+        {
+            self.current_len = total_len as u32;
+        }
+        self.current_xfer_cnt = 0;
+
+        while offset < total_len {
+            let chunk_len = core::cmp::min(constants::BUFFER_MODE_SIZE, total_len - offset);
+            let chunk = &bytes[offset..offset + chunk_len];
+            let is_first = offset == 0;
+            let is_last = offset + chunk_len >= total_len;
+
+            // Copy data to hardware buffer BEFORE issuing command
+            self.copy_to_buffer(chunk)?;
+
+            // Set TX byte count in i2cc0c (len - 1)
+            #[allow(clippy::cast_possible_truncation)]
+            self.regs()
+                .i2cc0c()
+                .modify(|_, w| unsafe { w.tx_data_byte_count().bits((chunk_len - 1) as u8) });
+
+            // Clear interrupts before command
+            self.clear_interrupts(0xffff_ffff);
+            self.completion = false;
+
+            // Build command based on chunk position
+            // First chunk: PKT_EN + addr + START + TX_CMD + TX_BUFF_EN
+            // Subsequent chunks: PKT_EN + TX_CMD + TX_BUFF_EN (NO START, NO addr)
+            let mut cmd = constants::AST_I2CM_PKT_EN
+                | constants::AST_I2CM_TX_CMD
+                | constants::AST_I2CM_TX_BUFF_EN;
+
+            // Only send START and address on first chunk
+            if is_first {
+                cmd |= constants::ast_i2cm_pkt_addr(addr) | constants::AST_I2CM_START_CMD;
+            }
+
+            // Add STOP on last chunk
+            if is_last {
+                cmd |= constants::AST_I2CM_STOP_CMD;
+            }
+
+            // Issue command to i2cm18
+            self.regs().i2cm18().write(|w| unsafe { w.bits(cmd) });
+
+            // Wait for completion
+            self.wait_completion(constants::DEFAULT_TIMEOUT_US)?;
+
+            // Check for errors
+            let status = self.regs().i2cm14().read().bits();
+            if status & constants::AST_I2CM_PKT_ERROR != 0 {
+                if status & constants::AST_I2CM_TX_NAK != 0 {
+                    return Err(I2cError::NoAcknowledge);
+                }
+                return Err(I2cError::Abnormal);
+            }
+
+            #[allow(clippy::cast_possible_truncation)]
+            {
+                self.current_xfer_cnt += chunk_len as u32;
+            }
+            offset += chunk_len;
+        }
+
+        Ok(())
+    }
+
+    /// Read in buffer mode
+    ///
+    /// Uses hardware buffer for efficient multi-byte transfers.
+    /// Single transaction model: START+addr on first chunk only,
+    /// subsequent chunks continue the transaction without re-addressing.
+    /// Reference: `ast1060_i2c.rs` `do_i2cm_rx()` lines 762-810
+    fn read_buffer_mode(&mut self, addr: u8, buffer: &mut [u8]) -> Result<(), I2cError> {
+        let total_len = buffer.len();
+        let mut offset = 0;
+
+        // Initialize transfer state
+        self.current_addr = addr;
+        #[allow(clippy::cast_possible_truncation)]
+        {
+            self.current_len = total_len as u32;
+        }
+        self.current_xfer_cnt = 0;
+
+        while offset < total_len {
+            let chunk_len = core::cmp::min(constants::BUFFER_MODE_SIZE, total_len - offset);
+            let is_first = offset == 0;
+            let is_last = offset + chunk_len >= total_len;
+
+            // Set RX buffer size in i2cc0c (len - 1)
+            #[allow(clippy::cast_possible_truncation)]
+            self.regs()
+                .i2cc0c()
+                .modify(|_, w| unsafe { w.rx_pool_buffer_size().bits((chunk_len - 1) as u8) });
+
+            // Clear interrupts before command
+            self.clear_interrupts(0xffff_ffff);
+            self.completion = false;
+
+            // Build command based on chunk position
+            // First chunk: PKT_EN + addr + START + RX_CMD + RX_BUFF_EN
+            // Subsequent chunks: PKT_EN + RX_CMD + RX_BUFF_EN (NO START, NO addr)
+            let mut cmd = constants::AST_I2CM_PKT_EN
+                | constants::AST_I2CM_RX_CMD
+                | constants::AST_I2CM_RX_BUFF_EN;
+
+            // Only send START and address on first chunk
+            if is_first {
+                cmd |= constants::ast_i2cm_pkt_addr(addr) | constants::AST_I2CM_START_CMD;
+            }
+
+            // Add NACK and STOP on last chunk
+            if is_last {
+                cmd |= constants::AST_I2CM_RX_CMD_LAST | constants::AST_I2CM_STOP_CMD;
+            }
+
+            // Issue command to i2cm18
+            self.regs().i2cm18().write(|w| unsafe { w.bits(cmd) });
+
+            // Wait for completion
+            self.wait_completion(constants::DEFAULT_TIMEOUT_US)?;
+
+            // Check for errors
+            let status = self.regs().i2cm14().read().bits();
+            if status & constants::AST_I2CM_PKT_ERROR != 0 {
+                if status & constants::AST_I2CM_TX_NAK != 0 {
+                    return Err(I2cError::NoAcknowledge);
+                }
+                return Err(I2cError::Abnormal);
+            }
+
+            // Copy from hardware buffer AFTER successful transfer
+            let chunk = &mut buffer[offset..offset + chunk_len];
+            self.copy_from_buffer(chunk)?;
+
+            #[allow(clippy::cast_possible_truncation)]
+            {
+                self.current_xfer_cnt += chunk_len as u32;
+            }
+            offset += chunk_len;
+        }
+
+        Ok(())
+    }
+
+    /// Handle interrupt (process completion status)
+    pub fn handle_interrupt(&mut self) -> Result<(), I2cError> {
+        let status = self.regs().i2cm14().read().bits();
+
+        // Check for packet mode completion
+        if status & constants::AST_I2CM_PKT_DONE != 0 {
+            self.completion = true;
+            self.clear_interrupts(constants::AST_I2CM_PKT_DONE);
+
+            // Check for errors
+            if status & constants::AST_I2CM_PKT_ERROR != 0 {
+                if status & constants::AST_I2CM_TX_NAK != 0 {
+                    return Err(I2cError::NoAcknowledge);
+                }
+                if status & constants::AST_I2CM_ARBIT_LOSS != 0 {
+                    return Err(I2cError::ArbitrationLoss);
+                }
+                if status & constants::AST_I2CM_ABNORMAL != 0 {
+                    return Err(I2cError::Abnormal);
+                }
+                return Err(I2cError::Bus);
+            }
+
+            return Ok(());
+        }
+
+        // Check for byte mode completion
+        if status & (constants::AST_I2CM_TX_ACK | constants::AST_I2CM_RX_DONE) != 0 {
+            self.completion = true;
+            self.clear_interrupts(status);
+            return Ok(());
+        }
+
+        // Check for errors
+        if status & constants::AST_I2CM_TX_NAK != 0 {
+            self.clear_interrupts(status);
+            return Err(I2cError::NoAcknowledge);
+        }
+
+        if status & constants::AST_I2CM_ABNORMAL != 0 {
+            self.clear_interrupts(status);
+            return Err(I2cError::Abnormal);
+        }
+
+        if status & constants::AST_I2CM_ARBIT_LOSS != 0 {
+            self.clear_interrupts(status);
+            return Err(I2cError::ArbitrationLoss);
+        }
+
+        if status & constants::AST_I2CM_SCL_LOW_TO != 0 {
+            self.clear_interrupts(status);
+            return Err(I2cError::Timeout);
+        }
+
+        Ok(())
+    }
+
+    // =========================================================================
+    // DMA mode
+    //
+    // Uses system SRAM (non-cached, caller-allocated) as the I2C DMA buffer.
+    // The DMA engine can move up to 4096 bytes in a single START/STOP
+    // transaction.
+    //
+    // Register layout for DMA master TX:
+    //   i2cm1c: dmatx_buf_len_byte = (len-1), dmatx_buf_len_wr_enbl_for_cur_write_cmd = 1
+    //   i2cm30: sdramdmabuffer_base_addr = physical address of DMA buffer
+    // For DMA master RX:
+    //   i2cm1c: dmarx_buf_len_byte = (len-1), dmarx_buf_len_wr_enbl_for_cur_write_cmd = 1
+    //   i2cm34: sdramdmabuffer_base_addr1 = physical address of DMA buffer
+    //
+    // Reference: aspeed-rust/src/i2c/ast1060_i2c.rs aspeed_i2c_write/read DmaMode branch
+    // =========================================================================
+
+    /// Write in DMA mode (up to 4096 bytes in a single transaction)
+    ///
+    /// The DMA buffer supplied to [`Ast1060I2c::new_with_dma`] is used as the
+    /// staging area. For transfers larger than `DMA_MODE_MAX_SIZE` the data is
+    /// chunked into successive START-less continuation transactions (i.e. the bus
+    /// is NOT released between chunks).
+    fn write_dma_mode(&mut self, addr: u8, bytes: &[u8]) -> Result<(), I2cError> {
+        let total_len = bytes.len();
+        let mut offset = 0;
+
+        self.current_addr = addr;
+        #[allow(clippy::cast_possible_truncation)]
+        {
+            self.current_len = total_len as u32;
+        }
+        self.current_xfer_cnt = 0;
+
+        while offset < total_len {
+            let chunk_len = core::cmp::min(constants::DMA_MODE_MAX_SIZE, total_len - offset);
+            let chunk = &bytes[offset..offset + chunk_len];
+            let is_first = offset == 0;
+            let is_last = offset + chunk_len >= total_len;
+
+            // Copy chunk to DMA buffer (non-cached SRAM)
+            {
+                let dma_buf = self.dma_buf.as_deref_mut().ok_or(I2cError::Invalid)?;
+                if dma_buf.len() < chunk_len {
+                    return Err(I2cError::Invalid);
+                }
+                dma_buf[..chunk_len].copy_from_slice(chunk);
+            }
+
+            let phy_addr = {
+                let dma_buf = self.dma_buf.as_deref().ok_or(I2cError::Invalid)?;
+                dma_buf.as_ptr() as u32
+            };
+
+            // Set DMA TX length in i2cm1c (len - 1)
+            #[allow(clippy::cast_possible_truncation)]
+            self.regs().i2cm1c().write(|w| unsafe {
+                w.dmatx_buf_len_byte()
+                    .bits((chunk_len - 1) as u16)
+                    .dmatx_buf_len_wr_enbl_for_cur_write_cmd()
+                    .set_bit()
+            });
+
+            // Set DMA TX buffer base address in i2cm30
+            self.regs()
+                .i2cm30()
+                .write(|w| unsafe { w.sdramdmabuffer_base_addr().bits(phy_addr) });
+
+            self.clear_interrupts(0xffff_ffff);
+            self.completion = false;
+
+            // Build command
+            let mut cmd = constants::AST_I2CM_PKT_EN
+                | constants::AST_I2CM_TX_CMD
+                | constants::AST_I2CM_TX_DMA_EN;
+
+            if is_first {
+                cmd |= constants::ast_i2cm_pkt_addr(addr) | constants::AST_I2CM_START_CMD;
+            }
+            if is_last {
+                cmd |= constants::AST_I2CM_STOP_CMD;
+            }
+
+            self.regs().i2cm18().write(|w| unsafe { w.bits(cmd) });
+
+            self.wait_completion(constants::DEFAULT_TIMEOUT_US)?;
+
+            let status = self.regs().i2cm14().read().bits();
+            if status & constants::AST_I2CM_PKT_ERROR != 0 {
+                if status & constants::AST_I2CM_TX_NAK != 0 {
+                    return Err(I2cError::NoAcknowledge);
+                }
+                return Err(I2cError::Abnormal);
+            }
+
+            #[allow(clippy::cast_possible_truncation)]
+            {
+                self.current_xfer_cnt += chunk_len as u32;
+            }
+            offset += chunk_len;
+        }
+
+        Ok(())
+    }
+
+    /// Read in DMA mode (up to 4096 bytes in a single transaction)
+    fn read_dma_mode(&mut self, addr: u8, buffer: &mut [u8]) -> Result<(), I2cError> {
+        let total_len = buffer.len();
+        let mut offset = 0;
+
+        self.current_addr = addr;
+        #[allow(clippy::cast_possible_truncation)]
+        {
+            self.current_len = total_len as u32;
+        }
+        self.current_xfer_cnt = 0;
+
+        while offset < total_len {
+            let chunk_len = core::cmp::min(constants::DMA_MODE_MAX_SIZE, total_len - offset);
+            let is_first = offset == 0;
+            let is_last = offset + chunk_len >= total_len;
+
+            {
+                let dma_buf = self.dma_buf.as_deref().ok_or(I2cError::Invalid)?;
+                if dma_buf.len() < chunk_len {
+                    return Err(I2cError::Invalid);
+                }
+            }
+
+            let phy_addr = {
+                let dma_buf = self.dma_buf.as_deref().ok_or(I2cError::Invalid)?;
+                dma_buf.as_ptr() as u32
+            };
+
+            // Set DMA RX length in i2cm1c (len - 1)
+            #[allow(clippy::cast_possible_truncation)]
+            self.regs().i2cm1c().modify(|_, w| unsafe {
+                w.dmarx_buf_len_byte()
+                    .bits((chunk_len - 1) as u16)
+                    .dmarx_buf_len_wr_enbl_for_cur_write_cmd()
+                    .set_bit()
+            });
+
+            // Set DMA RX buffer base address in i2cm34
+            self.regs()
+                .i2cm34()
+                .modify(|_, w| unsafe { w.sdramdmabuffer_base_addr1().bits(phy_addr) });
+
+            self.clear_interrupts(0xffff_ffff);
+            self.completion = false;
+
+            // Build command
+            let mut cmd = constants::AST_I2CM_PKT_EN
+                | constants::AST_I2CM_RX_CMD
+                | constants::AST_I2CM_RX_DMA_EN;
+
+            if is_first {
+                cmd |= constants::ast_i2cm_pkt_addr(addr) | constants::AST_I2CM_START_CMD;
+            }
+            if is_last {
+                cmd |= constants::AST_I2CM_RX_CMD_LAST | constants::AST_I2CM_STOP_CMD;
+            }
+
+            self.regs().i2cm18().write(|w| unsafe { w.bits(cmd) });
+
+            self.wait_completion(constants::DEFAULT_TIMEOUT_US)?;
+
+            let status = self.regs().i2cm14().read().bits();
+            if status & constants::AST_I2CM_PKT_ERROR != 0 {
+                if status & constants::AST_I2CM_TX_NAK != 0 {
+                    return Err(I2cError::NoAcknowledge);
+                }
+                return Err(I2cError::Abnormal);
+            }
+
+            // Copy from DMA buffer into caller's buffer
+            {
+                let dma_buf = self.dma_buf.as_deref().ok_or(I2cError::Invalid)?;
+                buffer[offset..offset + chunk_len].copy_from_slice(&dma_buf[..chunk_len]);
+            }
+
+            #[allow(clippy::cast_possible_truncation)]
+            {
+                self.current_xfer_cnt += chunk_len as u32;
+            }
+            offset += chunk_len;
+        }
+
+        Ok(())
+    }
+}
diff --git a/target/ast10x0/peripherals/i2c/mod.rs b/target/ast10x0/peripherals/i2c/mod.rs
new file mode 100644
index 0000000..583f3d6
--- /dev/null
+++ b/target/ast10x0/peripherals/i2c/mod.rs
@@ -0,0 +1,81 @@
+// Licensed under the Apache-2.0 license
+
+//! AST1060 I2C bare-metal driver core
+//!
+//! This module provides a portable, hardware-abstraction layer for the AST1060 I2C controller.
+//! It is designed to be usable in both bare-metal and RTOS environments without requiring
+//! OS-specific dependencies.
+//!
+//! # Features
+//!
+//! - Multi-master support
+//! - Master and slave (target) mode
+//! - **Buffer mode with 32-byte hardware FIFO**
+//! - **DMA mode with up to 4096-byte transfers** (requires non-cached SRAM buffer)
+//! - Clock stretching and bus recovery
+//! - `SMBus` alert support
+//! - Configurable speeds: Standard (100kHz), Fast (400kHz), Fast-plus (1MHz)
+//!
+//! # Architecture
+//!
+//! The driver is split into focused modules:
+//!
+//! - `controller`: Core hardware abstraction and initialization
+//! - `master`: Master mode operations (read/write)
+//! - `slave`: Slave (target) mode operations
+//! - `transfer`: Low-level transfer state machine
+//! - `timing`: Clock timing configuration
+//! - `recovery`: Bus recovery mechanisms
+//! - `types`: Core type definitions
+//! - `error`: Error types
+//! - `constants`: Hardware register constants
+//!
+//! # Usage Example
+//!
+//! ```rust,no_run
+//! use ast10x0_peripherals::i2c::*;
+//! use ast1060_pac;
+//!
+//! // Initialize I2C global registers ONCE before any controller use.
+//! init_i2c_global();
+//!
+//! // Build the I2C driver around UART1's register block. The yield
+//! // closure is invoked between status polls inside wait_completion.
+//! let config = I2cConfig::default();
+//! let mut i2c = unsafe {
+//!     Ast1060I2c::new(
+//!         ast1060_pac::I2c1::ptr(),
+//!         ast1060_pac::I2cbuff1::ptr(),
+//!         config,
+//!         |_ns| core::hint::spin_loop(),
+//!     )?
+//! };
+//!
+//! // Perform a master read.
+//! let mut data = [0u8; 4];
+//! i2c.read(0x50, &mut data)?;
+//! ```
+
+mod constants;
+mod controller;
+mod error;
+mod global;
+mod hal_impl;
+mod master;
+mod recovery;
+mod slave;
+mod timing;
+mod transfer;
+mod types;
+
+// Re-export public API
+pub use constants::*;
+pub use controller::Ast1060I2c;
+pub use error::I2cError;
+pub use global::init_i2c_global;
+pub use slave::{SlaveBuffer, SlaveConfig, SlaveEvent};
+pub use types::*;
+
+// Re-export HAL implementations for external use
+#[allow(unused_imports)]
+pub use hal_impl::*;
diff --git a/target/ast10x0/peripherals/i2c/recovery.rs b/target/ast10x0/peripherals/i2c/recovery.rs
new file mode 100644
index 0000000..0ebdc96
--- /dev/null
+++ b/target/ast10x0/peripherals/i2c/recovery.rs
@@ -0,0 +1,67 @@
+// Licensed under the Apache-2.0 license
+
+//! Bus recovery implementation
+
+use super::{constants, controller::Ast1060I2c, error::I2cError};
+
+impl<Y: FnMut(u32)> Ast1060I2c<'_, Y> {
+    /// Recover the I2C bus from stuck condition
+    pub fn recover_bus(&mut self) -> Result<(), I2cError> {
+        // Disable master and slave functionality
+        self.regs()
+            .i2cc00()
+            .modify(|_, w| w.enbl_master_fn().clear_bit().enbl_slave_fn().clear_bit());
+
+        // Enable master functionality
+        self.regs()
+            .i2cc00()
+            .modify(|_, w| w.enbl_master_fn().set_bit());
+
+        // Clear all interrupts
+        self.clear_interrupts(0xffff_ffff);
+
+        // Check SDA/SCL state before attempting recovery
+        // Only recover if SDA is stuck low while SCL is high
+        let line_status = self.regs().i2cc08().read();
+        if line_status.sampled_sdaline_state().bit() || !line_status.sampled_sclline_state().bit() {
+            // SDA is not stuck low, or SCL is also stuck - can't recover this way
+            return Err(I2cError::BusRecoveryFailed);
+        }
+
+        // Issue bus recovery command via I2CM18 (command register)
+        self.regs()
+            .i2cm18()
+            .modify(|_, w| w.enbl_bus_recover_cmd().set_bit());
+
+        // Wait for recovery completion
+        let mut timeout = 100_000; // 100ms
+        while timeout > 0 {
+            let status = self.regs().i2cm14().read().bits();
+
+            if status & constants::AST_I2CM_BUS_RECOVER != 0 {
+                self.clear_interrupts(constants::AST_I2CM_BUS_RECOVER);
+
+                if status & constants::AST_I2CM_BUS_RECOVER_FAIL != 0 {
+                    return Err(I2cError::BusRecoveryFailed);
+                }
+
+                return Ok(());
+            }
+
+            timeout -= 1;
+        }
+
+        Err(I2cError::Timeout)
+    }
+
+    /// Check if bus recovery is needed
+    #[must_use]
+    pub fn needs_recovery(&self) -> bool {
+        let status = self.regs().i2cm14().read().bits();
+
+        // Check for stuck conditions
+        (status & constants::AST_I2CM_SCL_LOW_TO != 0)
+            || (status & constants::AST_I2CM_SDA_DL_TO != 0)
+            || (status & constants::AST_I2CM_ABNORMAL != 0)
+    }
+}
diff --git a/target/ast10x0/peripherals/i2c/slave.rs b/target/ast10x0/peripherals/i2c/slave.rs
new file mode 100644
index 0000000..f969e26
--- /dev/null
+++ b/target/ast10x0/peripherals/i2c/slave.rs
@@ -0,0 +1,640 @@
+// Licensed under the Apache-2.0 license
+
+//! AST1060 I2C Slave/Target Mode Implementation
+//!
+//! This module provides slave (target) mode functionality for the AST1060 I2C controllers.
+//! In slave mode, the controller responds to requests from an external I2C master.
+
+use super::I2cXferMode;
+
+use super::{constants, controller::Ast1060I2c, error::I2cError};
+
+/// Hardware buffer size (32 bytes / 8 DWORDs)
+const BUFFER_SIZE: usize = 32;
+
+/// Maximum slave receive buffer size (hardware limitation)
+pub const SLAVE_BUFFER_SIZE: usize = 256;
+
+/// Slave RX DMA enable bit in slave command register (i2cs28 bit 9).
+///
+/// When set, the hardware writes received bytes into the DMA buffer pointed to
+/// by i2cs38/i2cs3c instead of the 32-byte FIFO. Supports up to 4096-byte transfers.
+const AST_I2CS_RX_DMA_EN: u32 = 1 << 9;
+
+/// Slave mode configuration
+#[derive(Debug, Clone, Copy)]
+pub struct SlaveConfig {
+    /// Primary slave address (7-bit)
+    pub address: u8,
+    /// Enable packet mode for slave
+    pub packet_mode: bool,
+    /// Use buffer mode (32 bytes) vs byte mode (1 byte)
+    pub buffer_mode: bool,
+}
+
+impl SlaveConfig {
+    /// Create a new slave configuration
+    pub fn new(address: u8) -> Result<Self, I2cError> {
+        if address > 0x7F {
+            return Err(I2cError::InvalidAddress);
+        }
+
+        Ok(Self {
+            address,
+            packet_mode: true, // Recommended for performance
+            buffer_mode: true, // Recommended for performance
+        })
+    }
+}
+
+/// Slave mode events
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum SlaveEvent {
+    /// Master is requesting to read from us (we need to send data)
+    ReadRequest,
+    /// Master is writing to us (we're receiving data)
+    WriteRequest,
+    /// Data received from master
+    DataReceived { len: usize },
+    /// Data sent to master
+    DataSent { len: usize },
+    /// Data received from master and send data to master (combined event)
+    DataReceivedAndSent { rx_len: usize, tx_len: usize },
+    /// Stop condition received
+    Stop,
+}
+
+/// Slave mode data buffer for application-level buffering
+pub struct SlaveBuffer {
+    data: [u8; SLAVE_BUFFER_SIZE],
+    len: usize,
+}
+
+impl Default for SlaveBuffer {
+    fn default() -> Self {
+        Self::new()
+    }
+}
+
+impl SlaveBuffer {
+    #[must_use]
+    pub const fn new() -> Self {
+        Self {
+            data: [0u8; SLAVE_BUFFER_SIZE],
+            len: 0,
+        }
+    }
+
+    #[must_use]
+    pub fn data(&self) -> &[u8] {
+        &self.data[..self.len]
+    }
+
+    pub fn data_mut(&mut self) -> &mut [u8] {
+        &mut self.data[..self.len]
+    }
+
+    pub fn set_len(&mut self, len: usize) {
+        self.len = len.min(SLAVE_BUFFER_SIZE);
+    }
+
+    #[must_use]
+    pub fn len(&self) -> usize {
+        self.len
+    }
+
+    #[must_use]
+    pub fn is_empty(&self) -> bool {
+        self.len == 0
+    }
+
+    pub fn clear(&mut self) {
+        self.len = 0;
+    }
+
+    pub fn write(&mut self, data: &[u8]) -> usize {
+        let to_copy = data.len().min(SLAVE_BUFFER_SIZE);
+        self.data[..to_copy].copy_from_slice(&data[..to_copy]);
+        self.len = to_copy;
+        to_copy
+    }
+}
+
+impl<Y: FnMut(u32)> Ast1060I2c<'_, Y> {
+    #[inline]
+    fn slave_rx_len(&self) -> usize {
+        if self.xfer_mode == I2cXferMode::DmaMode {
+            self.regs().i2cs4c().read().dmarx_actual_len_byte().bits() as usize
+        } else {
+            self.regs()
+                .i2cc0c()
+                .read()
+                .actual_rxd_pool_buffer_size()
+                .bits() as usize
+        }
+    }
+
+    /// Arm slave receive path based on transfer mode.
+    ///
+    /// This mirrors the old AST1060 driver behavior where packet-slave IRQ
+    /// branches re-arm either RX FIFO or RX DMA depending on `xfer_mode`.
+    fn arm_slave_receive(&mut self, cmd: &mut u32) {
+        if self.xfer_mode == I2cXferMode::DmaMode {
+            if let Some(dma_buf) = self.dma_buf.as_deref_mut() {
+                let dma_addr = dma_buf.as_mut_ptr() as u32;
+                let dma_len = u16::try_from(dma_buf.len().min(4096) - 1).unwrap_or(u16::MAX);
+                unsafe {
+                    self.regs().i2cs4c().write(|w| w.bits(0));
+                    self.regs().i2cs38().write(|w| w.bits(dma_addr));
+                    self.regs().i2cs3c().write(|w| w.bits(dma_addr));
+                    self.regs().i2cs2c().write(|w| {
+                        w.dmarx_buf_len_byte()
+                            .bits(dma_len)
+                            .dmarx_buf_len_wr_enbl_for_cur_cmd()
+                            .set_bit()
+                    });
+                }
+                *cmd |= AST_I2CS_RX_DMA_EN;
+            } else {
+                *cmd |= constants::AST_I2CS_RX_BUFF_EN;
+                self.regs().i2cc0c().write(|w| unsafe {
+                    w.rx_pool_buffer_size().bits(constants::I2C_BUF_SIZE - 1)
+                });
+            }
+        } else if self.xfer_mode == I2cXferMode::BufferMode {
+            *cmd |= constants::AST_I2CS_RX_BUFF_EN;
+            self.regs()
+                .i2cc0c()
+                .write(|w| unsafe { w.rx_pool_buffer_size().bits(constants::I2C_BUF_SIZE - 1) });
+        } else {
+            *cmd &= !constants::AST_I2CS_PKT_MODE_EN;
+        }
+    }
+
+    /// Configure the controller for slave mode
+    pub fn configure_slave(&mut self, config: &SlaveConfig) -> Result<(), I2cError> {
+        // Ensure master mode is disabled first
+        self.regs()
+            .i2cc00()
+            .modify(|_, w| w.enbl_master_fn().clear_bit());
+
+        // Set slave address
+        self.regs().i2cs40().write(|w| unsafe {
+            w.slave_dev_addr1()
+                .bits(config.address)
+                .enbl_slave_dev_addr1only_for_new_reg_mode()
+                .bit(true)
+        });
+
+        // Clear slave interrupts
+        self.clear_slave_interrupts();
+
+        // Enable slave mode and save address byte in packet mode (I2CC00 bit 20)
+        // This makes the hardware include the destination address byte in the receive buffer
+        // which is required for MCTP-over-SMBus (DSP0237) packet format.
+        self.regs().i2cc00().modify(|r, w| unsafe {
+            w.bits(
+                r.bits() | constants::AST_I2CC_SLAVE_EN | constants::AST_I2CC_SLAVE_PKT_SAVE_ADDR,
+            )
+        });
+
+        // Configure slave mode
+        let mut cmd = 0u32;
+
+        if config.packet_mode {
+            cmd |= constants::AST_I2CS_PKT_MODE_EN;
+            cmd |= constants::AST_I2CS_ACTIVE_ALL;
+        }
+
+        if self.xfer_mode == I2cXferMode::BufferMode {
+            cmd |= constants::AST_I2CS_RX_BUFF_EN;
+            self.regs()
+                .i2cc0c()
+                .write(|w| unsafe { w.rx_pool_buffer_size().bits(constants::I2C_BUF_SIZE - 1) });
+        } else if self.xfer_mode == I2cXferMode::DmaMode {
+            if let Some(dma_buf) = self.dma_buf.as_deref_mut() {
+                // Arm slave DMA: point hardware at the non-cached buffer and enable RX_DMA.
+                // i2cs38/i2cs3c hold the physical DMA buffer address (same address in
+                // both registers — the hardware uses both for different address widths).
+                // i2cs2c sets the DMA receive length and enables the length register.
+                let dma_addr = dma_buf.as_mut_ptr() as u32;
+                let dma_len = u16::try_from(dma_buf.len().min(4096) - 1).unwrap_or(u16::MAX);
+                unsafe {
+                    self.regs().i2cs38().write(|w| w.bits(dma_addr));
+                    self.regs().i2cs3c().write(|w| w.bits(dma_addr));
+                    self.regs().i2cs2c().write(|w| {
+                        w.dmarx_buf_len_byte()
+                            .bits(dma_len)
+                            .dmarx_buf_len_wr_enbl_for_cur_cmd()
+                            .set_bit()
+                    });
+                }
+                cmd |= AST_I2CS_RX_DMA_EN;
+            } else {
+                // No DMA buffer provided — fall back to buffer mode.
+                cmd |= constants::AST_I2CS_RX_BUFF_EN;
+                self.regs().i2cc0c().write(|w| unsafe {
+                    w.rx_pool_buffer_size().bits(constants::I2C_BUF_SIZE - 1)
+                });
+            }
+        } else {
+            cmd &= !constants::AST_I2CS_PKT_MODE_EN;
+        }
+
+        // Set slave command register
+        unsafe {
+            self.regs().i2cs28().write(|w| w.bits(cmd));
+        }
+
+        // Enable slave interrupts
+        self.enable_slave_interrupts();
+
+        Ok(())
+    }
+
+    /// Enable slave mode interrupts
+    fn enable_slave_interrupts(&mut self) {
+        let mut mask = constants::AST_I2CS_PKT_DONE | constants::AST_I2CS_INACTIVE_TO;
+        if self.xfer_mode == I2cXferMode::BufferMode || self.xfer_mode == I2cXferMode::DmaMode {
+            mask |= constants::AST_I2CM_ABNORMAL
+                | constants::AST_I2CM_NORMAL_STOP
+                | constants::AST_I2CM_RX_DONE
+                | constants::AST_I2CM_TX_ACK;
+        }
+
+        unsafe {
+            self.regs().i2cs20().write(|w| w.bits(mask));
+        }
+    }
+
+    /// Clear slave mode interrupts
+    fn clear_slave_interrupts(&mut self) {
+        unsafe {
+            self.regs().i2cs24().write(|w| w.bits(0xFFFF_FFFF));
+            let _ = self.regs().i2cs24().read().bits();
+        }
+    }
+
+    /// Enable slave mode (re-enable after disable)
+    ///
+    /// This re-enables slave mode and interrupts without reconfiguring the address.
+    /// Use `configure_slave()` for initial setup, this for re-enabling after `disable_slave()`.
+    pub fn enable_slave(&mut self) {
+        // Enable slave mode
+        self.regs()
+            .i2cc00()
+            .modify(|_, w| w.enbl_slave_fn().set_bit());
+
+        // Enable slave interrupts
+        self.enable_slave_interrupts();
+    }
+
+    /// Disable slave mode
+    pub fn disable_slave(&mut self) {
+        // Disable interrupts
+        unsafe {
+            self.regs().i2cs20().write(|w| w.bits(0));
+        }
+
+        // Clear interrupts
+        self.clear_slave_interrupts();
+
+        // Disable slave mode
+        self.regs()
+            .i2cc00()
+            .modify(|_, w| w.enbl_slave_fn().clear_bit());
+    }
+
+    /// Check if slave has received data
+    #[must_use]
+    pub fn slave_has_data(&self) -> bool {
+        let status = self.regs().i2cs24().read().bits();
+        (status & constants::AST_I2CS_RX_DONE) != 0
+    }
+
+    /// Read data received in slave mode
+    pub fn slave_read(&mut self, buffer: &mut [u8]) -> Result<usize, I2cError> {
+        // Get receive length from buffer length register
+        if self.xfer_mode == I2cXferMode::BufferMode {
+            let len = self
+                .regs()
+                .i2cc0c()
+                .read()
+                .actual_rxd_pool_buffer_size()
+                .bits() as usize;
+            let to_read = len.min(buffer.len()).min(BUFFER_SIZE);
+
+            // Read from buffer
+            self.copy_from_buffer(&mut buffer[..to_read])?;
+
+            // Re-enable RX buffer
+            let mut cmd = constants::AST_I2CS_ACTIVE_ALL | constants::AST_I2CS_PKT_MODE_EN;
+            cmd |= constants::AST_I2CS_RX_BUFF_EN;
+            unsafe {
+                self.regs().i2cs28().write(|w| w.bits(cmd));
+            }
+
+            Ok(to_read)
+        } else if self.xfer_mode == I2cXferMode::DmaMode {
+            // DMA mode: the hardware has already DMA'd into `self.dma_buf`.
+            // Read actual received byte count from the DMA status register.
+            let hw_len = self.regs().i2cs4c().read().dmarx_actual_len_byte().bits() as usize;
+            let to_read = hw_len.min(buffer.len());
+
+            if let Some(dma_buf) = self.dma_buf.as_deref() {
+                let src_len = to_read.min(dma_buf.len());
+                buffer[..src_len].copy_from_slice(&dma_buf[..src_len]);
+            }
+
+            // Re-arm slave DMA for next receive
+            let mut cmd = constants::AST_I2CS_ACTIVE_ALL | constants::AST_I2CS_PKT_MODE_EN;
+            if let Some(dma_buf) = self.dma_buf.as_deref_mut() {
+                let dma_addr = dma_buf.as_mut_ptr() as u32;
+                let dma_len = u16::try_from(dma_buf.len().min(4096) - 1).unwrap_or(u16::MAX);
+                unsafe {
+                    self.regs().i2cs4c().write(|w| w.bits(0));
+                    self.regs().i2cs38().write(|w| w.bits(dma_addr));
+                    self.regs().i2cs3c().write(|w| w.bits(dma_addr));
+                    self.regs().i2cs2c().write(|w| {
+                        w.dmarx_buf_len_byte()
+                            .bits(dma_len)
+                            .dmarx_buf_len_wr_enbl_for_cur_cmd()
+                            .set_bit()
+                    });
+                }
+                cmd |= AST_I2CS_RX_DMA_EN;
+            } else {
+                cmd |= constants::AST_I2CS_RX_BUFF_EN;
+            }
+            unsafe {
+                self.regs().i2cs28().write(|w| w.bits(cmd));
+            }
+
+            Ok(to_read)
+        } else {
+            // byte mode
+            buffer[0] = self.regs().i2cc08().read().rx_byte_buffer().bits();
+
+            let cmd = constants::AST_I2CS_ACTIVE_ALL;
+            self.regs().i2cs28().write(|w| unsafe { w.bits(cmd) });
+
+            self.clear_slave_interrupts();
+            Ok(1)
+        }
+    }
+
+    /// Write data to send in slave mode (in response to read request)
+    pub fn slave_write(&mut self, data: &[u8]) -> Result<usize, I2cError> {
+        if data.is_empty() {
+            return Ok(0);
+        }
+
+        if self.xfer_mode == I2cXferMode::BufferMode {
+            let to_write = 1;
+
+            // Copy data to buffer
+            self.copy_to_buffer(&data[..to_write])?;
+
+            // Set transfer length
+            #[allow(clippy::cast_possible_truncation)]
+            self.regs()
+                .i2cc0c()
+                .write(|w| unsafe { w.tx_data_byte_count().bits(to_write as u8 - 1) });
+
+            // Trigger slave transmit
+            let mut cmd = constants::AST_I2CS_ACTIVE_ALL | constants::AST_I2CS_PKT_MODE_EN;
+            cmd |= constants::AST_I2CS_TX_BUFF_EN;
+            unsafe {
+                self.regs().i2cs28().write(|w| w.bits(cmd));
+            }
+            Ok(to_write)
+        } else if self.xfer_mode == I2cXferMode::DmaMode {
+            // In DMA mode, copy data to DMA buffer and set TX length
+            let dma_buf = self.dma_buf.as_deref_mut().ok_or(I2cError::Invalid)?;
+
+            // Copy data to DMA buffer starting at offset 0
+            let to_write = data.len().min(dma_buf.len());
+            unsafe {
+                core::ptr::copy_nonoverlapping(data.as_ptr(), dma_buf.as_mut_ptr(), to_write);
+            }
+
+            // Clear TX status/offset register
+            unsafe {
+                self.regs().i2cs4c().write(|w| w.bits(0));
+            }
+
+            // Set TX length (len - 1) and enable write
+            let tx_len = u16::try_from(to_write - 1).map_err(|_| I2cError::Invalid)?;
+            unsafe {
+                self.regs().i2cs2c().modify(|_, w| {
+                    w.dmatx_buf_len_byte()
+                        .bits(tx_len)
+                        .dmatx_buf_len_wr_enbl_for_cur_cmd()
+                        .set_bit()
+                });
+            }
+
+            // Trigger slave transmit with TX DMA enabled
+            let mut cmd = constants::AST_I2CS_ACTIVE_ALL | constants::AST_I2CS_PKT_MODE_EN;
+            cmd |= constants::AST_I2CS_TX_DMA_EN;
+            unsafe {
+                self.regs().i2cs28().write(|w| w.bits(cmd));
+            }
+
+            Ok(to_write)
+        } else {
+            // byte mode
+            let cmd = constants::AST_I2CS_ACTIVE_ALL | constants::AST_I2CS_TX_CMD;
+            unsafe {
+                self.regs()
+                    .i2cc08()
+                    .write(|w| w.tx_byte_buffer().bits(data[0]));
+                self.regs().i2cs28().write(|w| w.bits(cmd));
+            }
+            self.clear_slave_interrupts();
+
+            Ok(1)
+        }
+    }
+
+    /// Handle slave mode interrupt
+    #[allow(clippy::too_many_lines)]
+    pub fn handle_slave_interrupt(&mut self) -> Option<SlaveEvent> {
+        let status = self.regs().i2cs24().read().bits();
+
+        if status == 0 {
+            return None;
+        }
+
+        // Check for errors first
+        if (status & constants::AST_I2CS_PKT_ERROR) != 0 {
+            self.clear_slave_interrupts();
+            return None;
+        }
+
+        if (status & constants::AST_I2CS_PKT_DONE) != 0 {
+            let mut cmd: u32 = constants::AST_I2CS_ACTIVE_ALL | constants::AST_I2CS_PKT_MODE_EN;
+            unsafe {
+                self.regs()
+                    .i2cs24()
+                    .write(|w| w.bits(constants::AST_I2CS_PKT_DONE));
+            }
+            let sts = status & (!(constants::AST_I2CS_PKT_DONE | constants::AST_I2CS_PKT_ERROR));
+            if sts == constants::AST_I2CS_SLAVE_MATCH
+                || sts == constants::AST_I2CS_SLAVE_MATCH | constants::AST_I2CS_RX_DONE
+            {
+                // S: Sw
+                return Some(SlaveEvent::WriteRequest);
+            } else if sts == constants::AST_I2CS_SLAVE_MATCH | constants::AST_I2CS_WAIT_RX_DMA
+                || sts
+                    == constants::AST_I2CS_SLAVE_MATCH
+                        | constants::AST_I2CS_RX_DONE
+                        | constants::AST_I2CS_WAIT_RX_DMA
+            {
+                // S: Sw|D
+                self.arm_slave_receive(&mut cmd);
+                unsafe {
+                    self.regs().i2cs28().write(|w| w.bits(cmd));
+                }
+                return Some(SlaveEvent::DataReceived {
+                    len: self.slave_rx_len(),
+                });
+            } else if sts == constants::AST_I2CS_SLAVE_MATCH | constants::AST_I2CS_STOP {
+                // S: Sw|P
+                self.arm_slave_receive(&mut cmd);
+                unsafe {
+                    self.regs().i2cs28().write(|w| w.bits(cmd));
+                }
+                return Some(SlaveEvent::Stop);
+            } else if sts == constants::AST_I2CS_RX_DONE | constants::AST_I2CS_STOP
+                || sts == constants::AST_I2CS_RX_DONE | constants::AST_I2CS_WAIT_RX_DMA
+                || sts
+                    == constants::AST_I2CS_RX_DONE
+                        | constants::AST_I2CS_WAIT_RX_DMA
+                        | constants::AST_I2CS_STOP
+                || sts
+                    == constants::AST_I2CS_RX_DONE_NAK
+                        | constants::AST_I2CS_RX_DONE
+                        | constants::AST_I2CS_STOP
+                || sts
+                    == constants::AST_I2CS_SLAVE_MATCH
+                        | constants::AST_I2CS_RX_DONE
+                        | constants::AST_I2CS_STOP
+                || sts
+                    == constants::AST_I2CS_SLAVE_MATCH
+                        | constants::AST_I2CS_RX_DONE
+                        | constants::AST_I2CS_WAIT_RX_DMA
+                        | constants::AST_I2CS_STOP
+                || sts
+                    == constants::AST_I2CS_SLAVE_MATCH
+                        | constants::AST_I2CS_RX_DONE_NAK
+                        | constants::AST_I2CS_RX_DONE
+                        | constants::AST_I2CS_STOP
+            {
+                // S: (Sw)|D|(P)
+                return Some(SlaveEvent::DataReceived {
+                    len: self.slave_rx_len(),
+                });
+            } else if sts == constants::AST_I2CS_RX_DONE | constants::AST_I2CS_WAIT_TX_DMA
+                || sts
+                    == constants::AST_I2CS_SLAVE_MATCH
+                        | constants::AST_I2CS_RX_DONE
+                        | constants::AST_I2CS_WAIT_TX_DMA
+            {
+                // S: rx_done | wait_tx
+                return Some(SlaveEvent::DataReceivedAndSent {
+                    rx_len: self.slave_rx_len(),
+                    tx_len: usize::from(
+                        self.regs().i2cc0c().read().tx_data_byte_count().bits() + 1,
+                    ),
+                });
+            } else if sts == constants::AST_I2CS_SLAVE_MATCH | constants::AST_I2CS_WAIT_TX_DMA {
+                // S: Sw | wait_tx
+                return Some(SlaveEvent::DataSent {
+                    len: usize::from(self.regs().i2cc0c().read().tx_data_byte_count().bits() + 1),
+                });
+            } else if sts == constants::AST_I2CS_WAIT_TX_DMA {
+                // S: wait_tx
+                return Some(SlaveEvent::DataSent {
+                    len: usize::from(self.regs().i2cc0c().read().tx_data_byte_count().bits() + 1),
+                });
+            } else if sts == constants::AST_I2CS_TX_NAK | constants::AST_I2CS_STOP
+                || sts == constants::AST_I2CS_STOP
+            {
+                // S: (TX_NAK)|P
+                self.arm_slave_receive(&mut cmd);
+                unsafe {
+                    self.regs().i2cs28().write(|w| w.bits(cmd));
+                }
+            } else {
+                // TODO packet slave sts
+            }
+        } else {
+            //byte irq
+            let cmd: u32 = constants::AST_I2CS_ACTIVE_ALL;
+
+            if status
+                == constants::AST_I2CS_SLAVE_MATCH
+                    | constants::AST_I2CS_RX_DONE
+                    | constants::AST_I2CS_WAIT_RX_DMA
+            {
+                // S: Sw|D
+                let _byte_data = self.regs().i2cc08().read().rx_byte_buffer().bits();
+                self.regs().i2cs28().write(|w| unsafe { w.bits(cmd) });
+                self.regs().i2cs24().write(|w| unsafe { w.bits(status) });
+                self.regs().i2cs24().read().bits();
+                return Some(SlaveEvent::WriteRequest);
+            } else if status
+                == constants::AST_I2CS_SLAVE_MATCH
+                    | constants::AST_I2CS_RX_DONE
+                    | constants::AST_I2CS_WAIT_RX_DMA
+                    | constants::AST_I2CS_STOP
+                    | constants::AST_I2CS_TX_NAK
+                || status
+                    == constants::AST_I2CS_SLAVE_MATCH
+                        | constants::AST_I2CS_RX_DONE
+                        | constants::AST_I2CS_WAIT_RX_DMA
+                        | constants::AST_I2CS_STOP
+            {
+                // S: Sw|D|P
+                let _byte_data = self.regs().i2cc08().read().rx_byte_buffer().bits();
+                self.regs().i2cs28().write(|w| unsafe { w.bits(cmd) });
+                self.regs().i2cs24().write(|w| unsafe { w.bits(status) });
+                return Some(SlaveEvent::WriteRequest);
+            } else if status == constants::AST_I2CS_RX_DONE | constants::AST_I2CS_WAIT_RX_DMA {
+                // S: rD
+                return Some(SlaveEvent::DataReceived { len: 1 });
+            } else if status
+                == constants::AST_I2CS_SLAVE_MATCH
+                    | constants::AST_I2CS_RX_DONE
+                    | constants::AST_I2CS_WAIT_TX_DMA
+            {
+                // S: Sr|D
+                // received one byte
+                let _byte_data = self.regs().i2cc08().read().rx_byte_buffer().bits();
+                return Some(SlaveEvent::DataSent { len: 1 });
+            } else if status == constants::AST_I2CS_TX_ACK | constants::AST_I2CS_WAIT_TX_DMA {
+                // S: tD
+                return Some(SlaveEvent::DataSent { len: 1 });
+            } else if status == constants::AST_I2CS_STOP
+                || status == constants::AST_I2CS_STOP | constants::AST_I2CS_TX_NAK
+                || status
+                    == constants::AST_I2CS_SLAVE_MATCH
+                        | constants::AST_I2CS_STOP
+                        | constants::AST_I2CS_TX_NAK
+                || status
+                    == constants::AST_I2CS_SLAVE_MATCH
+                        | constants::AST_I2CS_WAIT_RX_DMA
+                        | constants::AST_I2CS_STOP
+                        | constants::AST_I2CS_TX_NAK
+            {
+                // S: P
+                self.regs().i2cs28().write(|w| unsafe { w.bits(cmd) });
+                self.regs().i2cs24().write(|w| unsafe { w.bits(status) });
+                return Some(SlaveEvent::Stop);
+            }
+            // TODO byte slave sts
+        }
+        None
+    }
+}
diff --git a/target/ast10x0/peripherals/i2c/target_adapter.rs b/target/ast10x0/peripherals/i2c/target_adapter.rs
new file mode 100644
index 0000000..14b438a
--- /dev/null
+++ b/target/ast10x0/peripherals/i2c/target_adapter.rs
@@ -0,0 +1,274 @@
+// Licensed under the Apache-2.0 license
+
+//! I2C Target (Slave) Adapter
+//!
+//! This module provides an adapter that bridges the low-level [`Ast1060I2c`]
+//! slave API with a user-supplied `TargetCallbacks` implementation. The
+//! adapter polls hardware events from the controller and dispatches them
+//! to the callback methods, keeping the hardware layer (slave.rs)
+//! decoupled from any particular target abstraction.
+//!
+//! # Example
+//!
+//! ```rust,ignore
+//! use ast10x0_peripherals::i2c::target_adapter::{TargetAdapter, TargetCallbacks};
+//!
+//! struct MyTarget { /* ... */ }
+//! impl TargetCallbacks for MyTarget { /* ... */ }
+//!
+//! let mut adapter = TargetAdapter::new(i2c_controller, MyTarget { /* ... */ });
+//! adapter.configure(0x42)?;
+//!
+//! // In an interrupt handler or poll loop:
+//! adapter.process_events()?;
+//! ```
+
+use super::controller::Ast1060I2c;
+use super::error::I2cError;
+use super::slave::{SlaveConfig, SlaveEvent};
+
+/// Internal hardware buffer for adapter operations
+const ADAPTER_BUFFER_SIZE: usize = 32;
+
+/// Target adapter that bridges hardware events to [`TargetCallbacks`].
+///
+/// Wraps an [`Ast1060I2c`] controller and a user-provided callback
+/// implementation, mapping hardware slave events to the appropriate
+/// callback methods.
+///
+/// # Type Parameters
+///
+/// * `'a` - Lifetime of the I2C register references
+/// * `T` - User's [`TargetCallbacks`] implementation
+/// * `Y` - Yield closure threaded through the underlying [`Ast1060I2c`]
+pub struct TargetAdapter<'a, T, Y: FnMut(u32)> {
+    /// The underlying hardware controller
+    i2c: Ast1060I2c<'a, Y>,
+    /// User's target implementation
+    target: T,
+    /// Internal receive buffer
+    rx_buffer: [u8; ADAPTER_BUFFER_SIZE],
+    /// Internal transmit buffer
+    tx_buffer: [u8; ADAPTER_BUFFER_SIZE],
+    /// Flag indicating we're in a transaction
+    in_transaction: bool,
+}
+
+impl<'a, T, Y: FnMut(u32)> TargetAdapter<'a, T, Y> {
+    /// Create a new target adapter.
+    ///
+    /// # Arguments
+    ///
+    /// * `i2c` - The I2C controller configured for the desired bus
+    /// * `target` - User's target implementation
+    ///
+    /// # Returns
+    ///
+    /// A new `TargetAdapter` instance ready to be configured.
+    pub fn new(i2c: Ast1060I2c<'a, Y>, target: T) -> Self {
+        Self {
+            i2c,
+            target,
+            rx_buffer: [0u8; ADAPTER_BUFFER_SIZE],
+            tx_buffer: [0u8; ADAPTER_BUFFER_SIZE],
+            in_transaction: false,
+        }
+    }
+
+    /// Get a reference to the underlying target implementation.
+    pub fn target(&self) -> &T {
+        &self.target
+    }
+
+    /// Get a mutable reference to the underlying target implementation.
+    pub fn target_mut(&mut self) -> &mut T {
+        &mut self.target
+    }
+
+    /// Get a reference to the underlying I2C controller.
+    pub fn controller(&self) -> &Ast1060I2c<'a, Y> {
+        &self.i2c
+    }
+
+    /// Get a mutable reference to the underlying I2C controller.
+    pub fn controller_mut(&mut self) -> &mut Ast1060I2c<'a, Y> {
+        &mut self.i2c
+    }
+
+    /// Decompose the adapter into its parts.
+    ///
+    /// Returns the I2C controller and target implementation.
+    pub fn into_parts(self) -> (Ast1060I2c<'a, Y>, T) {
+        (self.i2c, self.target)
+    }
+}
+
+impl<'a, T, Y: FnMut(u32)> TargetAdapter<'a, T, Y>
+where
+    T: TargetCallbacks,
+{
+    /// Configure the controller for target (slave) mode.
+    ///
+    /// # Arguments
+    ///
+    /// * `address` - The 7-bit I2C address to respond to
+    ///
+    /// # Returns
+    ///
+    /// `Ok(())` on success, or an error if configuration fails.
+    pub fn configure(&mut self, address: u8) -> Result<(), I2cError> {
+        let config = SlaveConfig::new(address)?;
+        self.i2c.configure_slave(&config)?;
+
+        // Initialize the target with the address
+        self.target
+            .on_init(address)
+            .map_err(|_| I2cError::SlaveError)?;
+
+        self.in_transaction = false;
+        Ok(())
+    }
+
+    /// Disable target mode.
+    pub fn disable(&mut self) {
+        self.i2c.disable_slave();
+        self.in_transaction = false;
+    }
+
+    /// Process pending hardware events.
+    ///
+    /// This method should be called from an interrupt handler or poll loop.
+    /// It reads hardware status, maps events to trait callbacks, and handles
+    /// data transfer between hardware and the target implementation.
+    ///
+    /// # Returns
+    ///
+    /// `Ok(Some(event))` if an event was processed, `Ok(None)` if no events pending,
+    /// or an error if processing failed.
+    pub fn process_events(&mut self) -> Result<Option<SlaveEvent>, I2cError> {
+        let Some(event) = self.i2c.handle_slave_interrupt() else {
+            return Ok(None);
+        };
+
+        match event {
+            SlaveEvent::AddressMatch => {
+                if !self.in_transaction {
+                    self.target.on_transaction_start(false);
+                    self.in_transaction = true;
+                }
+                self.target.on_address_match();
+            }
+
+            SlaveEvent::ReadRequest => {
+                // Master wants to read from us - get data from target
+                let len = self
+                    .target
+                    .on_read(&mut self.tx_buffer)
+                    .map_err(|_| I2cError::SlaveError)?;
+
+                // Send data to master
+                if len > 0 {
+                    let to_send = len.min(ADAPTER_BUFFER_SIZE);
+                    self.i2c.slave_write(&self.tx_buffer[..to_send])?;
+                }
+            }
+
+            SlaveEvent::WriteRequest => {
+                // Master wants to write to us - nothing to do here,
+                // data will arrive in DataReceived
+            }
+
+            SlaveEvent::DataReceived { len } => {
+                // Read data from hardware
+                let read_len = self.i2c.slave_read(&mut self.rx_buffer)?;
+                let actual_len = read_len.min(len);
+
+                if actual_len > 0 {
+                    // Pass data to target
+                    self.target
+                        .on_write(&self.rx_buffer[..actual_len])
+                        .map_err(|_| I2cError::SlaveError)?;
+                }
+            }
+
+            SlaveEvent::DataSent { len: _ } => {
+                // Data was sent successfully - target can track if needed
+                self.target.on_data_sent();
+            }
+
+            SlaveEvent::Stop => {
+                self.target.on_stop();
+                self.in_transaction = false;
+            }
+        }
+
+        Ok(Some(event))
+    }
+
+    /// Poll for and process all pending events.
+    ///
+    /// Continues processing until no more events are available.
+    ///
+    /// # Returns
+    ///
+    /// The count of events processed, or an error if processing failed.
+    pub fn process_all_events(&mut self) -> Result<usize, I2cError> {
+        let mut count: usize = 0;
+        while self.process_events()?.is_some() {
+            count = count.saturating_add(1);
+        }
+        Ok(count)
+    }
+}
+
+/// Target callback trait for adapter integration.
+///
+/// This trait defines the callbacks that the adapter invokes in response to
+/// hardware events from the I2C controller in slave mode. Implementors plug
+/// in their own logic for address-match, read, write, and stop events.
+pub trait TargetCallbacks {
+    /// Error type for callback operations
+    type Error;
+
+    /// Called when the target is initialized with an address.
+    ///
+    /// # Arguments
+    ///
+    /// * `address` - The 7-bit I2C address assigned to this target
+    fn on_init(&mut self, address: u8) -> Result<(), Self::Error>;
+
+    /// Called when a transaction starts.
+    ///
+    /// # Arguments
+    ///
+    /// * `repeated` - True if this is a repeated start condition
+    fn on_transaction_start(&mut self, repeated: bool);
+
+    /// Called when our address is matched.
+    fn on_address_match(&mut self);
+
+    /// Called when the master requests data (read operation).
+    ///
+    /// # Arguments
+    ///
+    /// * `buffer` - Buffer to fill with data to send to master
+    ///
+    /// # Returns
+    ///
+    /// Number of bytes written to buffer, or an error.
+    fn on_read(&mut self, buffer: &mut [u8]) -> Result<usize, Self::Error>;
+
+    /// Called when the master sends data (write operation).
+    ///
+    /// # Arguments
+    ///
+    /// * `data` - Data received from the master
+    fn on_write(&mut self, data: &[u8]) -> Result<(), Self::Error>;
+
+    /// Called when data has been sent to the master.
+    fn on_data_sent(&mut self) {}
+
+    /// Called when a stop condition is received.
+    fn on_stop(&mut self);
+}
+
diff --git a/target/ast10x0/peripherals/i2c/timing.rs b/target/ast10x0/peripherals/i2c/timing.rs
new file mode 100644
index 0000000..3e93030
--- /dev/null
+++ b/target/ast10x0/peripherals/i2c/timing.rs
@@ -0,0 +1,223 @@
+// Licensed under the Apache-2.0 license
+
+//! Timing configuration for I2C
+//!
+//! The AST1060 I2C controller uses a hierarchical clock system:
+//! 1. HPLL (1 `GHz`) → APB clock (typically 50 `MHz`)
+//! 2. APB clock → 4 base clocks (configured in I2CG10)
+//! 3. Base clock → SCL frequency via per-controller divider
+//!
+//! This module calculates the optimal divider chain to achieve
+//! the requested I2C bus speed while meeting timing specifications.
+//!
+//! # Design: Dependency Injection
+//!
+//! Clock frequencies are provided via `ClockConfig` in `I2cConfig`, rather than
+//! being read directly from SCU/I2C global registers. This provides:
+//! - **Decoupling**: I2C module has no hidden dependencies on SCU
+//! - **Testability**: Unit tests can inject known clock values
+//! - **Portability**: Different board configurations are easily supported
+//! - **Clear ownership**: System init owns clock detection, I2C just uses values
+//!
+//! # I2C Timing Requirements
+//!
+//! | Mode       | Speed    | tLOW (min) | tHIGH (min) | tBuf (min) |
+//! |------------|----------|------------|-------------|------------|
+//! | Standard   | 100 kHz  | 4.7 µs     | 4.0 µs      | 4.7 µs     |
+//! | Fast       | 400 kHz  | 1.3 µs     | 0.6 µs      | 1.3 µs     |
+//! | Fast-plus  | 1 `MHz`    | 0.5 µs     | 0.26 µs     | 0.5 µs     |
+//!
+//! # Clock Derivation
+//!
+//! The I2C global register I2CG10 configures four base clock dividers:
+//! ```text
+//! I2CG10[7:0]   = base_clk1 divisor → Fast-plus (1 MHz) source
+//! I2CG10[15:8]  = base_clk2 divisor → Fast (400 kHz) source
+//! I2CG10[23:16] = base_clk3 divisor → Standard (100 kHz) source
+//! I2CG10[31:24] = base_clk4 divisor → Recovery timeout source
+//! ```
+//!
+//! Default I2CG10 value: `0x6222_0803`
+//! With 50 `MHz` APB clock:
+//! - `base_clk1` (0x03): 50M / ((3+2)/2) = 20 `MHz` → 1 `MHz` with div=20
+//! - `base_clk2` (0x08): 50M / ((8+2)/2) = 10 `MHz` → 400 kHz with div=25
+//! - `base_clk3` (0x22): 50M / ((34+2)/2) = 2.77 `MHz` → 100 kHz with div=28
+
+use super::error::I2cError;
+use super::types::{ClockConfig, I2cConfig, I2cSpeed};
+use ast1060_pac::i2c::RegisterBlock;
+use core::cmp::min;
+
+/// Maximum divider ratio per base clock
+const MAX_DIVIDER_RATIO: u32 = 32;
+
+/// I2C speed frequencies in Hz
+const SPEED_STANDARD: u32 = 100_000;
+const SPEED_FAST: u32 = 400_000;
+const SPEED_FAST_PLUS: u32 = 1_000_000;
+
+/// Configure I2C timing based on speed and injected clock configuration
+///
+/// Uses the `ClockConfig` from `I2cConfig` to calculate proper SCL timing
+/// that meets I2C specifications. No direct hardware register access for
+/// clock detection - all clock values come from the injected config.
+///
+/// # Arguments
+///
+/// * `regs` - Reference to the I2C controller register block
+/// * `config` - I2C configuration containing speed, timeout, and clock settings
+///
+/// # Returns
+///
+/// * `Ok(())` on successful configuration
+/// * `Err(I2cError::Invalid)` if timing cannot be achieved
+pub fn configure_timing(regs: &RegisterBlock, config: &I2cConfig) -> Result<(), I2cError> {
+    let clocks = &config.clock_config;
+
+    // Step 1: Get target speed
+    let target_speed = match config.speed {
+        I2cSpeed::Standard => SPEED_STANDARD,
+        I2cSpeed::Fast => SPEED_FAST,
+        I2cSpeed::FastPlus => SPEED_FAST_PLUS,
+    };
+
+    // Step 2: Find best base clock and divider using injected clock config
+    let (div, divider_ratio) = calculate_divider(clocks, target_speed)?;
+
+    // Step 3: Calculate SCL high/low times
+    // I2C spec: tLOW should be slightly longer than tHIGH
+    // Default ratio: 9/16 low, 7/16 high (asymmetric for spec compliance)
+    let scl_low = calculate_scl_low(divider_ratio);
+    let scl_high = calculate_scl_high(divider_ratio, scl_low);
+    let scl_high_min = scl_high.saturating_sub(1);
+
+    // Step 4: Build timeout configuration if enabled
+    let (timeout_divisor, timeout_timer) = if config.smbus_timeout {
+        // SMBus timeout: 25-35ms per specification
+        // With base_clk_divisor=2, timer=8 gives approximately 35ms timeout
+        (2u8, 8u8)
+    } else {
+        (0u8, 0u8)
+    };
+
+    // Step 5: Write AC timing register with all fields in a single write
+    // This avoids corrupting fields with multiple separate writes
+    regs.i2cc04().write(|w| unsafe {
+        w.base_clk_divisor_tbase_clk()
+            .bits((div & 0xf) as u8)
+            .cycles_of_master_sclclklow_pulse_width_tcklow()
+            .bits(scl_low)
+            .cycles_of_master_sclclkhigh_pulse_width_tckhigh()
+            .bits(scl_high)
+            .cycles_of_master_sclclkhigh_minimum_pulse_width_tckhigh_min()
+            .bits(scl_high_min)
+            .timeout_base_clk_divisor_tout_base_clk()
+            .bits(timeout_divisor)
+            .timeout_timer()
+            .bits(timeout_timer)
+    });
+
+    Ok(())
+}
+
+/// Calculate optimal base clock selection and divider ratio
+///
+/// Tries each base clock in order (APB, clk1, clk2, clk3, clk4) and finds
+/// the first one that can achieve the target speed with a divider ≤ 32.
+///
+/// # Arguments
+///
+/// * `clocks` - Injected clock configuration
+/// * `target_speed` - Desired I2C bus speed in Hz
+///
+/// # Returns
+///
+/// * `(div, ratio)` - Base clock selector (0-4+) and divider ratio
+fn calculate_divider(clocks: &ClockConfig, target_speed: u32) -> Result<(u32, u32), I2cError> {
+    // Avoid division by zero
+    if target_speed == 0 {
+        return Err(I2cError::Invalid);
+    }
+
+    // Try each base clock in order, finding first that works with divider <= 32
+    let candidates = [
+        (0u32, clocks.apb_clock_hz),
+        (1u32, clocks.base_clk1_hz),
+        (2u32, clocks.base_clk2_hz),
+        (3u32, clocks.base_clk3_hz),
+    ];
+
+    for (div, base_clk) in candidates {
+        if base_clk == 0 {
+            continue; // Skip unconfigured clocks
+        }
+        if base_clk / target_speed <= MAX_DIVIDER_RATIO {
+            let mut ratio = base_clk / target_speed;
+            // Round up if we'd exceed target speed
+            if ratio > 0 && base_clk / ratio > target_speed {
+                ratio = ratio.saturating_add(1);
+            }
+            // Ensure ratio is at least 1
+            if ratio == 0 {
+                ratio = 1;
+            }
+            return Ok((div, ratio));
+        }
+    }
+
+    // Fall back to base_clk4 with extended divider for very slow speeds
+    if clocks.base_clk4_hz == 0 {
+        return Err(I2cError::Invalid);
+    }
+
+    let mut div = 4u32;
+    let mut ratio = clocks.base_clk4_hz / target_speed;
+    let mut inc = 0u32;
+
+    // Shift ratio down until it fits in MAX_DIVIDER_RATIO
+    while ratio.saturating_add(inc) > MAX_DIVIDER_RATIO {
+        inc |= ratio & 1; // Track if we need to round up
+        ratio >>= 1;
+        div = div.saturating_add(1);
+    }
+    ratio = ratio.saturating_add(inc);
+
+    // Round up if needed
+    if ratio > 0 && clocks.base_clk4_hz / ratio > target_speed {
+        ratio = ratio.saturating_add(1);
+    }
+
+    // Clamp to valid range
+    ratio = min(ratio, MAX_DIVIDER_RATIO);
+    if ratio == 0 {
+        ratio = 1;
+    }
+    div &= 0xf;
+
+    Ok((div, ratio))
+}
+
+/// Calculate SCL low time cycles
+///
+/// I2C specification requires tLOW to be longer than tHIGH.
+/// We use 9/16 of the period for low time (56.25%).
+fn calculate_scl_low(divider_ratio: u32) -> u8 {
+    // scl_low = (ratio * 9/16) - 1, clamped to 4 bits
+    let scl_low = (divider_ratio * 9 / 16).saturating_sub(1);
+    #[allow(clippy::cast_possible_truncation)]
+    let result = min(scl_low, 0xf) as u8;
+    result
+}
+
+/// Calculate SCL high time cycles
+///
+/// High time is the remainder after low time and 2 cycles for edges.
+fn calculate_scl_high(divider_ratio: u32, scl_low: u8) -> u8 {
+    // scl_high = ratio - scl_low - 2 (2 cycles for rise/fall edges)
+    let scl_high = divider_ratio
+        .saturating_sub(u32::from(scl_low))
+        .saturating_sub(2);
+    #[allow(clippy::cast_possible_truncation)]
+    let result = min(scl_high, 0xf) as u8;
+    result
+}
diff --git a/target/ast10x0/peripherals/i2c/transfer.rs b/target/ast10x0/peripherals/i2c/transfer.rs
new file mode 100644
index 0000000..5610034
--- /dev/null
+++ b/target/ast10x0/peripherals/i2c/transfer.rs
@@ -0,0 +1,190 @@
+// Licensed under the Apache-2.0 license
+
+//! Transfer mode implementation
+//!
+//! Note: The `start_transfer`, `start_byte_mode`, and `start_buffer_mode` functions
+//! are kept for potential future use or testing. The main byte/buffer mode logic
+//! is now in master.rs with inline command building for better control.
+
+use super::{constants, controller::Ast1060I2c, error::I2cError, types::I2cXferMode};
+
+#[allow(dead_code)]
+impl<Y: FnMut(u32)> Ast1060I2c<'_, Y> {
+    /// Start a transfer (common setup for byte/buffer modes)
+    ///
+    /// Note: Currently unused - byte/buffer mode functions in master.rs
+    /// handle command building directly for better control.
+    pub(crate) fn start_transfer(
+        &mut self,
+        addr: u8,
+        is_read: bool,
+        len: usize,
+    ) -> Result<(), I2cError> {
+        if len == 0 || len > 255 {
+            return Err(I2cError::Invalid);
+        }
+
+        self.current_addr = addr;
+        #[allow(clippy::cast_possible_truncation)]
+        {
+            self.current_len = len as u32;
+        }
+        self.current_xfer_cnt = 0;
+        self.completion = false;
+
+        // Clear any previous status
+        self.clear_interrupts(0xffff_ffff);
+
+        match self.xfer_mode {
+            I2cXferMode::ByteMode => {
+                self.start_byte_mode(addr, is_read, len);
+                Ok(())
+            }
+            I2cXferMode::BufferMode => self.start_buffer_mode(addr, is_read, len),
+            // DMA mode transfers are initiated directly in master.rs (write_dma_mode / read_dma_mode)
+            I2cXferMode::DmaMode => Err(super::error::I2cError::Invalid),
+        }
+    }
+
+    /// Start transfer in byte mode
+    ///
+    /// Byte mode uses packet mode with single-byte transfers.
+    /// Command register is i2cm18, data register is i2cc08.
+    fn start_byte_mode(&mut self, addr: u8, is_read: bool, len: usize) {
+        // Build command: packet mode + address + start
+        let mut cmd = constants::AST_I2CM_PKT_EN
+            | constants::ast_i2cm_pkt_addr(addr)
+            | constants::AST_I2CM_START_CMD;
+
+        if is_read {
+            cmd |= constants::AST_I2CM_RX_CMD;
+            // For last byte (or single byte), send NACK and STOP
+            if len == 1 {
+                cmd |= constants::AST_I2CM_RX_CMD_LAST | constants::AST_I2CM_STOP_CMD;
+            }
+        } else {
+            cmd |= constants::AST_I2CM_TX_CMD;
+            // For last byte (or single byte), send STOP
+            if len == 1 {
+                cmd |= constants::AST_I2CM_STOP_CMD;
+            }
+        }
+
+        // Issue command to i2cm18 (Master Command Register)
+        unsafe {
+            self.regs().i2cm18().write(|w| w.bits(cmd));
+        }
+    }
+
+    /// Start transfer in buffer mode (up to 32 bytes)
+    ///
+    /// Buffer mode uses packet mode with hardware buffer for multi-byte transfers.
+    /// All command bits go to i2cm18 in a single write.
+    fn start_buffer_mode(&mut self, addr: u8, is_read: bool, len: usize) -> Result<(), I2cError> {
+        if len > constants::BUFFER_MODE_SIZE {
+            return Err(I2cError::Invalid);
+        }
+
+        // Configure buffer size in i2cc0c before issuing command
+        #[allow(clippy::cast_possible_truncation)]
+        if is_read {
+            // Set RX buffer size (len - 1)
+            self.regs()
+                .i2cc0c()
+                .modify(|_, w| unsafe { w.rx_pool_buffer_size().bits((len - 1) as u8) });
+        } else {
+            // Set TX byte count (len - 1)
+            self.regs()
+                .i2cc0c()
+                .modify(|_, w| unsafe { w.tx_data_byte_count().bits((len - 1) as u8) });
+        }
+
+        // Build command: PKT_EN + address + START + TX/RX + BUFF_EN + STOP
+        let mut cmd = constants::AST_I2CM_PKT_EN
+            | constants::ast_i2cm_pkt_addr(addr)
+            | constants::AST_I2CM_START_CMD;
+
+        if is_read {
+            cmd |= constants::AST_I2CM_RX_CMD
+                | constants::AST_I2CM_RX_BUFF_EN
+                | constants::AST_I2CM_RX_CMD_LAST;
+        } else {
+            cmd |= constants::AST_I2CM_TX_CMD | constants::AST_I2CM_TX_BUFF_EN;
+        }
+
+        // Add stop for last chunk
+        cmd |= constants::AST_I2CM_STOP_CMD;
+
+        // Issue command to i2cm18 (Master Command Register) - single write
+        unsafe {
+            self.regs().i2cm18().write(|w| w.bits(cmd));
+        }
+
+        Ok(())
+    }
+
+    /// Copy data to hardware buffer (for writes)
+    pub(crate) fn copy_to_buffer(&mut self, data: &[u8]) -> Result<(), I2cError> {
+        if data.len() > constants::BUFFER_MODE_SIZE {
+            return Err(I2cError::Invalid);
+        }
+
+        let buff_regs = self.buff_regs();
+        let mut idx = 0;
+
+        while idx < data.len() {
+            // Pack bytes into DWORD (little-endian)
+            let mut dword: u32 = 0;
+            for byte_pos in 0..4 {
+                if idx + byte_pos < data.len() {
+                    dword |= u32::from(data[idx + byte_pos]) << (byte_pos * 8);
+                }
+            }
+
+            let dword_idx = idx / 4;
+            if dword_idx >= 8 {
+                return Err(I2cError::Invalid);
+            }
+
+            // Write to buffer register array (AST1060 has 8 DWORDs = 32 bytes)
+            unsafe {
+                buff_regs.buff(dword_idx).write(|w| w.bits(dword));
+            }
+
+            idx += 4;
+        }
+
+        Ok(())
+    }
+
+    /// Copy data from hardware buffer (for reads)
+    pub(crate) fn copy_from_buffer(&self, data: &mut [u8]) -> Result<(), I2cError> {
+        if data.len() > constants::BUFFER_MODE_SIZE {
+            return Err(I2cError::Invalid);
+        }
+
+        let buff_regs = self.buff_regs();
+        let mut idx = 0;
+
+        while idx < data.len() {
+            let dword_idx = idx / 4;
+            if dword_idx >= 8 {
+                return Err(I2cError::Invalid);
+            }
+
+            // Read from buffer register array
+            let dword = buff_regs.buff(dword_idx).read().bits();
+
+            // Extract bytes from DWORD (little-endian)
+            for byte_pos in 0..4 {
+                if idx + byte_pos < data.len() {
+                    data[idx + byte_pos] = ((dword >> (byte_pos * 8)) & 0xFF) as u8;
+                }
+            }
+
+            idx += 4;
+        }
+
+        Ok(())
+    }
+}
diff --git a/target/ast10x0/peripherals/i2c/types.rs b/target/ast10x0/peripherals/i2c/types.rs
new file mode 100644
index 0000000..4a7519b
--- /dev/null
+++ b/target/ast10x0/peripherals/i2c/types.rs
@@ -0,0 +1,266 @@
+// Licensed under the Apache-2.0 license
+
+//! Core types for AST1060 I2C driver
+//!
+//! These types are portable and have no OS dependencies.
+
+use ast1060_pac;
+
+/// I2C controller identifier (0-13 for AST1060)
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub struct Controller(pub u8);
+
+impl Controller {
+    /// Create a new controller instance
+    #[must_use]
+    pub const fn new(id: u8) -> Option<Self> {
+        if id < 14 {
+            Some(Self(id))
+        } else {
+            None
+        }
+    }
+
+    /// Get the controller ID
+    #[must_use]
+    pub const fn id(&self) -> u8 {
+        self.0
+    }
+}
+
+// Implement conversions for compatibility
+impl From<Controller> for u8 {
+    fn from(c: Controller) -> u8 {
+        c.0
+    }
+}
+
+// NOTE: We intentionally don't implement From<u8> for Controller
+// because it would bypass validation. Use Controller::new() or TryFrom instead.
+impl TryFrom<u8> for Controller {
+    type Error = ();
+
+    fn try_from(id: u8) -> Result<Self, Self::Error> {
+        Self::new(id).ok_or(())
+    }
+}
+
+// ============================================================================
+// Design Note: I2C Address Validation
+// ============================================================================
+//
+// embedded-hal defines `SevenBitAddress` as a type alias for `u8`:
+//   pub type SevenBitAddress = u8;
+//
+// This means I2C addresses are NOT validated at the type level in embedded-hal.
+// We follow this convention for compatibility, but validate at runtime in:
+// - SlaveConfig::new() - validates slave address is ≤ 0x7F
+// - Master operations - hardware will NAK invalid addresses
+//
+// Alternative approaches considered but rejected:
+// 1. Custom newtype - breaks embedded-hal I2c<SevenBitAddress> trait impl
+// 2. Const generics - too restrictive, addresses often come from runtime config
+//
+// The tradeoff is: embedded-hal compatibility > compile-time address validation
+
+/// Clock configuration for I2C timing calculations
+///
+/// This struct decouples the I2C driver from direct SCU/I2C global register access,
+/// enabling:
+/// - Unit testing without hardware
+/// - Portability across different board configurations
+/// - Clear ownership boundaries between system initialization and peripheral drivers
+///
+/// # Usage
+///
+/// For typical AST1060 configurations, use `ClockConfig::ast1060_default()`.
+/// For custom configurations or testing, construct directly.
+///
+/// # Example
+///
+/// ```rust,no_run
+/// use aspeed_ddk::i2c_core::ClockConfig;
+///
+/// // Use default AST1060 configuration (50 MHz APB)
+/// let clocks = ClockConfig::ast1060_default();
+///
+/// // Or read from hardware during system init
+/// let clocks = ClockConfig::from_hardware();
+/// ```
+#[derive(Debug, Clone, Copy)]
+pub struct ClockConfig {
+    /// APB bus clock frequency in Hz
+    pub apb_clock_hz: u32,
+    /// Base clock 1 frequency in Hz (for Fast-plus mode, ~20 `MHz`)
+    pub base_clk1_hz: u32,
+    /// Base clock 2 frequency in Hz (for Fast mode, ~10 `MHz`)
+    pub base_clk2_hz: u32,
+    /// Base clock 3 frequency in Hz (for Standard mode, ~2.77 `MHz`)
+    pub base_clk3_hz: u32,
+    /// Base clock 4 frequency in Hz (for recovery timeout)
+    pub base_clk4_hz: u32,
+}
+
+impl ClockConfig {
+    /// HPLL frequency (1 `GHz`) used for APB clock derivation
+    const HPLL_FREQ: u32 = 1_000_000_000;
+
+    /// Default AST1060 clock configuration
+    ///
+    /// Based on typical AST1060 configuration with:
+    /// - APB clock: 50 `MHz` (HPLL / 20)
+    /// - I2CG10 register: `0x6222_0803`
+    ///
+    /// This can be used when the actual hardware configuration matches
+    /// these defaults, or for testing purposes.
+    #[must_use]
+    pub const fn ast1060_default() -> Self {
+        // APB = 50 MHz (HPLL / ((9+1) * 2))
+        // I2CG10 = 0x6222_0803:
+        //   base_clk1 divisor = 0x03 → 50M / ((3+2)/2) = 20 MHz
+        //   base_clk2 divisor = 0x08 → 50M / ((8+2)/2) = 10 MHz
+        //   base_clk3 divisor = 0x22 → 50M / ((34+2)/2) = 2.77 MHz
+        //   base_clk4 divisor = 0x62 → 50M / ((98+2)/2) = 1 MHz
+        Self {
+            apb_clock_hz: 50_000_000,
+            base_clk1_hz: 20_000_000,
+            base_clk2_hz: 10_000_000,
+            base_clk3_hz: 2_770_000,
+            base_clk4_hz: 1_000_000,
+        }
+    }
+
+    /// Read clock configuration from hardware registers
+    ///
+    /// This reads the actual APB clock divider from SCU and the I2C base
+    /// clock dividers from I2C global registers.
+    ///
+    /// # Safety
+    ///
+    /// This function accesses SCU and I2C global registers. It should be
+    /// called during system initialization when these peripherals are
+    /// properly configured.
+    #[must_use]
+    pub fn from_hardware() -> Self {
+        // Read APB clock from SCU
+        let scu = unsafe { &*ast1060_pac::Scu::ptr() };
+        let apb_divider = u32::from(scu.scu310().read().apbbus_pclkdivider_sel().bits());
+        let apb_clock_hz = Self::HPLL_FREQ / ((apb_divider + 1) * 2);
+
+        // Read base clock dividers from I2C global registers
+        let i2cg = unsafe { &*ast1060_pac::I2cglobal::ptr() };
+        let i2cg10 = i2cg.i2cg10().read();
+
+        // Formula: base_clk = APB * 10 / ((divisor + 2) * 10 / 2)
+        let calc_base = |divisor: u8| -> u32 {
+            let divisor_u32 = u32::from(divisor);
+            (apb_clock_hz * 10) / ((divisor_u32 + 2) * 10 / 2)
+        };
+
+        Self {
+            apb_clock_hz,
+            base_clk1_hz: calc_base(i2cg10.base_clk1divisor_basedivider1().bits()),
+            base_clk2_hz: calc_base(i2cg10.base_clk2divisor_basedivider2().bits()),
+            base_clk3_hz: calc_base(i2cg10.base_clk3divisor_basedivider3().bits()),
+            base_clk4_hz: calc_base(i2cg10.base_clk4divisor_basedivider4().bits()),
+        }
+    }
+}
+
+impl Default for ClockConfig {
+    fn default() -> Self {
+        Self::ast1060_default()
+    }
+}
+
+/// I2C configuration
+///
+/// Default configuration is optimized for MCTP-over-I2C:
+/// - Fast mode (400 kHz) - standard MCTP speed
+/// - Buffer mode - efficient for MCTP packet transfers
+/// - `SMBus` timeout enabled - required for robust MCTP operation
+/// - Clock config read from hardware - assumes app pre-configured I2CG10
+#[derive(Debug, Clone, Copy)]
+pub struct I2cConfig {
+    /// Transfer mode (byte-by-byte or buffer)
+    pub xfer_mode: I2cXferMode,
+    /// Bus speed (Standard/Fast/FastPlus)
+    pub speed: I2cSpeed,
+    /// Enable multi-master support
+    pub multi_master: bool,
+    /// Enable `SMBus` timeout detection (25-35ms per `SMBus` spec)
+    pub smbus_timeout: bool,
+    /// Enable `SMBus` alert interrupt
+    pub smbus_alert: bool,
+    /// Clock configuration for timing calculations
+    pub clock_config: ClockConfig,
+}
+
+impl Default for I2cConfig {
+    /// Create default I2C configuration reading clocks from hardware
+    ///
+    /// The application must pre-configure I2C global clocks (I2CG10)
+    /// before creating an I2C driver instance. This default reads
+    /// the actual clock values from hardware registers.
+    ///
+    /// For DMA mode, set `xfer_mode` to [`I2cXferMode::DmaMode`] and
+    /// pass a DMA buffer via
+    /// [`Ast1060I2c::new_with_dma`](super::controller::Ast1060I2c::new_with_dma).
+    fn default() -> Self {
+        Self {
+            xfer_mode: I2cXferMode::BufferMode,
+            speed: I2cSpeed::Fast,
+            multi_master: false,
+            smbus_timeout: true,
+            smbus_alert: false,
+            clock_config: ClockConfig::from_hardware(),
+        }
+    }
+}
+
+impl I2cConfig {
+    /// Create configuration with static default clocks (for testing)
+    ///
+    /// Uses hardcoded AST1060 default clock values. Prefer `default()`
+    /// which reads actual values from hardware.
+    #[must_use]
+    pub fn with_static_clocks() -> Self {
+        Self {
+            clock_config: ClockConfig::ast1060_default(),
+            ..Self {
+                xfer_mode: I2cXferMode::BufferMode,
+                speed: I2cSpeed::Fast,
+                multi_master: false,
+                smbus_timeout: true,
+                smbus_alert: false,
+                clock_config: ClockConfig::ast1060_default(),
+            }
+        }
+    }
+}
+
+/// Transfer mode selection
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum I2cXferMode {
+    /// Byte-by-byte mode (1 byte at a time)
+    ByteMode,
+    /// Buffer mode (up to 32 bytes via hardware buffer)
+    BufferMode,
+    /// DMA mode (up to 4096 bytes via system memory DMA)
+    ///
+    /// Requires a pre-allocated, cache-coherent DMA buffer passed to
+    /// [`Ast1060I2c::new_with_dma`]. The buffer must be placed in a
+    /// non-cached memory region (e.g. `#[link_section = ".ram_nc"]`).
+    DmaMode,
+}
+
+/// I2C bus speed
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum I2cSpeed {
+    /// Standard mode: 100 kHz
+    Standard,
+    /// Fast mode: 400 kHz
+    Fast,
+    /// Fast-plus mode: 1 `MHz`
+    FastPlus,
+}
diff --git a/target/ast10x0/peripherals/lib.rs b/target/ast10x0/peripherals/lib.rs
index b9084e9..141dead 100644
--- a/target/ast10x0/peripherals/lib.rs
+++ b/target/ast10x0/peripherals/lib.rs
@@ -3,5 +3,6 @@
 
 #![no_std]
 
+pub mod i2c;
 pub mod scu;
 pub mod uart;