QEMU 模拟器学习(二)之 USB 设备之BOT & UAS协议

笔者来介绍一下QEMU 里面USB得BOT 以及UAS协议

4. BOT 协议解析(dev-storage.c,bInterfaceProtocol=0x50)

接口:2 个 Bulk 端点 ------ EP1 IN (数据读 / CSW)、EP2 OUT(CBW / 数据写)。

4.0 BOT 包格式字节级定义:CBW 与 CSW

结构体定义:dev-storage.c L30(cbw)/ msd.h L20(csw)。BOT 只有两种协议包,全部字段小端(Little-Endian);数据阶段夹在两者之间,不属于任何协议包。

CBW(Command Block Wrapper,主机→设备,EP2 OUT,固定 31 字节,s->mode==USB_MSDM_CBW 时接收)
偏移 长度 字段 值/含义 QEMU 处理
0 4 sig LE 0x43425355(ASCII "USBC") 签名校验(L425,不匹配 → STALL
4 4 tag 主机自选命令标识,任意值 CSW 原样回填 ;QEMU 直接当 SCSI tag 传给 scsi_req_new
8 4 data_len dCBWDataTransferLength:主机预期数据阶段总字节数 存入 s->data_len 驱动状态机:==0 → CSW;>0 看 flags 方向位
12 1 flags bit7=0x80 为数据方向,其余位保留 0x80=IN(设备→主机,读命令)→ mode=DATAIN;否则 OUT → mode=DATAOUT
13 1 lun bCBWLUN,0~15 scsi_device_find,不存在 → STALL
14 1 cmd_len 有效值 1~16 直接传 scsi_req_new(..., cbw.cmd_len)(非法长度由 SCSI 层拒绝)
15 16 cmd[16] SCSI CDB(WRITE10/READ10/INQUIRY...) 有效字节由 cmd_len 决定,多余忽略

包长不是 31 直接 Bad CBW size → STALL(L420-423。

CSW(Command Status Wrapper,设备→主机,EP1 IN,固定 13 字节,由 usb_msd_command_complete 填充)
偏移 长度 字段 值/含义 QEMU 处理
0 4 sig LE 0x53425355(ASCII "USBS" ,与 CBW 的 "USBC" 仅末字符不同:0x53='S') command_complete 填入并有 assert(
4 4 tag 回填对应 CBW 的 tag,主机靠它配对命令 csw.tag = cpu_to_le32(req->tag)
8 4 residue dCSWDataResidue:data_len 声明与实际传输量之差 csw.residue = cpu_to_le32(s->data_len)(正常为 0;短读/写、介质满时非 0)
12 1 status bCSWStatus,见下表 req->status != 0 → 非 0 即 1

bCSWStatus 取值:

值 名称 含义 QEMU 生成条件
0x00 COMMAND_PASSED 命令成功 req->status == 0(SCSI GOOD)
0x01 COMMAND_FAILED 命令失败 req->status != 0(sense 数据需主机另发 REQUEST_SENSE 获取)
0x02 PHASE_ERROR 阶段错误(如方向不符) QEMU 不生成------协议违规直接 STALL 端点,走 MassStorageReset 恢复
数据阶段包(DATAOUT / DATAIN):无协议封装的裸 SCSI 数据

与 CBW/CSW 不同,数据阶段的载荷没有任何协议头 ------不含 tag、长度、CRC 或任何协议字段,就是磁盘扇区内容的原样字节流(scsi_req_get_buf() 缓冲直通,非 512 倍数也照传):

属性 值
协议封装 无(纯裸数据)
归属识别 隐式:CBW 已声明 data_len+方向,s->mode 状态机记住"当前包属于哪条命令"
单事务上限 端点 max_packet_size(FS 64B / HS 512B / SS 1024B,SuperSpeed burst 可连发 16 包)
总量 恒等于 CBW.data_len,QEMU 用 s->data_len 递减计账
结束信号 data_len 减到 0;短包 (actual_length < iov.size)表示提前结束(残留场景,配 usb_packet_skip 吞余量)
字节来源/去向 scsi_req_get_buf(s->req) + s->scsi_off,由 usb_msd_copy_data() 搬运

usb_msd_copy_data()(dev-storage.c L211-226搬运公式:

复制代码
len = MIN(包剩余 iov.size - actual_length, SCSI 缓冲剩余 s->scsi_len)
usb_packet_copy(p, scsi_req_get_buf(s->req) + scsi_off, len)  ← OUT: iov→缓冲;IN: 缓冲→iov
scsi_len -= len; scsi_off += len; data_len -= len
scsi_len==0 或 data_len==0 → scsi_req_continue()(要下一段缓冲或收尾)

物理层的 DATA0/DATA1 PID 翻转、CRC16、token/handshake 帧格式不属于设备模型 ------QEMU 中由 HCD 模拟(xHCI 按 TRB 处理),设备只见 p->iov 字节。xHCI 还可能把多个物理包聚合为一个 p->combined 大包(§3.4),聚合与否不改变"裸数据"本质。

4.1 状态机 s->mode

复制代码
USB_MSDM_CBW ──收31B CBW──┬─ data_len==0 ──────────────→ USB_MSDM_CSW
                           ├─ flags&0x80 (d2h) ───────→ USB_MSDM_DATAIN
                           └─ else (h2d) ─────────────→ USB_MSDM_DATAOUT
USB_MSDM_DATAOUT ──data_len 减到 0──→ USB_MSDM_CSW
USB_MSDM_DATAIN  ──data_len 减到 0──→ USB_MSDM_CSW
USB_MSDM_CSW     ──IN 端点读走 13B CSW──→ USB_MSDM_CBW

4.2 命令(CBW)传输 ------ OUT 端点(dev-storage.c L399

USB_TOKEN_OUT + s->mode == USB_MSDM_CBW:

  1. 包长必须 31 字节,usb_packet_copy() 读出 struct usb_msd_cbw
  2. 校验签名 0x43425355('USBC')、LUN 存在
  3. 按 data_len/flags 切换 DATAIN/DATAOUT/CSW 模式
  4. scsi_req_new(scsi_dev, tag, cbw.lun, cbw.cmd, cbw.cmd_len, NULL) ------ CBW.cmd16 即 SCSI CDB
  5. scsi_req_enqueue();有数据阶段则 scsi_req_continue()

4.3 数据传输

  • DATAOUT(主机→设备写) :usb_msd_copy_data() 把包数据拷到 scsi_req_get_buf(s->req) + scsi_off,scsi_len 用完或 data_len 清零时 scsi_req_continue() 继续要缓冲;包未填满则 s->packet = p; p->status = USB_RET_ASYNC 挂起等 SCSI 回调
  • DATAIN(设备→主机读) :同样经 usb_msd_copy_data() 从 SCSI 缓冲拷入包
  • SCSI 回调 usb_msd_transfer_data()(dev-storage.c L242(file:///d:/workspace/embeddedTeam/SimulatorProject/fspd-qemu/hw/usb/dev-storage.c#L242)):SCSI 层给了新缓冲(len),若有挂起包则继续 copy;包满则完成包

4.3a 一条写命令:1 个 CBW + N 个 OUT 包(不是多个 CBW)

CBW 数量恒等于 SCSI 命令数量;一条命令的数据由多个 OUT 事务分片流式传完。 以 high-speed 4KB WRITE(10) 为例:

复制代码
主机                                   设备(QEMU)
│  1 个 CBW(31B) ──EP2 OUT──→          │  s->data_len = cbw.data_len = 4096;mode: CBW→DATAOUT
│                                      │  scsi_req_new()(L448 只此一次)
│  OUT 包 #1 (≤512B)                   │  usb_msd_copy_data();data_len -= 512
│  OUT 包 #2 ... #8 (共 8 个)            │  scsi 缓冲用完 → scsi_req_continue()(L224-225)再要缓冲
│  (4096 字节发完)                    │  data_len == 0 → mode: CSW(L475)
│  ←──EP1 IN── 1 个 CSW(13B)────────   │  命令完成,回状态 → mode: CBW

要点:

  • 单个 USB 事务的上限是端点最大包长,不是 512 :full-speed bulk 64B;high-speed bulk 512B (4KB = 8 个 OUT 包);SuperSpeed bulk 1024B 且 bMaxBurst=15(一个 burst 连续 16 包 ≈ 16KB)。分片对设备透明------usb_msd_copy_data()(L211)按 p->iov.size 搬运并累加 scsi_off/递减 data_len,无"包必须等于扇区大小"之类约束。
  • 数据中途绝不允许第二个 CBW:mode 停在 DATAOUT/DATAIN,此时 EP2 再来 OUT 包只会被当作数据,非数据阶段合法的包一律 STALL。
  • 严格串行 :主机必须等到 CSW 回来、状态机回到 USB_MSDM_CBW 后才能发下一个 CBW(BOT 单状态机,无并发)。
  • 多个 CBW 只对应多条独立 SCSI 命令 :大块写(如 1MB)由 guest 块层拆成多条 WRITE(10)(各自带 LBA/传输长度的 CDB),于是出现多个独立的 CBW → N 个数据包 → CSW 序列;每条命令内部仍是 1 CBW + N 包。
  • 读命令(DATAIN)同理:1 个 CBW + N 个 IN 数据包 + 1 个 CSW。

4.4 状态(CSW)回传 ------ IN 端点

usb_msd_command_complete()(dev-storage.c L264填 13 字节 CSW:

  • sig='USBS'(0x53425355)、tag(回填 CBW.tag)、residue = s->data_len(残留)、status = req->status != 0
  • usb_msd_send_status() 用 usb_packet_copy() 把 CSW 发给 IN 包,之后回到 CBW 态
  • 特殊时序:写命令的"status read 包"(DATAOUT 态 IN 端点收到的包)也是异步挂起、等命令完成后回 CSW

4.4a 写场景:数据发完但盘还没写完,主机提前发 IN 状态读包怎么办

背景 :主机把写数据发完(自己一侧的 OUT URB 全部完成),不等于设备把数据落盘------数据还在 SCSI/BlockBackend 流水线里。BOT 协议规定主机此时就在 EP1 排队一个 IN 传输等 CSW(驱动常把 OUT 数据 URB 和 IN 状态 URB 一起提交,靠传输顺序保证不乱)。这个 IN 包合法但可能提前到达,分三种情况:

IN 包到达时设备状态 代码分支 处理
mode==DATAOUT 且 data_len!=0(写数据还没到齐) L497-500 协议错误 → STALL:主机不可能在数据阶段未结束时读 CSW
mode==DATAOUT 且 data_len==0、包 ≥13B(数据齐了,SCSI 写未完成) L497-505 挂起 IN 包 :s->packet = p; p->status = USB_RET_ASYNC,注释原文 "Waiting for SCSI write to complete"。mode 保持 DATAOUT,不回 CSW
mode==CSW 且 s->req 非空(命令仍 inflight) L512-516 同样 ASYNC 挂起等完成
mode==CSW 且 s->req==NULL(已完成) L517-520 立即 usb_msd_send_status() 回 CSW,mode→CBW

情况 2 的完整生命周期(你问的场景):

复制代码
① 最后一批写数据:OUT 包把 s->data_len 减到 0
   ├─ 注意:mode 此刻仍是 DATAOUT(DATAOUT→CSW 的迁移在 L475 只发生于
   │   csw.residue 非 0 的异常分支;正常路径 data_len==0 不在这里改 mode)
   └─ 若 scsi 缓冲也耗尽,L480 的包未满条件不成立则本 OUT 包同步完成;
      否则该 OUT 包自己也 ASYNC 挂在 s->packet 上等 usb_msd_transfer_data

② 主机的 IN 状态读包到达 EP1(devep==1, pid==IN, mode==DATAOUT, data_len==0)
   └─ L501-504:s->packet = <IN包>,返回 USB_RET_ASYNC
      → core 把它挂到 EP1 的队列,HCD 的 IN 传输保持 pending,guest 阻塞在等 CSW

③ 磁盘写完 → SCSI 总线回调 usb_msd_command_complete(req, resid)(L264)
   ├─ 先填好 s->csw(sig/tag/residue/status)
   ├─ s->packet 非空 → 进入 L277 判断:
   │     data_len==0 && mode==DATAOUT  ⇒ "A deferred packet ... must be
   │     the status read packet"(L278-279 注释)
   ├─ usb_msd_send_status(s, p):13B CSW 拷进那个挂起的 IN 包 iov
   ├─ s->mode = USB_MSDM_CBW          ← 状态机在这一刻才复位
   ├─ p->status = USB_RET_SUCCESS
   ├─ usb_msd_packet_complete(s):先 s->packet=NULL(防重入)→ usb_packet_complete
   └─ scsi_req_unref;s->req = NULL

④ core 完成回调 → HCD 通知 guest:IN 传输完成、缓冲里是 13B CSW
   └─ guest usbstor 校验 tag,本次 CBW 事务结束,可以发下一条命令

两个关键设计点:

  • mode 不为挂起包提前跳转:DATAOUT→CSW 的正常迁移其实发生在 SCSI 完成回调里(L281),而非数据收齐时。这样若主机违规在数据未齐时发 IN,状态机仍能识别错误并 STALL,而不是误给 CSW。
  • 设备只持有一个包,但 IN/OUT 包不会争用 s->packet :最后一个 OUT 包在 usb_msd_transfer_data 喂入 SCSI 缓冲时就被完成(L257-260);等 IN 包到达挂起时,OUT 方向早已没有在设备手里的包。command_complete 用 data_len==0 && mode==DATAOUT 这个组合来辨认"手里的挂起包是状态读包而非数据包"(L277),与 CSW 态分支(L282)殊途同归。

异常变体:csw.residue != 0(SCSI 实际接受的数据少于 CBW 声明,如介质容量不足)时,设备不 STALL,而是用 usb_packet_skip() 吞掉主机多给的 OUT 字节 / 少给 IN 字节(L467-479、L528-540),最终仍回一个带 residue 的 CSW 让主机知晓。

本节完整时序图(含"提前发 IN 状态读包→ASYNC 挂起→盘写完回填 CSW"全程)见 §4.6 图 A;读命令、无数据命令、core 层排队/STALL 机制分别见 §4.6 图 B/C/D。

4.5 类请求(usb_msd_handle_control)

  • MassStorageReset (0xff):s->mode = USB_MSDM_CBW 复位状态机
  • GetMaxLun (0xfe):遍历 SCSI 总线返回最大 LUN
  • 其余先走 usb_desc_handle_control()(标准描述符请求),未识别则 STALL

4.6 全流程时序图(PlantUML):usb_handle_packet → BOT → SCSI → usb_packet_complete

参与者对照(全部图通用):G=Guest(usbstor + xHCI 驱动)、H=QEMU HCD 模拟器(hcd-xhci.c)、C=USB core(core.c)、D=usb-storage 设备(dev-storage.c)、K=SCSI 层(scsi-bus.c + scsi-disk.c)、B=BlockBackend(aio/线程池)。

方向约定:实线 = 提交/调用(G→H→C→D→K→B),虚线 = 返回/异步完成回调(B→K→D→C→H→G)。

图 A:写命令------数据收完、盘未写完,主机提前发 IN 状态读包(§4.4a 情形2,最典型)
plantuml 复制代码
@startuml
title 图A BOT写命令:CBW → 数据OUT×N → [提前到达的IN状态读包] → 盘写完 → 回CSW

participant G as "Guest\nusbstor + xHCI驱动"
participant H as "HCD\nhcd-xhci.c"
participant C as "USB core\ncore.c"
participant D as "usb-storage\ndev-storage.c"
participant K as "SCSI\nscsi-bus/disk"
participant B as "BlockBackend\n(aio)"

== ① CBW 下发(EP2 OUT,同步完成) ==
G -> H : 提交 31B OUT 传输(CBW,内含 WRITE10 CDB)
H -> C : usb_packet_setup(p, OUT, ep2)\nusb_handle_packet(p)
C -> C : usb_process_one:ep!=0\n→ usb_device_handle_data
C -> D : usb_msd_handle_data\n(devep=2, OUT, mode=CBW)
D -> D : 校验CBW签名/LUN\ns->data_len=cbw.data_len\nmode: CBW→DATAOUT
D -> K : scsi_req_new(cdb=cbw.cmd)\nscsi_req_enqueue(返回len)\n→ scsi_req_continue
K -> D : 同步回调 scsi_req_data\n→ usb_msd_transfer_data(len)\n(首次:分配整段写缓冲)\ns->scsi_len = len
D --> C : CBW 包 SUCCESS(包满,同步)
C --> H : usb_pcap + COMPLETE\nport->ops->complete(p)
H --> G : TRB 完成 → 中断

== ② 数据阶段(EP2 OUT × N 包,缓冲已备好 → 同步完成) ==
loop N 个 OUT 数据包(HS 512B/包)
  G -> H : 提交 OUT 数据包
  H -> C : usb_handle_packet(p)
  C -> D : usb_msd_handle_data\n(mode=DATAOUT)
  D -> K : usb_msd_copy_data\n包→scsi_req_get_buf+scsi_off\ndata_len/scsi_len 递减
  alt 最后一个包:scsi_len 用尽
    K -> B : scsi_req_continue→scsi_write_data\n→blk_aio_pwritev(异步落盘)
  end
  D --> C : SUCCESS(包满,同步)
  C --> H : complete(p) → 中断
end
note over D
此时 s->data_len==0,但 mode 仍是 DATAOUT
(正常路径不迁移,DATAOUT→CSW 发生在
command_complete 里,见 §4.4a)
end note

== ③ 主机提前发 IN 状态读包(盘还没写完) ==
G -> H : 提交 13B IN 传输(读 CSW)
H -> C : usb_packet_setup(p, IN, ep1)\nusb_handle_packet(p)
C -> D : usb_msd_handle_data\n(devep=1, IN, mode=DATAOUT)
D -> D : data_len==0 且包≥13B\n(若 data_len!=0 → fail→STALL)
D --> C : s->packet = p\nUSB_RET_ASYNC\n"Waiting for SCSI write to complete"
C -> C : 状态置 ASYNC,挂 ep->queue\nHCD 传输保持 pending
note over G,H : 无完成事件\nguest 的 CSW URB 挂起等待

== ④ 盘写完 → 完成回调回填挂起的 IN 包 ==
B --> K : aio 写完成
K --> D : scsi_req_complete\n→ usb_msd_command_complete\n(bus->info->complete 回调)
D -> D : 填 s->csw(sig/tag/residue/status)\ndata_len==0 && mode==DATAOUT\n⇒ 挂起包是 status read packet
D -> D : usb_msd_send_status\n13B CSW 拷入挂起 IN 包\nmode→CBW(此刻才复位)
D --> C : p->status=SUCCESS\ns->packet=NULL(防重入)\n→ usb_packet_complete(dev,p)
C --> H : 出队→COMPLETE\nport->ops->complete(p)
H --> G : IN 传输完成(13B CSW)→ 中断
G -> G : usbstor 校验 tag,命令结束
@enduml
图 B:读命令------IN 数据包 ASYNC 挂起,usb_msd_transfer_data 回调回填
plantuml 复制代码
@startuml
title 图B BOT读命令:CBW → IN数据包(ASYNC) → 盘读完成transfer_data回填 → CSW

participant G as "Guest\nusbstor + xHCI驱动"
participant H as "HCD\nhcd-xhci.c"
participant C as "USB core\ncore.c"
participant D as "usb-storage\ndev-storage.c"
participant K as "SCSI\nscsi-bus/disk"
participant B as "BlockBackend\n(aio)"

== ① CBW(EP2 OUT):READ10 ==
G -> H : 31B OUT 传输
H -> C : usb_handle_packet
C -> D : usb_msd_handle_data\n(EP2 OUT, mode=CBW)
D -> D : flags&0x80 ⇒ mode: CBW→DATAIN\ns->data_len = cbw.data_len
D -> K : scsi_req_new + scsi_req_enqueue\nscsi_req_continue → scsi_read_data\n→ blk_aio_preadv(异步读盘)
D --> C : SUCCESS(CBW 包同步完成)

== ② 首个 IN 数据包:盘数据未回 → ASYNC 挂起 ==
G -> H : IN 传输(ep1)
H -> C : usb_handle_packet
C -> D : usb_msd_handle_data\n(EP1 IN, mode=DATAIN)
D -> D : s->scsi_len==0(数据未到)\n包未满 → s->packet = p
D --> C : USB_RET_ASYNC
C -> C : 挂 ep->queue(ASYNC)

== ③ 盘读完成 → transfer_data 回调回填 ==
B --> K : aio 读完成
K --> D : scsi_req_data\n→ usb_msd_transfer_data(req, len)\ns->scsi_len=len, scsi_off=0
D -> D : usb_msd_copy_data\nSCSI缓冲→挂起IN包iov\n包满 → SUCCESS
D --> C : usb_msd_packet_complete\n(先 s->packet=NULL)\n→ usb_packet_complete
C --> H : complete(p) → 中断
H --> G : IN 数据传输完成

== ④ 后续数据包:缓冲有余 → 同步完成 ==
loop 剩余数据包
  G -> H : 下一个 IN 数据包
  H -> C : usb_handle_packet
  C -> D : mode=DATAIN, scsi_len>0
  D -> D : usb_msd_copy_data 拷出(同步填包)
  alt scsi_len 用尽且 data_len>0
    D -> K : scsi_req_continue\n→ 下一块 blk_aio_preadv\n(下一包将再次 ASYNC 等回调)
  else 最后一块拷完(data_len==0)
    D -> K : scsi_req_continue → scsi_read_data
    K --> D : sector_count==0\n→ scsi_req_complete\n→ usb_msd_command_complete\n(重入发生在本包handle_data内)\ns->packet==NULL,data_len==0\n⇒ mode→CSW
  end
  D --> C : SUCCESS(同步)
  C --> H : complete(p)
end

== ⑤ 主机读 CSW(同步完成) ==
G -> H : 13B IN(ep1)
H -> C : usb_handle_packet
C -> D : usb_msd_handle_data\n(mode=CSW, s->req==NULL)
D -> D : usb_msd_send_status → mode→CBW
D --> C : SUCCESS(同步)
C --> H : complete(p) → 中断
H --> G : 收到 CSW,命令结束

note over D,K
变体:命令完成时仍有未填满的挂起数据包(短读/残数)→
command_complete 的 else 分支(L283-291)用 usb_packet_skip
补齐,包以 actual_length<iov.size(短包)完成,mode→CSW。
end note
@enduml
图 C:无数据命令(data_len==0)------CSW 读包先到、命令后完成(CSW 态挂起分支)
plantuml 复制代码
@startuml
title 图C BOT无数据命令:CSW读包先到 → CSW态ASYNC挂起 → 命令完成后回填

participant G as "Guest\nusbstor + xHCI驱动"
participant H as "HCD\nhcd-xhci.c"
participant C as "USB core\ncore.c"
participant D as "usb-storage\ndev-storage.c"
participant K as "SCSI\nscsi-disk.c"
participant B as "BlockBackend"

== ① CBW(SYNCHRONIZE CACHE,data_len==0) ==
G -> H : 31B OUT(CBW)
H -> C : usb_handle_packet
C -> D : usb_msd_handle_data\n(EP2 OUT, mode=CBW)
D -> D : data_len==0 ⇒ mode: CBW→**CSW**\n(跳过数据阶段)
D -> K : scsi_req_new + scsi_req_enqueue\nsend_command 返回 0\n→ blk_aio_flush(异步,立即返回)
D --> C : SUCCESS(CBW 包同步完成)

== ② 主机 IN 读 CSW,命令在途 → 挂起(CSW 态分支) ==
G -> H : 13B IN(ep1)
H -> C : usb_handle_packet
C -> D : usb_msd_handle_data\n(EP1 IN, mode=CSW)
D -> D : s->req != NULL(命令未完成)
D --> C : s->packet = p\nUSB_RET_ASYNC
C -> C : 挂 ep->queue,HCD 传输 pending

== ③ flush 完成 → command_complete 的 CSW 分支回填 ==
B --> K : aio flush 完成
K --> D : scsi_req_complete\n→ usb_msd_command_complete
D -> D : mode==CSW 分支(L282-284)\nusb_msd_send_status\n13B CSW 拷入挂起包\nmode → CBW
D --> C : SUCCESS;s->packet=NULL\n→ usb_packet_complete
C --> H : complete(p) → 中断
H --> G : 收到 CSW,命令结束

note over D,K
对照(全同步路径):TEST_UNIT_READY 等无数据命令在
scsi_disk_emulate_command 尾部同步 scsi_req_complete
(scsi-disk.c L2283-2285)→ 到 IN CSW 包到达时
s->req==NULL,走 L517-520 立即 send_status,全程无 ASYNC。
end note
@enduml
图 D:core 层通用机制------ADD_TO_QUEUE 保序重放 与 STALL/halted 清队
plantuml 复制代码
@startuml
title 图D USB core:ASYNC在途时后续包排队保序;STALL后halted清空队列

participant G as "Guest"
participant H as "HCD"
participant C as "USB core\ncore.c"
participant D as "设备\nhandle_data"

== 情形1:ASYNC 包在途,后续包 ADD_TO_QUEUE 保序 ==
G -> H : 包 P1
H -> C : usb_handle_packet(P1)
C -> D : usb_process_one → handle_data(P1)
D --> C : USB_RET_ASYNC(P1 由设备持有)
C -> C : P1 挂 ep->queue(ASYNC)
G -> H : 包 P2(同端点,非 pipeline)
H -> C : usb_handle_packet(P2)
C -> C : 队列非空且无流水线\n→ usb_queue_one(P2)
C --> H : P2 状态 QUEUED(不交设备)
note over C : P2 不进设备,避免与 P1 乱序\n(pipeline 端点例外,直接处理)

== 前包完成,while 循环自动重放 ==
D --> C : (后端回调)usb_packet_complete(dev, P1)
C -> C : complete_one(P1):出队\n→ port->ops->complete(P1)
C --> H : P1 完成
H --> G : P1 传输完成
C -> C : while:ep->queue 还有 QUEUED 的 P2
C -> D : usb_process_one(P2)(自动重放)
D --> C : P2 结果\n(SUCCESS→立即complete;\nASYNC→挂起后break)
C --> H : complete(P2)(若同步完成)

== 情形2:设备 STALL → 端点 halted + 队列清空 ==
G -> H : 包 P3
H -> C : usb_handle_packet(P3)
C -> D : usb_process_one → handle_data(P3)
D --> C : USB_RET_STALL\n(协议违规,如数据未齐就来读 CSW)
C -> C : complete_one(P3)\nstatus!=SUCCESS ⇒ ep->halted=true
C -> C : while 循环:halted ⇒\n队列中 QUEUED 包全部以\nREMOVE_FROM_QUEUE 完成
C --> H : complete(P3)(STALL)
H --> G : 传输失败
G -> C : CLEAR_FEATURE(ENDPOINT_HALT)\n或 BOT 类请求 MassStorageReset(0xff)\n(EP0,复位 s->mode)
note over C
halted 端点来新包时 usb_handle_packet
L432-435 自动清除 halt(要求队列为空)。
end note
@enduml

5. UAS 协议解析(dev-uas.c,bInterfaceProtocol=0x62)

接口:4 个 Bulk 端点(管道),全部带 bDescriptorType 0x24 管道 ID 扩展描述符:

端点 管道 ID 方向 用途
EP1 UAS_PIPE_ID_COMMAND(0x01) OUT 收 IU:COMMAND / TASK_MGMT
EP2 UAS_PIPE_ID_STATUS(0x02) IN 发 IU:SENSE / RESPONSE / READ_READY / WRITE_READY
EP3 UAS_PIPE_ID_DATA_IN(0x03) IN 读数据
EP4 UAS_PIPE_ID_DATA_OUT(0x04) OUT 写数据

5.0 UAS IU 包格式字节级定义

结构体定义:。UAS 所有交互都是 IU(Information Unit) = 4 字节通用头 + 载荷;关键字段大端(Big-Endian) ------与 BOT 相反(be16_to_cpu/be64_to_cpu/cpu_to_be16)。

通用 IU 头(4 字节,uas_iu_header)
偏移 长度 字段 值/含义
0 1 id IU 类型:0x01 COMMAND / 0x03 SENSE / 0x04 RESPONSE / 0x05 TASK_MGMT / 0x06 READ_READY / 0x07 WRITE_READY(0x02 保留)。QEMU 以 iu.hdr.id 分发
1 1 reserved 保留,必须 0
2 2 tag BE16 命令标识;USB3 streams 模式下 tag == stream ID,必须 1...16(L709;USB2 无此约束但须唯一

tag 的双重身份是 UAS 核心设计 :既是命令配对键(SENSE IU 回填同一 tag),又是 USB3 stream 路由键(st->stream = tag,[dev-uas.c L353。

COMMAND IU(id=0x01,Command pipe OUT,≥32 字节,uas_iu_command)
偏移 长度 字段 值/含义 QEMU 处理
0~3 4 hdr 通用头(id=0x01,tag=命令标识) ---
4 1 prio_taskattr bit6:3=priority (0~15);bit2:0=task attribute(0=Simple,1=Head of Queue,2=Ordered,3=ACA) 不解析(忽略,SCSI 层全按 Simple 处理)
5 1 reserved_1 保留 ---
6 1 add_cdb_length bit7:2=附加 CDB 长度(单位 dword/4 字节),bit1:0 保留 不支持非 0 :L704-707"additional adb length not yet supported" → fake sense
7 1 reserved_2 保留 ---
8 8 lun BE64 ,SAM-4 LUN 格式(单级 LUN 即 0x0000...00ll) be64_to_cpu → usb_uas_get_dev
16 16 cdb[16] SCSI CDB(固定 16 字节槽,有效长度由 CDB opcode 自身决定) cdb_len = sizeof(cdb) + add_cdb_length,直接传 scsi_req_new
32 变长 add_cdb[] 附加 CDB(长度 = add_cdb_length × 4 字节) 未实现
SENSE IU(id=0x03,Status pipe IN,16~34 字节,uas_iu_sense,由 usb_uas_queue_sense 填充 L414-429

相当于 BOT 的 CSW + sense 数据二合一:

偏移 长度 字段 值/含义 QEMU 处理
0~3 4 hdr 通用头(id=0x03,tag=对应命令的 tag) ---
4 2 status_qualifier BE16 状态限定符 恒填 0(cpu_to_be16(0))
6 1 status SCSI 状态字节(比 BOT 的 1bit 信息量大得多):0x00=GOOD,0x02=CHECK_CONDITION,0x08=BUSY 等 usb_uas_queue_sense(status)
7 7 reserved[7] 保留 ---
14 2 sense_length BE16,sense_data 有效长度 仅 status != GOOD 时填(scsi_req_get_sense 返回值)
16 0~18 sense_data[18] SCSI sense([0]=0x70 当前错误,[2]=sense key,[7]=10 附加长度,[12]=ASC,[13]=ASCQ) scsi_req_get_sense() 生成;fake sense 路径手工填(L439-443

线上长度 = 4 + 12 + slen:status==GOOD 时 16 字节,失败时最长 34 字节。

RESPONSE IU(id=0x04,Status pipe IN,固定 8 字节,uas_iu_response,TMF 的"回执")
偏移 长度 字段 值/含义
0~3 4 hdr 通用头(id=0x04,tag=对应 TMF 的 tag)
4 3 add_response_info[3] 附加响应信息,QEMU 不填(g_new0 清零)
7 1 response_code 响应码,见下表
response_code 宏 含义
0x00 TMF_COMPLETE 任务管理成功
0x02 INVALID_INFO_UNIT IU 格式非法
0x04 TMF_NOT_SUPPORTED TMF 不支持(QEMU 对 ABORT_TASK/LOGICAL_UNIT_RESET 之外大多回此码)
0x05 TMF_FAILED TMF 失败
0x08 TMF_SUCCEEDED 查询类 TMF:任务存在
0x09 INCORRECT_LUN LUN 不存在
0x0a OVERLAPPED_TAG tag 与在途命令重叠
TASK_MGMT IU(id=0x05,Command pipe OUT,固定 16 字节,uas_iu_task_mgmt)
偏移 长度 字段 值/含义 QEMU 处理
0~3 4 hdr 通用头(id=0x05,tag=本次 TMF 的标识) ---
4 1 function TMF 功能码,见下表 usb_uas_task(L762
5 1 reserved 保留 ---
6 2 task_tag BE16,被管理命令的 tag 仅 ABORT_TASK/QUERY_TASK 用;其余 TMF 忽略
8 8 lun BE64,目标 LUN ---
function 宏 QEMU 实现
0x01 ABORT_TASK scsi_req_cancel(按 task_tag 找请求)
0x08 LOGICAL_UNIT_RESET device_cold_reset
0x02/0x04/0x10/0x40/0x80/0x81/0x82 其余 回 TMF_NOT_SUPPORTED
READ_READY / WRITE_READY IU(id=0x06/0x07,Status pipe IN,仅 4 字节)

只有通用头,无载荷 (usb_uas_queue_status(req->uas, st, 0),L455。仅 USB2 模式 使用,是数据管道的门控令牌 (USB3 下 start_next_transfer 直接 return,不发):

  • READ_READY(id=0x06,tag=X):设备已备好 X 的读数据,主机可向 DATA_IN 管道发 IN 包
  • WRITE_READY(id=0x07,tag=X):设备已备好 X 的写缓冲,主机可向 DATA_OUT 管道发 OUT 包
数据管道包(DATA_IN / DATA_OUT):无 IU 头的裸数据------UAS 包格式唯一的例外

COMMAND/SENSE/RESPONSE/READY 全是 IU,唯独数据管道上的包没有 IU 头 ------载荷同样是磁盘扇区原样字节流(scsi_req_get_buf() 缓冲直通):

属性 值
协议封装 无 IU 头(UAS 包类型中唯一不带 4 字节通用头的)
归属识别(USB3) 不在载荷里 :p->stream == tag,归属在 USB 事务层(TRB 挂在哪条 stream ring 上)携带,载荷本身零标识
归属识别(USB2) READY IU 门控:datain2/dataout2 单槽独占,包到达时直接取当前 owner
方向 管道物理隔离:EP3 只能 IN、EP4 只能 OUT------BOT 里靠 flags 声明的方向在这里物理消失
总量 无 CBW 式声明------长度由 SCSI 层从 CDB 解析,QEMU 用 buf_size/buf_off 按 SCSI 缓冲段记账
字节来源/去向 scsi_req_get_buf(req->req) + buf_off,由 usb_uas_copy_data() 搬运

usb_uas_copy_data()(dev-uas.c L496-517搬运公式:

复制代码
length = MIN(SCSI 缓冲剩余 buf_size - buf_off, 包剩余 iov.size - actual_length)
usb_packet_copy(req->data, scsi_req_get_buf(req->req) + buf_off, length)  ← OUT: iov→缓冲;IN: 缓冲→iov
buf_off += length; data_off += length
包满(actual_length == iov.size)→ usb_uas_complete_data_packet
缓冲用完(buf_off == buf_size)→ 清零 + scsi_req_continue()(要下一段缓冲)

与 BOT 数据阶段的两个结构差异:

  1. 挂起槽位按 tag 分 :数据包可覆盖整段传输长度(如 64KB 读),盘数据未回时 ASYNC 挂在 req->data 上,usb_uas_scsi_transfer_data 回调逐段回填;BOT 同机制但只有一个 s->packet 槽(无 tag 概念),UAS 每 tag 一槽
  2. 无 residue 概念 :SCSI 实际数据少了就少填(buf_off/data_off 记账),由 SENSE IU 报实际完成量;没有 BOT 那种"CBW 声明 vs 实际传输"的差额字段
BOT vs UAS 包格式关键差异速查
维度 BOT CBW/CSW UAS IU
字节序 全小端(LE) 全大端(BE)------be16_to_cpu(hdr.tag)、be64_to_cpu(lun)
数据阶段载荷 无协议头,裸数据(归属靠 s->mode 状态机) 同样无 IU 头,裸数据(归属靠 stream / READY IU)
签名/类型 魔数签名("USBC"/"USBS") 单字节 IU ID
命令标识 32 位 tag(LE) 16 位 tag(BE),兼作 stream ID
CDB CBW.cmd16 + cmd_len 显式长度 COMMAND IU.cdb16(+ add_cdb 变长扩展,QEMU 未实现)
LUN 1 字节 8 字节 BE(SAM-4 格式,支持多级 LUN)
状态回传 CSW 1 字节(0/1/2)+ residue;sense 要另发 REQUEST_SENSE SENSE IU 内嵌 SCSI status + 18 字节 sense,一次到位
数据长度声明 CBW.data_len 4 字节(设备靠它判阶段) 无此字段------长度由 SCSI 层从 CDB 解析
方向声明 CBW.flags bit7 无此字段------方向由管道物理决定(DATA_IN/DATA_OUT 独立端点)

最后两行是两种协议的设计哲学分水岭:BOT 是"主机声明、设备校验" (data_len/flags 全在 CBW 里,不匹配即 Phase Error/STALL);UAS 是"管道即语义"(哪条端点收发本身就说明一切,IU 只携带 SCSI 层必要信息)。

5.1 命令 IU 解析(usb_uas_handle_data → usb_uas_command,dev-uas.c L697

  • Command pipe(OUT)收 uas_iu,按 iu.hdr.id 分发:UAS_UI_COMMAND → usb_uas_command();UAS_UI_TASK_MGMT → usb_uas_task()
  • usb_uas_command():
    1. 校验 tag 唯一(重叠 → fake sense OVERLAPPED_COMMANDS)、LUN 有效
    2. usb_uas_alloc_request() 建 UASRequest(tag/lun/dev),入 uas->requests 队列
    3. USB3 streams 模式下,若该 tag 的 data 包(data3[tag])先到则直接挂上
    4. scsi_req_new(dev, tag, lun, iu.command.cdb, cdb_len, req) + scsi_req_enqueue() → scsi_req_continue()
  • 错误路径通过 usb_uas_queue_fake_sense() 回 SENSE IU

5.2 数据传输

  • USB3(streams,UAS_MAX_STREAMS=16) :p->stream == tag,data 包直接匹配 usb_uas_find_request(uas, p->stream),命中后 usb_uas_copy_data()(dev-uas.c L496(file:///d:/workspace/embeddedTeam/SimulatorProject/fspd-qemu/hw/usb/dev-uas.c#L496))在包与 scsi_req_get_buf(req->req)+buf_off 间搬运;未命中先缓存 data3[stream] 挂 ASYNC
  • USB2(无 streams) :data 包到来时若当前无激活请求 → STALL。主机须先在 Status pipe 收到 READ_READY/WRITE_READY IU(由 usb_uas_start_next_transfer() 在 datain2/dataout2 上调度)才能发数据包
  • SCSI 回调 usb_uas_scsi_transfer_data():置 buf_off=0, buf_size=len;有挂起包则 copy,否则调 usb_uas_start_next_transfer() 挑下一个请求发 ready IU
  • 包满 → usb_uas_complete_data_packet() 完成;SCSI 缓冲用完 → scsi_req_continue()

5.3 状态 IU 下发(Status pipe,IN)

  • usb_uas_queue_status() 把 UASStatus 挂 uas->results 队列;若已有 host 的 status 包(status2 或 status3[stream])则调度 status_bh(bottom half usb_uas_send_status_bh)在数据事务结束后统一发送------保证数据先于状态完成
  • 无挂起包时 usb_wakeup(ep, stream) 通知 HCD 来取
  • usb_uas_scsi_command_complete() → usb_uas_queue_sense():填 SENSE IU(status、sense_length、sense_data 由 scsi_req_get_sense() 生成)
  • Task Mgmt(usb_uas_task):ABORT_TASK → scsi_req_cancel;LOGICAL_UNIT_RESET → device_cold_reset;回 RESPONSE IU

5.4 核心区别(BOT vs UAS)

维度 BOT UAS
端点 2 个 bulk(命令/数据/状态复用) 4 个独立管道 + 16 streams
命令封装 31B CBW(含 16B CDB) IU(COMMAND IU 含 16B CDB + add_cdb)
并发 串行(单状态机,一次一个命令) TCQ,多 tag 并发(requests 队列)
状态回传 13B CSW(IN 数据端点) SENSE/RESPONSE IU(Status pipe,USB3 走 stream)
USB2 数据同步 无(协议顺序保证) READ_READY/WRITE_READY IU
异步包存储 s->packet 单包 data3[]/status3[](按 stream)或 datain2/dataout2/status2

5.5 UAS streams 机制详解(USB3 多流并发的完整链路)

5.5.1 streams 是什么:同一端点内的多条虚拟子管道

xHCI 的 bulk streams = 把一个 bulk 端点复用出多条独立"子管道"(stream),每条 stream 有自己独立的传输环(transfer ring)。三条关键协议约定:

  • doorbell 携带 stream ID :guest 敲 doorbell 时,寄存器低 8 位是 epid、高 16 位是 stream ID(hcd-xhci.c L3146(file:///d:/workspace/embeddedTeam/SimulatorProject/fspd-qemu/hw/usb/hcd-xhci.c#L3146));xHC 只 fetch 该 stream 自己的 ring
  • UAS 约定 stream ID == 命令 tag :usb_uas_alloc_status 在 streams 模式下 st->stream = tag(dev-uas.c L353(file:///d:/workspace/embeddedTeam/SimulatorProject/fspd-qemu/hw/usb/dev-uas.c#L353));命令校验 tag > UAS_MAX_STREAMS → INVALID_TAG(L709(file:///d:/workspace/embeddedTeam/SimulatorProject/fspd-qemu/hw/usb/dev-uas.c#L709))。tag 合法域 1...16,stream 0 保留 (xhci_find_stream 的 assert(streamid != 0))------这就是 UAS 最多 16 条命令在途的结构性原因
  • 单端点无队头阻塞:不同 stream 的数据互不排队(同一 stream 内保序),多 tag 数据在同一对端点上真正并行
5.5.2 从描述符到 USBPacket:stream 的全链路建立
步骤 发生时机 代码落点
1. 描述符声明 设备 realize SuperSpeed companion 描述符 .bmAttributes_super = UAS_STREAM_BM_ATTR(4)------仅 STATUS / DATA_IN / DATA_OUT 三端点,COMMAND 管道无 streams
2. 端点能力生效 SET_CONFIGURATION usb_desc_set_config → usb_ep_set_max_streams(desc.c L388):ep->max_streams = 1 << 4 = 16
3. xHC 建流数组 guest Configure Endpoint xhci_alloc_streams(hcd-xhci.c L878按 LSA(Linear Stream Array)建 pstreams[1..16],每流一个 XHCIStreamContext;设备侧 usb_device_alloc_streams 是 nop(bus.c L211,模拟设备无需真实分配)
4. 传输提交携带 stream 每次 doorbell doorbell streamid = (val >> 16) & 0xffff(hcd-xhci.c L3146→ xhci_kick_epctx(epctx, streamid) → xhci_find_stream(:LSA 下 streamid 直接索引 pstreams[];首次访问从 guest 内存的 stream context 读 ring 基址并 xhci_ring_init,SCT 必须为 1)→ 只 fetch 该 stream ring 上的 TRB → xfer->streamid → xhci_setup_packet → usb_packet_setup(p, dir, ep, xfer->streamid, ...)(hcd-xhci.c L1610写入 p->stream)

设备侧模式开关只有一个判断:uas_using_streams() = dev->speed == USB_SPEED_SUPER(dev-uas.c L338------不查询 max_streams 是否协商过,速度即模式。

core 层对 stream 的两条调度豁免(这是并发的调度基础):

  • core.c L437):if (QTAILQ_EMPTY(&p->ep->queue) || p->ep->pipeline || p->stream)------带 stream 的包不受"端点队列非空须 ADD_TO_QUEUE 排队"约束,直接交设备处理
  • core.c L470:assert(p->stream || QTAILQ_FIRST(&ep->queue) == p)------complete 时带 stream 的包允许不是队头(不同 stream 间本就无序)

反向(设备→主机)唤醒也精确到 stream:usb_wakeup(ep, stream)(core.c L95)→ bus->ops->wakeup_endpoint → xhci_wakeup_endpoint(hcd-xhci.c L3295()→ xhci_kick_ep(slot, epid, stream) 只踢那一条流的 ring。

5.5.3 设备侧数据结构:per-stream 挂起槽位

对照 BOT 的单槽 s->packet(dev-uas.c L113-121):

c 复制代码
/* usb 2.0 only */
USBPacket *status2;        /* 唯一的 status 读包槽 */
UASRequest *datain2;       /* 当前独占 DATA_IN 管道的请求 */
UASRequest *dataout2;      /* 当前独占 DATA_OUT 管道的请求 */
/* usb 3.0 only */
USBPacket *data3[UAS_MAX_STREAMS + 1];    /* [1..16]:先于 COMMAND IU 到达的数据包暂存 */
USBPacket *status3[UAS_MAX_STREAMS + 1];  /* [1..16]:该 stream 尚无 IU 时挂起的 status 读包 */

另有 UASRequest.data(当前挂在该请求上的数据包)与 results 队列(UASStatus.stream 记录归属流)。核心思想:BOT 用一个槽猜"包属于谁"(靠 mode),UAS 用 stream/tag 让包自带身份。

5.5.4 数据面:stream==tag 直达,无 ready IU 门控

数据包到达 DATA_IN/DATA_OUT 管道(usb_uas_handle_data L872-912(:

  1. p->stream > UAS_MAX_STREAMS → STALL(err_stream,L914
  2. req = usb_uas_find_request(uas, p->stream)(L575()------stream 就是 tag,直接定位请求
  3. req 未找到 (COMMAND IU 还没到------主机先把数据 TRB 放上流 ring 是合法的)→ data3[p->stream] = p + ASYNC 暂存();等 usb_uas_command 收到该 tag 的 COMMAND IU 时反手挂上(L725-728
  4. req 找到 → req->data = p(L896()+ usb_uas_copy_data(L496(:按 MIN(buf_size - buf_off, 包剩余) 在 SCSI 缓冲与包间搬运;SCSI 缓冲未就绪(buf_size==0)则拷 0 字节,包 ASYNC 挂在 req 上等 usb_uas_scsi_transfer_data 回调;包满或 req->complete → 同步完成
  5. 收尾 usb_uas_start_next_transfer(L520):streams 模式第一行直接 return------无需调度,主机本来就知道往哪条流发数据

对照 USB2(p->stream==0):req 取 datain2/dataout2 指针,为 NULL → "no inflight request" STALL(L890()------主机必须先在 Status pipe 收到 READ_READY/WRITE_READY IU 才允许发数据包。

5.5.5 状态面:results 队列 + per-stream 配对投递

SENSE / RESPONSE / READY IU 全部经 usb_uas_queue_status(L385:

  • IU 入 results 队列(FIFO,,streams 模式每个 UASStatus.stream = tag
  • 该 stream 已有挂起的 status 读包 (status3[st->stream])→ qemu_bh_schedule(status_bh)(L397------注释明确:调度 bh 确保在途数据事务先完成再补状态,避免"数据未到 guest、状态先到"的乱序
  • 没有 → usb_wakeup(ep, st->stream)(精确唤醒该流,主机侧 uas 驱动此前已在该流的 STATUS ring 上预挂了 IN TRB,xHC 直接提交状态读传输

主机 IN 读 status:

  • p->stream 非零 → 在 results 队列里搜索 st->stream == p->stream(L849,不是取队头------跨 stream 允许乱序投递 );搜到立即填包完成;没搜到 → status3[p->stream] = p + ASYNC(L855
  • stream==0(USB2)→ 取队头投递,否则挂 status2(L863

usb_uas_send_status_bh(L358):从头遍历 results,取 status3[st->stream] 的包填 IU 完成;队头 IU 没有配对包就 break------即投递整体仍受 FIFO 约束,后面有配对包的 IU 也得等队头先走。

5.5.6 命令面约束与 TMF
  • streams 模式 tag 必须 ≤ 16(两处:COMMAND 与 TASK_MGMT IU),否则 fake sense INVALID_TAG
  • tag 重叠(find_request 命中)→ OVERLAPPED_TAG fake sense
  • COMMAND 管道本身无 streams (描述符层面),多命令仍串行下发;但 SCSI 层 tcq=true 并发执行
  • TMF:ABORT_TASK → scsi_req_cancel;LOGICAL_UNIT_RESET → device_cold_reset;回 RESPONSE IU(同样走 stream==tag 的投递路径)
5.5.7 USB2 vs USB3 streams 全景对照
维度 USB2(无 streams) USB3(streams)
数据包定位请求 datain2/dataout2 单槽指针(ready IU 赋予独占权) p->stream==tag 直达 find_request
ready IU 必需:READ_READY/WRITE_READY 门控数据,start_next_transfer 一次挑一个请求激活 不发:start_next_transfer 直接 return
数据包先于命令 IU 不可能(没 ready 主机不会发)→ "no inflight request" STALL 合法,data3[tag] 暂存,命令到达时挂上
status 读包槽 status2 单槽,FIFO 队头投递 status3[1..16] 按 stream 配对,可乱序投递
唤醒粒度 usb_wakeup(ep, 0) 整端点 usb_wakeup(ep, stream) 精确到流
在途命令数 datain/dataout 方向各 1 + 无数据命令若干 最多 16 条(tag 1...16)
core 调度 普通包(受队列保序) L437豁免排队、L470 豁免队头

5.6 UAS 全流程时序图(PlantUML)

参与者沿用 §4.6 约定:G=Guest(uas 驱动 + xHCI)、H=HCD、C=USB core、K=SCSI 层、B=BlockBackend;UAS 设备为 D(dev-uas.c)。

图 E:UAS USB3 streams------stream==tag,两条命令并发(写 tag=5 / 读 tag=9)
plantuml 复制代码
@startuml
title 图E UAS USB3 streams:stream==tag,两条命令并发(写tag=5 / 读tag=9)

participant G as "Guest\nuas驱动 + xHCI"
participant H as "HCD\nhcd-xhci.c"
participant C as "USB core\ncore.c"
participant D as "usb-uas\ndev-uas.c"
participant K as "SCSI\nscsi-bus/disk"
participant B as "BlockBackend\n(aio)"

== ① 建流(一次性的,配置阶段) ==
G -> H : Configure Endpoint\n为 STATUS/DATA 端点分配 stream context
H -> H : xhci_alloc_streams\n建 pstreams[1..16](每流独立 ring)
note over H
SuperSpeed 描述符 bmAttributes_super=4
⇒ ep->max_streams=16
约定 stream ID == 命令 tag(1..16,0 保留)
end note

== ② COMMAND IU 下发(COMMAND 管道无 streams,串行 OUT) ==
G -> H : COMMAND IU(tag=5,WRITE10)
H -> C : usb_handle_packet(p, stream=0)
C -> D : usb_uas_handle_data(COMMAND pipe)
D -> K : usb_uas_command:tag≤16 校验\nscsi_req_new/enqueue(tag=5)
K -> D : scsi_req_continue\n→ 分配写缓冲(transfer_data)
D -> D : req5.data==NULL\n→ start_next_transfer 直接return\n(streams 模式无需 ready IU)
D --> C : SUCCESS
G -> H : COMMAND IU(tag=9,READ10)
H -> C : usb_handle_packet
C -> D : usb_uas_command(tag=9)
D -> K : scsi_req_new/enqueue\nscsi_req_continue → blk_aio_preadv
D --> C : SUCCESS

== ③ 数据阶段:每条流各走各的 ring,互不阻塞 ==
G -> H : doorbell(DATA_OUT, stream=5)\nTRB 已放在 stream5 自己的 ring 上
H -> C : usb_packet_setup(p, OUT, ep4, stream=5)\nusb_handle_packet
note over C : core L437:带 stream 的包\n豁免端点队列保序
C -> D : usb_uas_handle_data(DATA_OUT)\nfind_request(p->stream=5) → req5
D -> D : req5.data = p\nusb_uas_copy_data → SCSI 写缓冲
D --> C : 包满 SUCCESS(或 ASYNC 等回调)
G -> H : doorbell(DATA_IN, stream=9)
H -> C : usb_packet_setup(p, IN, ep3, stream=9)
C -> D : find_request(9) → req9\n盘数据未回,buf_size==0
D --> C : req9.data = p, data_async=true\nUSB_RET_ASYNC
C -> C : 挂 ep->queue(ASYNC)
note over G,B
tag=5 的写数据与 tag=9 的读数据
在同一对端点上并发进行------
streams 消除了 bulk 单管道的队头阻塞
end note

== ④ 后端完成 → SENSE IU 精确唤醒对应流 ==
B --> K : tag=9 盘读完成
K --> D : usb_uas_scsi_transfer_data(req9, len)\nbuf_off=0, buf_size=len
D -> D : req9.data 有挂起包\nusb_uas_copy_data 填包
D --> C : complete_data_packet\np->status=SUCCESS
C --> H : complete → 中断
H --> G : stream9 IN 传输完成(数据到 guest)
B --> K : tag=5 盘写完成
K --> D : usb_uas_scsi_command_complete(req5)\n→ usb_uas_queue_sense
D -> D : SENSE IU 入 results\nst->stream = tag = 5
D -> C : usb_wakeup(ep_status, stream=5)
C -> H : wakeup_endpoint(ep, 5)\nxhci_kick_ep 只踢 stream5 的 ring
H --> G : 主机此前已在 stream5 的 STATUS ring\n预挂 IN TRB → xHC 提交状态读传输
G -> H : IN status(stream=5)
H -> C : usb_handle_packet
C -> D : STATUS pipe:\n搜 results 中 st->stream==5
D --> C : SENSE IU 填包 SUCCESS
H --> G : SENSE IU(tag=5,status=GOOD)
note over D : tag=9 完成时同理:\nqueue_sense(stream=9) → wakeup(ep, 9)
@enduml
图 F:UAS USB2(无 streams)------READ_READY/WRITE_READY IU 门控数据管道
plantuml 复制代码
@startuml
title 图F UAS USB2:READ_READY IU 门控,datain2 单槽串行接力

participant G as "Guest\nuas驱动 + xHCI"
participant H as "HCD\nhcd-xhci.c"
participant C as "USB core\ncore.c"
participant D as "usb-uas\ndev-uas.c"
participant K as "SCSI\nscsi-disk"
participant B as "BlockBackend"

== ① 两条读命令入队(均未获得数据管道独占权) ==
G -> H : COMMAND IU(tag=7,READ10)
H -> C : usb_handle_packet(stream=0)
C -> D : usb_uas_command → scsi_req_new/enqueue
D -> K : scsi_req_continue → blk_aio_preadv
D --> C : SUCCESS
G -> H : COMMAND IU(tag=8,READ10)
H -> C : usb_handle_packet
C -> D : usb_uas_command(tag=8 入 requests 队列)
D --> C : SUCCESS
note over D : datain2==NULL:tag7/tag8 都未 active\n(都还没拿到 DATA_IN 管道)

== ② 盘读完成 → start_next_transfer 挑 tag7 发 READ_READY ==
B --> K : tag=7 读完成
K --> D : usb_uas_scsi_transfer_data(req7, len)\nreq7.data==NULL → start_next_transfer
D -> D : 遍历 requests:req7 未 active\n且 FROM_DEV 且 datain2==NULL\n⇒ datain2=req7,req7.active=true
D -> D : usb_uas_queue_read_ready\nREAD_READY IU 入 results(stream=0)
D -> C : usb_wakeup(ep_status, 0)
C -> H : wakeup → kick STATUS 端点
H --> G : IN status 传输 → READ_READY IU(tag=7)
G -> G : uas 驱动记下 tag7 拥有 DATA_IN

== ③ 主机发数据包(合法) ==
G -> H : DATA_IN 数据包(stream=0)
H -> C : usb_handle_packet
C -> D : req = uas->datain2(=req7)
D -> D : req7.data = p\nusb_uas_copy_data(缓冲已就绪)
D --> C : 包满 SUCCESS
H --> G : 数据传输完成
note over D
违规对照:主机没收到 READY 就发数据包 ⇒
datain2==NULL → error "no inflight request"
→ USB_RET_STALL,需 reset 恢复
end note

== ④ tag7 完成 → SENSE IU;free_request 释放 datain2 → 接力 tag8 ==
B --> K : 数据全部读完成
K --> D : usb_uas_scsi_command_complete(req7)\n→ usb_uas_queue_sense
D -> D : SENSE IU 入 results
H -> C : IN status 包
C -> D : STATUS pipe:取队头 IU
D --> C : SENSE IU(tag=7)
D -> D : usb_uas_scsi_free_request(req7)\ndatain2=NULL → start_next_transfer\n⇒ 挑 tag8 发 READ_READY
H --> G : SENSE IU(tag=7)
note over D : datain2/dataout2 各至多一个 owner:\nUSB2 下数据管道同方向严格串行
@enduml
图 G:UAS streams 竞态与保序------data3/status3 双向暂存 + status_bh
plantuml 复制代码
@startuml
title 图G UAS streams 竞态:数据包先于命令到(data3)与 status 包先于 IU(status3+bh)

participant G as "Guest"
participant H as "HCD\nhcd-xhci.c"
participant C as "USB core\ncore.c"
participant D as "usb-uas\ndev-uas.c"
participant K as "SCSI"

== 竞态1:数据包先于 COMMAND IU 到达(data3 暂存) ==
G -> H : doorbell(DATA_OUT, stream=3)\n(驱动先把数据 TRB 放上了 stream3 ring)
H -> C : usb_packet_setup(stream=3)\nusb_handle_packet
C -> D : usb_uas_handle_data(DATA_OUT)\nfind_request(3) == NULL
D --> C : data3[3] = p\nUSB_RET_ASYNC
C -> C : 挂 ep->queue(ASYNC)\n(core L470:stream 包允许不在队头)
G -> H : COMMAND IU(tag=3)
H -> C : usb_handle_packet
C -> D : usb_uas_command\nscsi_req_new/enqueue(tag=3)
D -> D : data3[3] != NULL\n⇒ req->data = data3[3]\ndata_async=true;data3[3]=NULL
D -> K : scsi_req_continue → transfer_data\n缓冲就绪后 usb_uas_copy_data 填该包
D --> C : COMMAND IU 包 SUCCESS

== 竞态2:status 读包先于 SENSE IU(status3 暂存 + bh 保序) ==
G -> H : doorbell(STATUS, stream=3)\n(驱动已在 stream3 的 STATUS ring 预挂 IN TRB)
H -> C : usb_handle_packet
C -> D : STATUS pipe:搜 results 无 stream==3
D --> C : status3[3] = p\nUSB_RET_ASYNC
C -> C : 挂 ep->queue
K --> D : (稍后)command_complete(req3)\n→ usb_uas_queue_sense
D -> D : queue_status:\nstatus3[3] 有挂起包\n⇒ qemu_bh_schedule(status_bh)
note over D,C
status_bh 语义(queue_status 注释):
等在途数据事务先完成,再发状态包,
避免"数据未到 guest、状态先到"乱序
end note
C -> D : bh 执行 usb_uas_send_status_bh\n从头遍历 results\n取 status3[st->stream] 的包
D --> C : SENSE IU 填包,p->status=SUCCESS\n→ usb_packet_complete
H --> G : stream3 IN 完成:SENSE IU(tag=3)
note over D
bh 是 FIFO:队头 IU 没有配对包就 break,
后面有配对包的 IU 也得等队头先走
end note

== 对应清理路径:HCD 撤销在途包 → cancel_io ==
H -> D : usb_uas_cancel_io(p)
D -> D : 依次匹配 status2 / status3[i] /\ndata3[i] / req->data,命中槽位置 NULL\n(全部未命中 ⇒ assert 失败)
@enduml

6. 端点布局总览(EP0 与各协议数据端点)

6.1 设备端点存储结构(usb.h L203(file:///d:/workspace/embeddedTeam/SimulatorProject/fspd-qemu/include/hw/usb.h#L203))

每个 USBDevice 内置 1 个控制端点 + 15 个 IN + 15 个 OUT 数据端点槽位:

c 复制代码
#define USB_MAX_ENDPOINTS  15
USBDevice {
    USBEndpoint ep_ctl;                  /* EP0 控制端点:nr=0, type=CONTROL, max_packet_size=64 */
    USBEndpoint ep_in[15], ep_out[15];   /* 数据端点,按 (nr-1) 索引,type/包大小随 SET_CONFIGURATION 生效 */
};
  • usb_ep_init()/usb_ep_reset()(初始化槽位与每端点包队列 ep->queue
  • HCD 用 usb_ep_get(dev, pid, nr) 取端点;usb_find_device(port, addr)(core.c L356按 SET_ADDRESS 分配的地址路由到设备
  • ep->pipeline(xHCI 流水线)、ep->max_streams、ep->halted(SET_FEATURE ENDPOINT_HALT / 传输错误置位)都挂在端点上

6.2 usb-storage(BOT 0x50)端点布局

三个机型(usb-storage / usb-bot,dev-storage-classic.c共用 dev-storage.c 的同一套描述符,端点完全相同:

端点 类型/方向 full high super 作用
EP0 控制(双向) 8B 64B 512B(bMaxPacketSize0=9) 枚举 + 类请求(MassStorageReset / GetMaxLun)
EP1 IN Bulk 64B 512B 1024B + bMaxBurst=15 DATA-IN 数据 + CSW 状态(复用)
EP2 OUT Bulk 64B 512B 1024B + bMaxBurst=15 CBW 命令 + DATA-OUT 数据(复用)

仅 2 个数据端点,命令/数据/状态在不同阶段时分复用 同一端点,靠 s->mode 状态机区分------这就是 BOT 只能串行、无并发的根因。

机型差异在 SCSI 总线能力(SCSIBusInfo),不在 USB 端点:

机型 tcq max_lun 说明
usb-storage(classic,dev-storage-classic.c) false 0 realize 时从 drive 属性自动创建唯一 SCSI 设备
usb-bot(dev-storage-bot.c) false 15 纯 BOT 前端,SCSI 设备由用户挂
TYPE_USB_STORAGE(dev-storage.c,抽象基类) --- --- 提供 handle_data/handle_control/usb_desc 公共实现

6.3 usb-uas(0x62)端点布局

4 个 Bulk 端点 = 4 条独立"管道",每个端点附 bDescriptorType=0x24 管道 ID 扩展描述符:

端点 管道 ID 方向 high super 作用
EP0 --- 控制(双向) 64B 512B 仅标准枚举请求(无类请求)
EP1 COMMAND (0x01) OUT 512B 1024B + burst15,无流 主机下发 COMMAND IU / TASK_MGMT IU
EP2 STATUS (0x02) IN 512B 1024B + burst15 + 16 streams 设备上报 SENSE / RESPONSE / READ_READY / WRITE_READY IU
EP3 DATA_IN (0x03) IN 512B 1024B + burst15 + 16 streams 主机读数据
EP4 DATA_OUT (0x04) OUT 512B 1024B + burst15 + 16 streams 主机写数据
  • 流(streams,UAS_STREAM_BM_ATTR=4 → UAS_MAX_STREAMS=16)仅 SuperSpeed 启用 (uas_using_streams() 判断),且 Command pipe 不启用流 ------命令仍按端点串行下发,数据/状态按 stream == tag 多路并发
  • 命令/数据/状态分管道并行 + 16 流,是 UAS 支持多命令并发(TCQ,usb_uas_scsi_info.tcq=true,max_lun=255)的结构基础

6.4 端点数量对比

协议 端点总数(含 EP0) 数据端点 并发能力
BOT(usb-storage/usb-bot) 3 2(时分复用) 串行,一次一条命令(s->packet 单挂起包)
UAS(usb-uas) 5 4(独立管道) TCQ 多 tag 并发;USB3 再 ×16 streams
相关推荐
张一西2 小时前
QEMU 模拟器学习(二)之 USB 设备模拟与协议学习
qemu·usb·uas·bot
shimly1234563 小时前
(undone) 解析 qemu-6.0.0 源码 (4) 添加 niubi board - 2
qemu
冰山一脚20136 小时前
cirrus显卡驱动笔记
qemu·drm
shimly1234567 小时前
(done) 解析 qemu-6.0.0 源码 (2) 寻找入口函数 main
qemu
ggaofeng19 天前
QEMU中硬盘用哪种协议读写速度快
qemu·磁盘类型
GKxx1 个月前
在 HarmonyOS 上给 QEMU 搭一个最小 aarch64 Linux guest(内核 + busybox initramfs)
linux·华为·qemu·harmonyos·鸿蒙·鸿蒙pc
k4m7v2pz2 个月前
在Arch Linux笔记本上搭建ReactOS 0.4.15虚拟机:从Mac远程开发调试Windows XP风格应用的完整指南
qemu·虚拟机·kvm·交叉编译·mingw-w64·reactos
深念Y2 个月前
基于 NapCat 与本地 RAG 的群聊 AI 机器人方案(ARM64 部署)
人工智能·ai·机器人·node.js·自动化·情感陪伴·bot
冰山一脚20132 个月前
qemu设备监听虚拟机状态笔记
qemu