尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

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

QEMU 模拟器学习(二)之 USB 设备模拟与协议学习 笔者学习qemu 模拟器今天讲一下usb设备这块基于三个文件进行分析hw/usb/core.c— USB 控制传输与数据Bulk传输的通用逻辑hw/usb/dev-storage.c— BOTBulk-Only Transport协议号 0x50协议解析hw/usb/dev-uas.c— UASUSB 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 BusSCSIBusInfo 回调 │ .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.cEP0控制传输用setup_state状态机驱动SETUP → DATA → ACK → IDLE状态含义SETUP_STATE_IDLE空闲SETUP_STATE_SETUP收到 SETUP 且控制请求异步处理中SETUP_STATE_DATADATA 阶段收/发 data_bufSETUP_STATE_ACK状态阶段无数据控制传输直接进入SETUP_STATE_PARAMxHCI 参数化传输do_parameter2.1 do_token_setup[core.c L129]校验包长必须为 8 字节usb_packet_copy()读入s-setup_buf解析wLengthsetup_buf[7..6]→s-setup_len超data_buf容量则 STALL解析 request/value/indexIN 方向请求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 方向请求1USB_DIR_ININ 事务若干次设备→主机发 data_bufOUT 事务主机确认已收数据控制写OUT 方向请求0OUT 事务若干次主机→设备收 data_bufwLength0则直接进 STATUSIN 事务设备确认已处理注意方向规则STATUS 阶段方向与 DATA 阶段恒相反且 STATUS 事务是零长度包写完请求后p-actual_length 0。2.2a do_token_out[core.c L229]——主机发 OUT 事务时干什么按setup_state × 请求方向四个分支见代码 switchsetup_state请求方向场景动作DATAOUT!(setup_buf[0]USB_DIR_IN)控制写的数据阶段usb_packet_copy()把包数据拷入data_buf setup_indexsetup_index setup_len时收满转 ACK 态DATAIN协议错误读请求不该有 DATA-OUT置SETUP_STATE_IDLEUSB_RET_STALLACKIN控制读的状态阶段SETUP→DATA-IN→这里的 OUT主机零长包确认收完数据 → 设备转 IDLEusb_pcap_ctrl(p,false)抓包收尾transfer OKACKOUT写请求 ACK 态后又来 OUT容错忽略多余输出ignore additional output其它SETUP/IDLE/PARAM—协议错误USB_RET_STALL2.3 do_token_in[core.c L181] ——主机发 IN 事务时干什么setup_state请求方向场景动作DATAINsetup_buf[0]USB_DIR_IN控制读的数据阶段从data_buf setup_index拷出min(setup_len - setup_index, p-iov.size)到包发完转 ACK 态DATAOUT协议错误写请求不该有 DATA-IN置 IDLE USB_RET_STALLACKOUT!(...USB_DIR_IN)控制写的状态阶段设备此刻才真正执行请求调用usb_device_handle_control()此时 data_buf 已收满——SETUP 阶段故意不执行的 OUT 请求在此执行USB_RET_ASYNC则挂起等usb_generic_async_ctrl_complete否则转 IDLE、actual_length0回零长状态包ACKIN读请求 ACK 态又来 IN静默忽略break无动作不 STALL其它SETUP/IDLE/PARAM—协议错误USB_RET_STALL2.3a BOT 设备usb-storageEP0 上的典型事务序列请求类型完整序列各步落点GET_DESCRIPTOR枚举控制读SETUP → DATA-IN×n → STATUS-OUTdo_token_setup立即执行填 data_buf →do_token_in(DATA) 分片发出 →do_token_out(ACK,IN 方向) 收确认回 IDLEGetMaxLun (0xfe)控制读BOT 类SETUP → DATA-IN(1B LUN) → STATUS-OUT同上data_buf 填最大 LUNSET_CONFIGURATION控制写无数据SETUP → STATUS-INdo_token_setup不执行 → 直接转 ACKwLength0→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-parameter64bit里一次下发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 再走类自定义请求未识别 → STALLusb_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未配置返回 0SET_CONFIGURATIONusb_desc_set_config()激活选中的USBDescConfig按描述符初始化各接口/端点type、max_packet_size、max_streams、pipeline 全部生效GET_STATUS返回 self-powered / remote-wakeup 位CLEAR_FEATURE/SET_FEATURE清/置dev-remote_wakeupGET_INTERFACE/SET_INTERFACE读取/切换altsetting[]usb_desc_set_interfaceSET_SEL/SET_ISOCH_DELAYSuperSpeed 专用直接成功VendorQMS OS 描述符Windows 驱动匹配标准请求未命中ret 0后进入类自定义请求usb-storageMassStorageReset (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.c3.1 usb_handle_packet[core.c L420]HCD 提交包的唯一入口ep-halted时提交新包会自动清除 halt端点队列为空、或ep-pipelinexHCI 流水线、或p-streamUAS 流→ 直接usb_process_one()返回值处理USB_RET_ASYNC→ 包入ep-queue状态 ASYNCisoc 禁止 asynchost 侧设备如 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见上节控制传输非 EP0Bulk/Interrupt/Isocusb_pcap_data(p, true)抓包后usb_device_handle_data(dev, p)——Bulk 命令与数据都从这里进入 BOT/UAS 的 handle_data3.3 usb_packet_complete[core.c L486]usb_packet_complete_one()状态非 SUCCESS 或short_not_ok 短包→ep-halted true从队列摘除并回调 HCD随后循环重放队列halted 时清空队列REMOVE_FROM_QUEUEQUEUED 的包重新usb_process_one()注意设备代码惯用手法先把s-packet NULL再调用usb_packet_complete()防止重入见usb_msd_packet_complete3.4 数据搬运原语usb_packet_copy(p, ptr, bytes)按 pid 决定方向OUTiov→ptrINptr→iov处理p-combinedxHCI 聚合包usb_packet_skip(p, bytes)IN 方向跳过并置零usb_packet_setup()/usb_packet_addbuf()HCD 组包用3.5 端点、PID 与设备模式CBW mode三者的关系三个概念分属不同层次共同决定“一个 USB 包到底承载什么”概念所属层次取值语义端点号p-ep-nrUSB 管道拓扑0EP0/ 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)在 L382p-ep-nr 0 → 控制传输pid 三值分发SETUP/IN/OUT → do_token_* p-ep-nr ! 0 → usb_device_handle_datapid 只剩 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 只认 devep2IN 只认 devep1否则 STALL switch (s-mode) ← 第三层BOT 会话阶段CBW / DATAOUT / CSW / DATAINBOT 语义消歧表deveppids-mode主机在做什么设备动作2OUTCBW发 31B CBW 命令解析 CBW → scsi_req_new/enqueue按 data_len/flags 切换 mode2OUTDATAOUT发写数据usb_msd_copy_data → SCSI 缓冲2OUT其他—STALLgoto fail1INDATAIN收读数据SCSI 缓冲 → 包1INCSW收 13B CSW 状态usb_msd_send_status → 回 CBW 态1INDATAOUT写命令未完成时提前发 status read挂 ASYNC命令完成后回 CSWmode 迁移由 CBW 内容驱动data_len0 → CSWflags0x80d2h→ 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 放一个 DIRIN 的 TRB 并敲 doorbell——真实硬件此刻向 EP1 发IN token 包。QEMU 的 HCD 模拟器构造包USBPacket.pid是包属性而非设备状态由 HCD 在usb_packet_setup()[core.c L582](里设置pid 来源是 guest 布置的传输描述符方向位xHCIxfer-in_xfer epctx-type 2hcd-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-1609EHCIqTD token 的 PID 字段get_field(qtd-token, QTD_TOKEN_PID)hcd-ehci.c L418UHCITD token 的 PID 位同源逻辑设备按 pidepmode 被动响应包经usb_handle_packet → usb_process_one → usb_msd_handle_data(devep1, pidIN, modeCSW)进入usb_msd_send_status()回 13B CSWmode→CBW。写命令里 OUT→IN 的完整时序三层软件接力guest usbstor: 数据发完按 BOT 下一步是读 CSW → 提交 EP1 IN 传输描述符标 IN guest xHCI 驱动: 往 EP1 ring 放 DIRIN 的 TRB敲 doorbell硬件将发 IN token QEMU xhci: 扫 ring → usb_packet_setup(p, USB_TOKEN_IN, ep1) → usb_handle_packet() QEMU 设备: usb_msd_handle_data(devep1, IN, modeCSW) → 发 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.3SCSI 命令完成回调里再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.stateASYNC挂 ep-queueHCD 传输保持 pending 不产生完成事件guest 看到的是传输进行中 ⋯⋯ 后端异步工作磁盘 dma/aio BH / SCSI 状态机推进⋯⋯ 后端回调如 usb_msd_command_complete ├─ 往 p-iov 填数据usb_packet_copyp-status USB_RET_SUCCESS ├─ 设备先把自己的包指针清空防重入再调 usb_packet_complete(dev, p) ▼ usb_packet_complete_one从 ep-queue 摘除 → COMPLETE → port-ops-complete() ▼ HCD complete 回调传输描述符写完成状态 → 稍后中断通知 guestDMA 数据已在 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/NAKusb_packet_complete_one有 assert非 pipeline 端点必须保序ASYNC 包未完成时后续包只能 ADD_TO_QUEUEisoc 禁止 async、interrupt async 仅限 host 侧设备core 有 assertBOT 的挂起槽位是单包s-packet串行协议任一时刻至多一个包在设备手里三类包复用它写数据包等 SCSI 缓冲、读数据包等 SCSI 数据、提前到达的 CSW 读包等命令完成。UAS 则是多槽USB2 用datain2/dataout2/status2各一USB3 按 stream 用data3[tag]/status3[tag]数组支持并发。
返回列表