earlgrey: Implement initial platform service

Signed-off-by: Chris Frantz <cfrantz@google.com>
diff --git a/target/earlgrey/firmware/hwe/BUILD.bazel b/target/earlgrey/firmware/hwe/BUILD.bazel
index 7238435..9006cb2 100644
--- a/target/earlgrey/firmware/hwe/BUILD.bazel
+++ b/target/earlgrey/firmware/hwe/BUILD.bazel
@@ -54,7 +54,6 @@
         "//protocol/usb/stack",
         "//services/flash:client",
         "//target/earlgrey/drivers:usb_driver",
-        "//target/earlgrey/registers:pinmux",
         "//target/earlgrey/registers:top_earlgrey",
         "//target/earlgrey/registers:usbdev",
         "//target/earlgrey/services/sysmgr:client",
@@ -129,7 +128,12 @@
     tags = ["kernel"],
     visibility = ["//visibility:public"],
     deps = [
+        "//target/earlgrey/drivers:gpio",
+        "//target/earlgrey/pinout",
+        "//target/earlgrey/services/platform",
+        "//target/earlgrey/services/sysmgr:client",
         "//util/error",
+        "//util/ipc",
         "//util/zfmt",
         "@pigweed//pw_kernel/userspace",
         "@pigweed//pw_status/rust:pw_status",
@@ -161,6 +165,7 @@
     platform = "//target/earlgrey",
     system_config = ":system_config",
     tags = ["kernel"],
+    visibility = ["//visibility:public"],
 )
 
 rust_binary_no_panics_test(
diff --git a/target/earlgrey/firmware/hwe/platform.rs b/target/earlgrey/firmware/hwe/platform.rs
index 8389b49..894baa1 100644
--- a/target/earlgrey/firmware/hwe/platform.rs
+++ b/target/earlgrey/firmware/hwe/platform.rs
@@ -5,19 +5,136 @@
 #![no_main]
 
 use pw_status::Error;
-use userspace::time::{sleep_until, Clock, Duration, SystemClock};
 use userspace::{process_entry, syscall};
 use util_error::{AsStatus, ErrorCode};
 use util_zfmt::messages::{ProcessExit, ProcessStart};
 
-/*
- * TODO: implement platform server.
- */
-
 fn platform_server() -> Result<(), ErrorCode> {
+    use earlgrey_gpio::EarlGreyGpio;
+    use earlgrey_pinout::dualsbs::DualSideBySide;
+    use earlgrey_pinout::swstraps::SwStraps;
+    use earlgrey_pinout::Pinout;
+    use earlgrey_platform::reset::{ResetPolicy, TargetCpuReset};
+    use earlgrey_platform::server::PlatformServer;
+    use earlgrey_platform::spimux::SpiMuxHandler;
+    use earlgrey_platform::usbmux::UsbMuxHandler;
+    use earlgrey_sysmgr_client::{ResetInfo, SysmgrClient};
+    use platform_codegen::{handle, signals};
+    use userspace::syscall::Signals;
+    use userspace::time::{Clock, Duration, SystemClock};
+    use util_ipc::IpcHandle;
+
+    // SAFETY: the platform process has exclusive access to the GPIO & Pinmux peripherals.
+    let mut gpio = unsafe { EarlGreyGpio::new() };
+    let sysmgr = SysmgrClient::new(IpcHandle::new(handle::SYSMGR_PLATFORM));
+
+    // 1. Unconditionally configure SwStraps pinmux.
+    SwStraps::configure(&mut gpio)?;
+
+    // 2. Read software straps.
+    let straps = SwStraps::read_straps(&mut gpio)?;
+    util_zfmt::debug!("SW_STRAPs read: {straps:02x}", straps = straps);
+
+    // 3. Send the strap value to sysmgr.
+    sysmgr.set_software_straps(straps)?;
+
+    // 4. Retrieve BootInfo from sysmgr.
+    let boot_info = sysmgr.get_boot_info()?;
+    let is_low_power = (boot_info.reset.reason & ResetInfo::REASON_LOW_POWER_EXIT) != 0;
+
+    // 5. Examine straps, configure board pinmux if power-on-reset, and create handlers.
+    let (usb_mux, spi_mux, reset_policy, usb_sig, rst0_sig, rst1_sig) = match straps {
+        SwStraps::TEACUP_BOARD | SwStraps::BRINGUP_STRAPS1 | SwStraps::BRINGUP_STRAPS2 => {
+            if !is_low_power {
+                DualSideBySide::configure(&mut gpio)?;
+            }
+            (
+                UsbMuxHandler::new(DualSideBySide::USB_PRESENCE_N, DualSideBySide::USB_MUX_CTRL),
+                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,
+                ),
+                ResetPolicy::TargetCpu(TargetCpuReset::new(
+                    DualSideBySide::RST_CTRL0_N,
+                    DualSideBySide::RST_MON0_N,
+                    DualSideBySide::RST_MON1_N,
+                )),
+                signals::GPIO_16,
+                signals::GPIO_17,
+                signals::GPIO_18,
+            )
+        }
+        _ => {
+            // Fall back to DualSideBySide for undefined strapping values.
+            if !is_low_power {
+                DualSideBySide::configure(&mut gpio)?;
+            }
+            (
+                UsbMuxHandler::new(DualSideBySide::USB_PRESENCE_N, DualSideBySide::USB_MUX_CTRL),
+                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,
+                ),
+                ResetPolicy::TargetCpu(TargetCpuReset::new(
+                    DualSideBySide::RST_CTRL0_N,
+                    DualSideBySide::RST_MON0_N,
+                    DualSideBySide::RST_MON1_N,
+                )),
+                signals::GPIO_16,
+                signals::GPIO_17,
+                signals::GPIO_18,
+            )
+        }
+    };
+
+    let mut server = PlatformServer::new(gpio, usb_mux, spi_mux, reset_policy);
+    server.set_exit_deadline(SystemClock::now() + Duration::from_secs(10));
+    server.start(is_low_power)?;
+
     loop {
-        sleep_until(SystemClock::now() + Duration::from_secs(600))
-            .map_err(ErrorCode::kernel_error)?;
+        if server.should_exit() {
+            return Ok(());
+        }
+        let deadline = server.next_deadline();
+        let wait_res = syscall::object_wait(
+            handle::PLATFORM_INTERRUPTS,
+            usb_sig | rst0_sig | rst1_sig,
+            deadline,
+        );
+
+        match wait_res {
+            Ok(wait_return) => {
+                let signals = wait_return.pending_signals;
+
+                if (signals & usb_sig) != Signals::empty() {
+                    server.handle_usb_presence_interrupt()?;
+                }
+                if (signals & rst0_sig) != Signals::empty() {
+                    server.handle_rst_mon_interrupt(0)?;
+                }
+                if (signals & rst1_sig) != Signals::empty() {
+                    server.handle_rst_mon_interrupt(1)?;
+                }
+
+                syscall::interrupt_ack(handle::PLATFORM_INTERRUPTS, signals)
+                    .map_err(ErrorCode::kernel_error)?;
+            }
+            Err(Error::DeadlineExceeded) => {
+                if server.should_exit() {
+                    return Ok(());
+                }
+                server.handle_timeout()?;
+            }
+            Err(e) => {
+                return Err(ErrorCode::kernel_error(e));
+            }
+        }
     }
 }
 
