fix(usb): uhci 控制传输逐 TD 校验并暴露错误位,补描述符落盘探针

- _wait_for_complete 原先只查 status TD 的 bit21|bit22,DATA 阶段的 CRC/超时(bit18)、bitstuff(bit17)、babble(bit20) 会被当成成功 ⇒ GET_DESCRIPTOR 拿到零字节缓冲区仍返回 0,最终表现为 [usb] device class=00 proto=00 ep=0、usb_kbd 找不到键盘(现场)。改为对齐 Linux usb-uhci.h:新增 TD_CTRL_ERR_MASK/NAK_MASK 与 _td_error(),control_transfer 收尾逐个校验 setup/data/status 三个 TD,失败时打印三个状态字。

- usb.vp 增加两条探针:[usb] devdesc blen/btype/maxpkt0 与 [usb] cfgdesc total/b0/t0/iface/proto/ep,用于区分数据未落盘与解析未命中。
This commit is contained in:
TermiNexus committed 2026-10-16 15:15:58 +08:00
1 parent 89907682ec
commit 1b2f6853fc
2 files changed
+72 -5

No files matched your search

+56 -4
View File
@@ -48,6 +48,14 @@ define TD_CTRL_SPD = 0x20000000
define TD_CTRL_CERR_SHIFT = 27
define TD_CTRL_CERR_MASK = 0x18000000
# TD 状态字里的错误位(对照 Linux usb-uhci.h 的 TD_CTRL_ANY_ERROR / uhci_status_bits):
# bit17 Bitstuff / bit18 CRC-Timeout / bit20 Babble / bit21 DataBufErr / bit22 Stalled
# 原先只查 bit21|bit22,DATA 阶段若有 CRC/超时错误会被当成"成功但零字节",
# 于是 config 描述符读回来全 0 → iface_class=0 → usb_kbd 永远找不到键盘(现场)。
define TD_CTRL_ERR_MASK = 0x00760000
# bit19 NAK:C_ERR(3) 用尽后仍被 NAK ⇒ 设备一直不给数据
define TD_CTRL_NAK_MASK = 0x00080000
define TD_TOKEN_SETUP = 0x2D
define TD_TOKEN_IN = 0x69
define TD_TOKEN_OUT = 0xE1
@@ -225,8 +233,10 @@ def _wait_for_complete(td: uhci_td*, timeout_ms: int) -> int:
while timer.timer_get_ticks() < deadline:
status: uint32 = td.ctrl_status
if (status & TD_CTRL_ACTIVE) == 0:
if (status & 0x00600000) != 0:
if (status & TD_CTRL_ERR_MASK) != 0:
return -1
if (status & TD_CTRL_NAK_MASK) != 0:
return -3
return 0
poll_cnt += 1
if poll_cnt >= 100:
@@ -235,6 +245,14 @@ def _wait_for_complete(td: uhci_td*, timeout_ms: int) -> int:
asm.hlt()
return -2
# 单个 TD 的完成判定(fail-fast):0=正常 1=仍在 ACTIVE 2=错误位 3=NAK
def _td_error(td: uhci_td*) -> int:
st: uint32 = td.ctrl_status
if (st & TD_CTRL_ACTIVE) != 0: return 1
if (st & TD_CTRL_ERR_MASK) != 0: return 2
if (st & TD_CTRL_NAK_MASK) != 0: return 3
return 0
def reset(io_base: uint16) -> int:
global _io_base
_io_base = io_base
@@ -386,8 +404,11 @@ def control_transfer(dev_addr: uint8, endpoint: uint8, ls: int, max_pkt: uint8,
_td_set_token(setup_td, TD_TOKEN_SETUP, dev_addr, endpoint, TD_TOKEN_DATA0, 8)
setup_td.buffer = uint32(uint64(setup_buf))
# data/status 两个 TD 事先声明:收尾时要逐个校验完成状态(见下方 fail-fast 段)
data_td: uhci_td* = None
status_td: uhci_td* = None
if data_len > 0:
data_td: uhci_td* = uhci_td*(uint64(tds) + uhci_td.__sizeof__())
data_td = uhci_td*(uint64(tds) + uhci_td.__sizeof__())
_td_set_link(data_td, uint32(uint64(data_td) + uhci_td.__sizeof__()), 0, 0)
_td_set_ctrl(data_td, ls, max_pkt)
if data_in:
@@ -396,7 +417,7 @@ def control_transfer(dev_addr: uint8, endpoint: uint8, ls: int, max_pkt: uint8,
_td_set_token(data_td, TD_TOKEN_OUT, dev_addr, endpoint, TD_TOKEN_DATA1, data_len)
data_td.buffer = uint32(uint64(data_buf))
status_td: uhci_td* = uhci_td*(uint64(data_td) + uhci_td.__sizeof__())
status_td = uhci_td*(uint64(data_td) + uhci_td.__sizeof__())
_td_set_link(status_td, 0, 0, 1)
_td_set_ctrl(status_td, ls, max_pkt)
if data_in:
@@ -405,7 +426,7 @@ def control_transfer(dev_addr: uint8, endpoint: uint8, ls: int, max_pkt: uint8,
_td_set_token(status_td, TD_TOKEN_IN, dev_addr, endpoint, TD_TOKEN_DATA1, 0)
status_td.buffer = 0
else:
status_td: uhci_td* = uhci_td*(uint64(tds) + uhci_td.__sizeof__())
status_td = uhci_td*(uint64(tds) + uhci_td.__sizeof__())
_td_set_link(status_td, 0, 0, 1)
_td_set_ctrl(status_td, ls, max_pkt)
_td_set_token(status_td, TD_TOKEN_IN, dev_addr, endpoint, TD_TOKEN_DATA1, 0)
@@ -416,5 +437,36 @@ def control_transfer(dev_addr: uint8, endpoint: uint8, ls: int, max_pkt: uint8,
result: int = _wait_for_complete(status_td, 500)
_qh_set_element(_ctrl_qh, 0, 1)
# fail-fast:不只等 status TD —— DATA 阶段(以及 setup/status)的完成状态逐个校验。
# 只等 status TD 会漏掉"DATA 阶段 CRC/超时但 status 正常"的情形 ⇒ 返回 0 却零字节,
# 调用方(GET_DESCRIPTOR)拿到空缓冲区继续走,最终表现为 iface_class=0(现场)。
fail: int = 0
which: int = 0
st_setup: uint32 = setup_td.ctrl_status
st_data: uint32 = 0
st_status: uint32 = status_td.ctrl_status
e_setup: int = _td_error(setup_td)
if e_setup != 0:
fail = 0 - 10 - e_setup
which = 0
if data_td is not None:
st_data = data_td.ctrl_status
e_data: int = _td_error(data_td)
if e_data != 0:
fail = 0 - 20 - e_data
which = 1
e_status: int = _td_error(status_td)
if e_status != 0:
fail = 0 - 30 - e_status
which = 2
if fail != 0:
_db: [128]char
viperlib.snprintf(&_db, 128, "[uhci] ctrl fail dev=%d ep=%d which=%d setup=0x%x data=0x%x status=0x%x\n",
uint32(dev_addr), uint32(endpoint), uint32(which),
uint32(st_setup), uint32(st_data), uint32(st_status))
serial.puts(&_db)
mm.free(tds)
if fail != 0:
return fail
return result
+16 -1
View File
@@ -204,6 +204,12 @@ def enumerate_port(port: int) -> int:
desc: usb_dev_desc* = dev_buf
max_pkt: uint8 = desc.bMaxPacketSize0
if max_pkt < 8: max_pkt = 8
# 探针:设备描述符只要读回 18/8 字节,bLength 必为 18、bDescriptorType 必为 1。
# 全 0 ⇒ 控制传输的 DATA 阶段没落盘(而非解析问题)。
_db0: [96]char
viperlib.snprintf(&_db0, 96, "[usb] devdesc blen=%d btype=%d maxpkt0=%d\n",
int(desc.bLength), int(desc.bDescriptorType), int(desc.bMaxPacketSize0))
serial.puts(&_db0)
new_addr: uint8 = uint8(_device_count + 1)
result = set_address(0, ls, max_pkt, new_addr)
@@ -260,7 +266,16 @@ def enumerate_port(port: int) -> int:
mm.free(dev_buf)
return -8
_parse_config(dev, config_buf, total_len)
_parse_config(dev, config_buf, total_len)
# 探针:配置描述符读回后,wTotalLength 应为 0x22/0x23、首字节应为 09 02(bLength=9, CONFIG)。
# total=0 或首字节为 0 ⇒ DATA 阶段没传回来;有内容但 iface 仍为 0 ⇒ 解析/布局问题。
_cb: uint8* = config_buf
_db1: [128]char
viperlib.snprintf(&_db1, 128, "[usb] cfgdesc total=%d b0=%02X t0=%02X iface=%02X proto=%02X ep=%d\n",
int(total_len), int(_cb[0]), int(_cb[1]),
int(dev.iface_class), int(dev.iface_protocol), int(dev.ep_in))
serial.puts(&_db1)
result = set_configuration(new_addr, ls, max_pkt, cfg.bConfigurationValue)
if result != 0: