services/mctp: add mctp server, echo handler, and server tests
diff --git a/services/mctp/echo/BUILD.bazel b/services/mctp/echo/BUILD.bazel index 4cfe7a4..2a280d1 100644 --- a/services/mctp/echo/BUILD.bazel +++ b/services/mctp/echo/BUILD.bazel
@@ -11,6 +11,7 @@ visibility = ["//visibility:public"], deps = [ "//services/mctp/api:mctp_api", + "@pigweed//pw_log/rust:pw_log", ], )
diff --git a/services/mctp/echo/src/lib.rs b/services/mctp/echo/src/lib.rs index 6fde1bf..27951df 100644 --- a/services/mctp/echo/src/lib.rs +++ b/services/mctp/echo/src/lib.rs
@@ -13,7 +13,9 @@ #![no_std] -use openprot_mctp_api::{MctpClient, MctpError, MctpRespChannel, Stack, StackListener}; +use openprot_mctp_api::{ + MctpClient, MctpError, MctpReqChannel, MctpRespChannel, Stack, StackListener, +}; /// Default MCTP message type used by the echo app. pub const ECHO_MSG_TYPE: u8 = 1; @@ -40,12 +42,113 @@ } /// Process one echo receive/send cycle. -/// -/// Returns an error from either receive or response send. -pub fn echo_once<L>(listener: &mut L, buf: &mut [u8]) -> Result<(), MctpError> +pub fn echo_once<L>(listener: &mut L, buf: &mut [u8]) -> Result<(), openprot_mctp_api::MctpError> where L: openprot_mctp_api::MctpListener, { let (_meta, msg, mut resp) = listener.recv(buf)?; resp.send(msg) } + +/// Run the echo loop forever, echoing received messages. +pub fn run<L>(listener: &mut L) -> ! +where + L: openprot_mctp_api::MctpListener, +{ + let mut buf = [0u8; 255]; + loop { + match listener.recv(&mut buf) { + Ok((_meta, msg, mut resp)) => { + let _ = resp.send(msg); + } + Err(e) => { + // Timeouts are expected noise from the short recv deadline; suppress them. + if !e.is_timeout() { + pw_log::error!("echo recv failed: code={}", e.code as u32); + } + } + } + } +} + +/// Run the echo loop with periodic sends to a peer endpoint, forever. +pub fn run_with_peer<C: MctpClient, L: openprot_mctp_api::MctpListener>( + stack: &Stack<C>, + peer_eid: u8, + listener: &mut L, +) -> ! { + loop { + run_with_peer_round_trip_limit(stack, peer_eid, listener, u32::MAX); + } +} + +/// Run the echo loop with periodic sends to a peer endpoint, returning after +/// `max_round_trips` successful request/response exchanges. +pub fn run_with_peer_round_trip_limit<C: MctpClient, L: openprot_mctp_api::MctpListener>( + stack: &Stack<C>, + peer_eid: u8, + listener: &mut L, + max_round_trips: u32, +) { + let mut buf = [0u8; 255]; + let mut iteration: u32 = 0; + // 10 iterations between sends keeps the channel active without flooding it. + let send_interval = 10; + + // Keep retrying until the first successful round-trip so a slow-booting + // peer (>1 min) does not cause us to give up before it is ready. + loop { + match listener.recv(&mut buf) { + Ok((_meta, msg, mut resp)) => { + let _ = resp.send(msg); + } + Err(e) => { + // Timeouts are expected noise from the short recv deadline; suppress them. + if !e.is_timeout() { + pw_log::error!("echo recv failed: code={}", e.code as u32); + } + } + } + + if let Ok(mut req) = stack.req(peer_eid, 10000) { + let test_msg = b"echo_test"; + let _ = req.send(ECHO_MSG_TYPE, test_msg); + if req.recv(&mut buf).is_ok() { + break; + } + } + } + + // First round-trip succeeded; count it and continue with periodic sends. + let mut completed_round_trips: u32 = 1; + + loop { + iteration = iteration.wrapping_add(1); + + // Periodically try to send a test message to the peer. + if iteration % send_interval == 0 + && completed_round_trips < max_round_trips + && let Ok(mut req) = stack.req(peer_eid, 100) + { + let test_msg = b"echo_test"; + let _ = req.send(ECHO_MSG_TYPE, test_msg); + // Try to receive response with a short timeout. + if req.recv(&mut buf).is_ok() { + completed_round_trips = completed_round_trips.saturating_add(1); + } + } + + // Listen for incoming messages and echo them back. + match listener.recv(&mut buf) { + Ok((_meta, msg, mut resp)) => { + let _ = resp.send(msg); + } + Err(e) => { + // Timeouts are expected noise from the short recv deadline; suppress them. + if !e.is_timeout() { + pw_log::error!("echo recv failed: code={}", e.code as u32); + } + } + } + } +}
diff --git a/target/ast10x0/defs.bzl b/target/ast10x0/defs.bzl index dd10172..7e2e827 100644 --- a/target/ast10x0/defs.bzl +++ b/target/ast10x0/defs.bzl
@@ -10,17 +10,24 @@ }) def _system_image_test_impl(ctx): - master_elf = ctx.attr.image[SystemImageInfo].elf + image_info = ctx.attr.image[SystemImageInfo] executable_symlink = ctx.actions.declare_file(ctx.label.name) - ctx.actions.symlink(output = executable_symlink, target_file = master_elf) + ctx.actions.symlink(output = executable_symlink, target_file = image_info.elf) - runfiles = ctx.attr.image[DefaultInfo].default_runfiles + bin_symlink = ctx.actions.declare_file(ctx.label.name + ".bin") + ctx.actions.symlink(output = bin_symlink, target_file = image_info.bin) + + runfiles = ctx.runfiles(files = [bin_symlink]).merge( + ctx.attr.image[DefaultInfo].default_runfiles, + ) if ctx.attr.slave_image: - slave_elf = ctx.attr.slave_image[SystemImageInfo].elf - slave_symlink = ctx.actions.declare_file(ctx.label.name + ".slave.elf") - ctx.actions.symlink(output = slave_symlink, target_file = slave_elf) - runfiles = ctx.runfiles(files = [slave_symlink]).merge( + slave_info = ctx.attr.slave_image[SystemImageInfo] + slave_elf_symlink = ctx.actions.declare_file(ctx.label.name + ".slave.elf") + ctx.actions.symlink(output = slave_elf_symlink, target_file = slave_info.elf) + slave_bin_symlink = ctx.actions.declare_file(ctx.label.name + ".slave.bin") + ctx.actions.symlink(output = slave_bin_symlink, target_file = slave_info.bin) + runfiles = ctx.runfiles(files = [slave_elf_symlink, slave_bin_symlink]).merge( runfiles.merge(ctx.attr.slave_image[DefaultInfo].default_runfiles), )
diff --git a/target/ast10x0/harness/test_runner.py b/target/ast10x0/harness/test_runner.py index a49188d..ec36b64 100644 --- a/target/ast10x0/harness/test_runner.py +++ b/target/ast10x0/harness/test_runner.py
@@ -94,24 +94,59 @@ def _acquire_lock(host: str, timeout: int = _LOCK_ACQUIRE_TIMEOUT) -> bool: """Atomically create lock file on Pi; retry until timeout.""" deadline = time.time() + timeout - create = f"set -o noclobber && echo $$ > {_LOCK_PATH}" + local_user = os.environ.get("USER", "unknown") + # On noclobber failure, cat outputs the lock file contents so we get + # owner info from the same SSH call without a second round-trip. + create = ( + f"set -o noclobber && " + f"printf '%s\\t%s\\n' '{local_user}' \"$(date +%s)\" > {_LOCK_PATH} || " + f"{{ cat {_LOCK_PATH} 2>/dev/null; false; }}" + ) stale_check = ( f"mtime=$(stat -c %Y {_LOCK_PATH} 2>/dev/null) && " f"now=$(date +%s) && " f"[ $(( now - mtime )) -gt {_LOCK_STALE_THRESHOLD} ] && " f"rm -f {_LOCK_PATH}" ) + last_printed = 0.0 while time.time() < deadline: r = _ssh(host, create, capture_output=True) if r.returncode == 0: return True - if r.stderr.strip(): - print(r.stderr.decode(errors="replace").strip(), file=sys.stderr) - return False + if not r.stdout.strip(): + if msg := r.stderr.decode(errors="replace").strip(): + print(f"RUNNER: {msg}", file=sys.stderr) + return False + continue + parts = r.stdout.decode(errors="replace").split() + now = time.time() + if len(parts) == 2 and now - last_printed >= 30: + lock_user, acq_ts = parts[0], int(parts[1]) + held_s = int(now) - acq_ts + mins, secs = divmod(held_s, 60) + held = f"{mins}m {secs}s" if mins else f"{secs}s" + stat_r = _ssh( + host, + f"now=$(date +%s); mtime=$(stat -c %Y {_LOCK_PATH} 2>/dev/null); echo $((now - mtime))", + capture_output=True, + ) + touch_ago = int(stat_r.stdout.decode().strip() or -1) + if touch_ago > _LOCK_STALE_THRESHOLD: + print( + f"RUNNER: removing stale lock held by {lock_user} {held} ago, last touched {touch_ago}s ago...", + file=sys.stderr, + ) + else: + print( + f"RUNNER: Pi locked by {lock_user} {held} ago, last touched {touch_ago}s ago; waiting...", + file=sys.stderr, + ) + last_printed = now _ssh(host, stale_check, capture_output=True) time.sleep(2) print( - f"Timeout acquiring Pi lock after {timeout}s (Pi may be busy)", file=sys.stderr + f"RUNNER: timeout acquiring Pi lock after {timeout}s (Pi may be busy)", + file=sys.stderr, ) return False @@ -242,6 +277,7 @@ if not _acquire_lock(host): return False + print("RUNNER: lock acquired, starting test", file=sys.stderr) stop_touch = threading.Event() touch_thread = threading.Thread( @@ -383,13 +419,14 @@ if not image.suffix: slave_symlink = image.parent / (image.name + ".slave.elf") if slave_symlink.exists(): - slave_elf = slave_symlink.resolve() - slave_elf_path = slave_elf - args.slave_firmware = str(slave_elf.with_suffix(".bin")) + slave_elf_path = slave_symlink.resolve() + args.slave_firmware = str(image.parent / (image.name + ".slave.bin")) + args.firmware = str(image.parent / (image.name + ".bin")) image = image.resolve() + else: + args.firmware = str(image.with_suffix(".bin")) elf_path = image.with_suffix(".elf") - args.firmware = str(image.with_suffix(".bin")) if not elf_path.exists(): print(f"Error: ELF not found at {elf_path}", file=sys.stderr) return 1
diff --git a/target/ast10x0/peripherals/i2c/slave.rs b/target/ast10x0/peripherals/i2c/slave.rs index b603df6..8b118e8 100644 --- a/target/ast10x0/peripherals/i2c/slave.rs +++ b/target/ast10x0/peripherals/i2c/slave.rs
@@ -174,6 +174,14 @@ /// Configure the controller for slave mode pub fn configure_slave(&mut self, config: &SlaveConfig) -> Result<(), I2cError> { + // Disable master mode while the slave registers are programmed so the + // shared controller is quiescent during setup; its prior state is + // restored at the end so slave-only callers keep master off. + let master_was_enabled = self.regs().i2cc00().read().enbl_master_fn().bit(); + self.regs() + .i2cc00() + .modify(|_, w| w.enbl_master_fn().clear_bit()); + // Set slave address self.regs().i2cs40().write(|w| unsafe { w.slave_dev_addr1() @@ -245,6 +253,14 @@ // Enable slave interrupts self.enable_slave_interrupts(); + // Restore master mode to its prior state: dual master+slave callers + // (e.g. MCTP) get it back on; slave-only callers keep it off. + if master_was_enabled { + self.regs() + .i2cc00() + .modify(|_, w| w.enbl_master_fn().set_bit()); + } + Ok(()) }
diff --git a/target/ast10x0/tests/mctp/ipc_client/BUILD.bazel b/target/ast10x0/tests/mctp/ipc_client/BUILD.bazel index 2ac7521..56dbc70 100644 --- a/target/ast10x0/tests/mctp/ipc_client/BUILD.bazel +++ b/target/ast10x0/tests/mctp/ipc_client/BUILD.bazel
@@ -112,6 +112,7 @@ system_image_test( name = "ipc_client_qemu_test", image = ":ipc_client_image", + tags = ["qemu_only"], target_compatible_with = TARGET_COMPATIBLE_WITH, )
diff --git a/target/ast10x0/tests/mctp/server/BUILD.bazel b/target/ast10x0/tests/mctp/server/BUILD.bazel new file mode 100644 index 0000000..f6614d8 --- /dev/null +++ b/target/ast10x0/tests/mctp/server/BUILD.bazel
@@ -0,0 +1,223 @@ +# 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") + +filegroup( + name = "system_config", + srcs = ["system.json5"], + visibility = ["//visibility:public"], +) + +filegroup( + name = "peer_system_config", + srcs = ["peer_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", + ], +) + +rust_app( + name = "mctp_server", + srcs = ["main.rs"], + codegen_crate_name = "app_mctp_server", + edition = "2024", + system_config = ":system_config", + tags = ["kernel"], + target_compatible_with = TARGET_COMPATIBLE_WITH, + visibility = ["//visibility:public"], + deps = [ + "//services/i2c/api:i2c_api", + "//services/i2c/client:i2c_client", + "//services/i2c/client-ipc:i2c_client_ipc", + "//services/mctp/api:mctp_api", + "//services/mctp/server:mctp_server_lib", + "//services/mctp/transport-i2c:mctp_transport_i2c", + "@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", + ], +) + +rust_app( + name = "mctp_server_peer", + srcs = ["main_peer.rs"], + codegen_crate_name = "app_mctp_server_peer", + edition = "2024", + system_config = ":peer_system_config", + tags = ["kernel"], + target_compatible_with = TARGET_COMPATIBLE_WITH, + visibility = ["//visibility:public"], + deps = [ + "//services/i2c/api:i2c_api", + "//services/i2c/client:i2c_client", + "//services/i2c/client-ipc:i2c_client_ipc", + "//services/mctp/api:mctp_api", + "//services/mctp/server:mctp_server_lib", + "//services/mctp/transport-i2c:mctp_transport_i2c", + "@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", + ], +) + +rust_app( + name = "i2c_server_peer", + srcs = ["i2c_server_main_peer.rs"], + codegen_crate_name = "app_i2c_server_peer", + edition = "2024", + system_config = ":peer_system_config", + tags = ["kernel"], + target_compatible_with = TARGET_COMPATIBLE_WITH, + visibility = ["//visibility:public"], + deps = [ + "//services/i2c/server-runtime:i2c_server_runtime", + "//target/ast10x0/backend/i2c:i2c_backend_ast10x0", + "//target/ast10x0/peripherals", + "@pigweed//pw_kernel/userspace", + "@pigweed//pw_log/rust:pw_log", + ], +) + +rust_app( + name = "mctp_echo_client_peer", + srcs = ["echo_main_peer.rs"], + codegen_crate_name = "app_mctp_echo_client_peer", + edition = "2024", + system_config = ":peer_system_config", + tags = ["kernel"], + target_compatible_with = TARGET_COMPATIBLE_WITH, + visibility = ["//visibility:public"], + 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 = "i2c_server", + srcs = ["i2c_server_main.rs"], + codegen_crate_name = "app_i2c_server", + edition = "2024", + system_config = ":system_config", + tags = ["kernel"], + target_compatible_with = TARGET_COMPATIBLE_WITH, + visibility = ["//visibility:public"], + deps = [ + "//services/i2c/server-runtime:i2c_server_runtime", + "//target/ast10x0/backend/i2c:i2c_backend_ast10x0", + "//target/ast10x0/peripherals", + "@pigweed//pw_kernel/userspace", + "@pigweed//pw_log/rust:pw_log", + ], +) + +rust_app( + name = "mctp_echo_client", + srcs = ["echo_main.rs"], + codegen_crate_name = "app_mctp_echo_client", + edition = "2024", + system_config = ":system_config", + tags = ["kernel"], + target_compatible_with = TARGET_COMPATIBLE_WITH, + visibility = ["//visibility:public"], + 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", + ], +) + +system_image( + name = "mctp_server_image", + apps = [ + ":i2c_server", + ":mctp_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 = "mctp_server_peer_image", + apps = [ + ":i2c_server_peer", + ":mctp_server_peer", + ":mctp_echo_client_peer", + ], + kernel = ":target", + platform = "//target/ast10x0", + system_config = ":peer_system_config", + tags = ["kernel"], + target_compatible_with = TARGET_COMPATIBLE_WITH, + visibility = ["//visibility:public"], +) + +system_image_test( + name = "mctp_server_test", + image = ":mctp_server_image", + slave_image = ":mctp_server_peer_image", + tags = [ + "hardware", + "manual", + ], + target_compatible_with = select({ + "//target/ast10x0:qemu_enabled": ["@platforms//:incompatible"], + "//conditions:default": [], + }), +)
diff --git a/target/ast10x0/tests/mctp/server/echo_main.rs b/target/ast10x0/tests/mctp/server/echo_main.rs new file mode 100644 index 0000000..ef697af --- /dev/null +++ b/target/ast10x0/tests/mctp/server/echo_main.rs
@@ -0,0 +1,34 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +#![no_main] +#![no_std] + +use openprot_mctp_api::Stack; +use openprot_mctp_client_ipc::IpcMctpClient; +use openprot_mctp_echo::{prepare_listener_with_eid_and_timeout, run_with_peer}; +use userspace::{entry, syscall}; + +// Primary echo endpoint at EID 8, sends to peer at EID 9. +const ECHO_EID: u8 = 8; +const PEER_EID: u8 = 9; +const LISTEN_TIMEOUT_MS: u32 = 100; +#[entry] +fn entry() { + let stack = Stack::new(IpcMctpClient::new(app_mctp_echo_client::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 setup failed: code={}", e.code as u32); + syscall::process_exit(1); + } + }; + + run_with_peer(&stack, PEER_EID, &mut listener); +} + +#[panic_handler] +fn panic(_info: &core::panic::PanicInfo) -> ! { + loop {} +}
diff --git a/target/ast10x0/tests/mctp/server/echo_main_peer.rs b/target/ast10x0/tests/mctp/server/echo_main_peer.rs new file mode 100644 index 0000000..b8f6b54 --- /dev/null +++ b/target/ast10x0/tests/mctp/server/echo_main_peer.rs
@@ -0,0 +1,36 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +#![no_main] +#![no_std] + +use openprot_mctp_api::Stack; +use openprot_mctp_client_ipc::IpcMctpClient; +use openprot_mctp_echo::{prepare_listener_with_eid_and_timeout, run_with_peer_round_trip_limit}; +use userspace::{entry, syscall}; + +// Peer echo endpoint at EID 9, sends to primary at EID 8. +const ECHO_EID: u8 = 9; +const PEER_EID: u8 = 8; +const LISTEN_TIMEOUT_MS: u32 = 100; +const MAX_ROUND_TRIPS: u32 = 64; + +#[entry] +fn entry() { + let stack = Stack::new(IpcMctpClient::new(app_mctp_echo_client_peer::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 setup failed: code={}", e.code as u32); + syscall::process_exit(1); + } + }; + + run_with_peer_round_trip_limit(&stack, PEER_EID, &mut listener, MAX_ROUND_TRIPS); +} + +#[panic_handler] +fn panic(_info: &core::panic::PanicInfo) -> ! { + loop {} +}
diff --git a/target/ast10x0/tests/mctp/server/i2c_server_main.rs b/target/ast10x0/tests/mctp/server/i2c_server_main.rs new file mode 100644 index 0000000..40f295d --- /dev/null +++ b/target/ast10x0/tests/mctp/server/i2c_server_main.rs
@@ -0,0 +1,52 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! I2C server app: board init + open Bus 2 + run the server-runtime loop. + +#![no_main] +#![no_std] + +use app_i2c_server::{handle, signals}; +use ast10x0_peripherals::i2c::{ClockConfig, I2cConfig, I2cSpeed, I2cXferMode}; +use i2c_server_runtime::{run, Bus}; +use userspace::entry; + +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(), +}; + +#[unsafe(link_section = ".ram_nc")] +static mut MASTER_DMA_BUF: [u8; 4096] = [0u8; 4096]; +#[unsafe(link_section = ".ram_nc")] +static mut SLAVE_DMA_BUF: [u8; 256] = [0u8; 256]; + +#[entry] +fn entry() { + // SAFETY: board init ran init_bus(2) in the kernel; buffers are non-cached and owned here. + let master_dma_buf: &'static mut [u8] = + unsafe { &mut *core::ptr::addr_of_mut!(MASTER_DMA_BUF) }; + let slave_dma_buf: &'static mut [u8] = unsafe { &mut *core::ptr::addr_of_mut!(SLAVE_DMA_BUF) }; + let driver = + match unsafe { i2c_backend::open_bus_dma(2, &SLAVE_CFG, master_dma_buf, slave_dma_buf) } { + Ok(d) => d, + Err(_) => { + pw_log::error!("open_bus_dma(2) failed"); + loop {} + } + }; + + pw_log::info!("I2C server ready on Bus 2"); + + let mut buses = [Bus::new(handle::I2C, handle::I2C2_IRQ, driver)]; + run(handle::WG, signals::I2C2, &mut buses); +} + +#[panic_handler] +fn panic(_info: &core::panic::PanicInfo) -> ! { + loop {} +}
diff --git a/target/ast10x0/tests/mctp/server/i2c_server_main_peer.rs b/target/ast10x0/tests/mctp/server/i2c_server_main_peer.rs new file mode 100644 index 0000000..f2bba75 --- /dev/null +++ b/target/ast10x0/tests/mctp/server/i2c_server_main_peer.rs
@@ -0,0 +1,52 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! I2C server app: board init + open Bus 2 + run the server-runtime loop. + +#![no_main] +#![no_std] + +use app_i2c_server_peer::{handle, signals}; +use ast10x0_peripherals::i2c::{ClockConfig, I2cConfig, I2cSpeed, I2cXferMode}; +use i2c_server_runtime::{run, Bus}; +use userspace::entry; + +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(), +}; + +#[unsafe(link_section = ".ram_nc")] +static mut MASTER_DMA_BUF: [u8; 4096] = [0u8; 4096]; +#[unsafe(link_section = ".ram_nc")] +static mut SLAVE_DMA_BUF: [u8; 256] = [0u8; 256]; + +#[entry] +fn entry() { + // SAFETY: board init ran init_bus(2) in the kernel; buffers are non-cached and owned here. + let master_dma_buf: &'static mut [u8] = + unsafe { &mut *core::ptr::addr_of_mut!(MASTER_DMA_BUF) }; + let slave_dma_buf: &'static mut [u8] = unsafe { &mut *core::ptr::addr_of_mut!(SLAVE_DMA_BUF) }; + let driver = + match unsafe { i2c_backend::open_bus_dma(2, &SLAVE_CFG, master_dma_buf, slave_dma_buf) } { + Ok(d) => d, + Err(_) => { + pw_log::error!("open_bus_dma(2) failed"); + loop {} + } + }; + + pw_log::info!("I2C server peer ready on Bus 2"); + + let mut buses = [Bus::new(handle::I2C, handle::I2C2_IRQ, driver)]; + run(handle::WG, signals::I2C2, &mut buses); +} + +#[panic_handler] +fn panic(_info: &core::panic::PanicInfo) -> ! { + loop {} +}
diff --git a/target/ast10x0/tests/mctp/server/main.rs b/target/ast10x0/tests/mctp/server/main.rs new file mode 100644 index 0000000..577ffdf --- /dev/null +++ b/target/ast10x0/tests/mctp/server/main.rs
@@ -0,0 +1,317 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! MCTP Server — IPC Dispatch Loop +//! +//! Userspace service that receives MCTP requests over a Pigweed IPC channel, +//! dispatches them to the MCTP server core, and responds with results. +//! +//! # Architecture +//! +//! ```text +//! ┌─ Client ──────────────────────────┐ +//! │ channel_transact(request) │ +//! └──────────────┬────────────────────┘ +//! │ IPC channel +//! ▼ +//! ┌─ This Server ─────────────────────┐ +//! │ object_wait(READABLE) │ +//! │ channel_read → MctpRequestHeader │ +//! │ dispatch_mctp_op(op, server) │ +//! │ channel_respond ← MctpRespHeader │ +//! └──────────────┬────────────────────┘ +//! │ mctp-stack Router +//! ▼ +//! ┌─ I2C Transport ──────────────────┐ +//! │ I2cSender → I2C Server IPC │ +//! └──────────────────────────────────┘ +//! ``` +//! +//! # IPC Pattern +//! +//! Follows the same loop as `services/i2c/server/src/main.rs`: +//! +//! 1. `object_wait(handle, READABLE)` — block until a client sends a request +//! 2. `channel_read(handle)` — read the raw request bytes +//! 3. Parse `MctpRequestHeader`, dispatch via `dispatch_mctp_op` +//! 4. `channel_respond(handle)` — send response header + data +//! +//! # Handle Binding +//! +//! The IPC handles are provided by the `app_package` Bazel rule, which +//! generates `app_mctp_server::handle::*` from the system configuration. + +#![no_main] +#![no_std] + +use i2c_api::SlaveEvent; +use i2c_client::I2cClient; +use i2c_client_ipc::IpcTransport; +use openprot_mctp_api::wire::{ + self, MctpOp, MctpRequestHeader, 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; +use pw_status::Result; +use userspace::entry; +use userspace::syscall::{self, Signals}; +use userspace::time::{Clock, Duration, Instant, SystemClock}; + +use app_mctp_server::handle; + +const OWN_EID: u8 = 8; +const OWN_I2C_ADDR: u8 = 0x10; +const REMOTE_I2C_ADDR: u8 = 0x42; +const I2C_RX_MAX: usize = MAX_PAYLOAD_SIZE; + +// --------------------------------------------------------------------------- +// Server loop +// --------------------------------------------------------------------------- + +fn mctp_server_loop() -> Result<()> { + pw_log::info!("MCTP server starting"); + let sender = I2cSender::new( + I2cClient::new(IpcTransport::new(handle::I2C)), + OWN_I2C_ADDR, + REMOTE_I2C_ADDR, + ); + let mut i2c_rx_client = I2cClient::new(IpcTransport::new(handle::I2C)); + let i2c_receiver = MctpI2cReceiver::new(OWN_I2C_ADDR); + + i2c_rx_client.configure_slave(OWN_I2C_ADDR).map_err(|_| { + pw_log::error!("configure_slave failed"); + Error::Internal + })?; + + i2c_rx_client.enable_slave().map_err(|_| { + pw_log::error!("enable_slave failed"); + Error::Internal + })?; + + i2c_rx_client.enable_notification().map_err(|_| { + pw_log::error!("enable_notification failed"); + Error::Internal + })?; + + 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; I2C_RX_MAX]; + struct PendingRecv { + handle: Handle, + deadline: Instant, + } + + // Pending blocking-recv: holds the MCTP handle whose IPC reply is deferred + // until I2C data arrives, or until its timeout deadline expires. + let mut pending_recv: Option<PendingRecv> = None; + + // Register event sources with the WaitGroup. + // user_data=0 → IPC from a client (MCTP channel READABLE) + // user_data=1 → I2C USER signal (slave data latched by i2c server) + syscall::wait_group_add(handle::WG, handle::MCTP, Signals::READABLE, 0usize)?; + syscall::wait_group_add(handle::WG, handle::I2C, Signals::USER, 1usize)?; + + loop { + let wait_deadline = pending_recv + .as_ref() + .map(|pending| pending.deadline) + .unwrap_or(Instant::MAX); + let ev = match syscall::object_wait( + handle::WG, + Signals::READABLE | Signals::USER, + wait_deadline, + ) { + Ok(ev) => ev, + Err(pw_status::Error::DeadlineExceeded) => { + if pending_recv.take().is_some() { + let resp = + openprot_mctp_api::wire::MctpResponseHeader::error(ResponseCode::TimedOut); + response_buf[..openprot_mctp_api::wire::MctpResponseHeader::SIZE] + .copy_from_slice(&resp.to_bytes()); + let _ = syscall::channel_respond( + handle::MCTP, + &response_buf[..openprot_mctp_api::wire::MctpResponseHeader::SIZE], + ); + let _ = syscall::wait_group_add( + handle::WG, + handle::MCTP, + Signals::READABLE, + 0usize, + ); + } + continue; + } + Err(err) => return Err(err), + }; + + if ev.user_data == 1 { + // Inbound i2c data: fetch latched payload + metadata and feed to router. + match i2c_rx_client.slave_receive(&mut i2c_rx_buf) { + Ok(event) => { + if event.kind == SlaveEvent::DataReceived && event.data_len > 0 { + if let Ok((pkt, _)) = i2c_receiver.decode(&i2c_rx_buf[..event.data_len]) { + let _ = server.inbound(pkt); + } else { + pw_log::error!("i2c frame decode failed"); + } + } + } + Err(_) => { + pw_log::error!("slave_receive failed"); + } + } + // Satisfy any deferred blocking-recv now that inbound data was processed. + if let Some(pending) = pending_recv.as_ref() { + if let Some(meta) = server.try_recv(pending.handle, &mut recv_buf) { + let payload = &recv_buf[..meta.payload_size]; + let response_len = openprot_mctp_api::wire::encode_recv_response( + &mut response_buf, + meta.msg_type, + meta.msg_ic, + meta.remote_eid, + meta.msg_tag, + payload, + ) + .unwrap_or_else(|_| { + openprot_mctp_api::wire::encode_error_response( + &mut response_buf, + ResponseCode::InternalError, + ) + .unwrap_or(0) + }); + syscall::channel_respond(handle::MCTP, &response_buf[..response_len])?; + pending_recv = None; + syscall::wait_group_add(handle::WG, handle::MCTP, Signals::READABLE, 0usize)?; + } + } + } else { + // IPC from a client — channel_read is non-blocking here because + // the WaitGroup only fires after READABLE is set. + let len = syscall::channel_read(handle::MCTP, 0, &mut request_buf)?; + if pending_recv.is_some() { + // A blocking recv is in flight; reject the new caller so the + // kernel clears READABLE and stops re-firing this branch. + let resp = + openprot_mctp_api::wire::MctpResponseHeader::error(ResponseCode::InternalError); + response_buf[..openprot_mctp_api::wire::MctpResponseHeader::SIZE] + .copy_from_slice(&resp.to_bytes()); + syscall::channel_respond( + handle::MCTP, + &response_buf[..openprot_mctp_api::wire::MctpResponseHeader::SIZE], + )?; + continue; + } + + if len < MctpRequestHeader::SIZE { + // Truncated request — respond with error + let resp = + openprot_mctp_api::wire::MctpResponseHeader::error(ResponseCode::BadArgument); + response_buf[..openprot_mctp_api::wire::MctpResponseHeader::SIZE] + .copy_from_slice(&resp.to_bytes()); + syscall::channel_respond( + handle::MCTP, + &response_buf[..openprot_mctp_api::wire::MctpResponseHeader::SIZE], + )?; + continue; + } + + // Recv op: try immediately; defer response if no message is ready. + if MctpRequestHeader::from_bytes(&request_buf[..len]) + .and_then(|h| h.operation()) + .map_or(false, |op| matches!(op, MctpOp::Recv)) + { + let header = MctpRequestHeader::from_bytes(&request_buf[..len]).unwrap(); + let recv_handle = Handle(header.handle); + let payload = wire::get_request_payload(&request_buf[..len]); + if payload.len() < 4 { + let resp = openprot_mctp_api::wire::MctpResponseHeader::error( + ResponseCode::BadArgument, + ); + response_buf[..openprot_mctp_api::wire::MctpResponseHeader::SIZE] + .copy_from_slice(&resp.to_bytes()); + syscall::channel_respond( + handle::MCTP, + &response_buf[..openprot_mctp_api::wire::MctpResponseHeader::SIZE], + )?; + continue; + } + + let timeout_millis = u32::from_le_bytes(payload[..4].try_into().unwrap()); + match server.try_recv(recv_handle, &mut recv_buf) { + Some(meta) => { + let payload = &recv_buf[..meta.payload_size]; + let response_len = openprot_mctp_api::wire::encode_recv_response( + &mut response_buf, + meta.msg_type, + meta.msg_ic, + meta.remote_eid, + meta.msg_tag, + payload, + ) + .unwrap_or_else(|_| { + openprot_mctp_api::wire::encode_error_response( + &mut response_buf, + ResponseCode::InternalError, + ) + .unwrap_or(0) + }); + syscall::channel_respond(handle::MCTP, &response_buf[..response_len])?; + } + None => { + let deadline = if timeout_millis == 0 { + Instant::MAX + } else { + SystemClock::now() + .checked_add_duration(Duration::from_millis(timeout_millis as i64)) + .unwrap_or(Instant::MAX) + }; + // No message yet — remove MCTP from WaitGroup so the fast-path cannot + // re-fire while READABLE stays asserted on the open transaction. + pending_recv = Some(PendingRecv { + handle: recv_handle, + deadline, + }); + let _ = syscall::wait_group_remove(handle::WG, handle::MCTP); + } + } + } else { + // All other ops: dispatch and respond immediately. + 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 point +// --------------------------------------------------------------------------- + +#[entry] +fn entry() { + if let Err(e) = mctp_server_loop() { + pw_log::error!("mctp_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/server/main_peer.rs b/target/ast10x0/tests/mctp/server/main_peer.rs new file mode 100644 index 0000000..8de3de4 --- /dev/null +++ b/target/ast10x0/tests/mctp/server/main_peer.rs
@@ -0,0 +1,257 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! MCTP Server — IPC Dispatch Loop (peer role) +//! +//! Same service behavior as `main.rs`, but with complementary I2C addressing +//! for the second board in dual-EVB tests. + +#![no_main] +#![no_std] + +use i2c_api::SlaveEvent; +use i2c_client::I2cClient; +use i2c_client_ipc::IpcTransport; +use openprot_mctp_api::wire::{ + self, MctpOp, MctpRequestHeader, 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; +use pw_status::Result; +use userspace::entry; +use userspace::syscall::{self, Signals}; +use userspace::time::{Clock, Duration, Instant, SystemClock}; + +use app_mctp_server_peer::handle; + +const OWN_EID: u8 = 9; +const OWN_I2C_ADDR: u8 = 0x42; +const REMOTE_I2C_ADDR: u8 = 0x10; +const I2C_RX_MAX: usize = MAX_PAYLOAD_SIZE; + +fn mctp_server_loop() -> Result<()> { + pw_log::info!("MCTP server peer starting"); + let sender = I2cSender::new( + I2cClient::new(IpcTransport::new(handle::I2C)), + OWN_I2C_ADDR, + REMOTE_I2C_ADDR, + ); + let mut i2c_rx_client = I2cClient::new(IpcTransport::new(handle::I2C)); + let i2c_receiver = MctpI2cReceiver::new(OWN_I2C_ADDR); + + if i2c_rx_client.configure_slave(OWN_I2C_ADDR).is_err() { + pw_log::error!("configure_slave failed"); + return Err(Error::Internal); + } + if i2c_rx_client.enable_slave().is_err() { + pw_log::error!("enable_slave failed"); + return Err(Error::Internal); + } + if i2c_rx_client.enable_notification().is_err() { + pw_log::error!("enable_notification failed"); + return Err(Error::Internal); + } + + 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; I2C_RX_MAX]; + + syscall::wait_group_add(handle::WG, handle::MCTP, Signals::READABLE, 0usize)?; + syscall::wait_group_add(handle::WG, handle::I2C, Signals::USER, 1usize)?; + + struct PendingRecv { + handle: Handle, + deadline: Instant, + } + + let mut pending_recv: Option<PendingRecv> = None; + + loop { + let wait_deadline = pending_recv + .as_ref() + .map(|pending| pending.deadline) + .unwrap_or(Instant::MAX); + let ev = match syscall::object_wait( + handle::WG, + Signals::READABLE | Signals::USER, + wait_deadline, + ) { + Ok(ev) => ev, + Err(pw_status::Error::DeadlineExceeded) => { + if pending_recv.take().is_some() { + let resp = + openprot_mctp_api::wire::MctpResponseHeader::error(ResponseCode::TimedOut); + response_buf[..openprot_mctp_api::wire::MctpResponseHeader::SIZE] + .copy_from_slice(&resp.to_bytes()); + let _ = syscall::channel_respond( + handle::MCTP, + &response_buf[..openprot_mctp_api::wire::MctpResponseHeader::SIZE], + ); + let _ = syscall::wait_group_add( + handle::WG, + handle::MCTP, + Signals::READABLE, + 0usize, + ); + } + continue; + } + Err(err) => return Err(err), + }; + + if ev.user_data == 1 { + match i2c_rx_client.slave_receive(&mut i2c_rx_buf) { + Ok(event) => { + if event.kind == SlaveEvent::DataReceived && event.data_len > 0 { + if let Ok((pkt, _)) = i2c_receiver.decode(&i2c_rx_buf[..event.data_len]) { + let _ = server.inbound(pkt); + } else { + pw_log::error!("i2c frame decode failed"); + } + } + } + Err(_) => { + pw_log::error!("slave_receive failed"); + } + } + if let Some(pending) = pending_recv.as_ref() { + if let Some(meta) = server.try_recv(pending.handle, &mut recv_buf) { + let payload = &recv_buf[..meta.payload_size]; + let response_len = openprot_mctp_api::wire::encode_recv_response( + &mut response_buf, + meta.msg_type, + meta.msg_ic, + meta.remote_eid, + meta.msg_tag, + payload, + ) + .unwrap_or_else(|_| { + openprot_mctp_api::wire::encode_error_response( + &mut response_buf, + ResponseCode::InternalError, + ) + .unwrap_or(0) + }); + syscall::channel_respond(handle::MCTP, &response_buf[..response_len])?; + pending_recv = None; + syscall::wait_group_add(handle::WG, handle::MCTP, Signals::READABLE, 0usize)?; + } + } + } else { + let len = syscall::channel_read(handle::MCTP, 0, &mut request_buf)?; + if pending_recv.is_some() { + let resp = + openprot_mctp_api::wire::MctpResponseHeader::error(ResponseCode::InternalError); + response_buf[..openprot_mctp_api::wire::MctpResponseHeader::SIZE] + .copy_from_slice(&resp.to_bytes()); + syscall::channel_respond( + handle::MCTP, + &response_buf[..openprot_mctp_api::wire::MctpResponseHeader::SIZE], + )?; + continue; + } + + if len < MctpRequestHeader::SIZE { + let resp = + openprot_mctp_api::wire::MctpResponseHeader::error(ResponseCode::BadArgument); + response_buf[..openprot_mctp_api::wire::MctpResponseHeader::SIZE] + .copy_from_slice(&resp.to_bytes()); + syscall::channel_respond( + handle::MCTP, + &response_buf[..openprot_mctp_api::wire::MctpResponseHeader::SIZE], + )?; + continue; + } + + if MctpRequestHeader::from_bytes(&request_buf[..len]) + .and_then(|h| h.operation()) + .map_or(false, |op| matches!(op, MctpOp::Recv)) + { + let header = MctpRequestHeader::from_bytes(&request_buf[..len]).unwrap(); + let recv_handle = Handle(header.handle); + let payload = wire::get_request_payload(&request_buf[..len]); + if payload.len() < 4 { + let resp = openprot_mctp_api::wire::MctpResponseHeader::error( + ResponseCode::BadArgument, + ); + response_buf[..openprot_mctp_api::wire::MctpResponseHeader::SIZE] + .copy_from_slice(&resp.to_bytes()); + syscall::channel_respond( + handle::MCTP, + &response_buf[..openprot_mctp_api::wire::MctpResponseHeader::SIZE], + )?; + continue; + } + + let timeout_millis = u32::from_le_bytes(payload[..4].try_into().unwrap()); + match server.try_recv(recv_handle, &mut recv_buf) { + Some(meta) => { + let payload = &recv_buf[..meta.payload_size]; + let response_len = openprot_mctp_api::wire::encode_recv_response( + &mut response_buf, + meta.msg_type, + meta.msg_ic, + meta.remote_eid, + meta.msg_tag, + payload, + ) + .unwrap_or_else(|_| { + openprot_mctp_api::wire::encode_error_response( + &mut response_buf, + ResponseCode::InternalError, + ) + .unwrap_or(0) + }); + syscall::channel_respond(handle::MCTP, &response_buf[..response_len])?; + } + None => { + let deadline = if timeout_millis == 0 { + Instant::MAX + } else { + SystemClock::now() + .checked_add_duration(Duration::from_millis(timeout_millis as i64)) + .unwrap_or(Instant::MAX) + }; + pending_recv = Some(PendingRecv { + handle: recv_handle, + deadline, + }); + 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) = mctp_server_loop() { + pw_log::error!("mctp_server peer 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/server/peer_system.json5 b/target/ast10x0/tests/mctp/server/peer_system.json5 new file mode 100644 index 0000000..4a2cc20 --- /dev/null +++ b/target/ast10x0/tests/mctp/server/peer_system.json5
@@ -0,0 +1,116 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +// AST10x0 MCTP Server Configuration (peer image) +{ + 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: "i2c_server_peer", + flash_size_bytes: 65536, + processes: [ + { + name: "i2c_server_peer_process", + ram_size_bytes: 65536, + objects: [ + { + name: "wg", + type: "wait_group", + }, + { + name: "i2c", + type: "channel_handler", + }, + { + name: "i2c2_irq", + type: "interrupt", + irqs: [ + { + name: "i2c2", + number: 112, + }, + ], + }, + { + type: "thread", + name: "i2c_server_peer_thread", + kernel_stack_size_bytes: 4096, + }, + ], + memory_mappings: [ + { + name: "i2c_regs", + type: "device", + start_address: 0x7e7b0000, + size_bytes: 0x4000, + }, + ], + }, + ], + }, + { + name: "mctp_server_peer", + flash_size_bytes: 131072, + processes: [ + { + name: "mctp_server_peer_process", + ram_size_bytes: 65536, + objects: [ + { + name: "wg", + type: "wait_group", + }, + { + name: "mctp", + type: "channel_handler", + }, + { + name: "i2c", + type: "channel_initiator", + handler_process: "i2c_server_peer_process", + handler_object_name: "i2c", + }, + { + type: "thread", + name: "mctp_server_peer_thread", + kernel_stack_size_bytes: 4096, + }, + ], + }, + ], + }, + { + 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_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/server/system.json5 b/target/ast10x0/tests/mctp/server/system.json5 new file mode 100644 index 0000000..61387b2 --- /dev/null +++ b/target/ast10x0/tests/mctp/server/system.json5
@@ -0,0 +1,120 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +// AST10x0 MCTP Server Configuration +// +// Multi-app configuration: +// - i2c_server: owns i2c hardware and runs i2c_server_runtime +// - mctp_server: uses i2c transport over IPC + serves MCTP control 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: "i2c_server", + flash_size_bytes: 65536, + processes: [ + { + name: "i2c_server_process", + ram_size_bytes: 65536, + objects: [ + { + name: "wg", + type: "wait_group", + }, + { + name: "i2c", + type: "channel_handler", + }, + { + name: "i2c2_irq", + type: "interrupt", + irqs: [ + { + name: "i2c2", + number: 112, + }, + ], + }, + { + type: "thread", + name: "i2c_server_thread", + kernel_stack_size_bytes: 4096, + }, + ], + memory_mappings: [ + { + name: "i2c_regs", + type: "device", + start_address: 0x7e7b0000, + size_bytes: 0x4000, + }, + ], + }, + ], + }, + { + name: "mctp_server", + flash_size_bytes: 131072, + processes: [ + { + name: "mctp_server_process", + ram_size_bytes: 65536, + objects: [ + { + name: "wg", + type: "wait_group", + }, + { + name: "mctp", + type: "channel_handler", + }, + { + name: "i2c", + type: "channel_initiator", + handler_process: "i2c_server_process", + handler_object_name: "i2c", + }, + { + type: "thread", + name: "mctp_server_thread", + kernel_stack_size_bytes: 4096, + }, + ], + }, + ], + }, + { + 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_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/server/target.rs b/target/ast10x0/tests/mctp/server/target.rs new file mode 100644 index 0000000..ed4f034 --- /dev/null +++ b/target/ast10x0/tests/mctp/server/target.rs
@@ -0,0 +1,64 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +#![no_std] +#![no_main] + +use ast10x0_board::{Ast10x0Board, Ast10x0BoardDescriptor, I2cBusCfg}; +use ast10x0_peripherals::i2c::{ClockConfig, I2cConfig, I2cSpeed, I2cXferMode}; +use ast10x0_peripherals::scu::pinctrl; +use console_backend::console_backend_write_all; +use entry as _; +use target_common::{declare_target, TargetInterface}; + +pub struct Target; + +const I2C2_CFG: I2cConfig = I2cConfig { + speed: I2cSpeed::Standard, + xfer_mode: I2cXferMode::DmaMode, + multi_master: false, + smbus_timeout: false, + smbus_alert: false, + clock_config: ClockConfig::ast1060_default(), +}; + +static PINCTRL_GROUPS: [&[ast10x0_peripherals::scu::PinctrlPin]; 1] = [pinctrl::PINCTRL_I2C2]; +static I2C_BUSES: [I2cBusCfg; 1] = [I2cBusCfg { + bus: 2, + config: I2C2_CFG, +}]; + +impl TargetInterface for Target { + const NAME: &'static str = "AST10x0 MCTP Server"; + + fn main() -> ! { + // SAFETY: kernel main() runs once with exclusive hardware ownership. + if unsafe { + Ast10x0Board::new(Ast10x0BoardDescriptor { + pinctrl_groups: &PINCTRL_GROUPS, + i2c_buses: &I2C_BUSES, + }) + .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/third_party/crates_io/Cargo.toml b/third_party/crates_io/Cargo.toml index a83b7b6..b30765d 100644 --- a/third_party/crates_io/Cargo.toml +++ b/third_party/crates_io/Cargo.toml
@@ -51,7 +51,7 @@ fugit = "0.3.7" openprot-hal-blocking = { git = "https://github.com/rusty1968/openprot.git", rev = "c6cd23a" } heapless = { version = "0.9", default-features = false } -mctp = { git = "https://github.com/CodeConstruct/mctp-rs.git" } +mctp = { git = "https://github.com/CodeConstruct/mctp-rs.git", default-features = false } mctp-lib = { git = "https://github.com/OpenPRoT/mctp-lib.git", branch = "9e-buildup" } spdm-lib = { git = "https://github.com/OpenPRoT/spdm-lib.git", branch = "main" } # Pin to match Hubris ecosystem