diff --git a/target/earlgrey/firmware/hwe/system.json5 b/target/earlgrey/firmware/hwe/system.json5
index 4f8db6b..ba9a9c0 100644
--- a/target/earlgrey/firmware/hwe/system.json5
+++ b/target/earlgrey/firmware/hwe/system.json5
@@ -123,12 +123,39 @@
               handler_object_name: "logger_platform"
             },
             {
+              name: "sysmgr_platform",
+              type: "channel_initiator",
+              handler_process: "sysmgr",
+              handler_object_name: "sysmgr_service"
+            },
+            {
+              name: "platform_interrupts",
+              type: "interrupt",
+              irqs: [
+                { name: "gpio_16", number: 53 },
+                { name: "gpio_17", number: 54 },
+                { name: "gpio_18", number: 55 }
+              ]
+            },
+            {
               name: "platform_thread",
               kernel_stack_size_bytes: 2048,
               type: "thread"
             }
           ],
           memory_mappings: [
+            {
+              name: "gpio",
+              type: "device",
+              start_address: 0x40040000,
+              size_bytes: 0x1000
+            },
+            {
+              name: "pinmux",
+              type: "device",
+              start_address: 0x40460000,
+              size_bytes: 0x1000
+            }
           ]
         },
         {
@@ -235,14 +262,6 @@
               type: "device",
               start_address: 0x40320000,
               size_bytes: 0x1000
-            },
-            {
-              // TODO: eliminate pinmux from usbmgr after implementing pinmux
-              // configuration in the platform task.
-              name: "pinmux",
-              type: "device",
-              start_address: 0x40460000,
-              size_bytes: 0x1000
             }
           ]
         }
diff --git a/target/earlgrey/firmware/hwe/usbmgr.rs b/target/earlgrey/firmware/hwe/usbmgr.rs
index 9cb13f4..ef6b03e 100644
--- a/target/earlgrey/firmware/hwe/usbmgr.rs
+++ b/target/earlgrey/firmware/hwe/usbmgr.rs
@@ -323,24 +323,8 @@
     }
 }
 
