news 2026/10/1 15:37:38

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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_DATADATA 阶段(收/发 data_buf)
SETUP_STATE_ACK状态阶段(无数据控制传输直接进入)
SETUP_STATE_PARAMxHCI 参数化传输(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 bit7DATA 阶段STATUS 阶段(零长包)
控制读(IN 方向请求)1(USB_DIR_IN)IN 事务若干次(设备→主机发 data_buf)OUT 事务:主机确认已收数据
控制写(OUT 方向请求)0OUT 事务若干次(主机→设备收 data_buf);wLength==0则直接进 STATUSIN 事务:设备确认已处理

注意方向规则:STATUS 阶段方向与 DATA 阶段恒相反,且 STATUS 事务是零长度包(写完请求后p->actual_length = 0)。

2.2a do_token_out([core.c L229])——主机发 OUT 事务时干什么

按setup_state × 请求方向四个分支(见代码 switch):

setup_state请求方向场景动作
DATAOUT(!(setup_buf[0]&USB_DIR_IN))控制写的数据阶段usb_packet_copy()把包数据拷入data_buf + setup_index;setup_index >= setup_len时收满转 ACK 态
DATAIN协议错误(读请求不该有 DATA-OUT)置SETUP_STATE_IDLE+USB_RET_STALL
ACKIN控制读的状态阶段:SETUP→DATA-IN→这里的 OUT主机零长包确认收完数据 → 设备转 IDLE,usb_pcap_ctrl(p,false)抓包收尾(transfer OK)
ACKOUT写请求 ACK 态后又来 OUT容错:忽略多余输出(ignore additional output)
其它(SETUP/IDLE/PARAM)—协议错误USB_RET_STALL

2.3 do_token_in([core.c L181] ——主机发 IN 事务时干什么

setup_state请求方向场景动作
DATAIN(setup_buf[0]&USB_DIR_IN)控制读的数据阶段从data_buf + setup_index拷出min(setup_len - setup_index, p->iov.size)到包;发完转 ACK 态
DATAOUT协议错误(写请求不该有 DATA-IN)置 IDLE +USB_RET_STALL
ACKOUT(!(...USB_DIR_IN))控制写的状态阶段:设备此刻才真正执行请求调用usb_device_handle_control()(此时 data_buf 已收满——SETUP 阶段故意不执行的 OUT 请求在此执行);USB_RET_ASYNC则挂起等usb_generic_async_ctrl_complete;否则转 IDLE、actual_length=0(回零长状态包)
ACKIN读请求 ACK 态又来 IN静默忽略(break,无动作,不 STALL)
其它(SETUP/IDLE/PARAM)—协议错误USB_RET_STALL

2.3a BOT 设备(usb-storage)EP0 上的典型事务序列

请求类型完整序列各步落点
GET_DESCRIPTOR(枚举)控制读SETUP → DATA-IN×n → STATUS-OUTdo_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-INdo_token_setup不执行 → 直接转 ACK(wLength==0)→do_token_in(ACK) 才激活配置
MassStorageReset (0xff)控制写(无数据,BOT 类)SETUP → STATUS-INdo_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_ADDRESSdev->addr = value——设备获得总线地址,此后 HCD 用usb_find_device(port, addr)按地址路由
GET_DESCRIPTORusb_desc_get_descriptor()返回 device / config / string / qualifier / BOS 等描述符(枚举核心)
GET_CONFIGURATION返回当前bConfigurationValue(未配置返回 0)
SET_CONFIGURATIONusb_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_DELAYSuperSpeed 专用,直接成功
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->nrUSB 管道拓扑0(EP0)/ 1~15包走哪条管道:0=控制(枚举/类请求),非 0=数据传输
PIDp->pidUSB 事务层SETUP / IN / OUT包的“动词”:SETUP=控制传输起点;IN=主机收(设备→主机);OUT=主机发(主机→设备)
设备模式s->modeBOT 应用层(设备私有)CBW / DATAIN / DATAOUT / CSWBOT 独有的“会话阶段标签”:同一端点在不同阶段承载不同内容

分流点在 [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 语义消歧表:

deveppids->mode主机在做什么设备动作
2OUTCBW发 31B CBW 命令解析 CBW → scsi_req_new/enqueue;按 data_len/flags 切换 mode
2OUTDATAOUT发写数据usb_msd_copy_data → SCSI 缓冲
2OUT其他—STALL(goto fail)
1INDATAIN收读数据SCSI 缓冲 → 包
1INCSW收 13B CSW 状态usb_msd_send_status → 回 CBW 态
1INDATAOUT写命令未完成时提前发 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现在没数据/没缓冲,端点没错(如中断轮询)不入队,直接返回 HCDHCD 下个服务间隔再提交一个新包

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]数组支持并发。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/1 15:36:54

Bun.js 全面解析:从 Claude Code 泄漏事件说起的前端新势力

1. 引言&#xff1a;从 Claude Code 泄漏事件说起 2025 年初&#xff0c;一则关于 Claude Code 的代码泄漏事件在开发者社区引发热议。事件中&#xff0c;一段疑似 Claude Code 内部实现的代码片段被意外公开&#xff0c;而细心的开发者发现&#xff0c;这段代码的运行环境并非…

作者头像 李华
网站建设 2026/10/1 15:36:51

资质文件标准化归档方法,节省大量时间

做投标、办资质年审、应对合规审查的人&#xff0c;最头疼的往往不是 "办资质"&#xff0c;而是 "找资质"。翻翻你的电脑&#xff1a;一堆 "新建文件夹 (3)"、"最终版 (1)"、"绝对不改版"&#xff0c;营业执照、资质证书、检…

作者头像 李华
网站建设 2026/10/1 15:36:49

5分钟部署Utopia:一条Docker命令搭建企业知识库的速成教程

5分钟部署Utopia&#xff1a;一条Docker命令搭建企业知识库的速成教程 【免费下载链接】utopia 首个开源企业世界模型 项目地址: https://gitcode.com/deeplethe/utopia Utopia 是首个开源企业世界模型&#xff0c;由 DeepLethe 构建。它把时间感知与本体论融入底层&…

作者头像 李华