use alloc::format;
use core::time::Duration;
use dwmmc_host::{CardDetect, DwMmc, HostClock, rdif as dwmmc_rdif};
use fdt_edit::{Node, Phandle};
use log::{info, warn};
use rdif_pinctrl::{FdtPinctrl, PinctrlDevice};
use rdrive::{
probe::{OnProbeError, fdt::ClockLine},
register::{FdtInfo, ProbeFdt},
};
use sdmmc_protocol::{
Error, OperationPoll,
error::{ErrorContext, Phase},
sdio::{
card::{CardInfo, SdioSdmmc},
host2::SdioHost2Adapter,
init::SdioInitScratch,
},
};
use super::clock::{ScmiClockOps, enable_node_clocks, rdrive_named_clock, scmi_named_clock};
use crate::{block::ProbeFdtBlock, mmio::iomap, soc::RockchipFdtPinctrlParser};
const DWMMC_STABLE_REFERENCE_CLOCK: u32 = 50_000_000;
const ROCKCHIP_DWMMC_CLKGEN_DIV: u32 = 2;
const ENABLE_SD_SPEED_SELECTION: bool = true;
const RK3588_CRU_BASE: usize = 0xfd7c_0000;
const RK3588_CRU_SIZE: usize = 0x5c000;
const RK3588_SDMMC_CON0: usize = 0x0c30;
const RK3588_SDMMC_CON1: usize = 0x0c34;
const RK3588_SDMMC_PHASE_SHIFT: u32 = 1;
const RK3588_SDMMC_DRV_PHASE_DEG: u32 = 90;
const RK3588_SDMMC_SAMPLE_PHASE_DEG: u32 = 0;
const RK3588_SDMMC_SAMPLE_PHASE_CANDIDATES: [u32; 8] = [0, 45, 90, 135, 180, 225, 270, 315];
const SDMMC_INIT_POLL_DELAY: Duration = Duration::from_micros(1);
const SDMMC_INIT_RETRY_DELAY: Duration = Duration::from_millis(10);
type RockchipDwMmc = SdioSdmmc<SdioHost2Adapter<DwMmc>>;
enum RockchipDwMmcClock {
Rdrive(ClockLine),
Scmi(ScmiClockOps),
}
struct DwMmcClockSetup {
reference_clock: u32,
clock: RockchipDwMmcClock,
}
impl HostClock for RockchipDwMmcClock {
fn set_clock(&self, target_hz: u32) -> Result<u32, Error> {
if target_hz == 0 {
return Err(Error::InvalidArgument);
}
let cclkin = u64::from(target_hz) * u64::from(ROCKCHIP_DWMMC_CLKGEN_DIV);
let rate = match self {
Self::Rdrive(clock) => {
clock
.set_rate(cclkin)
.map_err(|_| Error::BadResponse(ErrorContext::new(Phase::Init)))?;
clock
.rate()
.map_err(|_| Error::BadResponse(ErrorContext::new(Phase::Init)))?
}
Self::Scmi(clock) => {
clock
.set_rate(cclkin)
.ok_or_else(|| Error::BadResponse(ErrorContext::new(Phase::Init)))?;
clock
.rate()
.ok_or_else(|| Error::BadResponse(ErrorContext::new(Phase::Init)))?
}
};
let bus_hz = rate / u64::from(ROCKCHIP_DWMMC_CLKGEN_DIV);
let bus_hz = validate_bus_clock(bus_hz)?;
info!(
"rockchip-dwmmc: ciu clock set target={} Hz cclkin={} Hz bus={} Hz",
target_hz, rate, bus_hz
);
Ok(bus_hz)
}
}
mod phase;
use phase::{init_rk3588_sdmmc_phase, tune_rk3588_sdmmc_sample_phase};
crate::model_register!(
name: "Rockchip SD",
level: ProbeLevel::PostKernel,
priority: ProbePriority::DEFAULT,
probe_kinds: &[
ProbeKind::Fdt {
compatibles: &["rockchip,rk3588-dw-mshc", "rockchip,rk3288-dw-mshc"],
on_probe: probe
}
],
);
fn probe(probe: ProbeFdt<'_>) -> Result<(), OnProbeError> {
let info = probe.info();
apply_rockchip_sd_resources(info)?;
let base_reg = info
.node
.regs()
.into_iter()
.next()
.ok_or(OnProbeError::other(alloc::format!(
"[{}] has no reg",
info.node.name()
)))?;
let mmio_size = base_reg.size.unwrap_or(0x1000);
info!(
"rockchip-dwmmc probe: node={}, addr={:#x}, size={:#x}",
info.node.name(),
base_reg.address as usize,
mmio_size
);
let mmio_base = iomap(base_reg.address as usize, mmio_size as usize)?;
let mut host = unsafe { DwMmc::new(mmio_base) };
host.set_card_detect(CardDetect::ControllerActiveLow);
let clock_setup = dwmmc_clock_setup(info);
if let Some(setup) = clock_setup {
info!(
"rockchip-dwmmc: using ciu reference clock {} Hz",
setup.reference_clock
);
host.set_reference_clock(setup.reference_clock);
if is_rk3588_dwmmc(info) {
init_rk3588_sdmmc_phase(info, setup.reference_clock)?;
}
host.set_external_clock(setup.clock);
} else {
warn!(
"rockchip-dwmmc: ciu clock not found; leaving DWMMC divider bypassed and relying on \
CRU rate"
);
}
let dma = axklib::dma::device_with_mask(u32::MAX as u64);
host.set_dma(dma.clone());
info!("rockchip-dwmmc: initialize card through native host2 bus ops");
let mut sd = SdioSdmmc::new_host2(host);
sd.set_sd_speed_selection_enabled(ENABLE_SD_SPEED_SELECTION);
let card_info = poll_card_init(&mut sd).map_err(|e| {
warn!("rockchip-dwmmc: card init failed: {:?}", e);
card_init_error(base_reg.address, mmio_size, e)
})?;
sd.host_mut()
.with_host_mut(|host| host.clear_external_clock());
info!(
"rockchip-dwmmc 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()
);
if let Some(reference_clock) = sd
.host()
.with_host(|host| validate_reference_clock(info, u64::from(host.reference_clock())))
&& is_rk3588_dwmmc(info)
{
tune_rk3588_sdmmc_sample_phase(&mut sd, reference_clock);
}
let dev = dwmmc_rdif::device(
sd,
dwmmc_rdif::dma_config(
"rockchip-sd",
card_info.capacity_blocks.unwrap_or(0),
true,
dma,
),
);
let irq = probe.register_block(dev)?;
info!("rockchip-sd block device registered irq={:?}", irq);
Ok(())
}
fn apply_rockchip_sd_resources(info: &FdtInfo<'_>) -> Result<(), OnProbeError> {
let Some(pinctrl) = rdrive::get_one::<PinctrlDevice>() else {
warn!(
"[{}] PinctrlDevice not found; skip SDMMC pinctrl and fixed regulators",
info.node.name()
);
return Ok(());
};
let mut pinctrl = pinctrl
.lock()
.map_err(|err| OnProbeError::other(format!("failed to lock PinctrlDevice: {err}")))?;
for (name, supply) in sd_supply_phandles(info.node.as_node()) {
if supply_has_fixed_gpio_enable(info, supply)? {
enable_fixed_regulator_with_pinctrl(&mut pinctrl, info, supply)?;
} else {
info!(
"[{}] {name} phandle {:?} is not a fixed GPIO regulator; skip pinctrl enable",
info.node.name(),
supply
);
}
}
enable_node_clocks(info, "SDMMC")?;
Ok(())
}
fn enable_fixed_regulator_with_pinctrl(
pinctrl: &mut PinctrlDevice,
info: &FdtInfo<'_>,
supply: Phandle,
) -> Result<(), OnProbeError> {
let regulator = info.get_by_phandle(supply).ok_or_else(|| {
OnProbeError::other(format!("SDMMC regulator phandle {supply:?} not found"))
})?;
let fdt = rdrive::with_fdt(Clone::clone)
.ok_or_else(|| OnProbeError::other("live FDT not found for SDMMC regulator"))?;
FdtPinctrl::apply_fixed_regulator(
pinctrl,
&fdt,
regulator.as_node(),
&RockchipFdtPinctrlParser,
"rockchip-sd-regulator",
)
.map_err(|err| {
OnProbeError::other(format!(
"failed to enable SDMMC regulator {supply:?} via pinctrl: {err}"
))
})?;
let startup_delay_us = regulator
.as_node()
.get_property("startup-delay-us")
.and_then(|prop| prop.get_u32())
.unwrap_or(0);
if startup_delay_us != 0 {
axklib::time::busy_wait(core::time::Duration::from_micros(u64::from(
startup_delay_us,
)));
}
Ok(())
}
fn sd_supply_phandles(node: &Node) -> impl Iterator<Item = (&'static str, Phandle)> + '_ {
["vmmc-supply", "vqmmc-supply"]
.into_iter()
.filter_map(|name| {
node.get_property(name)
.and_then(|prop| prop.get_u32())
.map(|phandle| (name, Phandle::from(phandle)))
})
}
fn supply_has_fixed_gpio_enable(
info: &FdtInfo<'_>,
phandle: Phandle,
) -> Result<bool, OnProbeError> {
let node = info
.get_by_phandle(phandle)
.ok_or_else(|| OnProbeError::other(format!("SD supply phandle {phandle:?} not found")))?;
Ok(regulator_has_fixed_gpio_enable(node.as_node()))
}
fn regulator_has_fixed_gpio_enable(node: &Node) -> bool {
node.compatibles()
.any(|compatible| compatible == "regulator-fixed")
&& (node.get_property("gpios").is_some()
|| node.get_property("gpio").is_some()
|| node.get_property("pinctrl-0").is_some())
}
fn poll_card_init(sd: &mut RockchipDwMmc) -> Result<CardInfo, Error> {
let mut scratch = SdioInitScratch::new();
let mut request = sd.submit_init(&mut scratch)?;
loop {
match sd.poll_init_request(&mut request)? {
OperationPoll::Pending => {
if request.take_needs_pace() {
axklib::time::busy_wait(SDMMC_INIT_RETRY_DELAY);
} else {
axklib::time::busy_wait(SDMMC_INIT_POLL_DELAY);
}
}
OperationPoll::Complete(info) => return Ok(info),
_ => return Err(Error::UnsupportedCommand),
}
}
}
fn init_error(address: u64, size: u64, err: Error) -> OnProbeError {
OnProbeError::other(format!(
"failed to initialize DWMMC device at [PA:{:?}, SZ:0x{:x}): {err:?}",
address, size
))
}
fn card_init_error(address: u64, size: u64, err: Error) -> OnProbeError {
if is_absent_card_init_error(err) {
warn!(
"rockchip-dwmmc: no responsive card at [PA:{:?}, SZ:0x{:x}); skipping controller: \
{err:?}",
address, size
);
return OnProbeError::NotMatch;
}
init_error(address, size, err)
}
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
)
}
_ => false,
}
}
fn dwmmc_clock_setup(info: &FdtInfo<'_>) -> Option<DwMmcClockSetup> {
match rdrive_named_clock(info, "ciu") {
Ok(Some(clock)) => {
if let Err(err) = clock.set_rate(DWMMC_STABLE_REFERENCE_CLOCK as u64) {
warn!(
"[{}] failed to set ciu clock {:?} to {} Hz: {err}",
info.node.name(),
clock.id(),
DWMMC_STABLE_REFERENCE_CLOCK
);
}
match clock.rate() {
Ok(rate) => Some(DwMmcClockSetup {
reference_clock: validate_reference_clock(info, rate)?,
clock: RockchipDwMmcClock::Rdrive(clock),
}),
Err(err) => {
warn!("[{}] failed to read ciu clock: {err}", info.node.name());
None
}
}
}
Ok(None) | Err(_) => {
if let Some(clock) = scmi_named_clock(info, "ciu") {
clock.set_rate(DWMMC_STABLE_REFERENCE_CLOCK as u64)?;
let rate = clock.rate()?;
return Some(DwMmcClockSetup {
reference_clock: validate_reference_clock(info, rate)?,
clock: RockchipDwMmcClock::Scmi(clock),
});
}
warn!(
"[{}] ciu clock provider is not available through CRU or SCMI",
info.node.name()
);
None
}
}
}
fn is_rk3588_dwmmc(info: &FdtInfo<'_>) -> bool {
info.node
.as_node()
.compatibles()
.any(|compatible| compatible == "rockchip,rk3588-dw-mshc")
}
fn validate_reference_clock(info: &FdtInfo<'_>, rate: u64) -> Option<u32> {
if rate == 0 || rate > u32::MAX as u64 {
warn!("[{}] invalid ciu clock rate {} Hz", info.node.name(), rate);
return None;
}
Some(rate as u32)
}
fn validate_bus_clock(rate: u64) -> Result<u32, Error> {
if rate == 0 || rate > u32::MAX as u64 {
return Err(Error::BadResponse(ErrorContext::new(Phase::Init)));
}
Ok(rate as u32)
}
#[cfg(test)]
mod tests {
use alloc::{vec, vec::Vec};
use sdmmc_protocol::error::ErrorContext;
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 sd_supply_phandles_reads_optional_vmmc_and_vqmmc() {
let mut node = Node::new("mmc@fe2c0000");
node.add_property(fdt_edit::Property::new(
"vmmc-supply",
0x1234_u32.to_be_bytes().to_vec(),
));
node.add_property(fdt_edit::Property::new(
"vqmmc-supply",
0x5678_u32.to_be_bytes().to_vec(),
));
let supplies = sd_supply_phandles(&node).collect::<Vec<_>>();
assert_eq!(
supplies,
vec![
("vmmc-supply", Phandle::from(0x1234)),
("vqmmc-supply", Phandle::from(0x5678))
]
);
}
#[test]
fn sd_supply_phandles_allows_absent_supplies() {
let node = Node::new("mmc@fe2c0000");
assert_eq!(sd_supply_phandles(&node).count(), 0);
}
#[test]
fn fixed_regulator_with_gpio_enable_is_pinctrl_controlled() {
let mut node = Node::new("vcc-3v3-sd-s0");
node.add_property(fdt_edit::Property::new(
"compatible",
b"regulator-fixed\0".to_vec(),
));
node.add_property(fdt_edit::Property::new("gpios", Vec::new()));
assert!(regulator_has_fixed_gpio_enable(&node));
}
#[test]
fn pmic_regulator_supply_is_not_pinctrl_controlled() {
let mut node = Node::new("PLDO_REG5");
node.add_property(fdt_edit::Property::new(
"regulator-name",
b"vccio_sd_s0\0".to_vec(),
));
assert!(!regulator_has_fixed_gpio_enable(&node));
}
}