Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions usb-host/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ repository = "https://github.com/drivercraft/CrabUSB"
version = "0.4.0"

[features]
default = ["aggressive_usb_reset"]
aggressive_usb_reset = []
libusb = ["libusb1-sys"]

[dependencies]
Expand Down
6 changes: 6 additions & 0 deletions usb-host/src/backend/xhci/delay.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#[inline]
pub fn delay_ms(ms: u32) {
for _ in 0..(ms as u64 * 100_000) {
core::hint::spin_loop();
}
}
19 changes: 19 additions & 0 deletions usb-host/src/backend/xhci/dwc3.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
//! DWC3 (DesignWare USB3 Controller) detection for RK3588.

use core::ptr::NonNull;

const DWC3_REG_OFFSET: usize = 0xC100;
const DWC3_GSNPSID_MASK: u32 = 0xFFFF0000;
const DWC3_GSNPSID_CORE_3: u32 = 0x55330000;
const DWC3_GSNPSID_CORE_31: u32 = 0x33310000;

/// # Safety
/// The caller must ensure the XHCI base address is valid and properly mapped.
pub unsafe fn is_dwc3_xhci(xhci_base: NonNull<u8>) -> bool {
unsafe {
let gsnpsid_addr = xhci_base.as_ptr().add(DWC3_REG_OFFSET + 0x20) as *const u32;
let id = gsnpsid_addr.read_volatile();
let masked = id & DWC3_GSNPSID_MASK;
masked == DWC3_GSNPSID_CORE_3 || masked == DWC3_GSNPSID_CORE_31
}
}
62 changes: 49 additions & 13 deletions usb-host/src/backend/xhci/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,15 @@ use xhci::{

mod context;
mod def;
mod delay;
mod device;
pub mod dwc3;
mod endpoint;
mod event;
mod interface;
mod reg;
mod ring;
pub mod rk3588_phy;
mod root;

use crate::{
Expand All @@ -26,6 +29,7 @@ use crate::{
use def::*;

pub struct Xhci {
mmio_base: NonNull<u8>,
reg: XhciRegisters,
root: Option<RootHub>,
port_wake: AtomicWaker,
Expand All @@ -39,28 +43,26 @@ impl usb_if::host::Controller for Xhci {
&'_ mut self,
) -> futures::future::LocalBoxFuture<'_, core::result::Result<(), usb_if::host::USBError>> {
async {
// 4.2 Host Controller Initialization
self.init_dwc3_if_needed();
self.init_ext_caps().await?;
// After Chip Hardware Reset6 wait until the Controller Not Ready (CNR) flag
// in the USBSTS is ‘0’ before writing any xHC Operational or Runtime
// registers.
self.chip_hardware_reset().await?;
// Program the Max Device Slots Enabled (MaxSlotsEn) field in the CONFIG
// register (5.4.7) to enable the device slots that system software is going to
// use.

self.reg = XhciRegisters::new(self.mmio_base);

let max_slots = self.setup_max_device_slots();
let root_hub = RootHub::new(max_slots as _, self.reg.clone(), self.dma_mask)?;
root_hub.init()?;
self.root = Some(root_hub);
// trace!("Root hub initialized with max slots: {max_slots}");
self.root()?.wait_for_running().await;

// GL3523 hub workaround: Toggle VBUS after xHCI is running
// This resets the hub so it sees immediate host activity when it powers up.
// Without this, the hub enters standby mode and doesn't respond to Rx.Detect.
self.toggle_vbus_if_rk3588().await;

self.root()?.lock().enable_irq();
self.root()?.lock().reset_ports();

// Additional delay after port reset for device detection stability
// Linux kernel typically waits for device connection stabilization
sleep(Duration::from_millis(100)).await;

Ok(())
}
.boxed_local()
Expand Down Expand Up @@ -119,15 +121,50 @@ impl usb_if::host::Controller for Xhci {
impl Xhci {
pub fn new(mmio_base: NonNull<u8>, dma_mask: usize) -> Box<Self> {
Box::new(Self {
mmio_base,
reg: XhciRegisters::new(mmio_base),
root: None,
port_wake: AtomicWaker::new(),
dma_mask,
})
}

fn init_dwc3_if_needed(&self) {
if unsafe { dwc3::is_dwc3_xhci(self.mmio_base) } {
debug!("Detected DWC3-based XHCI controller");
}
}

/// Toggle VBUS power if this is an RK3588 USB3_1 controller
///
/// This is a workaround for the GL3523 USB hub cold-start issue on Orange Pi 5 Plus.
/// The hub enters a non-responsive standby state if VBUS is present but no USB host
/// activity occurs within ~1-2 seconds. By toggling VBUS after the xHCI controller
/// is running, we reset the hub so it sees immediate host activity when it powers up.
#[cfg(feature = "aggressive_usb_reset")]
async fn toggle_vbus_if_rk3588(&self) {
let base_addr = self.mmio_base.as_ptr() as usize;

if rk3588_phy::is_rk3588_usb3_port1(base_addr) {
debug!("RK3588 USB3_1: Applying GL3523 hub VBUS toggle workaround");

unsafe {
rk3588_phy::toggle_vbus_port1(rk3588_phy::VBUS_OFF_MS, rk3588_phy::VBUS_ON_WAIT_MS);
}

sleep(Duration::from_millis(200)).await;
}
}

/// No-op variant when aggressive_usb_reset feature is disabled
#[cfg(not(feature = "aggressive_usb_reset"))]
async fn toggle_vbus_if_rk3588(&self) {
// VBUS toggle disabled - feature flag not set
}

async fn chip_hardware_reset(&mut self) -> Result {
debug!("Reset begin ...");

self.reg.operational.usbcmd.update_volatile(|c| {
c.clear_run_stop();
});
Expand Down Expand Up @@ -156,7 +193,6 @@ impl Xhci {
}
debug!("Reset finish");

// debug!("Is 64 bit {}", self.is_64_byte());
Ok(())
}

Expand Down
39 changes: 39 additions & 0 deletions usb-host/src/backend/xhci/rk3588_phy.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
//! RK3588 GPIO VBUS control for GL3523 hub workaround.

use core::ptr::write_volatile;

use super::delay::delay_ms;

const GPIO3_BASE: usize = 0xFEC40000;
const GPIO_SWPORT_DR_L: usize = 0x0000;
const GPIO_SWPORT_DDR_L: usize = 0x0008;
const GPIO3_B7_BIT: u32 = 1 << 15;
const WRITE_MASK_BIT15: u32 = 1 << 31;

pub const VBUS_OFF_MS: u32 = 1000;
pub const VBUS_ON_WAIT_MS: u32 = 500;

const RK3588_USB3_PORT1_BASE: usize = 0xFC400000;

pub fn is_rk3588_usb3_port1(xhci_base: usize) -> bool {
xhci_base == RK3588_USB3_PORT1_BASE
}

/// # Safety
/// Direct hardware register access. Caller must ensure this is RK3588 hardware.
pub unsafe fn toggle_vbus_port1(off_ms: u32, on_wait_ms: u32) {
unsafe {
let ddr_ptr = (GPIO3_BASE + GPIO_SWPORT_DDR_L) as *mut u32;
write_volatile(ddr_ptr, WRITE_MASK_BIT15 | GPIO3_B7_BIT);

let dr_ptr = (GPIO3_BASE + GPIO_SWPORT_DR_L) as *mut u32;
write_volatile(dr_ptr, WRITE_MASK_BIT15);
}
delay_ms(off_ms);

unsafe {
let dr_ptr = (GPIO3_BASE + GPIO_SWPORT_DR_L) as *mut u32;
write_volatile(dr_ptr, WRITE_MASK_BIT15 | GPIO3_B7_BIT);
}
delay_ms(on_wait_ms);
}
103 changes: 18 additions & 85 deletions usb-host/src/backend/xhci/root.rs
Original file line number Diff line number Diff line change
Expand Up @@ -279,15 +279,28 @@ impl Root {
let regs = &mut self.reg;
let port_len = regs.port_register_set.len();

// Enable port power for all ports
for i in 0..port_len {
let portsc = regs.port_register_set.read_volatile_at(i).portsc;
if !portsc.port_power() {
regs.port_register_set.update_volatile_at(i, |port| {
port.portsc.set_port_power();
});
for _ in 0..100000 {
core::hint::spin_loop();
}
}
}

// Reset all ports
for i in 0..port_len {
debug!("Port {i} start reset",);
regs.port_register_set.update_volatile_at(i, |port| {
port.portsc.set_0_port_enabled_disabled();
port.portsc.set_port_reset();
});
}

// Wait for port reset completion with proper timing
// Wait for port reset completion
for i in 0..port_len {
let mut timeout = 0;
while regs
Expand All @@ -298,28 +311,23 @@ impl Root {
{
spin_loop();
timeout += 1;
// Add timeout protection to avoid infinite loop
if timeout > 100000 {
debug!("Port {i} reset timeout");
break;
}
}

// After reset completion, wait for device to settle
// This is critical for device detection - Linux kernel waits ~50ms
// Reference: USB 2.0 spec requires minimum 10ms recovery time
// after port reset, but some devices need more time
// Wait for device to settle after reset
// USB 2.0 spec requires minimum 10ms recovery time
let mut delay_count = 0;
while delay_count < 50000 {
// ~50ms at typical CPU speeds
spin_loop();
delay_count += 1;
}

debug!("Port {i} reset completed, checking status");
let portsc = regs.port_register_set.read_volatile_at(i).portsc;
debug!(
"Port {i} status after reset: enabled={}, connected={}, speed={}",
"Port {i}: enabled={}, connected={}, speed={}",
portsc.port_enabled_disabled(),
portsc.current_connect_status(),
portsc.port_speed()
Expand Down Expand Up @@ -354,81 +362,6 @@ impl Root {
.portsc
.port_speed()
}

/// 解析端口速度 ID 为实际速度 (Mbps)
#[allow(dead_code)]
pub fn parse_port_speed_to_mbps(&self, port: PortId) -> Option<u32> {
let speed_id = self.port_speed(port);
if speed_id == 0 {
return None; // PSIV 值 0 是保留的
}

// TODO: 从 xHCI Extended Capabilities 中查找对应的 PSI
// 现在先返回常见速度的硬编码值
match speed_id {
1 => Some(12), // USB 1.x Full Speed (12 Mbps)
2 => Some(480), // USB 2.0 High Speed (480 Mbps)
3 => Some(5000), // USB 3.0 SuperSpeed (5 Gbps = 5000 Mbps)
4 => Some(10000), // USB 3.1 SuperSpeedPlus (10 Gbps)
5 => Some(20000), // USB 3.2 SuperSpeedPlus (20 Gbps)
_ => None,
}
}

/// 获取端口速度的描述字符串
#[allow(dead_code)]
pub fn port_speed_description(&self, port: PortId) -> &'static str {
let speed_id = self.port_speed(port);
match speed_id {
0 => "Unknown",
1 => "Full Speed (12 Mbps)",
2 => "High Speed (480 Mbps)",
3 => "SuperSpeed (5 Gbps)",
4 => "SuperSpeedPlus (10 Gbps)",
5 => "SuperSpeedPlus (20 Gbps)",
_ => "Reserved/Unknown",
}
}

/// 根据端口速度计算默认控制端点的最大包大小
/// 基于 USB 规范和 xHCI 规范 4.3.3 Address Assignment
#[allow(dead_code)]
pub fn calculate_default_max_packet_size(&self, port: PortId) -> u16 {
let speed_id = self.port_speed(port);
match speed_id {
0 => 8, // Unknown/Reserved - 使用最小值
1 => 64, // Full Speed (USB 1.1): 8, 16, 32, 或 64 字节
// 规范建议初始化时使用 64,后续通过设备描述符确认实际值
2 => 64, // High Speed (USB 2.0): 固定 64 字节
3 => 512, // SuperSpeed (USB 3.0): 固定 512 字节
4 => 512, // SuperSpeedPlus (USB 3.1): 固定 512 字节
5 => 512, // SuperSpeedPlus (USB 3.2): 固定 512 字节
_ => 8, // 其他/未知速度 - 使用最保守的值
}
}

/// 获取特定速度的详细包大小信息
#[allow(dead_code)]
pub fn get_packet_size_info(&self, port: PortId) -> (u16, &'static str) {
let speed_id = self.port_speed(port);
match speed_id {
0 => (8, "Unknown speed, using minimum"),
1 => (64, "Full Speed: 8/16/32/64 bytes possible, using 64"),
2 => (64, "High Speed: fixed 64 bytes"),
3 => (512, "SuperSpeed: fixed 512 bytes"),
4 => (512, "SuperSpeedPlus 10G: fixed 512 bytes"),
5 => (512, "SuperSpeedPlus 20G: fixed 512 bytes"),
_ => (8, "Reserved/Unknown speed, using minimum"),
}
}

// pub fn is_64_byte(&self) -> bool {
// self.reg
// .capability
// .hccparams1
// .read_volatile()
// .addressing_capability()
// }
}

#[derive(Clone)]
Expand Down
2 changes: 1 addition & 1 deletion usb-host/src/err.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ impl From<dma_api::DError> for HostError {
fn from(value: dma_api::DError) -> Self {
match value {
dma_api::DError::NoMemory => Self(USBError::NoMemory),
dma_api::DError::DmaMaskNotMatch { mask, got } => Self(USBError::NoMemory),
dma_api::DError::DmaMaskNotMatch { mask: _, got: _ } => Self(USBError::NoMemory),
dma_api::DError::LayoutError => Self(USBError::NoMemory),
}
}
Expand Down
1 change: 0 additions & 1 deletion usb-host/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
#![cfg_attr(not(feature = "libusb"), no_std)]
#![feature(iterator_try_collect)]

#[macro_use]
extern crate alloc;
#[macro_use]
extern crate log;
Expand Down
Loading