mctp: Add I2C MCTP throughput test Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
diff --git a/target/ast10x0/tests/mctp/throughput/BUILD.bazel b/target/ast10x0/tests/mctp/throughput/BUILD.bazel new file mode 100644 index 0000000..e1b8070 --- /dev/null +++ b/target/ast10x0/tests/mctp/throughput/BUILD.bazel
@@ -0,0 +1,191 @@ +# Licensed under the Apache-2.0 license +# SPDX-License-Identifier: Apache-2.0 + +load("@pigweed//pw_kernel/tooling:rust_app.bzl", "rust_app") +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("@rules_rust//rust:defs.bzl", "rust_binary") +load("//target/ast10x0:defs.bzl", "TARGET_COMPATIBLE_WITH", "system_image_test") + +# MCTP-over-I2C throughput between two physical AST1060 EVBs (Bus 2 wired +# bus-to-bus), using the echo apps. Card A runs a passive echo responder; card B +# (the "requester" image) runs the requester that times round trips. +# +# On each card the I2C driver and the MCTP server live in ONE process +# (`mctp_i2c_server`), so MCTP packets reach the bus without an I2C IPC hop. +# The echo app is a separate process talking to that server over IPC. + +filegroup( + name = "system_config", + srcs = ["system.json5"], + visibility = ["//visibility:public"], +) + +filegroup( + name = "requester_system_config", + srcs = ["requester_system.json5"], + visibility = ["//visibility:public"], +) + +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: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_kernel/userspace", + ], +) + +SERVER_DEPS = [ + "//services/i2c/api:i2c_api", + "//services/mctp/api:mctp_api", + "//services/mctp/server:mctp_server_lib", + "//services/mctp/transport-i2c:mctp_transport_i2c", + "//target/ast10x0/backend/i2c:i2c_backend_ast10x0", + "//target/ast10x0/peripherals", + "@pigweed//pw_kernel/syscall:syscall_user", + "@pigweed//pw_kernel/userspace", + "@pigweed//pw_log/rust:pw_log", + "@pigweed//pw_status/rust:pw_status", + "@rust_crates//:embedded-hal", + "@rust_crates//:mctp", + "@rust_crates//:mctp-lib", +] + +rust_app( + name = "mctp_i2c_server", + srcs = [ + "server_common.rs", + "server_main.rs", + ], + codegen_crate_name = "app_mctp_i2c_server", + crate_root = "server_main.rs", + edition = "2024", + system_config = ":system_config", + tags = ["kernel"], + target_compatible_with = TARGET_COMPATIBLE_WITH, + deps = SERVER_DEPS, +) + +rust_app( + name = "mctp_i2c_server_peer", + srcs = [ + "server_common.rs", + "server_main_peer.rs", + ], + codegen_crate_name = "app_mctp_i2c_server_peer", + crate_root = "server_main_peer.rs", + edition = "2024", + system_config = ":requester_system_config", + tags = ["kernel"], + target_compatible_with = TARGET_COMPATIBLE_WITH, + deps = SERVER_DEPS, +) + +rust_app( + name = "mctp_echo_client", + srcs = ["echo_responder_main.rs"], + codegen_crate_name = "app_mctp_echo_client", + edition = "2024", + system_config = ":system_config", + tags = ["kernel"], + target_compatible_with = TARGET_COMPATIBLE_WITH, + deps = [ + "//services/mctp/api:mctp_api", + "//services/mctp/client-ipc:mctp_client_ipc", + "//services/mctp/echo:mctp_echo", + "@pigweed//pw_kernel/userspace", + "@pigweed//pw_log/rust:pw_log", + ], +) + +rust_app( + name = "mctp_echo_client_peer", + srcs = ["throughput_main.rs"], + codegen_crate_name = "app_mctp_echo_client_peer", + edition = "2024", + system_config = ":requester_system_config", + tags = ["kernel"], + target_compatible_with = TARGET_COMPATIBLE_WITH, + deps = [ + "//services/mctp/api:mctp_api", + "//services/mctp/client-ipc:mctp_client_ipc", + "//services/mctp/echo:mctp_echo", + "@pigweed//pw_kernel/userspace", + "@pigweed//pw_log/rust:pw_log", + "@pigweed//pw_status/rust:pw_status", + ], +) + +system_image( + name = "throughput_responder_image", + apps = [ + ":mctp_i2c_server", + ":mctp_echo_client", + ], + kernel = ":target", + platform = "//target/ast10x0", + system_config = ":system_config", + tags = ["kernel"], + target_compatible_with = TARGET_COMPATIBLE_WITH, + visibility = ["//visibility:public"], +) + +system_image( + name = "throughput_requester_image", + apps = [ + ":mctp_i2c_server_peer", + ":mctp_echo_client_peer", + ], + kernel = ":target", + platform = "//target/ast10x0", + system_config = ":requester_system_config", + tags = ["kernel"], + target_compatible_with = TARGET_COMPATIBLE_WITH, + visibility = ["//visibility:public"], +) + +# `slave_image` is flashed first so the responder is listening before the +# requester boots. Passes when the requester completes every round trip. +system_image_test( + name = "mctp_throughput_test", + timeout = "long", + image = ":throughput_requester_image", + slave_image = ":throughput_responder_image", + tags = [ + "hardware", + "manual", + ], + target_compatible_with = select({ + "//target/ast10x0:qemu_enabled": ["@platforms//:incompatible"], + "//conditions:default": [], + }), +)
diff --git a/target/ast10x0/tests/mctp/throughput/echo_responder_main.rs b/target/ast10x0/tests/mctp/throughput/echo_responder_main.rs new file mode 100644 index 0000000..1157a62 --- /dev/null +++ b/target/ast10x0/tests/mctp/throughput/echo_responder_main.rs
@@ -0,0 +1,50 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! Echo responder for the MCTP-over-I2C throughput test (card A, EID 8). +//! +//! Passive: listens for echo messages and reflects each payload back to the +//! sender. It never originates traffic, so the requester's measurements are +//! not perturbed by responder-initiated sends. + +#![no_main] +#![no_std] + +use app_mctp_echo_client::handle; +use openprot_mctp_api::wire::MAX_PAYLOAD_SIZE; +use openprot_mctp_api::Stack; +use openprot_mctp_client_ipc::IpcMctpClient; +use openprot_mctp_echo::{echo_once, prepare_listener_with_eid_and_timeout}; +use userspace::{entry, syscall}; + +const ECHO_EID: u8 = 8; +// 0 = block until a request arrives. +const LISTEN_TIMEOUT_MS: u32 = 0; + +#[entry] +fn entry() { + let stack = Stack::new(IpcMctpClient::new(handle::MCTP)); + let mut listener = + match prepare_listener_with_eid_and_timeout(&stack, ECHO_EID, LISTEN_TIMEOUT_MS) { + Ok(listener) => listener, + Err(e) => { + pw_log::error!("echo responder setup failed: code={}", e.code as u32); + syscall::process_exit(1); + } + }; + + pw_log::info!("echo responder ready, EID {}", ECHO_EID as u32); + let mut buf = [0u8; MAX_PAYLOAD_SIZE]; + loop { + if let Err(e) = echo_once(&mut listener, &mut buf) { + if !e.is_timeout() { + pw_log::error!("echo responder failed: code={}", e.code as u32); + } + } + } +} + +#[panic_handler] +fn panic(_info: &core::panic::PanicInfo) -> ! { + loop {} +}
diff --git a/target/ast10x0/tests/mctp/throughput/requester_system.json5 b/target/ast10x0/tests/mctp/throughput/requester_system.json5 new file mode 100644 index 0000000..10c8b84 --- /dev/null +++ b/target/ast10x0/tests/mctp/throughput/requester_system.json5
@@ -0,0 +1,88 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +// AST10x0 MCTP throughput test (requester, card B) +// +// The I2C driver and MCTP server share one process (mctp_i2c_server_peer); the +// echo app talks to it over a single MCTP IPC channel. +{ + arch: { + type: "armv7m", + vector_table_start_address: 0x00000000, + vector_table_size_bytes: 1792, + }, + kernel: { + flash_start_address: 0x00000700, + flash_size_bytes: 129280, + ram_start_address: 0x00060000, + ram_size_bytes: 131072, + }, + apps: [ + { + name: "mctp_i2c_server_peer", + flash_size_bytes: 131072, + processes: [ + { + name: "mctp_i2c_server_peer_process", + ram_size_bytes: 131072, + objects: [ + { + name: "wg", + type: "wait_group", + }, + { + name: "mctp", + type: "channel_handler", + }, + { + name: "i2c2_irq", + type: "interrupt", + irqs: [ + { + name: "i2c2", + number: 112, + }, + ], + }, + { + type: "thread", + name: "mctp_i2c_server_peer_thread", + kernel_stack_size_bytes: 4096, + }, + ], + memory_mappings: [ + { + name: "i2c_regs", + type: "device", + start_address: 0x7e7b0000, + size_bytes: 0x4000, + }, + ], + }, + ], + }, + { + name: "mctp_echo_client_peer", + flash_size_bytes: 16384, + processes: [ + { + name: "mctp_echo_client_peer_process", + ram_size_bytes: 32768, + objects: [ + { + name: "mctp", + type: "channel_initiator", + handler_process: "mctp_i2c_server_peer_process", + handler_object_name: "mctp", + }, + { + type: "thread", + name: "mctp_echo_client_peer_thread", + kernel_stack_size_bytes: 2048, + }, + ], + }, + ], + }, + ], +}
diff --git a/target/ast10x0/tests/mctp/throughput/server_common.rs b/target/ast10x0/tests/mctp/throughput/server_common.rs new file mode 100644 index 0000000..6e6a1d3 --- /dev/null +++ b/target/ast10x0/tests/mctp/throughput/server_common.rs
@@ -0,0 +1,259 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +// Shared body of the combined I2C + MCTP server, `include!`d by `server_main.rs` +// and `server_main_peer.rs`; each defines `handle`, `signals`, the EIDs and the +// I2C addresses before including it. +// +// The I2C driver for Bus 2 lives in this process, so the MCTP stack drives it +// directly (no I2C IPC hop): outbound packets go through `SharedBus`, inbound +// packets are drained from the slave RX buffer on the I2C IRQ. + +use core::cell::RefCell; + +use ast10x0_peripherals::create_pins; +use ast10x0_peripherals::i2c::{ClockConfig, I2cConfig, I2cSpeed, I2cXferMode}; +use embedded_hal::i2c::{ErrorType, I2c, Operation, SevenBitAddress}; +use i2c_api::seam::{I2cIsrEvent, I2cSlaveBuffer, I2cSlaveCore, I2cSlaveEvent}; +use openprot_mctp_api::wire::{ + self, MctpOp, MctpRequestHeader, MctpResponseHeader, MAX_PAYLOAD_SIZE, MAX_REQUEST_SIZE, + MAX_RESPONSE_SIZE, +}; +use openprot_mctp_api::{Handle, ResponseCode}; +use openprot_mctp_server::dispatch::{self, DispatchOutcome}; +use openprot_mctp_transport_i2c::{I2cSender, MctpI2cReceiver}; +use pw_status::{Error, Result}; +use userspace::entry; +use userspace::syscall::{self, Signals}; +use userspace::time::{Clock, Duration, Instant, SystemClock}; + +const SLAVE_CFG: I2cConfig = I2cConfig { + speed: I2cSpeed::Standard, + xfer_mode: I2cXferMode::DmaMode, + multi_master: false, + smbus_timeout: false, + smbus_alert: false, + clock_config: ClockConfig::ast1060_default(), +}; + +/// Lets the MCTP sender (master writes) and the IRQ handler (slave reads) +/// share the one bus driver. Single-threaded, so `RefCell` never contends. +struct SharedBus<'a, B>(&'a RefCell<B>); + +impl<B: ErrorType> ErrorType for SharedBus<'_, B> { + type Error = B::Error; +} + +impl<B: I2c<SevenBitAddress>> I2c<SevenBitAddress> for SharedBus<'_, B> { + fn transaction( + &mut self, + address: SevenBitAddress, + operations: &mut [Operation<'_>], + ) -> core::result::Result<(), Self::Error> { + self.0.borrow_mut().transaction(address, operations) + } +} + +fn respond_error(code: ResponseCode, response_buf: &mut [u8]) -> Result<()> { + response_buf[..MctpResponseHeader::SIZE] + .copy_from_slice(&MctpResponseHeader::error(code).to_bytes()); + syscall::channel_respond(handle::MCTP, &response_buf[..MctpResponseHeader::SIZE]) +} + +fn respond_recv( + meta: &openprot_mctp_api::RecvMetadata, + recv_buf: &[u8], + response_buf: &mut [u8], +) -> Result<()> { + let len = wire::encode_recv_response( + response_buf, + meta.msg_type, + meta.msg_ic, + meta.remote_eid, + meta.msg_tag, + &recv_buf[..meta.payload_size], + ) + .unwrap_or_else(|_| { + wire::encode_error_response(response_buf, ResponseCode::InternalError).unwrap_or(0) + }); + syscall::channel_respond(handle::MCTP, &response_buf[..len]) +} + +fn server_loop() -> Result<()> { + pw_log::info!("MCTP+I2C server starting"); + + // The kernel routed Bus 2's pins at the SCU before starting us; userspace has no SCU grant, so + // we bind the already-routed pins rather than re-muxing them. + // SAFETY: sole pin creation site in this binary, at boot; the pins! table is this chip's true pin map. + let pins = unsafe { create_pins() }; + let (scl, sda) = (pins.scu418_0, pins.scu418_1); + let (Some(master_dma_buf), Some(slave_dma_buf)) = ( + i2c_backend::non_cached_buf!(4096), + i2c_backend::non_cached_buf!(512), + ) else { + pw_log::error!("i2c DMA buffers already taken"); + return Err(Error::Internal); + }; + let Ok(mut driver) = + i2c_backend::open_bus_dma(scl, sda, &SLAVE_CFG, master_dma_buf, slave_dma_buf) + else { + pw_log::error!("i2c bus open failed"); + return Err(Error::Internal); + }; + if driver.configure_slave_address(OWN_I2C_ADDR).is_err() || driver.enable_slave_mode().is_err() + { + pw_log::error!("i2c slave setup failed"); + return Err(Error::Internal); + } + let bus = RefCell::new(driver); + + let sender = I2cSender::new(SharedBus(&bus), OWN_I2C_ADDR, REMOTE_I2C_ADDR); + let i2c_receiver = MctpI2cReceiver::new(OWN_I2C_ADDR); + let mut server = openprot_mctp_server::Server::<_, 16>::new(mctp::Eid(OWN_EID), 0, sender); + + let mut request_buf = [0u8; MAX_REQUEST_SIZE]; + let mut response_buf = [0u8; MAX_RESPONSE_SIZE]; + let mut recv_buf = [0u8; MAX_PAYLOAD_SIZE]; + let mut i2c_rx_buf = [0u8; MAX_PAYLOAD_SIZE]; + + // A blocking recv whose IPC reply is deferred until a packet arrives or + // its deadline passes. + struct PendingRecv { + handle: Handle, + deadline: Instant, + } + let mut pending_recv: Option<PendingRecv> = None; + + // user_data=0 → MCTP client channel READABLE, user_data=1 → I2C IRQ. + syscall::wait_group_add(handle::WG, handle::MCTP, Signals::READABLE, 0usize)?; + syscall::wait_group_add(handle::WG, handle::I2C2_IRQ, signals::I2C2, 1usize)?; + + loop { + let wait_deadline = pending_recv + .as_ref() + .map_or(Instant::MAX, |pending| pending.deadline); + let ev = match syscall::object_wait( + handle::WG, + Signals::READABLE | signals::I2C2, + wait_deadline, + ) { + Ok(ev) => ev, + Err(Error::DeadlineExceeded) => { + if pending_recv.take().is_some() { + let _ = respond_error(ResponseCode::TimedOut, &mut response_buf); + let _ = syscall::wait_group_add( + handle::WG, + handle::MCTP, + Signals::READABLE, + 0usize, + ); + } + continue; + } + Err(err) => return Err(err), + }; + + if ev.user_data == 1 { + // Slave IRQ: drain a received packet, ack, feed the router. + let event = bus.borrow_mut().try_next_slave_event(); + if let Ok(Some((I2cIsrEvent::SlaveWrRecvd, _))) = event { + match bus.borrow_mut().read_slave_buffer(&mut i2c_rx_buf) { + Ok(n) if n > 0 => match i2c_receiver.decode(&i2c_rx_buf[..n]) { + Ok((pkt, _)) => { + let _ = server.inbound(pkt); + } + Err(_) => pw_log::error!("i2c frame decode failed"), + }, + Ok(_) => {} + Err(_) => pw_log::error!("read_slave_buffer failed"), + } + } + if syscall::interrupt_ack(handle::I2C2_IRQ, ev.pending_signals & signals::I2C2) + .is_err() + { + pw_log::error!("interrupt_ack failed"); + } + // Satisfy any deferred blocking recv now that a packet was processed. + if let Some(pending) = pending_recv.as_ref() { + if let Some(meta) = server.try_recv(pending.handle, &mut recv_buf) { + respond_recv(&meta, &recv_buf, &mut response_buf)?; + pending_recv = None; + syscall::wait_group_add(handle::WG, handle::MCTP, Signals::READABLE, 0usize)?; + } + } + continue; + } + + // IPC from a client — non-blocking, the WaitGroup fired on READABLE. + let len = syscall::channel_read(handle::MCTP, 0, &mut request_buf)?; + if pending_recv.is_some() { + // A blocking recv is in flight; reject so READABLE clears. + respond_error(ResponseCode::InternalError, &mut response_buf)?; + continue; + } + if len < MctpRequestHeader::SIZE { + respond_error(ResponseCode::BadArgument, &mut response_buf)?; + continue; + } + + let header = MctpRequestHeader::from_bytes(&request_buf[..len]); + if header + .as_ref() + .and_then(|h| h.operation()) + .is_some_and(|op| matches!(op, MctpOp::Recv)) + { + let recv_handle = Handle(header.map_or(0, |h| h.handle)); + let payload = wire::get_request_payload(&request_buf[..len]); + if payload.len() < 4 { + respond_error(ResponseCode::BadArgument, &mut response_buf)?; + continue; + } + let timeout_millis = u32::from_le_bytes([payload[0], payload[1], payload[2], payload[3]]); + match server.try_recv(recv_handle, &mut recv_buf) { + Some(meta) => respond_recv(&meta, &recv_buf, &mut response_buf)?, + None => { + let deadline = if timeout_millis == 0 { + Instant::MAX + } else { + SystemClock::now() + .checked_add_duration(Duration::from_millis(timeout_millis as u64)) + .unwrap_or(Instant::MAX) + }; + pending_recv = Some(PendingRecv { + handle: recv_handle, + deadline, + }); + // Remove MCTP from the WaitGroup so READABLE on the open + // transaction can't re-fire it. + let _ = syscall::wait_group_remove(handle::WG, handle::MCTP); + } + } + } else { + let response_len = match dispatch::dispatch_mctp_op( + &request_buf[..len], + &mut response_buf, + &mut server, + &mut recv_buf, + 0, + ) { + DispatchOutcome::Reply(n) => n, + DispatchOutcome::Pending { .. } => unreachable!("Recv handled above"), + }; + syscall::channel_respond(handle::MCTP, &response_buf[..response_len])?; + } + } +} + +#[entry] +fn entry() { + if let Err(e) = server_loop() { + pw_log::error!("mctp_i2c_server exiting with error"); + let _ = syscall::process_exit(e as u32); + } + loop {} +} + +#[panic_handler] +fn panic(_info: &core::panic::PanicInfo) -> ! { + loop {} +}
diff --git a/target/ast10x0/tests/mctp/throughput/server_main.rs b/target/ast10x0/tests/mctp/throughput/server_main.rs new file mode 100644 index 0000000..53439da --- /dev/null +++ b/target/ast10x0/tests/mctp/throughput/server_main.rs
@@ -0,0 +1,15 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! Combined I2C + MCTP server for the throughput test (responder, card A). + +#![no_main] +#![no_std] + +use app_mctp_i2c_server::{handle, signals}; + +const OWN_EID: u8 = 8; +const OWN_I2C_ADDR: u8 = 0x10; +const REMOTE_I2C_ADDR: u8 = 0x42; + +include!("server_common.rs");
diff --git a/target/ast10x0/tests/mctp/throughput/server_main_peer.rs b/target/ast10x0/tests/mctp/throughput/server_main_peer.rs new file mode 100644 index 0000000..c54afbc --- /dev/null +++ b/target/ast10x0/tests/mctp/throughput/server_main_peer.rs
@@ -0,0 +1,15 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! Combined I2C + MCTP server for the throughput test (requester, card B). + +#![no_main] +#![no_std] + +use app_mctp_i2c_server_peer::{handle, signals}; + +const OWN_EID: u8 = 9; +const OWN_I2C_ADDR: u8 = 0x42; +const REMOTE_I2C_ADDR: u8 = 0x10; + +include!("server_common.rs");
diff --git a/target/ast10x0/tests/mctp/throughput/system.json5 b/target/ast10x0/tests/mctp/throughput/system.json5 new file mode 100644 index 0000000..e7937e9 --- /dev/null +++ b/target/ast10x0/tests/mctp/throughput/system.json5
@@ -0,0 +1,88 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +// AST10x0 MCTP throughput test (responder, card A) +// +// The I2C driver and MCTP server share one process (mctp_i2c_server); the +// echo app talks to it over a single MCTP IPC channel. +{ + arch: { + type: "armv7m", + vector_table_start_address: 0x00000000, + vector_table_size_bytes: 1792, + }, + kernel: { + flash_start_address: 0x00000700, + flash_size_bytes: 129280, + ram_start_address: 0x00060000, + ram_size_bytes: 131072, + }, + apps: [ + { + name: "mctp_i2c_server", + flash_size_bytes: 131072, + processes: [ + { + name: "mctp_i2c_server_process", + ram_size_bytes: 131072, + objects: [ + { + name: "wg", + type: "wait_group", + }, + { + name: "mctp", + type: "channel_handler", + }, + { + name: "i2c2_irq", + type: "interrupt", + irqs: [ + { + name: "i2c2", + number: 112, + }, + ], + }, + { + type: "thread", + name: "mctp_i2c_server_thread", + kernel_stack_size_bytes: 4096, + }, + ], + memory_mappings: [ + { + name: "i2c_regs", + type: "device", + start_address: 0x7e7b0000, + size_bytes: 0x4000, + }, + ], + }, + ], + }, + { + name: "mctp_echo_client", + flash_size_bytes: 16384, + processes: [ + { + name: "mctp_echo_client_process", + ram_size_bytes: 32768, + objects: [ + { + name: "mctp", + type: "channel_initiator", + handler_process: "mctp_i2c_server_process", + handler_object_name: "mctp", + }, + { + type: "thread", + name: "mctp_echo_client_thread", + kernel_stack_size_bytes: 2048, + }, + ], + }, + ], + }, + ], +}
diff --git a/target/ast10x0/tests/mctp/throughput/target.rs b/target/ast10x0/tests/mctp/throughput/target.rs new file mode 100644 index 0000000..c543aff --- /dev/null +++ b/target/ast10x0/tests/mctp/throughput/target.rs
@@ -0,0 +1,49 @@ +// 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::scu::pinctrl; +use console_backend::console_backend_write_all; +use entry as _; +use target_common::{declare_target, TargetInterface}; + +pub struct Target; + +static PINCTRL_GROUPS: [&[ast10x0_peripherals::scu::PinctrlPin]; 1] = [pinctrl::PINCTRL_I2C2]; + +impl TargetInterface for Target { + const NAME: &'static str = "AST10x0 MCTP Throughput"; + + fn main() -> ! { + // SAFETY: kernel main() runs once with exclusive hardware ownership. + if unsafe { + Ast10x0Board::new(Ast10x0BoardDescriptor { + pinctrl_groups: &PINCTRL_GROUPS, + }) + .init() + } + .is_err() + { + loop {} + } + + codegen::start(); + loop {} + } + + fn shutdown(code: u32) -> ! { + let sentinel: &[u8] = if code == 0 { + b"TEST_RESULT:PASS\n" + } else { + 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/mctp/throughput/throughput_main.rs b/target/ast10x0/tests/mctp/throughput/throughput_main.rs new file mode 100644 index 0000000..447cd74 --- /dev/null +++ b/target/ast10x0/tests/mctp/throughput/throughput_main.rs
@@ -0,0 +1,107 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! MCTP-over-I2C throughput requester (card B, EID 9). +//! +//! Sends `ITERS` echo requests per payload size to the responder at EID 8, +//! verifies each echoed payload, and logs round-trip rate and goodput. Ends +//! the test with PASS only if every round trip completed with matching data. +//! +//! Goodput counts payload bytes in both directions (request + echo). + +#![no_main] +#![no_std] + +use app_mctp_echo_client_peer::handle; +use openprot_mctp_api::wire::MAX_PAYLOAD_SIZE; +use openprot_mctp_api::{MctpReqChannel, Stack}; +use openprot_mctp_client_ipc::IpcMctpClient; +use openprot_mctp_echo::ECHO_MSG_TYPE; +use pw_status::Error; +use userspace::time::{Clock, SystemClock}; +use userspace::{entry, syscall}; + +const OWN_EID: u8 = 9; +const RESPONDER_EID: u8 = 8; +const REQ_TIMEOUT_MS: u32 = 1000; +const WARMUP_TIMEOUT_MS: u32 = 10_000; +const ITERS: u32 = 100; +const PAYLOAD_SIZES: [usize; 6] = [1, 32, 64, 128, 255, 1023]; + +fn now_micros() -> u64 { + SystemClock::now().ticks() * 1_000_000 / SystemClock::TICKS_PER_SEC +} + +/// One echo round trip; `true` if the response matched the request. +fn round_trip(stack: &Stack<IpcMctpClient>, tx: &[u8], rx: &mut [u8], timeout: u32) -> bool { + let Ok(mut req) = stack.req(RESPONDER_EID, timeout) else { + return false; + }; + if req.send(ECHO_MSG_TYPE, tx).is_err() { + return false; + } + matches!(req.recv(rx), Ok((_, echoed)) if echoed == tx) +} + +fn fail() -> ! { + let _ = syscall::debug_shutdown(Err(Error::Internal)); + loop {} +} + +#[entry] +fn entry() { + let stack = Stack::new(IpcMctpClient::new(handle::MCTP)); + if let Err(e) = stack.set_eid(OWN_EID) { + pw_log::error!("set_eid failed: code={}", e.code as u32); + fail(); + } + + let mut tx = [0u8; MAX_PAYLOAD_SIZE]; + let mut rx = [0u8; MAX_PAYLOAD_SIZE]; + for (i, b) in tx.iter_mut().enumerate() { + *b = i as u8; + } + + // The responder may boot after us; retry until the first echo succeeds. + pw_log::info!("waiting for responder at EID {}", RESPONDER_EID as u32); + while !round_trip(&stack, &tx[..1], &mut rx, WARMUP_TIMEOUT_MS) {} + + let mut total_errors = 0u32; + for &size in PAYLOAD_SIZES.iter() { + let mut errors = 0u32; + let start = now_micros(); + for _ in 0..ITERS { + if !round_trip(&stack, &tx[..size], &mut rx, REQ_TIMEOUT_MS) { + errors += 1; + } + } + let elapsed_us = now_micros().saturating_sub(start).max(1); + let ok = (ITERS - errors) as u64; + let rt_per_sec_x100 = ok * 100_000_000 / elapsed_us; + let bytes_per_sec = ok * 2 * size as u64 * 1_000_000 / elapsed_us; + pw_log::info!( + "THROUGHPUT size={} ok={} err={} elapsed_us={} rt_per_sec_x100={} bytes_per_sec={}", + size as u32, + ok as u32, + errors as u32, + elapsed_us as u32, + rt_per_sec_x100 as u32, + bytes_per_sec as u32 + ); + total_errors += errors; + } + + if total_errors == 0 { + pw_log::info!("throughput test complete"); + let _ = syscall::debug_shutdown(Ok(())); + } else { + pw_log::error!("throughput test: {} failed round trips", total_errors as u32); + fail(); + } + loop {} +} + +#[panic_handler] +fn panic(_info: &core::panic::PanicInfo) -> ! { + loop {} +}