U 盘
《圈圈教你学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 */
#else
WBVAL(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 */
#else
0x00, /* bDeviceClass */
0x00, /* bDeviceSubClass */
0x00, /* bDeviceProtocol */
#endif
USBD_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, // bLength
USB_CONFIGURATION_DESCRIPTOR_TYPE, // bDescriptorType
WBVAL(USBD_WTOTALLENGTH_MAX), // wTotalLength
USBD_IF_NUM_MAX, // bNumInterfaces
0x01, // bConfigurationValue: 0x01 is used to select this configuration
0x00, // iConfiguration: no string to describe this configuration
USBD_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 FAT
fat_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 dir
dir_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 盘出来?