-/// Configures pinmux for the USB device.
-///
-/// Currently configures USB sense (VBUS detect) to constant high.
-fn usb_setup_pinmux() {
-    // TODO: move pinmux setup into the platform task.
-    use top_earlgrey::{PinmuxInsel, PinmuxPeripheralIn};
-    let mut pinmux = unsafe { pinmux::PinmuxAon::new() };
-
-    pinmux
-        .regs_mut()
-        .mio_periph_insel()
-        .at(PinmuxPeripheralIn::UsbdevSense as usize)
-        .modify(|_| (PinmuxInsel::ConstantOne as u32).into());
-}
-
 /// USB manager server entry point.
 fn usbmgr_server() -> Result<(), ErrorCode> {
-    usb_setup_pinmux();
     handle_usb()
 }
 
diff --git a/target/earlgrey/pinout/swstraps.rs b/target/earlgrey/pinout/swstraps.rs
index 6f95896..d589203 100644
--- a/target/earlgrey/pinout/swstraps.rs
+++ b/target/earlgrey/pinout/swstraps.rs
@@ -14,6 +14,13 @@
     pub const SW_STRAP0: GpioPin = GpioPin::Pin22;
     pub const SW_STRAP1: GpioPin = GpioPin::Pin23;
     pub const SW_STRAP2: GpioPin = GpioPin::Pin24;
