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:通过BULK OUT端点,发送31字节的CBW。设备收到后,先检查dCBWSignature。
- 设备校验签名:签名不对?直接丢弃,什么都不回。签名对了?继续往下走。
- 设备执行命令:解析CBWCB里的SCSI命令,比如读扇区、写扇区、查询容量等。
- 传输数据(可选):如果命令需要传输数据,就通过BULK IN或BULK OUT端点传。注意,数据方向由bmCBWFlags决定。
- 设备回CSW:命令执行完毕,设备通过BULK IN端点回复13字节的CSW。主机收到后,校验dCSWSignature和dCSWTag。
关键约束:CSW必须在所有数据传输完成后才能发送。如果设备在数据传输还没结束时就回了CSW,主机就会认为发生了阶段错误(Phase Error),bCSWStatus会置为2。这个错误一旦出现,主机通常会复位整个USB总线,重新枚举设备。
好了,CBW和CSW的结构就讲到这里。这两个结构虽然简单,但它们是MSC协议的基石。你想想看,没有CBW,主机怎么告诉设备要干什么?没有CSW,主机怎么知道设备干完了没有?所以,我建议你在写代码时,把CBW和CSW的构造、解析、校验都封装成独立的函数。这样不仅代码清晰,调试起来也方便——出问题了,直接看这几个函数的输入输出,很快就能定位到问题。