☰
STM32F103实现USB复合设备:自定义HID+MSC双接口详解
2026/10/19 12:32:31 网站建设 项目流程

简介:这是基于STM32F103C8T6的USB复合设备工程源码,实现自定义HID设备与MSC大容量存储功能在同一设备上协同工作。设计上将键盘、鼠标分别绑定到独立接口,避免部分主机在启动枚举阶段不请求报告描述符导致键鼠无法识别的问题;四个端点分工明确,EP0负责枚举,EP1、EP2分别承载键盘与鼠标,EP3支持MSC存储,并配有官方演示版与FAT16文件系统版本,默认采用FAT16。鼠标同时支持绝对定位与相对定位两种模式,便于不同场景使用。整个压缩包共1420个文件,大小22.53MB,主体为854个C源码文件与274个头文件,另有工程配置、链接脚本、库文件等,结构清晰,方便直接导入编译或按需修改。这一复合方案不只限于当前芯片,也可迁移到F1、F4、F0等系列,组合HID与MSC、CDC与MSC、HID与CDC,甚至三重复合设备。目前已有839人学习,尤其适合USB协议栈学习者与嵌入式开发工程师参考。

1. CUSTOMHID+MSC复合设备:一块F103凭什么同时当U盘和自定义HID

USB_CUSTOMHID+MSC这个工程,解决的是一个很实在的问题:一块STM32F103C8T6只有一个USB口,却想让上位机既能像U盘一样直接读写存储介质,又能用自定义HID协议做低延迟的双向数据通信。我拆这个工程时先确认了结论:F103的标准USB设备库完全支持这种复合设备,但描述符和接口分发逻辑必须自己动手改,不能拿官方HID例程和MSC例程简单拼凑。这个工程适合两类人:一类是做采集设备、加密狗、配置工具这类需要“存储+通信”双功能的嵌入式开发;另一类是刚接触USB协议栈、想搞清楚Windows到底怎么识别多接口设备的初学者。

2. 整体架构:一个设备描述符如何装下两个接口

2.1 复合设备的定义:为什么bDeviceClass必须写成0xEF

先厘清概念:USB复合设备(Composite Device)和USB组合设备(Compound Device)是两回事。复合设备是同一个芯片、同一个USB地址下挂多个接口,每个接口独立成为一个功能;组合设备则是多个USB控制器通过内部集线器组合成一个物理设备。这个工程属于前者,F103只有一个USB设备控制器,但枚举出来有两个功能接口,分别是HID和MSC。

Windows对复合设备的识别有个硬性条件:设备描述符里的bDeviceClass必须写成0xEF,bDeviceSubClass写成0x02,bDeviceProtocol写成0x01。看到这三个值,Windows会调用usbccgp.sys(USB Generic Parent Driver)接管设备,再根据IAD(Interface Association Descriptor,接口关联描述符)把每个接口拆出来绑定到各自的功能驱动。如果你仍然写0x00,Windows会把整个设备当单一功能设备处理,两个接口抢同一个驱动,结果通常是HID正常、MSC挂不上,或者反过来。

/* usb_desc.c —— 设备描述符 */ const uint8_t DeviceDescriptor[] = { 0x12, /* bLength */ 0x01, /* bDescriptorType */ 0x00, 0x02, /* bcdUSB = 2.00 */ 0xEF, /* bDeviceClass */ 0x02, /* bDeviceSubClass */ 0x01, /* bDeviceProtocol */ 0x40, /* bMaxPacketSize0 */ 0xXX, 0xXX, /* idVendor */ 0xXX, 0xXX, /* idProduct */ 0x00, 0x01, /* bcdDevice */ 0x01, /* iManufacturer */ 0x02, /* iProduct */ 0x03, /* iSerialNumber */ 0x02 /* bNumConfigurations */ };

