- 1 USB 大容量存储设备
- 2 设备描述符
- 3 字符串描述符
- 4 配置描述符集合
- 4.1 配置描述符
- 4.2 接口描述符
- 4.3 端点描述符
- 6 类特殊请求
- 6.1 Get Max LUN 请求
- 6.2 Bulk-Only Mass Storage Reset 请求
- 7 Bulk-Only 传输协议的数据流模型
- 7.1 CBW 的结构
- 7.2 CSW 的结构
- 7.3 对批量数据的处理
- 8 SCSI 命令集和 UFI 命令集
- 8.1 查询命令 INQUIRY
- 8.2 读格式化容量命令 READ FORMAT CAPACITIES
- 8.3 读容量命令 READ CAPACITY
- 8.4 READ(10) 命令
- 8.5 WRITE(10) 命令
- 8.6 REQUEST SENSE 命令
- 8.7 TEST UNIT READY 命令
- 9 FAT 文件系统
- 9.1 关于 DBR
- 9.2 关于 FAT 表
- 9.3 关于目录项
- 总结
《圈圈教你学USB》 第 8 章学习笔记。这篇文章我们会结合 daplink 源码来看。
1 USB 大容量存储设备
-
1)USB 协议规定的大容量存储设备(Mass Storage Device,MSD)包括:U 盘、USB 移动硬盘、USB 移动光驱。
-
2)MSD 设备的接口描述符中,有以下取值:
- (1)接口类(bInterfaceClass):0x08 表示 MSD 设备
- (2)接口子类(bInterfaceSubClass):0x06 表示 SCSI(Small Computer System Interface,小型计算机系统接口) 命令集
- (3)接口协议(bInterfaceProtocol):0x00/0x01 需要使用中断传输;0x05 仅使用批量传输
2 设备描述符
1)设备描述符的结构如下表:
2)设备描述符在 daplink 中的实现(位于 source/usb/usb_lib.c 文件):
/* USB Device Standard Descriptor */
__WEAK \
const U8 USBD_DeviceDescriptor[] = {USB_DEVICE_DESC_SIZE, /* bLength */USB_DEVICE_DESCRIPTOR_TYPE, /* bDescriptorType */
#if (USBD_BOS_ENABLE)WBVAL(0x0210), /* 2.10 */ /* bcdUSB */
#elif ((USBD_HS_ENABLE) || (USBD_MULTI_IF))WBVAL(0x0200), /* 2.00 */ /* bcdUSB */
#elseWBVAL(0x0110), /* 1.10 */ /* bcdUSB */
#endif
#if (USBD_MULTI_IF)USB_DEVICE_CLASS_MISCELLANEOUS, /* bDeviceClass */0x02, /* bDeviceSubClass */0x01, /* bDeviceProtocol */
#elif (USBD_CDC_ACM_ENABLE)USB_DEVICE_CLASS_COMMUNICATIONS, /* bDeviceClass CDC*/0x00, /* bDeviceSubClass */0x00, /* bDeviceProtocol */
#else0x00, /* bDeviceClass */0x00, /* bDeviceSubClass */0x00, /* bDeviceProtocol */
#endifUSBD_MAX_PACKET0, /* bMaxPacketSize0 */WBVAL(USBD_DEVDESC_IDVENDOR), /* idVendor */WBVAL(USBD_DEVDESC_IDPRODUCT), /* idProduct */WBVAL(USBD_DEVDESC_BCDDEVICE), /* bcdDevice */0x01, /* iManufacturer */0x02, /* iProduct */0x03 * USBD_STRDESC_SER_ENABLE, /* iSerialNumber */0x01 /* bNumConfigurations: one possible configuration*/
};
3 字符串描述符
在 daplink 中的实现由于篇幅原因不再展示,感兴趣的可以在 source/usb/usb_lib.c 文件中找 USBD_StringDescriptor 即可。
4 配置描述符集合
要想找到 daplink 中描述符的实现位置,可以首先找到定义在 source/usb/usb_def.h 文件中的 USB Descriptor Types(USB 描述符类型),然后根据这些类型来找到各个描述符。
4.1 配置描述符
1)配置描述符的结构:
2)配置描述符实现(见 source/usb/usb_lib.c 文件中的 start_desc_fill() 函数):
const U8 start_desc[] = {/* Configuration 1 */USB_CONFIGUARTION_DESC_SIZE, // bLengthUSB_CONFIGURATION_DESCRIPTOR_TYPE, // bDescriptorTypeWBVAL(USBD_WTOTALLENGTH_MAX), // wTotalLengthUSBD_IF_NUM_MAX, // bNumInterfaces0x01, // bConfigurationValue: 0x01 is used to select this configuration0x00, // iConfiguration: no string to describe this configurationUSBD_CFGDESC_BMATTRIBUTES | // bmAttributes(USBD_POWER << 6),USBD_CFGDESC_BMAXPOWER // bMaxPower, device power consumption
};
4.2 接口描述符
1)接口描述符的结构:
2)daplink 中,在 source/usb/usb_lib.c 文件定义了许多种接口描述符:HID(人机交互设备)、WEBUSB(网页端USB)、MSC(大容量设备类)、BULK(批量设备类)、ADC(音频设备类)、CDC_ACM(通信设备类)。
4.3 端点描述符
1)端点描述符的结构:
2)daplink 中,每个接口描述符后都会跟着端点描述符,比如我们的 MSC 接口描述符后面的端点描述符如下:
#define MSC_EP /* MSC Endpoints for Low-speed/Full-speed */ \
/* Endpoint, EP Bulk IN */ \USB_ENDPOINT_DESC_SIZE, /* bLength */ \USB_ENDPOINT_DESCRIPTOR_TYPE, /* bDescriptorType */ \USB_ENDPOINT_IN(USBD_MSC_EP_BULKIN), /* bEndpointAddress */ \USB_ENDPOINT_TYPE_BULK, /* bmAttributes */ \WBVAL(USBD_MSC_WMAXPACKETSIZE), /* wMaxPacketSize */ \0x00, /* bInterval: ignore for Bulk transfer */ \\
/* Endpoint, EP Bulk OUT */ \USB_ENDPOINT_DESC_SIZE, /* bLength */ \USB_ENDPOINT_DESCRIPTOR_TYPE, /* bDescriptorType */ \USB_ENDPOINT_OUT(USBD_MSC_EP_BULKOUT),/* bEndpointAddress */ \USB_ENDPOINT_TYPE_BULK, /* bmAttributes */ \WBVAL(USBD_MSC_WMAXPACKETSIZE), /* wMaxPacketSize */ \0x00, /* bInterval: ignore for Bulk transfer */
6 类特殊请求
第 1 章我们知道:
(1)USB 以包为基本单位来传输,多个包组成事务来保证传输完整性,同时包又可以分成不同的域。
(2)包的结构为:同步域 + 包标识符(PID,Packet Identifier) + [域] + 包结束符 EOP(End Of Packet)。
(3)根据包 PID 的不同可以将包分为:令牌包、数据包、握手包、特殊包。
第 3 章我们知道:
设备请求(结构见下图):可以通过 bmRequestType 的 bit[6:5] 来设置请求类型。如为 0 时为标准请求;为 1 时为类请求。
这一节将介绍 2 个类特殊请求(即 bmRequestType 的 bit[6:5] = 1)。
在 MSD 设备的 Bulk Only Transprot 协议中,规定了 2 个类特殊请求:Bulk-Only Mass Storage Reset(复位到命令状态) 和 Get Max LUN(获取最大逻辑单元)。
在 daplink 的 source/usb/usbd_core.c 文件的 USBD_EndPoint0() 函数中,可以看到 标准请求、类请求、厂商请求的处理逻辑,其中就包含下方的两个类特殊请求的处理,感兴趣可以看看。
6.1 Get Max LUN 请求
Get Max LUN 请求用来获取最大逻辑单元。
示例:A1 FE 00 00 00 00 01 00
A1 1010_0001B,请求类型为“类请求”
FE 请求代码为 GET MAX LUN
00 00
00 00 请求接口号
01 00 数据长度为 1 字节
6.2 Bulk-Only Mass Storage Reset 请求
Bulk-Only Mass Storage Reset 请求是通知设备接下来的批量端点输出数据为 CBW 包(Command Block Wrapper,命令块封装包)。
示例:TODO
7 Bulk-Only 传输协议的数据流模型
Bulk-Only 传输协议中,分为 3 个阶段:命令阶段、数据阶段、状态阶段。
(1)命令阶段:由主机通过批量端点发送一个 CBW,其中定义命令、传输数据方向、传输数据数量
(2)数据阶段:传输方向由命令阶段决定
(3)状态阶段:总是由设备返回该命令完成的状态
在 daplink 的 source/usb/msc/usbd_msc.c 文件的 USBD_MSC_BulkOut() 函数中,可以看到获取 CBW 与响应 CSW 的逻辑。
7.1 CBW 的结构
CBW(Command Block Wrapper,命令块封装包)的结构如下:
属性 | 描述 |
---|---|
dCBWSignature | 魔数 0x43425355(小端),意为 USBC(USB Command)的 ASCII 码 |
dCBWTag | CBW 标签,主机分配,设备响应时返回 |
dCBWDataTransferLength | 数据阶段传输的数据长度,单位:字节,小端 |
bmCBWFlags | CBW 标志,bit7 表示数据传输方向(0,输出;1,输入),bit[6:0] 为 0 |
bCBWLUN | 目标逻辑单元的编号。bit[3:0] 逻辑单元编号 |
bCBWCBLengt | CBWCB 长度。bit[4:0],有效范围 1~16 |
CBWCB | 需要执行的命令。见第 8 节。 |
7.2 CSW 的结构
CSW(Command Status Wrapper,命令状态封装包)结构如下:
属性 | 描述 |
---|---|
dCSWSignature | 魔数 0x53425355(小端),意为 USBS(USB Satus)的 ASCII 码 |
dCSWTag | CSW 包的标签,值为 CBW 包的 dCBWTag |
dCSWDataResidue | 命令完成时剩余字节数。表示实际完成传输的字节数与 dCBWDataTransferLength 的差值 |
bCSWStatus | 命令执行状态。0x00 成功;0x01 失败;0x02 阶段错误;其它保留 |
7.3 对批量数据的处理
8 SCSI 命令集和 UFI 命令集
SCSI(Small Computer System Interface,小型计算机系统接口)对命令及其响应规定了完整的协议。
在 U 盘中常用的 SCSI 命令有:INQUIRY、READ CAPACITY、READ(10)、WRITE(10) 等
UFI(USB Floppy Interface,USB 软盘接口)结合 SCSI-2 和 SFF-8070i 命令集抽取的命令。
UFI 命令出现在 CBW 包的 CBWCB 字段中。最多 16 个字节,不足补0,多余忽略。命令第 1 字节为操作代码。
见 《usbmass-ufi10.pdf》第 4 节 UFI Command Descriptions: https://www.usb.org/document-library/mass-storage-ufi-command-specification-10
8.1 查询命令 INQUIRY
见 daplink 中 source/usb/msc/usbd_msc.c 文件的 USBD_MSC_Inquiry() 函数。
8.2 读格式化容量命令 READ FORMAT CAPACITIES
8.3 读容量命令 READ CAPACITY
8.4 READ(10) 命令
8.5 WRITE(10) 命令
8.6 REQUEST SENSE 命令
8.7 TEST UNIT READY 命令
9 FAT 文件系统
FAT(File Allocation Table,文件分配表)是为了方便文件存储、检索、添加、删除等操作,而提出的一种链表式的文件组织结构。
磁盘物理上以 “扇区” 为单位来组织存储,而 FAT 文件系统则以 “簇” 为单位组织。两者的映射关系为:一般一个簇由几个扇区组成。
FAT 逻辑上是一张表格,表格中的每项保存文件的文件名、文件长度、创建日期、起始簇号等信息。
磁盘格式化成 FAT16 格式后的内容如下:
-
1)MBR(Master Boot Record,主引导记录):记录 MBR 引导代码、磁盘分区等信息。FAT16 每分区最大只有 2GB(65536x32KB) 存储空间,要映射更多的空间只能添加逻辑分区。
-
2)EBR(Extended Boot Record,扩展引导记录):MBR 中的磁盘分区表不够用时,使用 EBR
-
3)DBR(DOS Boot Record,磁盘操作系统引导记录):每个逻辑分区的引导记录。记录分区信息及引导代码。
MBR、EBR、DBR 通常只占用一个扇区(512KB)
-
4)FAT1/2:文件分配表,表格项记录文件相关信息。FAT2 为 FAT1 的副本。
9.1 关于 DBR
1)DBR 占据逻辑分区的 0 扇区,大小通常 512 字节。
示例:
来源:DAPLINK 源码的 source/daplink/drag-n-drop/virtual_fs.c 文件
static const mbr_t mbr_tmpl = {/*uint8_t[11]*/.boot_sector = {0xEB, 0x3C, 0x90,'M', 'S', 'D', '0', 'S', '4', '.', '1' // OEM Name in text (8 chars max)},/*uint16_t*/.bytes_per_sector = 0x0200, // 512 bytes per sector/*uint8_t */.sectors_per_cluster = 0x08, // 4k cluser/*uint16_t*/.reserved_logical_sectors = 0x0001, // mbr is 1 sector/*uint8_t */.num_fats = 0x02, // 2 FATs/*uint16_t*/.max_root_dir_entries = 0x0020, // 32 dir entries (max)/*uint16_t*/.total_logical_sectors = 0x1f50, // sector size * # of sectors = drive size/*uint8_t */.media_descriptor = 0xf8, // fixed disc = F8, removable = F0/*uint16_t*/.logical_sectors_per_fat = 0x0001, // FAT is 1k - ToDO:need to edit this/*uint16_t*/.physical_sectors_per_track = 0x0001, // flat/*uint16_t*/.heads = 0x0001, // flat/*uint32_t*/.hidden_sectors = 0x00000000, // before mbt, 0/*uint32_t*/.big_sectors_on_drive = 0x00000000, // 4k sector. not using large clusters/*uint8_t */.physical_drive_number = 0x00,/*uint8_t */.not_used = 0x00, // Current head. Linux tries to set this to 0x1/*uint8_t */.boot_record_signature = 0x29, // signature is present/*uint32_t*/.volume_id = 0x27021974, // serial number// needs to match the root dir label/*char[11]*/.volume_label = {'D', 'A', 'P', 'L', 'I', 'N', 'K', '-', 'D', 'N', 'D'},// unused by msft - just a label (FAT, FAT12, FAT16)/*char[8] */.file_system_type = {'F', 'A', 'T', '1', '6', ' ', ' ', ' '},/*uint8_t[448]*/.bootstrap = {// 篇幅原因省略引导代码},// Signature MUST be 0xAA55 to maintain compatibility (i.e. with Android)./*uint16_t*/.signature = 0xAA55,
};
9.2 关于 FAT 表
1)FAT 表紧跟着 DBR,即从扇区 1 开始。
示例:在 daplink 的 source/daplink/drag-n-drop/virtual_fs.c 文件中的 vfs_init() 函数,结合 FAT16 文件系统的格式,可以看到 mbr 块、fat 块、根目录块的初始化。
file_allocation_table_t fat;
................
void vfs_init(const vfs_filename_t drive_name, uint32_t disk_size)
{
................memset(&mbr, 0, sizeof(mbr));memset(&fat, 0, sizeof(fat));fat_idx = 0;
................// Initialize FATfat_idx = 0;write_fat(&fat, fat_idx, 0xFFF8); // Media type "media_descriptor"fat_idx++;write_fat(&fat, fat_idx, 0xFFFF); // FAT12 - always 0xFFF (no meaning), FAT16 - dirty/clean (clean = 0xFFFF)fat_idx++;
................// Initialize root dirdir_idx = 0;dir_current.f[dir_idx] = root_dir_entry;memcpy(dir_current.f[dir_idx].filename, drive_name, sizeof(dir_current.f[0].filename));dir_idx++;
................
}
9.3 关于目录项
目录项的结构:
示例:在 daplink 的 source/daplink/drag-n-drop/virtual_fs.c 文件中
typedef struct FatDirectoryEntry {vfs_filename_t filename;uint8_t attributes;uint8_t reserved;uint8_t creation_time_ms;uint16_t creation_time;uint16_t creation_date;uint16_t accessed_date;uint16_t first_cluster_high_16;uint16_t modification_time;uint16_t modification_date;uint16_t first_cluster_low_16;uint32_t filesize;
} __attribute__((packed)) FatDirectoryEntry_t;
总结
结合 daplink 来看,根据鸭子定律,当我们通过 USB 总线协议告诉 USB 主机:插入的设备识别为 64MB U 盘,用起来像 64MB U 盘,那么它就是容量为 64MB 的 U 盘。实际上,运行 daplink 的 MCU 怎么可能有那么大的 FLASH。
而且别说是 64MB,就是 64GB 也可以(需要文件系统支持),因为 daplink 只需要一次性数据,并不需要把复制到 U 盘的数据再读取出来,那么这个 DAPLINK U 盘完全就是你强任你强,清风绕山岗了。
同理,找一个具有 USB 外设的 4G 模块和一台云对象存储服务器,理论上是不是可以开发一个云 U 盘出来?