+
+    /// The teacup board normally operates with a strapping value of 0.
+    pub const TEACUP_BOARD: u32 = 0;
+    /// Strap-combo reserved for bringup.
+    pub const BRINGUP_STRAPS1: u32 = 1;
+    /// Strap-combo reserved for bringup.
+    pub const BRINGUP_STRAPS2: u32 = 2;
 }
 
 impl Pinout for SwStraps {
diff --git a/target/earlgrey/services/platform/BUILD.bazel b/target/earlgrey/services/platform/BUILD.bazel
new file mode 100644
index 0000000..f1ee29b
--- /dev/null
+++ b/target/earlgrey/services/platform/BUILD.bazel
@@ -0,0 +1,32 @@
+# Licensed under the Apache-2.0 license
+# SPDX-License-Identifier: Apache-2.0
+
+load("@rules_rust//rust:defs.bzl", "rust_library")
+
+package(default_visibility = ["//visibility:public"])
+
+rust_library(
+    name = "platform",
+    srcs = [
+        "lib.rs",
+        "reset.rs",
+        "server.rs",
+        "spimux.rs",
+        "usbmux.rs",
+    ],
+    crate_name = "earlgrey_platform",
+    edition = "2024",
+    deps = [
+        "//hal/blocking",
+        "//target/earlgrey/drivers:gpio",
+        "//target/earlgrey/registers:top_earlgrey",
+        "//target/earlgrey/util",
+        "//util/error",
+        "//util/ipc",
+        "//util/zfmt",
+        "@pigweed//pw_kernel/userspace",
+        "@pigweed//pw_status/rust:pw_status",
+        "@rust_crates//:zerocopy",
+        "@zfmt//zfmt",
+    ],
+)
diff --git a/target/earlgrey/services/platform/README.md b/target/earlgrey/services/platform/README.md
new file mode 100644
index 0000000..aad9aa5
--- /dev/null
+++ b/target/earlgrey/services/platform/README.md
@@ -0,0 +1,92 @@
+# Earlgrey Platform Service
+
+The Platform Service is a core service running in the Earlgrey Hardware Enablement (HWE) firmware. It is responsible for managing hardware strap pins, system reset monitoring and execution, and USB routing.
+
+## Overview
+
+The Platform Service performs the following key functions:
+1.  **Software Strap Configuration**: Reads the 3-bit hardware strap pins on boot using a robust dual-read procedure and reports the combined 6-bit value to the System Manager (`sysmgr`).
+2.  **Reset Execution**: Controls the system reset line `RST_CTRL0_N` to latch, measure (for 1 second), and release the target system reset.
+3.  **Reset Monitoring**: Monitors the reset monitor lines (`RST_MON0_N` and `RST_MON1_N`) for falling edges, triggering a new reset sequence if a reset request is detected.
+4.  **USB routing**: Monitors the USB presence line (`USB_PRESENCE_N`) and dynamically switches the USB Mux (`USB_MUX_CTRL`) to route USB signals appropriately.
+
+## Hardware Mappings
+
+The service interacts with the following pins (mapped via GPIO and Pinmux):
+*   **Reset Control**: `RST_CTRL0_N` (Pin 0 / Pad `IOA0`). `RST_CTRL1_N` (Pin 1 / Pad `IOA1`) is reserved as a backup.
+*   **Reset Monitors**: `RST_MON0_N` (Pin 17 / Pad `IOA2`) and `RST_MON1_N` (Pin 18 / Pad `IOA5`).
+*   **USB Presence**: `USB_PRESENCE_N` (Pin 16 / Pad `IOR11`).
+*   **USB Mux**: `USB_MUX_CTRL` (Pin 7 / Pad `IOC6`).
+*   **Software Straps**: `SW_STRAP0` (Pin 22 / Pad `IOC0`), `SW_STRAP1` (Pin 23 / Pad `IOC1`), `SW_STRAP2` (Pin 24 / Pad `IOC2`).
+*   **SPI Mux/Reset (Cold Boot only)**:
+    *   `SPI_MUX_CTRL` (Pin 4 / Pad `IOB8`)
+    *   `SPI_MUX_EN_N` (Pin 3 / Pad `IOB7`)
+    *   `SPI_RESET_N` (Pin 2 / Pad `IOA7`)
+    *   `SPI_HOST0_WP_N` (Pin 5 / Pad `IOA3`)
+    *   `SPI_HOST1_WP_N` (Pin 6 / Pad `IOA6`)
+
+## Startup Sequence
+
+Upon starting, the Platform Service (`//target/earlgrey/firmware/hwe/platform.rs`) executes the following sequence:
+1.  Unconditionally configures the `SwStraps` pinmux configuration.
+2.  Performs the **Strap Reading Procedure** (see below) via `SwStraps::read_straps` to determine the software strap value.
+3.  Sends the strap value to `sysmgr` via the `set_software_straps` IPC.
+4.  Retrieves `BootInfo` from `sysmgr` and checks whether the boot reason is a **Low Power Exit**.
+5.  Examines the strap combination (`0`, `1`, `2`, or falling back to `DualSideBySide` for undefined combinations):
+    *   If it is a **Cold Boot / Power-On Reset** (`!is_low_power_exit`), initializes the board pinmux configuration (`DualSideBySide`).
+    *   Constructs the `UsbMuxHandler`, `SpiMuxHandler`, and `ResetPolicy` (`TargetCpuReset`) state machines with their relevant GPIO pins based on the selected pinmux configuration.
+    *   Constructs `PlatformServer` and calls `server.start(is_low_power_exit)`.
+
+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 **Low Power Exit**, dispatches `ResetEvent::Start { is_low_power_exit: true }` to `ResetPolicy` to transition directly to the `Running` state.
+
+## Strap Reading Procedure
+
+Strap pins are read using a two-pass procedure to ensure stability and filter out noise:
+For each strap pin $i \in \{0, 1, 2\}$:
+1.  Configure `SW_STRAPi` pin with **no pull**.
+2.  Delay for 50 microseconds (`PINMUX_PROP_DELAY`).
+3.  Read the pin value -> `val1` (0 or 1).
+4.  Configure the pin pull **opposite** to `val1` (if `val1 == 0` -> pull `Up`, if `val1 == 1` -> pull `Down`).
+5.  Delay for 50 microseconds.
+6.  Read the pin value again -> `val2` (0 or 1).
+7.  The 2-bit result for this pin is `(val1 << 1) | val2`.
+
+The final 6-bit software strap value is constructed by combining the results:
+$$\text{strap\_value} = (\text{strap2} \ll 4) \mid (\text{strap1} \ll 2) \mid \text{strap0}$$
+
+## State Machine
+
+The platform service state machine coordinates target reset execution and runtime event handling.
+
+```mermaid
+stateDiagram-v2
+    [*] --> ColdBoot
+
+    ColdBoot --> LatchReset : start() [not Low Power]
+    ColdBoot --> Running : start() [Low Power Exit]
+
+    LatchReset --> Measure : Drive RST_CTRL0_N Low
+
+    Measure --> ReleaseReset : 1 second timeout
+
+    ReleaseReset --> Running : Drive RST_CTRL0_N High
+
+    state Running {
+        [*] --> WaitEvent
+        WaitEvent --> LatchReset : RST_MON0_N / RST_MON1_N Falling Edge
+        WaitEvent --> WaitEvent : USB_PRESENCE_N Edge (Toggle USB_MUX_CTRL)
+    }
+```
+
+### States
+*   **ColdBoot**: Initial state. Performs startup initialization, strap reading, and decides whether to perform a reset based on the boot reason.
+*   **LatchReset**: Asserts the target reset by driving `RST_CTRL0_N` Low.
+*   **Measure**: Waits for 1 second to ensure the reset is registered by the target system.
+*   **ReleaseReset**: De-asserts the target reset by driving `RST_CTRL0_N` High.
+*   **Running**: Main loop. Listens for interrupts:
+    *   `RST_MON0_N` / `RST_MON1_N` falling edge: Transitions back to `LatchReset` to execute a reset.
+    *   `USB_PRESENCE_N` edge: Updates the `USB_MUX_CTRL` output (High on unplug/rising edge, Low on plug-in/falling edge).
diff --git a/target/earlgrey/services/platform/lib.rs b/target/earlgrey/services/platform/lib.rs
new file mode 100644
index 0000000..163d64a
--- /dev/null
+++ b/target/earlgrey/services/platform/lib.rs
@@ -0,0 +1,9 @@
+// Licensed under the Apache-2.0 license
+// SPDX-License-Identifier: Apache-2.0
+
+#![no_std]
+
+pub mod reset;
+pub mod server;
+pub mod spimux;
+pub mod usbmux;
diff --git a/target/earlgrey/services/platform/reset.rs b/target/earlgrey/services/platform/reset.rs
new file mode 100644
index 0000000..d1af486
--- /dev/null
+++ b/target/earlgrey/services/platform/reset.rs
@@ -0,0 +1,194 @@
+// Licensed under the Apache-2.0 license
+// SPDX-License-Identifier: Apache-2.0
+
+use earlgrey_gpio::{EarlGreyGpio, GpioMask, GpioPin};
+use openprot_hal_blocking::gpio_port::{
+    EdgeSensitivity, GpioInterrupt, GpioPort, InterruptOperation, PinMask,
+};
+use userspace::time::{Clock, Duration, Instant, SystemClock};
+use util_error::ErrorCode;
+use zfmt::Zfmt;
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum TargetCpuState {
+    ColdBoot,
+    LatchReset,
+    Measure,
+    ReleaseReset,
+    Running,
+}
+
+#[derive(Zfmt, Clone)]
+#[zfmt(format = "Platform State: {state}")]
+pub struct StateTransition {
+    pub state: &'static str,
+}
+
+#[derive(Zfmt, Clone)]
+#[zfmt(format = "Reset Monitor: {monitor}")]
+pub struct ResetMonitor {
+    pub monitor: u32,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum ResetEvent {
+    Start { is_low_power_exit: bool },
+    MonitorFallingEdge { monitor_index: usize },
+    Timeout,
+}
+
+pub struct TargetCpuReset {
+    pub rst_ctrl0_n: GpioPin,
+    pub rst_mon0_n: GpioPin,
+    pub rst_mon1_n: GpioPin,
+    pub state: TargetCpuState,
+    next_deadline: Instant,
+}
+
+impl TargetCpuReset {
+    pub const fn new(rst_ctrl0_n: GpioPin, rst_mon0_n: GpioPin, rst_mon1_n: GpioPin) -> Self {
+        Self {
+            rst_ctrl0_n,
+            rst_mon0_n,
+            rst_mon1_n,
+            state: TargetCpuState::ColdBoot,
+            next_deadline: Instant::MAX,
+        }
+    }
+
+    pub fn setup_interrupts(&self, gpio: &mut EarlGreyGpio) -> Result<(), ErrorCode> {
+        let rst_mon0 = GpioMask::from(self.rst_mon0_n);
+        gpio.irq_configure(rst_mon0, EdgeSensitivity::FallingEdge)
+            .map_err(ErrorCode::from)?;
+        gpio.irq_control(rst_mon0, InterruptOperation::Enable)
+            .map_err(ErrorCode::from)?;
+
+        let rst_mon1 = GpioMask::from(self.rst_mon1_n);
+        gpio.irq_configure(rst_mon1, EdgeSensitivity::FallingEdge)
+            .map_err(ErrorCode::from)?;
+        gpio.irq_control(rst_mon1, InterruptOperation::Enable)
+            .map_err(ErrorCode::from)?;
+
+        Ok(())
+    }
+
+    pub fn handle_event(
+        &mut self,
+        event: ResetEvent,
+        gpio: &mut EarlGreyGpio,
+    ) -> Result<(), ErrorCode> {
+        match event {
+            ResetEvent::Start { is_low_power_exit } => {
+                if !is_low_power_exit {
+                    self.transition_to_latch_reset(gpio)?;
+                } else {
+                    self.transition_to_running();
+                }
+            }
+            ResetEvent::MonitorFallingEdge { monitor_index } => {
+                let pin = if monitor_index == 0 {
+                    self.rst_mon0_n
+                } else {
+                    self.rst_mon1_n
+                };
+                let pin_mask = GpioMask::from(pin);
+
+                gpio.irq_control(pin_mask, InterruptOperation::Clear)
+                    .map_err(ErrorCode::from)?;
+
+                util_zfmt::info!(ResetMonitor {
+                    monitor: monitor_index as u32
+                });
+
+                self.transition_to_latch_reset(gpio)?;
+            }
+            ResetEvent::Timeout => match self.state {
+                TargetCpuState::Measure => {
+                    self.transition_to_release_reset(gpio)?;
+                }
+                _ => {
+                    self.next_deadline = Instant::MAX;
+                }
+            },
+        }
+        Ok(())
+    }
+
+    fn transition_to_latch_reset(&mut self, gpio: &mut EarlGreyGpio) -> Result<(), ErrorCode> {
+        self.state = TargetCpuState::LatchReset;
+        util_zfmt::info!(StateTransition {
+            state: "LatchReset"
+        });
+        gpio.set_reset(GpioMask::empty(), GpioMask::from(self.rst_ctrl0_n))
+            .map_err(ErrorCode::from)?;
+
+        self.transition_to_measure();
+        Ok(())
+    }
+
+    fn transition_to_measure(&mut self) {
+        self.state = TargetCpuState::Measure;
+        util_zfmt::info!(StateTransition { state: "Measure" });
+        self.next_deadline = SystemClock::now() + Duration::from_secs(1);
+    }
+
+    fn transition_to_release_reset(&mut self, gpio: &mut EarlGreyGpio) -> Result<(), ErrorCode> {
+        self.state = TargetCpuState::ReleaseReset;
+        util_zfmt::info!(StateTransition {
+            state: "ReleaseReset"
+        });
+        gpio.set_reset(GpioMask::from(self.rst_ctrl0_n), GpioMask::empty())
+            .map_err(ErrorCode::from)?;
+
+        self.transition_to_running();
+        Ok(())
+    }
+
+    fn transition_to_running(&mut self) {
+        self.state = TargetCpuState::Running;
+        util_zfmt::info!(StateTransition { state: "Running" });
+        self.next_deadline = Instant::MAX;
+    }
+
+    pub fn next_deadline(&self) -> Instant {
+        self.next_deadline
+    }
+
+    pub fn state(&self) -> TargetCpuState {
+        self.state
+    }
+}
+
+pub enum ResetPolicy {
+    TargetCpu(TargetCpuReset),
+}
+
+impl ResetPolicy {
+    pub fn setup_interrupts(&self, gpio: &mut EarlGreyGpio) -> Result<(), ErrorCode> {
+        match self {
+            Self::TargetCpu(policy) => policy.setup_interrupts(gpio),
+        }
+    }
+
+    pub fn handle_event(
+        &mut self,
+        event: ResetEvent,
+        gpio: &mut EarlGreyGpio,
+    ) -> Result<(), ErrorCode> {
+        match self {
+            Self::TargetCpu(policy) => policy.handle_event(event, gpio),
+        }
+    }
+
+    pub fn next_deadline(&self) -> Instant {
+        match self {
+            Self::TargetCpu(policy) => policy.next_deadline(),
+        }
+    }
+
+    pub fn state(&self) -> TargetCpuState {
+        match self {
+            Self::TargetCpu(policy) => policy.state(),
+        }
+    }
+}
diff --git a/target/earlgrey/services/platform/server.rs b/target/earlgrey/services/platform/server.rs
new file mode 100644
index 0000000..009ef24
--- /dev/null
+++ b/target/earlgrey/services/platform/server.rs
@@ -0,0 +1,87 @@
+// Licensed under the Apache-2.0 license
+// SPDX-License-Identifier: Apache-2.0
+
+use crate::reset::{ResetEvent, ResetPolicy, TargetCpuState};
+use crate::spimux::{SpiMuxEvent, SpiMuxHandler};
+use crate::usbmux::{UsbMuxEvent, UsbMuxHandler};
+use earlgrey_gpio::EarlGreyGpio;
+use userspace::time::{Clock, Instant, SystemClock};
+use util_error::ErrorCode;
+
+pub use crate::reset::TargetCpuState as State;
+
+pub struct PlatformServer {
+    gpio: EarlGreyGpio,
+    usb_mux: UsbMuxHandler,
+    spi_mux: SpiMuxHandler,
+    reset_policy: ResetPolicy,
+    exit_deadline: Instant,
+}
+
+impl PlatformServer {
+    pub fn new(
+        gpio: EarlGreyGpio,
+        usb_mux: UsbMuxHandler,
+        spi_mux: SpiMuxHandler,
+        reset_policy: ResetPolicy,
+    ) -> Self {
+        Self {
+            gpio,
+            usb_mux,
+            spi_mux,
+            reset_policy,
+            exit_deadline: Instant::MAX,
+        }
+    }
+
+    pub fn state(&self) -> TargetCpuState {
+        self.reset_policy.state()
+    }
+
+    pub fn next_deadline(&self) -> Instant {
+        self.reset_policy.next_deadline().min(self.exit_deadline)
+    }
+
+    pub fn set_exit_deadline(&mut self, deadline: Instant) {
+        self.exit_deadline = deadline;
+    }
+
+    pub fn should_exit(&self) -> bool {
+        SystemClock::now() >= self.exit_deadline
+    }
+
+    pub fn start(&mut self, is_low_power_exit: bool) -> Result<(), ErrorCode> {
+        // 1. Configure interrupts on reset monitors and USB presence.
+        self.usb_mux.setup_interrupts(&mut self.gpio)?;
+        self.reset_policy.setup_interrupts(&mut self.gpio)?;
+
+        // 2. Dispatch initial startup events.
+        if !is_low_power_exit {
+            self.spi_mux
+                .handle_event(SpiMuxEvent::ColdBoot, &mut self.gpio)?;
+        }
+        self.reset_policy
+            .handle_event(ResetEvent::Start { is_low_power_exit }, &mut self.gpio)?;
+
+        Ok(())
+    }
+
+    pub fn handle_timeout(&mut self) -> Result<(), ErrorCode> {
+        self.reset_policy
+            .handle_event(ResetEvent::Timeout, &mut self.gpio)
+    }
+
+    pub fn handle_usb_presence_interrupt(&mut self) -> Result<(), ErrorCode> {
+        self.usb_mux
+            .handle_event(UsbMuxEvent::PinChanged, &mut self.gpio)
+    }
+
+    pub fn handle_rst_mon_interrupt(&mut self, index: usize) -> Result<(), ErrorCode> {
+        self.reset_policy.handle_event(
+            ResetEvent::MonitorFallingEdge {
+                monitor_index: index,
+            },
+            &mut self.gpio,
+        )
+    }
+}
diff --git a/target/earlgrey/services/platform/spimux.rs b/target/earlgrey/services/platform/spimux.rs
new file mode 100644
index 0000000..137d2e7
--- /dev/null
+++ b/target/earlgrey/services/platform/spimux.rs
@@ -0,0 +1,57 @@
+// Licensed under the Apache-2.0 license
+// SPDX-License-Identifier: Apache-2.0
+
+use earlgrey_gpio::{EarlGreyGpio, GpioMask, GpioPin};
+use openprot_hal_blocking::gpio_port::{GpioPort, PinMask};
+use util_error::ErrorCode;
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum SpiMuxEvent {
+    ColdBoot,
+}
+
+pub struct SpiMuxHandler {
+    pub spi_mux_en_n: GpioPin,
+    pub spi_mux_ctrl: GpioPin,
+    pub spi_reset_n: GpioPin,
+    pub spi_host0_wp_n: GpioPin,
+    pub spi_host1_wp_n: GpioPin,
+}
+
+impl SpiMuxHandler {
+    pub const fn new(
+        spi_mux_en_n: GpioPin,
+        spi_mux_ctrl: GpioPin,
+        spi_reset_n: GpioPin,
+        spi_host0_wp_n: GpioPin,
+        spi_host1_wp_n: GpioPin,
+    ) -> Self {
+        Self {
+            spi_mux_en_n,
+            spi_mux_ctrl,
+            spi_reset_n,
+            spi_host0_wp_n,
+            spi_host1_wp_n,
+        }
+    }
+
+    pub fn handle_event(
+        &mut self,
+        event: SpiMuxEvent,
+        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)
+                    .map_err(ErrorCode::from)?;
+            }
+        }
+        Ok(())
+    }
+}
diff --git a/target/earlgrey/services/platform/usbmux.rs b/target/earlgrey/services/platform/usbmux.rs
new file mode 100644
index 0000000..06f03fa
--- /dev/null
+++ b/target/earlgrey/services/platform/usbmux.rs
@@ -0,0 +1,73 @@
+// Licensed under the Apache-2.0 license
+// SPDX-License-Identifier: Apache-2.0
+
+use earlgrey_gpio::{EarlGreyGpio, GpioMask, GpioPin};
+use openprot_hal_blocking::gpio_port::{
+    EdgeSensitivity, GpioInterrupt, GpioPort, InterruptOperation, PinMask,
+};
+use util_error::ErrorCode;
+use zfmt::Zfmt;
+
+#[derive(Zfmt, Clone)]
+#[zfmt(format = "USB Presence: {present}")]
+pub struct UsbPresence {
+    pub present: bool,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum UsbMuxEvent {
+    PinChanged,
+}
+
+pub struct UsbMuxHandler {
+    pub usb_presence_n: GpioPin,
+    pub usb_mux_ctrl: GpioPin,
+}
+
+impl UsbMuxHandler {
+    pub const fn new(usb_presence_n: GpioPin, usb_mux_ctrl: GpioPin) -> Self {
+        Self {
+            usb_presence_n,
+            usb_mux_ctrl,
+        }
+    }
+
+    pub fn setup_interrupts(&self, gpio: &mut EarlGreyGpio) -> Result<(), ErrorCode> {
+        let usb_pres = GpioMask::from(self.usb_presence_n);
+        gpio.irq_configure(usb_pres, EdgeSensitivity::BothEdges)
+            .map_err(ErrorCode::from)?;
+        gpio.irq_control(usb_pres, InterruptOperation::Enable)
+            .map_err(ErrorCode::from)?;
+        Ok(())
+    }
+
+    pub fn handle_event(
+        &mut self,
+        event: UsbMuxEvent,
+        gpio: &mut EarlGreyGpio,
+    ) -> Result<(), ErrorCode> {
+        match event {
+            UsbMuxEvent::PinChanged => {
+                let pin_mask = GpioMask::from(self.usb_presence_n);
+                gpio.irq_control(pin_mask, InterruptOperation::Clear)
+                    .map_err(ErrorCode::from)?;
+
+                let is_high = gpio
+                    .read_input()
+                    .map_err(ErrorCode::from)?
+                    .contains(pin_mask);
+                let usb_mux = GpioMask::from(self.usb_mux_ctrl);
+                if is_high {
+                    gpio.set_reset(usb_mux, GpioMask::empty())
+                        .map_err(ErrorCode::from)?;
+                    util_zfmt::info!(UsbPresence { present: false });
+                } else {
+                    gpio.set_reset(GpioMask::empty(), usb_mux)
+                        .map_err(ErrorCode::from)?;
+                    util_zfmt::info!(UsbPresence { present: true });
+                }
+            }
+        }
+        Ok(())
+    }
+}
diff --git a/target/earlgrey/services/sysmgr/client.rs b/target/earlgrey/services/sysmgr/client.rs
index f8e7884..3fd81c8 100644
--- a/target/earlgrey/services/sysmgr/client.rs
+++ b/target/earlgrey/services/sysmgr/client.rs
@@ -15,6 +15,7 @@
     pub const SYSMGR_OP_GET_BOOT_INFO: Opcode = Opcode::new(*b"MGBI");
     pub const SYSMGR_OP_SET_BOOT_POLICY: Opcode = Opcode::new(*b"MGBP");
     pub const SYSMGR_OP_REQ_REBOOT: Opcode = Opcode::new(*b"MGRB");
+    pub const SYSMGR_OP_SET_SW_STRAPS: Opcode = Opcode::new(*b"MSWS");
 }
 
 pub struct SysmgrClient<IPC: IpcChannel> {
@@ -75,6 +76,17 @@
     pub software_straps: u32,
 }
 
