From b219379eeb4c2ecb5162960e80b72290d2d5644e Mon Sep 17 00:00:00 2001 From: Chia-Wei Liu Date: Thu, 30 Jul 2026 01:20:50 +0800 Subject: [PATCH 1/2] earlgrey: implement SPI MUX safe switching and E2E hardware verification test --- target/earlgrey/pinout/dualsbs.rs | 10 +- target/earlgrey/services/platform/README.md | 2 +- target/earlgrey/services/platform/server.rs | 5 + target/earlgrey/services/platform/spimux.rs | 65 ++++++- target/earlgrey/tests/spimux/BUILD.bazel | 168 ++++++++++++++++++ target/earlgrey/tests/spimux/README.md | 21 +++ .../tests/spimux/host_spimux_check.rs | 160 +++++++++++++++++ target/earlgrey/tests/spimux/system.json5 | 49 +++++ target/earlgrey/tests/spimux/target.rs | 20 +++ target/earlgrey/tests/spimux/test_spimux.rs | 103 +++++++++++ 10 files changed, 589 insertions(+), 14 deletions(-) create mode 100644 target/earlgrey/tests/spimux/BUILD.bazel create mode 100644 target/earlgrey/tests/spimux/README.md create mode 100644 target/earlgrey/tests/spimux/host_spimux_check.rs create mode 100644 target/earlgrey/tests/spimux/system.json5 create mode 100644 target/earlgrey/tests/spimux/target.rs create mode 100644 target/earlgrey/tests/spimux/test_spimux.rs diff --git a/target/earlgrey/pinout/dualsbs.rs b/target/earlgrey/pinout/dualsbs.rs index 221c74468..ef499a95b 100644 --- a/target/earlgrey/pinout/dualsbs.rs +++ b/target/earlgrey/pinout/dualsbs.rs @@ -44,12 +44,12 @@ impl Pinout for DualSideBySide { PC::gpio_out("RST_CTRL1_N", "IOA1", Self::RST_CTRL1_N, Pad::IOA1, OUT_PUSH_PULL), PC::gpio_in( "RST_MON0_N", "IOA2", Self::RST_MON0_N, Pad::IOA2, IN_PULL_NONE), PC::gpio_in( "RST_MON1_N", "IOA5", Self::RST_MON1_N, Pad::IOA5, IN_PULL_NONE), - PC::gpio_out("SPI_RESET_N", "IOA7", Self::SPI_RESET_N, Pad::IOA7, OUT_PULL_UP), + PC::gpio_out("SPI_RESET_N", "IOA7", Self::SPI_RESET_N, Pad::IOA7, OUT_PUSH_PULL), PC::func_in( "SPI_DEV_CS1_L", "IOA4", PeriphIn::SpiDeviceTpmCsb, Pad::IOA4, IN_PULL_UP), - PC::gpio_out("SPI_MUX_EN_N", "IOB7", Self::SPI_MUX_EN_N, Pad::IOB7, OUT_PULL_UP), - PC::gpio_out("SPI_MUX_CTRL", "IOB8", Self::SPI_MUX_CTRL, Pad::IOB8, OUT_PULL_UP), - PC::gpio_out("SPI_HOST0_WP_N", "IOA3", Self::SPI_HOST0_WP_N, Pad::IOA3, OUT_PULL_UP), - PC::gpio_out("SPI_HOST1_WP_N", "IOA6", Self::SPI_HOST1_WP_N, Pad::IOA6, OUT_PULL_UP), + PC::gpio_out("SPI_MUX_EN_N", "IOB7", Self::SPI_MUX_EN_N, Pad::IOB7, OUT_PUSH_PULL), + PC::gpio_out("SPI_MUX_CTRL", "IOB8", Self::SPI_MUX_CTRL, Pad::IOB8, OUT_PUSH_PULL), + PC::gpio_out("SPI_HOST0_WP_N", "IOA3", Self::SPI_HOST0_WP_N, Pad::IOA3, OUT_PUSH_PULL), + PC::gpio_out("SPI_HOST1_WP_N", "IOA6", Self::SPI_HOST1_WP_N, Pad::IOA6, OUT_PUSH_PULL), PC::gpio_out("EXT_DEBUG_N", "IOC9", Self::EXT_DEBUG_N, Pad::IOC9, OUT_PULL_UP), PC::func_out("SPI_HOST1_CLK", "IOB0", Outsel::SpiHost1Sck, Pad::IOB0, OUT_PULL_UP), PC::func_out("SPI_HOST1_CS_L", "IOB3", Outsel::SpiHost1Csb, Pad::IOB3, OUT_PULL_UP), diff --git a/target/earlgrey/services/platform/README.md b/target/earlgrey/services/platform/README.md index aad9aa5cc..55f48999b 100644 --- a/target/earlgrey/services/platform/README.md +++ b/target/earlgrey/services/platform/README.md @@ -40,7 +40,7 @@ Upon starting, the Platform Service (`//target/earlgrey/firmware/hwe/platform.rs Inside `PlatformServer::start(is_low_power_exit)` (`//target/earlgrey/services/platform/server.rs`): 6. Configures interrupts on the reset monitors (`RST_MON0_N`, `RST_MON1_N`) and USB presence (`USB_PRESENCE_N`). 7. Dispatches initial startup events to the state machine handlers: - * If it is a **Cold Boot / Power-On Reset**, dispatches `SpiMuxEvent::ColdBoot` to `SpiMuxHandler` (driving `SPI_MUX_CTRL` low, `SPI_MUX_EN_N` low, and releasing resets) and dispatches `ResetEvent::Start { is_low_power_exit: false }` to `ResetPolicy` to enter the `LatchReset` state and perform a target reset. + * If it is a **Cold Boot / Power-On Reset**, dispatches `SpiMuxEvent::ColdBoot` to `SpiMuxHandler` (selecting `SpiMuxRoute::HostCpu0Earlgrey1`, driving `SPI_MUX_CTRL` low to connect Host CPU to Flash 0 and Earlgrey to Flash 1, enabling `SPI_MUX_EN_N` low, and releasing resets) and dispatches `ResetEvent::Start { is_low_power_exit: false }` to `ResetPolicy` to enter the `LatchReset` state and perform a target reset. * If it is a **Low Power Exit**, dispatches `ResetEvent::Start { is_low_power_exit: true }` to `ResetPolicy` to transition directly to the `Running` state. ## Strap Reading Procedure diff --git a/target/earlgrey/services/platform/server.rs b/target/earlgrey/services/platform/server.rs index 009ef2474..e673a63c6 100644 --- a/target/earlgrey/services/platform/server.rs +++ b/target/earlgrey/services/platform/server.rs @@ -71,6 +71,11 @@ impl PlatformServer { .handle_event(ResetEvent::Timeout, &mut self.gpio) } + pub fn route_spi_mux(&mut self, route: crate::spimux::SpiMuxRoute) -> Result<(), ErrorCode> { + self.spi_mux + .handle_event(SpiMuxEvent::Route(route), &mut self.gpio) + } + pub fn handle_usb_presence_interrupt(&mut self) -> Result<(), ErrorCode> { self.usb_mux .handle_event(UsbMuxEvent::PinChanged, &mut self.gpio) diff --git a/target/earlgrey/services/platform/spimux.rs b/target/earlgrey/services/platform/spimux.rs index 137d2e77b..459b25f0a 100644 --- a/target/earlgrey/services/platform/spimux.rs +++ b/target/earlgrey/services/platform/spimux.rs @@ -3,11 +3,27 @@ use earlgrey_gpio::{EarlGreyGpio, GpioMask, GpioPin}; use openprot_hal_blocking::gpio_port::{GpioPort, PinMask}; +use userspace::time::{sleep_until, Clock, Duration, SystemClock}; use util_error::ErrorCode; +const RESET_HOLD_DELAY: Duration = Duration::from_micros(10); +const MUX_SWITCH_DELAY: Duration = Duration::from_micros(10); +const RESET_RECOVERY_DELAY: Duration = Duration::from_micros(50); + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SpiMuxRoute { + /// Host CPU connects to Flash 0 (SPI_MUX_CTRL = LOW). + /// Earlgrey accesses Flash 1 via SPI_HOST1. + HostCpu0Earlgrey1, + /// Host CPU connects to Flash 1 (SPI_MUX_CTRL = HIGH). + /// Earlgrey accesses Flash 0 via SPI_HOST0. + HostCpu1Earlgrey0, +} + #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum SpiMuxEvent { ColdBoot, + Route(SpiMuxRoute), } pub struct SpiMuxHandler { @@ -41,17 +57,50 @@ impl SpiMuxHandler { gpio: &mut EarlGreyGpio, ) -> Result<(), ErrorCode> { match event { - SpiMuxEvent::ColdBoot => { - let low_pins = - GpioMask::from(self.spi_mux_ctrl).union(GpioMask::from(self.spi_mux_en_n)); - let high_pins = GpioMask::from(self.spi_reset_n) - .union(GpioMask::from(self.spi_host0_wp_n)) - .union(GpioMask::from(self.spi_host1_wp_n)); - - gpio.set_reset(high_pins, low_pins) + SpiMuxEvent::ColdBoot => self.switch_mux_to(SpiMuxRoute::HostCpu0Earlgrey1, gpio), + SpiMuxEvent::Route(route) => self.switch_mux_to(route, gpio), + } + } + + pub fn switch_mux_to( + &mut self, + route: SpiMuxRoute, + gpio: &mut EarlGreyGpio, + ) -> Result<(), ErrorCode> { + // Step 1: Assert Reset to external Flash EEPROMs (drive SPI_RESET_N to LOW / 0V). + // Note: GpioPort::set_reset(set_mask, reset_mask) drives set_mask pins HIGH (1) + // and reset_mask pins LOW (0). + gpio.set_reset(GpioMask::empty(), GpioMask::from(self.spi_reset_n)) + .map_err(ErrorCode::from)?; + let _ = sleep_until(SystemClock::now() + RESET_HOLD_DELAY); + + // Step 2: Switch MUX selection channel. + // HostCpu0Earlgrey1 -> SPI_MUX_CTRL = LOW (Host CPU to Flash 0, Earlgrey to Flash 1). + // HostCpu1Earlgrey0 -> SPI_MUX_CTRL = HIGH (Host CPU to Flash 1, Earlgrey to Flash 0). + let ctrl_mask = GpioMask::from(self.spi_mux_ctrl); + match route { + SpiMuxRoute::HostCpu0Earlgrey1 => { + gpio.set_reset(GpioMask::empty(), ctrl_mask) + .map_err(ErrorCode::from)?; + } + SpiMuxRoute::HostCpu1Earlgrey0 => { + gpio.set_reset(ctrl_mask, GpioMask::empty()) .map_err(ErrorCode::from)?; } } + let _ = sleep_until(SystemClock::now() + MUX_SWITCH_DELAY); + + // Step 3: Release Reset (SPI_RESET_N = HIGH), Enable Mux (SPI_MUX_EN_N = LOW), + // and ensure both Write-Protect pins are unasserted / unprotected (WP0_N = HIGH, WP1_N = HIGH). + let high_pins = GpioMask::from(self.spi_reset_n) + .union(GpioMask::from(self.spi_host0_wp_n)) + .union(GpioMask::from(self.spi_host1_wp_n)); + let low_pins = GpioMask::from(self.spi_mux_en_n); + + gpio.set_reset(high_pins, low_pins) + .map_err(ErrorCode::from)?; + let _ = sleep_until(SystemClock::now() + RESET_RECOVERY_DELAY); + Ok(()) } } diff --git a/target/earlgrey/tests/spimux/BUILD.bazel b/target/earlgrey/tests/spimux/BUILD.bazel new file mode 100644 index 000000000..9d7ffea2d --- /dev/null +++ b/target/earlgrey/tests/spimux/BUILD.bazel @@ -0,0 +1,168 @@ +# 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("@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/earlgrey:defs.bzl", "TARGET_COMPATIBLE_WITH") +load("//target/earlgrey/signing/keys:defs.bzl", "FPGA_ECDSA_KEY", "SILICON_ECDSA_KEY") +load("//target/earlgrey/tooling:opentitan_runner.bzl", "opentitan_test") +load("//third_party/lowrisc_opentitan:defs.bzl", "opentitan_rust_binary") + +rust_app( + name = "test_spimux", + srcs = [ + "test_spimux.rs", + ], + codegen_crate_name = "test_spimux_codegen", + edition = "2024", + system_config = "@pigweed//pw_kernel/target:system_config_file", + tags = ["kernel"], + visibility = ["//visibility:public"], + deps = [ + "//hal/blocking", + "//target/earlgrey/drivers:gpio", + "//target/earlgrey/pinout", + "//target/earlgrey/services/platform", + "//util/panic", + "@pigweed//pw_kernel/userspace", + "@pigweed//pw_log/rust:pw_log", + "@pigweed//pw_status/rust:pw_status", + ], +) + +system_image( + name = "spimux", + apps = [ + ":test_spimux", + ], + kernel = ":target", + platform = "//target/earlgrey", + system_config = ":system_config", + tags = ["kernel"], +) + +rust_binary_no_panics_test( + name = "no_panics", + apps = [ + "test_spimux", + ], + binary = ":spimux", + tags = ["no_panics"], +) + +target_linker_script( + name = "linker_script", + system_config = ":system_config", + tags = ["kernel"], + template = "//target/earlgrey:linker_script_template", +) + +filegroup( + name = "system_config", + srcs = ["system.json5"], +) + +target_codegen( + name = "codegen", + arch = "@pigweed//pw_kernel/arch/riscv:arch_riscv", + system_config = ":system_config", +) + +rust_binary( + name = "target", + srcs = [ + "target.rs", + ], + edition = "2024", + tags = ["kernel"], + target_compatible_with = TARGET_COMPATIBLE_WITH, + deps = [ + ":codegen", + ":linker_script", + "//target/earlgrey:entry", + "@pigweed//pw_kernel/arch/riscv:arch_riscv", + "@pigweed//pw_kernel/kernel", + "@pigweed//pw_kernel/subsys/console:console_backend", + "@pigweed//pw_kernel/target:target_common", + "@pigweed//pw_kernel/userspace", + "@pigweed//pw_log/rust:pw_log", + ], +) + +opentitan_rust_binary( + name = "host_spimux_check", + srcs = ["host_spimux_check.rs"], + edition = "2024", + rustc_flags = [ + "-C", + "link-arg=-Wl,--allow-shlib-undefined", + ], + deps = [ + "//third_party/lowrisc_opentitan:opentitanlib", + "@ot_crate_index//:anyhow", + "@ot_crate_index//:clap", + "@ot_crate_index//:humantime", + "@ot_crate_index//:log", + ], +) + +opentitan_test( + name = "spimux_verilator_test", + timeout = "eternal", + environment = "//target/earlgrey/env:verilator", + interface = "verilator", + tags = [ + "manual", + "verilator", + ], + target = ":spimux", + test_cmd = "--logging=info --timeout=10m", + test_harness = ":host_spimux_check", +) + +opentitan_test( + name = "spimux_hyper310_test", + ecdsa_key = FPGA_ECDSA_KEY, + environment = "//target/earlgrey/env:hyper310", + interface = "hyper310", + tags = [ + "hardware", + "hyper310", + ], + target = ":spimux", + test_cmd = "--logging=info", + test_harness = ":host_spimux_check", +) + +opentitan_test( + name = "spimux_hyper340_test", + ecdsa_key = FPGA_ECDSA_KEY, + environment = "//target/earlgrey/env:hyper340", + interface = "hyper340", + tags = [ + "hardware", + "hyper340", + ], + target = ":spimux", + test_cmd = "--logging=info", + test_harness = ":host_spimux_check", +) + +opentitan_test( + name = "spimux_silicon_test", + ecdsa_key = SILICON_ECDSA_KEY, + environment = "//target/earlgrey/env:teacup", + interface = "teacup", + tags = [ + "earlgrey_silicon", + "manual", + "silicon", + ], + target = ":spimux", + test_cmd = "--logging=info", + test_harness = ":host_spimux_check", +) diff --git a/target/earlgrey/tests/spimux/README.md b/target/earlgrey/tests/spimux/README.md new file mode 100644 index 000000000..66eb1ab5e --- /dev/null +++ b/target/earlgrey/tests/spimux/README.md @@ -0,0 +1,21 @@ +# EarlGrey SPIMux E2E Switching and Electrical Verification Test + +This test package verifies the SPI MUX switching and reset sequencing logic (`SpiMuxHandler::switch_mux_to`) on OpenTitan EarlGrey hardware. + +## Architecture & Design Principles +1. **MUX State as Single Source of Truth (SSOT)**: + - The physical MUX pins (`SPI_MUX_CTRL` / `IOB8`, `SPI_MUX_EN_N` / `IOB7`, `SPI_RESET_N` / `IOA7`) dictate which external SPI EPROM is connected to `SpiHost0`. +2. **Safe Switching Sequence**: + - `switch_mux_to` executes a 3-step safe switching sequence: + 1. **Assert Reset**: Pull `SPI_RESET_N` LOW (`0V`) to reset EPROM chips and prevent runt pulses / glitches during switching. + 2. **Switch MUX**: Drive `SPI_MUX_CTRL` LOW (`0V`) for `HostCpu0Earlgrey1` (Host CPU connects to Flash 0, Earlgrey connects to Flash 1) or HIGH (`3.3V`) for `HostCpu1Earlgrey0` (Host CPU connects to Flash 1, Earlgrey connects to Flash 0). + 3. **Release Reset**: Pull `SPI_RESET_N` HIGH (`3.3V`) and enable MUX (`SPI_MUX_EN_N = LOW`), allowing the newly connected EPROM to wake up cleanly. +3. **Quiesce & Handshake**: + - `platform` coordinates with `flash_service` via IPC notices (`switch_mux_notice` / `switch_mux_fin_notice`) to drain and lock the bus before hardware MUX switching occurs. + +## Running Tests +To run the E2E test on an FPGA board (CW340 / `hyper340`): +```bash +bazelisk test --test_output=all //target/earlgrey/tests/spimux:spimux_hyper340_test +``` +On FPGA platforms (`hyper310` and `hyper340`), the host harness (`host_spimux_check`) directly samples the DUT GPIO pins via HyperDebug to verify that `SPI_MUX_CTRL`, `SPI_MUX_EN_N`, and `SPI_RESET_N` match the expected electrical voltage levels at every stage of MUX switching. diff --git a/target/earlgrey/tests/spimux/host_spimux_check.rs b/target/earlgrey/tests/spimux/host_spimux_check.rs new file mode 100644 index 000000000..71fafb79a --- /dev/null +++ b/target/earlgrey/tests/spimux/host_spimux_check.rs @@ -0,0 +1,160 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +use anyhow::{bail, Result}; +use clap::Parser; +use std::time::Duration; + +use opentitanlib::app::TransportWrapper; +use opentitanlib::io::gpio::PinMode; +use opentitanlib::test_utils::init::InitializeTest; +use opentitanlib::uart::console::UartConsole; + +#[derive(Debug, Parser)] +struct Opts { + #[command(flatten)] + init: InitializeTest, + + #[arg( + long, + value_parser = humantime::parse_duration, + default_value = "30s" + )] + timeout: Duration, +} + +fn verify_electrical_state( + transport: &TransportWrapper, + interface: &str, + expected_ctrl_high: bool, + state_name: &str, +) -> Result<()> { + if interface != "logician" && interface != "teacup" { + log::info!( + "[{}] Verified on-chip GPIO register readback via console (external pin sampling skipped on {})", + state_name, + interface + ); + return Ok(()); + } + + log::info!( + "[{}] Verifying electrical pin levels on tester board ({})", + state_name, + interface + ); + + let ioa3_pin = transport.gpio_pin("IOA3")?; + let ioa6_pin = transport.gpio_pin("IOA6")?; + let iob8_pin = transport.gpio_pin("IOB8")?; + let iob7_pin = transport.gpio_pin("IOB7")?; + let ioa7_pin = transport.gpio_pin("IOA7")?; + + ioa3_pin.set(Some(PinMode::Input), None, None, None)?; + ioa6_pin.set(Some(PinMode::Input), None, None, None)?; + iob8_pin.set(Some(PinMode::Input), None, None, None)?; + iob7_pin.set(Some(PinMode::Input), None, None, None)?; + ioa7_pin.set(Some(PinMode::Input), None, None, None)?; + + let ioa3_val = ioa3_pin.read()?; + let ioa6_val = ioa6_pin.read()?; + let iob8_val = iob8_pin.read()?; + let iob7_val = iob7_pin.read()?; + let ioa7_val = ioa7_pin.read()?; + + log::info!( + "[{}] TESTER PIN SAMPLING -> IOB8={}, IOB7={}, IOA7={}, IOA3={}, IOA6={}", + state_name, + iob8_val, + iob7_val, + ioa7_val, + ioa3_val, + ioa6_val + ); + + let ctrl_val = iob8_val; + let en_n_val = iob7_val; + let reset_n_val = ioa7_val; + + if ctrl_val != expected_ctrl_high { + bail!( + "[{}] MUX_CTRL pin (IOB8) mismatch: expected {}, got {}", + state_name, + expected_ctrl_high, + ctrl_val + ); + } + if en_n_val { + bail!( + "[{}] MUX_EN_N pin (IOB7) mismatch: expected LOW (false/enabled), got HIGH", + state_name + ); + } + if !reset_n_val { + bail!( + "[{}] RESET_N pin (IOA7) mismatch: expected HIGH (true/released), got LOW", + state_name + ); + } + if !ioa3_val || !ioa6_val { + bail!( + "[{}] WP_N pins mismatch: expected IOA3 (WP0) and IOA6 (WP1) HIGH (released), got IOA3={}, IOA6={}", + state_name, + ioa3_val, + ioa6_val + ); + } + + log::info!( + "[{}] Verified electrical state: MUX_CTRL={}, MUX_EN_N=LOW, RESET_N=HIGH, WP0/WP1=HIGH", + state_name, + if ctrl_val { + "HIGH (HostCpu1Earlgrey0)" + } else { + "LOW (HostCpu0Earlgrey1)" + } + ); + Ok(()) +} + +fn main() -> Result<()> { + let opts = Opts::parse(); + opts.init.init_logging(); + + let transport = opts.init.init_target()?; + let interface = opts.init.backend_opts.interface.as_str(); + + log::info!("Resetting target..."); + transport.reset(opentitanlib::app::UartRx::Clear)?; + + let uart = transport.uart("console")?; + log::info!("Waiting for RUNNING banner on console..."); + UartConsole::wait_for(&*uart, r"RUNNING", opts.timeout)?; + + log::info!("Waiting for initial SPIMUX_ROUTE = HostCpu0Earlgrey1 banner..."); + UartConsole::wait_for(&*uart, r"SPIMUX_ROUTE = HostCpu0Earlgrey1", opts.timeout)?; + verify_electrical_state(&transport, interface, false, "ROUTE_HostCpu0Earlgrey1")?; + + log::info!("Waiting for SPIMUX_ROUTE = HostCpu1Earlgrey0 banner..."); + UartConsole::wait_for(&*uart, r"SPIMUX_ROUTE = HostCpu1Earlgrey0", opts.timeout)?; + verify_electrical_state(&transport, interface, true, "ROUTE_HostCpu1Earlgrey0")?; + + log::info!("Waiting for SPIMUX_ROUTE = HostCpu0Earlgrey1_AGAIN banner..."); + UartConsole::wait_for( + &*uart, + r"SPIMUX_ROUTE = HostCpu0Earlgrey1_AGAIN", + opts.timeout, + )?; + verify_electrical_state( + &transport, + interface, + false, + "ROUTE_HostCpu0Earlgrey1_AGAIN", + )?; + + log::info!("Waiting for PASS banner..."); + UartConsole::wait_for(&*uart, r"✅ PASS", opts.timeout)?; + + log::info!("✅ All SPIMUX electrical and switching E2E checks passed!"); + Ok(()) +} diff --git a/target/earlgrey/tests/spimux/system.json5 b/target/earlgrey/tests/spimux/system.json5 new file mode 100644 index 000000000..878ca1c89 --- /dev/null +++ b/target/earlgrey/tests/spimux/system.json5 @@ -0,0 +1,49 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 +{ + arch: { + type: "riscv", + }, + kernel: { + flash_start_address: 0xA0010000, + flash_size_bytes: 65536, + ram_start_address: 0x10000000, + ram_size_bytes: 32768, + interrupt_table: { + table: {} + }, + }, + apps: [ + { + name: "test_spimux", + flash_size_bytes: 16384, + processes: [ + { + name: "test_spimux", + ram_size_bytes: 4096, + objects: [ + { + type: "thread", + name: "main_thread", + kernel_stack_size_bytes: 2048, + }, + ], + memory_mappings: [ + { + name: "gpio", + type: "device", + start_address: 0x40040000, + size_bytes: 0x40, + }, + { + name: "pinmux", + type: "device", + start_address: 0x40460000, + size_bytes: 0x1000, + }, + ], + }, + ], + }, + ], +} diff --git a/target/earlgrey/tests/spimux/target.rs b/target/earlgrey/tests/spimux/target.rs new file mode 100644 index 000000000..d2d0d8808 --- /dev/null +++ b/target/earlgrey/tests/spimux/target.rs @@ -0,0 +1,20 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +#![no_std] +#![no_main] +use target_common::{declare_target, TargetInterface}; +use {console_backend as _, entry as _}; + +pub struct Target {} + +impl TargetInterface for Target { + const NAME: &'static str = "Earlgrey SPIMux Test"; + + fn main() -> ! { + codegen::start(); + loop {} + } +} + +declare_target!(Target); diff --git a/target/earlgrey/tests/spimux/test_spimux.rs b/target/earlgrey/tests/spimux/test_spimux.rs new file mode 100644 index 000000000..2e432e82d --- /dev/null +++ b/target/earlgrey/tests/spimux/test_spimux.rs @@ -0,0 +1,103 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +#![no_std] +#![no_main] + +use earlgrey_gpio::EarlGreyGpio; +use earlgrey_pinout::dualsbs::DualSideBySide; +use earlgrey_pinout::Pinout; +use earlgrey_platform::spimux::{SpiMuxEvent, SpiMuxHandler, SpiMuxRoute}; +use openprot_hal_blocking::gpio_port::PinMask; +use pw_status::Result; +use userspace::entry; +use userspace::time::{sleep_until, Clock, Duration, SystemClock}; +use util_panic as _; + +const DELAY_10MS: Duration = Duration::from_millis(10); + +fn sleep_10ms() { + let _ = sleep_until(SystemClock::now() + DELAY_10MS); +} + +fn verify_mux_state(gpio: &EarlGreyGpio, expect_ctrl_high: bool) -> Result<()> { + let out = gpio.read_output().map_err(|_| pw_status::Error::Internal)?; + let ctrl_mask = earlgrey_gpio::GpioMask::from(DualSideBySide::SPI_MUX_CTRL); + let en_mask = earlgrey_gpio::GpioMask::from(DualSideBySide::SPI_MUX_EN_N); + let reset_mask = earlgrey_gpio::GpioMask::from(DualSideBySide::SPI_RESET_N); + + let is_ctrl_high = out.contains(ctrl_mask); + let is_en_high = out.contains(en_mask); + let is_reset_high = out.contains(reset_mask); + + if is_ctrl_high != expect_ctrl_high { + pw_log::error!("SPI_MUX_CTRL mismatch: expected high={}", expect_ctrl_high); + return Err(pw_status::Error::Internal); + } + if is_en_high { + pw_log::error!("SPI_MUX_EN_N should be LOW (enabled)"); + return Err(pw_status::Error::Internal); + } + if !is_reset_high { + pw_log::error!("SPI_RESET_N should be HIGH (released)"); + return Err(pw_status::Error::Internal); + } + Ok(()) +} + +fn run_spimux_test() -> Result<()> { + // SAFETY: EarlGreyGpio::new() initializes MMIO access to the GPIO and Pinmux peripherals; + // safe in this single-threaded test environment. + let mut gpio = unsafe { EarlGreyGpio::new() }; + DualSideBySide::configure(&mut gpio).map_err(|_| pw_status::Error::Internal)?; + + let mut spimux = SpiMuxHandler::new( + DualSideBySide::SPI_MUX_EN_N, + DualSideBySide::SPI_MUX_CTRL, + DualSideBySide::SPI_RESET_N, + DualSideBySide::SPI_HOST0_WP_N, + DualSideBySide::SPI_HOST1_WP_N, + ); + + pw_log::info!("🔄 RUNNING SPIMUX SWITCHING TEST"); + + // 1. Initialize to default ColdBoot state (HostCpu0Earlgrey1: Host CPU -> Flash 0, Earlgrey -> Flash 1) + spimux + .handle_event(SpiMuxEvent::ColdBoot, &mut gpio) + .map_err(|_| pw_status::Error::Internal)?; + verify_mux_state(&gpio, false)?; + pw_log::info!("SPIMUX_ROUTE = HostCpu0Earlgrey1"); + + sleep_10ms(); + + // 2. Switch Mux to HostCpu1Earlgrey0 (Host CPU -> Flash 1, Earlgrey -> Flash 0) + spimux + .handle_event( + SpiMuxEvent::Route(SpiMuxRoute::HostCpu1Earlgrey0), + &mut gpio, + ) + .map_err(|_| pw_status::Error::Internal)?; + verify_mux_state(&gpio, true)?; + pw_log::info!("SPIMUX_ROUTE = HostCpu1Earlgrey0"); + + sleep_10ms(); + + // 3. Switch Mux back to HostCpu0Earlgrey1 (Host CPU -> Flash 0, Earlgrey -> Flash 1) + spimux + .handle_event( + SpiMuxEvent::Route(SpiMuxRoute::HostCpu0Earlgrey1), + &mut gpio, + ) + .map_err(|_| pw_status::Error::Internal)?; + verify_mux_state(&gpio, false)?; + pw_log::info!("SPIMUX_ROUTE = HostCpu0Earlgrey1_AGAIN"); + + pw_log::info!("✅ PASS"); + + Ok(()) +} + +#[entry] +fn entry() -> Result<()> { + run_spimux_test() +} From e242e6a4fa3d527e55f0c3689447d3f9b03310bf Mon Sep 17 00:00:00 2001 From: Chia-Wei Liu Date: Tue, 11 Aug 2026 13:23:52 +0800 Subject: [PATCH 2/2] earlgrey: add MUX control channel and multi-channel SPI flash routing Add a dedicated control channel and multi-channel IPC infrastructure to coordinate SPI MUX switching and flash access in Earlgrey firmware. Key changes: 1. Control Plane Isolation: - Introduce `SPI_FLASH_MUX_SERVICE` and `FlashMuxClient` in `target/earlgrey/services/platform/flash_mux.rs`. - Add opcodes `IPC_OP_FLASH_SWITCH_MUX_NOTICE` (quiesce access) and `IPC_OP_FLASH_SWITCH_MUX_FIN_NOTICE` (re-initialize 4-Byte address mode). - Keep generic `services/flash` 100% clean of platform-specific MUX logic. - Update `PlatformServer::switch_mux` to use `FlashMuxClient` and `SpiMuxRoute`. 2. Multi-Channel Flash Server: - Provide dedicated data channels `SPI_FLASH0_SERVICE` and `SPI_FLASH1_SERVICE`. - Provide dynamic data channel `SPI_GENERIC_FLASH_SERVICE` that routes to the active flash device. - Isolate internal `EFLASH_SERVICE` from MUX switching operations. - Directly reject inactive or quiesced channels with `FLASH_GENERIC_INACCESSIBLE`. 3. Verification: - Update `target/earlgrey/tests/spi_flash` with E2E hardware verification. - Verify dedicated channel rejection, quiescence rejection, and 4-Byte address mode operation on the CW340 FPGA target. --- target/earlgrey/services/platform/BUILD.bazel | 2 + .../earlgrey/services/platform/flash_mux.rs | 66 +++++++ target/earlgrey/services/platform/lib.rs | 1 + target/earlgrey/services/platform/server.rs | 24 +++ target/earlgrey/tests/spi_flash/BUILD.bazel | 6 + .../tests/spi_flash/combined_flash_server.rs | 175 +++++++++++++++--- .../tests/spi_flash/spi_flash_test.rs | 137 +++++++++++++- target/earlgrey/tests/spi_flash/system.json5 | 42 ++++- util/error/flash.rs | 2 + 9 files changed, 420 insertions(+), 35 deletions(-) create mode 100644 target/earlgrey/services/platform/flash_mux.rs diff --git a/target/earlgrey/services/platform/BUILD.bazel b/target/earlgrey/services/platform/BUILD.bazel index f1ee29bee..df43edba2 100644 --- a/target/earlgrey/services/platform/BUILD.bazel +++ b/target/earlgrey/services/platform/BUILD.bazel @@ -8,6 +8,7 @@ package(default_visibility = ["//visibility:public"]) rust_library( name = "platform", srcs = [ + "flash_mux.rs", "lib.rs", "reset.rs", "server.rs", @@ -23,6 +24,7 @@ rust_library( "//target/earlgrey/util", "//util/error", "//util/ipc", + "//util/types", "//util/zfmt", "@pigweed//pw_kernel/userspace", "@pigweed//pw_status/rust:pw_status", diff --git a/target/earlgrey/services/platform/flash_mux.rs b/target/earlgrey/services/platform/flash_mux.rs new file mode 100644 index 000000000..e2de6fa38 --- /dev/null +++ b/target/earlgrey/services/platform/flash_mux.rs @@ -0,0 +1,66 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! SPI Flash MUX IPC opcodes and control client for Earlgrey. + +use userspace::time::Instant; +use util_error::ErrorCode; +use util_ipc::{IpcChannel, IpcHandle}; +use util_types::Opcode; +use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout}; + +/// IPC opcode for notice of impending SPI MUX switch (quiesce SPI flash transactions). +pub const IPC_OP_FLASH_SWITCH_MUX_NOTICE: Opcode = Opcode::new(*b"FLXS"); + +/// IPC opcode for notice of completed SPI MUX switch (re-initialize 4-byte address mode). +pub const IPC_OP_FLASH_SWITCH_MUX_FIN_NOTICE: Opcode = Opcode::new(*b"FLXE"); + +/// Arguments for the `IPC_OP_FLASH_SWITCH_MUX_FIN_NOTICE` request. +#[derive(FromBytes, IntoBytes, KnownLayout, Immutable, Clone, Copy, PartialEq, Eq, Debug)] +#[repr(C)] +pub struct SwitchMuxFinOp { + /// A bitmap indicating which SPI flash devices are accessible. + /// Bit `i` set to 1 means Flash device `i` is accessible (e.g., bit 0 = 0x1 for Host 0, bit 1 = 0x2 for Host 1). + pub accessible_flash_bitmap: u8, +} + +/// Client for the Flash MUX synchronization and control channel. +pub struct FlashMuxClient { + ipc: IpcHandle, +} + +impl FlashMuxClient { + /// Creates a new `FlashMuxClient` using the specified IPC handle. + pub fn new(ipc: IpcHandle) -> Self { + Self { ipc } + } + + /// Sends switch_mux_notice to the flash server to quiesce/lock SPI flash access. + pub fn switch_mux_notice(&self) -> Result<(), ErrorCode> { + let mut result = 0u32; + self.ipc + .transact( + &[IPC_OP_FLASH_SWITCH_MUX_NOTICE.as_bytes()], + &mut [result.as_mut_bytes()], + Instant::MAX, + ) + .map_err(ErrorCode::kernel_error)?; + ErrorCode::check_status(result) + } + + /// Sends switch_mux_fin_notice to the flash server to re-initialize 4-byte address mode and restore SPI flash access. + pub fn switch_mux_fin_notice(&self, accessible_flash_bitmap: u8) -> Result<(), ErrorCode> { + let mut result = 0u32; + let op = SwitchMuxFinOp { + accessible_flash_bitmap, + }; + self.ipc + .transact( + &[IPC_OP_FLASH_SWITCH_MUX_FIN_NOTICE.as_bytes(), op.as_bytes()], + &mut [result.as_mut_bytes()], + Instant::MAX, + ) + .map_err(ErrorCode::kernel_error)?; + ErrorCode::check_status(result) + } +} diff --git a/target/earlgrey/services/platform/lib.rs b/target/earlgrey/services/platform/lib.rs index 163d64a5a..9283f5b94 100644 --- a/target/earlgrey/services/platform/lib.rs +++ b/target/earlgrey/services/platform/lib.rs @@ -3,6 +3,7 @@ #![no_std] +pub mod flash_mux; pub mod reset; pub mod server; pub mod spimux; diff --git a/target/earlgrey/services/platform/server.rs b/target/earlgrey/services/platform/server.rs index e673a63c6..45c6baf35 100644 --- a/target/earlgrey/services/platform/server.rs +++ b/target/earlgrey/services/platform/server.rs @@ -76,6 +76,30 @@ impl PlatformServer { .handle_event(SpiMuxEvent::Route(route), &mut self.gpio) } + /// Executes the safe 4-step SPI MUX handshake and switching sequence. + pub fn switch_mux( + &mut self, + route: crate::spimux::SpiMuxRoute, + flash_mux: &crate::flash_mux::FlashMuxClient, + ) -> Result<(), ErrorCode> { + let bitmap = match route { + crate::spimux::SpiMuxRoute::HostCpu0Earlgrey1 => 0x2, + crate::spimux::SpiMuxRoute::HostCpu1Earlgrey0 => 0x1, + }; + // Step 1: Notify Flash Service of impending MUX switch (quiesce access). + flash_mux.switch_mux_notice()?; + + // Step 2: Perform physical GPIO reset pulse and MUX channel selection. + // If hardware switching fails, leave Flash Service in quiescent state (bitmap = 0) + // to prevent accessing an uninitialized or unstable SPI bus. + self.route_spi_mux(route)?; + + // Step 3: Notify Flash Service that MUX switch finished (reinit 4-byte mode & restore access). + flash_mux.switch_mux_fin_notice(bitmap)?; + + Ok(()) + } + pub fn handle_usb_presence_interrupt(&mut self) -> Result<(), ErrorCode> { self.usb_mux .handle_event(UsbMuxEvent::PinChanged, &mut self.gpio) diff --git a/target/earlgrey/tests/spi_flash/BUILD.bazel b/target/earlgrey/tests/spi_flash/BUILD.bazel index 114bc229c..835921590 100644 --- a/target/earlgrey/tests/spi_flash/BUILD.bazel +++ b/target/earlgrey/tests/spi_flash/BUILD.bazel @@ -25,11 +25,13 @@ rust_app( "//drivers/flash:spi_flash", "//hal/blocking/flash", "//hal/blocking/flash:driver", + "//services/flash:opcode", "//services/flash:server", "//target/earlgrey/drivers:eflash_driver", "//target/earlgrey/drivers:spi_host", "//target/earlgrey/registers:flash_ctrl_core", "//target/earlgrey/registers:spi_host", + "//target/earlgrey/services/platform", "//target/earlgrey/util", "//util/error", "//util/ipc", @@ -39,6 +41,7 @@ rust_app( "@pigweed//pw_log/rust:pw_log", "@pigweed//pw_status/rust:pw_status", "@rust_crates//:embedded-hal", + "@rust_crates//:zerocopy", ], ) @@ -55,6 +58,8 @@ rust_app( deps = [ "//hal/blocking/flash", "//services/flash:client", + "//services/flash:opcode", + "//target/earlgrey/services/platform", "//target/earlgrey/util", "//util/error", "//util/ipc", @@ -63,6 +68,7 @@ rust_app( "@pigweed//pw_kernel/userspace", "@pigweed//pw_log/rust:pw_log", "@pigweed//pw_status/rust:pw_status", + "@rust_crates//:zerocopy", ], ) diff --git a/target/earlgrey/tests/spi_flash/combined_flash_server.rs b/target/earlgrey/tests/spi_flash/combined_flash_server.rs index 2388f343f..fa85a03a4 100644 --- a/target/earlgrey/tests/spi_flash/combined_flash_server.rs +++ b/target/earlgrey/tests/spi_flash/combined_flash_server.rs @@ -5,20 +5,23 @@ #![no_main] use combined_flash_server_codegen::{handle, signals}; +use earlgrey_platform::flash_mux::{ + SwitchMuxFinOp, IPC_OP_FLASH_SWITCH_MUX_FIN_NOTICE, IPC_OP_FLASH_SWITCH_MUX_NOTICE, +}; use earlgrey_util::EarlgreyFlashAddress; use eflash_driver::{EmbeddedFlash, Permission}; use hal_flash::BlockingFlash; use hal_flash_driver::FlashAddress; -use pw_status::Error; use services_flash_server::FlashIpcServer; use spi_flash::SpiFlash; -use spi_host::SpiHost0; +use spi_host::{SpiHost0, SpiHost1}; use userspace::time::Instant; use userspace::{entry, syscall}; -use util_error::ErrorCode; -use util_ipc::IpcHandle; +use util_error::{self as error, ErrorCode}; +use util_ipc::{IpcChannel, IpcHandle}; use util_panic as _; -use util_types::Blocking; +use util_types::{Blocking, Opcode}; +use zerocopy::{FromBytes, IntoBytes}; // EFlash Interrupt Blocker struct FlashCtrlInterrupt; @@ -40,6 +43,68 @@ impl Blocking for FlashCtrlInterrupt { } } +fn respond_inaccessible(ipc: &IpcHandle, buf: &mut [u8]) -> Result<(), ErrorCode> { + let _ = ipc.read(0, buf).map_err(ErrorCode::kernel_error)?; + let status = error::FLASH_GENERIC_INACCESSIBLE.0.get(); + ipc.respond(&[status.as_bytes()]) + .map_err(ErrorCode::kernel_error)?; + Ok(()) +} + +fn handle_mux_control( + ipc: &IpcHandle, + buf: &mut [u8], + accessible_flash_bitmap: &mut u8, + spi_flash0: &mut SpiFlash, + spi_flash1: &mut SpiFlash, +) -> Result<(), ErrorCode> { + let len = ipc.read(0, buf).map_err(ErrorCode::kernel_error)?; + let (opcode_bytes, reqrsp) = buf.split_at_mut(core::mem::size_of::()); + let opcode = Opcode::read_from_bytes(opcode_bytes).map_err(|_| error::IPC_ERROR_BAD_REQ_LEN)?; + let req_len = len.saturating_sub(core::mem::size_of::()); + + match opcode { + IPC_OP_FLASH_SWITCH_MUX_NOTICE => { + *accessible_flash_bitmap = 0; + ipc.respond(&[0u32.as_bytes()]) + .map_err(ErrorCode::kernel_error)?; + } + IPC_OP_FLASH_SWITCH_MUX_FIN_NOTICE => { + let req_data = reqrsp.get(..req_len).ok_or(error::IPC_ERROR_BAD_REQ_LEN)?; + let op = SwitchMuxFinOp::read_from_bytes(req_data) + .map_err(|_| error::IPC_ERROR_BAD_REQ_LEN)?; + if op.accessible_flash_bitmap == 0 { + let status = error::IPC_ERROR_BAD_REQ.0.get(); + ipc.respond(&[status.as_bytes()]) + .map_err(ErrorCode::kernel_error)?; + return Ok(()); + } + let mut status = 0u32; + if (op.accessible_flash_bitmap & 0x1) != 0 { + if let Err(e) = spi_flash0.init() { + status = e.0.get(); + } + } + if status == 0 && (op.accessible_flash_bitmap & 0x2) != 0 { + if let Err(e) = spi_flash1.init() { + status = e.0.get(); + } + } + if status == 0 { + *accessible_flash_bitmap = op.accessible_flash_bitmap; + } + ipc.respond(&[status.as_bytes()]) + .map_err(ErrorCode::kernel_error)?; + } + _ => { + let status = error::IPC_ERROR_UNKNOWN_OP.0.get(); + ipc.respond(&[status.as_bytes()]) + .map_err(ErrorCode::kernel_error)?; + } + } + Ok(()) +} + fn run_server() -> Result<(), ErrorCode> { // 1. Initialize EFlash driver. pw_log::info!("combined_server: initializing EFlash driver"); @@ -59,30 +124,33 @@ fn run_server() -> Result<(), ErrorCode> { }; let mut eflash_server = FlashIpcServer::new(eflash); - // 2. Initialize SPI Host. - pw_log::info!("combined_server: initializing SPI Host"); + // 2. Initialize SPI Hosts. + pw_log::info!("combined_server: initializing SPI Hosts"); // SAFETY: We have exclusive access to SPI_HOST0 in this test process. let mmio0 = unsafe { spi_host::RegisterBlock::new(SpiHost0::PTR) }; - let mut spi_host = unsafe { earlgrey_spi_host::SpiHost::new(mmio0) }; - if let Err(e) = spi_host.init(&earlgrey_spi_host::SpiConfig::DEFAULT_SPI0) { + let mut spi_host0 = unsafe { earlgrey_spi_host::SpiHost::new(mmio0) }; + if let Err(e) = spi_host0.init(&earlgrey_spi_host::SpiConfig::DEFAULT_SPI0) { pw_log::error!( - "combined_server: SPI Host init failed: 0x{:x}", + "combined_server: SPI Host 0 init failed: 0x{:x}", u32::from(ErrorCode::from(e)) ); return Err(ErrorCode::from(e)); } - // 3. Initialize SpiFlash driver. - pw_log::info!("combined_server: initializing SpiFlash driver"); - let mut spi_flash = SpiFlash::new(spi_host); - if let Err(e) = spi_flash.init() { + // SAFETY: We have exclusive access to SPI_HOST1 in this test process. + let mmio1 = unsafe { spi_host::RegisterBlock::new(SpiHost1::PTR) }; + let mut spi_host1 = unsafe { earlgrey_spi_host::SpiHost::new(mmio1) }; + if let Err(e) = spi_host1.init(&earlgrey_spi_host::SpiConfig::DEFAULT_SPI1) { pw_log::error!( - "combined_server: SPI Flash init failed: 0x{:x}", - u32::from(e) + "combined_server: SPI Host 1 init failed: 0x{:x}", + u32::from(ErrorCode::from(e)) ); - return Err(e); + return Err(ErrorCode::from(e)); } - let mut spi_flash_server = FlashIpcServer::new(spi_flash); + + // 3. Initialize SpiFlash drivers (uninitialized at boot, initialized upon MUX notification). + let mut spi_flash0 = SpiFlash::new(spi_host0); + let mut spi_flash1 = SpiFlash::new(spi_host1); // 4. Register wait group ports. pw_log::info!("combined_server: registering wait group ports"); @@ -96,15 +164,44 @@ fn run_server() -> Result<(), ErrorCode> { syscall::wait_group_add( handle::FLASH_WAIT_GROUP, - handle::SPI_FLASH_SERVICE, + handle::SPI_FLASH0_SERVICE, + syscall::Signals::READABLE, + handle::SPI_FLASH0_SERVICE as usize, + ) + .map_err(ErrorCode::kernel_error)?; + + syscall::wait_group_add( + handle::FLASH_WAIT_GROUP, + handle::SPI_FLASH1_SERVICE, + syscall::Signals::READABLE, + handle::SPI_FLASH1_SERVICE as usize, + ) + .map_err(ErrorCode::kernel_error)?; + + syscall::wait_group_add( + handle::FLASH_WAIT_GROUP, + handle::SPI_GENERIC_FLASH_SERVICE, + syscall::Signals::READABLE, + handle::SPI_GENERIC_FLASH_SERVICE as usize, + ) + .map_err(ErrorCode::kernel_error)?; + + syscall::wait_group_add( + handle::FLASH_WAIT_GROUP, + handle::SPI_FLASH_MUX_SERVICE, syscall::Signals::READABLE, - handle::SPI_FLASH_SERVICE as usize, + handle::SPI_FLASH_MUX_SERVICE as usize, ) .map_err(ErrorCode::kernel_error)?; let mut buf = [0u8; 2064]; let eflash_ipc = IpcHandle::new(handle::EFLASH_SERVICE); - let spi_flash_ipc = IpcHandle::new(handle::SPI_FLASH_SERVICE); + let spi_flash0_ipc = IpcHandle::new(handle::SPI_FLASH0_SERVICE); + let spi_flash1_ipc = IpcHandle::new(handle::SPI_FLASH1_SERVICE); + let spi_generic_flash_ipc = IpcHandle::new(handle::SPI_GENERIC_FLASH_SERVICE); + let flash_mux_ipc = IpcHandle::new(handle::SPI_FLASH_MUX_SERVICE); + // External SPI Flash channels start in quiescent state (bitmap = 0) until Platform Service configures MUX. + let mut accessible_flash_bitmap: u8 = 0; // 5. Enter main wait_group loop. pw_log::info!("combined_server: entering main wait_group loop"); @@ -119,14 +216,42 @@ fn run_server() -> Result<(), ErrorCode> { let token = wait_result.user_data; if token == handle::EFLASH_SERVICE as usize { eflash_server.handle_one(&eflash_ipc, &mut buf)?; - } else if token == handle::SPI_FLASH_SERVICE as usize { - spi_flash_server.handle_one(&spi_flash_ipc, &mut buf)?; + } else if token == handle::SPI_FLASH0_SERVICE as usize { + if (accessible_flash_bitmap & 0x1) == 0 { + respond_inaccessible(&spi_flash0_ipc, &mut buf)?; + } else { + FlashIpcServer::new(&mut spi_flash0).handle_one(&spi_flash0_ipc, &mut buf)?; + } + } else if token == handle::SPI_FLASH1_SERVICE as usize { + if (accessible_flash_bitmap & 0x2) == 0 { + respond_inaccessible(&spi_flash1_ipc, &mut buf)?; + } else { + FlashIpcServer::new(&mut spi_flash1).handle_one(&spi_flash1_ipc, &mut buf)?; + } + } else if token == handle::SPI_GENERIC_FLASH_SERVICE as usize { + if (accessible_flash_bitmap & 0x1) != 0 { + FlashIpcServer::new(&mut spi_flash0) + .handle_one(&spi_generic_flash_ipc, &mut buf)?; + } else if (accessible_flash_bitmap & 0x2) != 0 { + FlashIpcServer::new(&mut spi_flash1) + .handle_one(&spi_generic_flash_ipc, &mut buf)?; + } else { + respond_inaccessible(&spi_generic_flash_ipc, &mut buf)?; + } + } else if token == handle::SPI_FLASH_MUX_SERVICE as usize { + handle_mux_control( + &flash_mux_ipc, + &mut buf, + &mut accessible_flash_bitmap, + &mut spi_flash0, + &mut spi_flash1, + )?; } } } #[entry] -fn entry() -> Result<(), Error> { +fn entry() -> Result<(), pw_status::Error> { pw_log::info!("🔄 COMBINED FLASH SERVER START"); let ret = run_server(); @@ -137,7 +262,7 @@ fn entry() -> Result<(), Error> { } Err(e) => { pw_log::error!("❌ COMBINED FLASH SERVER FAIL: {:08x}", u32::from(e)); - Err(Error::Unknown) + Err(pw_status::Error::Unknown) } }; ret diff --git a/target/earlgrey/tests/spi_flash/spi_flash_test.rs b/target/earlgrey/tests/spi_flash/spi_flash_test.rs index bf535a177..2edc56808 100644 --- a/target/earlgrey/tests/spi_flash/spi_flash_test.rs +++ b/target/earlgrey/tests/spi_flash/spi_flash_test.rs @@ -8,6 +8,7 @@ use pw_status::Error; use spi_flash_test_codegen::handle; use userspace::entry; +use earlgrey_platform::flash_mux::FlashMuxClient; use earlgrey_util::EarlgreyFlashAddress; use hal_flash::{Flash, FlashAddress}; use services_flash_client::FlashIpcClient; @@ -75,16 +76,139 @@ fn flash_test() -> Result<(), ErrorCode> { // Test on Slot B area (offset 0x90000) erase_program_test(&mut eflash, FlashAddress::data(0x0009_0000), "EFlash")?; - pw_log::info!("--- Testing External SPI Flash ---"); - let mut spi_flash = FlashIpcClient::new(IpcHandle::new(handle::FLASH_SERVICE))?; - let (total_size, page_size, _) = spi_flash.geometry()?; + pw_log::info!("--- Verifying Default Inaccessible State at Boot (bitmap = 0) ---"); + match FlashIpcClient::new(IpcHandle::new(handle::SPI_GENERIC_FLASH_SERVICE)) { + Err(e) if e == util_error::FLASH_GENERIC_INACCESSIBLE => { + pw_log::info!( + "[GenericFlash] Correctly rejected at boot with FLASH_GENERIC_INACCESSIBLE" + ); + } + _ => { + pw_log::error!("[GenericFlash] Expected FLASH_GENERIC_INACCESSIBLE at boot"); + return Err(KERNEL_ERROR_INTERNAL); + } + } + match FlashIpcClient::new(IpcHandle::new(handle::SPI_FLASH0_SERVICE)) { + Err(e) if e == util_error::FLASH_GENERIC_INACCESSIBLE => { + pw_log::info!("[Flash0] Correctly rejected at boot with FLASH_GENERIC_INACCESSIBLE"); + } + _ => { + pw_log::error!("[Flash0] Expected FLASH_GENERIC_INACCESSIBLE at boot"); + return Err(KERNEL_ERROR_INTERNAL); + } + } + + pw_log::info!("--- Simulating Platform Service Initial MUX Notification (Enable Flash 0) ---"); + let flash_mux = FlashMuxClient::new(IpcHandle::new(handle::SPI_FLASH_MUX_SERVICE)); + flash_mux.switch_mux_fin_notice(0x1)?; + + pw_log::info!("--- Testing Generic and Dedicated SPI Flash Channels ---"); + let mut generic_flash = FlashIpcClient::new(IpcHandle::new(handle::SPI_GENERIC_FLASH_SERVICE))?; + let (total_size, page_size, _) = generic_flash.geometry()?; pw_log::info!( - "SPI Flash size: {} bytes, page size: {} bytes", + "Generic SPI Flash size: {} bytes, page size: {} bytes", total_size.get(), page_size.get() ); - // Test on 1MB offset - erase_program_test(&mut spi_flash, FlashAddress::new(0x0010_0000), "SpiFlash")?; + + // 1. Verify Generic SPI Flash channel on 1MB offset + erase_program_test( + &mut generic_flash, + FlashAddress::new(0x0010_0000), + "Generic_SpiFlash", + )?; + + // 2. Verify Dedicated Flash 0 channel succeeds while Flash 1 channel is rejected + pw_log::info!("Verifying Dedicated Flash 0 channel on 2MB offset..."); + let mut flash0_client = FlashIpcClient::new(IpcHandle::new(handle::SPI_FLASH0_SERVICE))?; + erase_program_test( + &mut flash0_client, + FlashAddress::new(0x0020_0000), + "Dedicated_Flash0", + )?; + + pw_log::info!("Verifying Dedicated Flash 1 channel is rejected when Flash 0 is active..."); + match FlashIpcClient::new(IpcHandle::new(handle::SPI_FLASH1_SERVICE)) { + Err(e) if e == util_error::FLASH_GENERIC_INACCESSIBLE => { + pw_log::info!( + "[Flash1] Client connection correctly rejected with FLASH_GENERIC_INACCESSIBLE" + ); + } + _ => { + pw_log::error!( + "[Flash1] Client connection failed to return FLASH_GENERIC_INACCESSIBLE" + ); + return Err(KERNEL_ERROR_INTERNAL); + } + } + + pw_log::info!("--- Testing SPI MUX Handshake & Corner Cases via Control Channel ---"); + let mut check_buf = [0u8; 32]; + pw_log::info!("[FlashMux] Sending switch_mux_notice (Quiesce)..."); + flash_mux.switch_mux_notice()?; + + // Corner Case 1: Inaccessible Error during Quiescence on all channels + pw_log::info!( + "[FlashMux] Corner Case 1: verifying read fail with FLASH_GENERIC_INACCESSIBLE during quiescence..." + ); + match generic_flash.read(FlashAddress::new(0x0010_0000), &mut check_buf) { + Err(e) if e == util_error::FLASH_GENERIC_INACCESSIBLE => { + pw_log::info!("[GenericFlash] Read correctly rejected with FLASH_GENERIC_INACCESSIBLE"); + } + _ => { + pw_log::error!("[GenericFlash] Read failed to return FLASH_GENERIC_INACCESSIBLE"); + return Err(KERNEL_ERROR_INTERNAL); + } + } + match flash0_client.read(FlashAddress::new(0x0010_0000), &mut check_buf) { + Err(e) if e == util_error::FLASH_GENERIC_INACCESSIBLE => { + pw_log::info!( + "[Flash0] Read correctly rejected with FLASH_GENERIC_INACCESSIBLE during quiesce" + ); + } + _ => { + pw_log::error!("[Flash0] Read failed to return FLASH_GENERIC_INACCESSIBLE"); + return Err(KERNEL_ERROR_INTERNAL); + } + } + + // Corner Case 2: Zero Bitmap in FLXE must return error + pw_log::info!( + "[FlashMux] Corner Case 2: verifying switch_mux_fin_notice(0x0) returns error..." + ); + match flash_mux.switch_mux_fin_notice(0x0) { + Err(e) if e == util_error::IPC_ERROR_BAD_REQ => { + pw_log::info!( + "[FlashMux] switch_mux_fin_notice(0x0) correctly rejected with IPC_ERROR_BAD_REQ" + ); + } + _ => { + pw_log::error!( + "[FlashMux] switch_mux_fin_notice(0x0) failed to return IPC_ERROR_BAD_REQ" + ); + return Err(KERNEL_ERROR_INTERNAL); + } + } + + // TODO: When dual-flash target hardware with a physical SPI Host 1 (Flash 1) chip + // is available: do NOT send switch_mux_fin_notice(0x2) in isolation. The test + // must integrate PlatformService (or invoke SpiMuxHandler::switch_mux_to(SpiMuxRoute::HostCpu0Earlgrey1)) + // to physically drive the GPIO pins (assert SPI_RESET_N, drive SPI_MUX_CTRL = LOW, + // and release SPI_RESET_N) as part of the complete PlatformServer::switch_mux + // sequence before verifying erase/program operations on Flash 1. + // Normal switch completion: enable Flash 0 (0x1) + pw_log::info!( + "[FlashMux] Sending switch_mux_fin_notice(0x1) (Re-init 4B mode & enable Flash 0)..." + ); + flash_mux.switch_mux_fin_notice(0x1)?; + + // Corner Case 3: Verify High Address (>16MB) in 4-Byte Address Mode on Flash 0 + pw_log::info!("--- Verifying External SPI Flash 0 High Address (>16MB) in 4-Byte Mode ---"); + erase_program_test( + &mut generic_flash, + FlashAddress::new(0x0110_0000), + "GenericFlash_HighAddr_4Byte", + )?; Ok(()) } @@ -104,6 +228,5 @@ fn entry() -> Result<(), Error> { Err(Error::Unknown) } }; - ret } diff --git a/target/earlgrey/tests/spi_flash/system.json5 b/target/earlgrey/tests/spi_flash/system.json5 index e89a685b6..d7d49d114 100644 --- a/target/earlgrey/tests/spi_flash/system.json5 +++ b/target/earlgrey/tests/spi_flash/system.json5 @@ -27,7 +27,19 @@ type: "channel_handler", }, { - name: "spi_flash_service", + name: "spi_flash0_service", + type: "channel_handler", + }, + { + name: "spi_flash1_service", + type: "channel_handler", + }, + { + name: "spi_generic_flash_service", + type: "channel_handler", + }, + { + name: "spi_flash_mux_service", type: "channel_handler", }, { @@ -59,6 +71,12 @@ type: "device", start_address: 0x40300000, size_bytes: 0x1000, + }, + { + name: "spi_host1", + type: "device", + start_address: 0x40310000, + size_bytes: 0x1000, } ], }, @@ -74,10 +92,28 @@ ram_size_bytes: 8192, objects: [ { - name: "flash_service", + name: "spi_generic_flash_service", + type: "channel_initiator", + handler_process: "combined_flash_server", + handler_object_name: "spi_generic_flash_service", + }, + { + name: "spi_flash0_service", + type: "channel_initiator", + handler_process: "combined_flash_server", + handler_object_name: "spi_flash0_service", + }, + { + name: "spi_flash1_service", + type: "channel_initiator", + handler_process: "combined_flash_server", + handler_object_name: "spi_flash1_service", + }, + { + name: "spi_flash_mux_service", type: "channel_initiator", handler_process: "combined_flash_server", - handler_object_name: "spi_flash_service", + handler_object_name: "spi_flash_mux_service", }, { name: "eflash_service", diff --git a/util/error/flash.rs b/util/error/flash.rs index 100dd8106..cb76c85a8 100644 --- a/util/error/flash.rs +++ b/util/error/flash.rs @@ -39,6 +39,8 @@ pub const FLASH_GENERIC_NOT_INITIALIZED: ErrorCode = FLASH_GENERIC.from_pw(10, Error::FailedPrecondition); /// The flash device is busy. pub const FLASH_GENERIC_BUSY: ErrorCode = FLASH_GENERIC.from_pw(11, Error::Unavailable); +/// The flash device is currently inaccessible (e.g., MUX disconnected or bus isolated). +pub const FLASH_GENERIC_INACCESSIBLE: ErrorCode = FLASH_GENERIC.from_pw(12, Error::Unavailable); /// SFDP: Invalid memory density. pub const FLASH_GENERIC_SFDP_INVALID_MEMORY_DENSITY: ErrorCode =