QEMU 模拟器学习(二)之 USB 设备模拟与协议学习

笔者学习qemu 模拟器,今天讲一下usb设备这块

基于三个文件进行分析

  1. hw/usb/core.c --- USB 控制传输与数据(Bulk)传输的通用逻辑
  2. hw/usb/dev-storage.c --- BOT(Bulk-Only Transport,协议号 0x50)协议解析
  3. 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 的两个作用:

  1. 事务类型/路由:上面的分流 + 控制端点上 SETUP/IN/OUT 的分发
  2. 数据方向: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)按协议约定发起的下一个新事务的属性:

  1. guest 软件层决定下一步方向:USB 总线协议无"阶段"概念,是 usbstor/blk 层按 BOT 协议知道"OUT 数据发完 → 下一步收 CSW",于是提交一个 EP1 IN 的 bulk 传输请求(URB / 传输描述符),其中写明方向 IN。
  2. guest HCD 硬件执行 :如 xHCI 驱动往 EP1 的 endpoint ring 放一个 DIR=IN 的 TRB 并敲 doorbell------真实硬件此刻向 EP1 发 IN token 包。
  3. 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 位(同源逻辑)
  4. 设备按 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 失败),差别只在仿真内部。

设备侧契约(违反即挂死/断言):

  1. 返回 ASYNC 必须最终 complete 一次(成功或 STALL),否则 guest 该 URB 永久挂起
  2. 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,旧指针会被覆盖
  3. complete 时 status 不能还是 ASYNC/NAK(usb_packet_complete_one 有 assert)
  4. 非 pipeline 端点必须保序:ASYNC 包未完成时,后续包只能 ADD_TO_QUEUE
  5. isoc 禁止 async、interrupt async 仅限 host 侧设备(core 有 assert)

BOT 的挂起槽位是单包 s->packet (串行协议,任一时刻至多一个包在设备手里),三类包复用它:写数据包(等 SCSI 缓冲)、读数据包(等 SCSI 数据)、提前到达的 CSW 读包(等命令完成)。UAS 则是多槽:USB2 用 datain2/dataout2/status2 各一,USB3 按 stream 用 data3[tag]/status3[tag] 数组支持并发。

相关推荐
shimly1234562 小时前
(undone) 解析 qemu-6.0.0 源码 (4) 添加 niubi board - 2
qemu
冰山一脚20135 小时前
cirrus显卡驱动笔记
qemu·drm
你的强来了9986 小时前
DriverSwitcher:Windows USB设备驱动切换工具
驱动·usb·驱动切换工具
shimly1234567 小时前
(done) 解析 qemu-6.0.0 源码 (2) 寻找入口函数 main
qemu
我是谁??12 天前
VMware 虚拟机无法识别 USB 设备?一个参数搞定!
vmware·usb
橘色的喵12 天前
USB枚举、协商、传输:以USB相机为例看识别与格式协商
串口·usb·设备枚举
ggaofeng19 天前
QEMU中硬盘用哪种协议读写速度快
qemu·磁盘类型
小黑(感觉你阻抗≥1MΩ)22 天前
TYPE-C电路
usb·电路·原理图·type-c
一个平凡而乐于分享的小比特22 天前
解剖USB控制器移植:从VeriSilicon到MindMotion的“外科手术式”改造
usb·verisilicon·mindmotion