+impl ResetInfo {
+    pub const REASON_POR: u32 = 1 << 0;
+    pub const REASON_LOW_POWER_EXIT: u32 = 1 << 1;
+    pub const REASON_SW_RESET: u32 = 1 << 2;
+    pub const REASON_HW_REQ_SYSRST_CTRL: u32 = 1 << 3;
+    pub const REASON_HW_REQ_AON_TIMER: u32 = 1 << 4;
+    pub const REASON_HW_REQ_PWRMGR: u32 = 1 << 5;
+    pub const REASON_HW_REQ_ALERT_HANDLER: u32 = 1 << 6;
+    pub const REASON_HW_REQ_RV_DM: u32 = 1 << 7;
+}
+
 #[derive(Clone, FromBytes, IntoBytes, Immutable, KnownLayout, Zfmt)]
 #[repr(C)]
 #[zfmt(format = "{chip} {rom_ext} {app} {ownership} {reset}")]
@@ -139,4 +151,16 @@
             .map_err(ErrorCode::kernel_error)?;
         ErrorCode::check_status(result)
     }
+
+    pub fn set_software_straps(&self, straps: u32) -> Result<(), ErrorCode> {
+        let mut result = 0u32;
+        self.ipc
+            .transact(
+                &[op::SYSMGR_OP_SET_SW_STRAPS.as_bytes(), straps.as_bytes()],
+                &mut [result.as_mut_bytes()],
+                Instant::MAX,
+            )
+            .map_err(ErrorCode::kernel_error)?;
+        ErrorCode::check_status(result)
+    }
 }