这段描述符里,bMaxPacketSize0 = 0x40即64字节,是全速USB设备控制端点的最大包长。bNumConfigurations = 0x02这里其实只用一个配置,写1就行,写2也没有第二个配置描述符供主机请求,反而会多一次无谓的枚举请求。idVendor和idProduct在开发阶段可以用任意值,但量产时必须换成自己申请的VID/PID,不然和别的设备撞了会导致驱动冲突。

2.2 工程目录:标准USB库中真正需要改的四个文件

STM32F103C8T6用的是内置USB设备控制器,配套的USB Device库(就是标准外设库里那个STM32_USB-FS-Device_Driver)提供了完整的协议栈。工程目录大致长这样:

USB_CUSTOMHID_MSC/ ├── User/ │ ├── main.c // 主循环 + 外设初始化 │ ├── stm32f10x_it.c // 中断向量表 │ └── usb_conf.h // USB配置:端点地址、缓冲区分配 ├── USB/ │ ├── usb_desc.c // 设备/配置/字符串/报告描述符 │ ├── usb_desc.h │ ├── usb_prop.c // 标准请求和类请求的分发 │ ├── usb_prop.h │ ├── usb_endp.c // 非控制端点的数据收发 │ ├── usb_istr.c // USB总中断入口 │ ├── usb_pwr.c // 挂起/唤醒电源管理 │ ├── usb_scsi.c // SCSI命令实现 │ ├── usb_bot.c // BOT传输状态机 │ └── usb_mem.c // 数据拷贝到Packet Buffer ├── Libraries/ │ ├── CMSIS/ │ ├── STM32F10x_StdPeriph_Driver/ │ └── STM32_USB-FS-Device_Driver/ └── MDK-ARM/ └── USB_CUSTOMHID_MSC.uvproj

别看它文件多,真正需要手改的只有四个:usb_desc.c管描述符,usb_prop.c管请求分发,usb_endp.c管端点收发,usb_conf.h管端点和缓冲区规划。usb_bot.c和usb_scsi.c是MSC部分的业务逻辑,如果要换存储介质、调命令集,改这里。其余文件大部分是标准库原封不动的实现,不用碰。

这个工程的核心思路是在官方USB库的HID和MSC两个例程基础上做合并。合并的难点不在代码,而在状态机的耦合:控制端点的标准请求处理被usb_prop.c统一接管,类请求又要按接口号分别派发给HID和MSC的处理例程。pInformation->USBwIndex这个字段是关键,它保存了SETUP包里的接口号。

2.3 描述符布局:从配置描述符到端点的完整拓扑

配置描述符是USB枚举时最长、最容易写错的数据块。复合设备的配置描述符必须严格按这个顺序排列:配置描述符 → IAD → HID接口描述符 → HID描述符 → 端点描述符 → 端点描述符 → IAD → MSC接口描述符 → 端点描述符 → 端点描述符。顺序反了或者漏了IAD,Windows枚举直接报错。

/* usb_desc.c —— 配置描述符 */ const uint8_t CustomHID_MSC_ConfigDescriptor[] = { /* 1. Configuration Descriptor */ 0x09, 0x02, 0x50, 0x00, 0x02, 0x01, 0x00, 0xA0, 0x32, /* 2. IAD 1: 关联HID接口 */ 0x08, 0x0B, 0x00, 0x01, 0x03, 0x00, 0x00, 0x00, /* 3. Interface 0: HID */ 0x09, 0x04, 0x00, 0x00, 0x02, 0x03, 0x00, 0x00, 0x00, /* 4. HID Descriptor */ 0x09, 0x21, 0x10, 0x01, 0x00, 0x01, 0x22, 0x22, 0x00, /* 5. Endpoint: 0x81 IN Interrupt */ 0x07, 0x05, 0x81, 0x03, 0x40, 0x00, 0x10, /* 6. Endpoint: 0x01 OUT Interrupt */ 0x07, 0x05, 0x01, 0x03, 0x40, 0x00, 0x10, /* 7. IAD 2: 关联MSC接口 */ 0x08, 0x0B, 0x01, 0x01, 0x08, 0x06, 0x50, 0x00, /* 8. Interface 1: Mass Storage */ 0x09, 0x04, 0x01, 0x00, 0x02, 0x08, 0x06, 0x50, 0x00, /* 9. Endpoint: 0x82 IN Bulk */ 0x07, 0x05, 0x82, 0x02, 0x40, 0x00, 0x00, /* 10. Endpoint: 0x02 OUT Bulk */ 0x07, 0x05, 0x02, 0x02, 0x40, 0x00, 0x00, };

