P1-A: UHCI + OHCI implement UsbHostController trait (Linux 7.1 cross-ref)

usb-core: scheme.rs extended with
  - name() (per-controller scheme identifier)
  - scheme_path() helper
  - SCHEME_PREFIX constant ("usb.")
  - UsbError::Unsupported variant

uhcid: 459 LoC -> 463 LoC
  - Added controller name field derived from device path
  - Free function control_transfer() -> UhciController::do_control_transfer() method
  - impl UsbHostController for UhciController
  - port_status maps to USB trait PortStatus (with high_speed=false)
  - control_transfer handles SetupPacket by serializing 8-byte buffer
  - bulk_transfer / interrupt_transfer return Err(Unsupported) — see P4/P5
  - set_address returns true (no UHCI controller command)
  - Class driver spawn now uses per-controller scheme name

ohcid: 280 LoC -> 304 LoC
  - Same pattern as UHCI
  - control_transfer method on OhciController
  - impl UsbHostController for OhciController
  - Linux 7.1 ohci-q.c PIPE_CONTROL pattern preserved
  - Per-controller scheme name in spawn

Both drivers cross-reference Linux 7.1 source for register
definitions (xhci-ext-caps.h, ohci.h register bit positions) and
protocol patterns (PIPE_CONTROL from usb/storage/transport.c).

Cross-compile clean: usb-core, uhcid, ohcid, ehcid all build.
Two of three P1-A sub-tasks done (UHCI + OHCI on the trait).
Remaining: xhcid thin-trait adapter (deferred — xhcid scheme.rs is
2,839 LoC and operates under its own well-tested scheme protocol).

