3. CBW与CSW结构:命令块包装与命令状态包装

好,咱们今天来聊聊MSC协议里最核心的两个数据包——CBW和CSW。说白了,USB大容量存储设备的所有读写操作,都是靠这两个结构来沟通的。你想想看,主机要读U盘上的一个文件,总得告诉设备“我要读哪个扇区、读多少”吧?这个指令就装在CBW里。设备干完了活,也得回个话“干完了,有没有出错”,这个回复就是CSW。

我刚开始做USB驱动时,总觉得这两个结构很简单,不就是几个字节嘛。结果第一次调板子,设备就是不回CSW,卡了我整整两天。后来才发现,是CBW里的签名校验没通过,设备直接罢工了。嗯,从那以后,我对这两个结构再也不敢马虎了。

3.1 CBW的31字节结构

CBW全称是Command Block Wrapper,命令块包装。主机通过BULK OUT端点发送给设备,长度固定为31字节。不多不少,就31个字节,少一个设备都不认。

咱们来看看这31个字节是怎么分配的:

偏移 长度 字段名 说明
0 4 dCBWSignature 签名,固定为0x43425355(即"USBC")
4 4 dCBWTag 命令标签,主机自增的标识符
8 4 dCBWDataTransferLength 期望传输的数据字节数
12 1 bmCBWFlags 传输方向标志,0x00=主机到设备,0x80=设备到主机
13 1 bCBWLUN 逻辑单元号,通常为0
14 1 bCBWCBLength 命令块长度,范围1~16
15 16 CBWCB 命令块,即SCSI命令(如READ10、WRITE10)

这里有几个关键点,我重点说一下:

  • dCBWSignature:固定值0x43425355。设备收到CBW后,第一件事就是检查这个签名。不对?直接忽略,连CSW都不回。我遇到过有人把大小端搞反了,写成0x55534243,结果设备死活不响应。
  • dCBWTag:这个字段很重要。主机每次发送CBW时,会递增这个值。设备回复CSW时,会把同样的Tag带回来。这样主机就能把命令和响应一一对应起来。我建议你在调试时,把这个Tag打印出来,能快速定位乱序问题。
  • bmCBWFlags:就一个字节,但只有bit7有意义。0表示主机发数据给设备(比如写U盘),1表示设备发数据给主机(比如读U盘)。其他位必须为0。
  • CBWCB:这才是真正的命令内容。比如SCSI的READ10命令,就塞在这16个字节里。注意,bCBWCBLength告诉设备实际有效的命令长度,剩下的字节填充0。

核心要点:CBW的31个字节,一个都不能少,一个都不能错。特别是签名和Tag,是设备判断命令有效性的第一道关卡。

3.2 CSW的13字节结构

设备处理完CBW后,通过BULK IN端点回复CSW。CSW全称是Command Status Wrapper,长度固定为13字节。

偏移 长度 字段名 说明
0 4 dCSWSignature 签名,固定为0x53425355(即"USBS")
4 4 dCSWTag 命令标签,必须与CBW中的dCBWTag一致
8 4 dCSWDataResidue 剩余数据长度,实际传输与期望传输的差值
12 1 bCSWStatus 命令执行状态,0=成功,1=失败,2=阶段错误

CSW的结构比CBW简单,但坑一点都不少:

  • dCSWSignature:固定值0x53425355。主机收到CSW后,也要校验这个签名。我曾经见过设备固件里把签名写反了,主机端一直报"CSW签名错误",查了半天才发现是设备端的问题。
  • dCSWTag:必须和CBW里的Tag一模一样。如果对不上,主机就知道出问题了——要么是设备乱序回复,要么是总线上的数据错位了。
  • dCSWDataResidue:这个字段很多人不理解。举个例子,主机要求读512字节,但设备只回了256字节,那Residue就是256。如果完全传输成功,Residue就是0。这个字段在调试时特别有用,能帮你判断设备是不是提前终止了数据传输。
  • bCSWStatus:就一个字节,三种取值。0表示一切正常,1表示命令执行出错,2表示阶段错误(比如主机还在发数据,设备突然回了CSW)。我个人习惯在调试时,把每个CSW的Status都打印出来,一旦看到非0值,立刻停下来分析。