逐字节拆解一下:配置描述符开头0x50 0x00是总长度,低字节在前,等于80字节。数一数后面每一项的长度:9 + 8 + 9 + 9 + 7 + 7 + 8 + 9 + 7 + 7 = 80,正好对上。0x02是接口数量,HID和MSC各占一个。0xA0表示总线供电且支持远程唤醒,0x32是最大电流100mA(单位是2mA倍数)。

IAD的作用是把连续的接口“打包”成一个功能。第一个IAD的bFirstInterface = 0x00、bInterfaceCount = 0x01,HID接口号是0;第二个IAD的bFirstInterface = 0x01,对应MSC接口号1。bFunctionClass、bFunctionSubClass、bFunctionProtocol在IAD里的值必须和接口描述符一致,0x08, 0x06, 0x50分别代表Mass Storage类、SCSI指令集、Bulk-Only传输协议。

3. 核心实现:USB中断、HID报告和SCSI指令的配合

3.1 端点分配:8个端点如何规划给HID和MSC

F103的USB控制器有8个双向端点,每个端点有独立的IN和OUT方向。这个工程占用的是EP0、EP1、EP2三个端点号,规划如下:

端点方向用途传输类型最大包长
EP0双向控制传输/枚举Control64B
EP1 ININHID数据上行到PCInterrupt64B
EP1 OUTOUTHID数据下行到设备Interrupt64B
EP2 ININMSC批量数据上行Bulk64B
EP2 OUTOUTMSC批量数据下行Bulk64B

为什么HID用EP1、MSC用EP2?其实没有硬件强制,只要描述符里写的端点地址和实际初始化一致就行。但端点地址必须唯一,两个接口不能都用0x81端点。HID适合用中断端点,优点是延迟固定,适合小包实时通信;MSC用批量端点,吞吐高但延迟不可控,适合大数据块传输。

usb_conf.h里还要给每个端点分配Packet Buffer地址,这是F103 USB特别容易踩坑的地方:

/* usb_conf.h —— USB内嵌缓冲区分区 */ #define ENDP0_RX_ADDR 0x18 #define ENDP0_TX_ADDR 0x58 #define ENDP1_RX_ADDR 0x98 /* HID OUT 缓冲 */ #define ENDP1_TX_ADDR 0xD8 /* HID IN 缓冲 */ #define ENDP2_RX_ADDR 0x118 /* MSC OUT 缓冲 */ #define ENDP2_TX_ADDR 0x158 /* MSC IN 缓冲 */

地址之间间隔0x40,也就是64字节,正好是一个端点的最大包长。USB缓冲区是专用SRAM,按32位对齐处理,地址写重叠了不会报编译错误,但运行到端点收发时数据会互相覆盖,表现出来的症状非常玄学:HID收一个包,MSC的返回数据就被改了。

3.2 自定义HID:报告描述符与64字节双向传输

自定义HID和标准键盘鼠标的区别在报告描述符。键盘鼠标用的是系统分配好的Usage Page(0x01),自定义HID用厂商自定义的Page(0xFF00),这样系统不会抢占输入焦点,也不会误触发快捷键。

