SCSI 命令集精讲:READ(10)/WRITE(10) 与核心命令实现
说到 MSC(Mass Storage Class)设备,SCSI 命令就是它的灵魂。你想想看,USB 主机和 U 盘之间怎么沟通?就是靠这些命令。今天咱们就把最常用的几个命令掰开揉碎了讲。
我个人习惯把 SCSI 命令分成三类:数据搬运类、设备查询类、状态管理类。READ(10) 和 WRITE(10) 属于第一类,INQUIRY 和 READ CAPACITY 属于第二类。咱们一个一个来。
READ(10) 命令格式详解
READ(10) 是 MSC 设备最核心的数据读取命令。它的 CDB(Command Descriptor Block)固定 10 字节。我刚开始做 USB 存储时,总觉得 10 字节比 6 字节浪费,后来才发现多出来的字段太有用了。
| 字节偏移 | 位域 | 描述 | 备注 |
|---|---|---|---|
| 0 | 操作码 0x28 | READ(10) 命令标识 | 固定值 |
| 1 | Bit 5:0 = LUN | 逻辑单元号 + 保留位 | 通常为 0 |
| 2-5 | 逻辑块地址 (LBA) | 起始扇区号,4 字节大端 | 支持 2TB 以上 |
| 6 | 保留 + DPO/FUA | 缓存控制位 | 我一般置 0 |
| 7-8 | 传输长度 | 要读取的扇区数,2 字节 | 最大 65535 扇区 |
| 9 | 控制字节 | NACA/链接命令等 | 通常为 0 |
关键点:LBA 是 4 字节大端序。我见过不少新手在这里栽跟头——小端 CPU 直接 memcpy 就出错了。记得做字节序转换。
WRITE(10) 命令格式
WRITE(10) 和 READ(10) 几乎是对称的。操作码是 0x2A,其他字段完全一样。说白了就是一个读一个写,CDB 结构镜像。
| 字段 | READ(10) | WRITE(10) |
|---|---|---|
| 操作码 | 0x28 | 0x2A |
| LBA 位置 | 字节 2-5 | 字节 2-5 |
| 传输长度 | 字节 7-8 | 字节 7-8 |
| 数据方向 | 设备 → 主机 | 主机 → 设备 |
我的经验:调试时先用 READ(10) 读一个已知扇区,确认 LBA 和长度解析正确,再测 WRITE(10)。这样出问题能快速定位是命令解析还是存储介质的问题。
SCSI 状态字节与感知数据
命令执行完了,设备得告诉主机结果。这就是状态字节(Status Byte)和感知数据(Sense Data)的活。
状态字节只有 1 个字节,但信息量很大:
- 0x00 GOOD — 一切正常,我最喜欢看到这个
- 0x02 CHECK CONDITION — 出问题了,得进一步查感知数据
- 0x08 BUSY — 设备忙,主机应该重试
- 0x18 RESERVATION CONFLICT — 多主机环境下的锁冲突
当状态是 CHECK CONDITION 时,主机必须发 REQUEST SENSE 命令获取感知数据。感知数据格式如下:
| 字节 | 字段 | 说明 |
|---|---|---|
| 0 | 有效位 + 错误码 | 0x70 或 0x71 |
| 1 | 段号 | 通常为 0 |
| 2 | 感知键 (Sense Key) | 错误大类 |
| 3-6 | 信息字段 | 出错的 LBA |
| 7 | 附加感知长度 | 后续字节数 |
| 8-11 | 命令特定信息 | 依命令而定 |
| 12 | 附加感知码 (ASC) | 错误小类 |
| 13 | 附加感知码限定符 (ASCQ) | 更细的错误描述 |
踩坑提醒:我曾经在一个项目里发现设备返回 CHECK CONDITION 后,主机没有发 REQUEST SENSE,而是直接重试命令。结果设备卡在错误状态出不来。记住:CHECK CONDITION 后必须清感知数据,否则设备不会接受新命令。
INQUIRY 命令实现
INQUIRY 是主机枚举设备时发的第一个 SCSI 命令。它用来获取设备的基本信息。操作码是 0x12。
CDB 格式很简单:
- 字节 0: 0x12
- 字节 1: 保留 + EVPD 位
- 字节 2: 页面码(EVPD=1 时有效)
- 字节 3-4: 分配长度
- 字节 5: 控制
返回的标准 INQUIRY 数据是 36 字节:
| 字节 | 字段 | 典型值 |
|---|---|---|
| 0 | 外围设备类型 | 0x00(直接访问设备) |
| 1 | RMB + 版本 | 0x80(可移动介质) |
| 2 | 版本号 | 0x06(SPC-4) |
| 3 | 响应数据格式 | 0x02 |
| 4-7 | 附加长度 | 31 |
| 8-15 | 厂商标识 | "MyUSB " |
| 16-31 | 产品标识 | "Flash Disk " |
| 32-35 | 产品版本 | "1.00" |
建议:厂商标识和产品标识用 ASCII 空格补齐到固定长度。我习惯用 8 字节厂商 + 16 字节产品 + 4 字节版本。Windows 和 Linux 都会用这些字符串来识别设备。
READ CAPACITY 命令实现
READ CAPACITY 告诉主机:你的盘有多大?操作码是 0x25。返回 8 字节数据:
- 字节 0-3: 最后一个逻辑块地址(LBA)
- 字节 4-7: 块大小(字节数)
举个例子:一个 8GB 的 U 盘,扇区大小 512 字节。最后一个 LBA 就是 8×1024×1024×1024 ÷ 512 - 1 = 16777215。返回数据就是 0x00FFFFFF 和 0x00000200。
注意:READ CAPACITY 返回的是最后一个 LBA,不是总扇区数。总扇区数 = 最后一个 LBA + 1。我见过有人直接拿返回的 LBA 当总数用,结果少算了一个扇区。
实战:命令处理流程
在 USB MSC 设备端,命令处理是个典型的有限状态机。我画个流程:
每一步都有坑。我举个真实例子:有一次设备返回 CSW 时状态字节填错了,主机一直收不到 GOOD,反复重试。最后发现是 CSW 的 dCSWStatus 字段写成了 0x01 而不是 0x00。这种低级错误,调试器一看就知道。
调试技巧:用 USB 分析仪抓包,重点看三点:CBW 的 dCBWSignature 是不是 0x43425355、CSW 的 dCSWSignature 是不是 0x53425355、状态字节对不对。这三个对了,基本就通了。
嗯,SCSI 命令这块内容不少,但核心就这几个。READ(10) 和 WRITE(10) 是数据搬运的主力,INQUIRY 和 READ CAPACITY 是设备自报家门。状态字节和感知数据是错误处理的命脉。把这些吃透了,MSC 设备开发就成功了一大半。