diff --git a/Cargo.lock b/Cargo.lock index 1aea581cbc..247a002544 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -670,6 +670,7 @@ dependencies = [ "axklib", "bcm2835-sdhci", "crab-usb", + "cv181x-sdhci", "dma-api", "dwmmc-host", "eth-intel", @@ -709,7 +710,6 @@ dependencies = [ "sdhci-host", "sdio-host-cv1800", "sdmmc-protocol", - "sg200x-bsp", "simple-ahci", "some-serial", "spin", @@ -2620,6 +2620,18 @@ dependencies = [ "xi-unicode", ] +[[package]] +name = "cv181x-sdhci" +version = "0.1.0" +dependencies = [ + "dma-api", + "mmio-api", + "rdif-block", + "sdhci-host", + "sdio-host2", + "sdmmc-protocol", +] + [[package]] name = "darling" version = "0.13.4" diff --git a/Cargo.toml b/Cargo.toml index 1be5ebad47..2700cf0fa2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -261,6 +261,7 @@ usb-if = { version = "0.7.2", path = "drivers/usb/usb-if" } usb-serial = { version = "0.1.0", path = "drivers/usb/usb-serial" } dwmmc-host = { version = "0.3.0", path = "drivers/blk/dwmmc-host", default-features = false } phytium-mci-host = { version = "0.3.0", path = "drivers/blk/phytium-mci-host", default-features = false } +cv181x-sdhci = { version = "0.1.0", path = "drivers/blk/cv181x-sdhci", default-features = false } sdhci-host = { version = "0.3.0", path = "drivers/blk/sdhci-host", default-features = false } sdmmc-protocol = { version = "0.3.0", path = "drivers/blk/sdmmc-protocol", default-features = false } smoltcp = { version = "0.13.1", default-features = false } diff --git a/drivers/ax-driver/Cargo.toml b/drivers/ax-driver/Cargo.toml index c477180956..9aa33c5b96 100644 --- a/drivers/ax-driver/Cargo.toml +++ b/drivers/ax-driver/Cargo.toml @@ -36,7 +36,12 @@ ramdisk = ["block", "dep:ramdisk"] bcm2835-sdhci = ["block", "dep:bcm2835-sdhci"] ahci = ["block", "pci", "dep:simple-ahci"] nvme = ["block", "pci", "dep:nvme-driver"] -cvsd = ["block", "dep:sg200x-bsp"] +cvsd = [ + "block", + "dep:cv181x-sdhci", + "dep:sdmmc-protocol", + "sdmmc-protocol/rdif", +] fxmac = ["net", "dep:ax-alloc", "dep:ax-crate-interface", "dep:fxmac_rs"] intel-net = ["net", "pci", "dep:eth-intel"] realtek-rtl8125 = ["net", "pci", "dep:realtek-rtl8125"] @@ -130,6 +135,7 @@ ax-kspin = { workspace = true, optional = true } axklib.workspace = true bcm2835-sdhci = { workspace = true, optional = true } crab-usb = { workspace = true, optional = true } +cv181x-sdhci = { workspace = true, optional = true } dma-api.workspace = true dwmmc-host = { workspace = true, optional = true } eth-intel = { workspace = true, optional = true } @@ -168,7 +174,6 @@ sdhci-host = { workspace = true, optional = true } sdhci-cv1800 = { workspace = true, optional = true } sdio-host = { workspace = true, optional = true } sdmmc-protocol = { workspace = true, default-features = false, optional = true } -sg200x-bsp = { workspace = true, optional = true } simple-ahci = { workspace = true, optional = true } some-serial = { workspace = true, optional = true } spin.workspace = true diff --git a/drivers/ax-driver/build.rs b/drivers/ax-driver/build.rs index 13e5e5b118..7fbb2028c9 100644 --- a/drivers/ax-driver/build.rs +++ b/drivers/ax-driver/build.rs @@ -25,13 +25,10 @@ fn enable_cfg_flag(key: &str) { fn main() { let has_virtio_core = has_feature("virtio-core"); let has_virtio_dev = has_any_feature(VIRTIO_DEV_FEATURES); - let target_arch = std::env::var("CARGO_CFG_TARGET_ARCH").unwrap_or_default(); - let target_has_cvsd = matches!(target_arch.as_str(), "riscv32" | "riscv64"); - if has_virtio_core || has_virtio_dev { enable_cfg_flag("virtio_dev"); } - if has_any_feature(&["ahci", "bcm2835-sdhci"]) || (has_feature("cvsd") && target_has_cvsd) { + if has_any_feature(&["ahci", "bcm2835-sdhci"]) { enable_cfg_flag("sync_block_dev"); } diff --git a/drivers/ax-driver/src/block/cvsd.rs b/drivers/ax-driver/src/block/cvsd.rs index 3f706be8e2..19488fa105 100644 --- a/drivers/ax-driver/src/block/cvsd.rs +++ b/drivers/ax-driver/src/block/cvsd.rs @@ -1,14 +1,51 @@ -use rdrive::{PlatformDevice, probe::OnProbeError, register::ProbeFdt}; -#[cfg(any(target_arch = "riscv32", target_arch = "riscv64"))] -use {alloc::format, sg200x_bsp::sdmmc::Sdmmc}; +#[cfg(not(test))] +use alloc::format; +#[cfg(not(test))] +use core::time::Duration; -#[cfg(any(target_arch = "riscv32", target_arch = "riscv64"))] -use super::{SyncBlockOps, register_sync_block}; +use cv181x_sdhci::rdif as cv181x_rdif; +#[cfg(not(test))] +use cv181x_sdhci::{ + CV181X_SYSCON_REQUIRED_SIZE, CV181X_TOP_SYSCON_BASE, Cv181xConfig, Cv181xMmio, Cv181xSdhci, +}; +#[cfg(not(test))] +use log::{info, warn}; +#[cfg(not(test))] +use rdrive::{ + probe::OnProbeError, + register::{FdtInfo, ProbeFdt}, +}; +use sdmmc_protocol::{Error, error::Phase}; +#[cfg(not(test))] +use sdmmc_protocol::{ + OperationPoll, + sdio::{BusWidth, CardInfo, CardInitPreference, SdioHost2Adapter, SdioInitScratch, SdioSdmmc}, +}; + +#[cfg(not(test))] +use crate::{block::ProbeFdtBlock, mmio::iomap}; -#[cfg(any(target_arch = "riscv32", target_arch = "riscv64"))] -const BLOCK_SIZE: usize = 512; pub const DEVICE_NAME: &str = "cvsd"; +#[cfg(not(test))] +const DEFAULT_SDMMIF_SIZE: usize = 0x1000; +#[cfg(not(test))] +const DEFAULT_SYSCON_SIZE: usize = 0x8000; +const CVSD_IRQ_DRIVEN: bool = true; + +#[cfg(not(test))] +type CvsdCard = SdioSdmmc>; + +#[cfg(not(test))] +#[derive(Clone, Copy)] +struct CvsdFdtPolicy { + no_sd: bool, + no_mmc: bool, + no_sdio: bool, + non_removable: bool, +} + +#[cfg(not(test))] crate::model_register!( name: "FDT CVSD", level: ProbeLevel::PostKernel, @@ -19,139 +56,250 @@ crate::model_register!( }], ); +#[cfg(not(test))] fn probe_fdt(probe: ProbeFdt<'_>) -> Result<(), OnProbeError> { - let (info, plat_dev) = probe.into_parts(); + let info = probe.info(); let sdmmc = info.node.regs().into_iter().next().ok_or_else(|| { OnProbeError::other(alloc::format!("[{}] has no reg", info.node.name())) })?; - let syscon = info - .find_compatible(&["syscon"]) - .into_iter() - .find_map(|node| node.regs().into_iter().next()) - .ok_or_else(|| OnProbeError::other("CVSD syscon node not found in FDT"))?; - - register_mmio( - plat_dev, + let (syscon_addr, syscon_size) = cv181x_syscon(info)?; + + let core = iomap( sdmmc.address as usize, - sdmmc.size.unwrap_or(0x1000) as usize, - syscon.address as usize, - syscon.size.unwrap_or(0x1000) as usize, - ) + sdmmc.size.unwrap_or(DEFAULT_SDMMIF_SIZE as u64) as usize, + )?; + let syscon = iomap(syscon_addr, syscon_size)?; + + let config = cv181x_config(info); + let policy = cvsd_fdt_policy(info); + info!( + "cvsd probe: node={}, src={}Hz min={}Hz max={}Hz bus_width={:?} no_1v8={} no_mmc={} \ + no_sdio={} cd_gpio={}", + info.node.name(), + config.src_frequency_hz, + config.min_frequency_hz, + config.max_frequency_hz, + config.max_bus_width, + config.no_1v8, + policy.no_mmc, + policy.no_sdio, + config.has_card_detect_gpio, + ); + + let host = unsafe { Cv181xSdhci::new(Cv181xMmio::new(core, syscon), config) }; + let mut card = SdioSdmmc::new_host2(host); + card.set_sd_uhs_selection_enabled(false); + + let card_info = poll_card_init(&mut card, card_init_preference(policy)).map_err(|err| { + warn!("cvsd: card init failed: {:?}", err); + card_init_error( + sdmmc.address, + sdmmc.size.unwrap_or(DEFAULT_SDMMIF_SIZE as u64), + err, + ) + })?; + info!( + "cvsd card: kind={:?} high_capacity={} rca={} ocr={:#010x} capacity_blocks={:?} cid={} \ + ext_csd={}", + card_info.kind, + card_info.high_capacity, + card_info.rca, + card_info.ocr, + card_info.capacity_blocks, + card_info.cid.is_some(), + card_info.ext_csd.is_some() + ); + + let dev = cv181x_rdif::device( + card, + cvsd_block_config(card_info.capacity_blocks.unwrap_or(0)), + ); + let irq = probe.register_block(dev)?; + info!("cvsd block device registered irq={:?}", irq); + Ok(()) } -fn register_mmio( - plat_dev: PlatformDevice, - sdmmc_paddr: usize, - sdmmc_size: usize, - syscon_paddr: usize, - syscon_size: usize, -) -> Result<(), OnProbeError> { - if sdmmc_paddr == 0 || sdmmc_size == 0 || syscon_paddr == 0 || syscon_size == 0 { - return Err(OnProbeError::NotMatch); +#[cfg(not(test))] +fn cv181x_syscon(info: &FdtInfo<'_>) -> Result<(usize, usize), OnProbeError> { + for node in info.find_compatible(&["syscon"]) { + let Some(reg) = node.regs().into_iter().next() else { + continue; + }; + if reg.address == CV181X_TOP_SYSCON_BASE { + return Ok((reg.address as usize, cv181x_syscon_map_size(reg.size)?)); + } } - register_mmio_target(plat_dev, sdmmc_paddr, sdmmc_size, syscon_paddr, syscon_size) -} -#[cfg(any(target_arch = "riscv32", target_arch = "riscv64"))] -fn register_mmio_target( - plat_dev: PlatformDevice, - sdmmc_paddr: usize, - sdmmc_size: usize, - syscon_paddr: usize, - syscon_size: usize, -) -> Result<(), OnProbeError> { - let sdmmc = map_region(sdmmc_paddr, sdmmc_size, "CVSD")?; - let syscon = map_region(syscon_paddr, syscon_size, "SYSCON")?; - let driver = - CvsdDriver::new(sdmmc, syscon).map_err(|_| OnProbeError::other("CVSD init failed"))?; - register_sync_block(plat_dev, driver); - Ok(()) + Err(OnProbeError::other(format!( + "CVSD TOP syscon at PA:0x{CV181X_TOP_SYSCON_BASE:x} not found in FDT" + ))) } -#[cfg(not(any(target_arch = "riscv32", target_arch = "riscv64")))] -fn register_mmio_target( - _plat_dev: PlatformDevice, - _sdmmc_paddr: usize, - _sdmmc_size: usize, - _syscon_paddr: usize, - _syscon_size: usize, -) -> Result<(), OnProbeError> { - Err(OnProbeError::NotMatch) +#[cfg(not(test))] +fn cv181x_syscon_map_size(size: Option) -> Result { + let map_size = size.unwrap_or(DEFAULT_SYSCON_SIZE as u64); + if map_size < CV181X_SYSCON_REQUIRED_SIZE as u64 { + return Err(OnProbeError::other(format!( + "CVSD TOP syscon reg size 0x{map_size:x} is smaller than required 0x{:x}", + CV181X_SYSCON_REQUIRED_SIZE + ))); + } + Ok(map_size as usize) } -#[cfg(any(target_arch = "riscv32", target_arch = "riscv64"))] -fn map_region(address: usize, size: usize, name: &str) -> Result { - let mmio = axklib::mmio::ioremap_raw(address.into(), size) - .map_err(|err| OnProbeError::other(format!("failed to map {name}: {err:?}")))?; - Ok(mmio.as_ptr() as usize) +#[cfg(not(test))] +fn cv181x_config(info: &FdtInfo<'_>) -> Cv181xConfig { + let node = info.node.as_node(); + Cv181xConfig { + src_frequency_hz: fdt_u32(info, "src-frequency", 375_000_000), + min_frequency_hz: fdt_u32(info, "min-frequency", 400_000), + max_frequency_hz: fdt_u32(info, "max-frequency", 25_000_000), + max_bus_width: cv181x_bus_width(info), + no_1v8: node.get_property("no-1-8-v").is_some(), + has_card_detect_gpio: node.get_property("cvi-cd-gpios").is_some() + || node.get_property("cd-gpios").is_some(), + touch_power_enable_pin: false, + } + .normalized() } -#[cfg(any(target_arch = "riscv32", target_arch = "riscv64"))] -struct CvsdDriver(Sdmmc); - -// The SG2002 SD/MMC core stores MMIO registers as `UnsafeCell`-backed -// references, so the raw register block is intentionally not `Sync`. -// `CvsdDriver` is owned by `SyncBlockDevice`, which serializes all access -// through a mutex and never clones the driver, so moving that owner between -// execution contexts is sound. -#[cfg(any(target_arch = "riscv32", target_arch = "riscv64"))] -unsafe impl Send for CvsdDriver {} - -#[cfg(any(target_arch = "riscv32", target_arch = "riscv64"))] -impl CvsdDriver { - fn new(sdmmc: usize, syscon: usize) -> Result { - let sdmmc = unsafe { Sdmmc::new(sdmmc, syscon) }; - sdmmc.init().map_err(|_| ())?; - sdmmc.clk_en(true); - Ok(Self(sdmmc)) +#[cfg(not(test))] +fn cvsd_fdt_policy(info: &FdtInfo<'_>) -> CvsdFdtPolicy { + let node = info.node.as_node(); + CvsdFdtPolicy { + no_sd: node.get_property("no-sd").is_some(), + no_mmc: node.get_property("no-mmc").is_some(), + no_sdio: node.get_property("no-sdio").is_some(), + non_removable: node.get_property("non-removable").is_some(), } +} - fn checked_lba(block_id: u64, offset: usize) -> Result { - let lba = block_id - .checked_add(offset as u64) - .ok_or(rdif_block::BlkError::InvalidBlockIndex(block_id))?; - u32::try_from(lba).map_err(|_| rdif_block::BlkError::InvalidBlockIndex(block_id)) +#[cfg(not(test))] +fn cv181x_bus_width(info: &FdtInfo<'_>) -> BusWidth { + match fdt_u32(info, "bus-width", 4) { + 1 => BusWidth::Bit1, + 4 => BusWidth::Bit4, + 8 => { + warn!("cvsd: 8-bit bus-width requested for 4-bit SD0 pads; clamping to 4-bit"); + BusWidth::Bit4 + } + other => { + warn!("cvsd: unsupported bus-width {other}; using 4-bit"); + BusWidth::Bit4 + } } } -#[cfg(any(target_arch = "riscv32", target_arch = "riscv64"))] -impl SyncBlockOps for CvsdDriver { - fn name(&self) -> &'static str { - DEVICE_NAME +#[cfg(not(test))] +fn fdt_u32(info: &FdtInfo<'_>, name: &str, default: u32) -> u32 { + info.node + .as_node() + .get_property(name) + .and_then(|prop| prop.get_u32()) + .unwrap_or(default) +} + +fn cvsd_block_config(capacity_blocks: u64) -> cv181x_rdif::BlockConfig { + cv181x_rdif::fifo_config(DEVICE_NAME, capacity_blocks, CVSD_IRQ_DRIVEN) +} + +#[cfg(not(test))] +fn poll_card_init(card: &mut CvsdCard, preference: CardInitPreference) -> Result { + let mut scratch = SdioInitScratch::new(); + let mut request = card.submit_init_with_preference(preference, &mut scratch)?; + loop { + match card.poll_init_request(&mut request)? { + OperationPoll::Pending => { + if request.take_needs_pace() { + axklib::time::busy_wait(Duration::from_millis(10)); + } else { + core::hint::spin_loop(); + } + } + OperationPoll::Complete(info) => return Ok(info), + _ => return Err(Error::UnsupportedCommand), + } } +} - fn num_blocks(&self) -> u64 { - self.0.card_capacity_blocks() +#[cfg(not(test))] +fn card_init_preference(policy: CvsdFdtPolicy) -> CardInitPreference { + if policy.no_sd || policy.non_removable { + if policy.no_mmc { + warn!("cvsd: FDT has both no-sd/non-removable and no-mmc; probing SD only"); + return CardInitPreference::SdOnly; + } + CardInitPreference::MmcFirst + } else if policy.no_mmc { + CardInitPreference::SdOnly + } else { + CardInitPreference::SdFirst } +} - fn block_size(&self) -> usize { - BLOCK_SIZE +#[cfg(not(test))] +fn init_error(address: u64, size: u64, err: Error) -> OnProbeError { + OnProbeError::other(format!( + "failed to initialize CVSD device at [PA:{:?}, SZ:0x{:x}): {err:?}", + address, size + )) +} + +#[cfg(not(test))] +fn card_init_error(address: u64, size: u64, err: Error) -> OnProbeError { + if is_absent_card_init_error(err) { + warn!( + "cvsd: no responsive card at [PA:{:?}, SZ:0x{:x}); skipping controller: {err:?}", + address, size + ); + return OnProbeError::NotMatch; } - fn read_blocks(&mut self, block_id: u64, buf: &mut [u8]) -> Result<(), rdif_block::BlkError> { - if !buf.len().is_multiple_of(BLOCK_SIZE) { - return Err(rdif_block::BlkError::NotSupported); - } + init_error(address, size, err) +} - for (i, block) in buf.chunks_exact_mut(BLOCK_SIZE).enumerate() { - self.0 - .read_block(Self::checked_lba(block_id, i)?, block) - .map_err(|_| rdif_block::BlkError::Other("CVSD read failed"))?; +fn is_absent_card_init_error(err: Error) -> bool { + match err { + Error::NoCard => true, + Error::Timeout(ctx) | Error::Crc(ctx) | Error::BadResponse(ctx) => { + ctx.cmd.is_some() + && matches!( + ctx.phase, + Phase::CommandSend | Phase::ResponseWait | Phase::Init + ) } - Ok(()) + _ => false, } +} - fn write_blocks(&mut self, block_id: u64, buf: &[u8]) -> Result<(), rdif_block::BlkError> { - if !buf.len().is_multiple_of(BLOCK_SIZE) { - return Err(rdif_block::BlkError::NotSupported); - } +#[cfg(test)] +mod tests { + use sdmmc_protocol::error::ErrorContext; - for (i, block) in buf.chunks_exact(BLOCK_SIZE).enumerate() { - self.0 - .write_block(Self::checked_lba(block_id, i)?, block) - .map_err(|_| rdif_block::BlkError::Other("CVSD write failed"))?; - } - Ok(()) + use super::*; + + #[test] + fn command_timeout_during_card_init_is_absent_card() { + let err = Error::Timeout(ErrorContext::for_cmd(Phase::ResponseWait, 1)); + + assert!(is_absent_card_init_error(err)); + } + + #[test] + fn data_timeout_after_card_init_is_not_absent_card() { + let err = Error::Timeout(ErrorContext::for_cmd(Phase::DataRead, 17)); + + assert!(!is_absent_card_init_error(err)); + } + + #[test] + fn cvsd_block_io_uses_irq_driven_sdmmc_rdif_fifo_config() { + let config = cvsd_block_config(8); + + assert_eq!(config.name, DEVICE_NAME); + assert_eq!(config.capacity_blocks, 8); + assert!(!config.uses_dma()); + assert!(config.irq_driven); } } diff --git a/drivers/blk/cv181x-sdhci/Cargo.toml b/drivers/blk/cv181x-sdhci/Cargo.toml new file mode 100644 index 0000000000..30527e7f8f --- /dev/null +++ b/drivers/blk/cv181x-sdhci/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "cv181x-sdhci" +version = "0.1.0" +repository.workspace = true +edition = "2024" +license = "Apache-2.0" +description = "Cvitek CV181x/SG2002 SDHCI host wrapper for sdmmc-protocol" +keywords = ["sd", "sdhci", "cv181x", "sg2002", "no_std"] +categories = ["embedded", "no-std", "hardware-support"] + +[dependencies] +sdhci-host = { workspace = true, default-features = false } +sdmmc-protocol = { workspace = true, default-features = false, features = ["sdio", "rdif"] } +sdio-host2.workspace = true +dma-api.workspace = true +mmio-api.workspace = true +rdif-block.workspace = true + +[features] +default = [] diff --git a/drivers/blk/cv181x-sdhci/src/lib.rs b/drivers/blk/cv181x-sdhci/src/lib.rs new file mode 100644 index 0000000000..ff9a2d40af --- /dev/null +++ b/drivers/blk/cv181x-sdhci/src/lib.rs @@ -0,0 +1,804 @@ +//! CV181x/SG2002 SD-card host wrapper for the generic SDHCI backend. +//! +//! This crate owns only the Cvitek-specific top/pinmux/PHY programming around +//! the controller. Command, data, interrupt-status caching, and RDIF queue +//! semantics are delegated to [`sdhci_host::Sdhci`]. + +#![no_std] + +use core::ptr::NonNull; + +use sdhci_host::Sdhci; +use sdio_host2::{BusWidth, ClockHz, ClockSpeed, RequestPoll, SignalVoltage}; +use sdmmc_protocol::{Error as ProtocolError, sdio::SdioHost2Irq}; + +pub mod rdif; + +const DEFAULT_SRC_FREQUENCY_HZ: u32 = 375_000_000; +const DEFAULT_MIN_FREQUENCY_HZ: u32 = 400_000; +const DEFAULT_MAX_FREQUENCY_HZ: u32 = 25_000_000; + +/// CV181x TOP syscon physical base used by the SD0 power/pinmux registers. +pub const CV181X_TOP_SYSCON_BASE: u64 = 0x0300_0000; +/// Minimum syscon mapping required by this wrapper (TOP + pinmux/IO window). +pub const CV181X_SYSCON_REQUIRED_SIZE: usize = 0x2000; + +const SYSCON_PINMUX_OFFSET: usize = 0x1000; + +const TOP_SD_PWRSW_CTRL: usize = 0x1f4; +const TOP_SD_PWRSW_3V3: u32 = 0x9; +const TOP_SD_PWRSW_OFF: u32 = 0xe; +const TOP_SD_PWRSW_LOW_MASK: u32 = 0xf; + +const PINMUX_SDIO0_CD: usize = 0x34; +const PINMUX_SDIO0_PWR_EN: usize = 0x38; +const PINMUX_SDIO0_CLK: usize = 0x1c; +const PINMUX_SDIO0_CMD: usize = 0x20; +const PINMUX_SDIO0_D0: usize = 0x24; +const PINMUX_SDIO0_D1: usize = 0x28; +const PINMUX_SDIO0_D2: usize = 0x2c; +const PINMUX_SDIO0_D3: usize = 0x30; +const PINMUX_FUNC_SDIO0: u8 = 0x0; +const PINMUX_FUNC_XGPIO: u8 = 0x3; + +const IO_SDIO0_CD: usize = 0x900; +const IO_SDIO0_PWR_EN: usize = 0x904; +const IO_SDIO0_CLK: usize = 0xa00; +const IO_SDIO0_CMD: usize = 0xa04; +const IO_SDIO0_D0: usize = 0xa08; +const IO_SDIO0_D1: usize = 0xa0c; +const IO_SDIO0_D2: usize = 0xa10; +const IO_SDIO0_D3: usize = 0xa14; +const IO_PULL_UP: u8 = 1 << 2; +const IO_PULL_DOWN: u8 = 1 << 3; + +const REG_HOST_CONTROL1: usize = 0x28; +const REG_HOST_CONTROL2: usize = 0x3e; +const HOST_CTRL1_HIGH_SPEED: u8 = 1 << 2; +const HOST_CTRL2_UHS_MODE_MASK: u16 = 0x0007; +const HOST_CTRL2_UHS_SDR12: u16 = 0x0000; +const HOST_CTRL2_UHS_SDR25: u16 = 0x0001; + +const CVI_VENDOR_MSHC_CTRL: usize = 0x200; +const CVI_PHY_TX_RX_DLY: usize = 0x240; +const CVI_PHY_CONFIG: usize = 0x24c; +const MSHC_CTRL_DS_HS_BITS: u32 = (1 << 1) | (1 << 8) | (1 << 9); +const PHY_TX_RX_DLY_DS_HS: u32 = 0x0100_0100; +const PHY_CONFIG_DS_HS: u32 = 1; + +/// Already-mapped MMIO regions required by the portable CV181x wrapper. +#[derive(Clone, Copy)] +pub struct Cv181xMmio { + core: NonNull, + syscon: NonNull, +} + +impl Cv181xMmio { + pub const fn new(core: NonNull, syscon: NonNull) -> Self { + Self { core, syscon } + } + + pub const fn core(self) -> NonNull { + self.core + } + + pub const fn syscon(self) -> NonNull { + self.syscon + } + + fn pinmux(self) -> NonNull { + // SAFETY: OS glue maps the CV181x syscon window. The documented + // pinmux block lives at TOP_BASE + 0x1000 inside that mapping. + unsafe { NonNull::new_unchecked(self.syscon.as_ptr().add(SYSCON_PINMUX_OFFSET)) } + } +} + +/// Board/device policy for the CV181x SD-card controller. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Cv181xConfig { + pub src_frequency_hz: u32, + pub min_frequency_hz: u32, + pub max_frequency_hz: u32, + pub max_bus_width: BusWidth, + pub no_1v8: bool, + pub has_card_detect_gpio: bool, + pub touch_power_enable_pin: bool, +} + +impl Default for Cv181xConfig { + fn default() -> Self { + Self { + src_frequency_hz: DEFAULT_SRC_FREQUENCY_HZ, + min_frequency_hz: DEFAULT_MIN_FREQUENCY_HZ, + max_frequency_hz: DEFAULT_MAX_FREQUENCY_HZ, + max_bus_width: BusWidth::Bit4, + no_1v8: true, + has_card_detect_gpio: false, + touch_power_enable_pin: false, + } + } +} + +impl Cv181xConfig { + pub fn normalized(mut self) -> Self { + if self.src_frequency_hz == 0 { + self.src_frequency_hz = DEFAULT_SRC_FREQUENCY_HZ; + } + if self.min_frequency_hz == 0 { + self.min_frequency_hz = DEFAULT_MIN_FREQUENCY_HZ; + } + if self.max_frequency_hz == 0 { + self.max_frequency_hz = DEFAULT_MAX_FREQUENCY_HZ; + } + if self.max_frequency_hz < self.min_frequency_hz { + self.max_frequency_hz = self.min_frequency_hz; + } + self + } + + fn clamp_clock(self, hz: u32) -> u32 { + if hz == 0 { + return 0; + } + hz.clamp(self.min_frequency_hz, self.max_frequency_hz) + } + + fn supports_bus_width(self, width: BusWidth) -> bool { + matches!( + (self.max_bus_width, width), + (BusWidth::Bit1, BusWidth::Bit1) + | (BusWidth::Bit4, BusWidth::Bit1 | BusWidth::Bit4) + | ( + BusWidth::Bit8, + BusWidth::Bit1 | BusWidth::Bit4 | BusWidth::Bit8 + ) + ) + } +} + +/// CV181x SD-card host endpoint. +pub struct Cv181xSdhci { + inner: Sdhci, + mmio: Cv181xMmio, + config: Cv181xConfig, +} + +// SAFETY: The wrapper owns exclusive access to one SDHCI register file and the +// board-level syscon/pinmux window for the controller lifetime. It does not +// expose shared mutable access; IRQ extraction uses the cloned SDHCI IRQ core. +unsafe impl Send for Cv181xSdhci {} + +impl Cv181xSdhci { + /// Construct a CV181x SD-card host over already-mapped MMIO. + /// + /// # Safety + /// + /// `mmio.core` must point to an exclusively-owned CV181x SDHCI register + /// block and `mmio.syscon` must cover TOP_BASE including the pinmux block. + pub unsafe fn new(mmio: Cv181xMmio, config: Cv181xConfig) -> Self { + let inner = unsafe { Sdhci::new(mmio.core()) }; + let mut this = Self { + inner, + mmio, + config: config.normalized(), + }; + this.restore_ds_hs_phy(); + this + } + + pub const fn config(&self) -> Cv181xConfig { + self.config + } + + pub fn inner(&self) -> &Sdhci { + &self.inner + } + + pub fn inner_mut(&mut self) -> &mut Sdhci { + &mut self.inner + } + + pub fn into_inner(self) -> Sdhci { + self.inner + } + + pub fn configure_sd_power_on(&mut self) { + self.restore_3v3_power(); + self.setup_sd_pad(false); + self.setup_sd_io(false); + self.restore_ds_hs_phy(); + } + + pub fn configure_sd_power_off(&mut self) { + self.setup_sd_pad(true); + self.setup_sd_io(true); + self.close_power(); + } + + pub fn restore_3v3_power(&mut self) { + self.update_top_power(TOP_SD_PWRSW_3V3); + } + + pub fn close_power(&mut self) { + self.update_top_power(TOP_SD_PWRSW_OFF); + } + + pub fn setup_sd_pad(&mut self, unplug: bool) { + let pinmux = self.mmio.pinmux(); + let active_cd_func = if self.config.has_card_detect_gpio { + PINMUX_FUNC_XGPIO + } else { + PINMUX_FUNC_SDIO0 + }; + write_u8(pinmux, PINMUX_SDIO0_CD, active_cd_func); + + if self.config.touch_power_enable_pin { + write_u8(pinmux, PINMUX_SDIO0_PWR_EN, PINMUX_FUNC_SDIO0); + } + + let func = if unplug { + PINMUX_FUNC_XGPIO + } else { + PINMUX_FUNC_SDIO0 + }; + for off in [ + PINMUX_SDIO0_CLK, + PINMUX_SDIO0_CMD, + PINMUX_SDIO0_D0, + PINMUX_SDIO0_D1, + PINMUX_SDIO0_D2, + PINMUX_SDIO0_D3, + ] { + write_u8(pinmux, off, func); + } + } + + pub fn setup_sd_io(&mut self, reset: bool) { + let pinmux = self.mmio.pinmux(); + set_pull(pinmux, IO_SDIO0_CD, IO_PULL_UP, IO_PULL_DOWN); + set_pull(pinmux, IO_SDIO0_PWR_EN, IO_PULL_DOWN, IO_PULL_UP); + set_pull(pinmux, IO_SDIO0_CLK, IO_PULL_DOWN, IO_PULL_UP); + + let (set, clear) = if reset { + (IO_PULL_DOWN, IO_PULL_UP) + } else { + (IO_PULL_UP, IO_PULL_DOWN) + }; + for off in [ + IO_SDIO0_CMD, + IO_SDIO0_D0, + IO_SDIO0_D1, + IO_SDIO0_D2, + IO_SDIO0_D3, + ] { + set_pull(pinmux, off, set, clear); + } + } + + pub fn restore_ds_hs_phy(&mut self) { + let core = self.mmio.core(); + let mshc = read_u32(core, CVI_VENDOR_MSHC_CTRL) | MSHC_CTRL_DS_HS_BITS; + write_u32(core, CVI_VENDOR_MSHC_CTRL, mshc); + write_u32(core, CVI_PHY_TX_RX_DLY, PHY_TX_RX_DLY_DS_HS); + write_u32(core, CVI_PHY_CONFIG, PHY_CONFIG_DS_HS); + } + + fn update_top_power(&mut self, low_bits: u32) { + let cur = read_u32(self.mmio.syscon(), TOP_SD_PWRSW_CTRL); + write_u32( + self.mmio.syscon(), + TOP_SD_PWRSW_CTRL, + (cur & !TOP_SD_PWRSW_LOW_MASK) | low_bits, + ); + } + + fn program_clock( + &mut self, + target_hz: u32, + high_speed: bool, + uhs_mode: u16, + ) -> Result<(), sdio_host2::Error> { + let target_hz = self.config.clamp_clock(target_hz); + self.set_host_timing_bits(high_speed, uhs_mode); + self.inner + .enable_clock(self.config.src_frequency_hz, target_hz) + .map_err(map_protocol_error) + } + + fn set_clock_speed(&mut self, speed: ClockSpeed) -> Result<(), sdio_host2::Error> { + match speed { + ClockSpeed::Identification => { + self.program_clock(self.config.min_frequency_hz, false, HOST_CTRL2_UHS_SDR12) + } + ClockSpeed::Default | ClockSpeed::Sdr12 => { + self.program_clock(25_000_000, false, HOST_CTRL2_UHS_SDR12) + } + ClockSpeed::HighSpeed | ClockSpeed::Sdr25 => { + self.program_clock(50_000_000, true, HOST_CTRL2_UHS_SDR25) + } + ClockSpeed::Sdr50 | ClockSpeed::Sdr104 | ClockSpeed::Ddr50 | ClockSpeed::Hs200 + if self.config.no_1v8 => + { + Err(sdio_host2::Error::Unsupported) + } + ClockSpeed::Sdr50 | ClockSpeed::Sdr104 | ClockSpeed::Ddr50 | ClockSpeed::Hs200 => { + Err(sdio_host2::Error::Unsupported) + } + _ => Err(sdio_host2::Error::Unsupported), + } + } + + fn set_host_timing_bits(&mut self, high_speed: bool, uhs_mode: u16) { + let core = self.mmio.core(); + let mut ctrl1 = read_u8(core, REG_HOST_CONTROL1); + if high_speed { + ctrl1 |= HOST_CTRL1_HIGH_SPEED; + } else { + ctrl1 &= !HOST_CTRL1_HIGH_SPEED; + } + write_u8(core, REG_HOST_CONTROL1, ctrl1); + + let ctrl2 = (read_u16(core, REG_HOST_CONTROL2) & !HOST_CTRL2_UHS_MODE_MASK) + | (uhs_mode & HOST_CTRL2_UHS_MODE_MASK); + write_u16(core, REG_HOST_CONTROL2, ctrl2); + } + + fn apply_after(&mut self, after: AfterBusOp) -> Result<(), sdio_host2::Error> { + match after { + AfterBusOp::None => Ok(()), + AfterBusOp::PowerOn | AfterBusOp::ResetAll => { + self.configure_sd_power_on(); + Ok(()) + } + } + } +} + +impl SdioHost2Irq for Cv181xSdhci { + type Event = sdhci_host::Event; + type IrqHandle = sdhci_host::SdhciIrqHandle; + + fn completion_irq_enabled(&self) -> bool { + self.inner.completion_irq_enabled() + } + + fn enable_completion_irq(&mut self) -> Result<(), ProtocolError> { + self.inner.enable_completion_irq(); + Ok(()) + } + + fn disable_completion_irq(&mut self) -> Result<(), ProtocolError> { + self.inner.disable_completion_irq(); + Ok(()) + } + + fn irq_handle(&mut self) -> Self::IrqHandle { + self.inner.irq_endpoint() + } +} + +impl sdio_host2::SdioHost for Cv181xSdhci { + type TransactionRequest<'a> + = ::TransactionRequest<'a> + where + Self: 'a; + type BusRequest = BusRequest; + + unsafe fn submit_transaction<'a>( + &mut self, + transaction: sdio_host2::Transaction<'a>, + ) -> Result, sdio_host2::Error> + where + Self: 'a, + { + unsafe { sdio_host2::SdioHost::submit_transaction(&mut self.inner, transaction) } + } + + unsafe fn submit_transaction_owned<'a>( + &mut self, + transaction: sdio_host2::Transaction<'a>, + ) -> Result, sdio_host2::SubmitTransactionError<'a>> + where + Self: 'a, + { + unsafe { sdio_host2::SdioHost::submit_transaction_owned(&mut self.inner, transaction) } + } + + fn poll_transaction<'a>( + &mut self, + request: &mut Self::TransactionRequest<'a>, + ) -> Result, sdio_host2::PollRequestError> + where + Self: 'a, + { + sdio_host2::SdioHost::poll_transaction(&mut self.inner, request) + } + + fn abort_transaction<'a>( + &mut self, + request: &mut Self::TransactionRequest<'a>, + ) -> Result<(), sdio_host2::Error> + where + Self: 'a, + { + sdio_host2::SdioHost::abort_transaction(&mut self.inner, request) + } + + fn take_completed_dma<'a>( + &mut self, + request: &mut Self::TransactionRequest<'a>, + ) -> Option + where + Self: 'a, + { + sdio_host2::SdioHost::take_completed_dma(&mut self.inner, request) + } + + unsafe fn submit_bus_op( + &mut self, + op: sdio_host2::BusOp, + ) -> Result { + match op { + sdio_host2::BusOp::PowerOn => { + let request = unsafe { sdio_host2::SdioHost::submit_bus_op(&mut self.inner, op)? }; + Ok(BusRequest::inner(request, AfterBusOp::PowerOn)) + } + sdio_host2::BusOp::PowerOff => { + self.configure_sd_power_off(); + let request = unsafe { sdio_host2::SdioHost::submit_bus_op(&mut self.inner, op)? }; + Ok(BusRequest::inner(request, AfterBusOp::None)) + } + sdio_host2::BusOp::ResetAll => { + let request = unsafe { sdio_host2::SdioHost::submit_bus_op(&mut self.inner, op)? }; + Ok(BusRequest::inner(request, AfterBusOp::ResetAll)) + } + sdio_host2::BusOp::SetClock(speed) => { + Ok(BusRequest::ready(self.set_clock_speed(speed))) + } + sdio_host2::BusOp::SetClockHz(ClockHz(hz)) => Ok(BusRequest::ready( + self.program_clock(hz, hz > DEFAULT_MAX_FREQUENCY_HZ, HOST_CTRL2_UHS_SDR12), + )), + sdio_host2::BusOp::SetBusWidth(width) if !self.config.supports_bus_width(width) => { + Ok(BusRequest::ready(Err(sdio_host2::Error::Unsupported))) + } + sdio_host2::BusOp::SetSignalVoltage(SignalVoltage::V180) if self.config.no_1v8 => { + Ok(BusRequest::ready(Err(sdio_host2::Error::Unsupported))) + } + sdio_host2::BusOp::SetSignalVoltage(SignalVoltage::V330) => { + self.restore_3v3_power(); + let request = unsafe { sdio_host2::SdioHost::submit_bus_op(&mut self.inner, op)? }; + Ok(BusRequest::inner(request, AfterBusOp::None)) + } + _ => { + let request = unsafe { sdio_host2::SdioHost::submit_bus_op(&mut self.inner, op)? }; + Ok(BusRequest::inner(request, AfterBusOp::None)) + } + } + } + + fn poll_bus_op( + &mut self, + bus_request: &mut Self::BusRequest, + ) -> Result, sdio_host2::PollRequestError> { + match &mut bus_request.state { + BusRequestState::Ready(result) => { + let result = result + .take() + .ok_or(sdio_host2::PollRequestError::AlreadyCompleted)?; + bus_request.state = BusRequestState::Done; + Ok(RequestPoll::Ready(result)) + } + BusRequestState::Inner { + request: inner, + after, + } => match sdio_host2::SdioHost::poll_bus_op(&mut self.inner, inner)? { + RequestPoll::Pending => Ok(RequestPoll::Pending), + RequestPoll::Ready(result) => { + let result = result.and_then(|()| self.apply_after(*after)); + bus_request.state = BusRequestState::Done; + Ok(RequestPoll::Ready(result)) + } + }, + BusRequestState::Done => Err(sdio_host2::PollRequestError::AlreadyCompleted), + } + } + + fn abort_bus_op( + &mut self, + bus_request: &mut Self::BusRequest, + ) -> Result<(), sdio_host2::Error> { + let result = match &mut bus_request.state { + BusRequestState::Inner { request: inner, .. } => { + sdio_host2::SdioHost::abort_bus_op(&mut self.inner, inner) + } + BusRequestState::Ready(_) | BusRequestState::Done => Ok(()), + }; + bus_request.state = BusRequestState::Done; + result + } + + fn now_ms(&self) -> Option { + sdio_host2::SdioHost::now_ms(&self.inner) + } +} + +pub struct BusRequest { + state: BusRequestState, +} + +impl BusRequest { + fn ready(result: Result<(), sdio_host2::Error>) -> Self { + Self { + state: BusRequestState::Ready(Some(result)), + } + } + + fn inner(request: ::BusRequest, after: AfterBusOp) -> Self { + Self { + state: BusRequestState::Inner { request, after }, + } + } +} + +enum BusRequestState { + Ready(Option>), + Inner { + request: ::BusRequest, + after: AfterBusOp, + }, + Done, +} + +#[derive(Clone, Copy)] +enum AfterBusOp { + None, + PowerOn, + ResetAll, +} + +fn map_protocol_error(err: ProtocolError) -> sdio_host2::Error { + match err { + ProtocolError::Timeout(_) => sdio_host2::Error::Timeout, + ProtocolError::Crc(_) => sdio_host2::Error::Crc, + ProtocolError::NoCard => sdio_host2::Error::NoCard, + ProtocolError::Busy => sdio_host2::Error::Busy, + ProtocolError::UnsupportedCommand => sdio_host2::Error::Unsupported, + ProtocolError::Misaligned => sdio_host2::Error::Misaligned, + ProtocolError::InvalidArgument => sdio_host2::Error::InvalidArgument, + ProtocolError::BusError(_) => sdio_host2::Error::Bus, + ProtocolError::ReadError(_) + | ProtocolError::WriteError(_) + | ProtocolError::BadResponse(_) => sdio_host2::Error::Bus, + ProtocolError::CardError(_) | ProtocolError::CardLocked => sdio_host2::Error::Controller, + _ => sdio_host2::Error::Controller, + } +} + +fn set_pull(base: NonNull, off: usize, set: u8, clear: u8) { + let next = (read_u8(base, off) | set) & !clear; + write_u8(base, off, next); +} + +fn read_u8(base: NonNull, off: usize) -> u8 { + // SAFETY: caller-provided MMIO base covers the documented byte register. + unsafe { core::ptr::read_volatile(base.as_ptr().add(off) as *const u8) } +} + +fn write_u8(base: NonNull, off: usize, val: u8) { + // SAFETY: caller-provided MMIO base covers the documented byte register. + unsafe { core::ptr::write_volatile(base.as_ptr().add(off), val) } +} + +fn read_u16(base: NonNull, off: usize) -> u16 { + // SAFETY: caller-provided MMIO base covers the documented 16-bit register. + unsafe { core::ptr::read_volatile(base.as_ptr().add(off) as *const u16) } +} + +fn write_u16(base: NonNull, off: usize, val: u16) { + // SAFETY: caller-provided MMIO base covers the documented 16-bit register. + unsafe { core::ptr::write_volatile(base.as_ptr().add(off) as *mut u16, val) } +} + +fn read_u32(base: NonNull, off: usize) -> u32 { + // SAFETY: caller-provided MMIO base covers the documented 32-bit register. + unsafe { core::ptr::read_volatile(base.as_ptr().add(off) as *const u32) } +} + +fn write_u32(base: NonNull, off: usize, val: u32) { + // SAFETY: caller-provided MMIO base covers the documented 32-bit register. + unsafe { core::ptr::write_volatile(base.as_ptr().add(off) as *mut u32, val) } +} + +#[cfg(test)] +mod tests { + extern crate std; + + use super::*; + + #[repr(align(4))] + struct FakeMmio([u8; N]); + + impl FakeMmio { + fn new() -> Self { + Self([0; N]) + } + + fn base(&mut self) -> NonNull { + NonNull::new(self.0.as_mut_ptr()).unwrap() + } + } + + fn new_host<'a>( + core: &'a mut FakeMmio<0x400>, + syscon: &'a mut FakeMmio<0x2000>, + config: Cv181xConfig, + ) -> Cv181xSdhci { + let mmio = Cv181xMmio::new(core.base(), syscon.base()); + unsafe { Cv181xSdhci::new(mmio, config) } + } + + fn poll_ready_bus_op( + host: &mut Cv181xSdhci, + request: &mut BusRequest, + ) -> Result<(), sdio_host2::Error> { + match sdio_host2::SdioHost::poll_bus_op(host, request).unwrap() { + RequestPoll::Ready(result) => result, + RequestPoll::Pending => panic!("test bus op should complete synchronously"), + } + } + + #[test] + fn power_on_sequence_configures_3v3_pads_io_and_ds_hs_phy() { + let mut core = FakeMmio::new(); + let mut syscon = FakeMmio::new(); + write_u32(syscon.base(), TOP_SD_PWRSW_CTRL, 0xa5a5_a5a0); + write_u8( + unsafe { NonNull::new_unchecked(syscon.base().as_ptr().add(SYSCON_PINMUX_OFFSET)) }, + PINMUX_SDIO0_PWR_EN, + 0x7, + ); + + let mut host = new_host( + &mut core, + &mut syscon, + Cv181xConfig { + has_card_detect_gpio: true, + ..Cv181xConfig::default() + }, + ); + host.configure_sd_power_on(); + + let pinmux = + unsafe { NonNull::new_unchecked(syscon.base().as_ptr().add(SYSCON_PINMUX_OFFSET)) }; + assert_eq!( + read_u32(syscon.base(), TOP_SD_PWRSW_CTRL), + 0xa5a5_a5a0 | TOP_SD_PWRSW_3V3 + ); + assert_eq!(read_u8(pinmux, PINMUX_SDIO0_CD), PINMUX_FUNC_XGPIO); + assert_eq!(read_u8(pinmux, PINMUX_SDIO0_CLK), PINMUX_FUNC_SDIO0); + assert_eq!(read_u8(pinmux, PINMUX_SDIO0_CMD), PINMUX_FUNC_SDIO0); + assert_eq!(read_u8(pinmux, PINMUX_SDIO0_D3), PINMUX_FUNC_SDIO0); + assert_eq!(read_u8(pinmux, PINMUX_SDIO0_PWR_EN), 0x7); + assert_eq!(read_u8(pinmux, IO_SDIO0_CMD) & IO_PULL_UP, IO_PULL_UP); + assert_eq!(read_u8(pinmux, IO_SDIO0_CMD) & IO_PULL_DOWN, 0); + assert_eq!( + read_u32(core.base(), CVI_PHY_TX_RX_DLY), + PHY_TX_RX_DLY_DS_HS + ); + assert_eq!(read_u32(core.base(), CVI_PHY_CONFIG), PHY_CONFIG_DS_HS); + assert_eq!( + read_u32(core.base(), CVI_VENDOR_MSHC_CTRL) & MSHC_CTRL_DS_HS_BITS, + MSHC_CTRL_DS_HS_BITS + ); + } + + #[test] + fn power_off_switches_sd_pads_to_gpio_and_closes_power() { + let mut core = FakeMmio::new(); + let mut syscon = FakeMmio::new(); + let mut host = new_host(&mut core, &mut syscon, Cv181xConfig::default()); + + host.configure_sd_power_off(); + + let pinmux = + unsafe { NonNull::new_unchecked(syscon.base().as_ptr().add(SYSCON_PINMUX_OFFSET)) }; + assert_eq!(read_u8(pinmux, PINMUX_SDIO0_CLK), PINMUX_FUNC_XGPIO); + assert_eq!(read_u8(pinmux, PINMUX_SDIO0_D0), PINMUX_FUNC_XGPIO); + assert_eq!(read_u8(pinmux, IO_SDIO0_D0) & IO_PULL_DOWN, IO_PULL_DOWN); + assert_eq!( + read_u32(syscon.base(), TOP_SD_PWRSW_CTRL) & TOP_SD_PWRSW_LOW_MASK, + TOP_SD_PWRSW_OFF + ); + } + + #[test] + fn config_normalization_keeps_clock_bounds_valid() { + let config = Cv181xConfig { + src_frequency_hz: 0, + min_frequency_hz: 50_000_000, + max_frequency_hz: 25_000_000, + ..Cv181xConfig::default() + } + .normalized(); + + assert_eq!(config.src_frequency_hz, DEFAULT_SRC_FREQUENCY_HZ); + assert_eq!(config.max_frequency_hz, 50_000_000); + } + + #[test] + fn bus_width_limit_rejects_width_above_board_wiring() { + let mut core = FakeMmio::new(); + let mut syscon = FakeMmio::new(); + let mut host = new_host( + &mut core, + &mut syscon, + Cv181xConfig { + max_bus_width: BusWidth::Bit1, + ..Cv181xConfig::default() + }, + ); + + let mut request = unsafe { + sdio_host2::SdioHost::submit_bus_op( + &mut host, + sdio_host2::BusOp::SetBusWidth(BusWidth::Bit4), + ) + } + .unwrap(); + + assert_eq!( + poll_ready_bus_op(&mut host, &mut request), + Err(sdio_host2::Error::Unsupported) + ); + } + + #[test] + fn no_1v8_rejects_uhs_clock_and_voltage_paths() { + let mut core = FakeMmio::new(); + let mut syscon = FakeMmio::new(); + let mut host = new_host( + &mut core, + &mut syscon, + Cv181xConfig { + no_1v8: true, + ..Cv181xConfig::default() + }, + ); + + assert_eq!( + host.set_clock_speed(ClockSpeed::Sdr50), + Err(sdio_host2::Error::Unsupported) + ); + + let mut request = unsafe { + sdio_host2::SdioHost::submit_bus_op( + &mut host, + sdio_host2::BusOp::SetSignalVoltage(SignalVoltage::V180), + ) + } + .unwrap(); + + assert_eq!( + poll_ready_bus_op(&mut host, &mut request), + Err(sdio_host2::Error::Unsupported) + ); + } + + #[test] + fn high_speed_mode_sets_host_timing_even_when_clock_is_capped() { + let mut core = FakeMmio::new(); + let mut syscon = FakeMmio::new(); + let mut host = new_host(&mut core, &mut syscon, Cv181xConfig::default()); + + let _ = host.set_clock_speed(ClockSpeed::HighSpeed); + + assert_eq!( + read_u8(core.base(), REG_HOST_CONTROL1) & HOST_CTRL1_HIGH_SPEED, + HOST_CTRL1_HIGH_SPEED + ); + assert_eq!( + read_u16(core.base(), REG_HOST_CONTROL2) & HOST_CTRL2_UHS_MODE_MASK, + HOST_CTRL2_UHS_SDR25 + ); + } +} diff --git a/drivers/blk/cv181x-sdhci/src/rdif.rs b/drivers/blk/cv181x-sdhci/src/rdif.rs new file mode 100644 index 0000000000..40548b1e2e --- /dev/null +++ b/drivers/blk/cv181x-sdhci/src/rdif.rs @@ -0,0 +1,47 @@ +//! RDIF block-device adapter for [`crate::Cv181xSdhci`]. + +pub use protocol_rdif::{BlockConfig, BlockDevice, BlockQueue}; +pub use rdif_block::{ + BInterface, BIrqHandler, BOwnedQueue, BQueue, BlkError, IQueue, IQueueOwned, Interface, + OwnedRequest, PollError, QueueHandle, Request, RequestId as RdifRequestId, + RequestPoll as OwnedRequestPoll, RequestStatus, SubmitError, +}; +use sdmmc_protocol::{ + rdif as protocol_rdif, + sdio::{SdioHost2Adapter, SdioSdmmc}, +}; + +use crate::Cv181xSdhci; + +pub fn device( + card: SdioSdmmc>, + config: BlockConfig, +) -> BlockDevice> { + BlockDevice::new(card, config) +} + +pub const fn fifo_config( + name: &'static str, + capacity_blocks: u64, + irq_driven: bool, +) -> BlockConfig { + BlockConfig::fifo(name, capacity_blocks, irq_driven) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn fifo_config_is_irq_driven_without_dma() { + let config = fifo_config("cvsd", 16, true); + let limits = protocol_rdif::queue_limits(&config, config.dma_mask); + + assert_eq!(config.name, "cvsd"); + assert_eq!(config.capacity_blocks, 16); + assert!(config.irq_driven); + assert!(!config.uses_dma()); + assert_eq!(limits.max_blocks_per_request, 1); + assert_eq!(limits.max_segment_size, protocol_rdif::BLOCK_SIZE); + } +} diff --git a/drivers/blk/sdmmc-protocol/src/sdio.rs b/drivers/blk/sdmmc-protocol/src/sdio.rs index 7b968a86bc..4415768eb4 100644 --- a/drivers/blk/sdmmc-protocol/src/sdio.rs +++ b/drivers/blk/sdmmc-protocol/src/sdio.rs @@ -1061,10 +1061,22 @@ enum MmcSwitchRequestState { pub enum CardInitPreference { /// Probe SD first, then fall back to MMC. SdFirst, + /// Probe SD only. Use this when firmware marks the slot `no-mmc`. + SdOnly, /// Probe MMC first. Use this for controller instances wired to eMMC. MmcFirst, } +impl CardInitPreference { + fn starts_with_sd(self) -> bool { + matches!(self, Self::SdFirst | Self::SdOnly) + } + + fn allows_mmc_fallback(self) -> bool { + matches!(self, Self::SdFirst) + } +} + /// Caller-owned scratch buffers for SD/MMC initialization data commands. /// /// Keeping the buffers on the caller's side keeps the `SdioInitRequest` @@ -1855,17 +1867,14 @@ impl SdioSdmmc { SdioInitState::PollCmd0 => match self.host.poll_command_response()? { CommandResponsePoll::Pending => Ok(OperationPoll::Pending), CommandResponsePoll::Complete(_) => { - match request.preference { - CardInitPreference::SdFirst => { - let cmd = crate::cmd::cmd8(0x01, 0xAA); - self.host.submit_command(&cmd)?; - request.state = SdioInitState::PollCmd8; - } - CardInitPreference::MmcFirst => { - debug!("sdio: MMC-first init, trying CMD1"); - self.host.submit_command(&crate::cmd::cmd1(0))?; - request.state = SdioInitState::PollMmcInitial; - } + if request.preference.starts_with_sd() { + let cmd = crate::cmd::cmd8(0x01, 0xAA); + self.host.submit_command(&cmd)?; + request.state = SdioInitState::PollCmd8; + } else { + debug!("sdio: MMC-first init, trying CMD1"); + self.host.submit_command(&crate::cmd::cmd1(0))?; + request.state = SdioInitState::PollMmcInitial; } Ok(OperationPoll::Pending) } @@ -1902,6 +1911,9 @@ impl SdioSdmmc { Ok(OperationPoll::Pending) } Err(_sd_err) => { + if !request.preference.allows_mmc_fallback() { + return Err(_sd_err); + } debug!( "sdio: ACMD41 prologue failed ({:?}), trying MMC CMD1", _sd_err @@ -1925,6 +1937,9 @@ impl SdioSdmmc { let elapsed_exceeded = power_up_deadline_passed(&self.host, request.acmd41_started_ms); if request.acmd41_polls >= SdioInitTiming::MAX_POLLS || elapsed_exceeded { + if !request.preference.allows_mmc_fallback() { + return Err(Error::Timeout(ErrorContext::for_cmd(Phase::Init, 41))); + } warn!( "sdio: ACMD41 timed out after {} polls (~{} ms at the recommended \ cadence), trying MMC CMD1", @@ -1947,12 +1962,18 @@ impl SdioSdmmc { Ok(OperationPoll::Pending) } Ok(CommandResponsePoll::Complete(_)) => { + if !request.preference.allows_mmc_fallback() { + return Err(Error::BadResponse(ErrorContext::for_cmd(Phase::Init, 41))); + } debug!("sdio: ACMD41 returned bad response, trying MMC CMD1"); self.host.submit_command(&crate::cmd::cmd1(0))?; request.state = SdioInitState::PollMmcInitial; Ok(OperationPoll::Pending) } Err(_sd_err) => { + if !request.preference.allows_mmc_fallback() { + return Err(_sd_err); + } debug!("sdio: ACMD41 failed ({:?}), trying MMC CMD1", _sd_err); self.host.submit_command(&crate::cmd::cmd1(0))?; request.state = SdioInitState::PollMmcInitial; @@ -3439,6 +3460,24 @@ mod tests { ); } + #[test] + fn poll_init_request_sd_only_does_not_fallback_to_cmd1_after_acmd41_timeout() { + let mut driver = SdioSdmmc::new(MockHost::with_results(std::vec![Ok(Response::R3( + OcrResponse::from_raw(0x00FF_8000), + ))])); + let mut scratch = SdioInitScratch::new(); + let mut request = SdioInitRequest::new(CardInitPreference::SdOnly, &mut scratch); + request.state = SdioInitState::PollAcmd41; + request.sd_v2 = false; + request.acmd41_polls = SdioInitTiming::MAX_POLLS; + + assert!(matches!( + driver.poll_init_request(&mut request), + Err(Error::Timeout(_)) + )); + assert!(driver.host.commands.is_empty()); + } + #[test] fn submit_init_with_mmc_preference_skips_sd_probe_after_cmd0() { let mut driver = SdioSdmmc::new(MockHost::with_results(std::vec![Ok(ok_r1())]));