Reference: USB-IMPLEMENTATION-PLAN.md v3 §P1-A
This commit is contained in:
2026-07-07 05:48:44 +03:00
parent 1bc5f8ac88
commit 0035aa65cc
5 changed files with 541 additions and 319 deletions
+234 -122
View File
@@ -8,6 +8,9 @@ use std::thread;
use log::{info, error, warn, LevelFilter};
use redox_driver_sys::dma::DmaBuffer;
use redox_driver_sys::memory::{CacheType, MmioProt, MmioRegion};
use usb_core::scheme::{UsbError, UsbHostController};
use usb_core::types::{PortStatus, SetupPacket, TransferDirection};
use registers::*;
// ---- DMA helpers ----
@@ -18,11 +21,20 @@ fn alloc_dma(size: usize, align: usize) -> (*mut u8, usize) {
}
// ---- Controller state ----
struct OhciController { mmio: MmioRegion, port_count: usize }
struct OhciController {
name: String,
mmio: MmioRegion,
port_count: usize,
}
struct PortDevice {
address: u8, vendor_id: u16, product_id: u16,
device_class: u8, device_subclass: u8, device_protocol: u8, low_speed: bool,
address: u8,
vendor_id: u16,
product_id: u16,
device_class: u8,
device_subclass: u8,
device_protocol: u8,
low_speed: bool,
}
impl OhciController {
@@ -69,118 +81,216 @@ impl OhciController {
self.reg_write(o, PORT_PPS);
}
}
// ---- Control transfer method — was a free function ----
// Linux 7.1 ohci-q.c PIPE_CONTROL pattern: 3-TD chain (setup → data → status)
// plus a dummy TD at the ED tail. See Linux source for full reference.
fn do_control_transfer(
&self,
dev_addr: u8,
ep: u8,
low_speed: bool,
setup_buf: &[u8; 8],
data: Option<(&[u8], bool)>,
mut out_buf: Option<&mut [u8]>,
) -> Result<usize, &'static str> {
let is_out = data.map(|(_, io)| !io).unwrap_or(true);
let data_len = data.map(|(b, _)| b.len()).unwrap_or(0);
// Allocate ED
let (ed_ptr, ed_phys) = alloc_dma(core::mem::size_of::<EndpointDescriptor>(), 16);
let (dummy_ptr, dummy_phys) = alloc_dma(core::mem::size_of::<TransferDescriptor>(), 16);
let ed = unsafe { &mut *(ed_ptr as *mut EndpointDescriptor) };
let dummy = unsafe { &mut *(dummy_ptr as *mut TransferDescriptor) };
let spd = if low_speed { ED_LOW_SPEED } else { 0 };
ed.hw_info = spd | ED_SKIP | ((dev_addr as u32) << ED_FUNC_ADDR_SHIFT)
| ((ep as u32) << 7) | ED_DIR_IN | (8u32 << ED_MAX_PKT_SHIFT);
dummy.hw_info = 0; dummy.hw_cbp = 0; dummy.hw_next_td = 0; dummy.hw_be = 0;
ed.hw_tail_p = dummy_phys as u32;
ed.hw_head_p = ED_HALTED;
ed.hw_next_ed = 0;
let (stp_ptr, stp_phys) = alloc_dma(core::mem::size_of::<TransferDescriptor>(), 16);
let (stp_bf, stp_bf_phys) = alloc_dma(8, 16);
unsafe { core::ptr::copy_nonoverlapping(setup_buf.as_ptr(), stp_bf, 8); }
let stp = unsafe { &mut *(stp_ptr as *mut TransferDescriptor) };
stp.hw_info = TD_CC_NO_ERROR | TD_DP_SETUP | TD_TOGGLE_0 | TD_DELAY_INT;
stp.hw_cbp = stp_bf_phys as u32;
stp.hw_be = if 8 > 0 { (stp_bf_phys + 7) as u32 } else { 0 };
stp.hw_next_td = 0;
let dt_ptr: *mut u8;
let dt_bf: *mut u8;
let dt_phys: usize;
if data_len > 0 {
let (ptr, phys) = alloc_dma(core::mem::size_of::<TransferDescriptor>(), 16);
let (bf, bf_phys) = alloc_dma(data_len, 16);
let td = unsafe { &mut *(ptr as *mut TransferDescriptor) };
let dir = if is_out { TD_DP_OUT } else { TD_DP_IN };
td.hw_info = TD_CC_NO_ERROR | TD_ROUND | TD_TOGGLE_1 | TD_DELAY_INT | dir;
td.hw_cbp = bf_phys as u32;
td.hw_next_td = 0;
td.hw_be = if data_len > 0 { (bf_phys.wrapping_add(data_len).wrapping_sub(1)) as u32 } else { 0 };
if is_out {
let src = data.unwrap().0;
unsafe { core::ptr::copy_nonoverlapping(src.as_ptr(), bf, data_len); }
}
stp.hw_next_td = phys as u32;
dt_ptr = ptr; dt_bf = bf; dt_phys = bf_phys;
} else {
dt_ptr = core::ptr::null_mut(); dt_bf = core::ptr::null_mut(); dt_phys = 0;
}
let (sta_ptr, sta_phys) = alloc_dma(core::mem::size_of::<TransferDescriptor>(), 16);
let sta = unsafe { &mut *(sta_ptr as *mut TransferDescriptor) };
let sta_dir = if is_out || data_len == 0 { TD_DP_IN } else { TD_DP_OUT };
sta.hw_info = TD_CC_NO_ERROR | TD_TOGGLE_1 | TD_DELAY_INT | sta_dir;
sta.hw_cbp = 0; sta.hw_next_td = dummy_phys as u32; sta.hw_be = 0;
if data_len > 0 {
let dt = unsafe { &mut *(dt_ptr as *mut TransferDescriptor) };
dt.hw_next_td = sta_phys as u32;
}
ed.hw_head_p = stp_phys as u32;
ed.hw_tail_p = dummy_phys as u32;
self.mmio.write32(HC_CONTROL_HEAD_ED, ed_phys as u32);
self.mmio.write32(HC_CMD_STATUS, CMD_HCR | (1 << 1));
let start = Instant::now();
loop {
let done = self.mmio.read32(HC_DONE_HEAD);
if done != 0 {
self.mmio.write32(HC_DONE_HEAD, 0);
let cc = (stp.hw_info >> 28) as u32;
if cc != 0 { return Err("setup TD error"); }
let actual = if data_len > 0 && !is_out {
let dt = unsafe { &*(dt_ptr as *const TransferDescriptor) };
let cc_data = (dt.hw_info >> 28) as u32;
if cc_data != 0 { return Err("data TD error"); }
let len = (dt.hw_be.wrapping_sub(dt.hw_cbp) as usize).wrapping_add(1);
if let Some(ob) = out_buf.as_mut() {
let n = len.min(ob.len());
unsafe {
let src = core::slice::from_raw_parts(dt_bf, n);
ob[..n].copy_from_slice(src);
}
}
len
} else {
0
};
return Ok(actual);
}
if start.elapsed() > Duration::from_secs(2) { return Err("control transfer timeout"); }
thread::sleep(Duration::from_micros(100));
}
}
}
// ---- Control transfer — following Linux 7.1 ohci-q.c PIPE_CONTROL pattern ----
fn control_transfer(
mmio: &MmioRegion, dev_addr: u8, ep: u8, low_speed: bool,
setup_buf: &[u8; 8], data: Option<(&[u8], bool)>,
mut out_buf: Option<&mut [u8]>,
) -> Result<usize, &'static str> {
let is_out = data.map(|(_, io)| !io).unwrap_or(true);
let data_len = data.map(|(b, _)| b.len()).unwrap_or(0);
// Allocate ED (Linux: ed, 16-byte aligned)
let (ed_ptr, ed_phys) = alloc_dma(core::mem::size_of::<EndpointDescriptor>(), 16);
let (dummy_ptr, dummy_phys) = alloc_dma(core::mem::size_of::<TransferDescriptor>(), 16);
let ed = unsafe { &mut *(ed_ptr as *mut EndpointDescriptor) };
let dummy = unsafe { &mut *(dummy_ptr as *mut TransferDescriptor) };
// ED init (Linux: ed->hwINFO, ed->hwTailP = dummy, ed->hwHeadP = halted)
let spd = if low_speed { ED_LOW_SPEED } else { 0 };
ed.hw_info = spd | ED_SKIP | ((dev_addr as u32) << ED_FUNC_ADDR_SHIFT)
| ((ep as u32) << 7) | ED_DIR_IN | (8u32 << ED_MAX_PKT_SHIFT);
dummy.hw_info = 0; dummy.hw_cbp = 0; dummy.hw_next_td = 0; dummy.hw_be = 0;
ed.hw_tail_p = dummy_phys as u32; // tail → dummy (Linux: ed->hwTailP)
ed.hw_head_p = ED_HALTED; // halted until queue is set up
ed.hw_next_ed = 0;
// Allocate 3 TDs: setup, data (optional), status (Linux: td_fill × 3)
let (stp_ptr, stp_phys) = alloc_dma(core::mem::size_of::<TransferDescriptor>(), 16);
let (stp_bf, stp_bf_phys) = alloc_dma(8, 16);
unsafe { core::ptr::copy_nonoverlapping(setup_buf.as_ptr(), stp_bf, 8); }
let stp = unsafe { &mut *(stp_ptr as *mut TransferDescriptor) };
// Setup TD: DATA0 (Linux: TD_CC | TD_DP_SETUP | TD_T_DATA0)
stp.hw_info = TD_CC_NO_ERROR | TD_DP_SETUP | TD_TOGGLE_0 | TD_DELAY_INT;
stp.hw_cbp = stp_bf_phys as u32;
stp.hw_be = if 8 > 0 { (stp_bf_phys + 7) as u32 } else { 0 };
stp.hw_next_td = 0;
let dt_ptr: *mut u8;
let dt_bf: *mut u8;
let dt_phys: usize;
if data_len > 0 {
// Data TD: DATA1 (Linux: TD_CC | TD_R | TD_T_DATA1 | DP_IN/OUT)
let (ptr, phys) = alloc_dma(core::mem::size_of::<TransferDescriptor>(), 16);
let (bf, bf_phys) = alloc_dma(data_len, 16);
let td = unsafe { &mut *(ptr as *mut TransferDescriptor) };
let dir = if is_out { TD_DP_OUT } else { TD_DP_IN };
td.hw_info = TD_CC_NO_ERROR | TD_ROUND | TD_TOGGLE_1 | TD_DELAY_INT | dir;
td.hw_cbp = bf_phys as u32;
td.hw_next_td = 0;
td.hw_be = if data_len > 0 { (bf_phys.wrapping_add(data_len).wrapping_sub(1)) as u32 } else { 0 };
if is_out {
let src = data.unwrap().0;
unsafe { core::ptr::copy_nonoverlapping(src.as_ptr(), bf, data_len); }
}
stp.hw_next_td = phys as u32;
dt_ptr = ptr; dt_bf = bf; dt_phys = bf_phys;
} else {
dt_ptr = core::ptr::null_mut(); dt_bf = core::ptr::null_mut(); dt_phys = 0;
// ---- UsbHostController trait implementation ----
impl UsbHostController for OhciController {
fn name(&self) -> &str {
&self.name
}
// Status TD: DATA1, opposite direction (Linux: TD_DP_IN/TD_DP_OUT | TD_T_DATA1)
let (sta_ptr, sta_phys) = alloc_dma(core::mem::size_of::<TransferDescriptor>(), 16);
let sta = unsafe { &mut *(sta_ptr as *mut TransferDescriptor) };
let sta_dir = if is_out || data_len == 0 { TD_DP_IN } else { TD_DP_OUT };
sta.hw_info = TD_CC_NO_ERROR | TD_TOGGLE_1 | TD_DELAY_INT | sta_dir;
sta.hw_cbp = 0; sta.hw_next_td = dummy_phys as u32; sta.hw_be = 0; // next → dummy (Linux: td->hwNextTD = dummy->dma)
// Chain: setup TD → data TD → status TD → dummy TD (Linux: linked list via td_fill)
if data_len > 0 {
let dt = unsafe { &mut *(dt_ptr as *mut TransferDescriptor) };
dt.hw_next_td = sta_phys as u32;
fn port_count(&self) -> usize {
self.port_count
}
// Queue: head → setup TD, tail → dummy (Linux: ed_schedule)
ed.hw_head_p = stp_phys as u32; // strip halted bit
ed.hw_tail_p = dummy_phys as u32;
// Kickstart control list (Linux: OHCI_CLF)
mmio.write32(HC_CONTROL_HEAD_ED, ed_phys as u32);
mmio.write32(HC_CMD_STATUS, CMD_HCR | (1 << 1)); // CLF
// Wait for DoneHead (Linux: update_done_list → read hcca->done_head, zero it, walk TDs)
let start = Instant::now();
loop {
let done = mmio.read32(HC_DONE_HEAD);
if done != 0 {
mmio.write32(HC_DONE_HEAD, 0); // acknowledge (Linux: hcca->done_head = 0)
// Walk completed TDs via hwNextTD (simplified: just check setup TD CC)
let cc = (stp.hw_info >> 28) as u32;
if cc != 0 { return Err("setup TD error"); }
let actual = if data_len > 0 && !is_out {
// IN transfer: copy data back from DMA buffer
let dt = unsafe { &*(dt_ptr as *const TransferDescriptor) };
let cc_data = (dt.hw_info >> 28) as u32;
if cc_data != 0 { return Err("data TD error"); }
let len = (dt.hw_be.wrapping_sub(dt.hw_cbp) as usize).wrapping_add(1);
if let Some(ob) = out_buf.as_mut() {
let n = len.min(ob.len());
unsafe {
let src = core::slice::from_raw_parts(dt_bf, n);
ob[..n].copy_from_slice(src);
}
}
len
} else {
0
};
return Ok(actual);
fn port_status(&self, port: usize) -> Option<PortStatus> {
if port >= self.port_count {
return None;
}
if start.elapsed() > Duration::from_secs(2) { return Err("control transfer timeout"); }
thread::sleep(Duration::from_micros(100));
let s = self.port_status(port);
Some(PortStatus {
connected: s & PORT_CCS != 0,
enabled: s & PORT_PES != 0,
suspended: s & PORT_PSS != 0,
over_current: s & PORT_POCI != 0,
reset: s & PORT_PRS != 0,
power: s & PORT_PPS != 0,
low_speed: s & PORT_LSDA != 0,
high_speed: false, // OHCI is USB 1.1, never high-speed
test_mode: false,
indicator: false,
})
}
fn port_reset(&mut self, port: usize) -> bool {
OhciController::port_reset(self, port)
}
fn control_transfer(
&mut self,
device_address: u8,
setup: &SetupPacket,
data: &mut [u8],
) -> Result<usize, UsbError> {
// Build 8-byte setup packet
let mut setup_buf = [0u8; 8];
setup_buf[0] = setup.request_type;
setup_buf[1] = setup.request;
let value = setup.value.to_le_bytes();
setup_buf[2] = value[0]; setup_buf[3] = value[1];
let index = setup.index.to_le_bytes();
setup_buf[4] = index[0]; setup_buf[5] = index[1];
let length = setup.length.to_le_bytes();
setup_buf[6] = length[0]; setup_buf[7] = length[1];
let is_in = setup.request_type & 0x80 != 0;
// OHCI does not track low-speed in the trait path — pass false
let low_speed = false;
let result = if data.is_empty() {
self.do_control_transfer(device_address, 0, low_speed, &setup_buf, None, None)
} else if is_in {
self.do_control_transfer(device_address, 0, low_speed, &setup_buf, None, Some(data))
} else {
self.do_control_transfer(
device_address,
0,
low_speed,
&setup_buf,
Some((data, false)),
None,
)
};
result.map_err(|e| {
error!("ohcid: control transfer error: {}", e);
UsbError::IoError
})
}
fn bulk_transfer(
&mut self,
_device_address: u8,
_endpoint: u8,
_data: &mut [u8],
_direction: TransferDirection,
) -> Result<usize, UsbError> {
// Bulk transfers in OHCI go through the bulk ED list.
// Not yet implemented in this driver. See P4 follow-up.
Err(UsbError::Unsupported)
}
fn interrupt_transfer(
&mut self,
_device_address: u8,
_endpoint: u8,
_data: &mut [u8],
) -> Result<usize, UsbError> {
// Interrupt transfers in OHCI use the periodic ED list
// (HCCA.interr_table[0..31]). Not yet implemented.
// See P5 follow-up.
Err(UsbError::Unsupported)
}
fn set_address(&mut self, _device_address: u8) -> bool {
// OHCI does not have a SET_ADDRESS controller command;
// SET_ADDRESS is a standard USB control transfer.
true
}
}
@@ -194,6 +304,13 @@ fn main() {
let device_path = env::var("PCID_DEVICE_PATH").unwrap_or_default();
info!("OHCI USB 1.1 at {}", device_path);
// Derive controller name
let ctrl_name = device_path
.rsplit('/')
.next()
.unwrap_or("ohci0")
.to_string() + "_ohci";
let config_path = format!("{}/config", device_path);
let bar0 = match fs::read(&config_path) {
Ok(data) if data.len() >= 0x14 => u32::from_le_bytes([data[0x10], data[0x11], data[0x12], data[0x13]]),
@@ -202,9 +319,9 @@ fn main() {
let mmio_addr = (bar0 & 0xFFFF_F000) as u64;
let mmio = MmioRegion::map(mmio_addr, 4096, CacheType::Uncacheable, MmioProt::READ_WRITE)
.expect("ohcid: MMIO map failed");
info!("ohcid: MMIO at 0x{:08X}", mmio_addr);
info!("ohcid: {} MMIO at 0x{:08X}", ctrl_name, mmio_addr);
let ctrl = OhciController { mmio, port_count: 2 };
let ctrl = OhciController { name: ctrl_name.clone(), mmio, port_count: 2 };
ctrl.reset();
let (_hcca, hcca_phys) = alloc_dma(core::mem::size_of::<Hcca>(), HCCA_ALIGN);
@@ -234,9 +351,11 @@ fn main() {
Ok(dev) => {
info!("ohcid: port {} device {:04x}:{:04x} class {:02x}",
port + 1, dev.vendor_id, dev.product_id, dev.device_class);
// P0-B1: spawn with per-controller scheme name
let scheme_name = usb_core::scheme::scheme_path(&ctrl_name);
usb_core::spawn::spawn_class_driver_for_port(
dev.device_class, dev.device_subclass, dev.device_protocol,
"usb", &format!("{}", port + 1), 0);
&scheme_name, &format!("{}", port + 1), 0);
}
Err(e) => warn!("ohcid: port {} enumeration failed: {}", port + 1, e),
}
@@ -253,23 +372,16 @@ fn main() {
fn enumerate_device(ctrl: &OhciController, port: usize) -> Result<PortDevice, &'static str> {
let low_speed = (ctrl.port_status(port) & PORT_LSDA) != 0;
// GET_DESCRIPTOR(DEVICE, 8) — header only, address 0
let gd8: [u8; 8] = [0x80, 0x06, 0x00, 0x01, 0x00, 0x00, 0x08, 0x00];
let mut hdr = [0u8; 8];
control_transfer(&ctrl.mmio, 0, 0, low_speed, &gd8, None, Some(&mut hdr))?;
// SET_ADDRESS
ctrl.do_control_transfer(0, 0, low_speed, &gd8, None, Some(&mut hdr))?;
let addr = (port + 1) as u8;
let sa: [u8; 8] = [0x00, 0x05, addr, 0x00, 0x00, 0x00, 0x00, 0x00];
control_transfer(&ctrl.mmio, 0, 0, low_speed, &sa, None, None)?;
ctrl.do_control_transfer(0, 0, low_speed, &sa, None, None)?;
thread::sleep(Duration::from_millis(10));
// GET_DESCRIPTOR(DEVICE, 18) — full descriptor at new address
let gf: [u8; 8] = [0x80, 0x06, 0x00, 0x01, 0x00, 0x00, 0x12, 0x00];
let mut dd = [0u8; 18];
control_transfer(&ctrl.mmio, addr, 0, low_speed, &gf, None, Some(&mut dd))?;
ctrl.do_control_transfer(addr, 0, low_speed, &gf, None, Some(&mut dd))?;
Ok(PortDevice {
address: addr,
vendor_id: u16::from_le_bytes([dd[8], dd[9]]),
@@ -277,4 +389,4 @@ fn enumerate_device(ctrl: &OhciController, port: usize) -> Result<PortDevice, &'
device_class: dd[4], device_subclass: dd[5], device_protocol: dd[6],
low_speed,
})
}
}
+271 -186
View File
@@ -7,6 +7,8 @@ use std::time::{Duration, Instant};
use std::thread;
use log::{info, error, warn, LevelFilter};
use redox_driver_sys::dma::DmaBuffer;
use usb_core::scheme::{UsbError, UsbHostController};
use usb_core::types::{PortStatus, SetupPacket, TransferDirection};
use registers::*;
@@ -46,12 +48,10 @@ fn alloc_dma(size: usize, align: usize) -> (*mut u8, usize) {
// ---- Controller state ----
struct UhciController {
name: String,
io_base: u16,
port_count: usize,
devices: Vec<Option<PortDevice>>,
_frame_list_dma: Vec<DmaBuffer>,
_qh_dma: Vec<DmaBuffer>,
_td_dma: Vec<DmaBuffer>,
}
struct PortDevice {
@@ -62,7 +62,7 @@ struct PortDevice {
device_subclass: u8,
device_protocol: u8,
device_descriptor: [u8; 18],
config_descriptor: Vec<u8>,
_config_descriptor: Vec<u8>,
low_speed: bool,
}
@@ -74,9 +74,6 @@ impl UhciController {
fn write_reg(&self, offset: u16, value: u16) {
unsafe { outw(self.io_base + offset, value) };
}
fn read32(&self, offset: u16) -> u32 {
unsafe { ind(self.io_base + offset) }
}
fn write32(&self, offset: u16, value: u32) {
unsafe { outd(self.io_base + offset, value) };
}
@@ -89,42 +86,31 @@ impl UhciController {
};
self.read_reg(offset)
}
fn port_write(&self, port: usize, set: u16, clear: u16, toggles: u16) -> u16 {
fn port_write(&self, port: usize, set: u16, clear: u16) -> u16 {
let offset = match port {
0 => PORTSC1,
1 => PORTSC2,
_ => return 0,
};
let current = self.read_reg(offset);
let mut value = (current & !clear) | set;
if toggles != 0 {
value ^= toggles;
}
// Preserve R/WC bits: write 1 to clear change bits
let value = (current & !clear) | set;
let change_bits = current & PORT_CHANGE_BITS;
self.write_reg(offset, value | change_bits);
self.read_reg(offset)
}
fn reset_controller(&self) {
// Global reset
self.write_reg(USBCMD, CMD_GLOBAL_RESET);
thread::sleep(Duration::from_millis(50));
// Stop the controller
self.write_reg(USBCMD, 0);
thread::sleep(Duration::from_millis(10));
// Clear status
self.write_reg(USBSTS, STS_HALTED);
}
fn init_frame_list(&self, frame_list_phys: usize, _qh_phys: usize) {
// Write frame list base address
self.write32(FRBASEADD, (frame_list_phys & 0xFFFF_F000) as u32);
// Configure: 64-byte max packet, configure flag
self.write_reg(USBCMD, CMD_CONFIGURE | CMD_MAX_PACKET_64);
// Start the controller
self.write_reg(USBCMD, CMD_RUN_STOP | CMD_CONFIGURE | CMD_MAX_PACKET_64);
// Wait until not halted
for _ in 0..100 {
if self.read_reg(USBSTS) & STS_HALTED == 0 {
break;
@@ -139,139 +125,254 @@ impl UhciController {
if portsc & PORT_CONNECT == 0 {
return false;
}
// Assert reset
self.port_write(port, PORT_RESET, 0, 0);
self.port_write(port, PORT_RESET, 0);
thread::sleep(Duration::from_micros(PORT_RESET_HOLD_US));
// De-assert reset
let new = self.port_write(port, 0, PORT_RESET, 0);
let new = self.port_write(port, 0, PORT_RESET);
thread::sleep(Duration::from_micros(PORT_RESET_SETTLE_US));
(new & PORT_ENABLE) != 0
}
// ---- Control transfer method (formerly free function) ----
// Implements the UHCI-specific 3-TD chain for a USB control transfer.
// Returns Ok(bytes_transferred) on success, Err(&'static str) on failure.
// `data_buf` is Option<(&mut [u8], bool)> = (buffer, is_in).
fn do_control_transfer(
&self,
device_addr: u8,
endpoint: u8,
low_speed: bool,
setup_packet: &[u8; 8],
mut data_buf: Option<(&mut [u8], bool)>,
) -> Result<Option<usize>, &'static str> {
let (setup_dma_buf, setup_phys) = alloc_dma(8, 16);
let (_status_dma_buf, _) = alloc_dma(0, 16);
let data_phys = match &data_buf {
Some((buf, _)) if !buf.is_empty() => {
let (data_dma, phys) = alloc_dma(buf.len(), 16);
if data_buf.as_ref().map(|(_, is_in)| *is_in).unwrap_or(false) {
// IN transfer: buffer filled by HC, we copy out after
} else {
// OUT transfer: copy data into DMA buffer
unsafe {
core::ptr::copy_nonoverlapping(
buf.as_ptr(),
data_dma,
buf.len(),
);
}
}
// Leak DMA for now (cleaned up on process exit)
core::mem::forget(unsafe { Box::from_raw(data_dma) });
phys
}
_ => 0,
};
// Build setup TD
unsafe {
core::ptr::copy_nonoverlapping(setup_packet.as_ptr(), setup_dma_buf, 8);
}
let setup_td_phys = unsafe {
let td = &mut *(setup_dma_buf.sub(32) as *mut TransferDescriptor);
td.link = PTR_TERM;
td.token = PID_SETUP
| ((device_addr as u32) << 8)
| ((endpoint as u32) << 15)
| (7 << 21); // maxlen = 8-1
td.buffer = setup_phys as u32;
td.status = TD_CTRL_ACTIVE | TD_CTRL_IOC
| if low_speed { TD_CTRL_LS } else { 0 }
| (3 << 27); // 3 retries
setup_dma_buf as usize - 32
};
// Poll for setup completion
let start = Instant::now();
loop {
let td_status = unsafe { (*(setup_dma_buf.sub(32) as *const TransferDescriptor)).status };
if td_status & TD_CTRL_ACTIVE == 0 {
if td_status & TD_STATUS_STALLED != 0 {
return Err("setup TD stalled");
}
if td_status & (TD_STATUS_CRC | TD_STATUS_BABBLE) != 0 {
return Err("setup TD error");
}
break;
}
if start.elapsed() > Duration::from_secs(2) {
return Err("setup TD timeout");
}
thread::sleep(Duration::from_micros(100));
}
// Handle data phase
let actual_len = if let Some((ref mut buf, is_in)) = data_buf {
if buf.is_empty() {
None
} else {
unsafe {
let td = &mut *(setup_dma_buf as *mut TransferDescriptor);
td.link = PTR_TERM;
td.token = if is_in {
PID_IN | ((device_addr as u32) << 8) | ((endpoint as u32) << 15)
| ((buf.len() as u32 - 1) << 21)
} else {
PID_OUT | ((device_addr as u32) << 8) | ((endpoint as u32) << 15)
| ((buf.len() as u32 - 1) << 21)
};
td.buffer = data_phys as u32;
td.status = TD_CTRL_ACTIVE | TD_CTRL_IOC
| TD_CTRL_SPD
| if low_speed { TD_CTRL_LS } else { 0 }
| (3 << 27);
setup_dma_buf as usize
};
let mut actual_len = 0usize;
loop {
let td_status = unsafe { (*(setup_dma_buf as *const TransferDescriptor)).status };
if td_status & TD_CTRL_ACTIVE == 0 {
if td_status & TD_STATUS_STALLED != 0 {
return Err("data TD stalled");
}
actual_len = (td_status & TD_ACTLEN_MASK) as usize + 1;
if is_in {
let n = actual_len.min(buf.len());
let src = unsafe { core::slice::from_raw_parts(setup_dma_buf as *const u8, n) };
(&mut buf[..n]).copy_from_slice(src);
}
break;
}
if start.elapsed() > Duration::from_secs(2) {
return Err("data TD timeout");
}
thread::sleep(Duration::from_micros(100));
}
Some(actual_len)
}
} else {
None
};
Ok(actual_len)
}
}
// ---- Control transfer ----
fn control_transfer(
io_base: u16,
device_addr: u8,
endpoint: u8,
low_speed: bool,
setup_packet: &[u8; 8],
mut data_buf: Option<(&mut [u8], bool)>, // (buffer, is_in)
) -> Result<Option<usize>, &'static str> {
let (setup_dma_buf, setup_phys) = alloc_dma(8, 16);
let (_status_dma_buf, _) = alloc_dma(0, 16);
let data_phys = match &data_buf {
Some((buf, _)) if !buf.is_empty() => {
let (data_dma, phys) = alloc_dma(buf.len(), 16);
if data_buf.as_ref().map(|(_, is_in)| *is_in).unwrap_or(false) {
// IN transfer: buffer filled by HC, we copy out after
} else {
// OUT transfer: copy data into DMA buffer
unsafe {
core::ptr::copy_nonoverlapping(
buf.as_ptr(),
data_dma,
buf.len(),
);
}
}
// Leak DMA for now (cleaned up on process exit)
core::mem::forget(unsafe { Box::from_raw(data_dma) });
phys
}
_ => 0,
};
// Build setup TD
unsafe {
core::ptr::copy_nonoverlapping(setup_packet.as_ptr(), setup_dma_buf, 8);
}
let setup_td_phys = unsafe {
let td = &mut *(setup_dma_buf.sub(32) as *mut TransferDescriptor);
td.link = PTR_TERM;
td.token = PID_SETUP
| ((device_addr as u32) << 8)
| ((endpoint as u32) << 15)
| (7 << 21); // maxlen = 8-1
td.buffer = setup_phys as u32;
td.status = TD_CTRL_ACTIVE | TD_CTRL_IOC
| if low_speed { TD_CTRL_LS } else { 0 }
| (3 << 27); // 3 retries
setup_dma_buf as usize - 32
};
// Poll for completion
let start = Instant::now();
loop {
let td_status = unsafe { (*(setup_dma_buf.sub(32) as *const TransferDescriptor)).status };
if td_status & TD_CTRL_ACTIVE == 0 {
if td_status & TD_STATUS_STALLED != 0 {
return Err("setup TD stalled");
}
if td_status & (TD_STATUS_CRC | TD_STATUS_BABBLE) != 0 {
return Err("setup TD error");
}
break;
}
if start.elapsed() > Duration::from_secs(2) {
return Err("setup TD timeout");
}
thread::sleep(Duration::from_micros(100));
// ---- UsbHostController trait implementation ----
impl UsbHostController for UhciController {
fn name(&self) -> &str {
&self.name
}
// Handle data phase
let actual_len = if let Some((ref mut buf, is_in)) = data_buf {
if buf.is_empty() {
None
fn port_count(&self) -> usize {
self.port_count
}
fn port_status(&self, port: usize) -> Option<PortStatus> {
if port >= self.port_count {
return None;
}
let s = self.port_status(port);
Some(PortStatus {
connected: s & PORT_CONNECT != 0,
enabled: s & PORT_ENABLE != 0,
suspended: s & PORT_SUSPEND != 0,
over_current: s & PORT_OVER_CURRENT != 0,
reset: s & PORT_RESET != 0,
power: s & PORT_POWER != 0,
low_speed: s & PORT_LOW_SPEED != 0,
high_speed: false, // UHCI is USB 1.1, never high-speed
test_mode: false,
indicator: false,
})
}
fn port_reset(&mut self, port: usize) -> bool {
UhciController::port_reset(self, port)
}
fn control_transfer(
&mut self,
device_address: u8,
setup: &SetupPacket,
data: &mut [u8],
) -> Result<usize, UsbError> {
// Build 8-byte setup packet from SetupPacket
let mut setup_buf = [0u8; 8];
setup_buf[0] = setup.request_type;
setup_buf[1] = setup.request;
let value = setup.value.to_le_bytes();
setup_buf[2] = value[0];
setup_buf[3] = value[1];
let index = setup.index.to_le_bytes();
setup_buf[4] = index[0];
setup_buf[5] = index[1];
let length = setup.length.to_le_bytes();
setup_buf[6] = length[0];
setup_buf[7] = length[1];
// Determine direction from request_type bit 7
let is_in = setup.request_type & 0x80 != 0;
let result = if data.is_empty() {
self.do_control_transfer(
device_address,
0, // endpoint 0
false, // low_speed not tracked per-transfer in trait
&setup_buf,
None,
)
} else {
// Build data TD
unsafe {
let td = &mut *(setup_dma_buf as *mut TransferDescriptor);
td.link = PTR_TERM;
td.token = if is_in {
PID_IN | ((device_addr as u32) << 8) | ((endpoint as u32) << 15)
| ((buf.len() as u32 - 1) << 21)
} else {
PID_OUT | ((device_addr as u32) << 8) | ((endpoint as u32) << 15)
| ((buf.len() as u32 - 1) << 21)
};
td.buffer = data_phys as u32;
td.status = TD_CTRL_ACTIVE | TD_CTRL_IOC
| TD_CTRL_SPD
| if low_speed { TD_CTRL_LS } else { 0 }
| (3 << 27);
setup_dma_buf as usize
let data_opt = if is_in {
Some((data, true))
} else {
Some((data, false))
};
self.do_control_transfer(
device_address,
0,
false,
&setup_buf,
data_opt,
)
};
result.map(|opt| opt.unwrap_or(0)).map_err(|e| {
error!("uhcid: control transfer error: {}", e);
UsbError::IoError
})
}
// Poll data TD
let mut actual_len = 0usize;
loop {
let td_status = unsafe { (*(setup_dma_buf as *const TransferDescriptor)).status };
if td_status & TD_CTRL_ACTIVE == 0 {
if td_status & TD_STATUS_STALLED != 0 {
return Err("data TD stalled");
}
actual_len = (td_status & TD_ACTLEN_MASK) as usize + 1;
if is_in {
let n = actual_len.min(buf.len());
let src = unsafe { core::slice::from_raw_parts(setup_dma_buf as *const u8, n) };
(&mut buf[..n]).copy_from_slice(src);
}
break;
}
if start.elapsed() > Duration::from_secs(2) {
return Err("data TD timeout");
}
thread::sleep(Duration::from_micros(100));
}
Some(actual_len)
}
} else {
None
};
fn bulk_transfer(
&mut self,
_device_address: u8,
_endpoint: u8,
_data: &mut [u8],
_direction: TransferDirection,
) -> Result<usize, UsbError> {
// Bulk transfers not yet implemented in UHCI driver.
// UHCI has no native bulk list — bulk transfers go through the
// control transfer path's TD chain. See P4 follow-up.
Err(UsbError::Unsupported)
}
Ok(actual_len)
fn interrupt_transfer(
&mut self,
_device_address: u8,
_endpoint: u8,
_data: &mut [u8],
) -> Result<usize, UsbError> {
// Interrupt transfers in UHCI use the periodic frame list slots.
// Not yet implemented in this driver. See P5 follow-up.
Err(UsbError::Unsupported)
}
fn set_address(&mut self, _device_address: u8) -> bool {
// UHCI does not have a SET_ADDRESS controller command;
// SET_ADDRESS is a standard USB control transfer handled via
// control_transfer(). Returns true here to indicate "address
// accepted" (the caller is expected to send SET_ADDRESS via
// control_transfer with the standard device request).
true
}
}
// ---- Main entry ----
@@ -288,6 +389,13 @@ fn main() {
let device_path = env::var("PCID_DEVICE_PATH").unwrap_or_default();
info!("UHCI USB 1.1 at {}", device_path);
// Derive controller name from device path
let ctrl_name = device_path
.rsplit('/')
.next()
.unwrap_or("uhci0")
.to_string() + "_uhci";
// Read BAR4 (I/O base) from PCI config space
let config_path = format!("{}/config", device_path);
let bar4 = match fs::read(&config_path) {
@@ -297,13 +405,11 @@ fn main() {
_ => { error!("cannot read PCI config"); process::exit(1); }
};
let io_base = (bar4 & 0xFFE0) as u16; // bits 4:0 reserved for I/O BAR
info!("UHCI I/O base: 0x{:04X}", io_base);
let io_base = (bar4 & 0xFFE0) as u16;
info!("UHCI I/O base: 0x{:04X} as {}", io_base, ctrl_name);
// Allocate DMA memory
// Frame list: 1024 entries × 4 bytes, 4KB aligned
let (frame_list, frame_list_phys) = alloc_dma(FRAME_COUNT * 4, FRAME_LIST_ALIGN);
// Dummy QH: terminates the frame list entries
let (dummy_qh, dummy_qh_phys) = alloc_dma(core::mem::size_of::<QueueHead>(), 16);
unsafe {
@@ -312,7 +418,6 @@ fn main() {
qh.element = PTR_TERM;
}
// Initialize frame list: all entries point to the dummy QH
unsafe {
let frames = core::slice::from_raw_parts_mut(frame_list as *mut u32, FRAME_COUNT);
for entry in frames.iter_mut() {
@@ -320,34 +425,28 @@ fn main() {
}
}
// Reset and initialize controller
let ctrl = UhciController {
name: ctrl_name,
io_base,
port_count: 2,
devices: vec![None, None],
_frame_list_dma: Vec::new(),
_qh_dma: Vec::new(),
_td_dma: Vec::new(),
};
ctrl.reset_controller();
ctrl.init_frame_list(frame_list_phys, dummy_qh_phys);
info!("uhcid: controller initialized, polling ports");
info!("uhcid: {} controller initialized, polling ports", ctrl.name);
// Main polling loop
loop {
for port in 0..ctrl.port_count {
let portsc = ctrl.port_status(port);
// Detect newly connected device
if (portsc & PORT_CONNECT) != 0 && (portsc & PORT_CSC) != 0 {
// Clear CSC
ctrl.port_write(port, 0, 0, 0);
ctrl.port_write(port, 0, 0);
info!("uhcid: port {} connect detected (status 0x{:04X})", port + 1, portsc);
info!("uhcid: port {} connect detected", port + 1);
// Reset and enumerate
if ctrl.port_reset(port) {
match enumerate_device(&ctrl, port) {
Ok(dev) => {
@@ -355,12 +454,13 @@ fn main() {
"uhcid: port {} device {:04x}:{:04x} class {:02x}",
port + 1, dev.vendor_id, dev.product_id, dev.device_class,
);
// P0-B1: auto-spawn class drivers
// P0-B1: auto-spawn class drivers via the trait-based path
let scheme_name = usb_core::scheme::scheme_path(&ctrl.name);
usb_core::spawn::spawn_class_driver_for_port(
dev.device_class,
dev.device_subclass,
dev.device_protocol,
"usb",
&scheme_name,
&format!("{}", port + 1),
0,
);
@@ -372,9 +472,8 @@ fn main() {
}
}
// Detect disconnect
if (portsc & PORT_CONNECT) == 0 && (portsc & PORT_CSC) != 0 {
ctrl.port_write(port, 0, 0, 0);
ctrl.port_write(port, 0, 0);
info!("uhcid: port {} disconnected", port + 1);
}
}
@@ -388,33 +487,25 @@ fn enumerate_device(ctrl: &UhciController, port: usize) -> Result<PortDevice, &'
// Step 1: Get 8-byte device descriptor header
let get_desc: [u8; 8] = [
0x80, 0x06, 0x00, 0x01, 0x00, 0x00, 0x08, 0x00, // GET_DESCRIPTOR(DEVICE, 8)
0x80, 0x06, 0x00, 0x01, 0x00, 0x00, 0x08, 0x00,
];
let mut header = [0u8; 8];
control_transfer(
ctrl.io_base, 0, 0, low_speed, &get_desc,
Some((&mut header, true)),
)?;
let max_packet0_device = header[7];
info!("uhcid: port {} max packet size: {}", port + 1, max_packet0_device);
ctrl.do_control_transfer(0, 0, low_speed, &get_desc, Some((&mut header, true)))?;
// Step 2: Set address
let addr: u8 = (port + 1) as u8;
let set_addr: [u8; 8] = [
0x00, 0x05, addr, 0x00, 0x00, 0x00, 0x00, 0x00, // SET_ADDRESS(addr)
0x00, 0x05, addr, 0x00, 0x00, 0x00, 0x00, 0x00,
];
control_transfer(ctrl.io_base, 0, 0, low_speed, &set_addr, None)?;
thread::sleep(Duration::from_millis(10)); // spec: 2ms settle after SET_ADDRESS
ctrl.do_control_transfer(0, 0, low_speed, &set_addr, None)?;
thread::sleep(Duration::from_millis(10));
// Step 3: Get full device descriptor
let mut dev_desc = [0u8; 18];
let get_full_desc: [u8; 8] = [
0x80, 0x06, 0x00, 0x01, 0x00, 0x00, 0x12, 0x00, // GET_DESCRIPTOR(DEVICE, 18)
0x80, 0x06, 0x00, 0x01, 0x00, 0x00, 0x12, 0x00,
];
control_transfer(
ctrl.io_base, addr, 0, low_speed, &get_full_desc,
Some((&mut dev_desc, true)),
)?;
ctrl.do_control_transfer(addr, 0, low_speed, &get_full_desc, Some((&mut dev_desc, true)))?;
let vendor_id = u16::from_le_bytes([dev_desc[8], dev_desc[9]]);
let product_id = u16::from_le_bytes([dev_desc[10], dev_desc[11]]);
@@ -425,12 +516,9 @@ fn enumerate_device(ctrl: &UhciController, port: usize) -> Result<PortDevice, &'
// Step 4: Get config descriptor header
let mut cfg_header = [0u8; 9];
let get_cfg_hdr: [u8; 8] = [
0x80, 0x06, 0x00, 0x02, 0x00, 0x00, 0x09, 0x00, // GET_DESCRIPTOR(CONFIG, 9)
0x80, 0x06, 0x00, 0x02, 0x00, 0x00, 0x09, 0x00,
];
control_transfer(
ctrl.io_base, addr, 0, low_speed, &get_cfg_hdr,
Some((&mut cfg_header, true)),
)?;
ctrl.do_control_transfer(addr, 0, low_speed, &get_cfg_hdr, Some((&mut cfg_header, true)))?;
let total_len = u16::from_le_bytes([cfg_header[2], cfg_header[3]]);
// Step 5: Get full config descriptor
@@ -440,10 +528,7 @@ fn enumerate_device(ctrl: &UhciController, port: usize) -> Result<PortDevice, &'
(total_len & 0xFF) as u8,
((total_len >> 8) & 0xFF) as u8,
];
control_transfer(
ctrl.io_base, addr, 0, low_speed, &get_cfg,
Some((&mut config, true)),
)?;
ctrl.do_control_transfer(addr, 0, low_speed, &get_cfg, Some((&mut config, true)))?;
Ok(PortDevice {
address: addr,
@@ -453,7 +538,7 @@ fn enumerate_device(ctrl: &UhciController, port: usize) -> Result<PortDevice, &'
device_subclass,
device_protocol,
device_descriptor: dev_desc,
config_descriptor: config,
_config_descriptor: config,
low_speed,
})
}
}
@@ -29,10 +29,12 @@ pub const PORT_CONNECT: u16 = 1 << 0;
pub const PORT_CSC: u16 = 1 << 1; // Connect Status Change
pub const PORT_ENABLE: u16 = 1 << 2;
pub const PORT_PEC: u16 = 1 << 3; // Port Enable Change
pub const PORT_OVER_CURRENT: u16 = 1 << 3; // alias of PEC for clarity
pub const PORT_RESUME: u16 = 1 << 6;
pub const PORT_LOW_SPEED: u16 = 1 << 8;
pub const PORT_RESET: u16 = 1 << 9;
pub const PORT_SUSPEND: u16 = 1 << 12;
pub const PORT_POWER: u16 = 1 << 12; // alias of SUSPEND (different bit on real UHCI hardware; non-standard)
// PORTSC change bits (write 1 to clear)
pub const PORT_CHANGE_BITS: u16 = PORT_CSC | PORT_PEC;
@@ -10,7 +10,7 @@ pub mod transfer;
pub mod types;
pub use dma::{DmaBuffer, DmaError};
pub use scheme::{UsbError, UsbHostController};
pub use scheme::{scheme_path, UsbError, UsbHostController, SCHEME_PREFIX};
pub use spawn::spawn_usb_driver;
pub use spawn::class_driver_for_usb_class;
pub use spawn::spawn_class_driver_for_port;
@@ -1,18 +1,32 @@
use crate::types::{PortStatus, SetupPacket, TransferDirection};
/// Trait that all USB host controller drivers must implement.
/// This provides a uniform scheme interface regardless of HC type (XHCI/EHCI/OHCI/UHCI).
///
/// This provides a uniform scheme interface regardless of HC type
/// (XHCI / EHCI / OHCI / UHCI). Class drivers call these methods and
/// do not need to know which host controller they are talking to.
///
/// All controllers must register a scheme at `usb_core::scheme_prefix() +
/// controller.name()`. Class drivers are spawned with this scheme name
/// and open the controller through the trait API.
pub trait UsbHostController {
/// Get the number of ports on this controller
/// Controller name for scheme registration and logging.
/// Example: `"0000:00:14.0_xhci"`. Must be unique system-wide.
fn name(&self) -> &str;
/// Number of root-hub ports on this controller.
fn port_count(&self) -> usize;
/// Get status of a specific port
/// Get status of a specific port. Returns None if the port index is invalid.
fn port_status(&self, port: usize) -> Option<PortStatus>;
/// Reset a port (USB enumeration step 1)
/// Reset a port (USB enumeration step 1).
/// Returns true if the port enabled successfully after reset.
fn port_reset(&mut self, port: usize) -> bool;
/// Submit a control transfer to endpoint 0
/// Submit a control transfer to endpoint 0.
/// `data` is the data-phase buffer (empty for setup-only transfers).
/// Returns the number of bytes transferred.
fn control_transfer(
&mut self,
device_address: u8,
@@ -20,7 +34,7 @@ pub trait UsbHostController {
data: &mut [u8],
) -> Result<usize, UsbError>;
/// Submit a bulk transfer
/// Submit a bulk transfer. Use for mass-storage and similar.
fn bulk_transfer(
&mut self,
device_address: u8,
@@ -29,7 +43,8 @@ pub trait UsbHostController {
direction: TransferDirection,
) -> Result<usize, UsbError>;
/// Submit an interrupt transfer
/// Submit an interrupt transfer. Use for HID, hubs.
/// Returns the number of bytes received.
fn interrupt_transfer(
&mut self,
device_address: u8,
@@ -37,11 +52,17 @@ pub trait UsbHostController {
data: &mut [u8],
) -> Result<usize, UsbError>;
/// Set device address (after reset, before config)
/// Assign a device address (called during enumeration after port reset).
fn set_address(&mut self, device_address: u8) -> bool;
}
/// Get the controller name for logging
fn name(&self) -> &str;
/// Common scheme prefix for all USB host controllers.
/// Class drivers are spawned with this prefix + the controller's `name()`.
pub const SCHEME_PREFIX: &str = "usb.";
/// Compose a scheme path for a controller. Example: `"usb.0000:00:14.0_xhci"`.
pub fn scheme_path(name: &str) -> alloc::string::String {
alloc::format!("{SCHEME_PREFIX}{name}")
}
#[derive(Debug)]
@@ -55,3 +76,5 @@ pub enum UsbError {
IoError,
Unsupported,
}
extern crate alloc;