/* usb_desc.c —— 自定义HID报告描述符 */ const uint8_t CustomHID_ReportDescriptor[] = { 0x06, 0x00, 0xFF, // Usage Page (Vendor Defined 0xFF00) 0x09, 0x01, // Usage (0x01) 0xA1, 0x01, // Collection (Application) 0x09, 0x02, // Usage (0x02) 0x15, 0x00, // Logical Minimum (0) 0x26, 0xFF, 0x00, // Logical Maximum (255) 0x75, 0x08, // Report Size (8 bits) 0x95, 0x40, // Report Count (64) 0x81, 0x02, // Input (Data, Var, Abs) 0x09, 0x03, // Usage (0x03) 0x15, 0x00, // Logical Minimum (0) 0x26, 0xFF, 0x00, // Logical Maximum (255) 0x75, 0x08, // Report Size (8 bits) 0x95, 0x40, // Report Count (64) 0x91, 0x02, // Output (Data, Var, Abs) 0xC0 // End Collection };

这个报告描述符定义了输入和输出两种报告,各64字节。Input对应设备发给PC的数据,走EP1 IN;Output对应PC发给设备的数据,走EP1 OUT。Logical Maximum设成255意味着每个字节的取值范围是0~255,不做缩放。如果你想加报告ID,在Usage Page之后插一个0x85, 0x01,但注意报告里第一个字节就要变成报告ID,有效数据只剩63字节。

HID的发送函数是这个工程里最短也是最容易用错的代码:

/* usb_endp.c —— HID发送函数 */ void CustomHID_SendPacket(uint8_t *buf, uint16_t len) { if (len > 64) len = 64; UserToPMABufferCopy(buf, ENDP1_TX_ADDR, len); SetEPTxCount(ENDP1, len); SetEPTxStatus(ENDP1, EP_TX_VALID); }

UserToPMABufferCopy把用户数据拷贝到USB硬件缓冲区,SetEPTxCount设置本次传输长度,SetEPTxStatus把端点状态改成VALID,USB控制器自动把数据发出去。调用前一定要先查状态:如果上一次数据还没发完,再次调用CustomHID_SendPacket会覆盖缓冲,导致上一包丢失。标准做法是用GetEPTxStatus(ENDP1)判断当前是不是EP_TX_NAK,是NAK状态才能写入新数据。

3.3 MSC的BOT协议:从CBW到SCSI指令再到CSW

MSC部分是整个工程逻辑最重的模块。BOT(Bulk-Only Transport)协议是串行化的三幕剧:PC先发一个31字节的CBW(Command Block Wrapper)命令块,设备解析里头的SCSI指令,执行完再回一个13字节的CSW(Command Status Wrapper)状态块。数据结构长这样:

/* usb_bot.c —— CBW与CSW结构体 */ typedef struct { uint32_t dCBWSignature; /* 固定0x43425355,即'USBC' */ uint32_t dCBWTag; /* 命令标签,回CSW时必须原样返回 */ uint32_t dCBWDataTransferLength; /* 期望传输的数据字节数 */ uint8_t bmCBWFlags; /* 0x00=主机到设备,0x80=设备到主机 */ uint8_t bCBWLUN; uint8_t bCBWCBLength; /* CBWCB数组有效长度 */ uint8_t CBWCB[16]; /* SCSI命令块 */ } CBW_t; typedef struct { uint32_t dCSWSignature; /* 固定0x53425355,即'USBS' */ uint32_t dCSWTag; uint32_t dCSWDataResidue; /* 未传输的字节数 */ uint8_t bCSWStatus; /* 0=成功,1=失败,2=相位错误 */ } CSW_t;

dCSWTag必须回显CBW里的值,这是PC用来匹配请求的关键。dCSWDataResidue表示没传完的数据长度,如果主机要读2个扇区,设备只回了1个,residue就是512。bCSWStatus是三个值的决定点:正常为0,命令执行失败为1,数据阶段传输方向和CBW里声明的不一致则为2。

SCSI指令解析放在usb_scsi.c里,一个U盘能被Windows挂载,只需要实现下面这些命令:

SCSI指令操作码作用
TEST_UNIT_READY0x00查询设备是否就绪
INQUIRY0x12返回厂商、产品名、版本号
MODE_SENSE_60x1A返回介质写保护状态等参数
READ_CAPACITY0x25返回总扇区数和扇区大小
READ_100x28读取指定LBA开始的若干扇区
WRITE_100x2A写入指定LBA开始的若干扇区
START_STOP_UNIT0x1B启动/停止介质

核心是READ_10和WRITE_10,这两个命令用LBA(逻辑块地址)和TransferLength字段描述读写范围,每个块固定512字节:

/* usb_scsi.c —— READ_10处理 */ static int32_t SCSI_Read10(uint8_t *buf, uint32_t lba, uint16_t transfer_len) { const uint32_t sector_size = 512; if (lba + transfer_len > TOTAL_SECTORS) { return -1; /* 越界直接失败 */ } for (uint16_t i = 0; i < transfer_len; i++) { SPI_FLASH_ReadBuffer(buf + i * sector_size, (lba + i) * sector_size, sector_size); } return 0; }

LBA到Flash地址的换算很简单,lba * 512就是字节偏移。但要注意transfer_len不能盲目信任,PC端可能会一次请求256个扇区(128KB),而缓冲区只有64或128字节,这时候要做拆包,按端点缓冲上限分多次发送。越界检查也不能省,否则恶意构造的CBW可能让单片机访问非法地址,直接进HardFault。

3.4 存储介质映射:SPI Flash与内部Flash的取舍

U盘数据最终要落到存储介质上。工程里通过USE_SPI_FLASH宏切换两种介质,抽象层接口保持一致:

/* 存储介质抽象层 */ #ifdef USE_SPI_FLASH /* 外挂W25Q64 SPI NOR Flash,8MB */ #define MEDIA_Init SPI_FLASH_Init #define MEDIA_ReadSector SPI_FLASH_ReadBuffer #define MEDIA_WriteSector SPI_FLASH_WriteBuffer #define MEDIA_EraseBlock SPI_FLASH_EraseSector #define TOTAL_SECTORS 16384 /* 8MB / 512B */ #else /* 使用内部Flash最后16KB */ #define MEDIA_Init IntFlash_Init #define MEDIA_ReadSector IntFlash_ReadSector #define MEDIA_WriteSector IntFlash_WriteSector #define MEDIA_EraseBlock IntFlash_ErasePage #define TOTAL_SECTORS 32 /* 16KB / 512B */ #endif

SPI NOR Flash的写入有块擦除限制,W25Q系列按4KB扇区擦除。写一个512字节扇区时,内部流程是:先把整个4KB扇区读进RAM,改掉其中512字节,再整块擦除写回。这让WRITE_10的速度明显比读取慢,连续写1MB数据时尤其明显,这不是代码bug,是NOR Flash的固有属性。如果你要做日志型写入频繁的场景,内部Flash方案反而不适合频繁擦写,寿命撑不住;SPI Flash的擦写次数普遍在10万次以上,更耐造。

4. 编译烧录与运行验证:从点亮板子到电脑认出HID+U盘

4.1 工程配置:芯片选择、时钟与编译选项

工程用Keil MDK打开后,第一件事是确认芯片型号是STM32F103C8T6,对应STM32F10X_MD(中等容量)宏。C8T6是64KB Flash、20KB RAM,对于USB复合设备这种代码量,Flash稍紧张,编译时开-O2优化可以把代码缩到50KB以内。

时钟配置是整个USB能否工作的前提。F103的USB控制器需要48MHz时钟,这个48MHz来自PLL。用8MHz外部晶振的话,PLL倍频9倍到72MHz,再1.5分频得到48MHz。如果工程里用的是内部RC振荡器,USB会因为时钟精度不够而枚举不稳定,强烈建议用外部8MHz晶振。

/* system_stm32f10x.c 或 main.c 中的时钟配置 */ RCC_PLLConfig(RCC_PLLSource_PLLCLK, 9, 1); /* PLL=72MHz */ RCC_USBCLKConfig(RCC_USBCLKSource_PLLCLK_1Div5); /* 72/1.5=48MHz */

这两行配置缺一不可。RCC_PLLConfig的第一个参数选择PLL时钟源,第二个是倍频系数9;RCC_USBCLKConfig选择1.5分频。如果USB时钟不是精确的48MHz,设备枚举会时好时坏,表现为有时插上识别正常、有时报设备描述符请求失败,治这种问题先别怀疑代码,用示波器量一下USB_D+上是否稳定。

4.2 烧录与枚举:第一次插上时Windows的提示

用ST-Link V2连接SWD接口,Keil里配置好下载算法后就能烧录。烧录完拔掉ST-Link,用USB线连接F103板子上的USB口到电脑。第一次插入时,Windows右下角会弹出“正在安装设备驱动程序”的提示,然后设备管理器里会出现两个条目:一个“USB输入设备”(HID部分),一个“磁盘驱动器”(MSC部分)。注意观察磁盘驱动器是否显示正确容量——SPI Flash方案显示8MB,内部Flash方案显示16KB。

如果设备管理器里出现的是“未知USB设备(设备描述符请求失败)”,说明枚举没走通。先用USB树状查看器(比如USBTreeView)抓取设备返回的原始描述符,重点看配置描述符的长度字段是否和实际返回一致。这一步比看任何调试日志都高效,因为USB协议栈在枚举失败时往往根本跑不到应用代码。

4.3 联调测试:验证HID和MSC都工作正常

HID通信验证可以用Python的pywinusb库,写一个几十行的脚本就能收发数据:

import pywinusb.hid as hid def read_handler(data): print("收到设备数据:", bytes(data)) # VID/PID 需要和 usb_desc.c 里一致 filter = hid.HidDeviceFilter(vendor_id=0x1234, product_id=0x5678) device = filter.get_devices()[0] device.open() device.set_raw_data_handler(read_handler) device.send_output_report(bytes(range(64))) # 发送 64 字节 Output input("按回车退出...") device.close()

这段脚本会枚举VID=0x1234、PID=0x5678的HID设备,发送一个0x00到0x3F的递增序列。如果设备端把数据原样回显到Input报告,终端里就能看到同样的序列,说明HID双向通路是通的。

MSC验证更直观:在“此电脑”里看到新磁盘后,右键格式化选FAT32,然后拷贝一个文件进去,拔掉USB再重新插上,文件还在且能正常打开。更严谨一点,拷入文件后用校验工具算一下MD5,拔插后再算一次,两个值一致说明写入没有静默错误。格式化本身就是在做全盘读写测试,如果格式化能顺利完成,BOT协议栈和存储介质基本是健康的。

5. 避坑与排查:复合设备最常踩的五个坑

5.1 枚举阶段的三类翻车现场

现象一:设备管理器报“设备描述符请求失败”,设备始终在未知设备状态。

原因:九成是描述符数据错误,排名第一的是配置描述符总长度字段和对不齐。比如在2.3节末尾那80字节的描述符,如果0x50 0x00写成了0x3F 0x00,主机请求描述符时收到的长度与设备实际返回的长度不一致,Windows直接放弃枚举。

解决:用USB树状查看器抓取设备返回的原始描述符,拿原始字节和源码里的数组逐字节比对。没有抓包工具就用Bus Hound,枚举阶段的GET_DESCRIPTOR请求记录都能看到。

现象二:HID能识别,但磁盘驱动器出不来,设备管理器里多一个带感叹号的“大容量存储设备”。

原因:IAD的bFirstInterface和接口描述符的bInterfaceNumber对不上。比如IAD里写了bFirstInterface = 0x00,但MSC接口号实际是1,Windows按IAD去找接口0对应的接口描述符,找错对象后驱动加载失败。

解决:把IAD的bFirstInterface改成和MSC接口描述符的bInterfaceNumber一致,这是复合设备最容易忽略的关联关系。

现象三:插上USB后电脑提示“无法识别的USB设备”,但用示波器看D+引脚波形正常。

原因:USB时钟频率不是精确的48MHz。F103的USB控制器对时钟精度要求较高,如果系统用的是内部RC而不是外部晶振,即使PLL配置看起来正确,实际频率偏差也可能超过USB规范允许的范围。

解决:用外部8MHz晶振,确认RCC_USBCLKConfig配置为PLL 1.5分频。改了晶振后要同时检查SystemInit里的RCC_PLLConfig倍频系数,8MHz乘以9等于72MHz,这个是标准值。

5.2 读写阶段的两个隐蔽问题

现象四:U盘能格式化,但拷贝大文件时中途报错,小文件没问题。

原因:BOT状态机对数据阶段长度的处理有缺陷。READ_10或WRITE_10如果返回的数据长度和CBW里声明的dCBWDataTransferLength不一致,CSW的dCSWDataResidue就要正确计算,写错的话Windows会认为数据传输“意外结束”。

解决:检查每个SCSI命令执行完后的CSW回包,确认dCSWDataResidue = dCBWDataTransferLength - 实际传输字节数,如果实际传输的比声明少,residue必须非零,否则Windows判定为协议错误。

现象五:HID发送函数调用后,上位机偶尔收不到数据,但重新上电后又好了。

原因:CustomHID_SendPacket在上一次数据还没发完时被再次调用,数据覆盖。这个问题在中断里跑发送逻辑时特别常见:主循环在不断调用发送函数,而USB中断还没完成上一次EP1的传输。

解决:发送前检查端点状态,GetEPTxStatus(ENDP1)返回EP_TX_NAK时才允许写入新数据。或者用一个发送忙标志位,在IN端点发送完成中断里清掉,这样既不会丢失数据也不会阻塞主循环。

6. 二次开发:HID协议定制与MSC介质替换

改HID协议,先改报告描述符。比如你要加一个报告ID,在Usage Page之后插入0x85, 0x01,PC端发送的Output报告第一个字节就会变成报告ID,有效数据从第二个字节开始。这个工程里设备端收到数据后会把pBuffer[0]当作普通数据填充,要改成按报告ID分发到不同业务逻辑。想加Feature报告的话,在报告描述符里多定义一个字段,用0xB1(Feature)替换0x81,然后配置一个方向对应的端点。

换MSC存储介质,最省事的做法是保持MEDIA_Init/ReadSector/WriteSector/EraseBlock四个函数签名不变,内部换成新介质的驱动。如果换成SD卡,注意SD卡需要先SPI模式初始化,4GB容量对应TOTAL_SECTORS = 8388608,要把READ_CAPACITY返回值同步改掉,否则Windows识别成错误容量的磁盘。加双LUN也不难,让GET_MAX_LUN返回1,然后bCBWLUN字段区分LUN 0和LUN 1,可以做到一个LUN当只读配置盘、另一个LUN当可写数据盘。

我自己改这类复合设备项目,有一套固定的验证流程:每次改完描述符,先用USB树状查看器确认枚举字节流,再跑一遍HID回环测试,最后对MSC分区格式化并做一次完整文件写入校验。三步都通过才敢说这个改动是稳的。之前吃过一次亏,只改了报告长度没改描述符里的Report Count,结果Windows枚举HID接口时报描述符校验错误,整个设备都识别不了,又得抓包看原始描述符才定位到问题。从那以后我每次改USB描述符都强制走一遍这三板斧。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询