1. MSC协议基础:USB大容量存储设备类协议概述
大家好,我是你们的讲师。今天咱们正式开讲MSC协议。说白了,MSC就是让USB设备能被主机当成一块硬盘来用的协议。U盘、移动硬盘、读卡器,这些设备背后跑的都是MSC。
我刚开始做USB开发时,总觉得MSC协议很神秘。后来踩了几个坑才明白——它其实就是一套标准化的“怎么把数据块搬进搬出”的规则。你想想看,主机要读写U盘,总得有个统一的对话方式吧?MSC就是干这个的。
1.1 大容量存储设备类协议概述
USB规范把设备分成很多类。MSC属于“大容量存储设备类”,类代码是0x08。这个类下面又分几个子类:
| 子类代码 | 子类名称 | 典型应用 |
|---|---|---|
| 0x01 | RBC(简化块命令) | 闪存设备、小型存储 |
| 0x02 | ATAPI(CD/DVD) | 光驱、光盘刻录机 |
| 0x03 | QIC-157(磁带) | 磁带机(现在很少见了) |
| 0x04 | UFI(软盘) | USB软驱(古董级) |
| 0x05 | SCSI透明命令集 | U盘、移动硬盘(最常见) |
| 0x06 | LSB(逻辑块命令) | 某些嵌入式存储 |
嗯,这里要注意:我们平时做U盘开发,用的基本都是子类0x05——SCSI透明命令集。为什么叫“透明”?因为USB主机把SCSI命令原封不动地封装在USB包里发出去,设备端收到后直接解析执行。中间不做任何转换,所以叫“透明”。
核心要点:MSC协议本质上是在USB总线上跑SCSI命令。主机发SCSI命令(比如READ10、WRITE10),设备执行后返回状态。就这么简单。
1.2 Bulk-Only传输与CBI传输对比
MSC协议定义了两种传输方式:Bulk-Only和CBI。我当年刚接触时,以为CBI是更高级的版本,结果完全搞反了。
Bulk-Only传输——这是目前绝对的主流。它只用Bulk端点来传输数据和控制信息。结构非常清晰:
- 命令阶段:主机通过Bulk OUT端点发送CBW(命令块包装)
- 数据阶段:根据命令方向,通过Bulk IN或Bulk OUT传输数据
- 状态阶段:设备通过Bulk IN端点返回CSW(命令状态包装)
CBI传输——全称Control/Bulk/Interrupt。它用控制端点发命令,Bulk端点传数据,Interrupt端点收状态。听起来很合理对吧?但实际用起来问题不少。
我做过一个对比表格,大家一看就明白:
| 对比项 | Bulk-Only | CBI |
|---|---|---|
| 命令传输 | Bulk OUT端点 | 控制端点(默认管道) |
| 数据传输 | Bulk IN/OUT端点 | Bulk IN/OUT端点 |
| 状态返回 | Bulk IN端点(CSW) | Interrupt IN端点 |
| 端点数量 | 2个(IN+OUT) | 3个(IN+OUT+中断) |
| 协议复杂度 | 低 | 高 |
| 兼容性 | 极好(Windows/Linux/macOS都支持) | 差(很多系统已弃用) |
| 当前使用 | 几乎所有U盘、移动硬盘 | 基本淘汰 |
避坑指南:我曾经接手过一个老项目,设备用的是CBI传输。在Windows 7上跑得好好的,换到Windows 10就死活不认。查了两天才发现——微软从Win8开始就移除了CBI驱动。所以新项目千万别碰CBI,老老实实用Bulk-Only。
为什么会这样?我个人理解是:CBI的设计思路太“学院派”了。它把命令、数据、状态分开用不同端点传,逻辑上很清晰。但实际USB总线是共享带宽的,多一个端点就多一层调度开销。Bulk-Only把所有东西塞进两个Bulk端点,简单粗暴,反而更稳定。
1.3 端点描述符与接口描述符解析
好,接下来咱们看看描述符。这是USB枚举阶段设备向主机“自我介绍”的数据结构。我建议初学者先把接口描述符和端点描述符吃透,其他的后面慢慢补。
接口描述符——它告诉主机:我这个设备里有一个MSC功能。关键字段如下:
| 字段 | 长度 | 说明 |
|---|---|---|
| bInterfaceNumber | 1字节 | 接口编号,从0开始 |
| bAlternateSetting | 1字节 | 备用设置,MSC一般用0 |
| bNumEndpoints | 1字节 | 端点数量(不含端点0) |
| bInterfaceClass | 1字节 | 固定为0x08(MSC类) |
| bInterfaceSubClass | 1字节 | 子类代码,常见0x05或0x06 |
| bInterfaceProtocol | 1字节 | 协议代码,0x50=Bulk-Only,0x00=CBI |
| iInterface | 1字节 | 字符串描述符索引 |
嗯,这里有个关键点:bInterfaceProtocol字段。0x50表示Bulk-Only传输,0x00表示CBI传输。主机就是靠这个字段来判断该加载哪个驱动。
端点描述符——它描述每个端点的属性。Bulk-Only传输需要两个Bulk端点:
| 字段 | Bulk OUT端点 | Bulk IN端点 |
|---|---|---|
| bEndpointAddress | 0x01(OUT方向) | 0x82(IN方向) |
| bmAttributes | 0x02(Bulk类型) | 0x02(Bulk类型) |
| wMaxPacketSize | 0x40(64字节,全速) 0x200(512字节,高速) |
0x40(64字节,全速) 0x200(512字节,高速) |
| bInterval | 0x00(Bulk端点无间隔) | 0x00(Bulk端点无间隔) |
个人经验:我习惯把Bulk OUT端点地址设为0x01,Bulk IN设为0x82。这不是强制规定,但几乎所有MSC设备都这么用。如果你标新立异用0x02和0x84,某些主机的驱动可能会不认。嗯,这就是“约定优于配置”的典型例子。
咱们来看一个实际的描述符组合示例。这是一个典型的Bulk-Only MSC设备的接口+端点描述符:
// 接口描述符
0x09, // bLength: 9字节
0x04, // bDescriptorType: 接口描述符
0x00, // bInterfaceNumber: 0
0x00, // bAlternateSetting: 0
0x02, // bNumEndpoints: 2个端点
0x08, // bInterfaceClass: MSC类
0x05, // bInterfaceSubClass: SCSI透明命令集
0x50, // bInterfaceProtocol: Bulk-Only传输
0x00, // iInterface: 无字符串描述符
// 端点描述符 - Bulk OUT
0x07, // bLength: 7字节
0x05, // bDescriptorType: 端点描述符
0x01, // bEndpointAddress: OUT端点1
0x02, // bmAttributes: Bulk传输
0x40, 0x00, // wMaxPacketSize: 64字节(全速)
0x00, // bInterval: 0
// 端点描述符 - Bulk IN
0x07, // bLength: 7字节
0x05, // bDescriptorType: 端点描述符
0x82, // bEndpointAddress: IN端点2
0x02, // bmAttributes: Bulk传输
0x40, 0x00, // wMaxPacketSize: 64字节(全速)
0x00, // bInterval: 0
这段代码我手写过无数次。每次新项目开始,我都会从这段模板改起。你想想看,只要主机枚举时读到这组描述符,就知道:哦,这是一个MSC设备,用Bulk-Only协议,有两个Bulk端点。剩下的就是发SCSI命令了。
调试技巧:如果你在调试时发现设备枚举失败,先用USB分析仪抓包。重点看主机请求接口描述符后,设备返回的数据对不对。我遇到过好几次,问题出在bInterfaceProtocol写成了0x00(CBI),但设备实际只实现了Bulk-Only。主机加载CBI驱动后发命令,设备根本不认识,然后就死机了。
最后,我用一张图来总结MSC协议的整体架构。这张图我每次培训都会画,大家把它记在脑子里:
这张图把MSC协议分成了四层:主机层、USB总线传输层、设备层、存储介质层。数据从应用程序下来,经过文件系统、MSC驱动,封装成CBW通过USB总线发出去。设备收到后解析SCSI命令,读写存储介质,再通过CSW返回状态。整个过程环环相扣,缺一不可。