expand harness for master/slave, add i2c test
diff --git a/target/ast10x0/defs.bzl b/target/ast10x0/defs.bzl index 7b70d6f..dd10172 100644 --- a/target/ast10x0/defs.bzl +++ b/target/ast10x0/defs.bzl
@@ -2,7 +2,50 @@ # SPDX-License-Identifier: Apache-2.0 """Common definitions used by all ast10x0 targets.""" +load("@pigweed//pw_kernel/tooling:system_image.bzl", "SystemImageInfo") + TARGET_COMPATIBLE_WITH = select({ "//target/ast10x0:target_ast10x0": [], "//conditions:default": ["@platforms//:incompatible"], }) + +def _system_image_test_impl(ctx): + master_elf = ctx.attr.image[SystemImageInfo].elf + executable_symlink = ctx.actions.declare_file(ctx.label.name) + ctx.actions.symlink(output = executable_symlink, target_file = master_elf) + + runfiles = ctx.attr.image[DefaultInfo].default_runfiles + + if ctx.attr.slave_image: + slave_elf = ctx.attr.slave_image[SystemImageInfo].elf + slave_symlink = ctx.actions.declare_file(ctx.label.name + ".slave.elf") + ctx.actions.symlink(output = slave_symlink, target_file = slave_elf) + runfiles = ctx.runfiles(files = [slave_symlink]).merge( + runfiles.merge(ctx.attr.slave_image[DefaultInfo].default_runfiles), + ) + + return [DefaultInfo( + executable = executable_symlink, + runfiles = runfiles, + )] + +system_image_test = rule( + implementation = _system_image_test_impl, + test = True, + attrs = { + "image": attr.label( + doc = "The system_image target to test.", + mandatory = True, + providers = [SystemImageInfo], + executable = True, + cfg = "target", + ), + "slave_image": attr.label( + doc = "Optional slave system_image for paired two-device tests.", + mandatory = False, + default = None, + providers = [SystemImageInfo], + cfg = "target", + ), + }, +)
diff --git a/target/ast10x0/harness/evb_config.toml b/target/ast10x0/harness/evb_config.toml index 249e6d9..c169c60 100644 --- a/target/ast10x0/harness/evb_config.toml +++ b/target/ast10x0/harness/evb_config.toml
@@ -8,3 +8,8 @@ [uart] serial_port = "/dev/ttyUSB0" baudrate = 115200 + +[device_b] +srst_pin = 25 +fwspick_pin = 24 +serial_port = "/dev/ttyUSB1"
diff --git a/target/ast10x0/harness/pi_test_runner.py b/target/ast10x0/harness/pi_test_runner.py index 51d8d22..bd655cf 100644 --- a/target/ast10x0/harness/pi_test_runner.py +++ b/target/ast10x0/harness/pi_test_runner.py
@@ -13,7 +13,9 @@ import argparse import subprocess import sys +import threading import time +from contextlib import nullcontext from pathlib import Path try: @@ -75,11 +77,13 @@ print(f"Uploaded {size} bytes ({padding} bytes padding)", file=sys.stderr) +_stdout_lock = threading.Lock() + _SUCCESS_SENTINEL = b"TEST_RESULT:PASS" _FAILURE_SENTINELS = [b"TEST_RESULT:FAIL", b"panic"] -def _stream_uart(port: serial.Serial, timeout: int) -> bool: +def _stream_uart(port: serial.Serial, timeout: int, lock=None) -> bool: port.timeout = 1.0 deadline = time.time() + timeout if timeout else None buf = b"" @@ -90,8 +94,9 @@ data = port.read(1024) if data: try: - sys.stdout.buffer.write(data) - sys.stdout.buffer.flush() + with (lock or nullcontext()): + sys.stdout.buffer.write(data) + sys.stdout.buffer.flush() except (BrokenPipeError, OSError): return False buf += data @@ -103,6 +108,63 @@ buf = buf[-256:] +def _run_paired(args, firmware_path: Path, slave_firmware_path: Path) -> bool: + try: + port_b = serial.Serial( + args.slave_uart_device, + baudrate=args.baudrate, + timeout=1.0, + write_timeout=1.0, + ) + except serial.SerialException as e: + print(f"Error: could not open {args.slave_uart_device}: {e}", file=sys.stderr) + return False + + try: + port_a = serial.Serial( + args.uart_device, + baudrate=args.baudrate, + timeout=1.0, + write_timeout=1.0, + ) + except serial.SerialException as e: + port_b.close() + print(f"Error: could not open {args.uart_device}: {e}", file=sys.stderr) + return False + + try: + _sequence_to_fwspick_mode(args.slave_srst_pin, args.slave_fwspick_pin, port_b) + if not _wait_for_uart_ready(port_b): + return False + _upload_firmware(port_b, slave_firmware_path) + + _sequence_to_fwspick_mode(args.srst_pin, args.fwspick_pin, port_a) + if not _wait_for_uart_ready(port_a): + return False + _upload_firmware(port_a, firmware_path) + + results = [None, None] + + def _monitor(idx, port): + results[idx] = _stream_uart(port, args.timeout, _stdout_lock) + + threads = [ + threading.Thread(target=_monitor, args=(0, port_a)), + threading.Thread(target=_monitor, args=(1, port_b)), + ] + for t in threads: + t.start() + for t in threads: + t.join() + + return bool(results[0] and results[1]) + except KeyboardInterrupt: + return False + finally: + port_a.close() + port_b.close() + + def main() -> int: parser = argparse.ArgumentParser( description="AST1060 EVB hardware layer: GPIO, firmware upload, UART stream" @@ -145,6 +207,28 @@ action="store_true", help="Skip GPIO sequences and firmware upload; stream raw UART bytes only", ) + parser.add_argument( + "--slave-firmware", + default=None, + help="Slave firmware binary. When present, enables paired two-device mode.", + ) + parser.add_argument( + "--slave-uart-device", + default=None, + help="Serial port for device B (e.g. /dev/ttyUSB1)", + ) + parser.add_argument( + "--slave-srst-pin", + type=int, + default=None, + help="BCM GPIO pin connected to device B SRST", + ) + parser.add_argument( + "--slave-fwspick-pin", + type=int, + default=None, + help="BCM GPIO pin connected to device B FWSPICK", + ) args = parser.parse_args() if not args.stream_only: @@ -157,6 +241,24 @@ else: firmware_path = None + if args.slave_firmware: + missing = [ + name + for name, val in [ + ("--slave-uart-device", args.slave_uart_device), + ("--slave-srst-pin", args.slave_srst_pin), + ("--slave-fwspick-pin", args.slave_fwspick_pin), + ] + if val is None + ] + if missing: + parser.error(f"paired mode requires: {', '.join(missing)}") + slave_firmware_path = Path(args.slave_firmware) + if not slave_firmware_path.exists(): + print(f"Error: slave firmware not found: {slave_firmware_path}", file=sys.stderr) + return 1 + return 0 if _run_paired(args, firmware_path, slave_firmware_path) else 1 + try: port = serial.Serial( args.uart_device,
diff --git a/target/ast10x0/harness/test_runner.py b/target/ast10x0/harness/test_runner.py index 55c0e2d..e04b432 100644 --- a/target/ast10x0/harness/test_runner.py +++ b/target/ast10x0/harness/test_runner.py
@@ -127,13 +127,12 @@ class UartMonitor: """Detokenizes and displays raw UART bytes. Pass/fail is determined by pi_test_runner.py exit code.""" - def __init__(self, args: argparse.Namespace, elf_path: Path) -> None: - # elf_path is always derived on the host from firmware path; caller - # validates existence before constructing this object. + def __init__(self, args: argparse.Namespace, *elf_paths: Path) -> None: self.args = args self.log_file_handle = open(args.log_file, "w") if args.log_file else None + elf_args = [str(p) for p in elf_paths if p is not None] self.detokenizer = ( - Detokenizer(str(elf_path)) if _PW_TOKENIZER_AVAILABLE else None + Detokenizer(*elf_args) if _PW_TOKENIZER_AVAILABLE and elf_args else None ) self._token_parser = NestedMessageParser() if _PW_TOKENIZER_AVAILABLE else None @@ -257,6 +256,15 @@ else: remote_fw = None + remote_slave_fw = None + if args.slave_firmware: + slave_firmware_path = Path(args.slave_firmware) + remote_slave_fw = f"{remote_dir}/{slave_firmware_path.name}" + subprocess.run( + ["scp", "-q", str(slave_firmware_path), f"{host}:{remote_slave_fw}"], + check=True, + ) + remote_cmd = f"python3 -u {remote_dir}/pi_test_runner.py {uart_device}" if not args.parse_only: remote_cmd += f" {remote_fw}" @@ -268,6 +276,14 @@ ) if args.parse_only: remote_cmd += " --stream-only" + if remote_slave_fw: + device_b = config["device_b"] + remote_cmd += ( + f" --slave-firmware {remote_slave_fw}" + f" --slave-uart-device {device_b['serial_port']}" + f" --slave-srst-pin {device_b['srst_pin']}" + f" --slave-fwspick-pin {device_b['fwspick_pin']}" + ) proc = _ssh_stream(host, remote_cmd) try: @@ -348,8 +364,21 @@ # When invoked via --run_under on a system_image_test, Bazel passes the # no-suffix symlink (e.g. threads_test → threads.elf); resolve it first. image = Path(args.firmware) + + # When invoked via --run_under the firmware path has no suffix (it is the + # test-name symlink). The system_image_test rule places a companion + # <name>.slave.elf symlink alongside it when slave_image is set; detecting + # that file is how we enter paired mode without any extra CLI arguments. + args.slave_firmware = None + slave_elf_path = None if not image.suffix: + slave_symlink = image.parent / (image.name + ".slave.elf") + if slave_symlink.exists(): + slave_elf = slave_symlink.resolve() + slave_elf_path = slave_elf + args.slave_firmware = str(slave_elf.with_suffix(".bin")) image = image.resolve() + elf_path = image.with_suffix(".elf") args.firmware = str(image.with_suffix(".bin")) if not elf_path.exists(): @@ -359,7 +388,7 @@ args.pi_host = os.environ.get(AST1060_EVB_PI_HOST) or args.pi_host runner = Path(__file__).parent / "pi_test_runner.py" - monitor = UartMonitor(args, elf_path) + monitor = UartMonitor(args, elf_path, slave_elf_path) signal.signal(signal.SIGINT, lambda s, f: sys.exit(130))
diff --git a/target/ast10x0/peripherals/i2c/slave.rs b/target/ast10x0/peripherals/i2c/slave.rs index 729c825..6391910 100644 --- a/target/ast10x0/peripherals/i2c/slave.rs +++ b/target/ast10x0/peripherals/i2c/slave.rs
@@ -127,11 +127,14 @@ if self.xfer_mode == I2cXferMode::DmaMode { self.regs().i2cs4c().read().dmarx_actual_len_byte().bits() as usize } else { + // Hardware includes the I2C address byte in the buffer count (packet mode, + // I2CC00 bit 20). Subtract 1 to report only the payload byte count. self.regs() .i2cc0c() .read() .actual_rxd_pool_buffer_size() - .bits() as usize + .bits() + .saturating_sub(1) as usize } } @@ -561,12 +564,17 @@ }); } else if sts == constants::AST_I2CS_TX_NAK | constants::AST_I2CS_STOP || sts == constants::AST_I2CS_STOP + || sts + == constants::AST_I2CS_SLAVE_MATCH + | constants::AST_I2CS_TX_NAK + | constants::AST_I2CS_STOP { - // S: (TX_NAK)|P + // S: (Sr) (TX_NAK)|P — master read completed with NAK then STOP self.arm_slave_receive(&mut cmd); unsafe { self.regs().i2cs28().write(|w| w.bits(cmd)); } + return Some(SlaveEvent::Stop); } else { // TODO packet slave sts }
diff --git a/target/ast10x0/peripherals/scu/pinctrl.rs b/target/ast10x0/peripherals/scu/pinctrl.rs index bd92529..3f4cccd 100644 --- a/target/ast10x0/peripherals/scu/pinctrl.rs +++ b/target/ast10x0/peripherals/scu/pinctrl.rs
@@ -537,9 +537,21 @@ gen_pin_pairs!(SCU6B0, 0x6B0, 31); } -/// I2C1 pin group: SCL/SDA mux selection on SCU414[30:31]. +/// I2C1 pin group: SCL2/SDA2 mux selection on SCU414[30:31]. +/// +/// The SVD names these EnblSCL2FnPin/EnblSDA2FnPin, but they correspond to +/// PAC peripheral I2c1 (controller 1, base 0x7e7b_0100). The SVD uses +/// 1-based naming where "2" means "second bus", matching PAC I2c1. pub const PINCTRL_I2C1: &[PinctrlPin] = &[PIN_SCU414_30, PIN_SCU414_31]; +/// I2C2 pin group: SCL3/SDA3 mux selection on SCU418[0:1]. +/// +/// The SVD names these EnblSCL3FnPin/EnblSDA3FnPin, corresponding to +/// PAC peripheral I2c2 (controller 2, base 0x7e7b_0180). On the Test +/// Harness board, these pins are exposed on J15 and used for inter-device +/// I2C communication between the two AST1060 daughter cards. +pub const PINCTRL_I2C2: &[PinctrlPin] = &[PIN_SCU418_0, PIN_SCU418_1]; + /// Macro to safely modify a register bit (set or clear). macro_rules! modify_reg { ($reg:expr, $bit:expr, $clear:expr) => {{
diff --git a/target/ast10x0/tests/peripherals/i2c/i2c_irq/BUILD.bazel b/target/ast10x0/tests/peripherals/i2c/i2c_irq/BUILD.bazel new file mode 100644 index 0000000..21b2dbf --- /dev/null +++ b/target/ast10x0/tests/peripherals/i2c/i2c_irq/BUILD.bazel
@@ -0,0 +1,132 @@ +# Licensed under the Apache-2.0 license +# SPDX-License-Identifier: Apache-2.0 + +load("@pigweed//pw_kernel/tooling:system_image.bzl", "system_image") +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_test") + +COMMON_DEPS = [ + "//target/ast10x0:config", + "//target/ast10x0:entry", + "//target/ast10x0/board:ast10x0_board", + "//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", +] + +# --------------------------------------------------------------------------- +# Master image (device A) +# --------------------------------------------------------------------------- + +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"] + COMMON_DEPS, +) + +system_image( + name = "master", + kernel = ":target", + platform = "//target/ast10x0", + system_config = ":system_config", + tags = ["kernel"], + userspace = False, +) + +system_image_test( + name = "irq_test", + image = ":master", + slave_image = ":slave", + tags = ["hardware"], + target_compatible_with = select({ + "//target/ast10x0:qemu_enabled": ["@platforms//:incompatible"], + "//conditions:default": [], + }), +) + +rust_binary_no_panics_test( + name = "no_panics_test", + binary = ":master", + tags = ["kernel"], +) + +# --------------------------------------------------------------------------- +# Slave image (device B) +# --------------------------------------------------------------------------- + +filegroup( + name = "slave_system_config", + srcs = ["slave_system.json5"], +) + +target_codegen( + name = "slave_codegen", + arch = "@pigweed//pw_kernel/arch/arm_cortex_m:arch_arm_cortex_m", + system_config = ":slave_system_config", + target_compatible_with = TARGET_COMPATIBLE_WITH, +) + +target_linker_script( + name = "slave_linker_script", + system_config = ":slave_system_config", + tags = ["kernel"], + target_compatible_with = TARGET_COMPATIBLE_WITH, + template = "//target/ast10x0:linker_script_template", +) + +rust_binary( + name = "slave_target", + srcs = ["slave_target.rs"], + aliases = {":slave_codegen": "codegen"}, + edition = "2024", + tags = ["kernel"], + target_compatible_with = TARGET_COMPATIBLE_WITH, + deps = [":slave_codegen", ":slave_linker_script"] + COMMON_DEPS, +) + +system_image( + name = "slave", + kernel = ":slave_target", + platform = "//target/ast10x0", + system_config = ":slave_system_config", + tags = ["kernel"], + userspace = False, +) + +rust_binary_no_panics_test( + name = "slave_no_panics_test", + binary = ":slave", + tags = ["kernel"], +)
diff --git a/target/ast10x0/tests/peripherals/i2c/i2c_irq/slave_system.json5 b/target/ast10x0/tests/peripherals/i2c/i2c_irq/slave_system.json5 new file mode 100644 index 0000000..3a30e0f --- /dev/null +++ b/target/ast10x0/tests/peripherals/i2c/i2c_irq/slave_system.json5
@@ -0,0 +1,17 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +// I2C Slave IRQ Test — slave image (device B). Same layout as master. +{ + arch: { + type: "armv7m", + vector_table_start_address: 0x00000000, + vector_table_size_bytes: 1280, + }, + kernel: { + flash_start_address: 0x00000500, + flash_size_bytes: 262144, + ram_start_address: 0x00040500, + ram_size_bytes: 393216, + }, +}
diff --git a/target/ast10x0/tests/peripherals/i2c/i2c_irq/slave_target.rs b/target/ast10x0/tests/peripherals/i2c/i2c_irq/slave_target.rs new file mode 100644 index 0000000..085f1af --- /dev/null +++ b/target/ast10x0/tests/peripherals/i2c/i2c_irq/slave_target.rs
@@ -0,0 +1,159 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! I2C slave interrupt test — slave side (device B) +//! +//! Runs on the AST1060 Test Harness board with J15 pins 1 and 2 connected. +//! This binary is the SLAVE. Load it on device B before loading the master +//! image on device A. +//! +//! The slave listens at address 0x42, handles three transactions initiated +//! by the master, verifies the expected interrupt events, then reports PASS. + +#![no_std] +#![no_main] + +use ast10x0_board::{Ast10x0Board, Ast10x0BoardDescriptor}; +use ast10x0_peripherals::i2c::{ + Ast1060I2c, ClockConfig, I2cConfig, I2cSpeed, I2cXferMode, SlaveConfig, SlaveEvent, +}; +use ast10x0_peripherals::scu::pinctrl; +use codegen as _; +use console_backend::console_backend_write_all; +use entry as _; +use target_common::{TargetInterface, declare_target}; + +pub struct Target {} + +const SLAVE_ADDR: u8 = 0x42; +const EXPECTED_WRITE: &[u8] = &[0xAA, 0xBB, 0xCC, 0xDD]; +const READ_RESPONSE: &[u8] = &[0x55]; + +fn i2c2_config() -> I2cConfig { + I2cConfig { + xfer_mode: I2cXferMode::BufferMode, + speed: I2cSpeed::Fast, + multi_master: false, + smbus_timeout: true, + smbus_alert: false, + clock_config: ClockConfig::ast1060_default(), + } +} + +/// Poll handle_slave_interrupt until an event arrives or the budget runs out. +fn wait_event<Y: FnMut(u32)>( + slave: &mut Ast1060I2c<'_, Y>, + max_polls: u32, +) -> Option<SlaveEvent> { + for _ in 0..max_polls { + if let Some(ev) = slave.handle_slave_interrupt() { + return Some(ev); + } + core::hint::spin_loop(); + } + None +} + +fn run_slave() -> Result<(), &'static str> { + pw_log::info!("=== I2C slave IRQ test: SLAVE (device B) ==="); + pw_log::info!("Listening at addr 0x{:02x}. Start master (device A) now.", SLAVE_ADDR as u32); + + let board = Ast10x0Board::new(Ast10x0BoardDescriptor { + pinctrl_groups: &[pinctrl::PINCTRL_I2C2], + }); + // SAFETY: single call at boot with exclusive access to SCU/I2C global regs. + unsafe { board.init() }; + + // SAFETY: I2C2 registers accessed only through `slave` for this test. + let mut slave = unsafe { + Ast1060I2c::new( + ast1060_pac::I2c2::ptr(), + ast1060_pac::I2cbuff2::ptr(), + &i2c2_config(), + |_| core::hint::spin_loop(), + ) + } + .map_err(|_| "slave I2C2 init failed")?; + + let slave_cfg = SlaveConfig::new(SLAVE_ADDR).map_err(|_| "SlaveConfig::new failed")?; + + // ------------------------------------------------------------------ + // Test 1: receive master write → expect DataReceived + // ------------------------------------------------------------------ + slave + .configure_slave(&slave_cfg) + .map_err(|_| "test 1: configure_slave failed")?; + + match wait_event(&mut slave, 50_000_000) { + Some(SlaveEvent::DataReceived { len }) => { + if len != EXPECTED_WRITE.len() { + pw_log::error!("test 1: DataReceived len={} expected={}", len as u32, EXPECTED_WRITE.len() as u32); + return Err("test 1: DataReceived len mismatch"); + } + pw_log::info!("Test 1 passed: DataReceived len={}", len as u32); + } + Some(_) => return Err("test 1: unexpected slave event"), + None => return Err("test 1: timed out waiting for DataReceived"), + } + + // ------------------------------------------------------------------ + // Test 2: respond to master read → pre-arm TX, expect DataSent + // ------------------------------------------------------------------ + slave + .configure_slave(&slave_cfg) + .map_err(|_| "test 2: configure_slave failed")?; + + slave + .slave_write(READ_RESPONSE) + .map_err(|_| "test 2: slave_write failed")?; + + match wait_event(&mut slave, 50_000_000) { + Some(SlaveEvent::DataSent { len }) => { + pw_log::info!("Test 2 passed: DataSent len={}", len as u32); + } + Some(SlaveEvent::Stop) => { + pw_log::info!("Test 2 passed: Stop (short read path)"); + } + Some(_) => return Err("test 2: unexpected slave event"), + None => return Err("test 2: timed out waiting for DataSent"), + } + + // ------------------------------------------------------------------ + // Test 3: single-byte write from master → DataReceived then Stop + // ------------------------------------------------------------------ + slave + .configure_slave(&slave_cfg) + .map_err(|_| "test 3: configure_slave failed")?; + + match wait_event(&mut slave, 50_000_000) { + Some(SlaveEvent::DataReceived { len: _ }) + | Some(SlaveEvent::Stop) + | Some(SlaveEvent::WriteRequest) => { + pw_log::info!("Test 3 passed: data/stop/write-req observed"); + } + Some(_) => return Err("test 3: unexpected slave event"), + None => return Err("test 3: timed out waiting for event"), + } + + pw_log::info!("=== Slave tests complete ==="); + Ok(()) +} + +impl TargetInterface for Target { + const NAME: &'static str = "AST10x0 I2C Slave IRQ Slave"; + + fn main() -> ! { + let sentinel: &[u8] = match run_slave() { + Ok(()) => b"TEST_RESULT:PASS\n", + Err(e) => { + pw_log::error!("Slave test failed: {}", e as &str); + b"TEST_RESULT:FAIL\n" + } + }; + let _ = console_backend_write_all(sentinel); + #[expect(clippy::empty_loop)] + loop {} + } +} + +declare_target!(Target);
diff --git a/target/ast10x0/tests/peripherals/i2c/i2c_irq/system.json5 b/target/ast10x0/tests/peripherals/i2c/i2c_irq/system.json5 new file mode 100644 index 0000000..c11b564 --- /dev/null +++ b/target/ast10x0/tests/peripherals/i2c/i2c_irq/system.json5
@@ -0,0 +1,19 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +// AST10x0 I2C Slave IRQ Test Configuration +// Single kernel binary; no userspace apps. +// Memory layout mirrors i2c_init to keep the linker happy. +{ + 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/i2c_irq/target.rs b/target/ast10x0/tests/peripherals/i2c/i2c_irq/target.rs new file mode 100644 index 0000000..3dc8758 --- /dev/null +++ b/target/ast10x0/tests/peripherals/i2c/i2c_irq/target.rs
@@ -0,0 +1,121 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! I2C slave interrupt test — master side (device A) +//! +//! Runs on the AST1060 Test Harness board with J15 pins 1 and 2 connected, +//! which links I2C2 (PAC I2c2, SCU418[0:1]) between device A and device B. +//! +//! This binary is the MASTER. Load the companion slave binary +//! (i2c_slave_irq_slave image) on device B first, then load this image on +//! device A. Device B must be running and listening before device A starts +//! transmitting. +//! +//! Tests exercised: +//! 1. Master write → slave DataReceived interrupt +//! 2. Master read → slave DataSent interrupt (slave pre-arms TX) +//! 3. Zero-length write → slave Stop event + +#![no_std] +#![no_main] + +use ast10x0_board::{Ast10x0Board, Ast10x0BoardDescriptor}; +use ast10x0_peripherals::i2c::{Ast1060I2c, ClockConfig, I2cConfig, I2cSpeed, I2cXferMode}; +use ast10x0_peripherals::scu::pinctrl; +use codegen as _; +use console_backend::console_backend_write_all; +use entry as _; +use target_common::{TargetInterface, declare_target}; + +pub struct Target {} + +const SLAVE_ADDR: u8 = 0x42; +const WRITE_PAYLOAD: &[u8] = &[0xAA, 0xBB, 0xCC, 0xDD]; + +fn i2c2_config() -> I2cConfig { + I2cConfig { + xfer_mode: I2cXferMode::BufferMode, + speed: I2cSpeed::Fast, + multi_master: false, + smbus_timeout: true, + smbus_alert: false, + clock_config: ClockConfig::ast1060_default(), + } +} + +fn run_master() -> Result<(), &'static str> { + pw_log::info!("=== I2C slave IRQ test: MASTER (device A) ==="); + pw_log::info!("J15 must be connected. Load slave image on device B first."); + + let board = Ast10x0Board::new(Ast10x0BoardDescriptor { + pinctrl_groups: &[pinctrl::PINCTRL_I2C2], + }); + // SAFETY: single call at boot with exclusive access to SCU/I2C global regs. + unsafe { board.init() }; + + // SAFETY: I2C2 registers accessed only through `master` for this test. + let mut master = unsafe { + Ast1060I2c::new( + ast1060_pac::I2c2::ptr(), + ast1060_pac::I2cbuff2::ptr(), + &i2c2_config(), + |_| core::hint::spin_loop(), + ) + } + .map_err(|_| "master I2C2 init failed")?; + + // ------------------------------------------------------------------ + // Test 1: master write → slave DataReceived + // ------------------------------------------------------------------ + pw_log::info!("Test 1: master write"); + master + .write(SLAVE_ADDR, WRITE_PAYLOAD) + .map_err(|_| "test 1: master write failed (slave not responding — check J15 and slave firmware)")?; + pw_log::info!("Test 1 passed"); + + // ------------------------------------------------------------------ + // Test 2: master read → slave DataSent + // The slave pre-arms its TX buffer before this read arrives. + // ------------------------------------------------------------------ + pw_log::info!("Test 2: master read"); + let mut rx = [0u8; 1]; + master + .read(SLAVE_ADDR, &mut rx) + .map_err(|_| "test 2: master read failed")?; + if rx[0] != 0x55 { + pw_log::error!("test 2: got 0x{:02x}, expected 0x55", rx[0] as u32); + return Err("test 2: rx data mismatch"); + } + pw_log::info!("Test 2 passed: rx=0x{:02x}", rx[0] as u32); + + // ------------------------------------------------------------------ + // Test 3: single-byte write → slave Stop event after packet done + // ------------------------------------------------------------------ + pw_log::info!("Test 3: single-byte write (triggers slave Stop)"); + master + .write(SLAVE_ADDR, &[0xFF]) + .map_err(|_| "test 3: write failed")?; + pw_log::info!("Test 3 passed"); + + pw_log::info!("=== Master tests complete ==="); + Ok(()) +} + +impl TargetInterface for Target { + const NAME: &'static str = "AST10x0 I2C Slave IRQ Master"; + + fn main() -> ! { + let sentinel: &[u8] = match run_master() { + Ok(()) => b"TEST_RESULT:PASS\n", + Err(e) => { + pw_log::error!("Master test failed: {}", e as &str); + b"TEST_RESULT:FAIL\n" + } + }; + let _ = console_backend_write_all(sentinel); + #[expect(clippy::empty_loop)] + loop {} + } +} + +declare_target!(Target);