SCSI 命令集精讲:READ(10)/WRITE(10) 与核心命令实现

说到 MSC(Mass Storage Class)设备,SCSI 命令就是它的灵魂。你想想看,USB 主机和 U 盘之间怎么沟通?就是靠这些命令。今天咱们就把最常用的几个命令掰开揉碎了讲。

我个人习惯把 SCSI 命令分成三类:数据搬运类、设备查询类、状态管理类。READ(10) 和 WRITE(10) 属于第一类,INQUIRY 和 READ CAPACITY 属于第二类。咱们一个一个来。

SCSI 命令分类体系(MSC 常用子集) 数据搬运命令 设备查询命令 状态管理命令 READ(10) WRITE(10) READ(6) / WRITE(6) INQUIRY READ CAPACITY MODE SENSE TEST UNIT READY REQUEST SENSE SEND DIAGNOSTIC 本章重点覆盖区域 READ(10)/WRITE(10) 格式详解 + SCSI 状态字节与感知数据 INQUIRY 与 READ CAPACITY 命令实现细节

READ(10) 命令格式详解

READ(10) 是 MSC 设备最核心的数据读取命令。它的 CDB(Command Descriptor Block)固定 10 字节。我刚开始做 USB 存储时,总觉得 10 字节比 6 字节浪费,后来才发现多出来的字段太有用了。

字节偏移位域描述备注
0操作码 0x28READ(10) 命令标识固定值
1Bit 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)
操作码0x280x2A
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(直接访问设备)
1RMB + 版本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 设备端,命令处理是个典型的有限状态机。我画个流程:

MSC 设备端 SCSI 命令处理流程 接收 CBW 解析 CDB 执行命令 返回 CSW 错误 → CHECK CONDITION 等待 REQUEST SENSE

每一步都有坑。我举个真实例子:有一次设备返回 CSW 时状态字节填错了,主机一直收不到 GOOD,反复重试。最后发现是 CSW 的 dCSWStatus 字段写成了 0x01 而不是 0x00。这种低级错误,调试器一看就知道。

调试技巧:用 USB 分析仪抓包,重点看三点:CBW 的 dCBWSignature 是不是 0x43425355、CSW 的 dCSWSignature 是不是 0x53425355、状态字节对不对。这三个对了,基本就通了。

嗯,SCSI 命令这块内容不少,但核心就这几个。READ(10) 和 WRITE(10) 是数据搬运的主力,INQUIRY 和 READ CAPACITY 是设备自报家门。状态字节和感知数据是错误处理的命脉。把这些吃透了,MSC 设备开发就成功了一大半。