Skip to content

fix(usb): EHCI/OHCI/UHCI hot-plug, hubs and failed-port retries - #106

Merged
FengHeting merged 5 commits into
ViudiraTech:developfrom
JiTianYu391:fix/usb-legacy-hcd-irq
Oct 20, 2026
Merged

FengHeting merged 5 commits into
ViudiraTech:developfrom
JiTianYu391:fix/usb-legacy-hcd-irq

Conversation

@JiTianYu391

@JiTianYu391 JiTianYu391 commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

问题

EHCI / OHCI / UHCI 的根端口处理有 4 个独立的 bug(xHCI 正常)。每个都先在 QEMU 中复现、再修复验证;4 个提交各修一个:

# 提交 现象 根因
1 route the legacy INTx interrupt through the IO-APIC 开机后插拔无反应(EHCI/OHCI/UHCI) pci_request_irq() 没设 .legacy_base / .legacy_ioapic:handler 装在 CPU 异常向量(< 32)上,且未建 IO-APIC 路由
2 uhci: poll the root ports for connect changes UHCI 修好中断后仍检测不到插拔 UHCI 没有端口变化中断,RD 只在全局挂起时触发;worker 却只在 RD 后处理端口
3 give root-port devices depth 1 Hub 后面的设备 -EEXIST 注册失败 根端口设备 depth 为 0,被当成根 Hub,子设备路径丢掉父级(3-1 与 Hub 自身重名)
4 ehci: stop retrying a failed root port until reconnect EHCI 上枚举失败的设备无限重试,CPU 占满,系统进不了 shell 每次重试的端口复位又触发 PCD;与 xHCI 06255b7 的 failed_ports 同一问题