diff --git a/target/earlgrey/services/sysmgr/server.rs b/target/earlgrey/services/sysmgr/server.rs
index c8b6d96..4bd561a 100644
--- a/target/earlgrey/services/sysmgr/server.rs
+++ b/target/earlgrey/services/sysmgr/server.rs
@@ -79,7 +79,6 @@
             },
         };
 
-        util_zfmt::info!(info.clone());
         Ok(Self { info, retram })
     }
 
@@ -121,6 +120,19 @@
         Ok(&data[0..0])
     }
 
+    fn handle_set_software_straps<'a>(
+        &mut self,
+        data: &'a mut [u8],
+        reqsz: usize,
+    ) -> Result<&'a [u8], ErrorCode> {
+        let straps_bytes = data.get(..reqsz).ok_or(error::IPC_ERROR_BAD_REQ_LEN)?;
+        let straps =
+            u32::read_from_bytes(straps_bytes).map_err(|_| error::IPC_ERROR_BAD_REQ_LEN)?;
+        self.info.reset.software_straps = straps;
+        util_zfmt::info!(self.info.clone());
+        Ok(&data[0..0])
+    }
+
     fn handle_op<'a>(
         &mut self,
         opcode: Opcode,
@@ -131,6 +143,7 @@
             op::SYSMGR_OP_GET_BOOT_INFO => self.handle_get_boot_info(data, reqsz),
             op::SYSMGR_OP_REQ_REBOOT => self.handle_req_reboot(data, reqsz),
             op::SYSMGR_OP_SET_BOOT_POLICY => self.handle_set_boot_policy(data, reqsz),
+            op::SYSMGR_OP_SET_SW_STRAPS => self.handle_set_software_straps(data, reqsz),
             _ => Err(error::IPC_ERROR_UNKNOWN_OP),
         }
     }