diff --git a/MODULE.bazel b/MODULE.bazel index 12cf77598..8a60dd105 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -33,6 +33,10 @@ git_override( # Fix syscall_defs/syscall_user being gated on userspace_build_enabled, # which breaks userspace=False kernel builds (pigweed regression). "//third_party/pigweed:syscall_no_userspace_constraint.patch", + # Program the VeeR external-interrupt redirect table (MEIVT) in + # veer_pic early_init; without it the first external interrupt + # vectors through an unprogrammed table to address 0. + "//third_party/pigweed:veer_pic_meivt.patch", ], remote = "https://pigweed.googlesource.com/pigweed/pigweed", ) diff --git a/target/veer/config.rs b/target/veer/config.rs index b9268f7c6..52e099457 100644 --- a/target/veer/config.rs +++ b/target/veer/config.rs @@ -55,6 +55,10 @@ pub struct VeerPicConfig; impl VeerPicConfigInterface for VeerPicConfig { const PIC_BASE_ADDRESS: usize = PIC_BASE; const MAX_IRQS: u32 = 256; + // The VeeR core requires the external-interrupt redirect table to live + // in DCCM; this image does not otherwise use DCCM. 256 IRQs * 4 bytes + // fits well within the 16KiB DCCM. + const MEIVT_BASE_ADDRESS: Option = Some(0x5000_0000); } pub struct TimerConfig; diff --git a/target/veer/peripherals/i3c/BUILD.bazel b/target/veer/peripherals/i3c/BUILD.bazel new file mode 100644 index 000000000..ce2db063b --- /dev/null +++ b/target/veer/peripherals/i3c/BUILD.bazel @@ -0,0 +1,19 @@ +# Licensed under the Apache-2.0 license +# SPDX-License-Identifier: Apache-2.0 + +load("@rules_rust//rust:defs.bzl", "rust_library") +load("//target/veer:defs.bzl", "TARGET_COMPATIBLE_WITH") + +package(default_visibility = ["//visibility:public"]) + +rust_library( + name = "i3c", + srcs = ["lib.rs"], + crate_name = "caliptra_i3c_target", + edition = "2024", + target_compatible_with = TARGET_COMPATIBLE_WITH, + deps = [ + "//target/veer/registers", + "//third_party/caliptra:crate_tock-registers", + ], +) diff --git a/target/veer/peripherals/i3c/lib.rs b/target/veer/peripherals/i3c/lib.rs new file mode 100644 index 000000000..41d2b5d75 --- /dev/null +++ b/target/veer/peripherals/i3c/lib.rs @@ -0,0 +1,183 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! Caliptra-SS I3C target peripheral driver. +//! +//! Drives the i3c-core HCI TTI (Target Transaction Interface) on behalf of +//! firmware running on the VeeR RISC-V core. The i3c-core is always a +//! *target* (secondary) from the VeeR perspective — a BMC controller on the +//! I3C bus initiates all transfers. +//! +//! The Caliptra ROM initializes the I3C core before handing off to application +//! firmware. This driver assumes the ROM has already run and only manages the +//! data path (interrupt enable/disable, RX drain, TX queue, IBI). +//! +//! # TTI data path +//! +//! - **Incoming write** (controller → target): hardware pushes a descriptor +//! into `tti_rx_desc_queue_port` then `data_length` words into +//! `tti_rx_data_port`. Poll `rx_pending()` or enable the RX interrupt. +//! - **Outgoing read** (target → controller): firmware writes a descriptor +//! to `tti_tx_desc_queue_port` then `data_length` words to +//! `tti_tx_data_port`. +//! - **IBI**: write the IBI descriptor (MDB + payload length) to +//! `tti_tti_ibi_port` then the payload words; hardware raises the IBI on +//! the bus. + +#![no_std] + +use caliptra_ss_registers::i3c; + +use core::marker::PhantomData; +use tock_registers::interfaces::{ReadWriteable, Readable, Writeable}; + +/// MIPI DCR value for an MCTP endpoint. +pub const MCTP_DCR: u8 = 0xCC; + +/// Driver for the Caliptra-SS i3c-core in target (secondary) mode. +pub struct CaliptraI3cTarget { + regs: *const i3c::regs::I3c, + // !Send + !Sync: exclusive ownership of one physical peripheral + _not_send_sync: PhantomData<*mut ()>, +} + +impl CaliptraI3cTarget { + /// Construct the driver over the Caliptra-SS I3C peripheral. + /// + /// # Safety + /// + /// Caller must ensure exclusive access to the I3C peripheral for the + /// lifetime of this value. + pub const unsafe fn new() -> Self { + Self { + regs: i3c::I3C_CSR_ADDR as *const i3c::regs::I3c, + _not_send_sync: PhantomData, + } + } + + #[inline] + fn regs(&self) -> &i3c::regs::I3c { + // SAFETY: `new` guarantees a valid address and exclusive access. + unsafe { &*self.regs } + } + + // ------------------------------------------------------------------------- + // Interrupts + // ------------------------------------------------------------------------- + + /// Enable the RX descriptor threshold interrupt. + pub fn enable_rx_interrupt(&mut self) { + self.regs() + .tti_interrupt_enable + .modify(i3c::bits::InterruptEnable::RxDescStatEn::SET); + } + + /// Disable the RX descriptor threshold interrupt. + pub fn disable_rx_interrupt(&mut self) { + self.regs() + .tti_interrupt_enable + .modify(i3c::bits::InterruptEnable::RxDescStatEn::CLEAR); + } + + // ------------------------------------------------------------------------- + // TTI receive (controller → target private write) + // ------------------------------------------------------------------------- + + /// Return `true` if an RX descriptor is waiting in the TTI queue. + pub fn rx_pending(&self) -> bool { + self.regs() + .tti_interrupt_status + .is_set(i3c::bits::InterruptStatus::RxDescStat) + } + + /// Drain one incoming write into `buf`. Returns the number of bytes read, + /// or `None` if no descriptor was present. + pub fn rx_read(&mut self, buf: &mut [u8]) -> Option { + let regs = self.regs(); + if !regs + .tti_interrupt_status + .is_set(i3c::bits::InterruptStatus::RxDescStat) + { + return None; + } + + let desc = regs.tti_rx_desc_queue_port.get(); + // Lower 16 bits of descriptor carry the data length in bytes. + let len = (desc & 0xffff) as usize; + let nwords = (len + 3) / 4; + + let mut i = 0usize; + for _ in 0..nwords { + let word = regs.tti_rx_data_port.get(); + for &b in &word.to_le_bytes() { + if let Some(slot) = buf.get_mut(i) { + *slot = b; + } + i += 1; + } + } + + // W1C — clear the status bit. + regs.tti_interrupt_status + .write(i3c::bits::InterruptStatus::RxDescStat::SET); + + Some(len.min(buf.len())) + } + + // ------------------------------------------------------------------------- + // TTI transmit (target → controller private read response) + // ------------------------------------------------------------------------- + + /// Queue `data` as the response to the next private-read from the controller. + /// + /// The descriptor must be written before the data words: the hardware + /// (and the emulator model) opens a new TX buffer on the descriptor + /// write and appends subsequent data-port writes to it, matching the + /// upstream caliptra-mcu-sw runtime driver. + pub fn tx_write(&mut self, data: &[u8]) { + let regs = self.regs(); + // Descriptor: data_length in lower 16 bits; saturate rather than truncate. + regs.tti_tx_desc_queue_port + .set(u32::try_from(data.len()).unwrap_or(u16::MAX as u32)); + let mut chunks = data.chunks_exact(4); + for chunk in &mut chunks { + let word = u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]); + regs.tti_tx_data_port.set(word); + } + let rem = chunks.remainder(); + if !rem.is_empty() { + let mut tmp = [0u8; 4]; + tmp[..rem.len()].copy_from_slice(rem); + regs.tti_tx_data_port.set(u32::from_le_bytes(tmp)); + } + } + + // ------------------------------------------------------------------------- + // IBI (In-Band Interrupt — target → controller unsolicited notification) + // ------------------------------------------------------------------------- + + /// Raise an IBI with the given Mandatory Data Byte and optional payload. + /// Payload must be ≤255 bytes; excess bytes are silently dropped. + /// + /// The descriptor word must be written before the payload words: the + /// hardware (and the emulator model) parses the first word written to + /// the IBI port as the descriptor and takes the payload length from it. + pub fn ibi_raise(&mut self, mdb: u8, payload: &[u8]) { + let payload = &payload[..payload.len().min(255)]; + let regs = self.regs(); + // IBI descriptor: MDB in bits [31:24], payload length in bits [7:0]. + let desc = ((mdb as u32) << 24) | (payload.len() as u32 & 0xff); + regs.tti_tti_ibi_port.set(desc); + let mut chunks = payload.chunks_exact(4); + for chunk in &mut chunks { + let word = u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]); + regs.tti_tti_ibi_port.set(word); + } + let rem = chunks.remainder(); + if !rem.is_empty() { + let mut tmp = [0u8; 4]; + tmp[..rem.len()].copy_from_slice(rem); + regs.tti_tti_ibi_port.set(u32::from_le_bytes(tmp)); + } + } +} diff --git a/target/veer/registers/BUILD.bazel b/target/veer/registers/BUILD.bazel new file mode 100644 index 000000000..86500bad4 --- /dev/null +++ b/target/veer/registers/BUILD.bazel @@ -0,0 +1,17 @@ +# Licensed under the Apache-2.0 license +# SPDX-License-Identifier: Apache-2.0 + +load("@rules_rust//rust:defs.bzl", "rust_library") + +package(default_visibility = ["//visibility:public"]) + +# Umbrella crate re-exporting all Caliptra-SS peripheral register modules. +rust_library( + name = "registers", + srcs = ["registers.rs"], + crate_name = "caliptra_ss_registers", + edition = "2024", + deps = [ + "//third_party/caliptra/caliptra-mcu-sw:firmware_registers_generated", + ], +) diff --git a/target/veer/registers/README.md b/target/veer/registers/README.md new file mode 100644 index 000000000..a17295c51 --- /dev/null +++ b/target/veer/registers/README.md @@ -0,0 +1,28 @@ +# target/veer/registers + +Umbrella crate (`caliptra_ss_registers`) that re-exports the generated +register definitions for every Caliptra Subsystem peripheral. + +## Source + +Registers are generated from the Caliptra-SS SystemRDL sources by +`caliptra_mcu_registers_generator` and live in the pinned +`caliptra-mcu-sw` third-party dependency at +`registers/generated-firmware/src/`. Each peripheral module exposes a +`bits` sub-module of `tock_registers::register_bitfields!` types plus a +base address constant (e.g. `I3C_CSR_ADDR = 0x2000_4000`). + +## Usage + +Add `//target/veer/registers` to your `deps` and import the peripheral +module you need: + +```rust +use caliptra_ss_registers::i3c; +// Bitfield types for register reads/writes +use i3c::bits::Control; + +// Base address for MMIO pointer construction +const BASE: u32 = i3c::I3C_CSR_ADDR; + +``` diff --git a/target/veer/registers/registers.rs b/target/veer/registers/registers.rs new file mode 100644 index 000000000..7dda9e40d --- /dev/null +++ b/target/veer/registers/registers.rs @@ -0,0 +1,24 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! Caliptra Subsystem register definitions. +//! +//! Re-exports the generated firmware register modules from caliptra-mcu-sw so +//! peripheral drivers in `target/veer/peripherals/` have a single import path. + +#![no_std] + +pub use caliptra_mcu_registers_generated::axicdma; +pub use caliptra_mcu_registers_generated::defines; +pub use caliptra_mcu_registers_generated::doe_mbox; +pub use caliptra_mcu_registers_generated::el2_pic_ctrl; +pub use caliptra_mcu_registers_generated::fuses; +pub use caliptra_mcu_registers_generated::i3c; +pub use caliptra_mcu_registers_generated::lc_ctrl; +pub use caliptra_mcu_registers_generated::mbox; +pub use caliptra_mcu_registers_generated::mci; +pub use caliptra_mcu_registers_generated::otp_ctrl; +pub use caliptra_mcu_registers_generated::primary_flash_ctrl; +pub use caliptra_mcu_registers_generated::secondary_flash_ctrl; +pub use caliptra_mcu_registers_generated::sha512_acc; +pub use caliptra_mcu_registers_generated::soc; diff --git a/target/veer/tests/i3c_echo/BUILD.bazel b/target/veer/tests/i3c_echo/BUILD.bazel new file mode 100644 index 000000000..132a3a929 --- /dev/null +++ b/target/veer/tests/i3c_echo/BUILD.bazel @@ -0,0 +1,80 @@ +# 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", "rust_test") +load("//target/veer:defs.bzl", "TARGET_COMPATIBLE_WITH") +load("//target/veer/tooling:caliptra_runner.bzl", "caliptra_runner") + +package(default_visibility = ["//visibility:public"]) + +system_image( + name = "i3c_echo", + kernel = ":target", + platform = "//target/veer", +) + +target_linker_script( + name = "linker_script", + system_config = ":system_config", + tags = ["kernel"], + template = "//target/veer:linker_script_template", +) + +rust_binary_no_panics_test( + name = "no_panics_test", + binary = ":i3c_echo", +) + +filegroup( + name = "system_config", + srcs = ["system.json5"], +) + +target_codegen( + name = "codegen", + arch = "@pigweed//pw_kernel/arch/riscv:arch_riscv", + system_config = ":system_config", +) + +rust_binary( + name = "target", + srcs = ["target.rs"], + edition = "2024", + target_compatible_with = TARGET_COMPATIBLE_WITH, + deps = [ + ":codegen", + ":linker_script", + "//target/veer:entry", + "//target/veer/peripherals/i3c", + "@pigweed//pw_kernel/arch/riscv:arch_riscv", + "@pigweed//pw_kernel/kernel", + "@pigweed//pw_kernel/subsys/console:console_backend", + "@pigweed//pw_kernel/target:target_common", + "@pigweed//pw_log/rust:pw_log", + ], +) + +caliptra_runner( + name = "i3c_echo_runner", + interface = "emulator", + tags = ["manual"], + target = ":i3c_echo", +) + +rust_test( + name = "i3c_echo_test", + srcs = ["i3c_echo_host_test.rs"], + crate_root = "i3c_echo_host_test.rs", + edition = "2024", + data = [":i3c_echo_runner"], + # caliptra_runner.py hardcodes --i3c-port=65534; must not run in parallel. + tags = [ + "emulator", + "exclusive", + ], + deps = ["//target/veer/tests/i3c_host"], +) diff --git a/target/veer/tests/i3c_echo/i3c_echo_host_test.rs b/target/veer/tests/i3c_echo/i3c_echo_host_test.rs new file mode 100644 index 000000000..8e82fad86 --- /dev/null +++ b/target/veer/tests/i3c_echo/i3c_echo_host_test.rs @@ -0,0 +1,83 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! Host-side harness for the VeeR I3C echo test. +//! +//! Sends a private write, reads the firmware's echo back with a private +//! read, then sends a "DONE" write so the firmware exits 0. + +use i3c_host::{ + body_with_pec, connect_i3c_socket, read_outgoing_packet, send_private_read_on_stream, + send_private_write_on_stream, Runner, +}; +use std::thread; +use std::time::Duration; + +const PAYLOAD: [u8; 16] = [ + 0xa0, 0xa1, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, 0xab, 0xac, 0xad, 0xae, + 0xaf, +]; +const DONE: &[u8] = b"DONE"; + +#[test] +fn i3c_echo_host_test() { + let runner = Runner::spawn( + "target/veer/tests/i3c_echo/i3c_echo_runner.sh", + "waiting for private write", + ); + assert!( + runner.wait_ready(Duration::from_secs(600)), + "runner exited or timed out before firmware readiness" + ); + + let addr = runner.target_addr(); + let mut stream = + connect_i3c_socket(Duration::from_secs(5)).expect("failed to connect to I3C socket"); + // The firmware echoes the full body (payload + PEC) back verbatim. + let expected = body_with_pec(addr, &PAYLOAD); + + let mut echoed = false; + let mut attempts = 0u32; + 'outer: for _ in 0..20 { + attempts += 1; + if send_private_write_on_stream(&mut stream, addr, &PAYLOAD).is_err() { + thread::sleep(Duration::from_millis(250)); + continue; + } + thread::sleep(Duration::from_millis(100)); + if send_private_read_on_stream(&mut stream, addr).is_err() { + thread::sleep(Duration::from_millis(250)); + continue; + } + stream + .set_read_timeout(Some(Duration::from_secs(2))) + .expect("failed to set read timeout"); + // Drain packets until the echo shows up or the read times out; + // skip IBIs and stale responses from earlier attempts. + loop { + match read_outgoing_packet(&mut stream) { + Ok(pkt) => { + println!( + "I3C HOST TRACE: packet ibi=0x{:02x} from=0x{:02x} data={:02x?}", + pkt.ibi, pkt.from_addr, pkt.data + ); + if pkt.ibi == 0 && pkt.data == expected { + echoed = true; + break 'outer; + } + } + Err(_) => break, + } + } + } + assert!(echoed, "echo never received after {} attempts", attempts); + + // Host-driven teardown: keep sending DONE until the firmware exits. + while !runner.exited() { + let _ = send_private_write_on_stream(&mut stream, addr, DONE); + thread::sleep(Duration::from_millis(100)); + } + + let status = runner.wait(); + assert!(status.success(), "runner exited with status: {}", status); +} diff --git a/target/veer/tests/i3c_echo/system.json5 b/target/veer/tests/i3c_echo/system.json5 new file mode 100644 index 000000000..cc890eadf --- /dev/null +++ b/target/veer/tests/i3c_echo/system.json5 @@ -0,0 +1,16 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 +{ + arch: { + type: "riscv", + }, + kernel: { + flash_start_address: 0xA0010000, + flash_size_bytes: 65536, + ram_start_address: 0x10000000, + ram_size_bytes: 32768, + interrupt_table: { + table: {} + }, + }, +} diff --git a/target/veer/tests/i3c_echo/target.rs b/target/veer/tests/i3c_echo/target.rs new file mode 100644 index 000000000..92cde4783 --- /dev/null +++ b/target/veer/tests/i3c_echo/target.rs @@ -0,0 +1,64 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! I3C echo test. +//! +//! Echoes every received private write back verbatim via the TTI TX queue +//! (PEC byte included), so the host can read it back with a private read. +//! A write whose payload starts with ASCII "DONE" ends the test with +//! exit(0). Reception is pure-polling: this system image has an empty +//! interrupt table. + +#![no_std] +#![no_main] + +use caliptra_i3c_target::CaliptraI3cTarget; +use entry::exit; +use target_common::{declare_target, TargetInterface}; +use {console_backend as _, kernel as _}; + +pub struct Target {} + +// Emits PW_KERNEL_INTERRUPT_TABLE from the interrupt_table in system.json5. +codegen::declare_kernel_interrupt_handlers!(); + +impl TargetInterface for Target { + const NAME: &'static str = "Caliptra I3C Echo Test"; + + fn main() -> ! { + // SAFETY: single call at boot; Caliptra ROM has already initialized + // the I3C core and we are the only owner of the peripheral. + let mut i3c = unsafe { CaliptraI3cTarget::new() }; + pw_log::info!("I3C echo test: waiting for private write"); + + let mut buf = [0u8; 64]; + const MAX_POLLS: u32 = 10_000_000; + let mut polls = 0u32; + loop { + if let Some(len) = i3c.rx_read(&mut buf) { + if len >= 4 && buf[..4] == *b"DONE" { + pw_log::info!("I3C echo test: received DONE"); + exit(0); + } + let len = len.min(buf.len()); + i3c.tx_write(&buf[..len]); + pw_log::info!("I3C echo trace: echoed {} bytes", len as u32); + } + polls += 1; + if polls >= MAX_POLLS { + pw_log::info!("I3C echo test: timed out waiting for write"); + exit(2); + } + } + } + + fn shutdown(code: u32) -> ! { + match code { + 0 => pw_log::info!("PASS"), + _ => pw_log::info!("FAIL: {}", code), + } + exit(code); + } +} + +declare_target!(Target); diff --git a/target/veer/tests/i3c_host/BUILD.bazel b/target/veer/tests/i3c_host/BUILD.bazel new file mode 100644 index 000000000..1ca2bd422 --- /dev/null +++ b/target/veer/tests/i3c_host/BUILD.bazel @@ -0,0 +1,18 @@ +# Licensed under the Apache-2.0 license +# SPDX-License-Identifier: Apache-2.0 + +load("@rules_rust//rust:defs.bzl", "rust_library", "rust_test") + +package(default_visibility = ["//visibility:public"]) + +rust_library( + name = "i3c_host", + srcs = ["lib.rs"], + edition = "2024", +) + +rust_test( + name = "i3c_host_unit_test", + crate = ":i3c_host", + edition = "2024", +) diff --git a/target/veer/tests/i3c_host/lib.rs b/target/veer/tests/i3c_host/lib.rs new file mode 100644 index 000000000..886121a7f --- /dev/null +++ b/target/veer/tests/i3c_host/lib.rs @@ -0,0 +1,377 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! Shared host-side harness for VeeR emulator I3C tests. +//! +//! Wire protocol (see caliptra-mcu-sw common/testing/src/i3c_socket_server.rs): +//! - Host -> emulator: `to_addr: u8` + two LE u32 command words + data bytes. +//! `rnw` is bit 29 of the 64-bit command; `data_length` is bits 63:48. +//! - Emulator -> host: `ibi: u8`, `from_addr: u8`, LE u32 response +//! descriptor (`data_length` in bits 15:0), then data bytes. + +use std::io::{BufRead, BufReader, Read, Write}; +use std::net::TcpStream; +use std::path::{Path, PathBuf}; +use std::process::{Child, Command, ExitStatus, Stdio}; +use std::sync::atomic::{AtomicBool, AtomicU8, Ordering}; +use std::sync::Arc; +use std::thread::{self, JoinHandle}; +use std::time::{Duration, Instant}; + +pub const I3C_HOST: &str = "127.0.0.1"; +pub const I3C_PORT: u16 = 65534; +pub const DEFAULT_TARGET_ADDR: u8 = 0x08; + +pub fn crc8_smbus(data: &[u8]) -> u8 { + let mut crc: u8 = 0; + for &value in data { + crc ^= value; + for _ in 0..8 { + if (crc & 0x80) != 0 { + crc = (crc << 1) ^ 0x07; + } else { + crc <<= 1; + } + } + } + crc +} + +/// Payload followed by the SMBus PEC over (write-address byte + payload). +pub fn body_with_pec(target_addr: u8, payload: &[u8]) -> Vec { + let mut pec_input = Vec::with_capacity(1 + payload.len()); + pec_input.push(target_addr << 1); + pec_input.extend_from_slice(payload); + let pec = crc8_smbus(&pec_input); + let mut body = Vec::with_capacity(payload.len() + 1); + body.extend_from_slice(payload); + body.push(pec); + body +} + +pub fn make_private_write_header(target_addr: u8, data_len: u16) -> [u8; 9] { + // rnw (bit 29) = 0; data_length in bits 63:48 (word1 bits 31:16). + let cmd_word0: u32 = 0; + let cmd_word1: u32 = (data_len as u32) << 16; + let mut out = [0u8; 9]; + out[0] = target_addr; + out[1..5].copy_from_slice(&cmd_word0.to_le_bytes()); + out[5..9].copy_from_slice(&cmd_word1.to_le_bytes()); + out +} + +pub fn make_private_read_header(target_addr: u8) -> [u8; 9] { + // rnw (bit 29) = 1; data_length = 0 (the target reports its own length). + let cmd_word0: u32 = 1 << 29; + let cmd_word1: u32 = 0; + let mut out = [0u8; 9]; + out[0] = target_addr; + out[1..5].copy_from_slice(&cmd_word0.to_le_bytes()); + out[5..9].copy_from_slice(&cmd_word1.to_le_bytes()); + out +} + +pub fn connect_i3c_socket(timeout: Duration) -> Result { + let deadline = Instant::now() + timeout; + let mut last_err: Option = None; + while Instant::now() < deadline { + match TcpStream::connect((I3C_HOST, I3C_PORT)) { + Ok(s) => return Ok(s), + Err(e) => { + last_err = Some(e.to_string()); + thread::sleep(Duration::from_millis(50)); + } + } + } + Err(format!( + "timed out connecting to {}:{} ({})", + I3C_HOST, + I3C_PORT, + last_err.unwrap_or_else(|| "unknown error".to_string()) + )) +} + +pub fn send_private_write_on_stream( + stream: &mut TcpStream, + target_addr: u8, + payload: &[u8], +) -> Result<(), String> { + let body = body_with_pec(target_addr, payload); + let header = make_private_write_header(target_addr, body.len() as u16); + let mut frame = Vec::with_capacity(header.len() + body.len()); + frame.extend_from_slice(&header); + frame.extend_from_slice(&body); + stream + .write_all(&frame) + .map_err(|e| format!("failed writing I3C private-write frame: {}", e))?; + println!( + "I3C HOST TRACE: wrote frame to addr=0x{target_addr:02x} header={:02x?} body={:02x?}", + header, body + ); + Ok(()) +} + +pub fn send_private_read_on_stream( + stream: &mut TcpStream, + target_addr: u8, +) -> Result<(), String> { + let header = make_private_read_header(target_addr); + stream + .write_all(&header) + .map_err(|e| format!("failed writing I3C private-read command: {}", e))?; + println!("I3C HOST TRACE: wrote read command to addr=0x{target_addr:02x}"); + Ok(()) +} + +pub struct OutgoingPacket { + pub ibi: u8, + pub from_addr: u8, + pub data: Vec, +} + +pub fn read_outgoing_packet(reader: &mut R) -> Result { + let mut header = [0u8; 6]; + reader + .read_exact(&mut header) + .map_err(|e| format!("failed reading outgoing packet header: {}", e))?; + let descriptor = u32::from_le_bytes([header[2], header[3], header[4], header[5]]); + let len = (descriptor & 0xffff) as usize; + let mut data = vec![0u8; len]; + reader + .read_exact(&mut data) + .map_err(|e| format!("failed reading outgoing packet data ({} bytes): {}", len, e))?; + Ok(OutgoingPacket { + ibi: header[0], + from_addr: header[1], + data, + }) +} + +pub fn extract_target_addr(line: &str) -> Option { + let marker = "target DynamicI3cAddress("; + let start = line.find(marker)? + marker.len(); + let rest = &line[start..]; + let end = rest.find(')')?; + let parsed = rest[..end].parse::().ok()?; + u8::try_from(parsed).ok() +} + +fn resolve_runner_path(runner_rel_path: &str) -> PathBuf { + let srcdir = + std::env::var("TEST_SRCDIR").expect("missing TEST_SRCDIR environment variable"); + let workspace = + std::env::var("TEST_WORKSPACE").expect("missing TEST_WORKSPACE environment variable"); + let candidate = Path::new(&srcdir).join(&workspace).join(runner_rel_path); + if candidate.exists() { + return candidate; + } + panic!( + "unable to locate emulator runner at {:?}; TEST_SRCDIR={:?}, TEST_WORKSPACE={:?}", + candidate, + std::env::var("TEST_SRCDIR").ok(), + std::env::var("TEST_WORKSPACE").ok(), + ); +} + +fn resolve_runner_cwd(runner: &Path) -> PathBuf { + if let (Ok(srcdir), Ok(workspace)) = + (std::env::var("TEST_SRCDIR"), std::env::var("TEST_WORKSPACE")) + { + let root = Path::new(&srcdir).join(&workspace); + if root.exists() { + return root; + } + } + runner + .parent() + .expect("runner path has no parent directory") + .to_path_buf() +} + +/// A spawned emulator runner with background stdout/stderr watchers. +pub struct Runner { + child: Child, + ready: Arc, + exited: Arc, + target_addr: Arc, + stdout_thread: Option>, + stderr_thread: Option>, +} + +impl Runner { + /// Spawn `runner_rel_path` (workspace-relative runfiles path). `ready` + /// flips when `ready_marker` appears on either stream; `exited` flips + /// when stdout reaches EOF. + pub fn spawn(runner_rel_path: &str, ready_marker: &'static str) -> Self { + let runner = resolve_runner_path(runner_rel_path); + let cwd = resolve_runner_cwd(&runner); + let mut child = Command::new(&runner) + .current_dir(cwd) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .expect("failed to spawn emulator runner"); + + let ready = Arc::new(AtomicBool::new(false)); + let exited = Arc::new(AtomicBool::new(false)); + let target_addr = Arc::new(AtomicU8::new(DEFAULT_TARGET_ADDR)); + + let stdout = child.stdout.take().expect("failed to capture runner stdout"); + let stderr = child.stderr.take().expect("failed to capture runner stderr"); + + let watch = |reader: Box, + to_stderr: bool, + ready: Arc, + exited: Option>, + target_addr: Arc| { + move || { + let mut reader = BufReader::new(reader); + let mut line = String::new(); + loop { + line.clear(); + let n = reader + .read_line(&mut line) + .expect("failed to read runner output"); + if n == 0 { + break; + } + if to_stderr { + eprint!("{}", line); + } else { + print!("{}", line); + } + if let Some(addr) = extract_target_addr(&line) { + target_addr.store(addr, Ordering::Relaxed); + } + if line.contains(ready_marker) { + ready.store(true, Ordering::Relaxed); + } + } + if let Some(exited) = exited { + exited.store(true, Ordering::Relaxed); + } + } + }; + + let stdout_thread = thread::spawn(watch( + Box::new(stdout), + false, + Arc::clone(&ready), + Some(Arc::clone(&exited)), + Arc::clone(&target_addr), + )); + let stderr_thread = thread::spawn(watch( + Box::new(stderr), + true, + Arc::clone(&ready), + None, + Arc::clone(&target_addr), + )); + + Runner { + child, + ready, + exited, + target_addr, + stdout_thread: Some(stdout_thread), + stderr_thread: Some(stderr_thread), + } + } + + pub fn ready(&self) -> bool { + self.ready.load(Ordering::Relaxed) + } + + pub fn exited(&self) -> bool { + self.exited.load(Ordering::Relaxed) + } + + pub fn target_addr(&self) -> u8 { + self.target_addr.load(Ordering::Relaxed) + } + + /// Block until the ready marker is seen. Returns false if the runner + /// exits first or `timeout` elapses. + pub fn wait_ready(&self, timeout: Duration) -> bool { + let deadline = Instant::now() + timeout; + while Instant::now() < deadline { + if self.ready() { + return true; + } + if self.exited() { + return false; + } + thread::sleep(Duration::from_millis(10)); + } + false + } + + /// Join the watcher threads and reap the child. + pub fn wait(mut self) -> ExitStatus { + if let Some(t) = self.stdout_thread.take() { + t.join().expect("failed to join stdout watcher"); + } + if let Some(t) = self.stderr_thread.take() { + t.join().expect("failed to join stderr watcher"); + } + self.child.wait().expect("failed to wait for runner") + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::io::Cursor; + + #[test] + fn crc8_matches_known_value() { + // Address 0x08 (write byte 0x10) + payload [1,2,3,4] -> PEC 0xd1, + // as observed on the wire in the i3c_smoke test. + assert_eq!(crc8_smbus(&[0x10, 0x01, 0x02, 0x03, 0x04]), 0xd1); + } + + #[test] + fn body_appends_pec() { + assert_eq!( + body_with_pec(0x08, &[0x01, 0x02, 0x03, 0x04]), + vec![0x01, 0x02, 0x03, 0x04, 0xd1] + ); + } + + #[test] + fn write_header_layout() { + // data_length lands in word1 bits 31:16. + assert_eq!( + make_private_write_header(0x08, 5), + [0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00] + ); + } + + #[test] + fn read_header_sets_rnw_bit_29() { + assert_eq!( + make_private_read_header(0x08), + [0x08, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00] + ); + } + + #[test] + fn parses_outgoing_packet() { + // ibi=0, from_addr=8, descriptor len=3, data [aa,bb,cc]. + let bytes = [0x00, 0x08, 0x03, 0x00, 0x00, 0x00, 0xaa, 0xbb, 0xcc]; + let mut cursor = Cursor::new(&bytes[..]); + let pkt = read_outgoing_packet(&mut cursor).unwrap(); + assert_eq!(pkt.ibi, 0); + assert_eq!(pkt.from_addr, 8); + assert_eq!(pkt.data, vec![0xaa, 0xbb, 0xcc]); + } + + #[test] + fn extracts_dynamic_address() { + assert_eq!( + extract_target_addr("i3c target DynamicI3cAddress(9) attached"), + Some(9) + ); + assert_eq!(extract_target_addr("no address here"), None); + } +} diff --git a/target/veer/tests/i3c_ibi/BUILD.bazel b/target/veer/tests/i3c_ibi/BUILD.bazel new file mode 100644 index 000000000..7d16c25ea --- /dev/null +++ b/target/veer/tests/i3c_ibi/BUILD.bazel @@ -0,0 +1,80 @@ +# 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", "rust_test") +load("//target/veer:defs.bzl", "TARGET_COMPATIBLE_WITH") +load("//target/veer/tooling:caliptra_runner.bzl", "caliptra_runner") + +package(default_visibility = ["//visibility:public"]) + +system_image( + name = "i3c_ibi", + kernel = ":target", + platform = "//target/veer", +) + +target_linker_script( + name = "linker_script", + system_config = ":system_config", + tags = ["kernel"], + template = "//target/veer:linker_script_template", +) + +rust_binary_no_panics_test( + name = "no_panics_test", + binary = ":i3c_ibi", +) + +filegroup( + name = "system_config", + srcs = ["system.json5"], +) + +target_codegen( + name = "codegen", + arch = "@pigweed//pw_kernel/arch/riscv:arch_riscv", + system_config = ":system_config", +) + +rust_binary( + name = "target", + srcs = ["target.rs"], + edition = "2024", + target_compatible_with = TARGET_COMPATIBLE_WITH, + deps = [ + ":codegen", + ":linker_script", + "//target/veer:entry", + "//target/veer/peripherals/i3c", + "@pigweed//pw_kernel/arch/riscv:arch_riscv", + "@pigweed//pw_kernel/kernel", + "@pigweed//pw_kernel/subsys/console:console_backend", + "@pigweed//pw_kernel/target:target_common", + "@pigweed//pw_log/rust:pw_log", + ], +) + +caliptra_runner( + name = "i3c_ibi_runner", + interface = "emulator", + tags = ["manual"], + target = ":i3c_ibi", +) + +rust_test( + name = "i3c_ibi_test", + srcs = ["i3c_ibi_host_test.rs"], + crate_root = "i3c_ibi_host_test.rs", + edition = "2024", + data = [":i3c_ibi_runner"], + # caliptra_runner.py hardcodes --i3c-port=65534; must not run in parallel. + tags = [ + "emulator", + "exclusive", + ], + deps = ["//target/veer/tests/i3c_host"], +) diff --git a/target/veer/tests/i3c_ibi/i3c_ibi_host_test.rs b/target/veer/tests/i3c_ibi/i3c_ibi_host_test.rs new file mode 100644 index 000000000..83284b4bf --- /dev/null +++ b/target/veer/tests/i3c_ibi/i3c_ibi_host_test.rs @@ -0,0 +1,80 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! Host-side harness for the VeeR I3C IBI test. +//! +//! Sends a private write to trigger the firmware's IBI, asserts the IBI's +//! MDB arrives on the socket, then sends a "DONE" write so the firmware +//! exits 0. +//! +//! The emulator's IBI model forwards only the MDB to the controller side +//! (payload forwarding is an upstream TODO in emulator/periph/src/i3c.rs +//! check_ibi_buffer), so only the MDB is asserted here even though the +//! firmware raises the IBI with a payload. + +use i3c_host::{ + connect_i3c_socket, read_outgoing_packet, send_private_write_on_stream, Runner, +}; +use std::thread; +use std::time::Duration; + +const TRIGGER: [u8; 4] = [0xb0, 0xb1, 0xb2, 0xb3]; +const IBI_MDB: u8 = 0xA5; +const DONE: &[u8] = b"DONE"; + +#[test] +fn i3c_ibi_host_test() { + let runner = Runner::spawn( + "target/veer/tests/i3c_ibi/i3c_ibi_runner.sh", + "waiting for private write", + ); + assert!( + runner.wait_ready(Duration::from_secs(600)), + "runner exited or timed out before firmware readiness" + ); + + let addr = runner.target_addr(); + let mut stream = + connect_i3c_socket(Duration::from_secs(5)).expect("failed to connect to I3C socket"); + + let mut ibi_seen = false; + let mut attempts = 0u32; + 'outer: for _ in 0..20 { + attempts += 1; + if send_private_write_on_stream(&mut stream, addr, &TRIGGER).is_err() { + thread::sleep(Duration::from_millis(250)); + continue; + } + stream + .set_read_timeout(Some(Duration::from_secs(2))) + .expect("failed to set read timeout"); + // The IBI is forwarded spontaneously by the emulator's controller + // pump; no read command is needed. Drain packets until it shows up + // or the read times out. + loop { + match read_outgoing_packet(&mut stream) { + Ok(pkt) => { + println!( + "I3C HOST TRACE: packet ibi=0x{:02x} from=0x{:02x} data={:02x?}", + pkt.ibi, pkt.from_addr, pkt.data + ); + if pkt.ibi == IBI_MDB { + ibi_seen = true; + break 'outer; + } + } + Err(_) => break, + } + } + } + assert!(ibi_seen, "IBI never received after {} attempts", attempts); + + // Host-driven teardown: keep sending DONE until the firmware exits. + while !runner.exited() { + let _ = send_private_write_on_stream(&mut stream, addr, DONE); + thread::sleep(Duration::from_millis(100)); + } + + let status = runner.wait(); + assert!(status.success(), "runner exited with status: {}", status); +} diff --git a/target/veer/tests/i3c_ibi/system.json5 b/target/veer/tests/i3c_ibi/system.json5 new file mode 100644 index 000000000..cc890eadf --- /dev/null +++ b/target/veer/tests/i3c_ibi/system.json5 @@ -0,0 +1,16 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 +{ + arch: { + type: "riscv", + }, + kernel: { + flash_start_address: 0xA0010000, + flash_size_bytes: 65536, + ram_start_address: 0x10000000, + ram_size_bytes: 32768, + interrupt_table: { + table: {} + }, + }, +} diff --git a/target/veer/tests/i3c_ibi/target.rs b/target/veer/tests/i3c_ibi/target.rs new file mode 100644 index 000000000..c2071a8d5 --- /dev/null +++ b/target/veer/tests/i3c_ibi/target.rs @@ -0,0 +1,66 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! I3C IBI test. +//! +//! Raises an IBI (MDB 0xA5 with a 4-byte payload) each time a private write +//! arrives, so the host can assert the IBI reaches the controller side. A +//! write whose payload starts with ASCII "DONE" ends the test with exit(0). +//! Reception is pure-polling: this system image has an empty interrupt +//! table. + +#![no_std] +#![no_main] + +use caliptra_i3c_target::CaliptraI3cTarget; +use entry::exit; +use target_common::{declare_target, TargetInterface}; +use {console_backend as _, kernel as _}; + +pub struct Target {} + +// Emits PW_KERNEL_INTERRUPT_TABLE from the interrupt_table in system.json5. +codegen::declare_kernel_interrupt_handlers!(); + +const IBI_MDB: u8 = 0xA5; +const IBI_PAYLOAD: [u8; 4] = [0x11, 0x22, 0x33, 0x44]; + +impl TargetInterface for Target { + const NAME: &'static str = "Caliptra I3C IBI Test"; + + fn main() -> ! { + // SAFETY: single call at boot; Caliptra ROM has already initialized + // the I3C core and we are the only owner of the peripheral. + let mut i3c = unsafe { CaliptraI3cTarget::new() }; + pw_log::info!("I3C IBI test: waiting for private write"); + + let mut buf = [0u8; 64]; + const MAX_POLLS: u32 = 10_000_000; + let mut polls = 0u32; + loop { + if let Some(len) = i3c.rx_read(&mut buf) { + if len >= 4 && buf[..4] == *b"DONE" { + pw_log::info!("I3C IBI test: received DONE"); + exit(0); + } + i3c.ibi_raise(IBI_MDB, &IBI_PAYLOAD); + pw_log::info!("I3C IBI trace: raised IBI"); + } + polls += 1; + if polls >= MAX_POLLS { + pw_log::info!("I3C IBI test: timed out waiting for write"); + exit(2); + } + } + } + + fn shutdown(code: u32) -> ! { + match code { + 0 => pw_log::info!("PASS"), + _ => pw_log::info!("FAIL: {}", code), + } + exit(code); + } +} + +declare_target!(Target); diff --git a/target/veer/tests/i3c_irq/BUILD.bazel b/target/veer/tests/i3c_irq/BUILD.bazel new file mode 100644 index 000000000..3c4feb8ee --- /dev/null +++ b/target/veer/tests/i3c_irq/BUILD.bazel @@ -0,0 +1,80 @@ +# 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", "rust_test") +load("//target/veer:defs.bzl", "TARGET_COMPATIBLE_WITH") +load("//target/veer/tooling:caliptra_runner.bzl", "caliptra_runner") + +package(default_visibility = ["//visibility:public"]) + +system_image( + name = "i3c_irq", + kernel = ":target", + platform = "//target/veer", +) + +target_linker_script( + name = "linker_script", + system_config = ":system_config", + tags = ["kernel"], + template = "//target/veer:linker_script_template", +) + +rust_binary_no_panics_test( + name = "no_panics_test", + binary = ":i3c_irq", +) + +filegroup( + name = "system_config", + srcs = ["system.json5"], +) + +target_codegen( + name = "codegen", + arch = "@pigweed//pw_kernel/arch/riscv:arch_riscv", + system_config = ":system_config", +) + +rust_binary( + name = "target", + srcs = ["target.rs"], + edition = "2024", + target_compatible_with = TARGET_COMPATIBLE_WITH, + deps = [ + ":codegen", + ":linker_script", + "//target/veer:entry", + "//target/veer/peripherals/i3c", + "@pigweed//pw_kernel/arch/riscv:arch_riscv", + "@pigweed//pw_kernel/kernel", + "@pigweed//pw_kernel/subsys/console:console_backend", + "@pigweed//pw_kernel/target:target_common", + "@pigweed//pw_log/rust:pw_log", + ], +) + +caliptra_runner( + name = "i3c_irq_runner", + interface = "emulator", + tags = ["manual"], + target = ":i3c_irq", +) + +rust_test( + name = "i3c_irq_test", + srcs = ["i3c_irq_host_test.rs"], + crate_root = "i3c_irq_host_test.rs", + edition = "2024", + data = [":i3c_irq_runner"], + # caliptra_runner.py hardcodes --i3c-port=65534; must not run in parallel. + tags = [ + "emulator", + "exclusive", + ], + deps = ["//target/veer/tests/i3c_host"], +) diff --git a/target/veer/tests/i3c_irq/i3c_irq_host_test.rs b/target/veer/tests/i3c_irq/i3c_irq_host_test.rs new file mode 100644 index 000000000..f0fb4596a --- /dev/null +++ b/target/veer/tests/i3c_irq/i3c_irq_host_test.rs @@ -0,0 +1,42 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! Host-side harness for the VeeR interrupt-driven I3C RX test. +//! +//! Identical to the smoke harness: inject private-write frames until the +//! firmware (which discovers the frame via the I3C interrupt rather than +//! polling) receives one and exits. + +use i3c_host::{connect_i3c_socket, send_private_write_on_stream, Runner}; +use std::thread; +use std::time::Duration; + +const PAYLOAD: [u8; 4] = [0x01, 0x02, 0x03, 0x04]; + +#[test] +fn i3c_irq_host_test() { + let runner = Runner::spawn( + "target/veer/tests/i3c_irq/i3c_irq_runner.sh", + "waiting for private write", + ); + assert!( + runner.wait_ready(Duration::from_secs(600)), + "runner exited or timed out before firmware readiness" + ); + + let addr = runner.target_addr(); + let mut stream = + connect_i3c_socket(Duration::from_secs(5)).expect("failed to connect to I3C socket"); + + let mut sends = 0u32; + while !runner.exited() { + if send_private_write_on_stream(&mut stream, addr, &PAYLOAD).is_ok() { + sends += 1; + } + thread::sleep(Duration::from_millis(100)); + } + + let status = runner.wait(); + assert!(sends > 0, "no I3C private-write frame was ever sent"); + assert!(status.success(), "runner exited with status: {}", status); +} diff --git a/target/veer/tests/i3c_irq/system.json5 b/target/veer/tests/i3c_irq/system.json5 new file mode 100644 index 000000000..85daa5f0d --- /dev/null +++ b/target/veer/tests/i3c_irq/system.json5 @@ -0,0 +1,19 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 +{ + arch: { + type: "riscv", + }, + kernel: { + flash_start_address: 0xA0010000, + flash_size_bytes: 65536, + ram_start_address: 0x10000000, + ram_size_bytes: 32768, + interrupt_table: { + table: { + // IRQ 2 = I3C on the emulated VeeR PIC (McuRootBus::I3C_IRQ). + "2": "i3c_interrupt_handler", + } + }, + }, +} diff --git a/target/veer/tests/i3c_irq/target.rs b/target/veer/tests/i3c_irq/target.rs new file mode 100644 index 000000000..05c476ebb --- /dev/null +++ b/target/veer/tests/i3c_irq/target.rs @@ -0,0 +1,102 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! Interrupt-driven I3C RX test. +//! +//! Unlike the polling smoke test, main never touches the I3C status +//! registers to discover a frame: the TTI RX-descriptor interrupt (IRQ 2 on +//! the VeeR PIC, wired to `i3c_interrupt_handler` via system.json5) is the +//! only path from frame arrival to reception. The handler masks the RX +//! interrupt enable (the IRQ is level-triggered) and sets a flag; main +//! drains the frame and exits 0 on the expected payload, 1 on a mismatch, +//! and 2 on timeout. + +#![no_std] +#![no_main] + +use caliptra_i3c_target::CaliptraI3cTarget; +use core::sync::atomic::{AtomicBool, Ordering}; +use entry::exit; +use target_common::{declare_target, TargetInterface}; +use console_backend as _; + +// The declare_kernel_interrupt_handlers! expansion below imports +// kernel::interrupt_controller::InterruptController at module scope, which +// is what makes the enable_interrupt call in main resolve. + +pub struct Target {} + +// Emits PW_KERNEL_INTERRUPT_TABLE from the interrupt_table in system.json5. +codegen::declare_kernel_interrupt_handlers!(); + +/// I3C IRQ number on the emulated VeeR PIC; must match system.json5. +const I3C_IRQ: u32 = 2; + +// AtomicBool with store/load only: riscv32imc has no atomic RMW instructions. +static RX_EVENT: AtomicBool = AtomicBool::new(false); + +/// Referenced by name from system.json5; the generated wrapper calls this +/// with the concrete arch inside an interrupt guard. +pub fn i3c_interrupt_handler(_kernel: K) { + // The RxDescStat IRQ is level-triggered (asserted while enable & status + // are both set), so mask the enable here to deassert it; main drains + // the descriptor afterwards. + // + // SAFETY: main only spins on RX_EVENT while the interrupt is enabled, + // so this ephemeral handle cannot race main's register accesses. + let mut i3c = unsafe { CaliptraI3cTarget::new() }; + i3c.disable_rx_interrupt(); + RX_EVENT.store(true, Ordering::SeqCst); +} + +impl TargetInterface for Target { + const NAME: &'static str = "Caliptra I3C IRQ Test"; + + fn main() -> ! { + // SAFETY: single call at boot; Caliptra ROM has already initialized + // the I3C core and we are the only owner of the peripheral (the + // interrupt handler's handle is sequenced by RX_EVENTS, see above). + let mut i3c = unsafe { CaliptraI3cTarget::new() }; + i3c.enable_rx_interrupt(); + ::InterruptController::enable_interrupt(I3C_IRQ); + pw_log::info!("I3C IRQ test: waiting for private write"); + + const MAX_POLLS: u32 = 10_000_000; + let mut polls = 0u32; + while !RX_EVENT.load(Ordering::SeqCst) { + polls += 1; + if polls >= MAX_POLLS { + pw_log::info!("I3C IRQ test: timed out waiting for interrupt"); + exit(2); + } + core::hint::spin_loop(); + } + pw_log::info!("I3C IRQ trace: interrupt observed"); + + let mut buf = [0u8; 64]; + match i3c.rx_read(&mut buf) { + Some(len) if len >= 4 && buf[..4] == [0x01, 0x02, 0x03, 0x04] => { + pw_log::info!("I3C IRQ test: received expected payload OK"); + exit(0); + } + Some(len) => { + pw_log::info!("I3C IRQ test: unexpected payload len={}", len as u32); + exit(1); + } + None => { + pw_log::info!("I3C IRQ test: interrupt fired but no descriptor"); + exit(1); + } + } + } + + fn shutdown(code: u32) -> ! { + match code { + 0 => pw_log::info!("PASS"), + _ => pw_log::info!("FAIL: {}", code), + } + exit(code); + } +} + +declare_target!(Target); diff --git a/target/veer/tests/i3c_smoke/BUILD.bazel b/target/veer/tests/i3c_smoke/BUILD.bazel new file mode 100644 index 000000000..60158e3e9 --- /dev/null +++ b/target/veer/tests/i3c_smoke/BUILD.bazel @@ -0,0 +1,80 @@ +# 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", "rust_test") +load("//target/veer:defs.bzl", "TARGET_COMPATIBLE_WITH") +load("//target/veer/tooling:caliptra_runner.bzl", "caliptra_runner") + +package(default_visibility = ["//visibility:public"]) + +system_image( + name = "i3c_smoke", + kernel = ":target", + platform = "//target/veer", +) + +target_linker_script( + name = "linker_script", + system_config = ":system_config", + tags = ["kernel"], + template = "//target/veer:linker_script_template", +) + +rust_binary_no_panics_test( + name = "no_panics_test", + binary = ":i3c_smoke", +) + +filegroup( + name = "system_config", + srcs = ["system.json5"], +) + +target_codegen( + name = "codegen", + arch = "@pigweed//pw_kernel/arch/riscv:arch_riscv", + system_config = ":system_config", +) + +rust_binary( + name = "target", + srcs = ["target.rs"], + edition = "2024", + target_compatible_with = TARGET_COMPATIBLE_WITH, + deps = [ + ":codegen", + ":linker_script", + "//target/veer:entry", + "//target/veer/peripherals/i3c", + "@pigweed//pw_kernel/arch/riscv:arch_riscv", + "@pigweed//pw_kernel/kernel", + "@pigweed//pw_kernel/subsys/console:console_backend", + "@pigweed//pw_kernel/target:target_common", + "@pigweed//pw_log/rust:pw_log", + ], +) + +caliptra_runner( + name = "i3c_smoke_runner", + interface = "emulator", + tags = ["manual"], + target = ":i3c_smoke", +) + +rust_test( + name = "i3c_smoke_test", + srcs = ["i3c_smoke_host_test.rs"], + crate_root = "i3c_smoke_host_test.rs", + edition = "2024", + data = [":i3c_smoke_runner"], + # caliptra_runner.py hardcodes --i3c-port=65534; must not run in parallel. + tags = [ + "emulator", + "exclusive", + ], + deps = ["//target/veer/tests/i3c_host"], +) diff --git a/target/veer/tests/i3c_smoke/i3c_smoke_host_test.rs b/target/veer/tests/i3c_smoke/i3c_smoke_host_test.rs new file mode 100644 index 000000000..c2d12bad6 --- /dev/null +++ b/target/veer/tests/i3c_smoke/i3c_smoke_host_test.rs @@ -0,0 +1,47 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! Host-side harness for the VeeR I3C smoke test. +//! +//! Launches the emulator runner, waits for the firmware to report it is +//! waiting for a private write, then injects private-write frames over the +//! I3C TCP socket until the firmware receives one and exits. + +use i3c_host::{connect_i3c_socket, send_private_write_on_stream, Runner}; +use std::thread; +use std::time::Duration; + +const PAYLOAD: [u8; 4] = [0x01, 0x02, 0x03, 0x04]; + +#[test] +fn i3c_smoke_host_test() { + let runner = Runner::spawn( + "target/veer/tests/i3c_smoke/i3c_smoke_runner.sh", + // Only the firmware's own log line counts as readiness: the + // emulator's I3C socket opens minutes earlier, and frames sent that + // early race firmware boot. + "waiting for private write", + ); + assert!( + runner.wait_ready(Duration::from_secs(600)), + "runner exited or timed out before firmware readiness" + ); + + let addr = runner.target_addr(); + let mut stream = + connect_i3c_socket(Duration::from_secs(5)).expect("failed to connect to I3C socket"); + + // Keep sending until the runner exits: the firmware terminates the + // emulator as soon as one frame arrives. + let mut sends = 0u32; + while !runner.exited() { + if send_private_write_on_stream(&mut stream, addr, &PAYLOAD).is_ok() { + sends += 1; + } + thread::sleep(Duration::from_millis(100)); + } + + let status = runner.wait(); + assert!(sends > 0, "no I3C private-write frame was ever sent"); + assert!(status.success(), "runner exited with status: {}", status); +} diff --git a/target/veer/tests/i3c_smoke/system.json5 b/target/veer/tests/i3c_smoke/system.json5 new file mode 100644 index 000000000..cc890eadf --- /dev/null +++ b/target/veer/tests/i3c_smoke/system.json5 @@ -0,0 +1,16 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 +{ + arch: { + type: "riscv", + }, + kernel: { + flash_start_address: 0xA0010000, + flash_size_bytes: 65536, + ram_start_address: 0x10000000, + ram_size_bytes: 32768, + interrupt_table: { + table: {} + }, + }, +} diff --git a/target/veer/tests/i3c_smoke/target.rs b/target/veer/tests/i3c_smoke/target.rs new file mode 100644 index 000000000..dd3329f7f --- /dev/null +++ b/target/veer/tests/i3c_smoke/target.rs @@ -0,0 +1,82 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! I3C smoke test. +//! +//! Verifies that a private write sent by the test harness over TCP port +//! 65534 is received correctly with the expected payload +//! [0x01, 0x02, 0x03, 0x04]. Reception is pure-polling: this system image +//! has an empty interrupt table, so no peripheral interrupt is enabled. + +#![no_std] +#![no_main] + +use caliptra_i3c_target::CaliptraI3cTarget; +use entry::exit; +use target_common::{declare_target, TargetInterface}; +use {console_backend as _, kernel as _}; + +pub struct Target {} + +// Emits PW_KERNEL_INTERRUPT_TABLE from the interrupt_table in system.json5. +codegen::declare_kernel_interrupt_handlers!(); + +impl TargetInterface for Target { + const NAME: &'static str = "Caliptra I3C Smoke Test"; + + fn main() -> ! { + // SAFETY: single call at boot; Caliptra ROM has already initialized + // the I3C core and we are the only owner of the peripheral. + let mut i3c = unsafe { CaliptraI3cTarget::new() }; + pw_log::info!("I3C smoke test: waiting for private write"); + + let mut buf = [0u8; 64]; + // Poll until we receive a write or exhaust the iteration budget. + const MAX_POLLS: u32 = 10_000_000; + let mut polls = 0u32; + loop { + if polls % 1_000_000 == 0 { + let pending = i3c.rx_pending(); + pw_log::info!( + "I3C smoke trace: poll={} rx_pending={}", + polls, + if pending { 1u32 } else { 0u32 } + ); + } + + if let Some(len) = i3c.rx_read(&mut buf) { + pw_log::info!( + "I3C smoke trace: rx_read len={} first4={:02x} {:02x} {:02x} {:02x}", + len as u32, + buf[0], + buf[1], + buf[2], + buf[3] + ); + let expected = [0x01u8, 0x02, 0x03, 0x04]; + if len >= 4 && buf[..4] == expected { + pw_log::info!("I3C smoke test: received expected payload OK"); + exit(0); + } else { + pw_log::info!("I3C smoke test: unexpected payload len={}", len as u32); + exit(1); + } + } + polls += 1; + if polls >= MAX_POLLS { + pw_log::info!("I3C smoke test: timed out waiting for write"); + exit(2); + } + } + } + + fn shutdown(code: u32) -> ! { + match code { + 0 => pw_log::info!("PASS"), + _ => pw_log::info!("FAIL: {}", code), + } + exit(code); + } +} + +declare_target!(Target); diff --git a/third_party/caliptra/caliptra-mcu-sw/BUILD.bazel b/third_party/caliptra/caliptra-mcu-sw/BUILD.bazel index f40764767..282d9a80a 100644 --- a/third_party/caliptra/caliptra-mcu-sw/BUILD.bazel +++ b/third_party/caliptra/caliptra-mcu-sw/BUILD.bazel @@ -23,6 +23,19 @@ rust_library( ], ) +# no_std register definitions for Caliptra-SS peripherals (I3C, DMA, fuses, …). +rust_library( + name = "firmware_registers_generated", + srcs = ["@caliptra_mcu_sw//:all_files"], + crate_name = "caliptra_mcu_registers_generated", + crate_root = "@caliptra_mcu_sw//:registers/generated-firmware/src/lib.rs", + edition = "2021", + deps = [ + "//third_party/caliptra:crate_tock-registers", + "//third_party/caliptra:crate_zeroize", + ], +) + rust_library( name = "emulator_bmc", srcs = ["@caliptra_mcu_sw//:emulator_bmc_srcs"], @@ -183,23 +196,21 @@ rust_library( ], ) -# Emulator host binary +# Emulator host binary. Uses a local entry point instead of the upstream +# main.rs so the I3C controller socket pump is always started; see +# src/emulator_main.rs. rust_binary( name = "emulator", - srcs = ["@caliptra_mcu_sw//:emulator_srcs"], + srcs = ["src/emulator_main.rs"], aliases = { ":emulator_lib": "emulator", }, - crate_root = "@caliptra_mcu_sw//:emulator/app/src/main.rs", edition = "2021", deps = [ ":emulator_lib", ":mcu_testing_common", - "//third_party/caliptra:crate_log", "//third_party/caliptra/caliptra-sw:caliptra_emu_cpu", "@rust_caliptra_crates_host//:clap", - "@rust_caliptra_crates_host//:ctrlc", - "@rust_caliptra_crates_host//:simple_logger", ], ) diff --git a/third_party/caliptra/caliptra-mcu-sw/overlay.BUILD b/third_party/caliptra/caliptra-mcu-sw/overlay.BUILD index 832935550..7bd429ae2 100644 --- a/third_party/caliptra/caliptra-mcu-sw/overlay.BUILD +++ b/third_party/caliptra/caliptra-mcu-sw/overlay.BUILD @@ -17,6 +17,7 @@ exports_files( "rom/src/lib.rs", "common/testing/src/lib.rs", "registers/generated-emulator/src/lib.rs", + "registers/generated-firmware/src/lib.rs", "emulator/consts/src/lib.rs", "caliptra-util-host/apps/mailbox/server/src/lib.rs", "builder/src/lib.rs", diff --git a/third_party/caliptra/caliptra-mcu-sw/src/emulator_main.rs b/third_party/caliptra/caliptra-mcu-sw/src/emulator_main.rs new file mode 100644 index 000000000..b9aec8e96 --- /dev/null +++ b/third_party/caliptra/caliptra-mcu-sw/src/emulator_main.rs @@ -0,0 +1,33 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! Entry point for the Caliptra MCU emulator. +//! +//! Replaces the upstream `emulator/app/src/main.rs`, which only pumps I3C +//! socket traffic into the emulated target when built with one of its +//! `test-*` cargo features. Host-side tests drive firmware over the I3C TCP +//! socket (`--i3c-port`), so this entry point unconditionally starts the I3C +//! controller before entering the run loop. +//! +//! Firmware-requested exits terminate the process from within `step()` (the +//! emulator's exit-control peripheral calls `std::process::exit`), so the +//! loop only ends on fatal errors or breakpoints. + +use caliptra_emu_cpu::StepAction; +use clap::Parser; +use emulator::{Emulator, EmulatorArgs}; +use mcu_testing_common::MCU_RUNNING; +use std::io; + +fn main() -> io::Result<()> { + let cli = EmulatorArgs::parse(); + let mut emulator = Emulator::from_args(cli, false)?; + emulator.start_i3c_controller(); + while MCU_RUNNING.load(std::sync::atomic::Ordering::Relaxed) { + match emulator.step() { + StepAction::Break | StepAction::Fatal => break, + _ => {} + } + } + Ok(()) +} diff --git a/third_party/pigweed/veer_pic_meivt.patch b/third_party/pigweed/veer_pic_meivt.patch new file mode 100644 index 000000000..76c3a2175 --- /dev/null +++ b/third_party/pigweed/veer_pic_meivt.patch @@ -0,0 +1,56 @@ +diff -u -r a/pw_kernel/arch/riscv/veer_pic.rs b/pw_kernel/arch/riscv/veer_pic.rs +--- a/pw_kernel/arch/riscv/veer_pic.rs 2026-08-13 07:18:55.632731645 -0700 ++++ b/pw_kernel/arch/riscv/veer_pic.rs 2026-08-13 07:22:15.448047131 -0700 +@@ -280,6 +280,34 @@ + ); + set_global_priority(GLOBAL_PRIORITY); + ++ // Program the external-interrupt redirect table (MEIVT) if the ++ // target provides a location for it. VeeR delivers external ++ // interrupts by jumping to the address stored at MEIVT[irq], so ++ // point every entry at the mtvec trap vector: redirected ++ // interrupts then take the standard trap path and the claim id is ++ // read from MEIHAP as usual. Without this, the first external ++ // interrupt vectors through an unprogrammed table. ++ if let Some(base) = VeerPicConfig::MEIVT_BASE_ADDRESS { ++ // MEIVT must be 1KiB-aligned. ++ pw_assert::assert!(base & 0x3ff == 0); ++ let trap_vector = riscv::register::mtvec::read().bits() & !0b11; ++ for irq in 0..VeerPicConfig::MAX_IRQS as usize { ++ // SAFETY: the target guarantees the table region at `base` ++ // is reserved for the redirect table. ++ unsafe { ++ core::ptr::write_volatile( ++ (base + irq * core::mem::size_of::()) as *mut u32, ++ trap_vector as u32, ++ ); ++ } ++ } ++ // SAFETY: MEIVT (VeeR-specific CSR 0xBC8) selects the table ++ // initialized above. ++ unsafe { ++ core::arch::asm!("csrw 0xBC8, {}", in(reg) base); ++ } ++ } ++ + unsafe { + riscv::register::mie::set_mext(); + } +diff -u -r a/pw_kernel/config/lib.rs b/pw_kernel/config/lib.rs +--- a/pw_kernel/config/lib.rs 2026-08-13 07:18:55.628770650 -0700 ++++ b/pw_kernel/config/lib.rs 2026-08-13 07:21:21.367503980 -0700 +@@ -100,6 +100,14 @@ + /// The maximum number of interrupts the + /// PIC supports per context. + const MAX_IRQS: u32 = 256; ++ ++ /// Base address for the external-interrupt redirect table (MEIVT). ++ /// ++ /// Must be 1KiB-aligned. On cores that require the redirect table to ++ /// reside in DCCM (e.g. the Caliptra MCU integration), the address must ++ /// fall within DCCM. `None` leaves MEIVT unprogrammed, in which case ++ /// external interrupts must not be enabled. ++ const MEIVT_BASE_ADDRESS: Option = None; + } + + /// CLINT timer config.