依据

  1. QEMU usb_ehci_irq trace:插入时 PCD 已拉起(level 1, sts 0x4),内核从未读 USBSTS。写法与 e1000 / rtl8139 / rtl8169 一致:.legacy_base = IRQ_0, .legacy_ioapic = 1。

  2. Linux uhci-hcd.c start_rh() 设 HCD_FLAG_POLL_RH,注释 "All interrupts are enabled, even though RESUME won't do anything"。worker 本就每 1ms 唤醒,在循环开头读两个 PORTSC。

  3. 与 xHCI(device->depth = 1; // Root-port device: depth 1 (root hub depth 0))和 Linux usb_alloc_dev() 的 devpath 规则一致,修复后子设备名为 3-1.1。depth 在这三个驱动上没有其他用途。

  4. 故障注入(测试时让 *_enumerate_port() 在端口复位后强制失败,不在提交中):

    15 秒内失败次数 进入 shell
    EHCI 修复前 54 ❌
    EHCI 修复后 1 ✅
    OHCI / UHCI(未改) 1 ✅

    只在首次注入失败时:拔出后换盘插入可正常识别并读出数据。OHCI/UHCI 只响应变化位,且其端口复位会清掉自身产生的变化位,因此不需要此修复。

测试(QEMU q35,自动化脚本:串口驱动 guest shell + monitor 插拔;U 盘镜像写入特征文本,用 cat /dev/sdX 校验读出的是对应那块盘)

#105 + 本 PR:61/61 通过

控制器 U 盘 插入/读数据/拔出/换盘插入/读数据/拔出 键盘 插/拔 Hub 下 U 盘(同上流程) 内核日志
xHCI ✅ ✅ ✅ ✅
EHCI ✅ ✅ QEMU 不支持¹ ✅
OHCI ✅ ✅ ✅ ✅
UHCI ✅ ✅ ✅ ✅²

冷启动(EHCI+U盘,xHCI+U盘+键盘,OHCI+Hub(U盘)+键盘,UHCI+U盘+键盘):4 块盘各自读出正确数据,5 个 input event,日志干净。

仅本 PR(不含 #105):xHCI/OHCI/UHCI 全部通过;EHCI 设备因 #105 的位定义问题无法枚举,但只失败 1 次、系统正常启动(没有提交 4 时会卡死)。两个 PR 可按任意顺序合并。

¹ QEMU 的 usb-hub 只有全速,无法接到纯 EHCI 上(speed mismatch)。
² 拔除 Hub 时会打印一次 usb-uhci: USB error interrupt:设备消失时进行中的传输按规范报错(QEMU hcd-uhci.c USB_RET_NODEV → USBERR),属预期;Linux 对此不打印。

备注

  • 默认 .config 中 OHCI / UHCI 关闭,测试时临时打开;当前 develop 的 virtgpu_vq.c 编译不过(与本 PR 无关),测试时临时关闭 CONFIG_VIRTIO_GPU。
  • 观察到 msleep() 是忙等(nsleep 循环 cpu_relax),这是提交 4 的循环会饿死 init 的原因。属全局设计,未在本 PR 修改。

🤖 Generated with Claude Code

JiTianYu391 and others added 4 commits October 17, 2026 17:02
The three legacy HCDs ask pci_request_irq() for MSI or legacy INTx but
leave .legacy_base and .legacy_ioapic at 0.  None of these controllers
has MSI in QEMU (and few do on real hardware), so they always take the
INTx path, where that means:

- the handler is installed at vector = 0 + IRQ line, i.e. on a CPU
  exception vector (< 32);
- no IO-APIC redirection entry is added, so the interrupt never
  reaches the CPU.

The controllers still enumerate devices present at boot, because the
first worker pass scans every port without an interrupt, but every
later port change is lost: hot-plug and unplug do nothing.  xHCI is
not affected since it uses MSI/MSI-X.

Fill in the two fields the same way the other INTx users do (e1000,
rtl8139, rtl8169): .legacy_base = IRQ_0, .legacy_ioapic = 1.

Tested in QEMU (q35): QEMU's usb_ehci_irq trace shows the PCD
interrupt raised on plug while the kernel never read USBSTS; with this
change EHCI and OHCI detect plug, unplug and re-plug of a USB stick.

Signed-off-by: JiTianYu391 <smallcuteshb@outlook.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
UHCI has no port-change interrupt.  USBSTS.RD (Resume Detect) only
fires while the controller is in global suspend, yet uhci_worker()
only looked at a port after uhci_interrupt_handler() saw RD.  On a
running controller that never happens, so after the boot-time scan
no plug or unplug was ever noticed, even with working interrupts.

Linux handles this the same way: start_rh() sets HCD_FLAG_POLL_RH
("All interrupts are enabled, even though RESUME won't do anything")
and the root hub status is polled.  The worker already wakes every
1 ms, so read the two PORTSC registers there and treat any CSC/PEC
as a pending port.

Tested in QEMU (q35, piix3-usb-uhci + usb-storage): plug, unplug and
re-plug are all detected; before, none were.

Signed-off-by: JiTianYu391 <smallcuteshb@outlook.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
usb_enumerate_device() follows the xHCI and Linux convention that the
root hub is level 0: when the parent hub has depth 0 it names the
child "bus-port", otherwise "<parent path>.port".  xhci_enumerate_port()
sets depth = 1 on root-port devices, but the EHCI/OHCI/UHCI root-port
devices come from calloc() and stay at depth 0.

So a device behind a hub on one of these controllers lost the hub's
level in its name: the device on port 1 of a hub that sits on root
port 1 was named "3-1", the hub's own name, and failed to register
with -EEXIST:

  usb-hub: 3-1 port 1 enumerate failed: -17
  usb: 4-1: device model registration failed: -17

Set depth = 1 in the three root-port enumerate paths, as xHCI does, so
the child becomes "3-1.1" (Linux usb_alloc_dev(): "%d-%s" with
devpath "<parent devpath>.<port>").  depth has no other user on these
controllers.

Tested in QEMU (q35): cold boot with a USB stick behind a usb-hub on
OHCI root port 1, plus UHCI (where QEMU inserts a hub on its last
free port), now enumerates every device; before, both failed with
-EEXIST.

Signed-off-by: JiTianYu391 <smallcuteshb@outlook.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Once the EHCI interrupt is delivered, a device that fails enumeration
is retried forever.  Every attempt starts with a port reset, and the
reset itself raises PCD (QEMU's usb_port_reset() detaches and
re-attaches the device); the worker then sees "connected, no device"
and enumerates again.  Because msleep() busy-waits, the loop also
keeps a CPU fully busy: with a failing device on EHCI the boot never
reached the shell.

Do what xHCI does since 06255b7: remember the port in failed_ports
when enumeration fails, ignore its further change events, and forget
it only when the device goes away (CCS = 0) or a new connection is
reported (CSC = 1).  OHCI and UHCI do not need this: they only act on
a change bit, and their port reset clears the change bits it causes.

Tested in QEMU (q35) by forcing *_enumerate_port() to fail after the
port reset (test-only change, not part of this commit):

             failures in 15 s   shell
  EHCI before      54            no
  EHCI after        1            yes
  OHCI              1            yes
  UHCI              1            yes

With the fault injected only on the first attempt, unplugging the
device and plugging in another one enumerates it and reads its data.

Signed-off-by: JiTianYu391 <smallcuteshb@outlook.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@JiTianYu391 JiTianYu391 changed the title fix(usb): deliver EHCI/OHCI/UHCI interrupts and poll UHCI root ports fix(usb): EHCI/OHCI/UHCI hot-plug, hubs and failed-port retries Oct 17, 2026
@FengHeting
FengHeting merged commit e32d30d into ViudiraTech:develop Oct 20, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants