自制工业控制系统:U盘驱动实战——USB大容量存储SCSI命令与块设备接入
Contents
起因
立项清单里"读取 U 盘"是明确需求:现场拷加工程序、导出加工报表、固件升级的入口都是它。U 盘的命令集是借来的:SCSI 的前身 SASI 是 1979 年 Shugart 给磁盘驱动器定的接口,1986 年标准化成 SCSI,在企业存储里摸爬了十几年;USB 大容量存储类没有另起炉灶,把 SCSI 命令装进 CBW/CSW 信封走批量传输——所以驱动一个 U 盘,实际写的是一套打了折的 SCSI 主机。USB 协议详解把 CBW/CSW 的理论讲完了,EHCI 篇有了批量传输的队列机制,NVMe 篇立了 BlockDevice 抽象——这篇把三样东西拧起来:一个真的能挂 exFAT 的 U 盘驱动。
一、识别:接口三元组与端点
枚举完的设备逐接口检查 class/subclass/protocol:
| 字段 | 值 | 含义 |
|---|---|---|
| interface class | 0x08 | 大容量存储(MSC) |
| subclass | 0x06 | SCSI 透明命令集 |
| protocol | 0x50 | Bulk-Only Transport(BOT) |
命中就从端点描述符里拿 bulk-in / bulk-out 两个端点,在 EHCI 异步调度里给它们各挂一个 QH(队列挂法见 EHCI 篇)。端点的认法:bEndpointAddress bit7 是方向(1=IN)、bit0-6 是端点号;高速盘的 wMaxPacketSize 固定 512。设备速度枚举时顺手记下:EHCI 端口经 TT 挂进来的全速盘批量包长只有 64,吞吐掉到 1MB/s 量级——能识别就让它用,但界面要提示"全速盘,拷大文件请换高速盘",别让操作工以为系统坏了。
坑:subclass 0xFF(厂商私有,老 U 盘和"送的小礼品盘"常见)一律拒绝加载并提示换盘,别试图猜协议——那种盘各有各的脾气,工控系统不陪它玩。
二、命令层:CBW/CSW 的字节布局
一次传输 = CBW(out)→ DATA(in 或 out,长度可以为 0)→ CSW(in),三段在同一个 QH 上用 qTD 链串起来。
31 字节 CBW:
| 偏移 | 内容 |
|---|---|
| 0–3 | 签名 0x43425355(“USBC”) |
| 4–7 | tag(自增,回包核对用) |
| 8–11 | data 阶段字节数(大端) |
| 12 | flags:bit7=1 表示 IN |
| 13 | 保留(0) |
| 14 | LUN(单 LUN 盘就是 0) |
| 15 | 命令块长度(通常 10 或 16) |
| 16–30 | CBWCB(SCSI 命令块) |
13 字节 CSW:
| 偏移 | 内容 |
|---|---|
| 0–3 | 签名 0x53425355(“USBS”) |
| 4–7 | tag(必须与本次 CBW 对上) |
| 8–11 | residue(没传完的字节数) |
| 12 | status:0 成功 / 1 命令失败→查 sense / 2 相位错误→复位恢复 |
坑:tag 不核对等于没校验。 总线抖动时可能收到上一次传输的残留 CSW,tag 对不上就整条重传。
坑:dCBWDataTransferLength 必须与 DATA 阶段实际字节数严格一致。 READ 多要、WRITE 少给,守规矩的盘 STALL 或报 phase error,不守规矩的直接超时——宁可在上层把缓冲凑齐,也别让 CBW 的声明和实际传输对不上。
三、SCSI 最小命令集
工控 U 盘只需要 6 条:
| 命令 | 操作码 | 说明 |
|---|---|---|
| TEST UNIT READY | 0x00 | 探测就绪(配合 REQUEST SENSE) |
| REQUEST SENSE | 0x03 | 18B:KEY@2 / ASC@12 / ASCQ@13 |
| INQUIRY | 0x12 | 36B:厂商@8 产品@16 版本@32(自检报告的显示素材) |
| READ CAPACITY | 0x25 | 返回末 LBA + 块大小(u32 大端 ×2) |
| READ(10) | 0x28 | LBA@2–5,块数@7–8(大端) |
| WRITE(10) | 0x2A | 同上 |
|
|
坑:单命令块数上限规范不保证,老盘常见 128/255 块的隐性上限——保守 64 块(32KB)一条,超了就拆。 撞上限的症状是 STALL 或短读,不要硬试着加大。
坑:U 盘也有"就绪"状态机。 刚插上/spin-up 要几秒:TEST UNIT READY 失败 → REQUEST SENSE 看到 KEY 06 / ASC 28(介质变化)→ 轮询重试,上限 5 秒,别死等。
两个现场够用的细节:READ CAPACITY 回的末 LBA 是 0xFFFFFFFF 时表示真容量超 2TB,要换 READ CAPACITY(16)(0x9E)问 8 字节 LBA——U 盘撞上这条的概率很低,但代码里留个判断分支,比事后解释便宜。REQUEST SENSE 的 18 字节里,现场排障最常碰的组合:KEY 02/ASC 3A(无介质)、KEY 06/ASC 28(介质变化)、KEY 06/ASC 27(写保护)、KEY 03/ASC 11(读介质错误——盘快不行了,提示备份换盘)、KEY 05/ASC 21(LBA 越界——上层算错了,查自己代码)。错误处理按 KEY 大类路由,ASC 留给日志和界面文案,别一条条 if 硬编码。
四、错误处理:STALL 与复位恢复
bulk 传输的一切异常最终都表现为端点 STALL,处理分三级:
- 数据/状态阶段 STALL → 控制传输
CLEAR_FEATURE(ENDPOINT_HALT)清两个 bulk 端点各一遍(setup 包:bmRequestType 0x02、bRequest 0x01、wValue 0、wIndex=端点地址、wLength 0)→ 当前命令重试一次 - CSW status=1 → REQUEST SENSE 拿原因分类:介质没准备好→轮询;写保护→报错给上层
- status=2 或清了三次还 STALL → Bulk-Only Mass Storage Reset(类请求:bmRequestType 0x21、bRequest 0xFF、wIndex=接口号)→ 再 CLEAR_FEATURE → 重新 READ CAPACITY
坑(必踩):CLEAR_FEATURE 会把该端点的 data toggle 复位回 DATA0。 重试的 QH 里 DT 位必须同步清零,否则接下来第一次传输被静默丢弃——表现是"复位成功了但永远读不出数据",极难定位。
坑:拔盘瞬间在途传输会 NAK/超时混着来,错误路径必须把传输缓冲还回 DMA 池(不泄漏)且不持有文件系统锁。所以整个重试状态机放在协作层的一个 step 函数里跑(调度篇的框架),USB 永远不阻塞别人。
五、接入 BlockDevice
NVMe 篇的 trait 直接实现:
|
|
exFAT 层根本不知道底下是 NVMe 还是 U 盘——加工程序从 U 盘拷到硬盘,就是两个卷之间的普通读写。拔盘事件由枚举层广播,文件系统层收到后卸载卷、关闭打开的句柄,界面弹提示。卸载前发一次 SYNC CACHE(0x35)等 CSW 回来再收尾——写直达策略下它多半是空操作,但这几十毫秒的保险,比教会每个操作工"安全弹出"便宜。
性能预期摆正:USB 2.0 高速理论 480Mbps,批量传输实际 30~38MB/s 封顶,再乘单命令 64 块的往返效率,稳定 25MB/s 上下就该满意。想要快,靠 QH 链把多条读命令批量挂进异步调度,而不是加大单命令。
总结
实战清单:三元组识别 + 双 bulk 端点挂 QH;CBW/CSW 字节布局表常备;六条 SCSI 命令;64 块保守分段;STALL 三级处理(清端点→查 sense→类复位);DT 位复位纪律;tag 核对;重试状态机放协作层 step。跑通标志:INQUIRY 打出厂商/产品字符串;READ CAPACITY 容量与盘身标称一致;exFAT 卷挂载后读一个 100MB 文件 CRC 与宿主机一致;写入中途拔盘系统不崩、插回后重挂载继续用。
下一篇:日志系统——这个驱动里已经埋了好几处"记 ERROR 日志",该兑现它了。
参考链接
- USB Mass Storage Class: Bulk-Only Transport 规范(usb.org 文档页)
- T10 SPC / SBC(SCSI 命令集)
- OSDev Wiki: USB Mass Storage
- 本系列:EHCI 实战、USB协议详解、NVMe 实战(BlockDevice)、exFAT 实战、内存管理器(DMA 池)
Author 软件开发大郭
LastMod 2026-09-11