mctp: add IpcMctpClient crate and QEMU IPC wire-protocol test)
diff --git a/services/mctp/BUILD.bazel b/services/mctp/BUILD.bazel index 688e62e..9c01d3e 100644 --- a/services/mctp/BUILD.bazel +++ b/services/mctp/BUILD.bazel
@@ -8,6 +8,8 @@ name = "mctp_embedded_all", srcs = [ "//services/mctp/api:mctp_api", + # Kernel-only crate (depends on pw_kernel userspace; not host-testable). + "//services/mctp/client-ipc:mctp_client_ipc", "//services/mctp/echo:mctp_echo", "//services/mctp/server:mctp_server_lib", ],
diff --git a/services/mctp/client-ipc/BUILD.bazel b/services/mctp/client-ipc/BUILD.bazel new file mode 100644 index 0000000..06f23f7 --- /dev/null +++ b/services/mctp/client-ipc/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") + +rust_library( + name = "mctp_client_ipc", + srcs = ["src/lib.rs"], + crate_name = "openprot_mctp_client_ipc", + edition = "2024", + tags = ["kernel"], + visibility = ["//visibility:public"], + deps = [ + "//services/mctp/api:mctp_api", + "@pigweed//pw_kernel/userspace", + ], +)
diff --git a/services/mctp/client-ipc/src/lib.rs b/services/mctp/client-ipc/src/lib.rs new file mode 100644 index 0000000..cadc388 --- /dev/null +++ b/services/mctp/client-ipc/src/lib.rs
@@ -0,0 +1,217 @@ +// Licensed under the Apache-2.0 license + +//! MCTP IPC Client +//! +//! Provides an `MctpClient` implementation that communicates with the MCTP +//! server over a Pigweed IPC channel, using the wire protocol from +//! `openprot-mctp-api`. +//! +//! ## Usage +//! +//! ```rust,ignore +//! use openprot_mctp_client_ipc::IpcMctpClient; +//! use openprot_mctp_api::MctpClient; +//! +//! let client = IpcMctpClient::new(handle::MCTP); +//! +//! client.set_eid(8).unwrap(); +//! let listener = client.listener(1).unwrap(); +//! let meta = client.recv(listener, 0, &mut buf).unwrap(); +//! ``` + +#![no_std] +#![warn(missing_docs)] + +use core::cell::RefCell; + +use openprot_mctp_api::wire::{self, MctpResponseHeader, MAX_REQUEST_SIZE, MAX_RESPONSE_SIZE}; +use openprot_mctp_api::{Handle, MctpClient, MctpError, RecvMetadata, ResponseCode}; + +/// Internal mutable state for the IPC client. +struct ClientBuffers { + request_buf: [u8; MAX_REQUEST_SIZE], + response_buf: [u8; MAX_RESPONSE_SIZE], +} + +/// MCTP client that communicates with the MCTP server over Pigweed IPC. +/// +/// Uses `RefCell` for interior mutability so that `MctpClient` trait +/// methods (which take `&self`) can mutate the internal IPC buffers. +/// This keeps the caller-facing API simple while enabling efficient +/// buffer reuse across synchronous IPC transactions. +pub struct IpcMctpClient { + handle: u32, + inner: RefCell<ClientBuffers>, +} + +impl IpcMctpClient { + /// Create a new IPC MCTP client bound to the given channel handle. + /// + /// The handle comes from the application's `app_package`-generated + /// handle module (e.g., `handle::MCTP`). + pub fn new(handle: u32) -> Self { + Self { + handle, + inner: RefCell::new(ClientBuffers { + request_buf: [0u8; MAX_REQUEST_SIZE], + response_buf: [0u8; MAX_RESPONSE_SIZE], + }), + } + } + + /// Get the IPC channel handle. + pub fn channel_handle(&self) -> u32 { + self.handle + } + + /// Encode, send, and decode a transaction. + fn transact(&self, req_len: usize) -> Result<(MctpResponseHeader, usize), MctpError> { + let resp_len = self.send_recv(req_len)?; + let inner = self.inner.borrow(); + + if resp_len < MctpResponseHeader::SIZE || resp_len > MAX_RESPONSE_SIZE { + return Err(MctpError::from_code(ResponseCode::InternalError)); + } + + let header = wire::decode_response_header(&inner.response_buf[..resp_len]) + .map_err(|_| MctpError::from_code(ResponseCode::InternalError))?; + + if !header.is_success() { + return Err(MctpError::from_code(header.response_code())); + } + + Ok((header, resp_len)) + } + + fn send_recv(&self, req_len: usize) -> Result<usize, MctpError> { + if req_len > MAX_REQUEST_SIZE { + return Err(MctpError::from_code(ResponseCode::InternalError)); + } + let req_copy = { + let inner = self.inner.borrow(); + let mut buf = [0u8; MAX_REQUEST_SIZE]; + buf[..req_len].copy_from_slice(&inner.request_buf[..req_len]); + buf + }; + let mut inner = self.inner.borrow_mut(); + userspace::syscall::channel_transact( + self.handle, + &req_copy[..req_len], + &mut inner.response_buf, + userspace::time::Instant::MAX, + ) + .map_err(|_| MctpError::from_code(ResponseCode::InternalError)) + } +} + +impl MctpClient for IpcMctpClient { + fn req(&self, eid: u8) -> Result<Handle, MctpError> { + let req_len = { + let mut inner = self.inner.borrow_mut(); + wire::encode_req(&mut inner.request_buf, eid) + .map_err(|_| MctpError::from_code(ResponseCode::InternalError))? + }; + let (header, _) = self.transact(req_len)?; + Ok(Handle(header.handle)) + } + + fn listener(&self, msg_type: u8) -> Result<Handle, MctpError> { + let req_len = { + let mut inner = self.inner.borrow_mut(); + wire::encode_listener(&mut inner.request_buf, msg_type) + .map_err(|_| MctpError::from_code(ResponseCode::InternalError))? + }; + let (header, _) = self.transact(req_len)?; + Ok(Handle(header.handle)) + } + + fn get_eid(&self) -> u8 { + let req_len = { + let mut inner = self.inner.borrow_mut(); + match wire::encode_get_eid(&mut inner.request_buf) { + Ok(len) => len, + Err(_) => return 0, + } + }; + match self.transact(req_len) { + Ok((header, _)) => header.eid, + Err(_) => 0, + } + } + + fn set_eid(&self, eid: u8) -> Result<(), MctpError> { + let req_len = { + let mut inner = self.inner.borrow_mut(); + wire::encode_set_eid(&mut inner.request_buf, eid) + .map_err(|_| MctpError::from_code(ResponseCode::InternalError))? + }; + self.transact(req_len)?; + Ok(()) + } + + fn recv( + &self, + handle: Handle, + timeout_millis: u32, + buf: &mut [u8], + ) -> Result<RecvMetadata, MctpError> { + let req_len = { + let mut inner = self.inner.borrow_mut(); + wire::encode_recv(&mut inner.request_buf, handle.0, timeout_millis) + .map_err(|_| MctpError::from_code(ResponseCode::InternalError))? + }; + let (header, resp_len) = self.transact(req_len)?; + + let inner = self.inner.borrow(); + let payload = wire::get_response_payload(&inner.response_buf[..resp_len], &header) + .map_err(|_| MctpError::from_code(ResponseCode::InternalError))?; + + let copy_len = core::cmp::min(payload.len(), buf.len()); + buf[..copy_len].copy_from_slice(&payload[..copy_len]); + + Ok(RecvMetadata { + msg_type: header.msg_type, + msg_ic: header.flags & wire::flags::IC != 0, + msg_tag: header.tag, + remote_eid: header.eid, + payload_size: payload.len(), + }) + } + + fn send( + &self, + handle: Option<Handle>, + msg_type: u8, + eid: Option<u8>, + tag: Option<u8>, + integrity_check: bool, + buf: &[u8], + ) -> Result<u8, MctpError> { + let req_len = { + let mut inner = self.inner.borrow_mut(); + wire::encode_send( + &mut inner.request_buf, + handle.map(|h| h.0), + msg_type, + eid, + tag, + integrity_check, + buf, + ) + .map_err(|_| MctpError::from_code(ResponseCode::InternalError))? + }; + let (header, _) = self.transact(req_len)?; + Ok(header.tag) + } + + fn drop_handle(&self, handle: Handle) { + let req_len = { + let mut inner = self.inner.borrow_mut(); + match wire::encode_unbind(&mut inner.request_buf, handle.0) { + Ok(len) => len, + Err(_) => return, + } + }; + let _ = self.transact(req_len); + } +}
diff --git a/target/ast10x0/tests/mctp/ipc_client/BUILD.bazel b/target/ast10x0/tests/mctp/ipc_client/BUILD.bazel new file mode 100644 index 0000000..2ac7521 --- /dev/null +++ b/target/ast10x0/tests/mctp/ipc_client/BUILD.bazel
@@ -0,0 +1,122 @@ +# 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", "system_image_test") +load("@pigweed//pw_kernel/tooling:target_codegen.bzl", "target_codegen") +load("@pigweed//pw_kernel/tooling:target_linker_script.bzl", "target_linker_script") +load("@pigweed//pw_kernel/tooling/panic_detector:rust_binary_no_panics_test.bzl", "rust_binary_no_panics_test") +load("@rules_rust//rust:defs.bzl", "rust_binary") +load("//target/ast10x0:defs.bzl", "TARGET_COMPATIBLE_WITH") + +# ── System configuration ─────────────────────────────────────────────────────── + +filegroup( + name = "system_config", + srcs = ["system.json5"], +) + +# ── Kernel image ─────────────────────────────────────────────────────────────── + +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", + "@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", + "@pigweed//pw_log/rust:pw_log", + ], +) + +# ── Fake MCTP server ─────────────────────────────────────────────────────────── +# Speaks the openprot_mctp_api::wire protocol. No transport dependency. + +rust_app( + name = "fake_server", + srcs = ["fake_server_main.rs"], + codegen_crate_name = "app_fake_server", + edition = "2024", + system_config = ":system_config", + tags = ["kernel"], + target_compatible_with = TARGET_COMPATIBLE_WITH, + deps = [ + "//services/mctp/api:mctp_api", + "@pigweed//pw_kernel/userspace", + "@pigweed//pw_log/rust:pw_log", + ], +) + +# ── Client test app ──────────────────────────────────────────────────────────── +# Exercises IpcMctpClient; calls debug_shutdown(Ok|Err) to report result. + +rust_app( + name = "client_test", + srcs = ["client_main.rs"], + codegen_crate_name = "app_client_test", + 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", + "@pigweed//pw_kernel/userspace", + "@pigweed//pw_log/rust:pw_log", + "@pigweed//pw_status/rust:pw_status", + ], +) + +# ── System image ─────────────────────────────────────────────────────────────── + +system_image( + name = "ipc_client_image", + apps = [ + ":fake_server", + ":client_test", + ], + kernel = ":target", + platform = "//target/ast10x0", + system_config = ":system_config", + tags = ["kernel"], + target_compatible_with = TARGET_COMPATIBLE_WITH, + visibility = ["//visibility:public"], +) + +# ── Test target ──────────────────────────────────────────────────────────────── +# Run with: bazel test --config=virt_ast10x0 //target/ast10x0/tests/mctp/ipc_client:ipc_client_qemu_test + +system_image_test( + name = "ipc_client_qemu_test", + image = ":ipc_client_image", + target_compatible_with = TARGET_COMPATIBLE_WITH, +) + +rust_binary_no_panics_test( + name = "no_panics_test", + binary = ":ipc_client_image", + tags = ["kernel"], +)
diff --git a/target/ast10x0/tests/mctp/ipc_client/client_main.rs b/target/ast10x0/tests/mctp/ipc_client/client_main.rs new file mode 100644 index 0000000..ebfa00c --- /dev/null +++ b/target/ast10x0/tests/mctp/ipc_client/client_main.rs
@@ -0,0 +1,176 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! MCTP IPC-client exerciser for the QEMU test. +//! +//! Runs ten test cases against `fake_server` via `IpcMctpClient` and calls +//! `debug_shutdown(Ok(()))` on full pass or `debug_shutdown(Err(_))` on the +//! first failure. The kernel target writes `TEST_RESULT:PASS/FAIL` to UART. +//! +//! ## Test cases +//! +//! | TC | Method | Expected result | +//! |------|-----------------|------------------------------------------| +//! | TC-01 | set_eid(8) | Ok(()) | +//! | TC-02 | get_eid() | returns 8 | +//! | TC-03 | listener(5) | Ok(Handle(42)) | +//! | TC-04 | req(8) | Ok(Handle(43)) | +//! | TC-05 | recv(h42, …) | Ok(meta); msg_type=5, eid=8, | +//! | | | payload=[DE AD BE EF] | +//! | TC-06 | send(…, b"hi") | Ok(1) (tag) | +//! | TC-07 | drop_handle(h43)| no panic | +//! | TC-08 | listener(0xFE) | Err(BadArgument) | +//! | TC-09 | recv(h0, …) | Err(TimedOut) | +//! | TC-10 | recv(h0xFEFE,…)| Err(InternalError) (truncated response) | + +#![no_main] +#![no_std] + +use openprot_mctp_api::{Handle, MctpClient, ResponseCode}; +use openprot_mctp_client_ipc::IpcMctpClient; +use pw_status::Error; +use userspace::{entry, syscall}; + +use app_client_test::handle; + +#[entry] +fn entry() { + match run() { + Ok(()) => { + pw_log::info!("All test cases PASSED"); + let _ = syscall::debug_shutdown(Ok(())); + } + Err(()) => { + let _ = syscall::debug_shutdown(Err(Error::Internal)); + } + } + loop {} +} + +fn run() -> Result<(), ()> { + let client = IpcMctpClient::new(handle::MCTP); + + // ── TC-01: set_eid ──────────────────────────────────────────────────────── + pw_log::info!("TC-01: set_eid(8)"); + client.set_eid(8).map_err(|e| { + pw_log::error!("TC-01 FAIL: code={}", e.code as u32); + })?; + + // ── TC-02: get_eid ──────────────────────────────────────────────────────── + pw_log::info!("TC-02: get_eid()"); + let eid = client.get_eid(); + if eid != 8 { + pw_log::error!("TC-02 FAIL: expected 8, got {}", eid as u32); + return Err(()); + } + + // ── TC-03: listener ─────────────────────────────────────────────────────── + pw_log::info!("TC-03: listener(5)"); + let listener_handle = client.listener(5).map_err(|e| { + pw_log::error!("TC-03 FAIL: code={}", e.code as u32); + })?; + if listener_handle.0 != 42 { + pw_log::error!("TC-03 FAIL: expected handle 42, got {}", listener_handle.0 as u32); + return Err(()); + } + + // ── TC-04: req ──────────────────────────────────────────────────────────── + pw_log::info!("TC-04: req(8)"); + let req_handle = client.req(8).map_err(|e| { + pw_log::error!("TC-04 FAIL: code={}", e.code as u32); + })?; + if req_handle.0 != 43 { + pw_log::error!("TC-04 FAIL: expected handle 43, got {}", req_handle.0 as u32); + return Err(()); + } + + // ── TC-05: recv — success with payload ─────────────────────────────────── + pw_log::info!("TC-05: recv(listener_handle)"); + let mut recv_buf = [0u8; 16]; + let meta = client + .recv(listener_handle, 0, &mut recv_buf) + .map_err(|e| { + pw_log::error!("TC-05 FAIL: code={}", e.code as u32); + })?; + if meta.msg_type != 5 { + pw_log::error!("TC-05 FAIL: msg_type expected 5, got {}", meta.msg_type as u32); + return Err(()); + } + if meta.remote_eid != 8 { + pw_log::error!("TC-05 FAIL: remote_eid expected 8, got {}", meta.remote_eid as u32); + return Err(()); + } + if meta.payload_size != 4 { + pw_log::error!("TC-05 FAIL: payload_size expected 4, got {}", meta.payload_size as u32); + return Err(()); + } + if recv_buf[..4] != [0xDE, 0xAD, 0xBE, 0xEF] { + pw_log::error!("TC-05 FAIL: payload mismatch"); + return Err(()); + } + + // ── TC-06: send ─────────────────────────────────────────────────────────── + pw_log::info!("TC-06: send()"); + let tag = client + .send(None, 5, Some(8), None, false, b"hi") + .map_err(|e| { + pw_log::error!("TC-06 FAIL: code={}", e.code as u32); + })?; + if tag != 1 { + pw_log::error!("TC-06 FAIL: expected tag 1, got {}", tag as u32); + return Err(()); + } + + // ── TC-07: drop_handle ─────────────────────────────────────────────────── + pw_log::info!("TC-07: drop_handle(req_handle)"); + client.drop_handle(req_handle); + + // ── TC-08: error response — BadArgument ────────────────────────────────── + pw_log::info!("TC-08: listener(0xFE) should return BadArgument"); + match client.listener(0xFE) { + Err(e) if e.code == ResponseCode::BadArgument => {} + Err(e) => { + pw_log::error!("TC-08 FAIL: expected BadArgument, got {}", e.code as u32); + return Err(()); + } + Ok(_) => { + pw_log::error!("TC-08 FAIL: expected Err, got Ok"); + return Err(()); + } + } + + // ── TC-09: TimedOut response ───────────────────────────────────────────── + pw_log::info!("TC-09: recv(Handle(0)) should return TimedOut"); + match client.recv(Handle(0), 1000, &mut recv_buf) { + Err(e) if e.code == ResponseCode::TimedOut => {} + Err(e) => { + pw_log::error!("TC-09 FAIL: expected TimedOut, got {}", e.code as u32); + return Err(()); + } + Ok(_) => { + pw_log::error!("TC-09 FAIL: expected Err, got Ok"); + return Err(()); + } + } + + // ── TC-10: short (truncated) response — InternalError ──────────────────── + pw_log::info!("TC-10: recv(Handle(0xFEFE)) should return InternalError"); + match client.recv(Handle(0xFEFE), 1000, &mut recv_buf) { + Err(e) if e.code == ResponseCode::InternalError => {} + Err(e) => { + pw_log::error!("TC-10 FAIL: expected InternalError, got {}", e.code as u32); + return Err(()); + } + Ok(_) => { + pw_log::error!("TC-10 FAIL: expected Err, got Ok"); + return Err(()); + } + } + + Ok(()) +} + +#[panic_handler] +fn panic(_info: &core::panic::PanicInfo) -> ! { + loop {} +}
diff --git a/target/ast10x0/tests/mctp/ipc_client/fake_server_main.rs b/target/ast10x0/tests/mctp/ipc_client/fake_server_main.rs new file mode 100644 index 0000000..64c0f46 --- /dev/null +++ b/target/ast10x0/tests/mctp/ipc_client/fake_server_main.rs
@@ -0,0 +1,129 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! Fake MCTP server for the IPC-client QEMU test. +//! +//! Speaks the `openprot_mctp_api::wire` protocol over a Pigweed IPC channel. +//! No I2C, no serial — just a response table keyed on op code and selected +//! request fields. +//! +//! ## Response table +//! +//! | Op | Trigger | Response | +//! |---------------|--------------------------|------------------------------------------| +//! | SetEid | any | Success | +//! | GetEid | any | Success, eid = 8 | +//! | Listener | msg_type != 0xFE | Success, handle = 42 | +//! | Listener | msg_type == 0xFE (TC-08) | Error: BadArgument | +//! | Req | any | Success, handle = 43 | +//! | Recv | handle == 42 | Success, msg_type=5, eid=8, tag=0, | +//! | | | payload = [0xDE, 0xAD, 0xBE, 0xEF] | +//! | Recv | handle == 0 (TC-09) | Error: TimedOut | +//! | Recv | handle == 0xFEFE (TC-10) | 4-byte short response (no full header) | +//! | Send | any | Success, tag = 1 | +//! | Unbind | any | Success | + +#![no_main] +#![no_std] + +use openprot_mctp_api::wire::{ + self, MctpOp, MctpRequestHeader, MAX_REQUEST_SIZE, MAX_RESPONSE_SIZE, +}; +use openprot_mctp_api::ResponseCode; +use userspace::{entry, syscall}; +use userspace::syscall::Signals; +use userspace::time::Instant; + +use app_fake_server::handle; + +// Handle value used by the client to trigger a short (truncated) response. +const TC10_HANDLE: u32 = 0xFEFE; +// Handle value used by the client to trigger a TimedOut error response. +const TC09_HANDLE: u32 = 0; + +#[entry] +fn entry() { + let mut req_buf = [0u8; MAX_REQUEST_SIZE]; + let mut resp_buf = [0u8; MAX_RESPONSE_SIZE]; + + if syscall::wait_group_add(handle::WG, handle::MCTP, Signals::READABLE, 0usize).is_err() { + loop {} + } + + loop { + let _ = syscall::object_wait(handle::WG, Signals::READABLE, Instant::MAX); + + let len = match syscall::channel_read(handle::MCTP, 0, &mut req_buf) { + Ok(n) => n, + Err(_) => continue, + }; + + // Parse the request header once; handle TC-10 (short response) before + // the generic dispatch so we can bypass the full response encoder. + let header = match MctpRequestHeader::from_bytes(&req_buf[..len]) { + Some(h) => h, + None => { + let rlen = + wire::encode_error_response(&mut resp_buf, ResponseCode::BadArgument) + .unwrap_or(0); + let _ = syscall::channel_respond(handle::MCTP, &resp_buf[..rlen]); + continue; + } + }; + + // TC-10: respond with 4 bytes only so the client sees a truncated header. + if matches!(header.operation(), Some(MctpOp::Recv)) && header.handle == TC10_HANDLE { + let _ = syscall::channel_respond(handle::MCTP, &[0u8; 4]); + continue; + } + + let resp_len = dispatch(&header, &mut resp_buf); + let _ = syscall::channel_respond(handle::MCTP, &resp_buf[..resp_len]); + } +} + +/// Build a response for the given decoded header. +fn dispatch(header: &MctpRequestHeader, resp: &mut [u8]) -> usize { + let op = match header.operation() { + Some(op) => op, + None => { + return wire::encode_error_response(resp, ResponseCode::BadArgument).unwrap_or(0) + } + }; + + match op { + MctpOp::SetEid => wire::encode_success_response(resp).unwrap_or(0), + + MctpOp::GetEid => wire::encode_get_eid_response(resp, 8).unwrap_or(0), + + MctpOp::Listener => { + // TC-08: msg_type 0xFE triggers a BadArgument error response. + if header.msg_type == 0xFE { + wire::encode_error_response(resp, ResponseCode::BadArgument).unwrap_or(0) + } else { + wire::encode_handle_response(resp, 42).unwrap_or(0) + } + } + + MctpOp::Req => wire::encode_handle_response(resp, 43).unwrap_or(0), + + MctpOp::Recv => { + // TC-09: handle 0 triggers a TimedOut error response. + if header.handle == TC09_HANDLE { + wire::encode_error_response(resp, ResponseCode::TimedOut).unwrap_or(0) + } else { + let payload: [u8; 4] = [0xDE, 0xAD, 0xBE, 0xEF]; + wire::encode_recv_response(resp, 5, false, 8, 0, &payload).unwrap_or(0) + } + } + + MctpOp::Send => wire::encode_send_response(resp, 1).unwrap_or(0), + + MctpOp::Unbind => wire::encode_success_response(resp).unwrap_or(0), + } +} + +#[panic_handler] +fn panic(_info: &core::panic::PanicInfo) -> ! { + loop {} +}
diff --git a/target/ast10x0/tests/mctp/ipc_client/system.json5 b/target/ast10x0/tests/mctp/ipc_client/system.json5 new file mode 100644 index 0000000..331e17d --- /dev/null +++ b/target/ast10x0/tests/mctp/ipc_client/system.json5
@@ -0,0 +1,81 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +// AST10x0 MCTP IPC-client unit-exercise system image. +// +// Two-process layout: +// +// fake_server — channel_handler that returns deterministic wire responses +// client_test — IpcMctpClient exerciser; calls debug_shutdown(Ok|Err) +// +// Memory map (AST10x0: 768 KB SRAM, no XIP): +// 0x00000000 - 0x00000500 vector table (1280 B) +// 0x00000500 - 0x00020500 kernel flash (~128 KB) +// 0x00020500 - 0x00060500 app flash (256 KB total; 128 KB per app) +// 0x00060000 - 0x00080000 kernel RAM (128 KB) +// 0x00080000 - 0x000C0000 app RAM (256 KB total; 32 KB + 64 KB per process) +{ + arch: { + type: "armv7m", + vector_table_start_address: 0x00000000, + vector_table_size_bytes: 1280, + }, + kernel: { + flash_start_address: 0x00000500, + flash_size_bytes: 129792, + ram_start_address: 0x00060000, + ram_size_bytes: 131072, + }, + apps: [ + { + name: "fake_server", + flash_size_bytes: 131072, + processes: [ + { + name: "fake_server_process", + ram_size_bytes: 32768, + objects: [ + { + name: "wg", + type: "wait_group", + }, + { + name: "mctp", + type: "channel_handler", + }, + ], + threads: [ + { + name: "fake_server_thread", + kernel_stack_size_bytes: 4096, + }, + ], + }, + ], + }, + { + name: "client_test", + flash_size_bytes: 131072, + processes: [ + { + name: "client_test_process", + ram_size_bytes: 65536, + objects: [ + { + name: "mctp", + type: "channel_initiator", + handler_process: "fake_server_process", + handler_object_name: "mctp", + }, + ], + threads: [ + { + name: "client_test_thread", + kernel_stack_size_bytes: 4096, + }, + ], + }, + ], + }, + ], +}
diff --git a/target/ast10x0/tests/mctp/ipc_client/target.rs b/target/ast10x0/tests/mctp/ipc_client/target.rs new file mode 100644 index 0000000..2c20eec --- /dev/null +++ b/target/ast10x0/tests/mctp/ipc_client/target.rs
@@ -0,0 +1,40 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! Kernel target for the MCTP IPC-client QEMU test. +//! +//! Pass/fail is communicated by `client_test` calling +//! `syscall::debug_shutdown(Ok(()) | Err(...))`, which lands here +//! and writes the UART sentinel picked up by qemu_runner.py. + +#![no_std] +#![no_main] + +use console_backend::console_backend_write_all; +use target_common::{TargetInterface, declare_target}; +use entry as _; + +pub struct Target {} + +impl TargetInterface for Target { + const NAME: &'static str = "AST10x0 MCTP IPC-client test"; + + fn main() -> ! { + codegen::start(); + #[expect(clippy::empty_loop)] + 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);