调试技巧:我建议你在固件里加一个CSW日志缓冲区,把最近几十个CSW的Tag、Residue、Status都记录下来。出问题时,直接dump出来看,比用逻辑分析仪抓波形快多了。

3.3 dCBWSignature与dCSWSignature校验

签名校验是MSC协议的第一道安全门。为什么要有这个设计?说白了,就是为了防止总线上的数据错位。USB是包传输的,万一某个包丢了或者顺序乱了,设备收到的数据可能根本不是CBW。这时候如果设备傻乎乎地去解析,后果不堪设想。

CBW的签名是0x43425355,对应ASCII码的"USBC"。CSW的签名是0x53425355,对应"USBS"。你看,就差一个字母,C代表Command,S代表Status。这个设计挺巧妙的,一眼就能看出是命令还是状态。

校验逻辑其实很简单:

// 设备端CBW签名校验示例
uint8_t CBW_CheckSignature(CBW_t *cbw) {
    if (cbw->dCBWSignature != 0x43425355) {
        // 签名错误,直接丢弃,不回CSW
        return 0;  // 校验失败
    }
    return 1;  // 校验通过
}

// 主机端CSW签名校验示例
uint8_t CSW_CheckSignature(CSW_t *csw) {
    if (csw->dCSWSignature != 0x53425355) {
        // 签名错误,说明数据错位或设备异常
        return 0;  // 校验失败
    }
    return 1;  // 校验通过
}

这里有个细节要注意:签名是小端序存储的。也就是说,在内存里看到的是0x55, 0x53, 0x42, 0x43(CBW)和0x55, 0x53, 0x42, 0x53(CSW)。如果你用逻辑分析仪抓数据,看到的是字节流,不是32位整数。我刚开始做的时候,就因为这个大小端问题,折腾了好几个小时。

避坑指南:我曾经在一个项目里,设备端用STM32的HAL库,主机端用Linux的usb-storage驱动。设备发出去的CSW签名明明是0x53425355,但主机就是报签名错误。后来发现,STM32的USB库在发送多字节数据时,默认用了大端序。我手动把签名转成小端序后,问题就解决了。所以,一定要确认你的USB控制器和驱动层的数据字节序。

3.4 CBW与CSW的交互流程

理解了单个结构,咱们来看看它们是怎么配合工作的。下面这张图展示了主机和设备之间的一次完整命令交互:

CBW与CSW交互流程 主机 (Host) 设备 (Device) ① 发送CBW (31字节) ② 校验dCBWSignature ③ 执行SCSI命令 ④ 传输数据 (可选) ⑤ 回复CSW (13字节) ⑥ 校验dCSWSignature 说明:数据方向由bmCBWFlags决定。读操作时数据从设备到主机(虚线箭头向右), 写操作时数据从主机到设备(虚线箭头向左)。无数据传输的命令(如TEST UNIT READY)跳过步骤④。

整个流程其实就五步:

  1. 主机发CBW:通过BULK OUT端点,发送31字节的CBW。设备收到后,先检查dCBWSignature。
  2. 设备校验签名:签名不对?直接丢弃,什么都不回。签名对了?继续往下走。
  3. 设备执行命令:解析CBWCB里的SCSI命令,比如读扇区、写扇区、查询容量等。
  4. 传输数据(可选):如果命令需要传输数据,就通过BULK IN或BULK OUT端点传。注意,数据方向由bmCBWFlags决定。
  5. 设备回CSW:命令执行完毕,设备通过BULK IN端点回复13字节的CSW。主机收到后,校验dCSWSignature和dCSWTag。

关键约束:CSW必须在所有数据传输完成后才能发送。如果设备在数据传输还没结束时就回了CSW,主机就会认为发生了阶段错误(Phase Error),bCSWStatus会置为2。这个错误一旦出现,主机通常会复位整个USB总线,重新枚举设备。

好了,CBW和CSW的结构就讲到这里。这两个结构虽然简单,但它们是MSC协议的基石。你想想看,没有CBW,主机怎么告诉设备要干什么?没有CSW,主机怎么知道设备干完了没有?所以,我建议你在写代码时,把CBW和CSW的构造、解析、校验都封装成独立的函数。这样不仅代码清晰,调试起来也方便——出问题了,直接看这几个函数的输入输出,很快就能定位到问题。