Mctp Loopback Transport This adds a loopback transport for MCTP allowing MCTP testing and development on one device without needing a hardware transport. The proof-of-concept test sets up two MCTP servers, with the test application opening IPC connections to both and sending packets back and forth.
diff --git a/services/mctp/transport-loopback/BUILD.bazel b/services/mctp/transport-loopback/BUILD.bazel new file mode 100644 index 0000000..4ae3209 --- /dev/null +++ b/services/mctp/transport-loopback/BUILD.bazel
@@ -0,0 +1,31 @@ +# Licensed under the Apache-2.0 license + +load("@rules_rust//rust:defs.bzl", "rust_library", "rust_test") + +rust_library( + name = "mctp_transport_loopback", + srcs = glob(["src/**/*.rs"]), + crate_name = "openprot_mctp_transport_loopback", + edition = "2024", + visibility = ["//visibility:public"], + deps = [ + "//services/mctp/api:mctp_api", + "@rust_crates//:heapless", + "@rust_crates//:mctp", + "@rust_crates//:mctp-lib", + ], +) + +rust_test( + name = "mctp_transport_loopback_test", + srcs = glob(["tests/**/*.rs"]), + crate_root = "tests/loopback.rs", + edition = "2024", + deps = [ + ":mctp_transport_loopback", + "//services/mctp/api:mctp_api", + "//services/mctp/server:mctp_server_lib", + "@rust_crates//:mctp", + "@rust_crates//:mctp-lib", + ], +)
diff --git a/services/mctp/transport-loopback/Cargo.toml b/services/mctp/transport-loopback/Cargo.toml new file mode 100644 index 0000000..a5b67dd --- /dev/null +++ b/services/mctp/transport-loopback/Cargo.toml
@@ -0,0 +1,17 @@ +# Licensed under the Apache-2.0 license + +[package] +name = "openprot-mctp-transport-loopback" +version = "0.1.0" +edition = "2021" +description = "MCTP loopback transport binding for OpenPRoT" +license = "Apache-2.0" + +[dependencies] +openprot-mctp-api = { path = "../api" } +mctp-lib = { git = "https://github.com/9elements/mctp-lib.git", branch = "buildup", package = "mctp-lib" } +mctp = { git = "https://github.com/OpenPRoT/mctp-rs.git", branch = "sync-features", default-features = false } +heapless = { workspace = true } + +[dev-dependencies] +openprot-mctp-server = { path = "../server" }
diff --git a/services/mctp/transport-loopback/README.md b/services/mctp/transport-loopback/README.md new file mode 100644 index 0000000..f962010 --- /dev/null +++ b/services/mctp/transport-loopback/README.md
@@ -0,0 +1,70 @@ +# openprot-mctp-transport-loopback + +Loopback transport binding for the MCTP server, enabling direct server-to-server communication without physical transport. + +## Overview + +This crate implements MCTP loopback transport for testing and development. It provides a shared buffer mechanism that allows two MCTP servers to communicate directly, replacing the I2C transport layer with an in-memory packet exchange. + +This is particularly useful for: +- Unit testing MCTP protocols without hardware +- SPDM loopback configurations where requester and responder run on the same device +- Development and debugging of MCTP applications + +## Key Types + +- `LoopbackSender` — implements `mctp_lib::Sender` for outbound packets; writes to a shared buffer +- `LoopbackPair` — manages the bidirectional communication between two endpoints + +## Architecture + +Unlike I2C transport which requires encoding/decoding with headers and PEC, the loopback transport works with raw MCTP packets: + +``` +Server A → LoopbackSender → SharedBuffer → Server B.inbound() +Server B → LoopbackSender → SharedBuffer → Server A.inbound() +``` + +Each endpoint gets a `LoopbackSender` that writes to the peer's receive buffer. The application is responsible for polling and transferring packets between endpoints. + +## Usage + +```rust +use std::cell::RefCell; +use mctp::Eid; +use openprot_mctp_server::Server; +use openprot_mctp_transport_loopback::{LoopbackPair, LoopbackSender}; + +// Create a bidirectional loopback pair +let pair = LoopbackPair::new(); + +// Create senders for each endpoint +let sender_a = LoopbackSender::new(&pair.a_to_b); +let sender_b = LoopbackSender::new(&pair.b_to_a); + +// Create two MCTP servers +let server_a: RefCell<Server<_, 16>> = RefCell::new(Server::new(Eid(8), 0, sender_a)); +let server_b: RefCell<Server<_, 16>> = RefCell::new(Server::new(Eid(42), 0, sender_b)); + +// Send from A to B (using the MctpClient trait via a wrapper) +// ... + +// Transfer packets A→B +while let Some(pkt) = pair.pop_a_to_b() { + server_b.borrow_mut().inbound(&pkt).unwrap(); +} + +// Transfer packets B→A +while let Some(pkt) = pair.pop_b_to_a() { + server_a.borrow_mut().inbound(&pkt).unwrap(); +} +``` + +See `tests/loopback.rs` for complete examples including echo roundtrips. + +## Dependencies + +- `openprot-mctp-api` — API traits +- `mctp-lib` — `Sender` trait, fragmentation +- `mctp` — core MCTP types +- `heapless` — `no_std` collections
diff --git a/services/mctp/transport-loopback/src/lib.rs b/services/mctp/transport-loopback/src/lib.rs new file mode 100644 index 0000000..1c57064 --- /dev/null +++ b/services/mctp/transport-loopback/src/lib.rs
@@ -0,0 +1,27 @@ +// Licensed under the Apache-2.0 license + +//! # MCTP Loopback Transport Binding +//! +//! This crate provides a loopback transport binding for the MCTP server, +//! enabling direct server-to-server communication without physical transport. +//! +//! It implements [`mctp_lib::Sender`] for outbound MCTP packets and provides +//! a shared buffer mechanism for bidirectional packet exchange between two +//! MCTP endpoints. +//! +//! ## Design +//! +//! Unlike I2C transport which requires encoding/decoding with transport headers +//! and PEC, the loopback transport works with raw MCTP packets. This makes it +//! simpler and more efficient for in-memory communication. +//! +//! The [`LoopbackPair`] manages two unidirectional packet queues (A→B and B→A). +//! Each endpoint gets a [`LoopbackSender`] that writes to the appropriate queue. +//! The application polls the pair to transfer packets to the receiving server. + +#![no_std] +#![warn(missing_docs)] + +mod sender; + +pub use sender::{LoopbackPair, LoopbackSender};
diff --git a/services/mctp/transport-loopback/src/sender.rs b/services/mctp/transport-loopback/src/sender.rs new file mode 100644 index 0000000..76bce6c --- /dev/null +++ b/services/mctp/transport-loopback/src/sender.rs
@@ -0,0 +1,178 @@ +// Licensed under the Apache-2.0 license + +//! Loopback MCTP sender — in-memory transport binding. +//! +//! Provides a shared buffer mechanism for direct MCTP server-to-server +//! communication without physical transport encoding. + +use core::cell::RefCell; +use heapless::Vec as HVec; +use mctp::Result; +use mctp_lib::fragment::{Fragmenter, SendOutput}; + +/// Maximum packet size (MCTP baseline MTU). +const MAX_PACKET_SIZE: usize = 255; + +/// Maximum number of packets in flight per direction. +const MAX_QUEUE_DEPTH: usize = 16; + +/// A packet queue for one direction of loopback communication. +type PacketQueue = HVec<HVec<u8, MAX_PACKET_SIZE>, MAX_QUEUE_DEPTH>; + +/// Loopback MCTP sender. +/// +/// Implements `mctp_lib::Sender` to fragment and send MCTP packets +/// into a shared in-memory buffer. Each sender writes to a queue +/// that the peer endpoint reads from. +/// +/// Unlike I2C transport, this works with raw MCTP packets without +/// any transport-layer encoding. +/// +/// This struct uses interior mutability (RefCell) so it can be used +/// with the `Sender` trait which requires `&mut self`. +pub struct LoopbackSender<'a> { + /// Shared packet queue (packets going to the peer). + queue: &'a RefCell<PacketQueue>, +} + +impl<'a> LoopbackSender<'a> { + /// Create a new loopback sender writing to the given queue. + pub fn new(queue: &'a RefCell<PacketQueue>) -> Self { + Self { queue } + } +} + +impl mctp_lib::Sender for LoopbackSender<'_> { + fn send_vectored( + &mut self, + mut fragmenter: Fragmenter, + payload: &[&[u8]], + ) -> Result<mctp::Tag> { + loop { + let mut pkt = [0u8; MAX_PACKET_SIZE]; + match fragmenter.fragment_vectored(payload, &mut pkt) { + SendOutput::Packet(p) => { + // Push raw MCTP packet to queue + self.queue + .borrow_mut() + .push(HVec::from_slice(p).map_err(|_| mctp::Error::TxFailure)?) + .map_err(|_| mctp::Error::TxFailure)?; + } + SendOutput::Complete { tag, .. } => return Ok(tag), + SendOutput::Error { err, .. } => return Err(err), + } + } + } + + fn get_mtu(&self) -> usize { + MAX_PACKET_SIZE + } +} + +/// Bidirectional loopback packet manager. +/// +/// Manages two unidirectional packet queues for communication between +/// two MCTP endpoints (A and B). This is the shared state that both +/// senders write to and the application polls from. +/// +/// # Example +/// +/// ```ignore +/// use std::cell::RefCell; +/// use openprot_mctp_transport_loopback::{LoopbackPair, LoopbackSender}; +/// +/// let a_to_b = RefCell::new(Vec::new()); +/// let b_to_a = RefCell::new(Vec::new()); +/// +/// let sender_a = LoopbackSender::new(&a_to_b); +/// let sender_b = LoopbackSender::new(&b_to_a); +/// +/// let mut server_a = Server::new(Eid(8), 0, sender_a); +/// let mut server_b = Server::new(Eid(42), 0, sender_b); +/// +/// // A sends to B +/// server_a.send(...); +/// while !a_to_b.borrow().is_empty() { +/// let pkt = a_to_b.borrow_mut().swap_remove(0); +/// server_b.inbound(&pkt).unwrap(); +/// } +/// ``` +/// +/// For convenience, `LoopbackPair` provides a higher-level API that +/// manages both queues together. +pub struct LoopbackPair { + /// Packets from A to B. + pub a_to_b: RefCell<PacketQueue>, + /// Packets from B to A. + pub b_to_a: RefCell<PacketQueue>, +} + +impl LoopbackPair { + /// Create a new loopback pair. + pub fn new() -> Self { + Self { + a_to_b: RefCell::new(HVec::new()), + b_to_a: RefCell::new(HVec::new()), + } + } + + /// Pop the next packet from A's send queue (destined for B). + /// + /// Returns `None` if the queue is empty. + pub fn pop_a_to_b(&self) -> Option<HVec<u8, MAX_PACKET_SIZE>> { + let mut queue = self.a_to_b.borrow_mut(); + if queue.is_empty() { + None + } else { + Some(queue.swap_remove(0)) + } + } + + /// Pop the next packet from B's send queue (destined for A). + /// + /// Returns `None` if the queue is empty. + pub fn pop_b_to_a(&self) -> Option<HVec<u8, MAX_PACKET_SIZE>> { + let mut queue = self.b_to_a.borrow_mut(); + if queue.is_empty() { + None + } else { + Some(queue.swap_remove(0)) + } + } + + /// Clear all packets from A→B queue. + pub fn clear_a_to_b(&self) { + self.a_to_b.borrow_mut().clear(); + } + + /// Clear all packets from B→A queue. + pub fn clear_b_to_a(&self) { + self.b_to_a.borrow_mut().clear(); + } + + /// Get the number of packets pending A→B. + pub fn len_a_to_b(&self) -> usize { + self.a_to_b.borrow().len() + } + + /// Get the number of packets pending B→A. + pub fn len_b_to_a(&self) -> usize { + self.b_to_a.borrow().len() + } + + /// Check if A→B queue is empty. + pub fn is_empty_a_to_b(&self) -> bool { + self.a_to_b.borrow().is_empty() + } + + /// Check if B→A queue is empty. + pub fn is_empty_b_to_a(&self) -> bool { + self.b_to_a.borrow().is_empty() + } +} + +impl Default for LoopbackPair { + fn default() -> Self { + Self::new() + } +}
diff --git a/services/mctp/transport-loopback/tests/loopback.rs b/services/mctp/transport-loopback/tests/loopback.rs new file mode 100644 index 0000000..bb280ef --- /dev/null +++ b/services/mctp/transport-loopback/tests/loopback.rs
@@ -0,0 +1,248 @@ +// Licensed under the Apache-2.0 license + +//! MCTP loopback transport integration test. +//! +//! Verifies that two MCTP servers can communicate through the loopback +//! transport without any physical transport encoding. + +use std::cell::RefCell; + +use mctp::Eid; +use openprot_mctp_api::{Handle, MctpClient, MctpError, RecvMetadata, ResponseCode}; +use openprot_mctp_server::Server; +use openprot_mctp_transport_loopback::{LoopbackPair, LoopbackSender}; + +// --------------------------------------------------------------------------- +// Client-side wrapper (same as echo test) +// --------------------------------------------------------------------------- + +struct DirectClient<'a, S: mctp_lib::Sender, const N: usize> { + server: &'a RefCell<Server<S, N>>, +} + +impl<'a, S: mctp_lib::Sender, const N: usize> DirectClient<'a, S, N> { + fn new(server: &'a RefCell<Server<S, N>>) -> Self { + Self { server } + } +} + +impl<S: mctp_lib::Sender, const N: usize> MctpClient for DirectClient<'_, S, N> { + fn req(&self, eid: u8) -> Result<Handle, MctpError> { + self.server.borrow_mut().req(eid) + } + + fn listener(&self, msg_type: u8) -> Result<Handle, MctpError> { + self.server.borrow_mut().listener(msg_type) + } + + fn get_eid(&self) -> u8 { + self.server.borrow().get_eid() + } + + fn set_eid(&self, eid: u8) -> Result<(), MctpError> { + self.server.borrow_mut().set_eid(eid) + } + + fn recv( + &self, + handle: Handle, + _timeout_millis: u32, + buf: &mut [u8], + ) -> Result<RecvMetadata, MctpError> { + self.server + .borrow_mut() + .try_recv(handle, buf) + .ok_or(MctpError::from_code(ResponseCode::TimedOut)) + } + + fn send( + &self, + handle: Option<Handle>, + msg_type: u8, + eid: Option<u8>, + tag: Option<u8>, + integrity_check: bool, + buf: &[u8], + ) -> Result<u8, MctpError> { + self.server + .borrow_mut() + .send(handle, msg_type, eid, tag, integrity_check, buf) + } + + fn drop_handle(&self, handle: Handle) { + let _ = self.server.borrow_mut().unbind(handle); + } +} + +// --------------------------------------------------------------------------- +// Echo application logic (same as echo.rs) +// --------------------------------------------------------------------------- + +fn echo_once(client: &impl MctpClient, listener_handle: Handle) { + let mut recv_buf = [0u8; 255]; + let meta = client + .recv(listener_handle, 0, &mut recv_buf) + .expect("echo: should receive a message"); + + let payload = &recv_buf[..meta.payload_size]; + client + .send( + None, + meta.msg_type, + Some(meta.remote_eid), + Some(meta.msg_tag), + false, + payload, + ) + .expect("echo: should send response"); +} + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +/// Basic loopback: send a message from A to B and verify receipt. +#[test] +fn loopback_simple_send() { + let pair = LoopbackPair::new(); + let sender_a = LoopbackSender::new(&pair.a_to_b); + let sender_b = LoopbackSender::new(&pair.b_to_a); + + let server_a: RefCell<Server<_, 16>> = RefCell::new(Server::new(Eid(8), 0, sender_a)); + let server_b: RefCell<Server<_, 16>> = RefCell::new(Server::new(Eid(42), 0, sender_b)); + + let client_a = DirectClient::new(&server_a); + let client_b = DirectClient::new(&server_b); + + // B registers a listener for type 1 + let listener_b = client_b.listener(1).unwrap(); + + // A gets a request handle to send to EID 42 + let req_a = client_a.req(42).unwrap(); + + // A sends a message + let payload = b"Hello from A!"; + client_a + .send(Some(req_a), 1, None, None, false, payload) + .unwrap(); + + // Transfer packets A→B + while let Some(pkt) = pair.pop_a_to_b() { + server_b.borrow_mut().inbound(&pkt).unwrap(); + } + + // B receives the message + let mut recv_buf = [0u8; 255]; + let meta = client_b + .recv(listener_b, 0, &mut recv_buf) + .expect("B should receive message"); + + let received = &recv_buf[..meta.payload_size]; + assert_eq!(received, payload); + assert_eq!(meta.msg_type, 1); + assert_eq!(meta.remote_eid, 8); + + client_a.drop_handle(req_a); + client_b.drop_handle(listener_b); +} + +/// MCTP echo roundtrip through loopback transport. +/// +/// Server A listens for type-1 messages and echoes them. +/// Server B sends a request and verifies the echo response. +#[test] +fn loopback_echo_roundtrip() { + let pair = LoopbackPair::new(); + let sender_a = LoopbackSender::new(&pair.a_to_b); + let sender_b = LoopbackSender::new(&pair.b_to_a); + + let server_a: RefCell<Server<_, 16>> = RefCell::new(Server::new(Eid(8), 0, sender_a)); + let server_b: RefCell<Server<_, 16>> = RefCell::new(Server::new(Eid(42), 0, sender_b)); + + let client_a = DirectClient::new(&server_a); + let client_b = DirectClient::new(&server_b); + + // A registers listener for type 1 (echo responder) + let listener_a = client_a.listener(1).unwrap(); + + // B gets request handle to send to EID 8 + let req_b = client_b.req(8).unwrap(); + + // B sends a request + let payload = b"Hello MCTP loopback!"; + let _tag = client_b + .send(Some(req_b), 1, None, None, false, payload) + .unwrap(); + + // Transfer B→A + while let Some(pkt) = pair.pop_b_to_a() { + server_a.borrow_mut().inbound(&pkt).unwrap(); + } + + // A echoes the message + echo_once(&client_a, listener_a); + + // Transfer A→B + while let Some(pkt) = pair.pop_a_to_b() { + server_b.borrow_mut().inbound(&pkt).unwrap(); + } + + // B receives the echo response + let mut resp_buf = [0u8; 255]; + let resp_meta = client_b + .recv(req_b, 0, &mut resp_buf) + .expect("B should receive echo response"); + + let response = &resp_buf[..resp_meta.payload_size]; + assert_eq!(response, payload, "Echo response should match original"); + assert_eq!(resp_meta.msg_type, 1); + assert_eq!(resp_meta.remote_eid, 8); + + client_a.drop_handle(listener_a); + client_b.drop_handle(req_b); +} + +/// Multiple echo roundtrips. +#[test] +fn loopback_echo_multiple() { + let pair = LoopbackPair::new(); + let sender_a = LoopbackSender::new(&pair.a_to_b); + let sender_b = LoopbackSender::new(&pair.b_to_a); + + let server_a: RefCell<Server<_, 16>> = RefCell::new(Server::new(Eid(8), 0, sender_a)); + let server_b: RefCell<Server<_, 16>> = RefCell::new(Server::new(Eid(42), 0, sender_b)); + + let client_a = DirectClient::new(&server_a); + let client_b = DirectClient::new(&server_b); + + let listener = client_a.listener(1).unwrap(); + let req = client_b.req(8).unwrap(); + + for i in 0..5u8 { + let msg = [i; 32]; + + // B sends request + client_b + .send(Some(req), 1, None, None, false, &msg) + .unwrap(); + while let Some(pkt) = pair.pop_b_to_a() { + server_a.borrow_mut().inbound(&pkt).unwrap(); + } + + // A echoes + echo_once(&client_a, listener); + while let Some(pkt) = pair.pop_a_to_b() { + server_b.borrow_mut().inbound(&pkt).unwrap(); + } + + // B verifies echo + let mut resp_buf = [0u8; 255]; + let resp = client_b + .recv(req, 0, &mut resp_buf) + .unwrap_or_else(|_| panic!("iteration {i}: no response")); + assert_eq!(&resp_buf[..resp.payload_size], &msg); + } + + client_a.drop_handle(listener); + client_b.drop_handle(req); +}
diff --git a/target/ast1060-evb/mctp/BUILD.bazel b/target/ast1060-evb/mctp/BUILD.bazel index f813178..20508de 100644 --- a/target/ast1060-evb/mctp/BUILD.bazel +++ b/target/ast1060-evb/mctp/BUILD.bazel
@@ -8,6 +8,109 @@ load("//target/ast1060-evb:defs.bzl", "TARGET_COMPATIBLE_WITH") load("//target:uart_boot_image.bzl", "uart_boot_image") +# =========================================================================== +# MCTP Loopback Test (no I2C, in-process dual servers) +# =========================================================================== + +rust_binary( + name = "mctp_loopback_test", + srcs = ["mctp_loopback_test.rs"], + edition = "2024", + tags = ["kernel"], + deps = [ + ":app_mctp_loopback_test", + "//services/mctp/api:mctp_api", + "//services/mctp/server:mctp_server_lib", + "//services/mctp/transport-loopback:mctp_transport_loopback", + "@pigweed//pw_kernel/syscall:syscall_user", + "@pigweed//pw_kernel/userspace", + "@pigweed//pw_log/rust:pw_log", + "@pigweed//pw_status/rust:pw_status", + "@rust_crates//:mctp", + "@rust_crates//:mctp-lib", + ], +) + +app_package( + name = "app_mctp_loopback_test", + app_name = "mctp_loopback_test", + edition = "2024", + system_config = ":system_config_loopback", + tags = ["kernel"], +) + +system_image( + name = "mctp_loopback", + apps = [ + ":mctp_loopback_test", + ], + kernel = ":target_loopback", + platform = "//target/ast1060-evb", + system_config = ":system_config_loopback", + tags = ["kernel"], + target_compatible_with = TARGET_COMPATIBLE_WITH, + visibility = ["//visibility:public"], +) + +system_image_test( + name = "mctp_loopback_test_run", + image = ":mctp_loopback", + target_compatible_with = TARGET_COMPATIBLE_WITH, +) + +uart_boot_image( + name = "mctp_loopback_uart", + src = ":mctp_loopback", + out = "mctp_loopback_uart.bin", +) + +filegroup( + name = "system_config_loopback", + srcs = ["system_loopback.json5"], + visibility = ["//visibility:public"], +) + +target_codegen( + name = "codegen_loopback", + arch = "@pigweed//pw_kernel/arch/arm_cortex_m:arch_arm_cortex_m", + system_config = ":system_config_loopback", + target_compatible_with = TARGET_COMPATIBLE_WITH, +) + +target_linker_script( + name = "linker_script_loopback", + system_config = ":system_config_loopback", + tags = ["kernel"], + target_compatible_with = TARGET_COMPATIBLE_WITH, + template = "//target/ast1060-evb:linker_script_template", +) + +rust_binary( + name = "target_loopback", + srcs = [ + "target_loopback.rs", + ], + edition = "2024", + tags = ["kernel"], + target_compatible_with = TARGET_COMPATIBLE_WITH, + deps = [ + ":codegen_loopback", + ":linker_script_loopback", + "//target/ast1060-evb:console_backend_uart", + "//target/ast1060-evb:entry", + "@pigweed//pw_kernel/arch/arm_cortex_m:arch_arm_cortex_m", + "@pigweed//pw_kernel/kernel", + "@pigweed//pw_kernel/target:target_common", + "@pigweed//pw_kernel/userspace", + "@pigweed//pw_log/rust:pw_log", + "@rust_crates//:cortex-m-semihosting", + ], +) + +# =========================================================================== +# MCTP Echo (Original - with I2C server) +# =========================================================================== + # --------------------------------------------------------------------------- # MCTP echo binary (userspace process) # ---------------------------------------------------------------------------
diff --git a/target/ast1060-evb/mctp/mctp_loopback_test.rs b/target/ast1060-evb/mctp/mctp_loopback_test.rs new file mode 100644 index 0000000..4960ed3 --- /dev/null +++ b/target/ast1060-evb/mctp/mctp_loopback_test.rs
@@ -0,0 +1,259 @@ +// Licensed under the Apache-2.0 license + +//! MCTP Loopback Test Application +//! +//! This application demonstrates MCTP communication using loopback transport. +//! It creates two MCTP servers (A and B) within the same process, connected +//! via loopback transport, and verifies bidirectional message exchange. +//! +//! # Architecture +//! +//! ```text +//! ┌─────────────────────────────────────────────┐ +//! │ mctp_loopback_test (single process) │ +//! │ │ +//! │ ┌─ Server A (EID 8) ──┐ │ +//! │ │ Router │ │ +//! │ │ Listener(type=1) │ │ +//! │ └──────┬───────────────┘ │ +//! │ │ │ +//! │ │ LoopbackSender(A→B) │ +//! │ ▼ │ +//! │ [ LoopbackPair ] │ +//! │ │ │ +//! │ │ LoopbackSender(B→A) │ +//! │ ▼ │ +//! │ ┌─ Server B (EID 42) ─┐ │ +//! │ │ Router │ │ +//! │ │ Request handle │ │ +//! │ └──────────────────────┘ │ +//! └─────────────────────────────────────────────┘ +//! ``` +//! +//! # Test Sequence +//! +//! 1. Server A registers a listener for message type 1 +//! 2. Server B sends a request to EID 8 with type 1 +//! 3. Transfer packets B→A through loopback pair +//! 4. Server A receives the message on the listener +//! 5. Server A echoes the message back +//! 6. Transfer packets A→B through loopback pair +//! 7. Server B receives the echo response +//! 8. Verify payload matches + +#![no_main] +#![no_std] + +use core::cell::RefCell; + +use mctp::Eid; +use openprot_mctp_api::{Handle, MctpClient, MctpError, RecvMetadata, ResponseCode}; +use openprot_mctp_server::Server; +use openprot_mctp_transport_loopback::{LoopbackPair, LoopbackSender}; + +use pw_status::Result; +use userspace::entry; +use userspace::syscall; + +// --------------------------------------------------------------------------- +// DirectClient wrapper - provides MctpClient trait for in-process server +// --------------------------------------------------------------------------- + +struct DirectClient<'a, S: mctp_lib::Sender, const N: usize> { + server: &'a RefCell<Server<S, N>>, +} + +impl<'a, S: mctp_lib::Sender, const N: usize> DirectClient<'a, S, N> { + fn new(server: &'a RefCell<Server<S, N>>) -> Self { + Self { server } + } +} + +impl<S: mctp_lib::Sender, const N: usize> MctpClient for DirectClient<'_, S, N> { + fn req(&self, eid: u8) -> core::result::Result<Handle, MctpError> { + self.server.borrow_mut().req(eid) + } + + fn listener(&self, msg_type: u8) -> core::result::Result<Handle, MctpError> { + self.server.borrow_mut().listener(msg_type) + } + + fn get_eid(&self) -> u8 { + self.server.borrow().get_eid() + } + + fn set_eid(&self, eid: u8) -> core::result::Result<(), MctpError> { + self.server.borrow_mut().set_eid(eid) + } + + fn recv( + &self, + handle: Handle, + _timeout_millis: u32, + buf: &mut [u8], + ) -> core::result::Result<RecvMetadata, MctpError> { + self.server + .borrow_mut() + .try_recv(handle, buf) + .ok_or(MctpError::from_code(ResponseCode::TimedOut)) + } + + fn send( + &self, + handle: Option<Handle>, + msg_type: u8, + eid: Option<u8>, + tag: Option<u8>, + integrity_check: bool, + buf: &[u8], + ) -> core::result::Result<u8, MctpError> { + self.server + .borrow_mut() + .send(handle, msg_type, eid, tag, integrity_check, buf) + } + + fn drop_handle(&self, handle: Handle) { + let _ = self.server.borrow_mut().unbind(handle); + } +} + +// --------------------------------------------------------------------------- +// Test logic +// --------------------------------------------------------------------------- + +fn echo_once(client: &impl MctpClient, listener_handle: Handle) -> core::result::Result<(), MctpError> { + let mut recv_buf = [0u8; 255]; + let meta = client.recv(listener_handle, 0, &mut recv_buf)?; + + pw_log::info!( + "Echo: received {} bytes from EID {}", + meta.payload_size as u32, + meta.remote_eid as u32 + ); + + let payload = &recv_buf[..meta.payload_size]; + client.send( + None, + meta.msg_type, + Some(meta.remote_eid), + Some(meta.msg_tag), + false, + payload, + )?; + + Ok(()) +} + +fn run_loopback_test() -> Result<()> { + pw_log::info!("MCTP Loopback Test starting"); + + // Create loopback pair + let pair = LoopbackPair::new(); + let sender_a = LoopbackSender::new(&pair.a_to_b); + let sender_b = LoopbackSender::new(&pair.b_to_a); + + // Create two MCTP servers + let server_a: RefCell<Server<_, 16>> = RefCell::new(Server::new(Eid(8), 0, sender_a)); + let server_b: RefCell<Server<_, 16>> = RefCell::new(Server::new(Eid(42), 0, sender_b)); + + let client_a = DirectClient::new(&server_a); + let client_b = DirectClient::new(&server_b); + + pw_log::info!("Created Server A (EID 8) and Server B (EID 42)"); + + // A registers listener for type 1 (echo responder) + let listener_a = client_a + .listener(1) + .map_err(|_| pw_status::Error::Internal)?; + pw_log::info!("Server A: registered listener for type 1"); + + // B gets request handle to send to EID 8 + let req_b = client_b + .req(8) + .map_err(|_| pw_status::Error::Internal)?; + pw_log::info!("Server B: got request handle for EID 8"); + + // Run multiple echo roundtrips + for iteration in 0..5u8 { + let msg = [iteration; 32]; + + pw_log::info!("=== Iteration {} ===", iteration as u32); + + // B sends request + pw_log::info!("Server B: sending request"); + client_b + .send(Some(req_b), 1, None, None, false, &msg) + .map_err(|_| pw_status::Error::Internal)?; + + // Transfer B→A + let mut count = 0; + while let Some(pkt) = pair.pop_b_to_a() { + server_a.borrow_mut().inbound(&pkt).map_err(|_| pw_status::Error::Internal)?; + count += 1; + } + pw_log::info!("Transferred {} packets B→A", count as u32); + + // A echoes + pw_log::info!("Server A: echoing message"); + echo_once(&client_a, listener_a).map_err(|_| pw_status::Error::Internal)?; + + // Transfer A→B + count = 0; + while let Some(pkt) = pair.pop_a_to_b() { + server_b.borrow_mut().inbound(&pkt).map_err(|_| pw_status::Error::Internal)?; + count += 1; + } + pw_log::info!("Transferred {} packets A→B", count as u32); + + // B receives echo response + let mut resp_buf = [0u8; 255]; + let resp = client_b + .recv(req_b, 0, &mut resp_buf) + .map_err(|_| pw_status::Error::Internal)?; + + pw_log::info!( + "Server B: received response ({} bytes)", + resp.payload_size as u32 + ); + + // Verify payload + let response = &resp_buf[..resp.payload_size]; + if response != &msg[..] { + pw_log::error!("Payload mismatch!"); + return Err(pw_status::Error::DataLoss); + } + + pw_log::info!("✓ Iteration {} passed", iteration as u32); + } + + // Cleanup + client_a.drop_handle(listener_a); + client_b.drop_handle(req_b); + + pw_log::info!("=== All tests PASSED ==="); + Ok(()) +} + +// --------------------------------------------------------------------------- +// Entry point +// --------------------------------------------------------------------------- + +#[entry] +fn entry() -> ! { + match run_loopback_test() { + Ok(()) => { + pw_log::info!("MCTP loopback test completed successfully"); + let _ = syscall::debug_shutdown(Ok(())); + } + Err(e) => { + pw_log::error!("MCTP loopback test failed: {}", e as u32); + let _ = syscall::debug_shutdown(Err(e)); + } + } + loop {} +} + +#[panic_handler] +fn panic(_info: &core::panic::PanicInfo) -> ! { + loop {} +}
diff --git a/target/ast1060-evb/mctp/system.json5 b/target/ast1060-evb/mctp/system.json5 index 86c2abd..1b97419 100644 --- a/target/ast1060-evb/mctp/system.json5 +++ b/target/ast1060-evb/mctp/system.json5
@@ -5,28 +5,28 @@ // 768KB SRAM (640KB usable), executes from RAM // // Memory Layout (PMSAv7-friendly, power-of-2 aligned regions): -// 0x00000000 - 0x00000500: Vector table + kernel annotations (1280 bytes) -// 0x00000500 - 0x00020000: Kernel flash (~127KB, ends at 128KB boundary) +// 0x00000000 - 0x00000680: Vector table + kernel annotations (1664 bytes) +// 0x00000680 - 0x00020000: Kernel flash (~126KB, ends at 128KB boundary) // 0x00020000 - 0x00040000: i2c_server flash (128KB) // 0x00040000 - 0x00060000: mctp_server flash (128KB) // 0x00060000 - 0x00080000: mctp_echo flash (128KB) -// 0x00080000 - 0x00088000: i2c_server RAM (32KB) -// 0x00088000 - 0x00090000: mctp_server RAM (32KB) -// 0x00090000 - 0x00098000: mctp_echo RAM (32KB) -// 0x00098000 - 0x000A0000: Kernel RAM (32KB) +// 0x00080000 - 0x000A0000: Kernel RAM (128KB) +// 0x000A0000 - 0x000B0000: i2c_server RAM (64KB) +// 0x000B0000 - 0x000B8000: mctp_server RAM (32KB) +// 0x000B8000 - 0x000C0000: mctp_echo RAM (32KB) // -// Total: 0xA0000 = 640KB (fits within the 640KB usable window of the AST1060's 768KB SRAM). +// Total: 0xC0000 = 768KB (uses all of the AST1060's 768KB SRAM). { arch: { type: "armv7m", vector_table_start_address: 0x00000000, - vector_table_size_bytes: 1280, // 0x500 (272 vectors + thread/stack annotations) + vector_table_size_bytes: 1664, // 0x680 (includes vector table + annotations) }, kernel: { - flash_start_address: 0x00000500, // After vector table + annotations - flash_size_bytes: 129792, // 0x1FB00: ends at exactly 0x20000 (128KB boundary) - ram_start_address: 0x00098000, // After all app RAM (3 × 32KB = 0x18000 past 0x80000) - ram_size_bytes: 32768, // 32KB + flash_start_address: 0x00000680, // After vector table + annotations + flash_size_bytes: 129408, // ~126KB (ends at 0x00020000, power-of-2 boundary) + ram_start_address: 0x00080000, // After all flash regions + ram_size_bytes: 131072, // 128KB }, apps: [ // ──── I2C Server ──── @@ -35,7 +35,7 @@ { name: "i2c_server", flash_size_bytes: 131072, // 128KB for server code - ram_size_bytes: 32768, // 32KB RAM (register maps + buffers) + ram_size_bytes: 65536, // 64KB RAM (register maps + buffers) process: { name: "i2c server process", objects: [
diff --git a/target/ast1060-evb/mctp/system_loopback.json5 b/target/ast1060-evb/mctp/system_loopback.json5 new file mode 100644 index 0000000..8b52656 --- /dev/null +++ b/target/ast1060-evb/mctp/system_loopback.json5
@@ -0,0 +1,47 @@ +// Licensed under the Apache-2.0 license + +// AST1060-EVB MCTP Loopback Test Configuration +// ARM Cortex-M4 @ 200 MHz +// 768KB SRAM (640KB usable), executes from RAM +// +// Memory Layout (PMSAv7-friendly, power-of-2 aligned regions): +// 0x00000000 - 0x00000680: Vector table + kernel annotations (1664 bytes) +// 0x00000680 - 0x00020000: Kernel flash (~126KB, ends at 128KB boundary) +// 0x00020000 - 0x00040000: mctp_loopback_test flash (128KB) +// 0x00040000 - 0x00080000: Kernel RAM (256KB) +// 0x00080000 - 0x000A0000: mctp_loopback_test RAM (128KB) +// +// Total: 0xA0000 = 640KB +{ + arch: { + type: "armv7m", + vector_table_start_address: 0x00000000, + vector_table_size_bytes: 1664, // 0x680 (includes vector table + annotations) + }, + kernel: { + flash_start_address: 0x00000680, // After vector table + annotations + flash_size_bytes: 129408, // ~126KB (ends at 0x00020000, power-of-2 boundary) + ram_start_address: 0x00040000, // After flash regions + ram_size_bytes: 262144, // 256KB + }, + apps: [ + // ──── MCTP Loopback Test ──── + // Single application that manages two MCTP servers internally + // using loopback transport for bidirectional communication. + { + name: "mctp_loopback_test", + flash_size_bytes: 131072, // 128KB for test code + ram_size_bytes: 131072, // 128KB RAM (two servers + buffers) + process: { + name: "mctp loopback test process", + objects: [], // No IPC - everything is in-process + threads: [ + { + name: "mctp loopback test thread", + stack_size_bytes: 8192, // 8KB stack + }, + ], + }, + }, + ], +}
diff --git a/target/ast1060-evb/mctp/target.rs b/target/ast1060-evb/mctp/target.rs index 3cb0a2e..931afa5 100644 --- a/target/ast1060-evb/mctp/target.rs +++ b/target/ast1060-evb/mctp/target.rs
@@ -8,7 +8,6 @@ #![no_std] #![no_main] -use cortex_m_semihosting::debug::{EXIT_FAILURE, EXIT_SUCCESS, exit}; use target_common::{TargetInterface, declare_target}; use {console_backend as _, entry as _}; @@ -25,11 +24,6 @@ fn shutdown(code: u32) -> ! { pw_log::info!("Shutting down with code {}", code as u32); - let status = match code { - 0 => EXIT_SUCCESS, - _ => EXIT_FAILURE, - }; - exit(status); #[expect(clippy::empty_loop)] loop {} }
diff --git a/target/ast1060-evb/mctp/target_loopback.rs b/target/ast1060-evb/mctp/target_loopback.rs new file mode 100644 index 0000000..a784768 --- /dev/null +++ b/target/ast1060-evb/mctp/target_loopback.rs
@@ -0,0 +1,32 @@ +// Licensed under the Apache-2.0 license + +//! AST1060-EVB MCTP Loopback Test Target +//! +//! This target runs a single test application that manages two MCTP +//! servers internally using loopback transport (no I2C required). + +#![no_std] +#![no_main] + +use target_common::{TargetInterface, declare_target}; +use {console_backend as _, entry as _}; + +pub struct Target {} + +impl TargetInterface for Target { + const NAME: &'static str = "AST1060-EVB MCTP Loopback Test"; + + fn main() -> ! { + codegen_loopback::start(); + #[expect(clippy::empty_loop)] + loop {} + } + + fn shutdown(code: u32) -> ! { + pw_log::info!("Shutting down with code {}", code as u32); + #[expect(clippy::empty_loop)] + loop {} + } +} + +declare_target!(Target);