笔者学习qemu 模拟器,今天讲一下usb设备这块
基于三个文件进行分析
hw/usb/core.c--- USB 控制传输与数据(Bulk)传输的通用逻辑hw/usb/dev-storage.c--- BOT(Bulk-Only Transport,协议号 0x50)协议解析hw/usb/dev-uas.c--- UAS(USB Attached SCSI,协议号 0x62)协议解析
1. 核心调用关系总览
HCD 驱动 (hcd-xhci.c / hcd-ehci.c / hcd-uhci.c ...)
│ usb_port_ops->attach / detach / complete
▼
usb_handle_packet(dev, p) [core.c]
▼
usb_process_one(p) [core.c]
├─ p->ep->nr == 0(控制端点 EP0)
│ ├─ p->parameter != 0 → do_parameter() ← xHCI 参数化控制传输
│ └─ 按 p->pid 分发:
│ USB_TOKEN_SETUP → do_token_setup()
│ USB_TOKEN_IN → do_token_in()
│ USB_TOKEN_OUT → do_token_out()
│ (三者最终调用 usb_device_handle_control())
└─ p->ep->nr != 0(数据端点,含 Bulk)
└─ usb_device_handle_data(dev, p)
├─ BOT: usb_msd_handle_data() [dev-storage.c]
└─ UAS: usb_uas_handle_data() [dev-uas.c]
设备内部(SCSI 桥接):
scsi_req_new() → scsi_req_enqueue() → SCSI Bus(SCSIBusInfo 回调)
│ .transfer_data / .complete / .cancel
▼
usb_msd_transfer_data() / usb_msd_command_complete() [BOT]
usb_uas_scsi_transfer_data() / usb_uas_scsi_command_complete() [UAS]
│
▼
usb_packet_complete(dev, p) → port->ops->complete() → HCD 收到完成通知
关键入口/出口:
- 提交方向 :HCD →
usb_handle_packet()→ 设备handle_data/handle_control - 完成方向 :设备(通常在 SCSI 回调中)→
usb_packet_complete()→port->ops->complete()→ HCD - 异步机制 :设备返回
USB_RET_ASYNC后包挂到ep->queue,设备稍后调usb_packet_complete();USB_RET_ADD_TO_QUEUE则由usb_queue_one()排队,等前序包完成后在usb_packet_complete()的 while 循环里被usb_process_one()重放。
2. 控制传输逻辑(core.c,EP0)
控制传输用 setup_state 状态机驱动(SETUP → DATA → ACK → IDLE):
| 状态 | 含义 |
|---|---|
SETUP_STATE_IDLE |
空闲 |
SETUP_STATE_SETUP |
收到 SETUP 且控制请求异步处理中 |
SETUP_STATE_DATA |
DATA 阶段(收/发 data_buf) |
SETUP_STATE_ACK |
状态阶段(无数据控制传输直接进入) |
SETUP_STATE_PARAM |
xHCI 参数化传输(do_parameter) |
2.1 do_token_setup(core.c L129
- 校验包长必须为 8 字节,
usb_packet_copy()读入s->setup_buf - 解析
wLength(setup_buf[7..6])→s->setup_len;超data_buf容量则 STALL - 解析 request/value/index
- IN 方向请求 (
bmRequestType & USB_DIR_IN,如 GET_DESCRIPTOR):立即调用
usb_device_handle_control(s, p, request, value, index, setup_len, s->data_buf);
返回USB_RET_ASYNC则停在 SETUP 态(由usb_generic_async_ctrl_complete接续) - OUT 方向请求(如 SET_CONFIGURATION):不立即执行,收完 DATA 阶段后在 ACK 态执行
2.2 控制传输三阶段与 IN/OUT 事务的角色
USB 控制传输 = SETUP 事务 + DATA 事务(0~n 次)+ STATUS 事务 ,IN/OUT 是各阶段事务的 token(方向由 setup_buf[0] 即 bmRequestType 的 bit7 决定,与 BOT 数据端点 EP1 IN / EP2 OUT 无关------CBW/数据/CSW 全走 Bulk 端点,不经过控制传输):
| 请求类型 | bmRequestType bit7 | DATA 阶段 | STATUS 阶段(零长包) |
|---|---|---|---|
| 控制读(IN 方向请求) | 1(USB_DIR_IN) |
IN 事务若干次(设备→主机发 data_buf) | OUT 事务:主机确认已收数据 |
| 控制写(OUT 方向请求) | 0 | OUT 事务若干次(主机→设备收 data_buf);wLength==0 则直接进 STATUS |
IN 事务:设备确认已处理 |
注意方向规则:STATUS 阶段方向与 DATA 阶段恒相反 ,且 STATUS 事务是零长度包(写完请求后 p->actual_length = 0)。
2.2a do_token_out(core.c L229)------主机发 OUT 事务时干什么
按 setup_state × 请求方向 四个分支(见代码 switch):
| setup_state | 请求方向 | 场景 | 动作 |
|---|---|---|---|
| DATA | OUT(!(setup_buf[0]&USB_DIR_IN)) |
控制写的数据阶段 | usb_packet_copy() 把包数据拷入 data_buf + setup_index;setup_index >= setup_len 时收满转 ACK 态 |
| DATA | IN | 协议错误(读请求不该有 DATA-OUT) | 置 SETUP_STATE_IDLE + USB_RET_STALL |
| ACK | IN | 控制读的状态阶段:SETUP→DATA-IN→这里的 OUT | 主机零长包确认收完数据 → 设备转 IDLE,usb_pcap_ctrl(p,false) 抓包收尾(transfer OK) |
| ACK | OUT | 写请求 ACK 态后又来 OUT | 容错:忽略多余输出(ignore additional output) |
| 其它(SETUP/IDLE/PARAM) | --- | 协议错误 | USB_RET_STALL |
2.3 do_token_in(core.c L181 ------主机发 IN 事务时干什么
| setup_state | 请求方向 | 场景 | 动作 |
|---|---|---|---|
| DATA | IN(setup_buf[0]&USB_DIR_IN) |
控制读的数据阶段 | 从 data_buf + setup_index 拷出 min(setup_len - setup_index, p->iov.size) 到包;发完转 ACK 态 |
| DATA | OUT | 协议错误(写请求不该有 DATA-IN) | 置 IDLE + USB_RET_STALL |
| ACK | OUT(!(...USB_DIR_IN)) |
控制写的状态阶段:设备此刻才真正执行请求 | 调用 usb_device_handle_control()(此时 data_buf 已收满------SETUP 阶段故意不执行的 OUT 请求在此执行);USB_RET_ASYNC 则挂起等 usb_generic_async_ctrl_complete;否则转 IDLE、actual_length=0(回零长状态包) |
| ACK | IN | 读请求 ACK 态又来 IN | 静默忽略(break,无动作,不 STALL) |
| 其它(SETUP/IDLE/PARAM) | --- | 协议错误 | USB_RET_STALL |
2.3a BOT 设备(usb-storage)EP0 上的典型事务序列
| 请求 | 类型 | 完整序列 | 各步落点 |
|---|---|---|---|
GET_DESCRIPTOR(枚举) |
控制读 | SETUP → DATA-IN×n → STATUS-OUT | do_token_setup 立即执行填 data_buf → do_token_in(DATA) 分片发出 → do_token_out(ACK,IN 方向) 收确认回 IDLE |
GetMaxLun (0xfe) |
控制读(BOT 类) | SETUP → DATA-IN(1B LUN) → STATUS-OUT | 同上;data_buf 填最大 LUN |
SET_CONFIGURATION |
控制写(无数据) | SETUP → STATUS-IN | do_token_setup 不执行 → 直接转 ACK(wLength==0)→ do_token_in(ACK) 才激活配置 |
MassStorageReset (0xff) |
控制写(无数据,BOT 类) | SETUP → STATUS-IN | do_token_in(ACK) 时执行:s->mode = USB_MSDM_CBW 复位 BOT 状态机(错误恢复手段,如 usb_msd_fatal_error 后主机靠它解锁) |
2.4 do_parameter(core.c L267
xHCI 把 8 字节 setup 放在 p->parameter(64bit)里一次下发;OUT 时同时携带数据。直接解析后调用 usb_device_handle_control(),IN 时用 usb_packet_copy() 回填数据。
2.5 异步控制传输
设备 handle_control 返回 USB_RET_ASYNC 时必须改用
usb_generic_async_ctrl_complete(s, p)(而非直接 usb_packet_complete)来完成,它按 SETUP/ACK/PARAM 三种挂起状态分别收尾。
2.6 EP0 PID 分发之后:请求如何被执行
do_token_setup/in(或 do_parameter)调用 usb_device_handle_control() 后的完整链路:
usb_device_handle_control(dev, p, request, value, index, length, data) [bus.c L154]
└─ QOM 包装:klass->handle_control(...) ← 设备类回调
├─ usb-storage: usb_msd_handle_control() [dev-storage.c L345]
└─ usb-uas: usb_uas_handle_control() [dev-uas.c L651]
└─ 先调 usb_desc_handle_control() [desc.c L708] 处理 USB 标准请求
└─ ret >= 0 已处理则直接返回;ret < 0 再走类自定义请求,未识别 → STALL
usb_desc_handle_control()(desc.c L708)处理 USB 规范第 9 章标准请求(设备枚举的完整过程):
| 请求 | 处理内容 |
|---|---|
SET_ADDRESS |
dev->addr = value------设备获得总线地址,此后 HCD 用 usb_find_device(port, addr) 按地址路由 |
GET_DESCRIPTOR |
usb_desc_get_descriptor() 返回 device / config / string / qualifier / BOS 等描述符(枚举核心) |
GET_CONFIGURATION |
返回当前 bConfigurationValue(未配置返回 0) |
SET_CONFIGURATION |
usb_desc_set_config():激活选中的 USBDescConfig,按描述符初始化各接口/端点(type、max_packet_size、max_streams、pipeline 全部生效) |
GET_STATUS |
返回 self-powered / remote-wakeup 位 |
CLEAR_FEATURE / SET_FEATURE |
清/置 dev->remote_wakeup |
GET_INTERFACE / SET_INTERFACE |
读取/切换 altsetting[](usb_desc_set_interface) |
SET_SEL / SET_ISOCH_DELAY |
SuperSpeed 专用,直接成功 |
Vendor 'Q' |
MS OS 描述符(Windows 驱动匹配) |
标准请求未命中(ret < 0)后进入类自定义请求:
- usb-storage :
MassStorageReset (0xff)复位 CBW 状态机、GetMaxLun (0xfe)返回最大 LUN(dev-storage.c L345) - usb-uas :无类请求,一切未识别请求 →
USB_RET_STALL(dev-uas.c L651
EP0 相关的设备字段():setup_buf[8](SETUP 8 字节)、data_buf[4096](控制数据缓冲,setup_len 超过它即 STALL)、setup_state/setup_len/setup_index(状态机)、ep_ctl(控制端点实例,usb.h L255)。
3. Bulk 输出(数据传输)逻辑(core.c)
3.1 usb_handle_packet(core.c L420
HCD 提交包的唯一入口:
ep->halted时提交新包会自动清除 halt- 端点队列为空、或
ep->pipeline(xHCI 流水线)、或p->stream(UAS 流)→ 直接usb_process_one() - 返回值处理:
USB_RET_ASYNC→ 包入ep->queue,状态 ASYNC(isoc 禁止 async;host 侧设备如 usb-host 的 int 才允许)USB_RET_ADD_TO_QUEUE→usb_queue_one()- 其它(SUCCESS/NAK/STALL...)→ NAK 以外都置 COMPLETE 并
usb_pcap_data(p, false)
- 队列非空且无流水线 → 直接
usb_queue_one()排队保序
3.2 usb_process_one(core.c L369
- EP0:见上节控制传输
- 非 EP0(Bulk/Interrupt/Isoc):
usb_pcap_data(p, true)抓包后usb_device_handle_data(dev, p)------ Bulk 命令与数据都从这里进入 BOT/UAS 的 handle_data
3.3 usb_packet_complete(core.c L486
usb_packet_complete_one():状态非 SUCCESS 或short_not_ok && 短包→ep->halted = true;从队列摘除并回调 HCD- 随后循环重放队列:halted 时清空队列(REMOVE_FROM_QUEUE);QUEUED 的包重新
usb_process_one() - 注意设备代码惯用手法:先把
s->packet = NULL再调用usb_packet_complete(),防止重入(见usb_msd_packet_complete)
3.4 数据搬运原语
usb_packet_copy(p, ptr, bytes):按 pid 决定方向(OUT=iov→ptr,IN=ptr→iov),处理p->combined(xHCI 聚合包)usb_packet_skip(p, bytes):IN 方向跳过并置零usb_packet_setup()/usb_packet_addbuf():HCD 组包用
3.5 端点、PID 与设备模式(CBW mode)三者的关系
三个概念分属不同层次,共同决定"一个 USB 包到底承载什么":
| 概念 | 所属层次 | 取值 | 语义 |
|---|---|---|---|
端点号 p->ep->nr |
USB 管道拓扑 | 0(EP0)/ 1~15 | 包走哪条管道:0=控制(枚举/类请求),非 0=数据传输 |
PID p->pid |
USB 事务层 | SETUP / IN / OUT | 包的"动词":SETUP=控制传输起点;IN=主机收(设备→主机);OUT=主机发(主机→设备) |
设备模式 s->mode |
BOT 应用层(设备私有) | CBW / DATAIN / DATAOUT / CSW | BOT 独有的"会话阶段标签":同一端点在不同阶段承载不同内容 |
分流点在 usb_process_one)(if (p->ep->nr == 0) 在 L382):
p->ep->nr == 0 → 控制传输(pid 三值分发:SETUP/IN/OUT → do_token_*)
p->ep->nr != 0 → usb_device_handle_data(pid 只剩 IN/OUT 两值)
PID 的两个作用:
- 事务类型/路由:上面的分流 + 控制端点上 SETUP/IN/OUT 的分发
- 数据方向:
usb_packet_copy()按 pid 决定拷贝方向 ------ OUT/SETUP=从包 iov 读出(设备收数据),IN=向包 iov 写入(设备发数据)
BOT:端点 + pid + mode 三元组才能确定一个包的语义 (命令/数据/状态复用同一对端点)。usb_msd_handle_data 是三层嵌套判断(dev-storage.c L399:
switch (p->pid) ← 第一层 L413:事务方向(OUT / IN)
if (devep != 2 / != 1) ← 第二层 L415/L493:端点号(OUT 只认 devep==2,IN 只认 devep==1,否则 STALL)
switch (s->mode) ← 第三层:BOT 会话阶段(CBW / DATAOUT / CSW / DATAIN)
BOT 语义消歧表:
| devep | pid | s->mode | 主机在做什么 | 设备动作 |
|---|---|---|---|---|
| 2 | OUT | CBW | 发 31B CBW 命令 | 解析 CBW → scsi_req_new/enqueue;按 data_len/flags 切换 mode |
| 2 | OUT | DATAOUT | 发写数据 | usb_msd_copy_data → SCSI 缓冲 |
| 2 | OUT | 其他 | --- | STALL(goto fail) |
| 1 | IN | DATAIN | 收读数据 | SCSI 缓冲 → 包 |
| 1 | IN | CSW | 收 13B CSW 状态 | usb_msd_send_status → 回 CBW 态 |
| 1 | IN | DATAOUT | 写命令未完成时提前发 status read | 挂 ASYNC,命令完成后回 CSW |
mode 迁移由 CBW 内容驱动:data_len==0 → CSW;flags&0x80(d2h)→ DATAIN;否则 → DATAOUT(见 §4.1 状态机图)。
UAS 对照 :switch (p->ep->nr) 直接按管道 ID 分发(usb_uas_handle_data(file:///d:/workspace/embeddedTeam/SimulatorProject/fspd-qemu/hw/usb/dev-uas.c#L817)),端点号本身就是语义,没有 mode------pid 只剩"方向校验"一个作用(如 COMMAND 管道只应有 OUT 包)。UAS 把 BOT 的"mode 消歧"固化成了管道物理隔离(4 个端点各自专职)。
一句话总结:PID 是事务动词(SETUP/发/收),端点是传输通道(哪条管),mode 是 BOT 在复用通道时区分命令/数据/状态阶段的会话状态;UAS 用独立端点取代了 mode。
3.6 OUT 事务之后 PID 如何"变成" IN ------ 是的,主机又发起了新的 IN 事务
USB 是轮询式总线,主机是唯一的事务发起者 :设备从不主动发数据、也从不改变包的方向。每个事务都以主机发出 token 包开头------IN token 意为"请设备发数据"、OUT token 意为"主机要送数据"。因此 PID 不是设备"变成"的,而是主机(guest 侧驱动 + HCD)按协议约定发起的下一个新事务的属性:
- guest 软件层决定下一步方向:USB 总线协议无"阶段"概念,是 usbstor/blk 层按 BOT 协议知道"OUT 数据发完 → 下一步收 CSW",于是提交一个 EP1 IN 的 bulk 传输请求(URB / 传输描述符),其中写明方向 IN。
- guest HCD 硬件执行 :如 xHCI 驱动往 EP1 的 endpoint ring 放一个 DIR=IN 的 TRB 并敲 doorbell------真实硬件此刻向 EP1 发 IN token 包。
- QEMU 的 HCD 模拟器构造包 :
USBPacket.pid是包属性而非设备状态,由 HCD 在usb_packet_setup()(core.c L582()里设置,pid 来源是 guest 布置的传输描述符方向位:- xHCI:
xfer->in_xfer = epctx->type >> 2(hcd-xhci.c L1782,取自 TRB 方向)→xhci_setup_packet()里dir = xfer->in_xfer ? USB_TOKEN_IN : USB_TOKEN_OUT,再usb_packet_setup(&xfer->packet, dir, ...)(hcd-xhci.c L1590-1609) - EHCI:qTD token 的 PID 字段
get_field(qtd->token, QTD_TOKEN_PID)(hcd-ehci.c L418) - UHCI:TD token 的 PID 位(同源逻辑)
- xHCI:
- 设备按 pid+ep+mode 被动响应 :包经
usb_handle_packet → usb_process_one → usb_msd_handle_data(devep=1, pid=IN, mode=CSW)进入,usb_msd_send_status()回 13B CSW,mode→CBW。
写命令里 OUT→IN 的完整时序(三层软件接力):
guest usbstor: "数据发完,按 BOT 下一步是读 CSW" → 提交 EP1 IN 传输(描述符标 IN)
guest xHCI 驱动: 往 EP1 ring 放 DIR=IN 的 TRB,敲 doorbell(硬件将发 IN token)
QEMU xhci: 扫 ring → usb_packet_setup(p, USB_TOKEN_IN, ep1) → usb_handle_packet()
QEMU 设备: usb_msd_handle_data(devep=1, IN, mode=CSW) → 发 CSW → mode 回 CBW
要点:
- 方向与端点绑定:EP1 是 IN 端点、EP2 是 OUT 端点(描述符声明的物理属性),主机往 IN 端点只可能发 IN token------所以"OUT→IN"必然同时是"EP2→EP1"换管道。
- 设备侧没有反向状态 :
s->mode只决定"IN 包来的时候回什么"(DATAIN 回数据 / CSW 回状态 / DATAOUT 挂起等完成),永远不决定"下一个包是 IN 还是 OUT"------后者完全由主机选择。 - 设备未就绪时 :真实硬件对 IN token 回 NAK,主机控制器自动按 bulk 策略重试;QEMU 的等效实现是
s->packet = p; USB_RET_ASYNC挂起(§3.3),SCSI 命令完成回调里再usb_msd_send_status()+usb_packet_complete(),对 guest 表现为该传输最终完成。 - UAS 的 IN 方向(SENSE IU、DATA_IN)同理是主机发起 IN 事务;设备侧仅有
usb_wakeup()的"提醒主机尽快来轮询"通知(dev-uas.c,最终仍是主机发 IN token 取走数据。
3.7 USB_RET_ASYNC 异步机制:同步事务接口如何桥接异步后端
USB core 给设备的 handle_data/handle_control 是同步语义 (返回时数据应已就位、传输结果确定),但存储后端(SCSI → BlockBackend → 磁盘 io_uring/线程池/aio)天然异步------设备不能为了等盘而阻塞 QEMU 主线程。USB_RET_ASYNC 就是这两者之间的桥:
三种"暂时不能完成"返回值的区别:
| 返回值 | 含义 | 包的去向 | 谁来恢复 |
|---|---|---|---|
USB_RET_ASYNC |
设备接收了这个包(持有指针),数据/结果稍后才有 | core 置 USB_PACKET_ASYNC 并挂 ep->queue 尾(core.c L439-446() |
设备自己在后端回调里填数据并 usb_packet_complete() |
USB_RET_ADD_TO_QUEUE |
端点忙(前一个 ASYNC 包未完成),本包不交给设备 | usb_queue_one() 排队,状态 QUEUED |
前包完成时 usb_packet_complete() 的 while 循环自动 usb_process_one() 重放(core.c L493+() |
USB_RET_NAK |
现在没数据/没缓冲,端点没错(如中断轮询) | 不入队,直接返回 HCD | HCD 下个服务间隔再提交一个新包 |
ASYNC 包的完整生命周期:
HCD 提交 p ──usb_handle_packet──► usb_process_one ──► dev->handle_data(p)
│ 返回 USB_RET_ASYNC
▼
p.state=ASYNC,挂 ep->queue;HCD 传输保持 pending
(不产生完成事件,guest 看到的是"传输进行中")
⋯⋯ 后端异步工作(磁盘 dma/aio BH / SCSI 状态机推进)⋯⋯
后端回调(如 usb_msd_command_complete)
├─ 往 p->iov 填数据(usb_packet_copy),p->status = USB_RET_SUCCESS
├─ 设备先把自己的包指针清空(防重入),再调 usb_packet_complete(dev, p)
▼
usb_packet_complete_one:从 ep->queue 摘除 → COMPLETE → port->ops->complete()
▼
HCD complete 回调:传输描述符写完成状态 → 稍后中断通知 guest(DMA 数据已在 guest 缓冲)
▼
while 循环:同一 ep->queue 里若还有 QUEUED 包(ADD_TO_QUEUE 的),依次重放
与真实硬件的对应:真实设备对"来了 IN token 但数据没准备好"的响应是 NAK ,主机控制器按 bulk 调度策略自动无限重试,直到设备有数据时在某次重试里应答。QEMU 不逐个模拟 NAK/重试(省掉海量无效事务),而是把 HCD 的传输描述符一直挂起、设备就绪时一次完成------guest 可见行为等价(传输延迟后成功,或 STALL 失败),差别只在仿真内部。
设备侧契约(违反即挂死/断言):
- 返回 ASYNC 必须最终 complete 一次(成功或 STALL),否则 guest 该 URB 永久挂起
- complete 前先把私有包指针置 NULL(usb_msd_packet_complete L180-192(file:///d:/workspace/embeddedTeam/SimulatorProject/fspd-qemu/hw/usb/dev-storage.c#L180-L192)):
complete → HCD 回调 → guest 可能同步提交下一个包,调用链会重入 handle_data,旧指针会被覆盖 - complete 时 status 不能还是 ASYNC/NAK(
usb_packet_complete_one有 assert) - 非 pipeline 端点必须保序:ASYNC 包未完成时,后续包只能 ADD_TO_QUEUE
- isoc 禁止 async、interrupt async 仅限 host 侧设备(core 有 assert)
BOT 的挂起槽位是单包 s->packet (串行协议,任一时刻至多一个包在设备手里),三类包复用它:写数据包(等 SCSI 缓冲)、读数据包(等 SCSI 数据)、提前到达的 CSW 读包(等命令完成)。UAS 则是多槽:USB2 用 datain2/dataout2/status2 各一,USB3 按 stream 用 data3[tag]/status3[tag] 数组支持并发。