diff --git a/embassy-mcxa/Cargo.toml b/embassy-mcxa/Cargo.toml index c492433f5e..fbc38ac210 100644 --- a/embassy-mcxa/Cargo.toml +++ b/embassy-mcxa/Cargo.toml @@ -50,6 +50,9 @@ embedded-storage = "0.3.1" # Custom executor support embassy-executor = { version = "0.10.0", path = "../embassy-executor" } +# USB device driver trait layer +embassy-usb-driver = { version = "0.2.2", path = "../embassy-usb-driver" } + # `time` dependencies embassy-time = { version = "0.5.1", path = "../embassy-time" } embassy-time-driver = { version = "0.2.2", path = "../embassy-time-driver", features = ["tick-hz-1_000_000"] } @@ -59,7 +62,12 @@ grounded = { version = "0.2.1", features = ["cas", "critical-section"] } heapless = "0.9" maitake-sync = { version = "0.3.0", default-features = false, features = ["critical-section", "no-cache-pad"] } nb = "1.1.0" -nxp-pac = { git = "https://github.com/embassy-rs/nxp-pac.git", rev = "a46946e7f5ba3b5d8136f2962400300d54a99d6c", features = ["rt"] } +# TEMPORARY: pinned to the fork branch carrying the MCXA577 USBHS/USBPHY register +# blocks (upstream PR embassy-rs/nxp-pac#108) plus the MCXA577 eSPI register block +# (mcxa577-espi-pac, to be PR'd on top of #108). Switch both nxp-pac entries back +# to a released/main rev of `https://github.com/embassy-rs/nxp-pac.git` once those +# merge. +nxp-pac = { git = "https://github.com/bogdan-petru/nxp-pac.git", rev = "4835d42036e706815c223654c0f85e5eeb974cdb", features = ["rt"] } paste = "1.0.15" rand-core-06 = { package = "rand_core", version = "0.6" } @@ -67,7 +75,9 @@ rand-core-09 = { package = "rand_core", version = "0.9" } rand-core-10 = { package = "rand_core", version = "0.10" } [build-dependencies] -nxp-pac = { git = "https://github.com/embassy-rs/nxp-pac.git", rev = "a46946e7f5ba3b5d8136f2962400300d54a99d6c", default-features = false, features = ["metadata"] } +# TEMPORARY: see the note on the `[dependencies]` nxp-pac entry above. Switch back +# to `https://github.com/embassy-rs/nxp-pac.git` once the fork branches merge. +nxp-pac = { git = "https://github.com/bogdan-petru/nxp-pac.git", rev = "4835d42036e706815c223654c0f85e5eeb974cdb", default-features = false, features = ["metadata"] } proc-macro2 = "1.0.106" quote = "1.0.45" regex = "1.12.3" diff --git a/embassy-mcxa/src/clocks/periph_helpers.rs b/embassy-mcxa/src/clocks/periph_helpers.rs index 1e19d3a98e..6e512cae5d 100644 --- a/embassy-mcxa/src/clocks/periph_helpers.rs +++ b/embassy-mcxa/src/clocks/periph_helpers.rs @@ -15,6 +15,8 @@ use crate::pac::mrcc::{ AdcClkselMux, ClkdivHalt, ClkdivReset, ClkdivUnstab, CtimerClkselMux, FclkClkselMux, Lpi2cClkselMux, LpspiClkselMux, LpuartClkselMux, OstimerClkselMux, }; +#[cfg(feature = "mcxa5xx")] +use crate::pac::mrcc::{EspiClkselMux, PhyClkselMux, UsbClkselMux}; #[must_use] pub struct PreEnableParts { @@ -207,6 +209,188 @@ impl SPConfHelper for Clk1MConfig { } } +// +// USBHS +// + +/// Clock configuration for the MCXA5xx USBHS controller and PHY. +#[cfg(feature = "mcxa5xx")] +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[non_exhaustive] +pub struct UsbhsConfig { + /// Power state required for the SOSC reference clock. + pub power: PoweredClock, + /// USB PHY reference clock divider. + pub phy_div: Div4, +} + +#[cfg(feature = "mcxa5xx")] +impl Default for UsbhsConfig { + fn default() -> Self { + Self { + power: PoweredClock::NormalEnabledDeepSleepDisabled, + phy_div: Div4::no_div(), + } + } +} + +#[cfg(all(feature = "mcxa5xx", feature = "defmt"))] +impl defmt::Format for UsbhsConfig { + fn format(&self, f: defmt::Formatter) { + defmt::write!( + f, + "UsbhsConfig {{ power: {:?}, phy_div: {=u32} }}", + self.power, + self.phy_div.into_divisor() + ) + } +} + +#[cfg(feature = "mcxa5xx")] +impl SPConfHelper for UsbhsConfig { + fn pre_enable_config(&self, clocks: &Clocks) -> Result { + #[cfg(feature = "sosc-as-gpio")] + { + let _ = clocks; + return Err(ClockError::BadConfig { + clock: "usbhs phy", + reason: "requires SOSC pins", + }); + } + + #[cfg(not(feature = "sosc-as-gpio"))] + { + let freq = clocks.ensure_clk_in_active(&self.power)?; + if freq != 24_000_000 { + return Err(ClockError::BadConfig { + clock: "usbhs phy", + reason: "requires a 24 MHz SOSC reference", + }); + } + + let mrcc0 = crate::pac::MRCC0; + mrcc0 + .mrcc_usb1_clksel() + .write(|w| w.set_mux(UsbClkselMux::I2ClkUsbhs0PhyClkXtal)); + mrcc0 + .mrcc_usb1_phy_clksel() + .write(|w| w.set_mux(PhyClkselMux::I2ClkrootSosc)); + + let clkdiv = mrcc0.mrcc_usb1_phy_clkdiv(); + clkdiv.modify(|w| { + w.set_div(self.phy_div.into_bits()); + w.set_halt(ClkdivHalt::Off); + w.set_reset(ClkdivReset::Off); + }); + clkdiv.modify(|w| { + w.set_halt(ClkdivHalt::On); + w.set_reset(ClkdivReset::On); + }); + while clkdiv.read().unstab() == ClkdivUnstab::Off {} + + Ok(PreEnableParts { + freq: freq / self.phy_div.into_divisor(), + wake_guard: WakeGuard::for_power(&self.power), + }) + } + } +} + +// +// eSPI +// + +/// Selectable functional clocks for the eSPI controller. +#[cfg(feature = "mcxa5xx")] +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum EspiClockSel { + /// Gated FRO_HF / FIRC clock (the FRDM-MCXA577 test-bench selection). + FroHf, + /// SOSC/XTAL/EXTAL clock source. + #[cfg(not(feature = "sosc-as-gpio"))] + ClkIn, + /// PLL1 clock after its divider. + Pll1ClkDiv, + /// Disabled. + None, +} + +/// Clock configuration for the MCXA5xx eSPI controller. +#[cfg(feature = "mcxa5xx")] +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[non_exhaustive] +pub struct EspiConfig { + /// Power state required for the selected source clock. + pub power: PoweredClock, + /// Selected clock source. + pub source: EspiClockSel, + /// Pre-divider applied to the source clock. + pub div: Div4, +} + +#[cfg(all(feature = "mcxa5xx", feature = "defmt"))] +impl defmt::Format for EspiConfig { + fn format(&self, f: defmt::Formatter) { + defmt::write!( + f, + "EspiConfig {{ power: {:?}, source: {:?}, div: {=u32} }}", + self.power, + self.source, + self.div.into_divisor() + ) + } +} + +#[cfg(feature = "mcxa5xx")] +impl Default for EspiConfig { + fn default() -> Self { + Self { + power: PoweredClock::NormalEnabledDeepSleepDisabled, + source: EspiClockSel::FroHf, + div: Div4::no_div(), + } + } +} + +#[cfg(feature = "mcxa5xx")] +impl SPConfHelper for EspiConfig { + fn pre_enable_config(&self, clocks: &Clocks) -> Result { + let mrcc0 = crate::pac::MRCC0; + let clksel = mrcc0.mrcc_espi0_clksel(); + let clkdiv = mrcc0.mrcc_espi0_clkdiv(); + + // NOTE: the eSPI bus clock is host-driven; this functional clock only + // feeds the controller core, which auto-divides internally unless + // MCTRL.CLK_DIV_DISABLE is set, so no fmax check is applied here. + let (freq, variant) = match self.source { + EspiClockSel::FroHf => { + let freq = clocks.ensure_fro_hf_active(&self.power)?; + (freq, EspiClkselMux::I1ClkrootFircGated) + } + #[cfg(not(feature = "sosc-as-gpio"))] + EspiClockSel::ClkIn => { + let freq = clocks.ensure_clk_in_active(&self.power)?; + (freq, EspiClkselMux::I3ClkrootSosc) + } + EspiClockSel::Pll1ClkDiv => { + let freq = clocks.ensure_pll1_clk_div_active(&self.power)?; + (freq, EspiClkselMux::I6ClkrootSpllDiv) + } + EspiClockSel::None => { + clksel.write(|w| w.set_mux(EspiClkselMux::_RESERVED_7)); + clkdiv.modify(|w| { + w.set_reset(ClkdivReset::On); + w.set_halt(ClkdivHalt::On); + }); + return Ok(PreEnableParts::empty()); + } + }; + + apply_div4!(self, clksel, clkdiv, variant, freq) + } +} + /// Placeholder configuration for the DAC peripheral. /// /// The DAC HAL driver is not yet implemented, but the PAC metadata diff --git a/embassy-mcxa/src/espi/mod.rs b/embassy-mcxa/src/espi/mod.rs new file mode 100644 index 0000000000..f54a89fd26 --- /dev/null +++ b/embassy-mcxa/src/espi/mod.rs @@ -0,0 +1,1877 @@ +//! eSPI target (device) driver for the MCXA5xx. +//! +//! The MCXA577 integrates an eSPI target controller based on the Intel eSPI +//! v1.5 specification with an NXP-specific register interface. The controller +//! exposes five hardware "ports" that can be configured as ACPI-style +//! endpoints, index/data endpoints, mailboxes (including the OOB channel +//! mailbox), or bus-master/flash (SAF/MAF) ports, plus Port 80 POST-code +//! capture, virtual wires, and host IRQ injection. +//! +//! This driver is a port of the MCUX SDK `fsl_espi` driver to the embassy +//! model: the SDK's IRQ-callback architecture is replaced by an async +//! [`Espi::wait_event`] loop. All host-initiated activity (bus reset, virtual +//! wire changes, Port 80 writes, per-port reads/writes, SAF flash requests) +//! surfaces as [`Event`]s; responses are issued through the synchronous +//! methods on [`Espi`]. +//! +//! # Clocking and pins +//! The peripheral clock is routed through the standard clock helpers +//! ([`crate::clocks::periph_helpers::EspiConfig`]); the FRDM-MCXA577 test +//! bench uses `FRO_HF` undivided. The eSPI signals (CLK, CSn, DATA0-3, RST, +//! NOTIFY) are claimed as typed pins; on the FRDM-MCXA577 they live on +//! `P4_6..P4_13` (ALT11). +//! +//! # Mailbox RAM +//! The controller DMAs mailbox/flash payloads to and from a 4 KiB-aligned +//! window in system RAM ([`EspiRam`]), provided by the application. Port +//! windows are carved out of it via [`PortConfig::ram_offset`]. + +use core::future::poll_fn; +use core::marker::PhantomData; +use core::sync::atomic::{AtomicU32, Ordering}; +use core::task::Poll; + +use cortex_m::asm; +use embassy_hal_internal::Peri; +use embassy_sync::waitqueue::AtomicWaker; + +use crate::clocks::periph_helpers::EspiConfig; +use crate::clocks::{self, ClockError, Gate, WakeGuard}; +use crate::gpio::{DriveStrength, Pull, SealedPin, SlewRate}; +use crate::interrupt; +use crate::interrupt::typelevel::{Binding, Interrupt}; +use crate::pac::espi as pac_espi; +use crate::pac::espi::STAT as PortStat; +use crate::pac::port::Mux; + +#[inline] +fn regs() -> pac_espi::Espi { + crate::pac::ESPI0 +} + +// ---- DMA coherence hooks ---- +// +// The eSPI controller reads and writes [`EspiRam`] as an AHB master. The +// current MCX-A target has no data cache and its SRAM is non-cacheable, so the +// buffer stays coherent using only `dsb()` barriers. These two functions are +// the single porting hook: a part with a data cache must grow real +// clean/invalidate operations here. `Espi::new` debug-asserts that the data +// cache is disabled so the assumption cannot be silently violated. + +/// Barrier after the CPU writes [`EspiRam`], before a doorbell register write +/// makes the data visible to the controller. +#[inline] +fn dma_clean() { + asm::dsb(); +} + +/// Barrier before the CPU reads controller-written [`EspiRam`] contents. +#[inline] +fn dma_invalidate() { + asm::dsb(); +} + +/// Architectural address of the SCB Configuration and Control Register, and +/// its data-cache-enable bit (DC). +const SCB_CCR_ADDR: *const u32 = 0xE000_ED14 as *const u32; +const SCB_CCR_DC: u32 = 1 << 16; + +/// Debug-assert that the data cache is disabled (see the DMA coherence note +/// above; mirrors the USB driver's guard on its DMA structures). +#[inline] +fn assert_dma_noncacheable() { + // SAFETY: architectural read-only access to the System Control Block CCR. + let dcache_enabled = unsafe { core::ptr::read_volatile(SCB_CCR_ADDR) } & SCB_CCR_DC != 0; + debug_assert!( + !dcache_enabled, + "eSPI mailbox RAM assumes non-cacheable SRAM, but the data cache is enabled; implement \ + cache maintenance in dma_clean/dma_invalidate before enabling the D-cache" + ); +} + +/// Number of hardware ports exposed by the controller. +pub const PORT_COUNT: usize = 5; + +/// Size in bytes of the eSPI mailbox RAM window. +pub const RAM_SIZE: usize = 4096; + +/// Sentinel meaning "no port with this role is configured". +const INVALID_PORT: u8 = 0xFF; + +// ========================================================================= +// Configuration types (port of `espi_config_t` / `espi_port_config_t`) +// ========================================================================= + +/// Port function (PnCFG.TYPE). +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum PortType { + /// Unconfigured (reset state). + Unconfigured = 0x0, + /// ACPI-style endpoint (single data byte, host IO). + AcpiEndpoint = 0x1, + /// ACPI-style index/data pair. + AcpiIndexData = 0x2, + /// Bus-master memory, single transaction (MAF-style memory move). + BusMasterMemSingle = 0x4, + /// Bus-master flash, single transaction (SAF request port). + BusMasterFlashSingle = 0x5, + /// Shared mailbox. + MailboxShared = 0x8, + /// Single-direction mailbox. + MailboxSingle = 0x9, + /// Split mailbox. + MailboxSplit = 0xA, + /// Split mailbox carrying the OOB channel. + MailboxOobSplit = 0xB, + /// OEM mailbox. + MailboxOem = 0xC, +} + +/// Mailbox RAM window size (PnRAMUSE.LEN encoding, bytes = `4 << n`). +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum RamSize { + /// 4 bytes. + Size4B = 0, + /// 8 bytes. + Size8B = 1, + /// 16 bytes. + Size16B = 2, + /// 32 bytes. + Size32B = 3, + /// 64 bytes. + Size64B = 4, + /// 128 bytes. + Size128B = 5, + /// 256 bytes. + Size256B = 6, + /// 512 bytes. + Size512B = 7, +} + +impl RamSize { + /// Window size in bytes. + pub const fn bytes(self) -> usize { + 4 << (self as usize) + } +} + +/// Address decode base for a port (PnADDR.BASE_ASZ). +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum AddrBase { + /// Direct addressing: the port address is `PnADDR.OFF`. + Direct = 0, + /// `MAPBASE.BASE0 << 16 | PnADDR.OFF`. + Base0 = 1, + /// `MAPBASE.BASE1 << 16 | PnADDR.OFF`. + Base1 = 2, +} + +/// SPI wire modes advertised to the host (ESPICAP.SPICAP). +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum SpiMode { + /// Single I/O only. + SingleOnly = 0, + /// Single and dual I/O. + SingleAndDual = 1, + /// Single and quad I/O. + SingleAndQuad = 2, + /// Single, dual and quad I/O. + All = 3, +} + +/// Maximum SPI clock speed advertised to the host (ESPICAP.MAXSPD). +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum MaxSpeed { + /// Up to 20 MHz. + Speed20MHz = 0, + /// Up to 25 MHz. + Speed25MHz = 1, + /// Up to 33 MHz. + Speed33MHz = 2, + /// Up to 50 MHz. + Speed50MHz = 3, + /// Up to 66 MHz. + Speed66MHz = 4, +} + +/// eSPI/LPC enable mode (MCTRL.ENABLE). +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum EnableMode { + /// Controller disabled. + Disabled = 0, + /// eSPI mode. + Espi = 1, + /// LPC mode. + Lpc = 2, +} + +/// SAF minimum erase sector size (ESPICAP.SAFERA). +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum SafEraseSize { + /// 2 KiB sectors. + Erase2KB = 0, + /// 4 KiB sectors. + Erase4KB = 1, + /// 8 KiB sectors. + Erase8KB = 2, + /// 16 KiB sectors. + Erase16KB = 3, +} + +/// Maximum read-request size advertised for SAF (ESPICAP.TRGT_REQ_SIZE_SUPP). +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum ReadReqSize { + /// 64 bytes. + Req64B = 1, + /// 128 bytes. + Req128B = 2, + /// 256 bytes. + Req256B = 3, + /// 512 bytes. + Req512B = 4, + /// 1024 bytes. + Req1024B = 5, + /// 2048 bytes. + Req2048B = 6, + /// 4096 bytes. + Req4096B = 7, +} + +/// Maximum memory-channel payload (ESPICAP.MEMMX). +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum MemMaxPayload { + /// 64 bytes. + Max64B = 1, + /// 128 bytes. + Max128B = 2, + /// 256 bytes. + Max256B = 3, +} + +/// Maximum flash-channel payload (ESPICAP.FLASHMX). +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum FlashMaxPayload { + /// 64 bytes. + Max64B = 0, + /// 128 bytes. + Max128B = 1, + /// 256 bytes. + Max256B = 2, + /// 512 bytes. + Max512B = 3, +} + +/// Maximum OOB-channel payload (ESPICAP.OOBMX). +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum OobMaxPayload { + /// 64 bytes. + Max64B = 1, + /// 128 bytes. + Max128B = 2, + /// 256 bytes. + Max256B = 3, +} + +/// Configuration of one hardware port (PnCFG/PnRAMUSE/PnADDR). +#[derive(Clone, Copy, Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct PortConfig { + /// Port function. + pub port_type: PortType, + /// PnCFG.DIRECTION for single-direction mailboxes. + pub direction: u8, + /// Byte offset of this port's window inside [`EspiRam`]. + pub ram_offset: u16, + /// Size of this port's mailbox window. + pub ram_size: RamSize, + /// Port address offset within the selected base (PnADDR.OFF). + pub addr_offset: u16, + /// Address decode base selection. + pub addr_base: AddrBase, + /// Index register offset for index/data ports (PnADDR.IDXOFF). + pub idx_offset: u8, +} + +impl PortConfig { + /// Bytes of [`EspiRam`] this port occupies. Split mailboxes (including the + /// OOB mailbox) use two adjacent windows (receive + transmit). + fn ram_bytes(&self) -> usize { + match self.port_type { + PortType::MailboxSplit | PortType::MailboxOobSplit => 2 * self.ram_size.bytes(), + PortType::AcpiEndpoint | PortType::AcpiIndexData => 0, + _ => self.ram_size.bytes(), + } + } +} + +/// Location of the host-visible status block (STATADDR). +/// +/// The controller mirrors its channel status into host address space at +/// `base`/`offset` when enabled via [`Config::status_block`]. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct StatusBlockConfig { + /// Address decode base (same selection as port addressing). + pub base: AddrBase, + /// Byte offset within the selected base. Must be 8-byte aligned + /// (STATADDR.OFF holds address bits 15:3). + pub offset: u16, +} + +/// eSPI controller configuration (port of `espi_config_t`). +/// +/// `Default` mirrors the SDK `ESPI_GetDefaultConfig`: eSPI mode, all SPI wire +/// modes, 66 MHz, all optional channels disabled. +#[derive(Clone, Copy, Debug)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +#[non_exhaustive] +pub struct Config { + /// Peripheral clock routing. + pub clock: EspiConfig, + /// MAPBASE.BASE0 (upper 16 address bits for [`AddrBase::Base0`] ports). + pub base0_addr: u16, + /// MAPBASE.BASE1 (upper 16 address bits for [`AddrBase::Base1`] ports). + pub base1_addr: u16, + /// Advertise Slave-Attached-Flash support. + pub enable_saf: bool, + /// Advertise the OOB channel. + pub enable_oob: bool, + /// Enable Port 80 POST-code capture. + pub enable_p80: bool, + /// Use the dedicated alert pin instead of signaling on DATA1. + pub enable_alert_pin: bool, + /// Improve timing margin at high bus frequencies. + pub enable_early_sample: bool, + /// Disable the internal `espi_fast_clk` auto-divider. + pub disable_clk_div: bool, + /// Host-visible status block. `Some` programs STATADDR with the given + /// location and enables the block (MCTRL.SBLKENA); `None` leaves it + /// disabled. + pub status_block: Option, + /// SPI wire modes advertised to the host. + pub spi_mode: SpiMode, + /// Maximum bus speed advertised to the host. + pub bus_speed: MaxSpeed, + /// eSPI/LPC mode selection. + pub enable_mode: EnableMode, + /// SAF minimum erase sector size. + pub saf_erase_size: SafEraseSize, + /// Maximum SAF read-request size. + pub max_saf_rx_req_size: ReadReqSize, + /// Maximum memory-channel payload. + pub max_payload_size: MemMaxPayload, + /// Maximum flash-channel payload. + pub max_flash_payload_size: FlashMaxPayload, + /// Maximum OOB-channel payload. + pub max_oob_payload_size: OobMaxPayload, +} + +impl Default for Config { + fn default() -> Self { + Self { + clock: EspiConfig::default(), + base0_addr: 0, + base1_addr: 0, + enable_saf: false, + enable_oob: false, + enable_p80: false, + enable_alert_pin: false, + enable_early_sample: false, + disable_clk_div: false, + status_block: None, + spi_mode: SpiMode::All, + bus_speed: MaxSpeed::Speed66MHz, + enable_mode: EnableMode::Espi, + saf_erase_size: SafEraseSize::Erase2KB, + max_saf_rx_req_size: ReadReqSize::Req4096B, + max_payload_size: MemMaxPayload::Max256B, + max_flash_payload_size: FlashMaxPayload::Max512B, + max_oob_payload_size: OobMaxPayload::Max256B, + } + } +} + +/// Error returned while creating the eSPI driver. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +#[non_exhaustive] +pub enum InitError { + /// Clock configuration failed. + Clock(ClockError), + /// More ports configured than the hardware provides. + TooManyPorts, + /// A port's RAM window exceeds the [`EspiRam`] buffer. + RamOverflow { + /// Offending hardware port index. + port: u8, + }, + /// The status-block offset is not 8-byte aligned (STATADDR.OFF holds + /// address bits 15:3). + StatusBlockMisaligned, +} + +impl From for InitError { + fn from(err: ClockError) -> Self { + Self::Clock(err) + } +} + +// ========================================================================= +// Events +// ========================================================================= + +/// Snapshot of Port 80 POST-code state. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct Port80Status { + /// Latest POST code. + pub current: u8, + /// Previous POST code. + pub previous: u8, + /// POST-code counter (wraps at 16). + pub counter: u8, +} + +/// Host-driven virtual wire states (WIRERO snapshot). +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct VWireIn(pub u32); + +/// Forward each wire getter through the PAC's WIRERO field accessors so the +/// bit layout has a single source of truth. +macro_rules! vwire_in_bit { + ($(#[$doc:meta])* $name:ident, $pac_field:ident) => { + $(#[$doc])* + #[inline] + pub const fn $name(self) -> bool { + pac_espi::WIRERO(self.0).$pac_field() + } + }; +} + +impl VWireIn { + vwire_in_bit!( + /// Sleep S3 (active low). + slp_s3n, + SLP_S3N + ); + vwire_in_bit!( + /// Sleep S4 (active low). + slp_s4n, + SLP_S4N + ); + vwire_in_bit!( + /// Sleep S5 (active low). + slp_s5n, + SLP_S5N + ); + vwire_in_bit!( + /// Suspend status. + sus_stat, + SUS_STAT + ); + vwire_in_bit!( + /// Platform reset (active low). + pltrstn, + PLTRSTN + ); + vwire_in_bit!( + /// OOB reset warning. + oob_rst_warn, + OOB_RST_WARN + ); + vwire_in_bit!( + /// Host reset warning. + host_rst_warn, + HOST_RST_WARN + ); + vwire_in_bit!( + /// Suspend warning. + sus_warn, + SUS_WARN + ); + vwire_in_bit!( + /// Suspend power-down acknowledge (active low). + sus_pwrdn_ackn, + SUS_PWRDN_ACKN + ); + vwire_in_bit!( + /// Sleep A (active low). + slp_an, + SLP_AN + ); + vwire_in_bit!( + /// Wired LAN sleep. + slp_lan, + SLP_LAN + ); + vwire_in_bit!( + /// Wireless LAN sleep. + slp_wlan, + SLP_WLAN + ); + vwire_in_bit!( + /// Host entering C10. + host_c10n, + HOST_C10N + ); + + /// PCIe-to-EC wire group. + #[inline] + pub const fn p2e(self) -> u8 { + pac_espi::WIRERO(self.0).P2E() + } +} + +/// MCU-driven virtual wires (WIREWO write flags). +/// +/// `E2p` is an 8-bit group; all other wires are single-bit. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum VWireOut { + /// OOB reset acknowledge. + OobRstAck, + /// Wake / sensor input. + WakenScin, + /// Power management event. + Pmen, + /// SCI interrupt wire. + Scin, + /// SMI interrupt wire. + Smin, + /// RCIN reset request wire. + Rcinn, + /// Host reset acknowledge. + HostRstAck, + /// Suspend acknowledge. + SusAckN, + /// EC-to-PCIe wire group (8 bits). + E2p, + /// Boot done. + BootDone, + /// Boot error (active low). + BootErrn, + /// Deep-sleep-well power OK / reset. + DswPwrokRst, +} + +/// GPIO virtual-wire input snapshot (WIREIN_GPIO). +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct GpioWire { + /// Wire group index. + pub index: u8, + /// Valid mask for the four levels. + pub valid: u8, + /// Wire levels. + pub level: u8, +} + +/// Boot outcome reported to the host via the BOOT_ERR# virtual wire +/// (see [`Espi::boot_status`]). +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum BootStatus { + /// Boot completed successfully. + Success, + /// Boot failed. + Failure, +} + +/// Port error condition, decoded from PnSTAT.ERR0-3 based on the port type +/// (port of `espi_port_error_t`). +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum PortError { + /// Host wrote an endpoint while write-ready was still set. + EndpointWriteOverrun, + /// Host read an endpoint with no read data pending. + EndpointReadEmpty, + /// Endpoint transfer larger than one byte. + EndpointInvalidSize, + /// Invalid host access to a mailbox. + MailboxInvalidAccess, + /// Mailbox write overrun or read underrun. + MailboxOverrunUnderrun, + /// Host request exceeds the mailbox window. + MailboxSizeOverflow, + /// AHB/RAM access error during a mailbox transfer. + MailboxRamBusError, + /// From-host bus-master transfer failed. + MasterFromHostFailed, + /// Bus-master transfer overrun/underrun. + MasterOverrunUnderrun, + /// Flash erase failed. + MasterEraseFailed, + /// Bus-master AHB access error. + MasterBusError, +} + +/// Per-port event flags, as latched from PnSTAT by the interrupt handler. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct PortEvent { + /// Hardware port index. + pub port: u8, + /// Host read event (INTRD). + pub read: bool, + /// Host write event (INTWR). + pub write: bool, + /// Special event 0 (INTSPC0; e.g. index-data write, SAF completion pull). + pub spec0: bool, + /// Special event 1 (INTSPC1; e.g. mailbox read started). + pub spec1: bool, + /// Special event 2 (INTSPC2). + pub spec2: bool, + /// Special event 3 (INTSPC3; e.g. mailbox read done). + pub spec3: bool, + /// Decoded error condition, if any error bit was set. + pub error: Option, + /// WRSTAT field at interrupt time (flash request type for SAF ports). + pub wrstat: u8, +} + +/// Bus- and port-level events delivered by [`Espi::wait_event`]. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum Event { + /// eSPI bus reset (also delivered on the host re-configuring the link). + BusReset, + /// CRC error on the bus. + CrcError, + /// Host stall detected. + HostStall, + /// Host-to-MCU virtual wires changed; carries the new WIRERO snapshot. + WireChange(VWireIn), + /// GPIO virtual-wire activity; carries the WIREIN_GPIO snapshot. + GpioWire(GpioWire), + /// A previously pushed IRQ (see [`Espi::push_irq`]) completed. + IrqPushDone, + /// Port 80 POST code received. + Port80(Port80Status), + /// Activity on a hardware port. + Port(PortEvent), +} + +/// SAF (slave-attached flash) request kind. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum FlashRequestKind { + /// Host flash read. + Read, + /// Host flash write. + Write, + /// Host flash erase. + Erase, +} + +/// Decoded SAF flash request (port of `espi_flash_request_t`). +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct FlashRequest { + /// Request kind. + pub kind: FlashRequestKind, + /// Flash address, relative to the port's configured address offset. + pub addr: u32, + /// Request length in bytes. + pub length: u32, + /// Transaction tag to echo in completions. + pub tag: u8, + /// `true` for the initial read request; `false` for a continuation pull + /// (the host fetching the next split completion). + pub read_start: bool, +} + +/// SAF read-completion split position (port of `espi_saf_rx_completion_type_t`). +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum SafCompletionType { + /// Middle completion of a split sequence. + Middle = 0, + /// First completion of a split sequence. + First = 1, + /// Last completion of a split sequence. + Last = 2, + /// Only completion of a single-shot transaction. + Only = 3, +} + +/// PnIRULESTAT.SSTCL status controls (port of `espi_sstcl_t`, mailbox subset). +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum MailboxStatus { + /// MCU started writing data (host-read direction). + RdStarted = 0x1, + /// MCU completed writing data (host-read direction). + RdCompleted = 0x2, + /// Host-read direction empty. + RdEmpty = 0x3, + /// MCU started reading (host-write direction). + WrStarted = 0x4, + /// Host-write direction empty (message consumed). + WrEmpty = 0xC, + /// Both directions empty. + BothEmpty = 0xF, +} + +// ========================================================================= +// Interrupt handler and shared state +// ========================================================================= + +/// Bus-level pending event bits, derived from the PAC's MSTAT field setters +/// so the register layout has a single source of truth. +const PENDING_P80: u32 = { + let mut m = pac_espi::MSTAT(0); + m.set_P80INT(true); + m.0 +}; +const PENDING_BUSRST: u32 = { + let mut m = pac_espi::MSTAT(0); + m.set_BUSRST(pac_espi::MSTAT_BUSRST::from_bits(1)); + m.0 +}; +const PENDING_IRQUPD: u32 = { + let mut m = pac_espi::MSTAT(0); + m.set_IRQUPD(pac_espi::MSTAT_IRQUPD::from_bits(1)); + m.0 +}; +const PENDING_WIRECHG: u32 = { + let mut m = pac_espi::MSTAT(0); + m.set_WIRECHG(pac_espi::MSTAT_WIRECHG::from_bits(1)); + m.0 +}; +const PENDING_HSTALL: u32 = { + let mut m = pac_espi::MSTAT(0); + m.set_HSTALL(pac_espi::MSTAT_HSTALL::from_bits(1)); + m.0 +}; +const PENDING_CRCERR: u32 = { + let mut m = pac_espi::MSTAT(0); + m.set_CRCERR(pac_espi::MSTAT_CRCERR::from_bits(1)); + m.0 +}; +const PENDING_GPIO: u32 = { + let mut m = pac_espi::MSTAT(0); + m.set_GPIO(pac_espi::MSTAT_GPIO::from_bits(1)); + m.0 +}; + +/// PnSTAT bits that are independent single-bit event flags and therefore safe +/// to OR-accumulate across coalesced interrupts: the INT* flags and ERR0-ERR3, +/// derived from the PAC's STAT field setters. The RDSTAT/WRSTAT fields are +/// multi-bit state encodings that must never be ORed across snapshots; the +/// latest snapshot is kept separately in [`PORT_LAST_STAT`]. +const PORT_EVENT_MASK: u32 = { + let mut s = PortStat(0); + s.set_INTERR(true); + s.set_INTRD(true); + s.set_INTWR(true); + s.set_INTSPC0(true); + s.set_INTSPC1(true); + s.set_INTSPC2(true); + s.set_INSTSPC3(true); + s.set_ERR0(true); + s.set_ERR1(true); + s.set_ERR2(true); + s.set_ERR3(true); + s.0 +}; + +static BUS_PENDING: AtomicU32 = AtomicU32::new(0); +static PORT_PENDING: [AtomicU32; PORT_COUNT] = [const { AtomicU32::new(0) }; PORT_COUNT]; +/// Latest raw PnSTAT snapshot per port (overwritten, never ORed). +static PORT_LAST_STAT: [AtomicU32; PORT_COUNT] = [const { AtomicU32::new(0) }; PORT_COUNT]; +static WAKER: AtomicWaker = AtomicWaker::new(); + +fn reset_static_state() { + BUS_PENDING.store(0, Ordering::Relaxed); + for pending in &PORT_PENDING { + pending.store(0, Ordering::Relaxed); + } + for stat in &PORT_LAST_STAT { + stat.store(0, Ordering::Relaxed); + } + WAKER.wake(); +} + +type EspiInterrupt = crate::interrupt::typelevel::ESPI; + +/// Interrupt handler for the eSPI controller. +/// +/// Bind this with [`crate::bind_interrupts!`] to the `ESPI` interrupt. +pub struct InterruptHandler; + +impl interrupt::typelevel::Handler for InterruptHandler { + unsafe fn on_interrupt() { + let r = regs(); + let status = r.MSTAT().read(); + if status.0 == 0 { + return; + } + // Acknowledge everything we observed (write-1-to-clear; read-only + // state bits ignore the write). + r.MSTAT().write_value(status); + + // Latch per-port status. Only independent single-bit flags (INT*, + // ERR*) are OR-accumulated; the RDSTAT/WRSTAT state fields are + // multi-bit encodings, so ORing two snapshots would fabricate values + // (e.g. Read|Write reading as Erase). The latest snapshot is stored + // by overwrite for the state fields instead. + let portint = status.PORTINT(); + for (port, pending) in PORT_PENDING.iter().enumerate() { + if portint & (1 << port) != 0 { + let pstat = r.PORT(port).STAT().read(); + let mut clear = PortStat(0); + clear.set_INTERR(pstat.INTERR()); + clear.set_INTRD(pstat.INTRD()); + clear.set_INTWR(pstat.INTWR()); + clear.set_INTSPC0(pstat.INTSPC0()); + clear.set_INTSPC1(pstat.INTSPC1()); + clear.set_INTSPC2(pstat.INTSPC2()); + clear.set_INSTSPC3(pstat.INSTSPC3()); + r.PORT(port).STAT().write_value(clear); + PORT_LAST_STAT[port].store(pstat.0, Ordering::Relaxed); + pending.fetch_or(pstat.0 & PORT_EVENT_MASK, Ordering::Relaxed); + } + } + + let bus_bits = status.0 + & (PENDING_P80 + | PENDING_BUSRST + | PENDING_IRQUPD + | PENDING_WIRECHG + | PENDING_HSTALL + | PENDING_CRCERR + | PENDING_GPIO); + if bus_bits != 0 { + BUS_PENDING.fetch_or(bus_bits, Ordering::Relaxed); + } + + WAKER.wake(); + } +} + +// ========================================================================= +// Clock gate +// ========================================================================= + +impl Gate for crate::peripherals::ESPI0 { + type MrccPeriphConfig = EspiConfig; + + #[inline] + unsafe fn enable_clock() { + crate::pac::MRCC0.mrcc_glb_cc2().modify(|w| w.set_espi0(true)); + } + + #[inline] + unsafe fn disable_clock() { + crate::pac::MRCC0.mrcc_glb_cc2().modify(|w| w.set_espi0(false)); + } + + #[inline] + unsafe fn assert_reset() { + crate::pac::MRCC0.mrcc_glb_rst2().modify(|w| w.set_espi0(false)); + while Self::is_reset_released() {} + } + + #[inline] + unsafe fn release_reset() { + crate::pac::MRCC0.mrcc_glb_rst2().modify(|w| w.set_espi0(true)); + while !Self::is_reset_released() {} + } + + #[inline] + fn is_clock_enabled() -> bool { + crate::pac::MRCC0.mrcc_glb_cc2().read().espi0() + } + + #[inline] + fn is_reset_released() -> bool { + crate::pac::MRCC0.mrcc_glb_rst2().read().espi0() + } +} + +// ========================================================================= +// Pins +// ========================================================================= + +mod sealed { + pub trait Sealed {} +} + +impl sealed::Sealed for T {} + +macro_rules! espi_pin_trait { + ($(#[$doc:meta])* $name:ident) => { + $(#[$doc])* + pub trait $name: sealed::Sealed + PeripheralType + 'static { + /// Apply the eSPI pin profile (mux, no pulls, fast slew, normal + /// drive, push-pull, digital input enabled) to this pin. + fn configure(&self); + } + }; +} + +use embassy_hal_internal::PeripheralType; + +espi_pin_trait!( + /// eSPI clock input pin. + ClkPin +); +espi_pin_trait!( + /// eSPI chip-select input pin. + CsPin +); +espi_pin_trait!( + /// eSPI DATA0 pin. + Data0Pin +); +espi_pin_trait!( + /// eSPI DATA1 pin. + Data1Pin +); +espi_pin_trait!( + /// eSPI DATA2 pin. + Data2Pin +); +espi_pin_trait!( + /// eSPI DATA3 pin. + Data3Pin +); +espi_pin_trait!( + /// eSPI platform-reset input pin. + RstPin +); +espi_pin_trait!( + /// eSPI notify/alert output pin. + NotifyPin +); + +macro_rules! impl_espi_pin { + ($pin:ident, $mux:ident, $trait:ident) => { + impl $trait for crate::peripherals::$pin { + fn configure(&self) { + // Matches the SDK test-bench pin profile. + self.set_pull(Pull::Disabled); + self.set_slew_rate(SlewRate::Fast.into()); + self.set_drive_strength(DriveStrength::Normal.into()); + self.set_function(Mux::$mux); + self.set_enable_input_buffer(true); + } + } + }; +} + +// FRDM-MCXA577 primary pin set (PORT4, ALT11). +impl_espi_pin!(P4_10, Mux11, ClkPin); +impl_espi_pin!(P4_11, Mux11, CsPin); +impl_espi_pin!(P4_9, Mux11, Data0Pin); +impl_espi_pin!(P4_8, Mux11, Data1Pin); +impl_espi_pin!(P4_13, Mux11, Data2Pin); +impl_espi_pin!(P4_12, Mux11, Data3Pin); +impl_espi_pin!(P4_6, Mux11, RstPin); +impl_espi_pin!(P4_7, Mux11, NotifyPin); + +// Alternate pin set (PORT3, ALT5). +impl_espi_pin!(P3_16, Mux5, ClkPin); +impl_espi_pin!(P3_17, Mux5, CsPin); +impl_espi_pin!(P3_15, Mux5, Data0Pin); +impl_espi_pin!(P3_14, Mux5, Data1Pin); +impl_espi_pin!(P3_13, Mux5, Data2Pin); +impl_espi_pin!(P3_12, Mux5, Data3Pin); +impl_espi_pin!(P3_21, Mux5, RstPin); +impl_espi_pin!(P3_20, Mux5, NotifyPin); + +// ========================================================================= +// Mailbox RAM +// ========================================================================= + +/// Mailbox RAM window shared between the CPU and the eSPI controller. +/// +/// The controller addresses this buffer as an AHB master; RAMBASE requires +/// 4 KiB alignment and PnRAMUSE offsets address up to 4 KiB, so the window is +/// a full page. Allocate it statically (e.g. via `static_cell::StaticCell`) +/// and hand it to [`Espi::new`]. +#[repr(C, align(4096))] +pub struct EspiRam(pub [u8; RAM_SIZE]); + +impl EspiRam { + /// Create a zeroed RAM window. + pub const fn new() -> Self { + Self([0; RAM_SIZE]) + } +} + +impl Default for EspiRam { + fn default() -> Self { + Self::new() + } +} + +// ========================================================================= +// Driver +// ========================================================================= + +/// eSPI target driver. +pub struct Espi<'d> { + _phantom: PhantomData<&'d mut crate::peripherals::ESPI0>, + _wake_guard: Option, + ram: *mut u8, + /// Cached PnADDR.OFF per port (mirrors the SDK handle's `addrOffset`). + addr_offset: [u16; PORT_COUNT], + /// Cached PnRAMUSE per port. + ram_offset: [u16; PORT_COUNT], + ram_window: [usize; PORT_COUNT], + port_type: [Option; PORT_COUNT], + oob_port: u8, + saf_port: u8, +} + +impl<'d> Espi<'d> { + /// Create and start the eSPI target controller. + /// + /// Enables the peripheral clock through the standard clock helpers, + /// configures the pins, programs capabilities and per-port windows, and + /// enables the controller in the mode selected by + /// [`Config::enable_mode`]. Host interaction is reported via + /// [`Self::wait_event`] once the controller is enabled. + #[allow(clippy::too_many_arguments)] + pub fn new( + _peri: Peri<'d, crate::peripherals::ESPI0>, + _irq: impl Binding, + clk: Peri<'d, impl ClkPin>, + cs: Peri<'d, impl CsPin>, + io0: Peri<'d, impl Data0Pin>, + io1: Peri<'d, impl Data1Pin>, + io2: Peri<'d, impl Data2Pin>, + io3: Peri<'d, impl Data3Pin>, + rst: Peri<'d, impl RstPin>, + notify: Peri<'d, impl NotifyPin>, + ram: &'d mut EspiRam, + ports: &[PortConfig], + config: Config, + ) -> Result { + if ports.len() > PORT_COUNT { + return Err(InitError::TooManyPorts); + } + for (i, port) in ports.iter().enumerate() { + let end = port.ram_offset as usize + port.ram_bytes(); + if end > RAM_SIZE { + return Err(InitError::RamOverflow { port: i as u8 }); + } + } + if config.status_block.is_some_and(|sb| sb.offset & 0x7 != 0) { + return Err(InitError::StatusBlockMisaligned); + } + + // A previous driver instance may have left latched events behind; + // exclusivity itself is guaranteed by ownership of the `ESPI0` Peri. + assert_dma_noncacheable(); + reset_static_state(); + + // SAFETY: we own the eSPI peripheral and bring up its clock once. + let parts = unsafe { clocks::enable_and_reset::(&config.clock)? }; + + clk.configure(); + cs.configure(); + io0.configure(); + io1.configure(); + io2.configure(); + io3.configure(); + rst.configure(); + notify.configure(); + + let r = regs(); + let ram_ptr = ram.0.as_mut_ptr(); + + // RAM base and mapped bases. + r.RAMBASE().write(|w| w.set_RAM(ram_ptr as u32 >> 12)); + r.MAPBASE().write(|w| { + w.set_BASE0(config.base0_addr); + w.set_BASE1(config.base1_addr); + }); + + // Host-visible status block location (enabled via MCTRL.SBLKENA + // below; alignment validated up front). STATADDR.OFF holds address + // bits 15:3. + if let Some(sb) = config.status_block { + r.STATADDR().write(|w| { + w.set_OFF(sb.offset >> 3); + w.set_BASE(pac_espi::BASE::from_bits(sb.base as u8)); + }); + } + + // Advertised capabilities. + r.ESPICAP().write(|w| { + w.set_SPICAP(pac_espi::SPICAP::from_bits(config.spi_mode as u8)); + w.set_MAXSPD(pac_espi::MAXSPD::from_bits(config.bus_speed as u8)); + w.set_TRGT_REQ_SIZE_SUPP(pac_espi::TRGT_REQ_SIZE_SUPP::from_bits( + config.max_saf_rx_req_size as u8, + )); + w.set_MEMMX(pac_espi::MEMMX::from_bits(config.max_payload_size as u8)); + w.set_ALPIN(config.enable_alert_pin); + if config.enable_saf { + w.set_SAF(pac_espi::ESPICAP_SAF::from_bits(1)); + w.set_SAFERA(pac_espi::SAFERA::from_bits(config.saf_erase_size as u8)); + w.set_FLASHMX(pac_espi::FLASHMX::from_bits(config.max_flash_payload_size as u8)); + } + if config.enable_oob { + w.set_OOBOK(pac_espi::OOBOK::from_bits(1)); + w.set_OOBMX(pac_espi::OOBMX::from_bits(config.max_oob_payload_size as u8)); + } + }); + + // Per-port configuration. + let mut state = Self { + _phantom: PhantomData, + _wake_guard: parts.wake_guard, + ram: ram_ptr, + addr_offset: [0; PORT_COUNT], + ram_offset: [0; PORT_COUNT], + ram_window: [0; PORT_COUNT], + port_type: [None; PORT_COUNT], + oob_port: INVALID_PORT, + saf_port: INVALID_PORT, + }; + + let mut pena = 0u8; + for (i, port) in ports.iter().enumerate() { + let p = r.PORT(i); + p.CFG().write(|w| { + w.set_TYPE(pac_espi::CFG_TYPE::from_bits(port.port_type as u8)); + w.set_DIRECTION(pac_espi::CFG_DIRECTION::from_bits(port.direction)); + }); + p.RAMUSE().write(|w| { + w.set_OFF(port.ram_offset); + w.set_LEN(port.ram_size as u8); + }); + p.ADDR().write(|w| { + w.set_OFF(port.addr_offset); + w.set_BASE_ASZ(pac_espi::ADDR_BASE_ASZ::from_bits(port.addr_base as u8)); + w.set_IDXOFF(port.idx_offset); + }); + pena |= 1 << i; + + state.addr_offset[i] = port.addr_offset; + state.ram_offset[i] = port.ram_offset; + state.ram_window[i] = port.ram_size.bytes(); + state.port_type[i] = Some(port.port_type); + match port.port_type { + PortType::MailboxOobSplit => state.oob_port = i as u8, + PortType::BusMasterFlashSingle => state.saf_port = i as u8, + _ => {} + } + } + + // Master control: enable ports and the selected mode. + r.MCTRL().write(|w| { + w.set_PENA(pena); + w.set_ENABLE(pac_espi::ENABLE::from_bits(config.enable_mode as u8)); + w.set_P80ENA(config.enable_p80); + w.set_CLK_DIV_DISABLE(pac_espi::CLK_DIV_DISABLE::from_bits(config.disable_clk_div as u8)); + w.set_EARLY_SAMPLE(config.enable_early_sample); + w.set_SBLKENA(config.status_block.is_some()); + }); + + // Enable event interrupts (everything the event model reports) and + // all per-port interrupt rules. + r.MSTAT().write_value(pac_espi::MSTAT(0xFFFF_FFFF)); + r.INTENSET().write(|w| { + w.set_PORTINT(pac_espi::INTENSET_PORTINT::from_bits(pena)); + w.set_P80INT(pac_espi::INTENSET_P80INT::from_bits(config.enable_p80 as u8)); + w.set_BUSRST(pac_espi::INTENSET_BUSRST::from_bits(1)); + w.set_IRQUPD(pac_espi::INTENSET_IRQUPD::from_bits(1)); + w.set_WIRECHG(pac_espi::INTENSET_WIRECHG::from_bits(1)); + w.set_HSTALL(pac_espi::INTENSET_HSTALL::from_bits(1)); + w.set_CRCERR(pac_espi::INTENSET_CRCERR::from_bits(1)); + w.set_GPIO(pac_espi::INTENSET_GPIO::from_bits(1)); + }); + for i in 0..ports.len() { + r.PORT(i).IRULESTAT().modify(|w| { + w.set_INTERR(true); + w.set_INTRD(true); + w.set_INTWR(true); + w.set_INTSPC(0xF); + }); + } + + // SAFETY: enabling the controller interrupt. + unsafe { + EspiInterrupt::unpend(); + EspiInterrupt::enable(); + } + + Ok(state) + } + + // --------------------------------------------------------------------- + // Events + // --------------------------------------------------------------------- + + /// Wait for the next controller event. + /// + /// Events latched by the interrupt handler are drained one per call, bus + /// events first, then port events in ascending port order. + pub async fn wait_event(&mut self) -> Event { + poll_fn(|cx| { + WAKER.register(cx.waker()); + + if let Some(event) = self.take_bus_event() { + return Poll::Ready(event); + } + for (port, pending) in PORT_PENDING.iter().enumerate() { + let flags = PortStat(pending.swap(0, Ordering::Relaxed)); + if flags.0 != 0 { + let state = PortStat(PORT_LAST_STAT[port].load(Ordering::Relaxed)); + return Poll::Ready(Event::Port(self.decode_port_event(port as u8, flags, state))); + } + } + Poll::Pending + }) + .await + } + + fn take_bus_event(&mut self) -> Option { + let r = regs(); + loop { + let pending = BUS_PENDING.load(Ordering::Relaxed); + if pending == 0 { + return None; + } + // Select the highest-priority pending bus event. + let bit = if pending & PENDING_BUSRST != 0 { + PENDING_BUSRST + } else if pending & PENDING_CRCERR != 0 { + PENDING_CRCERR + } else if pending & PENDING_HSTALL != 0 { + PENDING_HSTALL + } else if pending & PENDING_WIRECHG != 0 { + PENDING_WIRECHG + } else if pending & PENDING_GPIO != 0 { + PENDING_GPIO + } else if pending & PENDING_P80 != 0 { + PENDING_P80 + } else if pending & PENDING_IRQUPD != 0 { + PENDING_IRQUPD + } else { + // Unknown latched bits; discard them. + BUS_PENDING.fetch_and(!pending, Ordering::Relaxed); + continue; + }; + + // Consume the bit BEFORE reading the payload registers: if a new + // event lands in between, it re-latches the bit and is delivered + // again with a fresh snapshot, instead of being lost with a stale + // one. + BUS_PENDING.fetch_and(!bit, Ordering::Relaxed); + + let event = match bit { + PENDING_BUSRST => Event::BusReset, + PENDING_CRCERR => Event::CrcError, + PENDING_HSTALL => Event::HostStall, + PENDING_WIRECHG => Event::WireChange(VWireIn(r.WIRERO().read().0)), + PENDING_GPIO => { + let gpio = r.WIREIN_GPIO().read(); + Event::GpioWire(GpioWire { + index: gpio.INDEX(), + valid: gpio.VALID().to_bits(), + level: gpio.LEVEL().to_bits(), + }) + } + PENDING_P80 => { + let p80 = r.P80STAT().read(); + Event::Port80(Port80Status { + current: p80.CURR(), + previous: p80.PREV(), + counter: p80.CNT(), + }) + } + _ => Event::IrqPushDone, + }; + return Some(event); + } + } + + /// `flags` carries the OR-accumulated single-bit event flags; `state` is + /// the latest raw PnSTAT snapshot for the multi-bit RDSTAT/WRSTAT fields. + fn decode_port_event(&self, port: u8, flags: PortStat, state: PortStat) -> PortEvent { + PortEvent { + port, + read: flags.INTRD(), + write: flags.INTWR(), + spec0: flags.INTSPC0(), + spec1: flags.INTSPC1(), + spec2: flags.INTSPC2(), + spec3: flags.INSTSPC3(), + error: flags.INTERR().then(|| self.decode_port_error(port, flags)).flatten(), + wrstat: state.WRSTAT().to_bits(), + } + } + + /// Port of `ESPI_GetPortErrorStatus`: ERR0-3 meaning depends on port type. + fn decode_port_error(&self, port: u8, pstat: PortStat) -> Option { + use PortError::*; + let ty = self.port_type[port as usize]?; + match ty { + PortType::AcpiEndpoint | PortType::AcpiIndexData => { + if pstat.ERR0() { + Some(EndpointWriteOverrun) + } else if pstat.ERR1() { + Some(EndpointReadEmpty) + } else if pstat.ERR2() { + Some(EndpointInvalidSize) + } else { + None + } + } + PortType::MailboxShared | PortType::MailboxSingle | PortType::MailboxSplit | PortType::MailboxOobSplit => { + if pstat.ERR0() { + Some(MailboxInvalidAccess) + } else if pstat.ERR1() { + Some(MailboxOverrunUnderrun) + } else if pstat.ERR2() { + Some(MailboxSizeOverflow) + } else if pstat.ERR3() { + Some(MailboxRamBusError) + } else { + None + } + } + PortType::BusMasterMemSingle | PortType::BusMasterFlashSingle => { + if pstat.ERR0() { + Some(MasterFromHostFailed) + } else if pstat.ERR1() { + Some(MasterOverrunUnderrun) + } else if pstat.ERR2() { + Some(MasterEraseFailed) + } else if pstat.ERR3() { + Some(MasterBusError) + } else { + None + } + } + _ => None, + } + } + + // --------------------------------------------------------------------- + // Status snapshots + // --------------------------------------------------------------------- + + /// Raw MSTAT snapshot (busy/in-reset/pending state bits). + pub fn status(&self) -> u32 { + regs().MSTAT().read().0 + } + + /// Raw advertised-capabilities register (ESPICAP). + pub fn capabilities(&self) -> u32 { + regs().ESPICAP().read().0 + } + + /// Raw host-negotiated configuration register (ESPICFG). + pub fn host_config(&self) -> u32 { + regs().ESPICFG().read().0 + } + + /// Current host-driven virtual wire states. + pub fn vwires(&self) -> VWireIn { + VWireIn(regs().WIRERO().read().0) + } + + /// Current GPIO virtual-wire input snapshot (WIREIN_GPIO). + pub fn gpio_wires(&self) -> GpioWire { + let gpio = regs().WIREIN_GPIO().read(); + GpioWire { + index: gpio.INDEX(), + valid: gpio.VALID().to_bits(), + level: gpio.LEVEL().to_bits(), + } + } + + /// Raw MCU-to-host virtual wire register (WIREWO), including DONE. + pub fn vwires_out_raw(&self) -> u32 { + regs().WIREWO().read().0 + } + + // --------------------------------------------------------------------- + // Virtual wires + // --------------------------------------------------------------------- + + /// Send a virtual wire to the host (port of `ESPI_SendVWire`). + /// + /// For single-bit wires `value` must be 0 or 1; for [`VWireOut::E2p`] it + /// is the full 8-bit group. Returns [`VWireBusy`] while a previous + /// virtual wire update is still in flight (WIREWO.DONE clear), matching + /// the SDK's busy semantics; poll and retry. + pub fn send_vwire(&mut self, wire: VWireOut, value: u8) -> Result<(), VWireBusy> { + let r = regs(); + if r.WIREWO().read().DONE() != pac_espi::DONE::DONE { + return Err(VWireBusy); + } + // Deliberately a fresh write, not read-modify-write: `ESPI_SendVWire` + // in the SDK builds the value from zero (`uint32_t reg = 0U`) and + // assigns it. WIREWO is a message-push register - the write triggers + // a virtual-wire update for the written group - not level state that + // must be preserved across writes. + r.WIREWO().write(|w| match wire { + VWireOut::OobRstAck => w.set_OOB_RST_ACK(value != 0), + VWireOut::WakenScin => w.set_WAKEN_SCIN(value != 0), + VWireOut::Pmen => w.set_PMEN(value != 0), + VWireOut::Scin => w.set_SCIN(value != 0), + VWireOut::Smin => w.set_SMIN(value != 0), + VWireOut::Rcinn => w.set_RCINN(value != 0), + VWireOut::HostRstAck => w.set_HOST_RST_ACK(value != 0), + VWireOut::SusAckN => w.set_SUSACKN(value != 0), + VWireOut::E2p => w.set_E2P(value), + VWireOut::BootDone => w.set_BOOT_DONE(value != 0), + VWireOut::BootErrn => w.set_BOOT_ERRN(pac_espi::BOOT_ERRN::from_bits(value & 1)), + VWireOut::DswPwrokRst => w.set_DSW_PWROK_RST(value != 0), + }); + Ok(()) + } + + /// Send a raw WIREWO mask (test-bench `send_vw_mask` command). + pub fn send_vwire_mask(&mut self, mask: u32) { + regs().WIREWO().write_value(pac_espi::WIREWO(mask)); + } + + // --------------------------------------------------------------------- + // Semantic virtual-wire helpers + // + // Thin wrappers over [`Self::send_vwire`] with the platform meaning and + // active-low polarity folded in, mirroring the embassy-imxrt eSPI API. + // All of them share `send_vwire`'s non-blocking semantics: they return + // [`VWireBusy`] while a previous wire update is still in flight instead + // of busy-waiting on WIREWO.DONE. + // --------------------------------------------------------------------- + + /// Drive the WAKE# wire to wake the host from Sx (lid switch, AC + /// insertion, or any general wake event). If the host is in S0, an SCI + /// is generated instead. + /// + /// WAKE# is active low: `wake(true)` asserts the event; the wire does + /// not auto-clear, so call `wake(false)` afterwards. + pub fn wake(&mut self, set: bool) -> Result<(), VWireBusy> { + self.send_vwire(VWireOut::WakenScin, !set as u8) + } + + /// Drive the PME# wire to wake the host from Sx through PCI power + /// management. + /// + /// PME# is active low: `pme(true)` asserts the event; call `pme(false)` + /// to clear it. + pub fn pme(&mut self, set: bool) -> Result<(), VWireBusy> { + self.send_vwire(VWireOut::Pmen, !set as u8) + } + + /// Drive the SCI# wire, causing the OS to invoke an ACPI method. + /// + /// SCI# is active low: `sci(true)` asserts the interrupt; call + /// `sci(false)` once the host has handled it. + pub fn sci(&mut self, set: bool) -> Result<(), VWireBusy> { + self.send_vwire(VWireOut::Scin, !set as u8) + } + + /// Drive the SMI# wire, causing the BIOS to invoke its system + /// management handler. + /// + /// SMI# is active low: `smi(true)` asserts the interrupt; call + /// `smi(false)` once the host has handled it. + pub fn smi(&mut self, set: bool) -> Result<(), VWireBusy> { + self.send_vwire(VWireOut::Smin, !set as u8) + } + + /// Drive the RCIN# wire to request a host CPU reset. + /// + /// RCIN# is active low: `rcin(true)` asserts the request. Normally the + /// platform resets in response, so clearing with `rcin(false)` is rarely + /// needed. + pub fn rcin(&mut self, set: bool) -> Result<(), VWireBusy> { + self.send_vwire(VWireOut::Rcinn, !set as u8) + } + + /// Acknowledge SUS_WARN# with SUS_ACK# (active low: asserts by driving + /// the wire low). + pub fn suspend_ack(&mut self) -> Result<(), VWireBusy> { + self.send_vwire(VWireOut::SusAckN, 0) + } + + /// Acknowledge HOST_RST_WARN with HOST_RST_ACK (active high). + pub fn host_reset_ack(&mut self) -> Result<(), VWireBusy> { + self.send_vwire(VWireOut::HostRstAck, 1) + } + + /// Acknowledge OOB_RST_WARN with OOB_RST_ACK (active high). + pub fn oob_reset_ack(&mut self) -> Result<(), VWireBusy> { + self.send_vwire(VWireOut::OobRstAck, 1) + } + + /// Signal SLAVE_BOOT_LOAD_DONE: the EC has finished booting and the + /// host may continue its G3 to S0 exit (active high). + pub fn boot_done(&mut self) -> Result<(), VWireBusy> { + self.send_vwire(VWireOut::BootDone, 1) + } + + /// Report the boot outcome on BOOT_ERR# (active low: `Success` leaves + /// the wire deasserted/high, `Failure` drives it low). + pub fn boot_status(&mut self, status: BootStatus) -> Result<(), VWireBusy> { + self.send_vwire(VWireOut::BootErrn, (status == BootStatus::Success) as u8) + } + + /// Send the EC-to-PCH (E2P) byte group. + pub fn e2p(&mut self, data: u8) -> Result<(), VWireBusy> { + self.send_vwire(VWireOut::E2p, data) + } + + /// Signal DSW_PWROK reset, to be sent when the host enters G3 + /// (active high). + pub fn dsw_pwrok_reset(&mut self) -> Result<(), VWireBusy> { + self.send_vwire(VWireOut::DswPwrokRst, 1) + } + + // --------------------------------------------------------------------- + // Host IRQ injection + // --------------------------------------------------------------------- + + /// Push an IRQ number to the host (port of `ESPI_PushIrq`). + /// + /// Completion is reported via [`Event::IrqPushDone`]. + pub fn push_irq(&mut self, irq: u8) { + regs().IRQPUSH().write(|w| w.set_IRQ(irq)); + } + + /// Whether the last pushed IRQ has been delivered. + pub fn is_irq_push_done(&self) -> bool { + regs().IRQPUSH().read().DONE() + } + + // --------------------------------------------------------------------- + // Port 80 + // --------------------------------------------------------------------- + + /// Current Port 80 status, if Port 80 capture is enabled. + pub fn port80_status(&self) -> Option { + let r = regs(); + if !r.MCTRL().read().P80ENA() { + return None; + } + let p80 = r.P80STAT().read(); + Some(Port80Status { + current: p80.CURR(), + previous: p80.PREV(), + counter: p80.CNT(), + }) + } + + /// Reset the Port 80 POST-code counter. + pub fn reset_port80_counter(&mut self) { + regs().P80STAT().modify(|w| w.set_RST(true)); + } + + // --------------------------------------------------------------------- + // Endpoint / mailbox data + // --------------------------------------------------------------------- + + /// Read PnDATAIN: `(index, data-or-length)` (port of `ESPI_GetEndpointData`). + pub fn endpoint_data(&self, port: u8) -> (u16, u8) { + let datain = regs().PORT(port as usize).DATAIN().read(); + (datain.IDX(), datain.DATA_LEN()) + } + + /// Write a response byte to PnDATAOUT (port of `ESPI_WritePortData`). + pub fn write_endpoint_data(&mut self, port: u8, data: u8) { + regs().PORT(port as usize).DATAOUT().write(|w| w.set_DATA(data)); + } + + /// Length in bytes of the mailbox message currently in the port window. + pub fn mailbox_msg_len(&self, port: u8) -> usize { + regs().PORT(port as usize).DATAIN().read().DATA_LEN() as usize + 1 + } + + /// Size in bytes of the port's mailbox window. + pub fn mailbox_size(&self, port: u8) -> usize { + self.ram_window[port as usize] + } + + /// Copy the current mailbox message out of the port's RAM window. + /// + /// Returns the number of bytes copied (clamped to `buf` and the window). + pub fn read_mailbox(&mut self, port: u8, buf: &mut [u8]) -> usize { + let window = self.ram_window[port as usize]; + let len = self.mailbox_msg_len(port).min(window).min(buf.len()); + dma_invalidate(); + // SAFETY: the port window was validated against RAM_SIZE at init. + unsafe { + core::ptr::copy_nonoverlapping(self.port_ram_ptr(port), buf.as_mut_ptr(), len); + } + len + } + + /// Update the port's SSTCL status field (mailbox handshakes). + pub fn set_mailbox_status(&mut self, port: u8, status: MailboxStatus) { + regs() + .PORT(port as usize) + .IRULESTAT() + .modify(|w| w.set_SSTCL(status as u8)); + } + + fn port_ram_ptr(&self, port: u8) -> *mut u8 { + // SAFETY: offset validated against RAM_SIZE at init. + unsafe { self.ram.add(self.ram_offset[port as usize] as usize) } + } + + // --------------------------------------------------------------------- + // OOB channel + // --------------------------------------------------------------------- + + /// Send an out-of-band message to the host (port of `ESPI_SendOOB`). + /// + /// Copies `data` into the OOB transmit window and triggers the message. + /// With `announce`, the "started by MCU" status is set first so hosts + /// that react to it can prepare. Completion is reported by a + /// [`Event::Port`] read event on the OOB port. + /// + /// OMFLEN.LEN is a 7-bit field; like the SDK, message lengths above 128 + /// bytes are truncated by the hardware encoding even though the OOB + /// window itself may be larger. + pub fn send_oob(&mut self, data: &[u8], announce: bool) -> Result<(), OobError> { + if self.oob_port == INVALID_PORT { + return Err(OobError::NotConfigured); + } + if data.is_empty() { + return Err(OobError::InvalidLength); + } + let port = self.oob_port; + let mb_size = self.ram_window[port as usize]; + if data.len() > mb_size { + return Err(OobError::InvalidLength); + } + + let p = regs().PORT(port as usize); + if announce { + p.IRULESTAT().modify(|w| w.set_SSTCL(0x1)); + } + + // Transmit half of the split OOB window sits above the receive half. + // SAFETY: the doubled window was validated against RAM_SIZE at init. + unsafe { + core::ptr::copy_nonoverlapping(data.as_ptr(), self.port_ram_ptr(port).add(mb_size), data.len()); + } + dma_clean(); + + // Port of `ESPI_TriggerOOBMsg`. Subtract before narrowing: OMFLEN.LEN + // is a 7-bit field, so like the SDK (which computes `length - 1` in + // u32 arithmetic and lets the field mask it) lengths above 128 bytes + // are truncated by the hardware encoding. + p.OMFLEN().modify(|w| w.set_LEN((data.len() - 1) as u8)); + p.IRULESTAT().modify(|w| w.set_SSTCL(0x2)); + Ok(()) + } + + /// Read the received out-of-band message (port of `ESPI_ReadOOB`). + /// + /// Returns the number of bytes copied, or [`OobError::Truncated`] with + /// the buffer filled if the message was longer than `buf`. + pub fn read_oob(&mut self, buf: &mut [u8]) -> Result { + if self.oob_port == INVALID_PORT { + return Err(OobError::NotConfigured); + } + let port = self.oob_port; + let window = self.ram_window[port as usize]; + let msg_len = self.mailbox_msg_len(port).min(window); + let len = msg_len.min(buf.len()); + dma_invalidate(); + // SAFETY: the window was validated against RAM_SIZE at init. + unsafe { + core::ptr::copy_nonoverlapping(self.port_ram_ptr(port), buf.as_mut_ptr(), len); + } + if len < msg_len { + Err(OobError::Truncated) + } else { + Ok(len) + } + } + + /// Hardware port index carrying the OOB channel, if configured. + pub fn oob_port(&self) -> Option { + (self.oob_port != INVALID_PORT).then_some(self.oob_port) + } + + // --------------------------------------------------------------------- + // SAF (slave-attached flash) + // --------------------------------------------------------------------- + + /// Hardware port index configured as the SAF request port, if any. + pub fn saf_port(&self) -> Option { + (self.saf_port != INVALID_PORT).then_some(self.saf_port) + } + + /// Decode a SAF flash request from a port event on the SAF port + /// (port of the request decode in `ESPI_HandleSAFIRQ`). + /// + /// Returns `None` if the event carries no flash request. The returned + /// address is relative to the SAF port's configured address offset + /// (wrapping on underflow); range-checking against the backing flash size + /// and issuing the failure completion is the application's job, as in the + /// SDK example. + /// + /// An initial request (host read/write/erase) takes priority over a + /// completion pull (`spec0`) if both flags coalesced into one event. The + /// eSPI flash channel serializes transactions (the host waits for each + /// completion), so a coalesced pull can only be the tail of an already + /// fully-served read sequence, where the SDK reference drops it too. + /// Continuation pulls carry `length == 0`, matching the zero-initialized + /// request struct the SDK driver hands to the flash callback. + pub fn flash_request(&self, event: &PortEvent) -> Option { + if self.saf_port == INVALID_PORT || event.port != self.saf_port { + return None; + } + let kind = match event.wrstat { + 1 => FlashRequestKind::Read, + 2 => FlashRequestKind::Write, + 3 => FlashRequestKind::Erase, + _ => return None, + }; + + let p = regs().PORT(self.saf_port as usize); + let datain = p.DATAIN().read(); + let length = datain.DATA_LEN() as u32 + 1; + let tag = datain.TAG(); + + if event.read || event.write { + // Flash address is big-endian in the first four window bytes. + let mut addr_bytes = [0u8; 4]; + dma_invalidate(); + // SAFETY: the window was validated against RAM_SIZE at init. + unsafe { + core::ptr::copy_nonoverlapping(self.port_ram_ptr(self.saf_port), addr_bytes.as_mut_ptr(), 4); + } + let addr = u32::from_be_bytes(addr_bytes).wrapping_sub(self.addr_offset[self.saf_port as usize] as u32); + Some(FlashRequest { + kind, + addr, + length, + tag, + read_start: true, + }) + } else if event.spec0 && kind == FlashRequestKind::Read { + // Host pulled a split completion; the app should push the next + // one. Like the SDK's zero-initialized request, no address or + // length accompanies a pull. + Some(FlashRequest { + kind, + addr: 0, + length: 0, + tag, + read_start: false, + }) + } else { + None + } + } + + /// Slice of the SAF port RAM region where request/completion payloads + /// live. For writes the payload starts at byte 4 (after the address). + /// + /// Unlike mailbox ports, flash ports are not bounded by PnRAMUSE.LEN: the + /// controller stores 4 address bytes plus up to the negotiated flash + /// payload (see [`Self::flash_max_payload`]), so the slice extends from + /// the port's RAM offset to the end of [`EspiRam`]. Lay out the RAM so no + /// other port window sits within `4 +` max payload above the SAF offset. + pub fn flash_window(&mut self) -> &mut [u8] { + assert!(self.saf_port != INVALID_PORT); + let offset = self.ram_offset[self.saf_port as usize] as usize; + dma_invalidate(); + // SAFETY: the offset was validated against RAM_SIZE at init, the + // slice stays within the `EspiRam` borrowed for 'd, and the borrow is + // tied to `&mut self`. + unsafe { core::slice::from_raw_parts_mut(self.port_ram_ptr(self.saf_port), RAM_SIZE - offset) } + } + + /// Program the flash completion transaction type and length + /// (port of `ESPI_SetFlashOpLen`). + /// + /// OMFLEN.LEN is a 7-bit field; like the SDK, lengths above 128 bytes are + /// truncated by the hardware encoding. + pub fn set_flash_op_len(&mut self, trans: u8, length: u32) { + assert!(self.saf_port != INVALID_PORT); + // Ensure completion data written to the flash window is visible to + // the controller before the length/completion doorbells. + dma_clean(); + regs().PORT(self.saf_port as usize).OMFLEN().write(|w| { + w.set_TRANS(pac_espi::OMFLEN_TRANS::from_bits(trans)); + w.set_LEN(length.saturating_sub(1) as u8); + }); + } + + /// Update the SAF completion status fields + /// (port of `ESPI_SetFlashCompletion`). + pub fn set_flash_completion(&mut self, tag: u8, state: u8, completion: SafCompletionType) { + assert!(self.saf_port != INVALID_PORT); + // Ensure completion data written to the flash window is visible to + // the controller before the completion is announced. + dma_clean(); + regs().PORT(self.saf_port as usize).IRULESTAT().modify(|w| { + w.set_SSTCL(state); + w.set_CPU_TAG(tag); + w.set_FLASH_COMPLETION_TYPE(pac_espi::IRULESTAT_FLASH_COMPLETION_TYPE::from_bits(completion as u8)); + }); + } + + /// Maximum host-negotiated flash payload in bytes + /// (port of `ESPI_GetFlashMaxPayload`). + pub fn flash_max_payload(&self) -> u32 { + 64 << regs().ESPICFG().read().FLASHSZ().to_bits() + } +} + +impl Drop for Espi<'_> { + fn drop(&mut self) { + let r = regs(); + // Stop the controller and mask its interrupt; the clock gate stays + // with the peripheral lifetime (matching the USB driver). + r.MCTRL() + .modify(|w| w.set_ENABLE(pac_espi::ENABLE::from_bits(EnableMode::Disabled as u8))); + r.INTENCLR().write_value(pac_espi::INTENCLR(0xFFFF_FFFF)); + EspiInterrupt::disable(); + } +} + +/// A previous virtual-wire update is still in flight (WIREWO.DONE clear). +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub struct VWireBusy; + +/// OOB channel errors. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum OobError { + /// No port is configured as [`PortType::MailboxOobSplit`]. + NotConfigured, + /// Message empty or larger than the OOB window. + InvalidLength, + /// Received message was longer than the provided buffer. + Truncated, +} + +/// SSTCL value: SAF request accepted (port of `kESPI_SSTCL_SAFReqAccepted`). +pub const SSTCL_SAF_REQ_ACCEPTED: u8 = 0x2; +/// SSTCL value: send SAF completion (port of `kESPI_SSTCL_SAFCompletion`). +pub const SSTCL_SAF_COMPLETION: u8 = 0x1; +/// OMFLEN.TRANS: SAF completion carrying data. +pub const OMFLEN_SAF_COMPLETION_WITH_DATA: u8 = 0x1; +/// OMFLEN.TRANS: SAF completion without data. +pub const OMFLEN_SAF_COMPLETION_NO_DATA: u8 = 0x2; +/// OMFLEN.TRANS: SAF completion failure. +pub const OMFLEN_SAF_COMPLETION_FAIL: u8 = 0x0; diff --git a/embassy-mcxa/src/lib.rs b/embassy-mcxa/src/lib.rs index 36fe9a69b6..5706d8b72c 100644 --- a/embassy-mcxa/src/lib.rs +++ b/embassy-mcxa/src/lib.rs @@ -42,6 +42,9 @@ pub mod crc; pub mod ctimer; #[cfg(mcxa_dma)] pub mod dma; +/// eSPI target (device) driver (MCXA5xx eSPI controller). +#[cfg(feature = "mcxa5xx")] +pub mod espi; #[cfg(feature = "executor-platform")] pub mod executor; #[cfg(mcxa_flexspi)] @@ -73,6 +76,9 @@ pub mod sgi; pub mod spi; #[cfg(mcxa_trng)] pub mod trng; +/// USB 2.0 full-speed device driver (MCXA5xx USBHS controller). +#[cfg(feature = "mcxa5xx")] +pub mod usb; #[cfg(mcxa_wwdt)] pub mod wwdt; diff --git a/embassy-mcxa/src/usb/clock.rs b/embassy-mcxa/src/usb/clock.rs new file mode 100644 index 0000000000..89a5d763e2 --- /dev/null +++ b/embassy-mcxa/src/usb/clock.rs @@ -0,0 +1,118 @@ +//! USBPHY bring-up for the MCXA5xx USBHS controller. +//! +//! Clock source selection and MRCC gate/reset handling live in the clocks +//! subsystem. This module only performs the USBPHY register sequence from the +//! NXP MCUXpresso SDK `USB_EhciPhyInit` path. + +/// PHY trim parameters for the high-speed transmit drivers. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +#[non_exhaustive] +pub struct PhyConfig { + /// `D_CAL` trim value. Must fit in the 4-bit hardware field. + pub d_cal: u8, + /// `TXCAL45DP` trim value. Must fit in the 4-bit hardware field. + pub txcal45dp: u8, + /// `TXCAL45DM` trim value. Must fit in the 4-bit hardware field. + pub txcal45dm: u8, + /// PLL divider select for the reference clock (2 = 24 MHz reference). + /// Must fit in the 3-bit hardware field. + pub pll_div_sel: u8, +} + +impl Default for PhyConfig { + fn default() -> Self { + Self { + d_cal: 0, + txcal45dp: 0, + txcal45dm: 0, + pll_div_sel: 2, + } + } +} + +impl PhyConfig { + /// FRDM-MCXA577 board calibration values, using the 24 MHz crystal reference. + pub const fn frdm_mcxa577() -> Self { + Self { + d_cal: 0x04, + txcal45dp: 0x07, + txcal45dm: 0x07, + pll_div_sel: 2, + } + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) enum PhyInitError { + InvalidConfig, + PllLock, +} + +/// Bring up the USBPHY PLL and transmitter trims. +/// +/// # Safety +/// +/// Must be called after the USBHS/USBPHY MRCC gates and resets are configured, +/// with exclusive access to USBPHY. The SOSC / PHY reference clock must already +/// be running. +pub(crate) unsafe fn init_phy(cfg: &PhyConfig) -> Result<(), PhyInitError> { + if cfg.d_cal > 0x0f || cfg.txcal45dp > 0x0f || cfg.txcal45dm > 0x0f || cfg.pll_div_sel > 0x07 { + return Err(PhyInitError::InvalidConfig); + } + + let phy = crate::pac::USB1_HS_PHY; + + phy.CTRL_CLR().write(|w| w.set_SFTRST(true)); + phy.ANACTRL_SET().write(|w| w.set_LVI_EN(true)); + phy.PLL_SIC_SET().write(|w| w.set_PLL_REG_ENABLE(true)); + + // SDK waits at least 15 us for the PLL regulator to stabilize. This is + // deliberately conservative in cycles because the CPU clock varies by app. + cortex_m::asm::delay(15_000); + + phy.PLL_SIC_SET().write(|w| w.set_PLL_POWER(true)); + phy.PLL_SIC_CLR().write(|w| { + w.set_PLL_DIV_SEL(0b111); + w.set_PLL_BYPASS(true); + }); + phy.PLL_SIC_SET().write(|w| { + w.set_PLL_DIV_SEL(cfg.pll_div_sel); + w.set_PLL_EN_USB_CLKS(true); + }); + + phy.CTRL_CLR().write(|w| w.set_CLKGATE(true)); + phy.PWD().write(|w| w.0 = 0); + + let mut locked = false; + for _ in 0..4_000_000 { + if phy.PLL_SIC().read().PLL_LOCK() == crate::pac::usbphy::PLL_LOCK::PLL_LOCKED { + locked = true; + break; + } + cortex_m::asm::nop(); + } + if !locked { + return Err(PhyInitError::PllLock); + } + + phy.TRIM_OVERRIDE_EN_SET().write(|w| { + w.set_DIV_SEL_OVERRIDE(true); + w.set_TX_D_CAL_OVERRIDE(true); + w.set_TX_CAL45DP_OVERRIDE(true); + w.set_TX_CAL45DM_OVERRIDE(true); + }); + phy.CTRL_SET().write(|w| { + w.set_ENUTMILEVEL2(true); + w.set_ENUTMILEVEL3(true); + }); + phy.PWD().write(|w| w.0 = 0); + + phy.TX().modify(|w| { + w.set_D_CAL(crate::pac::usbphy::D_CAL::from_bits(cfg.d_cal)); + w.set_TXCAL45DN(cfg.txcal45dm); + w.set_TXCAL45DP(cfg.txcal45dp); + }); + + Ok(()) +} diff --git a/embassy-mcxa/src/usb/ehci.rs b/embassy-mcxa/src/usb/ehci.rs new file mode 100644 index 0000000000..c17a009d82 --- /dev/null +++ b/embassy-mcxa/src/usb/ehci.rs @@ -0,0 +1,101 @@ +//! EHCI device-mode DMA structures for the MCXA5xx USBHS controller. +//! +//! The MMIO register blocks come from `nxp-pac`; the queue heads and transfer +//! descriptors are RAM data structures consumed directly by the controller. + +/// Device queue head (dQH). One per endpoint and direction (so `2 * N` total), +/// aligned to 64 bytes and laid out as a contiguous array in RAM. +/// +/// Layout per the Chipidea/EHCI device specification. +#[repr(C, align(64))] +#[derive(Clone, Copy)] +pub(crate) struct QueueHead { + /// Capabilities: max packet length, ZLT, IOS, Mult. + pub(crate) capabilities: u32, + /// Current dTD pointer (hardware-owned). + pub(crate) current_dtd: u32, + /// dTD overlay area (8 words): next pointer, token, 5 buffer pointers. + pub(crate) next_dtd: u32, + pub(crate) token: u32, + pub(crate) buffer: [u32; 5], + /// Reserved word to pad the overlay to spec. + pub(crate) _reserved: u32, + /// Setup packet buffer (8 bytes) for control endpoints. + pub(crate) setup: [u32; 2], + /// Padding to fill out to 64 bytes. + pub(crate) _pad: [u32; 4], +} + +impl QueueHead { + pub(crate) const fn new() -> Self { + Self { + capabilities: 0, + current_dtd: 0, + next_dtd: 1, // terminate bit set + token: 0, + buffer: [0; 5], + _reserved: 0, + setup: [0; 2], + _pad: [0; 4], + } + } +} + +/// Capability field: ZLT disable. +/// +/// The driver sets this because `embassy-usb` drives zero-length packets and +/// control status stages explicitly; the controller should not synthesize them. +pub(crate) const QH_CAP_ZLT: u32 = 1 << 29; +/// Capability field: interrupt-on-setup (control OUT QH). +pub(crate) const QH_CAP_IOS: u32 = 1 << 15; +/// Shift for the max-packet-length field in the QH capabilities word. +pub(crate) const QH_CAP_MAXLEN_SHIFT: u32 = 16; + +/// Device transfer descriptor (dTD), 32-byte aligned. +#[repr(C, align(32))] +#[derive(Clone, Copy)] +pub(crate) struct TransferDescriptor { + /// Next dTD pointer (bit0 = terminate). + pub(crate) next: u32, + /// Token: total bytes, IOC, status. + pub(crate) token: u32, + /// Buffer page pointers. + pub(crate) buffer: [u32; 5], + /// Padding to 32 bytes. + pub(crate) _pad: u32, +} + +impl TransferDescriptor { + pub(crate) const fn new() -> Self { + Self { + next: 1, // terminate + token: 0, + buffer: [0; 5], + _pad: 0, + } + } +} + +/// dTD next-pointer terminate bit. +pub(crate) const DTD_NEXT_TERMINATE: u32 = 1 << 0; +/// dTD token: total bytes shift (bits 30:16). +pub(crate) const DTD_TOKEN_TOTAL_SHIFT: u32 = 16; +/// dTD token: remaining byte-count mask after shifting by [`DTD_TOKEN_TOTAL_SHIFT`]. +pub(crate) const DTD_TOKEN_TOTAL_MASK: u32 = 0x7FFF; +/// dTD token: interrupt on complete. +pub(crate) const DTD_TOKEN_IOC: u32 = 1 << 15; +/// dTD token: active status bit. +pub(crate) const DTD_TOKEN_ACTIVE: u32 = 1 << 7; +/// dTD token: halted status bit. +pub(crate) const DTD_TOKEN_HALTED: u32 = 1 << 6; +/// dTD token: data-buffer error status bit. +pub(crate) const DTD_TOKEN_DATA_BUFFER_ERROR: u32 = 1 << 5; +/// dTD token: transaction error status bit. +pub(crate) const DTD_TOKEN_TRANSACTION_ERROR: u32 = 1 << 3; +/// dTD token: transfer status bits that indicate a completed transfer failed. +pub(crate) const DTD_TOKEN_ERROR_MASK: u32 = + DTD_TOKEN_HALTED | DTD_TOKEN_DATA_BUFFER_ERROR | DTD_TOKEN_TRANSACTION_ERROR; +/// dTD buffer pointer page size. Each dTD carries five page pointers. +pub(crate) const DTD_BUFFER_PAGE_SIZE: u32 = 0x1000; +/// dTD buffer pointer page mask. +pub(crate) const DTD_BUFFER_PAGE_MASK: u32 = DTD_BUFFER_PAGE_SIZE - 1; diff --git a/embassy-mcxa/src/usb/mod.rs b/embassy-mcxa/src/usb/mod.rs new file mode 100644 index 0000000000..bf6d8bf7ba --- /dev/null +++ b/embassy-mcxa/src/usb/mod.rs @@ -0,0 +1,1628 @@ +//! USB 2.0 device driver for the MCXA5xx USBHS controller. +//! +//! The MCXA5xx exposes a single Chipidea/EHCI USB-OTG core (`USB1_HS`) with an +//! integrated high-speed PHY. This driver operates that core in **device mode** +//! and forces **full speed** (12 Mbit/s) operation, which is the configuration +//! used by simple USB device classes such as HID. +//! +//! It implements the [`embassy_usb_driver`] traits so it can be used directly +//! with `embassy-usb` (e.g. the HID class). +//! +//! # Board clocking +//! The MCXA577 USBHS PHY expects the board clock tree to match the SDK/Zephyr +//! profile used by FRDM-MCXA577: over-drive voltage mode with the 24 MHz SOSC +//! crystal enabled. [`Driver::new`] returns an error if that clocking is not +//! available or if the USBPHY PLL does not lock. +//! +//! # Limitations +//! - Device mode only (no host / OTG role switching). +//! - Forced full speed; high-speed operation is not exposed. +//! - No hardware VBUS detection yet; the bus reports power detected at startup. +//! - Remote wakeup is not implemented. +//! - Isochronous endpoints and packet sizes greater than 64 bytes are not +//! supported. +//! - One transfer descriptor in flight per endpoint direction at a time. +//! +//! # Register access +//! MMIO register blocks come from `nxp-pac`. The local [`ehci`] module only +//! contains the EHCI DMA queue-head and transfer-descriptor RAM formats that are +//! not MMIO registers. + +mod clock; +mod ehci; + +use core::cell::UnsafeCell; +use core::future::poll_fn; +use core::marker::PhantomData; +use core::sync::atomic::{AtomicBool, AtomicU8, AtomicU16, AtomicU32, Ordering}; +use core::task::Poll; + +pub use clock::PhyConfig; +use cortex_m::asm; +use ehci::{ + DTD_BUFFER_PAGE_MASK, DTD_BUFFER_PAGE_SIZE, DTD_NEXT_TERMINATE, DTD_TOKEN_ACTIVE, DTD_TOKEN_ERROR_MASK, + DTD_TOKEN_IOC, DTD_TOKEN_TOTAL_MASK, DTD_TOKEN_TOTAL_SHIFT, QH_CAP_IOS, QH_CAP_MAXLEN_SHIFT, QH_CAP_ZLT, QueueHead, + TransferDescriptor, +}; +use embassy_hal_internal::Peri; +use embassy_sync::waitqueue::AtomicWaker; +use embassy_usb_driver::{ + Direction, EndpointAddress, EndpointAllocError, EndpointError, EndpointInfo, EndpointType, Event, Unsupported, +}; + +use crate::clocks::periph_helpers::UsbhsConfig; +use crate::clocks::{self, ClockError, Gate, WakeGuard}; +use crate::interrupt; +use crate::interrupt::typelevel::{Binding, Interrupt}; + +/// USBHS device-driver configuration. +/// +/// The controller is currently always placed in device mode and forced to +/// full-speed operation. The configuration carries board-specific PHY trim and +/// PLL settings so applications can use the FRDM-MCXA577 defaults or provide +/// calibrated values from their board support package. +#[derive(Clone, Copy, Debug, Eq, PartialEq, Default)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +#[non_exhaustive] +pub struct Config { + /// USBHS clock-source and PHY reference divider settings. + pub clock: UsbhsConfig, + /// USBPHY trim and PLL settings. + pub phy: PhyConfig, +} + +impl From for Config { + fn from(phy: PhyConfig) -> Self { + Self { phy, ..Self::default() } + } +} + +/// Error returned while creating the USB driver. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +#[non_exhaustive] +pub enum InitError { + /// USB clock configuration failed. + Clock(ClockError), + /// PHY trim or PLL configuration is outside the hardware field range. + PhyConfig, + /// USBPHY PLL did not lock. + PhyPllLock, +} + +impl From for InitError { + fn from(err: ClockError) -> Self { + Self::Clock(err) + } +} + +/// Number of bidirectional endpoint pairs supported (EP0..EP{N-1}). +/// +/// The MCXA577 USBHS controller exposes eight bidirectional endpoint pairs. +const EP_COUNT: usize = 8; + +/// Maximum packet size for a full-speed endpoint. +const FS_MAX_PACKET: u16 = 64; +const ENDPOINT_TYPE_UNALLOCATED: u8 = 0xFF; +const ALL_USBSTS: u32 = 0xFFFF_FFFF; +const ALL_ENDPOINT_BITS: u32 = 0xFFFF_FFFF; +const DEFAULT_BURST_SIZE: u32 = (0x10 << 8) | 0x10; +const ENDPOINT_WAIT_ITERS: usize = 100_000; + +// The PAC exposes generated field accessors for these registers. These raw +// masks stay local to the register wrapper because the endpoint registers are an +// indexed array in hardware but generated as distinct Rust types per endpoint. +const USBCMD_RS: u32 = 1 << 0; +const USBCMD_RST: u32 = 1 << 1; +const USBCMD_SUTW: u32 = 1 << 13; +const USBSTS_UI: u32 = 1 << 0; +const USBSTS_UEI: u32 = 1 << 1; +const USBSTS_PCI: u32 = 1 << 2; +const USBSTS_URI: u32 = 1 << 6; +const USBSTS_SLI: u32 = 1 << 8; +const USBMODE_CM_DEVICE: u32 = 0b10; +const USBMODE_SLOM: u32 = 1 << 3; +const PORTSC1_PFSC: u32 = 1 << 24; +const DEVICEADDR_USBADRA: u32 = 1 << 24; +const DEVICEADDR_USBADR_SHIFT: u32 = 25; +const EPCTRL_RXE: u32 = 1 << 7; +const EPCTRL_RXR: u32 = 1 << 6; +const EPCTRL_RXS: u32 = 1 << 0; +const EPCTRL_RXT_SHIFT: u32 = 2; +const EPCTRL_TXE: u32 = 1 << 23; +const EPCTRL_TXR: u32 = 1 << 22; +const EPCTRL_TXS: u32 = 1 << 16; +const EPCTRL_TXT_SHIFT: u32 = 18; + +/// Static DMA memory cell. +/// +/// The USBHS controller reads and writes these objects directly. This wrapper +/// keeps the global mutability localized and makes all access sites spell out +/// their raw-pointer safety instead of exposing `static mut` items throughout +/// the driver. +struct DmaCell(UnsafeCell); + +impl DmaCell { + const fn new(value: T) -> Self { + Self(UnsafeCell::new(value)) + } + + #[inline] + fn as_ptr(&self) -> *mut T { + self.0.get() + } +} + +// SAFETY: USB driver ownership plus per-endpoint transfer ownership serialize +// mutable access. The controller may access the memory concurrently as DMA, so +// all synchronization with hardware is done with explicit barriers. +unsafe impl Sync for DmaCell {} + +// ---- DMA-visible controller structures (statically allocated) ---- +// +// The USBHS controller reads and writes these structures directly via DMA. The +// current MCX-A target has no data cache and its SRAM is non-cacheable, so plain +// statics stay coherent with the controller using only the `dsb()` barriers in +// `dma_clean` / `dma_invalidate`. Those two functions are the single porting hook: +// a part with a data cache must grow real clean/invalidate operations there and +// place this state in a non-cacheable region. `Driver::new` debug-asserts that the +// data cache is disabled so the assumption cannot be silently violated. + +/// The device queue-head list. Must be 2 KiB aligned and contain `2 * EP_COUNT` +/// entries (OUT at even indices, IN at odd indices). +#[repr(C, align(2048))] +struct QhList { + qh: [QueueHead; EP_COUNT * 2], +} + +/// One transfer descriptor per endpoint direction. +#[repr(C, align(32))] +struct DtdList { + dtd: [TransferDescriptor; EP_COUNT * 2], +} + +static QH_LIST: DmaCell = DmaCell::new(QhList { + qh: [QueueHead::new(); EP_COUNT * 2], +}); +static DTD_LIST: DmaCell = DmaCell::new(DtdList { + dtd: [TransferDescriptor::new(); EP_COUNT * 2], +}); + +/// Per-endpoint-direction transfer-complete wakers (index = `2*ep + dir`). +static EP_WAKERS: [AtomicWaker; EP_COUNT * 2] = [const { AtomicWaker::new() }; EP_COUNT * 2]; +/// Waker for bus events (reset/suspend/resume) handled in [`Bus::poll`]. +static BUS_WAKER: AtomicWaker = AtomicWaker::new(); + +/// Bus event flags set by the interrupt handler and consumed by [`Bus::poll`]. +static FLAG_RESET: AtomicBool = AtomicBool::new(false); +static FLAG_SUSPEND: AtomicBool = AtomicBool::new(false); +static FLAG_RESUME: AtomicBool = AtomicBool::new(false); +static SUSPENDED: AtomicBool = AtomicBool::new(false); +static TRANSFER_ABORT_EPOCH: AtomicU32 = AtomicU32::new(0); + +static ERROR_COUNT: AtomicU32 = AtomicU32::new(0); +static RESET_COUNT: AtomicU32 = AtomicU32::new(0); +static SUSPEND_COUNT: AtomicU32 = AtomicU32::new(0); +static RESUME_COUNT: AtomicU32 = AtomicU32::new(0); + +/// Per-endpoint-direction configuration captured at allocation time and applied +/// by [`Bus::endpoint_set_enabled`] (index = `2*ep + dir`). +static EP_MAX_PACKET: [AtomicU16; EP_COUNT * 2] = [const { AtomicU16::new(0) }; EP_COUNT * 2]; +static EP_TYPE: [AtomicU8; EP_COUNT * 2] = [const { AtomicU8::new(ENDPOINT_TYPE_UNALLOCATED) }; EP_COUNT * 2]; + +/// Snapshot of USBHS diagnostic counters. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +#[non_exhaustive] +pub struct DiagnosticCounters { + /// USB error interrupts. + pub errors: u32, + /// Bus reset events. + pub resets: u32, + /// Suspend events. + pub suspends: u32, + /// Resume events. + pub resumes: u32, +} + +/// Return the current USBHS diagnostic counters. +pub fn diagnostic_counters() -> DiagnosticCounters { + DiagnosticCounters { + errors: ERROR_COUNT.load(Ordering::Relaxed), + resets: RESET_COUNT.load(Ordering::Relaxed), + suspends: SUSPEND_COUNT.load(Ordering::Relaxed), + resumes: RESUME_COUNT.load(Ordering::Relaxed), + } +} + +fn wake_all_endpoint_wakers() { + for waker in &EP_WAKERS { + waker.wake(); + } +} + +fn abort_endpoint_transfers() { + TRANSFER_ABORT_EPOCH.fetch_add(1, Ordering::Relaxed); + wake_all_endpoint_wakers(); +} + +fn reset_static_state() { + FLAG_RESET.store(false, Ordering::Relaxed); + FLAG_SUSPEND.store(false, Ordering::Relaxed); + FLAG_RESUME.store(false, Ordering::Relaxed); + SUSPENDED.store(false, Ordering::Relaxed); + TRANSFER_ABORT_EPOCH.fetch_add(1, Ordering::Relaxed); + ERROR_COUNT.store(0, Ordering::Relaxed); + RESET_COUNT.store(0, Ordering::Relaxed); + SUSPEND_COUNT.store(0, Ordering::Relaxed); + RESUME_COUNT.store(0, Ordering::Relaxed); + + for ep_type in &EP_TYPE { + ep_type.store(ENDPOINT_TYPE_UNALLOCATED, Ordering::Relaxed); + } + for max_packet in &EP_MAX_PACKET { + max_packet.store(0, Ordering::Relaxed); + } + + // SAFETY: Driver construction has exclusive ownership of the peripheral. + unsafe { + let qh = qh_ptr(); + let dtd = dtd_ptr(); + for i in 0..EP_COUNT * 2 { + (*qh)[i] = QueueHead::new(); + (*dtd)[i] = TransferDescriptor::new(); + } + core::ptr::write_bytes(EP_BUFFERS.as_ptr(), 0, 1); + } + + wake_all_endpoint_wakers(); +} + +#[derive(Clone, Copy)] +struct EndpointSlot { + index: usize, + dir: Direction, +} + +impl EndpointSlot { + #[inline] + fn new(index: usize, dir: Direction) -> Self { + Self { index, dir } + } + + #[inline] + fn from_addr(addr: EndpointAddress) -> Self { + let dir = if addr.is_in() { Direction::In } else { Direction::Out }; + Self::new(addr.index(), dir) + } + + #[inline] + fn array_index(self) -> usize { + self.index * 2 + self.dir_index() + } + + #[inline] + fn dir_index(self) -> usize { + match self.dir { + Direction::Out => 0, + Direction::In => 1, + } + } + + #[inline] + #[cfg_attr(not(feature = "defmt"), allow(dead_code))] + fn dir_num(self) -> u8 { + self.dir_index() as u8 + } + + #[inline] + fn endpoint_bit(self) -> u32 { + match self.dir { + Direction::Out => 1 << self.index, + Direction::In => 1 << (self.index + 16), + } + } + + #[inline] + fn is_enabled(self, r: UsbRegs) -> bool { + r.endptctrl(self.index) & endpoint_enable_bit(self) != 0 + } + + #[inline] + fn waker(self) -> &'static AtomicWaker { + &EP_WAKERS[self.array_index()] + } +} + +#[inline] +fn endpoint_type_bits(ep_type: EndpointType) -> u32 { + match ep_type { + EndpointType::Control => 0b00, + EndpointType::Isochronous => 0b01, + EndpointType::Bulk => 0b10, + EndpointType::Interrupt => 0b11, + } +} + +#[inline] +fn endpoint_ctrl_enable_bits(slot: EndpointSlot, ep_type: EndpointType) -> (u32, u32) { + let ty = endpoint_type_bits(ep_type); + match slot.dir { + Direction::Out => ( + 0b11 << EPCTRL_RXT_SHIFT, + (ty << EPCTRL_RXT_SHIFT) | EPCTRL_RXE | EPCTRL_RXR, + ), + Direction::In => ( + 0b11 << EPCTRL_TXT_SHIFT, + (ty << EPCTRL_TXT_SHIFT) | EPCTRL_TXE | EPCTRL_TXR, + ), + } +} + +#[inline] +fn endpoint_stall_bit(slot: EndpointSlot) -> u32 { + match slot.dir { + Direction::Out => EPCTRL_RXS, + Direction::In => EPCTRL_TXS, + } +} + +#[inline] +fn endpoint_enable_bit(slot: EndpointSlot) -> u32 { + match slot.dir { + Direction::Out => EPCTRL_RXE, + Direction::In => EPCTRL_TXE, + } +} + +#[inline] +fn endpoint_reset_bit(slot: EndpointSlot) -> u32 { + match slot.dir { + Direction::Out => EPCTRL_RXR, + Direction::In => EPCTRL_TXR, + } +} + +#[inline] +fn endpoint_max_packet(slot: EndpointSlot) -> Option { + let max_packet = EP_MAX_PACKET[slot.array_index()].load(Ordering::Relaxed); + (max_packet != 0).then_some(max_packet) +} + +#[inline] +fn qh_ptr() -> *mut [QueueHead; EP_COUNT * 2] { + // SAFETY: only constructs a raw pointer to DMA-owned static memory. + unsafe { core::ptr::addr_of_mut!((*QH_LIST.as_ptr()).qh) } +} + +#[inline] +fn dtd_ptr() -> *mut [TransferDescriptor; EP_COUNT * 2] { + // SAFETY: only constructs a raw pointer to DMA-owned static memory. + unsafe { core::ptr::addr_of_mut!((*DTD_LIST.as_ptr()).dtd) } +} + +#[inline] +fn ep_buffer_ptr(slot: EndpointSlot) -> *mut u8 { + // SAFETY: only constructs a raw pointer to the endpoint's dedicated bounce + // buffer. The endpoint future owns that slot while the transfer is active. + unsafe { core::ptr::addr_of_mut!((*EP_BUFFERS.as_ptr()).buf[slot.array_index()]) as *mut u8 } +} + +/// Controller register handle. +#[derive(Clone, Copy)] +struct UsbRegs(crate::pac::usbhs::Usbhs); + +impl UsbRegs { + #[inline] + #[cfg_attr(not(feature = "defmt"), allow(dead_code))] + fn id(self) -> u32 { + self.0.ID().read().0 + } + + #[inline] + fn usbcmd(self) -> u32 { + self.0.USBCMD().read().0 + } + + #[inline] + fn set_usbcmd(self, bits: u32) { + self.0.USBCMD().write(|w| w.0 = bits); + } + + #[inline] + fn modify_usbcmd(self, f: impl FnOnce(u32) -> u32) { + self.set_usbcmd(f(self.usbcmd())); + } + + #[inline] + fn usbsts(self) -> u32 { + self.0.USBSTS().read().0 + } + + #[inline] + fn clear_usbsts(self, bits: u32) { + self.0.USBSTS().write(|w| w.0 = bits); + } + + #[inline] + fn usbintr(self) -> u32 { + self.0.USBINTR().read().0 + } + + #[inline] + fn set_usbintr(self, bits: u32) { + self.0.USBINTR().write(|w| w.0 = bits); + } + + #[inline] + fn set_deviceaddr(self, bits: u32) { + self.0.DEVICEADDR().write(|w| w.0 = bits); + } + + #[inline] + fn set_endptlistaddr(self, bits: u32) { + self.0.ENDPTLISTADDR().write(|w| w.0 = bits); + } + + #[inline] + fn set_burstsize(self, bits: u32) { + self.0.BURSTSIZE().write(|w| w.0 = bits); + } + + #[inline] + fn portsc1(self) -> u32 { + self.0.PORTSC1().read().0 + } + + #[inline] + fn set_portsc1(self, bits: u32) { + self.0.PORTSC1().write(|w| w.0 = bits); + } + + #[inline] + fn modify_portsc1(self, f: impl FnOnce(u32) -> u32) { + self.set_portsc1(f(self.portsc1())); + } + + #[inline] + fn set_usbmode(self, bits: u32) { + self.0.USBMODE().write(|w| w.0 = bits); + } + + #[inline] + fn endptsetupstat(self) -> u32 { + self.0.ENDPTSETUPSTAT().read().0 + } + + #[inline] + fn clear_endptsetupstat(self, bits: u32) { + self.0.ENDPTSETUPSTAT().write(|w| w.0 = bits); + } + + #[inline] + fn endptprime(self) -> u32 { + self.0.ENDPTPRIME().read().0 + } + + #[inline] + fn set_endptprime(self, bits: u32) { + self.0.ENDPTPRIME().write(|w| w.0 = bits); + } + + #[inline] + fn endptflush(self) -> u32 { + self.0.ENDPTFLUSH().read().0 + } + + #[inline] + fn set_endptflush(self, bits: u32) { + self.0.ENDPTFLUSH().write(|w| w.0 = bits); + } + + #[inline] + fn endptcomplete(self) -> u32 { + self.0.ENDPTCOMPLETE().read().0 + } + + #[inline] + fn clear_endptcomplete(self, bits: u32) { + self.0.ENDPTCOMPLETE().write(|w| w.0 = bits); + } + + #[inline] + fn endptctrl(self, index: usize) -> u32 { + match index { + 0 => self.0.ENDPTCTRL0().read().0, + 1 => self.0.ENDPTCTRL1().read().0, + 2 => self.0.ENDPTCTRL2().read().0, + 3 => self.0.ENDPTCTRL3().read().0, + 4 => self.0.ENDPTCTRL4().read().0, + 5 => self.0.ENDPTCTRL5().read().0, + 6 => self.0.ENDPTCTRL6().read().0, + 7 => self.0.ENDPTCTRL7().read().0, + _ => unreachable!(), + } + } + + #[inline] + fn set_endptctrl(self, index: usize, bits: u32) { + match index { + 0 => self.0.ENDPTCTRL0().write(|w| w.0 = bits), + 1 => self.0.ENDPTCTRL1().write(|w| w.0 = bits), + 2 => self.0.ENDPTCTRL2().write(|w| w.0 = bits), + 3 => self.0.ENDPTCTRL3().write(|w| w.0 = bits), + 4 => self.0.ENDPTCTRL4().write(|w| w.0 = bits), + 5 => self.0.ENDPTCTRL5().write(|w| w.0 = bits), + 6 => self.0.ENDPTCTRL6().write(|w| w.0 = bits), + 7 => self.0.ENDPTCTRL7().write(|w| w.0 = bits), + _ => unreachable!(), + } + } + + #[inline] + fn modify_endptctrl(self, index: usize, f: impl FnOnce(u32) -> u32) { + self.set_endptctrl(index, f(self.endptctrl(index))); + } +} + +#[inline] +fn regs() -> UsbRegs { + UsbRegs(crate::pac::USB1) +} + +#[inline] +fn control_setup_pending() -> bool { + regs().endptsetupstat() & 1 != 0 +} + +#[inline] +fn dma_clean(_ptr: *const u8, _len: usize) { + // MCXA577 SRAM is currently non-cacheable. If this driver moves to a target + // with D-cache, clean the range here before the controller reads it. + asm::dsb(); +} + +#[inline] +fn dma_invalidate(_ptr: *const u8, _len: usize) { + // MCXA577 SRAM is currently non-cacheable. If this driver moves to a target + // with D-cache, invalidate the range here before the CPU reads it. + asm::dsb(); +} + +/// Architectural address of the SCB Configuration and Control Register, and its +/// data-cache-enable bit (DC). +const SCB_CCR_ADDR: *const u32 = 0xE000_ED14 as *const u32; +const SCB_CCR_DC: u32 = 1 << 16; + +/// Debug-assert that the data cache is disabled. The DMA structures rely on +/// non-cacheable SRAM for coherence with the controller (only `dsb()` barriers, +/// no clean/invalidate); on a future cached port this fires unless `dma_clean` / +/// `dma_invalidate` are taught real cache maintenance. +#[inline] +fn assert_dma_noncacheable() { + // SAFETY: architectural read-only access to the System Control Block CCR. + let dcache_enabled = unsafe { core::ptr::read_volatile(SCB_CCR_ADDR) } & SCB_CCR_DC != 0; + debug_assert!( + !dcache_enabled, + "USB DMA assumes non-cacheable SRAM, but the data cache is enabled; implement cache \ + maintenance in dma_clean/dma_invalidate before enabling the D-cache" + ); +} + +// ========================================================================= +// Interrupt handler +// ========================================================================= + +type UsbInterrupt = crate::interrupt::typelevel::USB1_HS; + +/// Interrupt handler for the USB controller. +/// +/// Bind this with [`crate::bind_interrupts!`] to the `USB1_HS` interrupt. +pub struct InterruptHandler; + +impl interrupt::typelevel::Handler for InterruptHandler { + unsafe fn on_interrupt() { + let r = regs(); + let status = r.usbsts() & r.usbintr(); + if status == 0 { + return; + } + + // Acknowledge all handled sources (write-1-to-clear). + r.clear_usbsts(status); + + if status & USBSTS_URI != 0 { + RESET_COUNT.fetch_add(1, Ordering::Relaxed); + SUSPENDED.store(false, Ordering::Relaxed); + // A reset means the bus is live again: discard any pending suspend/resume + // so a stale Suspend cannot be delivered after the Reset and leave the + // stack suspended while the bus is active. + FLAG_SUSPEND.store(false, Ordering::Relaxed); + FLAG_RESUME.store(false, Ordering::Relaxed); + FLAG_RESET.store(true, Ordering::Relaxed); + abort_endpoint_transfers(); + BUS_WAKER.wake(); + } + if status & USBSTS_SLI != 0 { + SUSPEND_COUNT.fetch_add(1, Ordering::Relaxed); + SUSPENDED.store(true, Ordering::Relaxed); + FLAG_SUSPEND.store(true, Ordering::Relaxed); + abort_endpoint_transfers(); + BUS_WAKER.wake(); + } + if status & USBSTS_PCI != 0 { + // Port-change also covers connect, enable, and speed changes. Only + // report Resume when it is an edge out of a known suspended state. + if SUSPENDED.swap(false, Ordering::Relaxed) { + RESUME_COUNT.fetch_add(1, Ordering::Relaxed); + FLAG_RESUME.store(true, Ordering::Relaxed); + BUS_WAKER.wake(); + } + } + if status & USBSTS_UEI != 0 { + ERROR_COUNT.fetch_add(1, Ordering::Relaxed); + // A transaction error retires only the offending dTD (its error bits are + // set and ACTIVE is cleared); it does not invalidate other endpoints. + // Wake all transfer waiters so the affected endpoint re-checks its + // descriptor and fails itself, but do NOT bump the global abort epoch: + // that would also unwind healthy in-flight transfers on other endpoints. + wake_all_endpoint_wakers(); + } + + if status & USBSTS_UI != 0 { + // Endpoint transfer(s) and/or setup packet(s) complete. + // Wake setup/OUT/IN waiters; the futures re-check hardware state. + let setup = r.endptsetupstat(); + let complete = r.endptcomplete(); + if complete != 0 { + r.clear_endptcomplete(complete); + } + for ep in 0..EP_COUNT { + let out = EndpointSlot::new(ep, Direction::Out); + let in_ = EndpointSlot::new(ep, Direction::In); + if setup & (1 << ep) != 0 { + out.waker().wake(); + in_.waker().wake(); + } + if complete & out.endpoint_bit() != 0 { + out.waker().wake(); + } + if complete & in_.endpoint_bit() != 0 { + in_.waker().wake(); + } + } + } + } +} + +// ========================================================================= +// Clock gate +// ========================================================================= + +impl Gate for crate::peripherals::USB1 { + type MrccPeriphConfig = UsbhsConfig; + + #[inline] + unsafe fn enable_clock() { + crate::pac::MRCC0.mrcc_glb_cc2().modify(|w| { + w.set_usb1(true); + w.set_usb1_phy(true); + }); + } + + #[inline] + unsafe fn disable_clock() { + crate::pac::MRCC0.mrcc_glb_cc2().modify(|w| { + w.set_usb1(false); + w.set_usb1_phy(false); + }); + } + + #[inline] + unsafe fn assert_reset() { + crate::pac::MRCC0.mrcc_glb_rst2().modify(|w| { + w.set_usb1(false); + w.set_usb1_phy(false); + }); + // Wait until BOTH reset bits read de-asserted. `is_reset_released()` is the + // AND of the two bits, so reusing it here would stop as soon as either one + // cleared rather than both. + while { + let rst2 = crate::pac::MRCC0.mrcc_glb_rst2().read(); + rst2.usb1() || rst2.usb1_phy() + } {} + } + + #[inline] + unsafe fn release_reset() { + crate::pac::MRCC0.mrcc_glb_rst2().modify(|w| { + w.set_usb1(true); + w.set_usb1_phy(true); + }); + while !Self::is_reset_released() {} + } + + #[inline] + fn is_clock_enabled() -> bool { + let cc2 = crate::pac::MRCC0.mrcc_glb_cc2().read(); + cc2.usb1() && cc2.usb1_phy() + } + + #[inline] + fn is_reset_released() -> bool { + let rst2 = crate::pac::MRCC0.mrcc_glb_rst2().read(); + rst2.usb1() && rst2.usb1_phy() + } +} + +/// Marker for OUT endpoints. +pub enum Out {} +/// Marker for IN endpoints. +pub enum In {} + +trait EndpointDir { + const DIR: Direction; +} + +impl EndpointDir for Out { + const DIR: Direction = Direction::Out; +} + +impl EndpointDir for In { + const DIR: Direction = Direction::In; +} + +#[derive(Clone, Copy)] +struct EndpointConfig { + ep_type: EndpointType, + max_packet_size: u16, +} + +impl EndpointConfig { + #[inline] + fn store(self, slot: EndpointSlot) { + let i = slot.array_index(); + EP_MAX_PACKET[i].store(self.max_packet_size, Ordering::Relaxed); + EP_TYPE[i].store(self.ep_type as u8, Ordering::Relaxed); + } + + #[inline] + fn load(slot: EndpointSlot) -> Option { + let i = slot.array_index(); + let ep_type = match EP_TYPE[i].load(Ordering::Relaxed) { + 0 => EndpointType::Control, + 1 => EndpointType::Isochronous, + 2 => EndpointType::Bulk, + 3 => EndpointType::Interrupt, + _ => return None, + }; + let max_packet_size = EP_MAX_PACKET[i].load(Ordering::Relaxed); + (max_packet_size != 0).then_some(Self { + ep_type, + max_packet_size, + }) + } +} + +// ========================================================================= +// Driver +// ========================================================================= + +/// USB device driver. +pub struct Driver<'d> { + _phantom: PhantomData<&'d mut crate::peripherals::USB1>, + _wake_guard: Option, + alloc_out: u8, + alloc_in: u8, +} + +impl<'d> Driver<'d> { + /// Create a new USB device driver, forced to full speed. + /// + /// This enables the USB clocks, brings up the PHY, and resets the controller + /// into device mode. The controller starts detached; the `embassy-usb` stack + /// attaches it via [`embassy_usb_driver::Bus::enable`]. + pub fn new( + _usb: Peri<'d, crate::peripherals::USB1>, + _irq: impl Binding, + config: impl Into, + ) -> Result { + let config = config.into(); + #[cfg(feature = "defmt")] + defmt::trace!( + "usb: init phy pll_div={=u8} d_cal={=u8} txcal45dp={=u8} txcal45dm={=u8}", + config.phy.pll_div_sel, + config.phy.d_cal, + config.phy.txcal45dp, + config.phy.txcal45dm + ); + assert_dma_noncacheable(); + reset_static_state(); + + // SAFETY: we own the USB peripheral and bring up its clocks/PHY once. + let parts = unsafe { clocks::enable_and_reset::(&config.clock)? }; + + // SAFETY: MRCC has enabled and released the USBHS/USBPHY clocks/resets. + unsafe { + clock::init_phy(&config.phy).map_err(|err| match err { + clock::PhyInitError::InvalidConfig => InitError::PhyConfig, + clock::PhyInitError::PllLock => InitError::PhyPllLock, + })?; + } + reset_controller(); + + // EP0 (control) is implicitly allocated. + Ok(Self { + _phantom: PhantomData, + _wake_guard: parts.wake_guard, + alloc_out: 1 << 0, + alloc_in: 1 << 0, + }) + } + + fn alloc_endpoint( + &mut self, + ep_type: EndpointType, + ep_addr: Option, + max_packet_size: u16, + interval_ms: u8, + ) -> Result, EndpointAllocError> { + if ep_type == EndpointType::Control + || ep_type == EndpointType::Isochronous + || max_packet_size == 0 + || max_packet_size > FS_MAX_PACKET + { + return Err(EndpointAllocError); + } + + let is_in = D::DIR == Direction::In; + let alloc = if is_in { &mut self.alloc_in } else { &mut self.alloc_out }; + + let index = match ep_addr { + Some(addr) => { + let i = addr.index(); + if addr.is_in() != is_in || i == 0 || i >= EP_COUNT || (*alloc & (1 << i)) != 0 { + return Err(EndpointAllocError); + } + i + } + None => { + // Endpoint 0 is reserved for control transfers. + let mut found = None; + for i in 1..EP_COUNT { + if *alloc & (1 << i) == 0 { + found = Some(i); + break; + } + } + found.ok_or(EndpointAllocError)? + } + }; + + *alloc |= 1 << index; + + let addr = EndpointAddress::from_parts(index, D::DIR); + let slot = EndpointSlot::from_addr(addr); + EndpointConfig { + ep_type, + max_packet_size, + } + .store(slot); + + #[cfg(feature = "defmt")] + defmt::trace!( + "usb: alloc ep{=usize} dir={=u8} type={=u8} mps={=u16} interval={=u8}", + index, + slot.dir_num(), + endpoint_type_bits(ep_type) as u8, + max_packet_size, + interval_ms + ); + + Ok(Endpoint { + _phantom: PhantomData, + info: EndpointInfo { + addr, + ep_type, + max_packet_size, + interval_ms, + }, + }) + } +} + +impl<'d> embassy_usb_driver::Driver<'d> for Driver<'d> { + type EndpointOut = Endpoint; + type EndpointIn = Endpoint; + type ControlPipe = ControlPipe; + type Bus = Bus; + + fn alloc_endpoint_out( + &mut self, + ep_type: EndpointType, + ep_addr: Option, + max_packet_size: u16, + interval_ms: u8, + ) -> Result { + self.alloc_endpoint::(ep_type, ep_addr, max_packet_size, interval_ms) + } + + fn alloc_endpoint_in( + &mut self, + ep_type: EndpointType, + ep_addr: Option, + max_packet_size: u16, + interval_ms: u8, + ) -> Result { + self.alloc_endpoint::(ep_type, ep_addr, max_packet_size, interval_ms) + } + + fn start(self, control_max_packet_size: u16) -> (Self::Bus, Self::ControlPipe) { + // Configure the control endpoint queue heads. + init_control_qh(control_max_packet_size); + + let bus = Bus { + power_detected: false, + control_max_packet_size, + _wake_guard: self._wake_guard, + }; + let control = ControlPipe { + max_packet_size: control_max_packet_size, + }; + (bus, control) + } +} + +// ========================================================================= +// Controller bring-up helpers +// ========================================================================= + +/// Reset the controller and place it in device mode (detached, full speed). +fn reset_controller() { + let r = regs(); + + // Stop the controller, then issue a controller reset and wait for it. + r.modify_usbcmd(|v| v & !USBCMD_RS); + r.modify_usbcmd(|v| v | USBCMD_RST); + while r.usbcmd() & USBCMD_RST != 0 {} + + // Device mode, setup-lockout off (we use the setup tripwire instead). + r.set_usbmode(USBMODE_CM_DEVICE | USBMODE_SLOM); + + // Force full speed. + r.modify_portsc1(|v| v | PORTSC1_PFSC); + + // Program the endpoint list base. + let qh_addr = QH_LIST.as_ptr() as u32; + r.set_endptlistaddr(qh_addr); + + #[cfg(feature = "defmt")] + defmt::trace!("usb: controller reset id=0x{=u32:08x} qh=0x{=u32:08x}", r.id(), qh_addr); + + // Reasonable default burst size. + r.set_burstsize(DEFAULT_BURST_SIZE); + + // Clear any pending status, leave interrupts disabled until `enable`. + r.clear_usbsts(ALL_USBSTS); + r.set_usbintr(0); +} + +/// Initialize the control-endpoint (EP0) queue heads. Called once during `start`. +fn init_control_qh(mps: u16) { + let out = EndpointSlot::new(0, Direction::Out); + let in_ = EndpointSlot::new(0, Direction::In); + EndpointConfig { + ep_type: EndpointType::Control, + max_packet_size: mps, + } + .store(out); + EndpointConfig { + ep_type: EndpointType::Control, + max_packet_size: mps, + } + .store(in_); + + // SAFETY: exclusive access during start-up; QH list is owned by the driver. + unsafe { + let qh = qh_ptr(); + // Disable automatic ZLP termination. `embassy-usb` requests status/ZLP + // packets explicitly via the control pipe and endpoint transfers. + // OUT QH[0]: interrupt-on-setup so SETUP packets notify us. + (*qh)[0].capabilities = ((mps as u32) << QH_CAP_MAXLEN_SHIFT) | QH_CAP_IOS | QH_CAP_ZLT; + (*qh)[0].next_dtd = DTD_NEXT_TERMINATE; + // IN QH[1]. + (*qh)[1].capabilities = ((mps as u32) << QH_CAP_MAXLEN_SHIFT) | QH_CAP_ZLT; + (*qh)[1].next_dtd = DTD_NEXT_TERMINATE; + } + + let r = regs(); + r.modify_endptctrl(0, |v| { + (v & !(EPCTRL_TXS | EPCTRL_RXS)) | EPCTRL_RXE | EPCTRL_TXE | EPCTRL_RXR | EPCTRL_TXR + }); +} + +// ========================================================================= +// Endpoint +// ========================================================================= + +/// A USB endpoint. +pub struct Endpoint { + _phantom: PhantomData, + info: EndpointInfo, +} + +/// Configure an endpoint's queue head and control register. +fn configure_endpoint(addr: EndpointAddress, ep_type: EndpointType, mps: u16) { + let index = addr.index(); + let slot = EndpointSlot::from_addr(addr); + let qhi = slot.array_index(); + + // SAFETY: queue-head list is owned by the driver; exclusive access here. + unsafe { + let qh = qh_ptr(); + // Disable automatic ZLP termination. The USB stack explicitly requests + // short/ZLP packets when the class transfer needs one. + (*qh)[qhi].capabilities = ((mps as u32) << QH_CAP_MAXLEN_SHIFT) | QH_CAP_ZLT; + (*qh)[qhi].next_dtd = DTD_NEXT_TERMINATE; + (*qh)[qhi].token = 0; + } + + let r = regs(); + let (type_mask, enable_bits) = endpoint_ctrl_enable_bits(slot, ep_type); + #[cfg(feature = "defmt")] + defmt::trace!( + "usb: configure ep{=usize} dir={=u8} type={=u8} mps={=u16}", + index, + slot.dir_num(), + endpoint_type_bits(ep_type) as u8, + mps + ); + r.modify_endptctrl(index, |v| { + // Reset the data toggle and enable the requested direction. + (v & !type_mask) | enable_bits + }); +} + +impl embassy_usb_driver::Endpoint for Endpoint { + fn info(&self) -> &EndpointInfo { + &self.info + } + + async fn wait_enabled(&mut self) { + let slot = EndpointSlot::from_addr(self.info.addr); + poll_fn(|cx| { + slot.waker().register(cx.waker()); + let r = regs(); + if slot.is_enabled(r) { + Poll::Ready(()) + } else { + Poll::Pending + } + }) + .await + } +} + +impl embassy_usb_driver::EndpointOut for Endpoint { + async fn read(&mut self, buf: &mut [u8]) -> Result { + let index = self.info.addr.index(); + ep_read(index, buf).await + } +} + +impl embassy_usb_driver::EndpointIn for Endpoint { + async fn write(&mut self, buf: &[u8]) -> Result<(), EndpointError> { + let index = self.info.addr.index(); + ep_write(index, buf, false).await + } +} + +// ========================================================================= +// Low-level transfer primitives (dTD prime + completion wait) +// ========================================================================= + +fn wait_register_clear(mut read: impl FnMut() -> u32, mask: u32) -> bool { + for _ in 0..ENDPOINT_WAIT_ITERS { + if read() & mask == 0 { + return true; + } + asm::nop(); + } + false +} + +fn clear_transfer_overlay(slot: EndpointSlot) { + let i = slot.array_index(); + // SAFETY: called after the endpoint direction is flushed or inactive. + unsafe { + let dtd = dtd_ptr(); + (*dtd)[i] = TransferDescriptor::new(); + + let qh = qh_ptr(); + (*qh)[i].next_dtd = DTD_NEXT_TERMINATE; + (*qh)[i].token = 0; + } +} + +fn flush_endpoint_bit(bit: u32) -> Result<(), EndpointError> { + if bit == 0 { + return Ok(()); + } + + let r = regs(); + r.set_endptflush(bit); + if wait_register_clear(|| r.endptflush(), bit) { + Ok(()) + } else { + Err(EndpointError::Disabled) + } +} + +fn flush_endpoint(slot: EndpointSlot) -> Result<(), EndpointError> { + let bit = slot.endpoint_bit(); + let res = flush_endpoint_bit(bit); + if res.is_ok() { + clear_transfer_overlay(slot); + } + res +} + +struct TransferGuard { + slot: EndpointSlot, + armed: bool, +} + +impl TransferGuard { + fn new(slot: EndpointSlot) -> Self { + Self { slot, armed: true } + } + + fn disarm(&mut self) { + self.armed = false; + } +} + +impl Drop for TransferGuard { + fn drop(&mut self) { + if self.armed { + let _ = flush_endpoint(self.slot); + } + } +} + +/// Prime a transfer descriptor on the given endpoint/direction and wait for it +/// to complete, returning the number of bytes transferred. +async fn ep_transfer( + slot: EndpointSlot, + buf_ptr: *mut u8, + len: usize, + abort_on_setup: bool, +) -> Result { + let dtd_i = slot.array_index(); + let qhi = dtd_i; + let abort_epoch = TRANSFER_ABORT_EPOCH.load(Ordering::Relaxed); + + // SAFETY: the descriptor/queue-head for this endpoint direction is owned by + // the caller for the duration of the transfer. + unsafe { + // Build the transfer descriptor. + let dtd = dtd_ptr(); + (*dtd)[dtd_i].next = DTD_NEXT_TERMINATE; + (*dtd)[dtd_i].token = ((len as u32) << DTD_TOKEN_TOTAL_SHIFT) | DTD_TOKEN_IOC | DTD_TOKEN_ACTIVE; + let base = buf_ptr as u32; + (*dtd)[dtd_i].buffer[0] = base; + for page in 1..5 { + (*dtd)[dtd_i].buffer[page] = (base & !DTD_BUFFER_PAGE_MASK) + (page as u32) * DTD_BUFFER_PAGE_SIZE; + } + + // Link it into the queue head overlay and clear status. + let qh = qh_ptr(); + (*qh)[qhi].next_dtd = core::ptr::addr_of!((*dtd)[dtd_i]) as u32; + (*qh)[qhi].token = 0; + } + + let r = regs(); + let prime_bit = slot.endpoint_bit(); + + dma_clean(buf_ptr, len); + // SAFETY: raw pointers to the descriptor and queue head just programmed. + unsafe { + let dtd = dtd_ptr(); + let qh = qh_ptr(); + dma_clean( + core::ptr::addr_of!((*dtd)[dtd_i]).cast(), + core::mem::size_of::(), + ); + dma_clean( + core::ptr::addr_of!((*qh)[qhi]).cast(), + core::mem::size_of::(), + ); + } + + // Prime the endpoint. We allow only one outstanding dTD per endpoint + // direction, so there is no need to use the add-dTD tripwire (ATDTW). + r.set_endptprime(prime_bit); + // Wait until the controller has acknowledged the prime. + if !wait_register_clear(|| r.endptprime(), prime_bit) { + let _ = flush_endpoint(slot); + return Err(EndpointError::Disabled); + } + + // Wait for completion via the waker, re-checking the descriptor status. + let mut guard = TransferGuard::new(slot); + let res = poll_fn(|cx| { + slot.waker().register(cx.waker()); + if TRANSFER_ABORT_EPOCH.load(Ordering::Relaxed) != abort_epoch { + let _ = flush_endpoint(slot); + return Poll::Ready(Err(EndpointError::Disabled)); + } + if !slot.is_enabled(r) { + let _ = flush_endpoint(slot); + return Poll::Ready(Err(EndpointError::Disabled)); + } + if abort_on_setup && control_setup_pending() { + let _ = flush_endpoint(slot); + return Poll::Ready(Err(EndpointError::Disabled)); + } + dma_invalidate(buf_ptr, len); + // SAFETY: raw pointer to the hardware-updated transfer descriptor. + unsafe { + let dtd = dtd_ptr(); + dma_invalidate( + core::ptr::addr_of!((*dtd)[dtd_i]).cast(), + core::mem::size_of::(), + ); + } + // SAFETY: reading the (volatile) hardware-updated descriptor token. + let token = unsafe { + let dtd = dtd_ptr(); + core::ptr::read_volatile(core::ptr::addr_of!((*dtd)[dtd_i].token)) + }; + if token & DTD_TOKEN_ACTIVE != 0 { + return Poll::Pending; + } + if token & DTD_TOKEN_ERROR_MASK != 0 { + #[cfg(feature = "defmt")] + defmt::warn!( + "usb: transfer error ep{=usize} dir={=u8} token=0x{=u32:08x}", + slot.index, + slot.dir_num(), + token + ); + return Poll::Ready(Err(EndpointError::Disabled)); + } + // Remaining bytes are in token[30:16]; transferred = requested - remaining. + let remaining = (token >> DTD_TOKEN_TOTAL_SHIFT) & DTD_TOKEN_TOTAL_MASK; + Poll::Ready(Ok(len.saturating_sub(remaining as usize))) + }) + .await; + + guard.disarm(); + res +} + +/// Static bounce buffers for endpoint transfers (one per endpoint direction), +/// ensuring DMA-visible, suitably-aligned storage. +#[repr(C, align(64))] +struct EpBuffers { + buf: [[u8; FS_MAX_PACKET as usize]; EP_COUNT * 2], +} +static EP_BUFFERS: DmaCell = DmaCell::new(EpBuffers { + buf: [[0; FS_MAX_PACKET as usize]; EP_COUNT * 2], +}); + +async fn ep_read(index: usize, buf: &mut [u8]) -> Result { + let slot = EndpointSlot::new(index, Direction::Out); + let bounce = ep_buffer_ptr(slot); + let cap = endpoint_max_packet(slot).unwrap_or(FS_MAX_PACKET) as usize; + let n = ep_transfer(slot, bounce, cap, false).await?; + if n > buf.len() { + return Err(EndpointError::BufferOverflow); + } + // SAFETY: `bounce` is valid for `n` bytes and `buf` for its length. + unsafe { core::ptr::copy_nonoverlapping(bounce, buf.as_mut_ptr(), n) }; + Ok(n) +} + +async fn ep_write(index: usize, buf: &[u8], abort_on_setup: bool) -> Result<(), EndpointError> { + let slot = EndpointSlot::new(index, Direction::In); + let cap = endpoint_max_packet(slot).unwrap_or(FS_MAX_PACKET) as usize; + if buf.len() > cap { + return Err(EndpointError::BufferOverflow); + } + let bounce = ep_buffer_ptr(slot); + // SAFETY: copying caller data into the owned bounce buffer. + unsafe { core::ptr::copy_nonoverlapping(buf.as_ptr(), bounce, buf.len()) }; + ep_transfer(slot, bounce, buf.len(), abort_on_setup).await?; + Ok(()) +} + +async fn ep_read_control(buf: &mut [u8]) -> Result { + let slot = EndpointSlot::new(0, Direction::Out); + let bounce = ep_buffer_ptr(slot); + let cap = endpoint_max_packet(slot).unwrap_or(FS_MAX_PACKET) as usize; + let n = ep_transfer(slot, bounce, cap, true).await?; + if n > buf.len() { + return Err(EndpointError::BufferOverflow); + } + // SAFETY: `bounce` is valid for `n` bytes and `buf` for its length. + unsafe { core::ptr::copy_nonoverlapping(bounce, buf.as_mut_ptr(), n) }; + Ok(n) +} + +async fn ep_zlp(index: usize, dir: Direction, abort_on_setup: bool) -> Result<(), EndpointError> { + let slot = EndpointSlot::new(index, dir); + // EHCI should not dereference buffer pointers for zero-length transfers, but + // giving it a real aligned address avoids relying on null-pointer behavior. + let bounce = ep_buffer_ptr(slot); + ep_transfer(slot, bounce, 0, abort_on_setup).await?; + Ok(()) +} + +// ========================================================================= +// Control pipe +// ========================================================================= + +/// Control endpoint (EP0) pipe. +pub struct ControlPipe { + max_packet_size: u16, +} + +impl embassy_usb_driver::ControlPipe for ControlPipe { + fn max_packet_size(&self) -> usize { + self.max_packet_size as usize + } + + async fn setup(&mut self) -> [u8; 8] { + let r = regs(); + poll_fn(|cx| { + EndpointSlot::new(0, Direction::Out).waker().register(cx.waker()); + let stat = r.endptsetupstat(); + if stat & 1 == 0 { + return Poll::Pending; + } + + // Acknowledge before the SUTW copy loop. If another SETUP arrives + // during the copy, hardware re-sets ENDPTSETUPSTAT and preserves it + // for the next `setup()` call. + r.clear_endptsetupstat(stat); + + // Read the setup packet using the setup tripwire so a back-to-back + // SETUP cannot corrupt the read. The retry loop is bounded so a wedged + // controller cannot spin the single-threaded executor forever; on + // exhaustion we proceed with the last read (a possibly torn packet the + // host re-issues on failure) and warn, rather than hang. + let mut iters = 0usize; + let setup = loop { + r.modify_usbcmd(|v| v | USBCMD_SUTW); + // SAFETY: control OUT queue head holds the latest setup bytes. + let bytes = unsafe { + let qh = qh_ptr(); + let w0 = core::ptr::read_volatile(core::ptr::addr_of!((*qh)[0].setup[0])); + let w1 = core::ptr::read_volatile(core::ptr::addr_of!((*qh)[0].setup[1])); + let mut b = [0u8; 8]; + b[0..4].copy_from_slice(&w0.to_le_bytes()); + b[4..8].copy_from_slice(&w1.to_le_bytes()); + b + }; + if r.usbcmd() & USBCMD_SUTW != 0 { + break bytes; + } + iters += 1; + if iters >= ENDPOINT_WAIT_ITERS { + #[cfg(feature = "defmt")] + defmt::warn!("usb: SUTW tripwire did not settle; using last setup read"); + break bytes; + } + }; + r.modify_usbcmd(|v| v & !USBCMD_SUTW); + r.modify_endptctrl(0, |v| v & !(EPCTRL_TXS | EPCTRL_RXS)); + + Poll::Ready(setup) + }) + .await + } + + async fn data_out(&mut self, buf: &mut [u8], _first: bool, _last: bool) -> Result { + // EP0 OUT owned by the control pipe. + // `embassy-usb-driver` chunks multi-packet control OUT transfers by + // `max_packet_size()`, so the EP0 bounce buffer only has to hold one + // full-speed control packet. + ep_read_control(buf).await + } + + async fn data_in(&mut self, data: &[u8], _first: bool, last: bool) -> Result<(), EndpointError> { + // EP0 IN owned by the control pipe. + ep_write(0, data, true).await?; + if last { + // Status stage: receive the host's zero-length OUT. + ep_zlp(0, Direction::Out, true).await?; + } + Ok(()) + } + + async fn accept(&mut self) { + if control_setup_pending() { + return; + } + // Status stage: send a zero-length IN packet. + // EP0 IN owned by control pipe. + let _ = ep_zlp(0, Direction::In, true).await; + } + + async fn reject(&mut self) { + if control_setup_pending() { + return; + } + // Stall EP0 in both directions to reject the request. + let r = regs(); + let _ = flush_endpoint(EndpointSlot::new(0, Direction::Out)); + let _ = flush_endpoint(EndpointSlot::new(0, Direction::In)); + r.modify_endptctrl(0, |v| v | EPCTRL_TXS | EPCTRL_RXS); + } + + async fn accept_set_address(&mut self, addr: u8) { + if control_setup_pending() { + return; + } + // EHCI: program the address with the "advance" bit so it is applied + // only after the status stage completes, then send the status IN. + let r = regs(); + r.set_deviceaddr(((addr as u32) << DEVICEADDR_USBADR_SHIFT) | DEVICEADDR_USBADRA); + // EP0 IN for status. + let _ = ep_zlp(0, Direction::In, true).await; + } +} + +// ========================================================================= +// Bus +// ========================================================================= + +/// USB bus control. +pub struct Bus { + power_detected: bool, + control_max_packet_size: u16, + _wake_guard: Option, +} + +impl Drop for Bus { + /// Stops the controller and masks its interrupt only. + /// + /// Clock and PHY teardown is intentionally NOT performed here. Like the nrf and + /// stm32 USB drivers, the MRCC gates and the PHY PLL are left to the peripheral + /// lifetime rather than `Bus::drop`: gating from here would reach back into + /// `MRCC0` and risk touching controller registers after their clock was removed, + /// because `embassy-usb` does not guarantee the endpoints are dropped before the + /// `Bus`. The clocks stay enabled for the borrowed `USB1` lifetime; a subsequent + /// `Driver::new` re-runs `enable_and_reset`, so re-initialization is clean. + fn drop(&mut self) { + let r = regs(); + r.modify_usbcmd(|v| v & !USBCMD_RS); + r.set_usbintr(0); + r.clear_usbsts(ALL_USBSTS); + UsbInterrupt::disable(); + } +} + +impl embassy_usb_driver::Bus for Bus { + async fn enable(&mut self) { + let r = regs(); + // Enable the interrupt sources we handle. + r.set_usbintr(USBSTS_UI | USBSTS_UEI | USBSTS_PCI | USBSTS_URI | USBSTS_SLI); + #[cfg(feature = "defmt")] + defmt::trace!("usb: bus enable"); + + // SAFETY: enabling the controller interrupt. + unsafe { + UsbInterrupt::unpend(); + UsbInterrupt::enable(); + } + + // Attach: set Run/Stop. + r.modify_usbcmd(|v| v | USBCMD_RS); + } + + async fn disable(&mut self) { + let r = regs(); + #[cfg(feature = "defmt")] + defmt::trace!("usb: bus disable"); + r.modify_usbcmd(|v| v & !USBCMD_RS); + r.set_usbintr(0); + UsbInterrupt::disable(); + } + + async fn poll(&mut self) -> Event { + poll_fn(|cx| { + BUS_WAKER.register(cx.waker()); + + if !self.power_detected { + // Surface an initial power-detected event so the stack proceeds. + self.power_detected = true; + return Poll::Ready(Event::PowerDetected); + } + + // Bus events are drained in fixed priority: reset, then suspend, then + // resume. Resume is only ever flagged on an edge out of an observed + // suspended state (see the interrupt handler), so a suspend->resume pair + // is emitted in order. Reset clears any pending suspend/resume below, so + // the bus is never left suspended after a reset. + if FLAG_RESET.swap(false, Ordering::Relaxed) { + #[cfg(feature = "defmt")] + defmt::trace!("usb: bus reset"); + // A reset supersedes any suspend/resume that raced ahead of this poll. + FLAG_SUSPEND.store(false, Ordering::Relaxed); + FLAG_RESUME.store(false, Ordering::Relaxed); + // Re-initialize endpoint 0 and clear setup/complete state. + let r = regs(); + let setup = r.endptsetupstat(); + r.clear_endptsetupstat(setup); + let complete = r.endptcomplete(); + r.clear_endptcomplete(complete); + if !wait_register_clear(|| r.endptprime(), ALL_ENDPOINT_BITS) { + #[cfg(feature = "defmt")] + defmt::warn!("usb: reset timed out waiting for ENDPTPRIME"); + } + let _ = flush_endpoint_bit(ALL_ENDPOINT_BITS); + r.set_deviceaddr(0); + init_control_qh(self.control_max_packet_size); + return Poll::Ready(Event::Reset); + } + if FLAG_SUSPEND.swap(false, Ordering::Relaxed) { + #[cfg(feature = "defmt")] + defmt::trace!("usb: suspend"); + return Poll::Ready(Event::Suspend); + } + if FLAG_RESUME.swap(false, Ordering::Relaxed) { + #[cfg(feature = "defmt")] + defmt::trace!("usb: resume"); + return Poll::Ready(Event::Resume); + } + Poll::Pending + }) + .await + } + + async fn remote_wakeup(&mut self) -> Result<(), Unsupported> { + Err(Unsupported) + } + + fn endpoint_set_enabled(&mut self, ep_addr: EndpointAddress, enabled: bool) { + let slot = EndpointSlot::from_addr(ep_addr); + if enabled { + if let Some(config) = EndpointConfig::load(slot) { + configure_endpoint(ep_addr, config.ep_type, config.max_packet_size); + } + } else { + let r = regs(); + #[cfg(feature = "defmt")] + defmt::trace!("usb: disable ep{=usize} dir={=u8}", slot.index, slot.dir_num()); + r.modify_endptctrl(slot.index, |v| v & !endpoint_enable_bit(slot)); + } + slot.waker().wake(); + } + + fn endpoint_set_stalled(&mut self, ep_addr: EndpointAddress, stalled: bool) { + let slot = EndpointSlot::from_addr(ep_addr); + let r = regs(); + #[cfg(feature = "defmt")] + defmt::trace!( + "usb: stall ep{=usize} dir={=u8} stalled={=u8}", + slot.index, + slot.dir_num(), + stalled as u8 + ); + r.modify_endptctrl(slot.index, |v| { + let bit = endpoint_stall_bit(slot); + if stalled { + v | bit + } else { + (v & !bit) | endpoint_reset_bit(slot) + } + }); + } + + fn endpoint_is_stalled(&mut self, ep_addr: EndpointAddress) -> bool { + let slot = EndpointSlot::from_addr(ep_addr); + regs().endptctrl(slot.index) & endpoint_stall_bit(slot) != 0 + } +} diff --git a/examples/mcxa5xx/Cargo.toml b/examples/mcxa5xx/Cargo.toml index 908dc1c2ae..2485f294d6 100644 --- a/examples/mcxa5xx/Cargo.toml +++ b/examples/mcxa5xx/Cargo.toml @@ -19,6 +19,7 @@ embassy-mcxa = { version = "0.1.0", path = "../../embassy-mcxa", features = ["de embassy-sync = { version = "0.8.0", path = "../../embassy-sync" } embassy-time = { version = "0.5.1", path = "../../embassy-time", features = ["defmt", "defmt-timestamp-uptime"] } embassy-time-driver = { version = "0.2.2", path = "../../embassy-time-driver", optional = true } +embassy-usb = { version = "0.6.0", path = "../../embassy-usb", features = ["defmt"] } embedded-io-async = "0.7.0" embedded-hal-async = "1.0" embedded-storage = "0.3.1" @@ -27,6 +28,7 @@ panic-probe = { version = "1.0", features = ["print-defmt"] } rand_core = "0.9" static_cell = "2.1.1" tmp108 = "0.4.0" +usbd-hid = "0.10.0" [features] default = ["default-executor"] diff --git a/examples/mcxa5xx/src/bin/espi_device.rs b/examples/mcxa5xx/src/bin/espi_device.rs new file mode 100644 index 0000000000..7ee107a882 --- /dev/null +++ b/examples/mcxa5xx/src/bin/espi_device.rs @@ -0,0 +1,831 @@ +//! Interactive eSPI device example for the FRDM-MCXA577. +//! +//! Rust port of the MCUX SDK `espi_device` test bench used with the TotalPhase +//! Promira eSPI host/analyzer setup (see the "MCXA577 eSPI Test Bench and +//! Development" write-up). The device exposes: +//! - an OOB mailbox (port 0), an ACPI endpoint at IO 0x100 (port 1), an +//! index/data endpoint at IO 0x200 (port 2), a mailbox at IO 0x300 (port 3), +//! and a 256-byte virtual slave-attached flash (port 4, base0 = 0x1000); +//! - Port 80 POST-code capture and virtual-wire reporting; +//! - the same serial console commands as the SDK example on the FRDM debug +//! UART (LPUART1, 115200-8-N-1) so `espi_autotest.py` / `espi_generator.py` +//! can drive it. + +#![no_std] +#![no_main] + +use core::fmt::Write as _; + +use defmt::info; +use embassy_executor::Spawner; +use embassy_futures::select::{Either, select}; +use embassy_mcxa::clocks::PoweredClock; +use embassy_mcxa::clocks::config::{Div8, SoscConfig, SoscMode}; +use embassy_mcxa::espi::{ + AddrBase, Config as EspiDriverConfig, Espi, EspiRam, Event, FlashRequest, FlashRequestKind, GpioWire, + InterruptHandler, MailboxStatus, OMFLEN_SAF_COMPLETION_FAIL, OMFLEN_SAF_COMPLETION_NO_DATA, + OMFLEN_SAF_COMPLETION_WITH_DATA, PortConfig, PortError, PortEvent, PortType, RamSize, SSTCL_SAF_COMPLETION, + SSTCL_SAF_REQ_ACCEPTED, SafCompletionType, VWireOut, +}; +use embassy_mcxa::lpuart::{Buffered, BufferedInterruptHandler, Config as UartConfig, Lpuart, LpuartTx}; +use embassy_mcxa::{bind_interrupts, peripherals}; +use embedded_io_async::Write; +use heapless::String; +use static_cell::StaticCell; +use {defmt_rtt as _, embassy_mcxa as hal, panic_probe as _}; + +bind_interrupts!(struct Irqs { + ESPI => InterruptHandler; + LPUART1 => BufferedInterruptHandler; +}); + +const FLASH_SIZE: usize = 256; +const SAF_RX_SPLIT_MAX: usize = 4; + +/// Pending split completions for a host flash read (port of `g_readQueue`). +struct ReadQueue { + item: [(u32, u32, SafCompletionType); SAF_RX_SPLIT_MAX], + tag: u8, + cur: usize, + tot: usize, +} + +impl ReadQueue { + const fn new() -> Self { + Self { + item: [(0, 0, SafCompletionType::Only); SAF_RX_SPLIT_MAX], + tag: 0, + cur: 0, + tot: 0, + } + } +} + +static ESPI_RAM: StaticCell = StaticCell::new(); + +/// UART print helper: format into a stack buffer, then write it out. +macro_rules! uprint { + ($tx:expr, $($arg:tt)*) => {{ + let mut s: String<512> = String::new(); + let _ = write!(s, $($arg)*); + let _ = $tx.write_all(s.as_bytes()).await; + }}; +} + +#[embassy_executor::main] +async fn main(_spawner: Spawner) { + let mut hal_config = hal::config::Config::default(); + // Match the SDK board profile: 24 MHz SOSC available, FRO_LF divider for + // the debug UART. The eSPI functional clock uses FRO_HF undivided. + hal_config.clock_cfg.sosc = Some(SoscConfig { + mode: SoscMode::CrystalOscillator, + frequency: 24_000_000, + power: PoweredClock::NormalEnabledDeepSleepDisabled, + }); + hal_config.clock_cfg.sirc.fro_12m_enabled = true; + hal_config.clock_cfg.sirc.fro_lf_div = Some(Div8::no_div()); + let p = hal::init(hal_config); + info!("=== eSPI device example ==="); + + // Debug-console UART (FRDM MCU-Link CMSIS-DAP COM port). + let uart_config = UartConfig { + baudrate_bps: 115_200, + rx_fifo_watermark: 0, + tx_fifo_watermark: 0, + ..Default::default() + }; + static TX_BUF: StaticCell<[u8; 512]> = StaticCell::new(); + static RX_BUF: StaticCell<[u8; 64]> = StaticCell::new(); + let mut uart = Lpuart::new_buffered( + p.LPUART1, + p.P1_9, + p.P1_8, + Irqs, + TX_BUF.init([0; 512]), + RX_BUF.init([0; 64]), + uart_config, + ) + .unwrap(); + let (tx, rx) = uart.split_ref(); + + // Port layout of the SDK test bench (`g_portCfg`). + let ports = [ + PortConfig { + port_type: PortType::MailboxOobSplit, + direction: 0, + ram_offset: 0x0000, + ram_size: RamSize::Size256B, + addr_offset: 0x0000, + addr_base: AddrBase::Direct, + idx_offset: 0, + }, + PortConfig { + port_type: PortType::AcpiEndpoint, + direction: 0, + ram_offset: 0x0100, + ram_size: RamSize::Size4B, + addr_offset: 0x0100, + addr_base: AddrBase::Direct, + idx_offset: 0, + }, + PortConfig { + port_type: PortType::AcpiIndexData, + direction: 1, + ram_offset: 0x0200, + ram_size: RamSize::Size4B, + addr_offset: 0x0200, + addr_base: AddrBase::Direct, + idx_offset: 1, + }, + PortConfig { + port_type: PortType::MailboxSingle, + direction: 0, + ram_offset: 0x0300, + ram_size: RamSize::Size256B, + addr_offset: 0x0300, + addr_base: AddrBase::Direct, + idx_offset: 0, + }, + PortConfig { + port_type: PortType::BusMasterFlashSingle, + direction: 0, + ram_offset: 0x0500, + ram_size: RamSize::Size64B, + addr_offset: 0x0500, + addr_base: AddrBase::Base0, + idx_offset: 0, + }, + ]; + + let mut espi_config = EspiDriverConfig::default(); + espi_config.base0_addr = 0x1000; + espi_config.base1_addr = 0x2000; + espi_config.enable_alert_pin = true; + espi_config.enable_oob = true; + espi_config.enable_saf = true; + espi_config.enable_p80 = true; + + let ram = ESPI_RAM.init(EspiRam::new()); + let mut espi = match Espi::new( + p.ESPI0, + Irqs, + p.P4_10, // CLK + p.P4_11, // CSn + p.P4_9, // DATA0 + p.P4_8, // DATA1 + p.P4_13, // DATA2 + p.P4_12, // DATA3 + p.P4_6, // RST + p.P4_7, // NOTIFY + ram, + &ports, + espi_config, + ) { + Ok(espi) => espi, + Err(e) => defmt::panic!("eSPI init failed: {:?}", e), + }; + info!("eSPI configured; waiting for host"); + + let mut flash = [0u8; FLASH_SIZE]; + let mut queue = ReadQueue::new(); + let mut last_gpio = espi.gpio_wires(); + let mut line = [0u8; 48]; + let mut line_len = 0usize; + let mut chunk = [0u8; 16]; + + uprint!(tx, "\r\nInteractive eSPI device example.\r\n"); + print_help(tx).await; + uprint!(tx, "> "); + + loop { + match select(espi.wait_event(), rx.read(&mut chunk)).await { + Either::First(event) => { + handle_event(&mut espi, tx, &mut flash, &mut queue, &mut last_gpio, event).await; + } + Either::Second(Ok(n)) => { + for &byte in &chunk[..n] { + match byte { + b'\r' | b'\n' => { + uprint!(tx, "\r\n"); + if line_len > 0 { + let cmd = core::str::from_utf8(&line[..line_len]).unwrap_or(""); + handle_command(&mut espi, tx, cmd).await; + line_len = 0; + } + uprint!(tx, "> "); + } + 0x08 | 0x7F => { + if line_len > 0 { + line_len -= 1; + uprint!(tx, "\x08 \x08"); + } + } + 0x20..=0x7E if line_len < line.len() => { + line[line_len] = byte; + line_len += 1; + let _ = tx.write_all(&[byte]).await; + } + _ => {} + } + } + } + Either::Second(Err(_)) => {} + } + } +} + +// ========================================================================= +// Event handling (port of ESPI_CommonCallback / ESPI_PortCallback) +// ========================================================================= + +async fn handle_event( + espi: &mut Espi<'_>, + tx: &mut LpuartTx<'_, Buffered>, + flash: &mut [u8; FLASH_SIZE], + queue: &mut ReadQueue, + last_gpio: &mut GpioWire, + event: Event, +) { + match event { + Event::BusReset => { + uprint!(tx, "eSPI bus reset.\r\n"); + print_espi_config(espi, tx).await; + } + Event::CrcError => uprint!(tx, "eSPI bus CRC Error!\r\n"), + Event::HostStall => info!("espi: host stall"), + Event::WireChange(vw) => { + uprint!(tx, "Virtual wire change: =0x{:08X}\r\n", vw.0); + for (bit, name) in [ + (vw.slp_s3n(), " - SLP_S3N\r\n"), + (vw.slp_s4n(), " - SLP_S4N\r\n"), + (vw.slp_s5n(), " - SLP_S5N\r\n"), + (vw.sus_stat(), " - SUS_STAT\r\n"), + (vw.pltrstn(), " - PLTRST\r\n"), + (vw.oob_rst_warn(), " - OOB_RST_WARN\r\n"), + (vw.host_rst_warn(), " - HOST_RST_WARN\r\n"), + (vw.sus_warn(), " - SUS_WARN\r\n"), + (vw.sus_pwrdn_ackn(), " - SUS_PWRDN_ACK\r\n"), + (vw.slp_an(), " - SLP_AN\r\n"), + (vw.slp_lan(), " - SLP_LAN\r\n"), + (vw.slp_wlan(), " - SLP_WLAN\r\n"), + ] { + if bit { + uprint!(tx, "{}", name); + } + } + if vw.p2e() != 0 { + uprint!(tx, " - P2E group set = {:02X}\r\n", vw.p2e()); + } + if vw.host_c10n() { + uprint!(tx, " - HOST_C10N\r\n"); + } + } + Event::GpioWire(gpio) => { + // Only report actual changes, like the SDK example. + if gpio != *last_gpio { + if gpio.valid != 0 { + uprint!( + tx, + "\r\nVirtual wire change: index {}, pin {}\r\n", + gpio.index, + gpio.level + ); + } + *last_gpio = gpio; + } + } + Event::IrqPushDone => uprint!(tx, "IRQ update completed\r\n"), + Event::Port80(p80) => { + uprint!( + tx, + "P80 Code 0x{:02X} (prev: 0x{:02X}, count: {})\r\n", + p80.current, + p80.previous, + p80.counter + ); + } + Event::Port(pe) => handle_port_event(espi, tx, flash, queue, pe).await, + } +} + +async fn handle_port_event( + espi: &mut Espi<'_>, + tx: &mut LpuartTx<'_, Buffered>, + flash: &mut [u8; FLASH_SIZE], + queue: &mut ReadQueue, + pe: PortEvent, +) { + if let Some(error) = pe.error { + // Numeric codes and explanations match the SDK's espi_port_error_t. + let (code, explain) = match error { + PortError::EndpointWriteOverrun => (1, Some(" - Endpoint Write Overrun: Host wrote when WRDY=1.\r\n")), + PortError::EndpointReadEmpty => (2, None), + PortError::EndpointInvalidSize => (3, Some(" - Endpoint Invalid Size: Transfer size > 1 byte.\r\n")), + PortError::MailboxInvalidAccess => ( + 10, + Some(" - Mailbox Invalid Access: Invalid host read/write access.\r\n"), + ), + PortError::MailboxOverrunUnderrun => ( + 11, + Some(" - Mailbox Overrun/Underrun: Write overrun or read underrun.\r\n"), + ), + PortError::MailboxSizeOverflow => ( + 12, + Some(" - Mailbox Size Overflow: Request size exceeds mailbox boundary.\r\n"), + ), + PortError::MailboxRamBusError => (13, Some(" - Mailbox RAM/Bus Error: AHB/RAM access error.\r\n")), + PortError::MasterFromHostFailed => (20, None), + PortError::MasterOverrunUnderrun => (21, None), + PortError::MasterEraseFailed => (22, Some(" - Flash erase failed.\r\n")), + PortError::MasterBusError => (23, None), + }; + uprint!(tx, "Port {} error code {}\r\n", pe.port, code); + if let Some(explain) = explain { + uprint!(tx, "{}", explain); + } + } + + if Some(pe.port) == espi.oob_port() { + if pe.read { + uprint!(tx, "OOB sent over.\r\n"); + } + if pe.write { + let mut buf = [0u8; 256]; + if let Ok(len) = espi.read_oob(&mut buf) { + uprint!(tx, "OOB received ({} bytes): ", len); + for byte in &buf[..len] { + uprint!(tx, "{:02X} ", byte); + } + uprint!(tx, "\r\n"); + } + } + return; + } + + if Some(pe.port) == espi.saf_port() { + if let Some(req) = espi.flash_request(&pe) { + handle_flash_request(espi, tx, flash, queue, req).await; + } + return; + } + + match pe.port { + // Port 1: ACPI endpoint. + 1 => { + if pe.write { + let (idx, data) = espi.endpoint_data(pe.port); + uprint!( + tx, + "Received endpoint message: idx = 0x{:X}, datain = 0x{:X}\r\n", + idx, + data + ); + if idx == 0 { + espi.write_endpoint_data(pe.port, data); + uprint!(tx, "Endpoint data ready: 0x{:X}\r\n", data); + } + } + if pe.read { + let (idx, _) = espi.endpoint_data(pe.port); + uprint!(tx, "Endpoint data read: idx = 0x{:X}\r\n", idx); + } + } + // Port 2: ACPI index/data. + 2 => { + if pe.spec0 { + let (idx, data) = espi.endpoint_data(pe.port); + uprint!( + tx, + "Received Index-data message: idx = 0x{:X}, datain = 0x{:X}\r\n", + idx, + data + ); + } + if pe.read { + let (idx, _) = espi.endpoint_data(pe.port); + espi.write_endpoint_data(pe.port, 0xBB); + uprint!( + tx, + "Index-data sent back: idx = 0x{:X}, dataout =0x{:X}\r\n", + idx, + 0xBBu32 + ); + } + } + // Other ports: mailbox handling. + _ => { + if pe.write || pe.spec0 { + let mut buf = [0u8; 256]; + let len = espi.read_mailbox(pe.port, &mut buf); + uprint!(tx, "Mailbox received ({} bytes): ", len); + for byte in &buf[..len] { + uprint!(tx, "{:02X} ", byte); + } + uprint!(tx, "\r\n"); + espi.set_mailbox_status(pe.port, MailboxStatus::WrEmpty); + } + if pe.read { + if pe.spec1 { + uprint!(tx, "Mailbox read started.\r\n"); + } + if pe.spec3 { + uprint!(tx, "Mailbox read done.\r\n"); + } + } + } + } +} + +// ========================================================================= +// SAF virtual flash (port of ExampleFlashOps) +// ========================================================================= + +async fn handle_flash_request( + espi: &mut Espi<'_>, + tx: &mut LpuartTx<'_, Buffered>, + flash: &mut [u8; FLASH_SIZE], + queue: &mut ReadQueue, + req: FlashRequest, +) { + match req.kind { + FlashRequestKind::Erase | FlashRequestKind::Write => { + let addr = req.addr as usize; + let mut length = req.length as usize; + if addr >= FLASH_SIZE { + espi.set_flash_op_len(OMFLEN_SAF_COMPLETION_FAIL, 0); + espi.set_flash_completion(req.tag, SSTCL_SAF_COMPLETION, SafCompletionType::Only); + return; + } + length = length.min(FLASH_SIZE - addr); + espi.set_flash_completion(req.tag, SSTCL_SAF_REQ_ACCEPTED, SafCompletionType::Middle); + + if req.kind == FlashRequestKind::Erase { + uprint!(tx, "[SAF] Erase addr=0x{:08X}, len={}\r\n", req.addr, length); + flash[addr..addr + length].fill(0xFF); + espi.set_flash_op_len(OMFLEN_SAF_COMPLETION_NO_DATA, length as u32); + espi.set_flash_completion(req.tag, SSTCL_SAF_COMPLETION, SafCompletionType::Middle); + } else { + let window = espi.flash_window(); + // Write payload starts after the 4 address bytes. + flash[addr..addr + length].copy_from_slice(&window[4..4 + length]); + espi.set_flash_op_len(OMFLEN_SAF_COMPLETION_NO_DATA, length as u32); + espi.set_flash_completion(req.tag, SSTCL_SAF_COMPLETION, SafCompletionType::Middle); + + uprint!( + tx, + "[SAF] Write addr=0x{:08X}, len={}, first bytes(Hex)=", + req.addr, + length + ); + for byte in flash[addr..addr + length].iter().take(8) { + uprint!(tx, "{:02X} ", byte); + } + uprint!(tx, "\r\n"); + } + } + FlashRequestKind::Read => { + // Promira WAIT_STATE workaround: skip the spurious length-0 pull. + if req.length == 0 { + return; + } + let max_payload = espi.flash_max_payload(); + + if req.read_start { + let addr = req.addr as usize; + if addr >= FLASH_SIZE { + espi.set_flash_op_len(OMFLEN_SAF_COMPLETION_FAIL, 0); + espi.set_flash_completion(req.tag, SSTCL_SAF_COMPLETION, SafCompletionType::Only); + return; + } + let length = (req.length as usize).min(FLASH_SIZE - addr) as u32; + + espi.set_flash_completion(req.tag, SSTCL_SAF_REQ_ACCEPTED, SafCompletionType::Middle); + + // Split the read into payload-sized completions. + queue.tag = req.tag; + queue.cur = 0; + queue.tot = 0; + let mut remaining = length; + let mut offset = 0u32; + while remaining > 0 && queue.tot < SAF_RX_SPLIT_MAX { + let trans_len = remaining.min(max_payload); + let rx_type = if length <= max_payload { + SafCompletionType::Only + } else if remaining == length { + SafCompletionType::First + } else if remaining == trans_len { + SafCompletionType::Last + } else { + SafCompletionType::Middle + }; + queue.item[queue.tot] = (req.addr + offset, trans_len, rx_type); + queue.tot += 1; + offset += trans_len; + remaining -= trans_len; + } + } + + if queue.cur < queue.tot { + let (addr, len, rx_type) = queue.item[queue.cur]; + let window = espi.flash_window(); + window[..len as usize].copy_from_slice(&flash[addr as usize..(addr + len) as usize]); + espi.set_flash_op_len(OMFLEN_SAF_COMPLETION_WITH_DATA, len); + espi.set_flash_completion(queue.tag, SSTCL_SAF_COMPLETION, rx_type); + queue.cur += 1; + } + + // Like the SDK example, report the request's own address/length + // (not the per-chunk values), with the window's first bytes. + uprint!( + tx, + "[SAF] Read addr=0x{:08X}, len={}, first bytes(Hex)=", + req.addr, + req.length + ); + let window = espi.flash_window(); + for byte in window.iter().take((req.length as usize).min(8)) { + uprint!(tx, "{:02X} ", byte); + } + uprint!(tx, "\r\n"); + } + } +} + +// ========================================================================= +// Console commands (port of the SDK example's interactive CLI) +// ========================================================================= + +const VW_FLAG_NAMES: [(&str, VWireOut); 12] = [ + ("dswpwrokrst", VWireOut::DswPwrokRst), + ("booterrn", VWireOut::BootErrn), + ("bootdone", VWireOut::BootDone), + ("e2p", VWireOut::E2p), + ("susackn", VWireOut::SusAckN), + ("hostrstack", VWireOut::HostRstAck), + ("rcinn", VWireOut::Rcinn), + ("smin", VWireOut::Smin), + ("scin", VWireOut::Scin), + ("pmen", VWireOut::Pmen), + ("wakenscin", VWireOut::WakenScin), + ("oobrstack", VWireOut::OobRstAck), +]; + +async fn print_help(tx: &mut LpuartTx<'_, Buffered>) { + uprint!(tx, "\r\nInteractive commands:\r\n"); + uprint!(tx, " show_config -- Show eSPI configuration\r\n"); + uprint!(tx, " status -- Show eSPI status flags\r\n"); + uprint!(tx, " espi_cap -- Show eSPI capabilities\r\n"); + uprint!(tx, " espi_cfg -- Show eSPI configurations\r\n"); + uprint!(tx, " wirero -- Show vwire read\r\n"); + uprint!(tx, " wirewo -- Show vwire write\r\n"); + uprint!(tx, " send_vw_mask -- Apply VW by mask (32-bit hex)\r\n"); + uprint!( + tx, + " send_vw_flag -- Set VW flag by name (val may be multi-bit)\r\n" + ); + uprint!(tx, " vw_flags -- List available VW flag names\r\n"); + uprint!( + tx, + " send_oob -- Send OOB payload (hex, e.g. AA55 or 0xAA 0x55)\r\n" + ); + uprint!(tx, " push_irq -- Push IRQ (0-255) to host\r\n"); + uprint!(tx, " reset_p80 -- Reset Port 80 counter\r\n"); + uprint!(tx, " help -- Help\r\n\r\n"); +} + +async fn print_vw_flag_list(tx: &mut LpuartTx<'_, Buffered>) { + uprint!(tx, "Available VWire flags (send_vw_flag ):\r\n"); + for (name, wire) in &VW_FLAG_NAMES { + let multi = *wire == VWireOut::E2p; + uprint!( + tx, + " {:<12} (bits={}){}\r\n", + name, + if multi { 8 } else { 1 }, + if multi { " [multi-bit]" } else { "" } + ); + } +} + +async fn print_espi_config(espi: &Espi<'_>, tx: &mut LpuartTx<'_, Buffered>) { + let raw = espi.host_config(); + // Decode through the PAC's typed ESPICFG accessors instead of magic shifts. + let cfg = hal::pac::espi::ESPICFG(raw); + uprint!(tx, "\n--- Current eSPI Configuration (0x{:08X}) ---\r\n", raw); + + uprint!( + tx, + " IO Mode: {}\r\n", + match cfg.SPIMOD().to_bits() { + 0 => "Single SPI", + 1 => "Dual SPI", + 2 => "Quad SPI", + _ => "Reserved", + } + ); + + uprint!( + tx, + " SPI Speed: {}\r\n", + match cfg.SPISPD().to_bits() { + 0 => "<=20 MHz", + 1 => "<=25 MHz", + 2 => "<=33 MHz", + 3 => "<=50 MHz", + 4 => "<=66 MHz", + _ => "Reserved", + } + ); + + uprint!( + tx, + " CRC Checking: {}\r\n", + if cfg.CRC() { "Enabled" } else { "Disabled" } + ); + uprint!( + tx, + " Alert Pin: {}\r\n", + if cfg.ALERT() { "Dedicated Pin" } else { "MISO" } + ); + if cfg.ALERT() { + uprint!( + tx, + " Alert Type: {}\r\n", + if cfg.ALERTOD() { "Open Drain" } else { "Push-Pull" } + ); + } + + uprint!(tx, " Channels:\r\n"); + uprint!( + tx, + " Ch0 (Memory): {}\r\n", + if cfg.MEMENA() { "Enabled" } else { "Disabled" } + ); + uprint!( + tx, + " Ch1 (VWire): {}\r\n", + if cfg.VWOK() { "Enabled" } else { "Disabled" } + ); + uprint!( + tx, + " Ch2 (OOB): {}\r\n", + if cfg.OOBOK() { "Enabled" } else { "Disabled" } + ); + uprint!( + tx, + " Ch3 (Flash): {}\r\n", + if cfg.FLSHOK() { "Enabled" } else { "Disabled" } + ); + + uprint!(tx, " Max Payload Sizes:\r\n"); + if cfg.MEMENA() { + uprint!(tx, " Memory: {} bytes\r\n", 64u32 << cfg.MEMSZ().to_bits()); + } + if cfg.OOBOK() { + uprint!(tx, " OOB: {} bytes\r\n", 64u32 << cfg.OOBSZ().to_bits()); + } + if cfg.FLSHOK() { + uprint!(tx, " Flash: {} bytes\r\n", 64u32 << cfg.FLASHSZ().to_bits()); + uprint!( + tx, + " Flash Erase: {}\r\n", + match cfg.FLSHERA().to_bits() { + 0 => "Disabled", + 1 => "4 KB", + 2 => "64 KB", + 3 => "4 KB & 64 KB", + 4 => "128 KB", + 5 => "256 KB", + _ => "Reserved", + } + ); + } + + uprint!( + tx, + " SAF Support: {}\r\n", + if cfg.SAF().to_bits() != 0 { "Yes" } else { "No" } + ); + uprint!( + tx, + " Bus Master: {}\r\n", + if cfg.BUSMOK() { "Enabled" } else { "Disabled" } + ); +} + +fn parse_u32(s: &str) -> Option { + let s = s.trim(); + if let Some(hex) = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X")) { + u32::from_str_radix(hex, 16).ok() + } else { + s.parse().ok() + } +} + +/// Parse hex bytes ("AA55", "AA 55", "0xAA 0x55") into `out`. +fn parse_hex_bytes(s: &str, out: &mut [u8]) -> Option { + let mut len = 0; + for token in s.split_whitespace() { + let mut token = token + .strip_prefix("0x") + .or_else(|| token.strip_prefix("0X")) + .unwrap_or(token); + while !token.is_empty() { + if token.len() < 2 || len >= out.len() { + return None; + } + out[len] = u8::from_str_radix(&token[..2], 16).ok()?; + len += 1; + token = &token[2..]; + } + } + Some(len) +} + +async fn handle_command(espi: &mut Espi<'_>, tx: &mut LpuartTx<'_, Buffered>, input: &str) { + let mut parts = input.trim().splitn(2, ' '); + let cmd = parts.next().unwrap_or(""); + let args = parts.next().unwrap_or("").trim(); + + match cmd { + "help" | "h" => print_help(tx).await, + "show_config" => print_espi_config(espi, tx).await, + "status" => uprint!(tx, "eSPI status: 0x{:08X}\r\n", espi.status()), + "espi_cap" => uprint!(tx, "eSPI capabilities: 0x{:08X}\r\n", espi.capabilities()), + "espi_cfg" => uprint!(tx, "eSPI configuration: 0x{:08X}\r\n", espi.host_config()), + "wirero" => uprint!(tx, "vwire read: 0x{:08X}\r\n", espi.vwires().0), + "wirewo" => uprint!(tx, "vwire write: 0x{:08X}\r\n", espi.vwires_out_raw()), + "send_vw_mask" => match parse_u32(args) { + Some(mask) => { + espi.send_vwire_mask(mask); + uprint!(tx, "VW sends as mask 0x{:08X}\r\n", mask); + } + None => uprint!(tx, "Missing mask\r\n"), + }, + "send_vw_flag" => { + let mut argv = args.split_whitespace(); + let (name, val) = (argv.next(), argv.next().and_then(parse_u32)); + match (name, val) { + (Some(name), Some(val)) => { + // Accept full SDK constant names too, like the C example. + let short = name.strip_prefix("kESPI_VWireWr_").unwrap_or(name); + let lookup = VW_FLAG_NAMES + .iter() + .find(|(flag_name, _)| flag_name.eq_ignore_ascii_case(short)); + match lookup { + Some((_, wire)) => { + // 0 = kStatus_Success, 3 = kStatus_Busy, as the C + // example prints the raw status_t value. + let result = espi.send_vwire(*wire, val as u8); + uprint!( + tx, + "\r\nESPI_SendVWire({}, val={}) -> {}\r\n", + name, + val, + if result.is_ok() { 0 } else { 3 } + ); + } + None => { + uprint!(tx, "Unknown VW flag name: {}\r\n", name); + print_vw_flag_list(tx).await; + } + } + } + _ => { + uprint!(tx, "Usage: send_vw_flag \r\n"); + print_vw_flag_list(tx).await; + } + } + } + "vw_flags" => print_vw_flag_list(tx).await, + "send_oob" => { + let max_oob = espi.oob_port().map(|p| espi.mailbox_size(p)).unwrap_or(0).min(256); + let mut buf = [0u8; 256]; + match parse_hex_bytes(args, &mut buf) { + Some(0) | None if args.is_empty() => uprint!(tx, "No OOB data provided\r\n"), + Some(0) => uprint!(tx, "No OOB data parsed\r\n"), + Some(len) if len > max_oob => uprint!(tx, "OOB data too long (max {} bytes)\r\n", max_oob), + Some(len) => { + let result = espi.send_oob(&buf[..len], true); + uprint!( + tx, + "ESPI_SendOOB -> {} (len={})\r\n", + if result.is_ok() { 0 } else { -1 }, + len + ); + } + None => uprint!(tx, "Invalid hex input (use two chars per byte, e.g. AA BB)\r\n"), + } + } + "push_irq" => match parse_u32(args) { + Some(irq) if irq <= 255 => espi.push_irq(irq as u8), + Some(irq) => uprint!(tx, "Invalid IRQ number: {} (must be 0-255)\r\n", irq), + None => uprint!(tx, "Missing IRQ number (0-255)\r\nUsage: push_irq \r\n"), + }, + "reset_p80" => { + espi.reset_port80_counter(); + uprint!(tx, "Port 80 counter reset.\r\n"); + } + _ => uprint!(tx, "Unknown command. Type 'help' for help.\r\n"), + } +} diff --git a/examples/mcxa5xx/src/bin/usb_hid_mouse.rs b/examples/mcxa5xx/src/bin/usb_hid_mouse.rs new file mode 100644 index 0000000000..a2e7a6a6b7 --- /dev/null +++ b/examples/mcxa5xx/src/bin/usb_hid_mouse.rs @@ -0,0 +1,179 @@ +//! USB HID mouse example for the FRDM-MCXA577 (MCX-A577). +//! +//! Enumerates the board as a USB full-speed HID mouse and slowly moves the +//! pointer back and forth. Connect the board's USB device port to a host. +//! +//! Build/run: +//! ```text +//! cargo run --release --bin usb_hid_mouse +//! ``` + +#![no_std] +#![no_main] + +use defmt::{info, panic, warn}; +use embassy_executor::Spawner; +use embassy_futures::join::join; +use embassy_mcxa::bind_interrupts; +use embassy_mcxa::clocks::config::{SoscConfig, SoscMode}; +use embassy_mcxa::clocks::{PoweredClock, VddLevel}; +use embassy_mcxa::usb::{Config as UsbDriverConfig, Driver, InterruptHandler, PhyConfig}; +use embassy_time::Timer; +use embassy_usb::class::hid::{HidBootProtocol, HidSubclass, HidWriter, ReportId, RequestHandler, State}; +use embassy_usb::control::OutResponse; +use embassy_usb::{Builder, Config, Handler}; +use usbd_hid::descriptor::{MouseReport, SerializedDescriptor}; +use {defmt_rtt as _, embassy_mcxa as hal, panic_probe as _}; + +bind_interrupts!(struct Irqs { + USB1_HS => InterruptHandler; +}); + +#[embassy_executor::main] +async fn main(_spawner: Spawner) { + let mut hal_config = hal::config::Config::default(); + // Match the SDK/Zephyr board profile; the USBHS PHY PLL needs this voltage + // mode to lock reliably on FRDM-MCXA577. + hal_config.clock_cfg.vdd_power.active_mode.level = VddLevel::OverDriveMode; + hal_config.clock_cfg.vdd_power.low_power_mode.level = VddLevel::OverDriveMode; + hal_config.clock_cfg.sosc = Some(SoscConfig { + mode: SoscMode::CrystalOscillator, + frequency: 24_000_000, + power: PoweredClock::NormalEnabledDeepSleepDisabled, + }); + let p = hal::init(hal_config); + info!("=== USB HID mouse example ==="); + + // Create the USB device driver (full speed). + info!("Creating USB driver"); + let mut driver_config = UsbDriverConfig::default(); + driver_config.phy = PhyConfig::frdm_mcxa577(); + let driver = match Driver::new(p.USB1, Irqs, driver_config) { + Ok(driver) => driver, + Err(e) => panic!("USB init failed: {:?}", e), + }; + info!("USB driver created"); + + // Configure the USB device descriptors. + let mut config = Config::new(0xc0de, 0xcafe); + config.manufacturer = Some("Embassy"); + config.product = Some("MCXA577 HID mouse"); + config.serial_number = Some("12345678"); + config.max_power = 100; + config.max_packet_size_0 = 64; + + // Buffers for the USB device stack. + let mut config_descriptor = [0; 256]; + let mut bos_descriptor = [0; 256]; + let mut msos_descriptor = [0; 256]; + let mut control_buf = [0; 64]; + let mut request_handler = MyRequestHandler {}; + let mut device_handler = MyDeviceHandler::new(); + + let mut state = State::new(); + + let mut builder = Builder::new( + driver, + config, + &mut config_descriptor, + &mut bos_descriptor, + &mut msos_descriptor, + &mut control_buf, + ); + builder.handler(&mut device_handler); + + // Create the HID mouse class. + let config = embassy_usb::class::hid::Config { + report_descriptor: MouseReport::desc(), + request_handler: Some(&mut request_handler), + poll_ms: 60, + max_packet_size: 8, + hid_subclass: HidSubclass::No, + hid_boot_protocol: HidBootProtocol::None, + }; + let mut writer = HidWriter::<_, 8>::new(&mut builder, &mut state, config); + + // Build the device. + let mut usb = builder.build(); + let usb_fut = usb.run(); + + // Move the mouse pointer back and forth. + let hid_fut = async { + let mut dir: i8 = 5; + loop { + Timer::after_millis(200).await; + + let report = MouseReport { + buttons: 0, + x: dir, + y: 0, + wheel: 0, + pan: 0, + }; + match writer.write_serialize(&report).await { + Ok(()) => {} + Err(e) => warn!("Failed to send report: {:?}", e), + } + dir = -dir; + } + }; + + join(usb_fut, hid_fut).await; +} + +struct MyRequestHandler {} + +impl RequestHandler for MyRequestHandler { + fn get_report(&mut self, id: ReportId, _buf: &mut [u8]) -> Option { + info!("Get report for {:?}", id); + None + } + + fn set_report(&mut self, id: ReportId, data: &[u8]) -> OutResponse { + info!("Set report for {:?}: {=[u8]}", id, data); + OutResponse::Accepted + } + + fn set_idle_ms(&mut self, id: Option, dur: u32) { + info!("Set idle rate for {:?} to {:?}", id, dur); + } + + fn get_idle_ms(&mut self, id: Option) -> Option { + info!("Get idle rate for {:?}", id); + None + } +} + +struct MyDeviceHandler {} + +impl MyDeviceHandler { + fn new() -> Self { + MyDeviceHandler {} + } +} + +impl Handler for MyDeviceHandler { + fn enabled(&mut self, enabled: bool) { + if enabled { + info!("Device enabled"); + } else { + info!("Device disabled"); + } + } + + fn reset(&mut self) { + info!("Bus reset"); + } + + fn addressed(&mut self, addr: u8) { + info!("USB address set to: {}", addr); + } + + fn configured(&mut self, configured: bool) { + if configured { + info!("Device configured, it may now draw up to the configured current from Vbus.") + } else { + info!("Device is no longer configured, the Vbus current limit is 100mA."); + } + } +} diff --git a/examples/mcxa5xx/src/bin/usb_serial.rs b/examples/mcxa5xx/src/bin/usb_serial.rs new file mode 100644 index 0000000000..d7f5fe2d49 --- /dev/null +++ b/examples/mcxa5xx/src/bin/usb_serial.rs @@ -0,0 +1,110 @@ +//! USB CDC-ACM serial echo example for the FRDM-MCXA577 (MCX-A577). +//! +//! Enumerates the board as a full-speed USB serial device and echoes every +//! packet received from the host. Connect the board's USB device port to a host. +//! +//! Build/run: +//! ```text +//! cargo run --release --bin usb_serial +//! ``` + +#![no_std] +#![no_main] + +use defmt::{info, panic}; +use embassy_executor::Spawner; +use embassy_futures::join::join; +use embassy_mcxa::bind_interrupts; +use embassy_mcxa::clocks::config::{SoscConfig, SoscMode}; +use embassy_mcxa::clocks::{PoweredClock, VddLevel}; +use embassy_mcxa::usb::{Config as UsbDriverConfig, Driver, InterruptHandler, PhyConfig}; +use embassy_usb::Builder; +use embassy_usb::class::cdc_acm::{CdcAcmClass, State}; +use embassy_usb::driver::EndpointError; +use {defmt_rtt as _, embassy_mcxa as hal, panic_probe as _}; + +bind_interrupts!(struct Irqs { + USB1_HS => InterruptHandler; +}); + +#[embassy_executor::main] +async fn main(_spawner: Spawner) { + let mut hal_config = hal::config::Config::default(); + // Match the SDK/Zephyr board profile; the USBHS PHY PLL needs this voltage + // mode to lock reliably on FRDM-MCXA577. + hal_config.clock_cfg.vdd_power.active_mode.level = VddLevel::OverDriveMode; + hal_config.clock_cfg.vdd_power.low_power_mode.level = VddLevel::OverDriveMode; + hal_config.clock_cfg.sosc = Some(SoscConfig { + mode: SoscMode::CrystalOscillator, + frequency: 24_000_000, + power: PoweredClock::NormalEnabledDeepSleepDisabled, + }); + let p = hal::init(hal_config); + info!("=== USB CDC-ACM serial example ==="); + + let mut driver_config = UsbDriverConfig::default(); + driver_config.phy = PhyConfig::frdm_mcxa577(); + let driver = match Driver::new(p.USB1, Irqs, driver_config) { + Ok(driver) => driver, + Err(e) => panic!("USB init failed: {:?}", e), + }; + + let mut config = embassy_usb::Config::new(0xc0de, 0xcafd); + config.manufacturer = Some("Embassy"); + config.product = Some("MCXA577 USB serial"); + config.serial_number = Some("12345678"); + config.max_power = 100; + config.max_packet_size_0 = 64; + + let mut config_descriptor = [0; 256]; + let mut bos_descriptor = [0; 256]; + let mut msos_descriptor = [0; 256]; + let mut control_buf = [0; 64]; + + let mut state = State::new(); + + let mut builder = Builder::new( + driver, + config, + &mut config_descriptor, + &mut bos_descriptor, + &mut msos_descriptor, + &mut control_buf, + ); + + let mut class = CdcAcmClass::new(&mut builder, &mut state, 64); + let mut usb = builder.build(); + let usb_fut = usb.run(); + + let echo_fut = async { + loop { + class.wait_connection().await; + info!("serial connected"); + let _ = echo(&mut class).await; + info!("serial disconnected"); + } + }; + + join(usb_fut, echo_fut).await; +} + +struct Disconnected; + +impl From for Disconnected { + fn from(value: EndpointError) -> Self { + match value { + EndpointError::BufferOverflow => panic!("USB serial buffer overflow"), + EndpointError::Disabled => Disconnected, + } + } +} + +async fn echo<'d>(class: &mut CdcAcmClass<'d, Driver<'d>>) -> Result<(), Disconnected> { + let mut buf = [0; 64]; + loop { + let n = class.read_packet(&mut buf).await?; + let data = &buf[..n]; + info!("serial rx: {:x}", data); + class.write_packet(data).await?; + } +}