Add ast10x0 i2c peripheral integration tests
diff --git a/target/ast10x0/tests/peripherals/i2c/BUILD.bazel b/target/ast10x0/tests/peripherals/i2c/BUILD.bazel
new file mode 100644
index 0000000..37f4531
--- /dev/null
+++ b/target/ast10x0/tests/peripherals/i2c/BUILD.bazel
@@ -0,0 +1,77 @@
+# Licensed under the Apache-2.0 license
+# SPDX-License-Identifier: Apache-2.0
+
+load("@pigweed//pw_kernel/tooling:system_image.bzl", "system_image", "system_image_test")
+load("@pigweed//pw_kernel/tooling:target_codegen.bzl", "target_codegen")
+load("@pigweed//pw_kernel/tooling:target_linker_script.bzl", "target_linker_script")
+load("@pigweed//pw_kernel/tooling/panic_detector:rust_binary_no_panics_test.bzl", "rust_binary_no_panics_test")
+load("@rules_rust//rust:defs.bzl", "rust_binary")
+load("//target/ast10x0:defs.bzl", "TARGET_COMPATIBLE_WITH")
+
+system_image(
+    name = "i2c",
+    kernel = ":target",
+    platform = "//target/ast10x0",
+    system_config = ":system_config",
+    tags = ["kernel"],
+    userspace = False,
+)
+
+system_image_test(
+    name = "i2c_integration_test",
+    image = ":i2c",
+    target_compatible_with = select({
+        "//target/ast10x0:qemu_enabled": ["@platforms//:incompatible"],
+        "//conditions:default": [],
+    }),
+)
+
+rust_binary_no_panics_test(
+    name = "no_panics_test",
+    binary = ":i2c",
+    tags = ["kernel"],
+)
+
+filegroup(
+    name = "system_config",
+    srcs = ["system.json5"],
+)
+
+target_codegen(
+    name = "codegen",
+    arch = "@pigweed//pw_kernel/arch/arm_cortex_m:arch_arm_cortex_m",
+    system_config = ":system_config",
+    target_compatible_with = TARGET_COMPATIBLE_WITH,
+)
+
+target_linker_script(
+    name = "linker_script",
+    system_config = ":system_config",
+    tags = ["kernel"],
+    target_compatible_with = TARGET_COMPATIBLE_WITH,
+    template = "//target/ast10x0:linker_script_template",
+)
+
+rust_binary(
+    name = "target",
+    srcs = ["target.rs"],
+    edition = "2024",
+    tags = ["kernel"],
+    target_compatible_with = TARGET_COMPATIBLE_WITH,
+    deps = [
+        ":codegen",
+        ":linker_script",
+        "//target/ast10x0:config",
+        "//target/ast10x0/board:ast10x0_board",
+        "//target/ast10x0:entry",
+        "//target/ast10x0/peripherals",
+        "@ast1060_pac",
+        "@pigweed//pw_kernel/arch/arm_cortex_m:arch_arm_cortex_m",
+        "@pigweed//pw_kernel/kernel",
+        "@pigweed//pw_kernel/subsys/console:console_backend",
+        "@pigweed//pw_kernel/target:target_common",
+        "@pigweed//pw_log/rust:pw_log",
+        "@pigweed//pw_status/rust:pw_status",
+        "@rust_crates//:cortex-m-semihosting",
+    ],
+)
diff --git a/target/ast10x0/tests/peripherals/i2c/system.json5 b/target/ast10x0/tests/peripherals/i2c/system.json5
new file mode 100644
index 0000000..91aaeba
--- /dev/null
+++ b/target/ast10x0/tests/peripherals/i2c/system.json5
@@ -0,0 +1,18 @@
+// Licensed under the Apache-2.0 license
+// SPDX-License-Identifier: Apache-2.0
+
+// AST10x0 Kernel I2C Test Configuration
+// Uses the same memory layout as the kernel-only tests.
+{
+    arch: {
+        type: "armv7m",
+        vector_table_start_address: 0x00000000,
+        vector_table_size_bytes: 1280,  // 0x500 (320 vectors)
+    },
+    kernel: {
+        flash_start_address: 0x00000500,  // After vector table
+        flash_size_bytes: 262144,         // 256KB for kernel code (in RAM)
+        ram_start_address: 0x00040500,    // RAM starts after code
+        ram_size_bytes: 393216,           // 384KB for data
+    },
+}
diff --git a/target/ast10x0/tests/peripherals/i2c/target.rs b/target/ast10x0/tests/peripherals/i2c/target.rs
new file mode 100644
index 0000000..518ca92
--- /dev/null
+++ b/target/ast10x0/tests/peripherals/i2c/target.rs
@@ -0,0 +1,469 @@
+// Licensed under the Apache-2.0 license
+// SPDX-License-Identifier: Apache-2.0
+
+#![no_std]
+#![no_main]
+
+use ast10x0_board::{Ast10x0Board, Ast10x0BoardDescriptor};
+use ast10x0_peripherals::i2c::{
+    Ast1060I2c, ClockConfig, I2cConfig, I2cError, I2cSpeed, I2cXferMode,
+};
+use ast10x0_peripherals::scu::pinctrl;
+use codegen as _;
+use console_backend as _;
+use cortex_m_semihosting::debug::{EXIT_FAILURE, EXIT_SUCCESS, exit};
+use entry as _;
+use target_common::{TargetInterface, declare_target};
+
+pub struct Target {}
+
+const ERR_INIT_FAILED: &str = "i2c controller init failed";
+const ERR_VERIFY_FAILED: &str = "i2c init register verification failed";
+
+struct ExpectedTiming {
+    base_clk_div: u8,
+    scl_low: u8,
+    scl_high: u8,
+    scl_high_min: u8,
+}
+
+fn i2c_error_str(error: I2cError) -> &'static str {
+    match error {
+        I2cError::Overrun => "Overrun",
+        I2cError::NoAcknowledge => "NoAcknowledge",
+        I2cError::Timeout => "Timeout",
+        I2cError::BusRecoveryFailed => "BusRecoveryFailed",
+        I2cError::Bus => "Bus",
+        I2cError::Busy => "Busy",
+        I2cError::Invalid => "Invalid",
+        I2cError::Abnormal => "Abnormal",
+        I2cError::ArbitrationLoss => "ArbitrationLoss",
+        I2cError::SlaveError => "SlaveError",
+        I2cError::InvalidAddress => "InvalidAddress",
+    }
+}
+
+fn expected_timing(speed: I2cSpeed) -> ExpectedTiming {
+    match speed {
+        // ClockConfig::ast1060_default() => base_clk3=2.77MHz, divider ratio=28
+        I2cSpeed::Standard => ExpectedTiming {
+            base_clk_div: 3,
+            scl_low: 14,
+            scl_high: 12,
+            scl_high_min: 11,
+        },
+        // base_clk2=10MHz, divider ratio=25
+        I2cSpeed::Fast => ExpectedTiming {
+            base_clk_div: 2,
+            scl_low: 13,
+            scl_high: 10,
+            scl_high_min: 9,
+        },
+        // base_clk1=20MHz, divider ratio=20
+        I2cSpeed::FastPlus => ExpectedTiming {
+            base_clk_div: 1,
+            scl_low: 10,
+            scl_high: 8,
+            scl_high_min: 7,
+        },
+    }
+}
+
+fn dump_i2c1_registers(name: &str, config: &I2cConfig) {
+    // SAFETY: The test has exclusive ownership of I2C1 during execution.
+    let regs = unsafe { &*ast1060_pac::I2c1::ptr() };
+    let c00 = regs.i2cc00().read();
+    let c04 = regs.i2cc04().read();
+    let m10 = regs.i2cm10().read();
+    let expected = expected_timing(config.speed);
+    let expected_timeout_div: u32 = if config.smbus_timeout { 2 } else { 0 };
+    let expected_timeout_timer: u32 = if config.smbus_timeout { 8 } else { 0 };
+
+    pw_log::error!("--- {} mode I2C1 register dump ---", name as &str);
+    pw_log::error!("i2cc00(raw)=0x{:08x}", c00.bits() as u32);
+    pw_log::error!("i2cc04(raw)=0x{:08x}", c04.bits() as u32);
+    pw_log::error!("i2cm10(raw)=0x{:08x}", m10.bits() as u32);
+
+    pw_log::error!(
+        "decoded: master_en={} auto_release={} dis_multimaster={} (expected={})",
+        c00.enbl_master_fn().bit() as u8,
+        c00.enbl_bus_autorelease_when_scllow_sdalow_or_slave_mode_inactive_timeout()
+            .bit() as u8,
+        c00.dis_multimaster_capability_for_master_fn_only().bit() as u8,
+        (!config.multi_master) as u8
+    );
+
+    pw_log::error!(
+        "decoded: base_clk_div={} (expected={}) scl_low={} (expected={})",
+        c04.base_clk_divisor_tbase_clk().bits() as u32,
+        expected.base_clk_div as u32,
+        c04.cycles_of_master_sclclklow_pulse_width_tcklow().bits() as u32,
+        expected.scl_low as u32
+    );
+
+    pw_log::error!(
+        "decoded: scl_high={} (expected={}) scl_high_min={} (expected={})",
+        c04.cycles_of_master_sclclkhigh_pulse_width_tckhigh().bits() as u32,
+        expected.scl_high as u32,
+        c04.cycles_of_master_sclclkhigh_minimum_pulse_width_tckhigh_min()
+            .bits() as u32,
+        expected.scl_high_min as u32
+    );
+
+    pw_log::error!(
+        "decoded: timeout_div={} (expected={}) timeout_timer={} (expected={})",
+        c04.timeout_base_clk_divisor_tout_base_clk().bits() as u32,
+        expected_timeout_div as u32,
+        c04.timeout_timer().bits() as u32,
+        expected_timeout_timer as u32
+    );
+
+    pw_log::error!(
+        "decoded: pkt_done_int={} bus_recover_int={} smbus_alert_int={} (expected={})",
+        m10.enbl_pkt_cmd_done_int().bit() as u8,
+        m10.enbl_bus_recover_done_int().bit() as u8,
+        m10.enbl_smbus_dev_alert_int().bit() as u8,
+        config.smbus_alert as u8
+    );
+}
+
+fn verify_init_registers(name: &str, config: &I2cConfig) -> Result<(), &'static str> {
+    // SAFETY: The test has exclusive ownership of I2C1 during execution.
+    let regs = unsafe { &*ast1060_pac::I2c1::ptr() };
+
+    let c00 = regs.i2cc00().read();
+    let c04 = regs.i2cc04().read();
+    let m10 = regs.i2cm10().read();
+    let expected = expected_timing(config.speed);
+
+    let mut ok = true;
+
+    let master_en = c00.enbl_master_fn().bit();
+    if !master_en {
+        pw_log::error!("{} verify: enbl_master_fn not set", name as &str);
+        ok = false;
+    }
+
+    let auto_release = c00
+        .enbl_bus_autorelease_when_scllow_sdalow_or_slave_mode_inactive_timeout()
+        .bit();
+    if !auto_release {
+        pw_log::error!("{} verify: bus auto-release not set", name as &str);
+        ok = false;
+    }
+
+    let dis_multimaster = c00.dis_multimaster_capability_for_master_fn_only().bit();
+    let expected_dis_multimaster = !config.multi_master;
+    if dis_multimaster != expected_dis_multimaster {
+        pw_log::error!(
+            "{} verify: dis_multimaster mismatch actual={} expected={}",
+            name as &str,
+            dis_multimaster as u8,
+            expected_dis_multimaster as u8
+        );
+        ok = false;
+    }
+
+    let base_clk_div = c04.base_clk_divisor_tbase_clk().bits();
+    if base_clk_div != expected.base_clk_div {
+        pw_log::error!(
+            "{} verify: base_clk_div mismatch actual={} expected={}",
+            name as &str,
+            base_clk_div as u32,
+            expected.base_clk_div as u32
+        );
+        ok = false;
+    }
+
+    let scl_low = c04.cycles_of_master_sclclklow_pulse_width_tcklow().bits();
+    if scl_low != expected.scl_low {
+        pw_log::error!(
+            "{} verify: scl_low mismatch actual={} expected={}",
+            name as &str,
+            scl_low as u32,
+            expected.scl_low as u32
+        );
+        ok = false;
+    }
+
+    let scl_high = c04.cycles_of_master_sclclkhigh_pulse_width_tckhigh().bits();
+    if scl_high != expected.scl_high {
+        pw_log::error!(
+            "{} verify: scl_high mismatch actual={} expected={}",
+            name as &str,
+            scl_high as u32,
+            expected.scl_high as u32
+        );
+        ok = false;
+    }
+
+    let scl_high_min = c04
+        .cycles_of_master_sclclkhigh_minimum_pulse_width_tckhigh_min()
+        .bits();
+    if scl_high_min != expected.scl_high_min {
+        pw_log::error!(
+            "{} verify: scl_high_min mismatch actual={} expected={}",
+            name as &str,
+            scl_high_min as u32,
+            expected.scl_high_min as u32
+        );
+        ok = false;
+    }
+
+    let timeout_div = c04.timeout_base_clk_divisor_tout_base_clk().bits();
+    let expected_timeout_div = if config.smbus_timeout { 2 } else { 0 };
+    if timeout_div != expected_timeout_div {
+        pw_log::error!(
+            "{} verify: timeout_div mismatch actual={} expected={}",
+            name as &str,
+            timeout_div as u32,
+            expected_timeout_div as u32
+        );
+        ok = false;
+    }
+
+    let timeout_timer = c04.timeout_timer().bits();
+    let expected_timeout_timer = if config.smbus_timeout { 8 } else { 0 };
+    if timeout_timer != expected_timeout_timer {
+        pw_log::error!(
+            "{} verify: timeout_timer mismatch actual={} expected={}",
+            name as &str,
+            timeout_timer as u32,
+            expected_timeout_timer as u32
+        );
+        ok = false;
+    }
+
+    let pkt_done_int = m10.enbl_pkt_cmd_done_int().bit();
+    if !pkt_done_int {
+        pw_log::error!("{} verify: pkt_cmd_done_int not enabled", name as &str);
+        ok = false;
+    }
+
+    let bus_recover_int = m10.enbl_bus_recover_done_int().bit();
+    if !bus_recover_int {
+        pw_log::error!("{} verify: bus_recover_done_int not enabled", name as &str);
+        ok = false;
+    }
+
+    let smbus_alert_int = m10.enbl_smbus_dev_alert_int().bit();
+    if smbus_alert_int != config.smbus_alert {
+        pw_log::error!(
+            "{} verify: smbus_alert_int mismatch actual={} expected={}",
+            name as &str,
+            smbus_alert_int as u8,
+            config.smbus_alert as u8
+        );
+        ok = false;
+    }
+
+    if ok {
+        Ok(())
+    } else {
+        pw_log::error!("--- {} mode I2C1 register dump ---", name as &str);
+        pw_log::error!("i2cc00(raw)=0x{:08x}", c00.bits() as u32);
+        pw_log::error!("i2cc04(raw)=0x{:08x}", c04.bits() as u32);
+        pw_log::error!("i2cm10(raw)=0x{:08x}", m10.bits() as u32);
+
+        pw_log::error!(
+            "decoded: master_en={} auto_release={} dis_multimaster={} (expected={})",
+            c00.enbl_master_fn().bit() as u8,
+            c00.enbl_bus_autorelease_when_scllow_sdalow_or_slave_mode_inactive_timeout()
+                .bit() as u8,
+            c00.dis_multimaster_capability_for_master_fn_only().bit() as u8,
+            (!config.multi_master) as u8
+        );
+
+        pw_log::error!(
+            "decoded: base_clk_div={} (expected={}) scl_low={} (expected={})",
+            c04.base_clk_divisor_tbase_clk().bits() as u32,
+            expected.base_clk_div as u32,
+            c04.cycles_of_master_sclclklow_pulse_width_tcklow().bits() as u32,
+            expected.scl_low as u32
+        );
+
+        pw_log::error!(
+            "decoded: scl_high={} (expected={}) scl_high_min={} (expected={})",
+            c04.cycles_of_master_sclclkhigh_pulse_width_tckhigh().bits() as u32,
+            expected.scl_high as u32,
+            c04.cycles_of_master_sclclkhigh_minimum_pulse_width_tckhigh_min()
+                .bits() as u32,
+            expected.scl_high_min as u32
+        );
+
+        pw_log::error!(
+            "decoded: timeout_div={} (expected={}) timeout_timer={} (expected={})",
+            c04.timeout_base_clk_divisor_tout_base_clk().bits() as u32,
+            expected_timeout_div as u32,
+            c04.timeout_timer().bits() as u32,
+            expected_timeout_timer as u32
+        );
+
+        pw_log::error!(
+            "decoded: pkt_done_int={} bus_recover_int={} smbus_alert_int={} (expected={})",
+            m10.enbl_pkt_cmd_done_int().bit() as u8,
+            m10.enbl_bus_recover_done_int().bit() as u8,
+            m10.enbl_smbus_dev_alert_int().bit() as u8,
+            config.smbus_alert as u8
+        );
+        Err(ERR_VERIFY_FAILED)
+    }
+}
+
+fn run_i2c_init_smoke_test() -> Result<(), &'static str> {
+    pw_log::info!("=== AST10x0 I2C init smoke test ===");
+
+    let board = Ast10x0Board::new(Ast10x0BoardDescriptor {
+        pinctrl_groups: &[pinctrl::PINCTRL_I2C1],
+    });
+
+    // SAFETY: Test target runs once at boot with exclusive access to the board.
+    unsafe { board.init() };
+    pw_log::info!("Board-level I2C global init complete");
+
+    run_init_case(
+        "standard",
+        I2cConfig {
+            speed: I2cSpeed::Standard,
+            xfer_mode: I2cXferMode::BufferMode,
+            multi_master: true,
+            smbus_timeout: true,
+            smbus_alert: false,
+            clock_config: ClockConfig::ast1060_default(),
+        },
+    )?;
+    run_init_case(
+        "fast",
+        I2cConfig {
+            speed: I2cSpeed::Fast,
+            xfer_mode: I2cXferMode::BufferMode,
+            multi_master: true,
+            smbus_timeout: true,
+            smbus_alert: false,
+            clock_config: ClockConfig::ast1060_default(),
+        },
+    )?;
+    run_init_case(
+        "fast-plus",
+        I2cConfig {
+            speed: I2cSpeed::FastPlus,
+            xfer_mode: I2cXferMode::BufferMode,
+            multi_master: false,
+            smbus_timeout: true,
+            smbus_alert: false,
+            clock_config: ClockConfig::ast1060_default(),
+        },
+    )?;
+    run_init_case_dma(
+        "dma-fast",
+        I2cConfig {
+            speed: I2cSpeed::Fast,
+            xfer_mode: I2cXferMode::DmaMode,
+            multi_master: true,
+            smbus_timeout: true,
+            smbus_alert: false,
+            clock_config: ClockConfig::ast1060_default(),
+        },
+    )?;
+
+    pw_log::info!("=== AST10x0 I2C init smoke test complete ===");
+    Ok(())
+}
+
+fn run_init_case(name: &str, config: I2cConfig) -> Result<(), &'static str> {
+    pw_log::info!("Instantiating controller 1 in {} mode", name as &str);
+
+    // SAFETY: The test owns the controller for the process lifetime and uses
+    // the matching I2C/I2CBUFF register pair for controller 1.
+    let result = unsafe {
+        Ast1060I2c::new(
+            ast1060_pac::I2c1::ptr(),
+            ast1060_pac::I2cbuff1::ptr(),
+            &config,
+            |_| core::hint::spin_loop(),
+        )
+    };
+
+    match result {
+        Ok(_i2c) => {
+            verify_init_registers(name, &config)?;
+            pw_log::info!("{} mode init+verify passed", name as &str);
+            Ok(())
+        }
+        Err(error) => {
+            let error_name = i2c_error_str(error);
+            pw_log::error!(
+                "{} mode init failed: {}",
+                name as &str,
+                error_name as &str
+            );
+            dump_i2c1_registers(name, &config);
+            Err(ERR_INIT_FAILED)
+        }
+    }
+}
+
+fn run_init_case_dma(name: &str, config: I2cConfig) -> Result<(), &'static str> {
+    pw_log::info!(
+        "Instantiating controller 1 in {} mode (new_with_dma)",
+        name as &str
+    );
+
+    let mut dma_buf = [0u8; 64];
+
+    // SAFETY: The test owns the controller for the process lifetime and uses
+    // the matching I2C/I2CBUFF register pair for controller 1.
+    let result = unsafe {
+        Ast1060I2c::new_with_dma(
+            ast1060_pac::I2c1::ptr(),
+            ast1060_pac::I2cbuff1::ptr(),
+            &config,
+            &mut dma_buf,
+            |_| core::hint::spin_loop(),
+        )
+    };
+
+    match result {
+        Ok(_i2c) => {
+            verify_init_registers(name, &config)?;
+            pw_log::info!("{} mode init+verify passed", name as &str);
+            Ok(())
+        }
+        Err(error) => {
+            let error_name = i2c_error_str(error);
+            pw_log::error!(
+                "{} mode init failed: {}",
+                name as &str,
+                error_name as &str
+            );
+            dump_i2c1_registers(name, &config);
+            Err(ERR_INIT_FAILED)
+        }
+    }
+}
+
+impl TargetInterface for Target {
+    const NAME: &'static str = "AST10x0 Kernel I2C";
+
+    fn main() -> ! {
+        let exit_status = match run_i2c_init_smoke_test() {
+            Ok(()) => EXIT_SUCCESS,
+            Err(error) => {
+                pw_log::error!("I2C init smoke test failed: {}", error as &str);
+                EXIT_FAILURE
+            }
+        };
+
+        exit(exit_status);
+        #[expect(clippy::empty_loop)]
+        loop {}
+    }
+
+    fn shutdown(code: u32) -> ! {
+        pw_log::info!("Shutting down with code {}", code as u32);
+        #[expect(clippy::empty_loop)]
+        loop {}
+    }
+}
+
+declare_target!(Target);