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主机(Host) 操作系统MSC驱动 → 文件系统驱动 → 应用程序 USB总线传输层 Bulk-Only协议:CBW → 数据 → CSW USB设备(Device) SCSI命令解析 → 块设备驱动 → 存储介质 存储介质(Flash / 硬盘 / SD卡) LBA逻辑块地址 → 物理扇区读写 数据流向:主机 → 设备 命令流向:主机 → 设备

这张图把MSC协议分成了四层:主机层、USB总线传输层、设备层、存储介质层。数据从应用程序下来,经过文件系统、MSC驱动,封装成CBW通过USB总线发出去。设备收到后解析SCSI命令,读写存储介质,再通过CSW返回状态。整个过程环环相扣,缺一不可。