资讯动态

USB枚举与描述符详解:从U盘插入到BOT传输的完整链路

发布时间:2026/10/21 19:25:19 来源:尧图企业网站定制
把U盘插进电脑系统几乎在同一时间弹出盘符。多数人觉得这事稀松平常但USB子系统内部刚刚完成了一次相当讲究的入职流程设备要自报家门操作系统要核验身份分配唯一地址激活配置然后才开始真正干活的大容量存储传输。这整套流程叫枚举而枚举过程中设备交给主机的每一份简历就是一组叫USB描述符的数据结构。这篇笔记我拿U盘当例子把设备描述符、配置描述符、接口描述符、端点描述符、字符串描述符逐个拆开讲清楚它们为什么这样写、主机怎么读、实际写U盘固件时最容易被卡住的几个细节。内容按枚举流程 → 描述符字段 → 抓包实测 → BOT传输 → 可直接抄的模板 → 故障排查的顺序展开适合刚接触USB协议栈、想搞懂Mass Storage设备或者正在调自己板子上U盘功能的开发者参考。1. 枚举对话从D被拉高到SET_CONFIGURATION主机靠什么认出U盘1.1 总线复位与高速握手不只是复位设备插入后USB主机控制器首先看到的是总线状态变化。全速和高速设备通过D线上的1.5kΩ上拉电阻告诉主机我来了低速设备则是在D-线上做同样的事。主机看到上拉后会先对端口做一次总线复位也就是把D和D-同时拉低至少10ms让设备回到默认状态。高速设备不会老实待在复位状态里它会检测到主机发出的Chirp K信号然后以Chirp K/J序列回应主机确认后双方才进入高速模式。很多第一次接触USB的人会忽略这一步把复位单纯理解成清零重来。实际上一旦设备声明自己支持高速后续所有描述符解析的语义都会变化最典型的就是端点最大包长从全速的64字节变成高速的512字节。如果高速握手失败设备并不会直接变成废品而是退化为全速运行现象就是U盘能用但速度上不去。调试的时候如果发现设备枚举成功、读写速度却始终只有每秒几MB先不要怀疑闪存多半是高速握手没成功。1.2 为什么第一次GET_DESCRIPTOR往往只读8字节复位之后设备处于Default状态地址是0主机发出第一个标准请求GET_DESCRIPTOR请求设备描述符。这个阶段有个很微妙的细节——主机通常只请求前8字节或者说即使请求了64字节设备可以只返回18字节但主机最少需要拿到前8字节。原因在于设备描述符的前8字节里包含了三个关键信息描述符长度、描述符类型、USB规范版本、设备类以及最重要的bMaxPacketSize0。bMaxPacketSize0告诉主机端点0的最大包长是多少主机只有知道这个值才能确定后续SET_ADDRESS请求的状态阶段该怎么拆分数据包。换句话说这是先探路再正式通信的策略。实际抓包里常见的组合是第一次GET_DESCRIPTOR(Device, wLength64)设备返回完整的18字节也有的固件实现只响应8字节主机拿到bMaxPacketSize0后并不重新请求直接进入SET_ADDRESS。两种情况都合法但要明白背后的逻辑主机需要的是足够完成下一步动作的信息而不是全部信息。1.3 SET_ADDRESS的时机差一个字都不行主机从设备描述符里拿到bMaxPacketSize0后会发送SET_ADDRESS请求给设备分配一个非0地址。这一步最容易踩坑的地方是实现细节USB规范规定设备必须在SET_ADDRESS请求的状态阶段完成之后才把内部地址寄存器切换到新地址。我见过不止一个新手写的固件在控制处理函数里收到SET_ADDRESS后立即切换地址结果设备状态阶段出错主机完全找不到设备。如果用的是现成USB控制器通常寄存器层面已经帮你处理了这个时序如果是自己用FPGA或者简单MCU硬搭协议栈这一步必须严格按规范来。SET_ADDRESS完成后设备进入Address状态。主机紧接着用新地址重新读取一次完整的设备描述符然后开始请求配置描述符。此时设备已经具备独立身份不再是总线上那个匿名设备了。2. 五类核心描述符逐字节拆解U盘在协议层的身份证2.1 设备描述符整体身份与EP0能力U盘的设备描述符固定18字节第一个字节是bLength0x12第二个字节是bDescriptorType0x01。USB协议里每种描述符都有固定类型编号主机靠这个字段来决定怎么解析后续数据。我对典型USB 2.0全速U盘设备描述符做了个字段展开这样看最直观字段长度典型值含义bLength10x12设备描述符总长18字节bDescriptorType10x01设备描述符类型bcdUSB20x0200实现USB 2.0规范bDeviceClass10x00类定义在接口描述符层bDeviceSubClass10x00保留bDeviceProtocol10x00保留bMaxPacketSize010x40EP0最大包长64字节idVendor20x1234(举例)厂商识别号idProduct20x5678(举例)产品识别号bcdDevice20x0100设备版本号iManufacturer10x01厂商字符串索引iProduct10x02产品字符串索引iSerialNumber10x03序列号字符串索引bNumConfigurations10x01配置数量U盘的设备描述符里bDeviceClass通常是0x00这意味着设备级不声明大类真正的身份要等到接口描述符再宣布。这是USB协议里很关键的分层思想设备描述符只描述我是谁接口描述符描述我会干什么。2.2 配置描述符wTotalLength的连锁效应配置描述符9字节但它引领的是一整套配置集合。主机第一次请求配置描述符时通常只请求9字节目的是读取wTotalLength字段。wTotalLength表示整个配置集合的总长度包括配置描述符本身、所有接口描述符、端点描述符以及可能存在的类特定描述符。U盘的配置集合很标准配置描述符9字节 接口描述符9字节 两个端点描述符各7字节总共32字节即wTotalLength0x0020。注意USB协议的多字节字段一律采用小端序所以wTotalLength在字节流里写作20 00。bmAttributes字段也值得留心。U盘通常写0x80表示总线供电且不支持远程唤醒。bit7必须恒为1bit6置1表示设备从总线取电bit5置1表示支持远程唤醒。如果你把bit5置1却又没有实现远程唤醒逻辑主机尝试唤醒时会发现总线没反应轻则驱动日志报错重则设备被标记为异常。bMaxPower字段的单位是2mA0x32等于50×2mA100mA。总线供电U盘如果实际峰值电流超过声明值某些严格的hub会直接拒绝供电。自供电设备可以写很小甚至写0x01反正不需要从总线拿多少电。2.3 接口描述符0x08 / 0x06 / 0x50才是U盘身份接口描述符是U盘身份的真正来源。bInterfaceClass0x08表示Mass Storage类bInterfaceSubClass0x06表示使用SCSI透明命令集bInterfaceProtocol0x50表示使用Bulk-Only Transport协议也就是常说的BOT。三个字段组合起来主机才确定这是一个使用SCSI命令、通过Bulk端点传输数据的U盘。bNumEndpoints2表示这个接口下有两个端点通常是一个Bulk OUT和一个Bulk IN。bInterfaceNumber0表示接口编号bAlternateSetting0表示当前备用设置。U盘一般只有一个接口一个备用设置不需要玩接口聚合那套复杂操作。补充一点如果bInterfaceProtocol0x62那就不属于BOT而是UASP协议。UASP在USB 3.0时代很常见它能支持命令队列突破BOT一次只能执行一条命令的瓶颈。但USB 2.0下的绝大多数U盘走BOT所以本篇只深入讲0x50。2.4 端点描述符两个Bulk通道的约定U盘的端点描述符有两个结构完全一样只是方向不同。OUT端点地址通常为0x01IN端点地址通常为0x82。端点地址的bit7表示方向低4位是端点号所以0x01是端点1的OUT方向0x82是端点2的IN方向。这里经常有人读迷糊0x82看起来像个很大的数其实是IN方向加端点号2的组合。bmAttributes0x02表示Bulk传输类型。wMaxPacketSize在全速U盘下是0x004064字节在高速U盘下是0x0200512字节。bInterval字段对Bulk端点必须为0如果从别的例程里抄来一个非0值某些主机控制器会给出警告甚至拒绝加载驱动。2.5 字符串描述符厂商、产品与序列号字符串描述符不是枚举必需的但如果设备描述符里的iManufacturer、iProduct、iSerialNumber指向非0索引主机就会来请求对应字符串。字符串描述符的结构很直接第一个字节是总长度第二个字节是类型0x03后面是UTF-16LE编码的字符数据。语言标识符描述符也是字符串描述符家族的一员固定4字节04 03 09 04表示默认语言是0x0409美国英语。字符串长度计算有一个最常见的错误来源我后面专门说。对于U盘来说序列号字符串的意义远超显示层面。Windows会拿厂商ID、产品ID和序列号三者组合来区分同一台机器上插过的不同U盘如果两块U盘序列号相同系统可能把它们当成同一设备盘符缓存、驱动绑定全都会乱。所以量产的U盘固件通常会提供一个动态更新序列号的接口。3. 一次真实枚举的抓包回放每个请求都在问什么3.1 抓包的完整请求序列下面这段是某块手写U盘固件在总线分析仪上抓到的完整枚举序列设备描述符数据里的VID/PID是保留给示例用的假值不要对号入座[0] RESET [1] 主→设: SETUP 80 06 00 01 00 00 40 00 // GET_DESCRIPTOR, 设备描述符, index0, len64 设→主: DATA 12 01 00 02 00 00 00 40 34 12 78 56 00 01 01 02 03 01 [2] 主→设: SETUP 00 05 01 00 00 00 00 00 // SET_ADDRESS, 地址1 [3] 主→设: SETUP 80 06 00 01 00 00 12 00 // GET_DESCRIPTOR, 设备描述符, len18 设→主: DATA 12 01 00 02 00 00 00 40 34 12 78 56 00 01 01 02 03 01 [4] 主→设: SETUP 80 06 00 02 00 00 09 00 // GET_DESCRIPTOR, 配置描述符, len9 设→主: DATA 09 02 20 00 01 01 00 80 32 [5] 主→设: SETUP 80 06 00 02 00 00 20 00 // 再次请求配置集合, len32 设→主: DATA 09 02 20 00 01 01 00 80 32 // 配置描述符 09 04 00 00 02 08 06 50 00 // 接口描述符 07 05 01 02 40 00 00 // 端点 OUT 07 05 82 02 40 00 00 // 端点 IN [6] 主→设: SETUP 80 06 00 03 00 00 04 00 // GET_DESCRIPTOR, 字符串: 语言ID 设→主: DATA 04 03 09 04 [7] 主→设: SETUP 80 06 01 03 00 00 40 00 // GET_DESCRIPTOR, 字符串: iProduct 设→主: DATA 18 03 4D 00 59 00 20 00 55 00 53 00 42 00 20 00 44 00 49 00 53 00 4B 00 [8] 主→设: SETUP 00 09 01 00 00 00 00 00 // SET_CONFIGURATION, 配置值1 [9] 主→设: SETUP 21 FF 00 00 00 00 00 00 // Mass Storage Reset, 类请求 [10] 主→设: SETUP A1 FE 00 00 00 00 01 00 // Get Max LUN 设→主: DATA 00 // LUN 数量 13.2 描述符字节流里容易读反的字段看抓包数据时最容易犯的错是把多字节字段按大端读。USB协议统一使用小端序设备描述符里的idVendor0x1234在字节流里写作34 12idProduct0x5678写作78 56配置描述符的wTotalLength0x0020写作20 00。刚开始分析抓包时我经常在纸上把字段反着写导致一个简单的设备描述符半天对不上号。还有一点第一次GET_DESCRIPTOR(Device)请求的wLength虽然是64但设备只需返回18字节的实际描述符不需要补齐64字节。如果固件死板地按请求长度填充响应缓冲区反而可能把缓冲区里的残留垃圾一起发出去主机解析到多余的字节后不一定报错但总线分析仪里看起来会很奇怪。3.3 SET_CONFIGURATION后还有两个类请求很多第一次做Mass Storage设备的同学以为枚举在一句SET_CONFIGURATION之后就结束了实际上主机接下来还会发两个BOT类特定的控制请求。第一个是Mass Storage ResetbmRequestType0x21bRequest0xFF用来把设备的BOT状态机复位到空闲状态。第二个是Get Max LUNbmRequestType0xA1bRequest0xFEwLength1设备需要返回一个字节表示逻辑单元数量通常是0x00。这两个请求在Linux的usb-storage驱动和Windows的USBSTOR驱动里都会出现。固件里如果只实现了标准请求而没有处理这两个类请求常见的现象是枚举看似成功设备管理器也认出了大容量存储设备但随后一直卡住无法读出盘符。处理起来很简单收到Mass Storage Reset就重置BOT内部状态机并返回一个零长度状态包收到Get Max LUN就返回单字节0x00。4. 类接口背后的BOT传输CBW与CSW怎样完成一次扇区读写4.1 CBW主机下达的命令邮票描述符只负责自我介绍真正读写U盘数据靠的是BOT协议的三步走命令块CBW、数据阶段、状态块CSW。CBW固定31字节由主机通过Bulk OUT端点发给设备。CBW里的关键字段包括dCBWSignature固定是USBC四个ASCII字符即0x43425355dCBWTag由主机随机生成设备在后续CSW里必须原样回显dwCBWTransferLength表示数据阶段的字节数bmCBWFlags为0x80表示数据阶段是设备到主机IN为0x00表示主机到设备OUTbCBWCBLength表示后面CBWCB命令块的长度范围1到16字节。4.2 CSW设备交回的收据设备处理完命令后通过Bulk IN端点返回13字节CSW。dCSWSignature固定是USBS四字节即0x53425355dCSWTag回显CBW里的tagdCSWDataResidue表示还剩多少数据没传完0表示数据传输量和预期完全一致bCSWStatus为0x00表示成功0x01表示命令执行失败0x02表示阶段错误。阶段错误是BOT协议里特别要小心的状态。USB协议规定设备如果发现主机在数据阶段收发方向与CBW声明不符或者主机在非预期时机发来CBW设备应进入错误恢复状态并在CSW里报告Phase Error之后主机应该发Mass Storage Reset来恢复。4.3 READ(10)的一次完整交互拿最常见的SCSI READ(10)命令举例命令码0x28。假设主机要读LBA 0的一个512字节扇区完整交互如下主机先发31字节CBW55 53 42 43 // USBC 04 03 02 01 // dCBWTag 0x01020304 00 02 00 00 // dwCBWTransferLength 512, 小端 80 // 数据阶段: 设备→主机 00 // LUN 0 0A // CBWCB长度 10 28 00 00 00 00 00 00 00 01 00 // READ(10), LBA0, 传输块数1 00 00 00 00 00 00 // 填充, 使总长度到31设备收到后从闪存里读出512字节数据通过Bulk IN端点分8包发给主机每包64字节。因为512正好是64的整数倍设备必须在数据包全部发完后补一个零长度包主机才知道数据阶段结束了。这一点极容易漏如果不发零长度包主机会一直等待数据最终超时。之后设备发13字节CSW55 53 42 53 // USBS 04 03 02 01 // dCSWTag, 回显CBW中的tag 00 00 00 00 // residue 0 00 // status 0, 成功主机收到合法CSW后这次读操作才算正式完成。4.4 为什么U盘必须用Bulk端点而不是中断或同步这个问题是很多初学者的疑问。U盘的传输特征是突发大量块数据且数据绝对不容丢失。Bulk端点具备CRC校验和错误重传机制传输效率和可靠性平衡得最好。中断端点适合鼠标键盘这类低频、低延迟的小数据同步端点适合音频视频这类对时延敏感但允许偶发丢包的数据流。这些都不符合U盘的需求场景。BOT协议选Bulk相当于是把可靠吞吐放到了第一位。5. 可直接抄的U盘描述符模板与配置要点5.1 C语言描述符数组模板我把自己经常用的一组U盘描述符C数组贴出来这套结构在我的全速U盘固件里验证过很多次。数组顺序和USB规范要求的返回顺序完全一致配置描述符集合连在一起返回。/* 设备描述符, 18字节 */ static const uint8_t usbd_dev_desc[] { 0x12, /* bLength */ 0x01, /* bDescriptorType */ 0x00, 0x02, /* bcdUSB 0x0200 */ 0x00, /* bDeviceClass */ 0x00, /* bDeviceSubClass */ 0x00, /* bDeviceProtocol */ 0x40, /* bMaxPacketSize0 64 */ 0x34, 0x12, /* idVendor 0x1234 */ 0x56, 0x78, /* idProduct 0x5678 */ 0x00, 0x01, /* bcdDevice 0x0100 */ 0x01, /* iManufacturer */ 0x02, /* iProduct */ 0x03, /* iSerialNumber */ 0x01 /* bNumConfigurations */ }; /* 配置描述符集合, 32字节 */ static const uint8_t usbd_cfg_desc[] { /* 配置描述符, 9字节 */ 0x09, 0x02, 0x20, 0x00, 0x01, 0x01, 0x00, 0x80, 0x32, /* 接口描述符, 9字节 */ 0x09, 0x04, 0x00, 0x00, 0x02, 0x08, 0x06, 0x50, 0x00, /* 端点 OUT 1, Bulk, 64字节 */ 0x07, 0x05, 0x01, 0x02, 0x40, 0x00, 0x00, /* 端点 IN 2, Bulk, 64字节 */ 0x07, 0x05, 0x82, 0x02, 0x40, 0x00, 0x00 }; /* 语言ID描述符 */ static const uint8_t usbd_str_langid[] { 0x04, 0x03, 0x09, 0x04 }; /* 厂商字符串, MYCORP, 7字符 */ static const uint8_t usbd_str_vendor[] { 0x0E, 0x03, M,0x00, Y,0x00, C,0x00, O,0x00, R,0x00, P,0x00 }; /* 产品字符串, MY USB DISK, 11字符 */ static const uint8_t usbd_str_product[] { 0x18, 0x03, M,0x00, Y,0x00, ,0x00, U,0x00, S,0x00, B,0x00, ,0x00, D,0x00, I,0x00, S,0x00, K,0x00 }; /* 序列号字符串, 202600001234, 12字符 */ static const uint8_t usbd_str_serial[] { 0x1A, 0x03, 2,0x00,0,0x00,2,0x00,6,0x00,0,0x00,0,0x00, 0,0x00,0,0x00,1,0x00,2,0x00,3,0x00,4,0x00 };字符串描述符的bLength计算要特别细心。bLength22×N其中N是字符个数。厂商字符串MYCORP是6个字符bLength22×6140x0E产品字符串MY USB DISK是11个字符bLength240x18序列号12位数字bLength260x1A。写错一个十六进制值主机解析字符串时长度错乱轻则显示乱码重则跳过后续字符串索引。5.2 高速与全速的参数差异如果目标是做一个USB 2.0高速U盘描述符里需要改动的地方不少设备描述符的bMaxPacketSize0必须为0x4064字节bcdUSB可以保持0x0200两个Bulk端点的wMaxPacketSize要改成0x0200512字节控制器必须正确响应高速Chirp握手。这里要提醒一个关键点高速设备的EP0最大包长在枚举早期就是64字节而全速设备在总线复位后的默认EP0包长可以是8字节随后在设备描述符里报告真正的包长。如果全速设备报告64字节它的控制器FIFO实际上必须能吞吐64字节的控制数据包否则枚举阶段设备描述符都传不完。高速U盘的Bulk端点声明512字节后固件里的FIFO和DMA配置必须同步跟上。我见过一个项目把端点描述符改成512但USB控制器的端点FIFO只配了128字节结果所有超过128字节的读请求全卡死。硬件资源不支持之前先别把描述符值写得比能力大。5.3 端点编号与控制器映射约束端点号不是随便写的。USB规范只要求端点号从1到15实际端点地址里还有方向位但具体MCU的USB设备控制器往往有限制。有的芯片要求Bulk端点必须按偶数OUT、奇数IN配对使用如果你声明的OUT端点号是1、IN端点号是2固件初始化时就会报端点配置冲突。写描述符之前先翻芯片手册里的端点分配表搞清楚哪些端点号可用、能否任意组合方向。很多人第一版固件死在描述符逻辑完全正确但硬件配不出来上这运气成分其实完全可以避免。另外一个常见坑字符串描述符的iSerialNumber指向0x03意味着主机一定会来请求字符串索引3。如果你固件里只实现了索引1和2却把设备描述符里的索引3写上在Windows上枚举大概率会卡住表现为设备管理器报无法识别的USB设备或直接连续复位。字符串索引和实际实现的对应表必须一致。6. 六个常见枚举与传输故障的排查链路6.1 设备描述符请求阶段的STALL / 超时现象最好辨认设备插上后Windows提示无法识别的USB设备Linux的dmesg里出现device descriptor read/64, error -71之类的错误。排查顺序从物理层往上走先看D/D-上是否有1.5kΩ上拉没有上拉主机根本检测不到连接再看设备是否能正确收到SETUP包在固件里给端点0的SETUP中断打一个调试断点看8字节的请求数据是否完整最后看设备在收到GET_DESCRIPTOR之后有没有往端点0 IN方向写入18字节描述符。这个阶段如果设备用MCU模拟EP0还要检查EP0的最大包长是否与bMaxPacketSize0一致不一致会导致主机按64字节收设备却按8字节发数据阶段永远无法正确结束。6.2 SET_ADDRESS之后设备失联设备在SET_ADDRESS之前一切正常主机发了SET_ADDRESS之后总线上一片死寂。最可能的根因就是我前面说过的时序问题固件在状态阶段完成前就切换了新地址。USB规范说得很清楚设备必须在SET_ADDRESS请求的状态阶段完成后才启用新地址。如果你的协议栈是参考Linux gadget框架那种成熟实现这部分不用操心如果是自己写的裸机状态机请务必检查控制传输状态机的状态切换逻辑。还有一种可能设备把新地址写错了位置。地址是主机在SETUP包wValue的低7位里给的如果代码里把wValue整个字节当成地址一旦主机分配0x80以上的临时地址设备就会跑到另一个不存在的地址上主机自然找不到。6.3 枚举成功但读写卡死 / 报错如果枚举一切顺利系统也认出了大容量存储设备但打开盘符时卡死、读写超时、或者Windows提示磁盘结构损坏且无法读取问题往往出在BOT传输层而不是描述符层。先用分析仪或日志看主机发出的CBW设备有没有收到。设备收到CBW后如果命令是READ(10)或WRITE(10)必须严格按照CBW→数据→CSW的顺序响应。数据阶段存在一个我说过的陷阱传输字节数正好是wMaxPacketSize整数倍时必须在数据末尾发零长度包否则主机永远等不到数据阶段结束。CSW必须紧跟数据阶段且tag要与CBW一致。如果设备在执行命令期间又收到了新的CBW这说明设备的状态机可能进入了Phase Error分支需要按规范响应bCSWStatus0x02并等待主机的Mass Storage Reset。6.4 高速握手缺席导致的速度异常设备枚举成功U盘也能正常读写但速度稳定在全速水平。先确认描述符里写的确实是高速参数如果wMaxPacketSize还停留在64主机自然会按全速处理。排除描述符后就要怀疑高速握手过程复位后的Chirp序列需要设备在检测到主机Chirp K后发出不少于1ms的Chirp K/J序列响应。上拉电阻的精度、走线长度都会影响握手成功率。有些开发板为了方便调试会通过拨码开关或跳线强制设备进入全速模式。如果跳线没还原设备永远不会做高速握手。调试时先确认硬件没有锁在全速模式。6.5 字符串描述符长度错误导致的引导异常字符串描述符的错误比较隐蔽。bLength如果写短了比如MY USB DISK实际需要24字节你却写成0x10主机读完16字节后发现字符串数据不完整可能直接判定描述符非法。如果写长了主机会把后面无关数据当成字符串的一部分显示出来的产品名就是乱码。我自己调试序列号更新功能时还遇到过一种状况量产工具修改序列号字符串后字符串长度没有同步更新新序列号比旧序列号多一位字符bLength还是旧值结果系统读到的序列号被截断Windows不认这个U盘老是重新装驱动。修这种问题最好的办法是把字符串生成逻辑集中到一个函数里每次更新内容时重新计算bLength。6.6 描述符与实际FIFO不匹配的隐蔽传输故障这类问题最头疼因为枚举阶段不会失败系统也认设备但一旦数据传输量超过某个值U盘就死掉。根因通常是端点描述符声明的wMaxPacketSize大于硬件实际配置的FIFO容量。主机按512字节组织数据包设备FIFO只有64字节容量多出来的数据直接丢。排查链路建议先用lsusb -v或设备管理器看系统解析出来的描述符参数确认wMaxPacketSize和执行环境一致然后用总线分析仪连续读写大文件观察数据阶段在哪一包开始出现NAK或数据截断最后回到控制器寄存器核对FIFO配置和端点描述符是否匹配。百分之九十的偶发性传输卡死都能在这个链条里定位。最后再说一个我的个人调试习惯做新的U盘固件时不要急着把完整固件一次写完。先用一个最简单的Bulk回环端点跑通枚举和收发的完整链路确认描述符和端点FIFO完全匹配再往上叠SCSI命令解释器和闪存读写。描述符这类东西一旦跑通后面很少需要改动真正让你头疼的永远是传输层和SCSI层。把调试范围缩小了问题才好定位。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