☰
STM32F103C8T6实现USB复合HID:单Endpoint同时支持键盘+鼠标
2026/10/20 3:02:34 网站建设 项目流程

简介:本资源是一份基于STM32F103C8T6的USB复合HID设备完整工程实现,面向嵌入式初学者与STM32进阶开发者,解决USB设备枚举、键盘鼠标双功能协同、HID报告描述符定制及HAL底层驱动适配等典型难点。项目使用STM32CubeMX图形化配置生成初始化框架,涵盖时钟树设置、USB OTG FS设备模式启用、双HID接口中断端点分配等关键步骤,并提供可直接编译下载的Keil MDK工程。压缩包含626个文件(7.58MB),主体为345个C源文件(含USB协议栈、HID报告处理、中断服务逻辑)、114个头文件(定义设备描述符、报告结构体、宏配置)及43个汇编启动文件,辅以ICF链接脚本、AXF可执行镜像和IOC工程配置文件,目录结构规范,便于理解USB设备固件分层设计。目前已有3418人学习下载,适合动手实践USB复合设备开发、掌握HID类协议实现细节与STM32CubeMX+HAL协同开发流程的工程师。

1. STM32F103C8T6用CubeMX配出USB复合设备:一个端点同时挂键盘+鼠标,不是“插两个U盘”,而是系统认作“一个带双功能的HID设备”

你有没有试过——在STM32F103C8T6这种资源紧巴巴的“蓝 pill”小板子上,想让电脑既把它当键盘(比如发Ctrl+Alt+Del),又当鼠标(比如自动移动+左键点击),但发现CubeMX里USB Device配置界面只允许选“HID Keyboard”或“HID Mouse”之一?选了键盘就没了鼠标,选了鼠标就敲不了字。这不是CubeMX的缺陷,而是你没理解HID复合设备(Composite Device)的本质:它不是靠物理多接口,而是靠单个USB Device描述符里嵌套多个Interface描述符,每个Interface声明自己是Keyboard或Mouse,共享同一套Endpoint(通常是EP1 IN)。Windows/Linux/macOS原生支持这种结构,只要Descriptor写对、报告描述符(Report Descriptor)不冲突、状态同步不打架。本项目就是一份可复现的、基于CubeMX + HAL + 手动补丁的完整落地方案,专治“只能二选一”的玄学困境。适合正在做自动化测试工装、嵌入式演示遥控器、或需要免驱人机交互的某跨平台系统开发者——不需要额外芯片、不改硬件、不换MCU型号,纯软件层撬开复合HID的大门。


2. 复合HID的底层逻辑:为什么必须手撕Descriptor,CubeMX生成器天生不支持Interface拆分

2.1 USB HID复合设备的协议骨架:Device → Configuration → Interface ×2 → Endpoint共用

USB规范中,一个Device可以有多个Configuration(配置),每个Configuration下可定义多个Interface(接口),而每个Interface可独立声明其Class(0x03)、SubClass(0x01)、Protocol(0x01键盘 / 0x02鼠标)。关键点在于:Interface之间不强制要求独占Endpoint。HID类设备约定使用中断传输(Interrupt IN),典型分配是EP1 IN用于上行报告。复合设备正是利用这一点:Configuration Descriptor里先描述Interface 0(Keyboard),再紧接着描述Interface 1(Mouse),两者都指向同一个bEndpointAddress = 0x81(IN方向,Endpoint 1)。操作系统枚举时,会为每个Interface创建独立的HID类设备实例——Windows设备管理器里显示为“HID键盘”和“HID鼠标”两个条目,但它们共用同一块STM32的USB外设寄存器和同一根D+/D-线。CubeMX的GUI限制在于:它把整个USB Device当作“单功能黑匣子”处理,生成的usbd_desc.c只预留一个USBD_InterfaceDesc数组项,且USBD_HID_Desc硬编码为单Interface结构。要突破,必须绕过GUI自动生成,手动注入第二Interface的Descriptor段。

2.2 CubeMX默认生成的HID Descriptor缺陷分析:缺少Interface 1定义与Report ID隔离

我们用CubeMX勾选“USB Device → HID Keyboard”后,生成的usbd_desc.c中核心片段如下(已简化):

/* Configuration Descriptor */ __ALIGN_BEGIN static uint8_t USBD_FS_CfgDesc[USB_FS_CONFIG_DESC_SIZ] __ALIGN_END = { 0x09, /* bLength: Configuation Descriptor size */ USB_DESC_TYPE_CONFIGURATION, /* bDescriptorType: Configuration */ USB_FS_CONFIG_DESC_SIZ, /* wTotalLength: Bytes returned */ 0x00, 0x01, /* bNumInterfaces: 1 interface */ 0x01, /* bConfigurationValue: Configuration value */ 0x00, /* iConfiguration: Index of string descriptor */ 0xC0, /* bmAttributes: Bus Powered */ 0x32, /* MaxPower 100 mA */ /* Interface Descriptor */ 0x09, /* bLength: Interface Descriptor size */ USB_DESC_TYPE_INTERFACE, /* bDescriptorType: Interface */ 0x00, /* bInterfaceNumber: Number of Interface */ 0x00, /* bAlternateSetting: Alternate setting */ 0x01, /* bNumEndpoints: One endpoint used */ 0x03, /* bInterfaceClass: HID */ 0x01, /* bInterfaceSubClass: 1=BOOT, 0=no boot */ 0x01, /* bInterfaceProtocol: 1=Keyboard */ 0x00, /* iInterface: Index of string descriptor */ /* HID Class Descriptor */ 0x09, /* bLength: HID Class Descriptor size */ USB_DESC_TYPE_HID, /* bDescriptorType: HID */ 0x11, /* bcdHID: HID Class Spec release number */ 0x01, 0x00, /* bCountryCode: Hardware target country */ 0x01, /* bNumDescriptors: Number of HID class descriptors to follow */ 0x22, /* bDescriptorType */ LOBYTE(KEYBOARD_REPORT_DESC_SIZE), /* wItemLength: Total length of Report descriptor */ HIBYTE(KEYBOARD_REPORT_DESC_SIZE), ... };

问题直击:

  • bNumInterfaces= 0x01 → 必须改为0x02;
  • Interface Descriptor只有一段(bInterfaceNumber=0x00),缺bInterfaceNumber=0x01的鼠标段;
  • HID Class Descriptor只出现一次,但每个Interface都需要自己的HID Class Descriptor(含各自Report Descriptor长度);
  • 更致命的是:两个Interface若共用同一Endpoint,其Report Descriptor必须用Report ID字段区分数据来源,否则主机无法分辨“这8字节是键盘按还是鼠标移”。CubeMX生成的键盘Report Descriptor默认无Report ID,鼠标同理——必须手动插入0x85, 0x01(键盘ID)和0x85, 0x02(鼠标ID)前缀。

提示:USB协议规定,当Configuration含多个HID Interface且共用Endpoint时,所有Report Descriptor必须启用Report ID,否则Windows可能只识别第一个Interface,第二个被静默丢弃。这是血泪经验踩出的第一道坑。

2.3 手动构建复合Descriptor:四步补丁法(改Config头、加Interface、加HID Class、改Report Desc)

我们不重写整个Descriptor,而是在CubeMX生成框架上做最小侵入式补丁。步骤如下:

步骤1:扩增Configuration Descriptor总长与接口数

定位USBD_FS_CfgDesc数组开头,修改两处:

  • wTotalLength:原值为USB_FS_CONFIG_DESC_SIZ,需重新计算。原键盘Descriptor约57字节,新增鼠标Interface需+25字节(Interface+HID Class+Report ID前缀),总长≈82字节 → 改为0x52, 0x00(小端序);
  • bNumInterfaces:从0x01改为0x02。
步骤2:在键盘Interface后追加鼠标Interface Descriptor

在键盘Interface Descriptor(9字节)和其HID Class Descriptor(9字节)之后,插入以下25字节(严格按USB规范顺序):

/* Mouse Interface Descriptor (Interface 1) */ 0x09, /* bLength */ USB_DESC_TYPE_INTERFACE, /* bDescriptorType */ 0x01, /* bInterfaceNumber: MUST be 0x01 for second interface */ 0x00, /* bAlternateSetting */ 0x01, /* bNumEndpoints: still one EP */ 0x03, /* bInterfaceClass: HID */ 0x01, /* bInterfaceSubClass: BOOT */ 0x02, /* bInterfaceProtocol: 0x02 = Mouse */ 0x00, /* iInterface */ /* HID Class Descriptor for Mouse Interface */ 0x09, /* bLength */ USB_DESC_TYPE_HID, /* bDescriptorType */ 0x11, /* bcdHID */ 0x01, 0x00, /* bCountryCode */ 0x01, /* bNumDescriptors */ 0x22, /* bDescriptorType: Report */ LOBYTE(MOUSE_REPORT_DESC_SIZE + 2), /* wItemLength: Report Desc size + 2 for Report ID prefix */ HIBYTE(MOUSE_REPORT_DESC_SIZE + 2),

注意:bInterfaceNumber必须为0x01,且bInterfaceProtocol必须为0x02,这是操作系统识别鼠标的唯一依据。wItemLength要+2,因为我们要在原始鼠标Report Descriptor前加0x85, 0x02。

步骤3:修改Report Descriptor,强制注入Report ID

找到KEYBOARD_ReportDesc[]数组,在其开头插入键盘Report ID声明:

__ALIGN_BEGIN static uint8_t KEYBOARD_ReportDesc[KEYBOARD_REPORT_DESC_SIZE + 2] __ALIGN_END = { 0x85, 0x01, /* REPORT_ID (1) for Keyboard */ 0x05, 0x01, /* USAGE_PAGE (Generic Desktop) */ 0x09, 0x06, /* USAGE (Keyboard) */ // ... 原始键盘Report Descriptor剩余内容 };

同理,修改MOUSE_ReportDesc[]:

__ALIGN_BEGIN static uint8_t MOUSE_ReportDesc[MOUSE_REPORT_DESC_SIZE + 2] __ALIGN_END = { 0x85, 0x02, /* REPORT_ID (2) for Mouse */ 0x05, 0x01, /* USAGE_PAGE (Generic Desktop) */ 0x09, 0x02, /* USAGE (Mouse) */ // ... 原始鼠标Report Descriptor剩余内容 };

关键参数说明:0x85是HID短Item标签"REPORT_ID",0x01/0x02是自定义ID值。主机收到数据时,会检查首字节判断来源——键盘数据以0x01开头,鼠标以0x02开头,HAL库发送时必须严格对齐。

步骤4:同步更新USBD_HID_ItfTypeDef中的发送函数

CubeMX生成的usbd_hid.c中,USBD_HID_SendReport()函数默认只处理单Report Buffer。我们需要扩展它,根据Report ID选择对应Buffer:

static uint8_t aBuf[8]; // 临时缓冲区,最大8字节(键盘6+鼠标3) uint8_t USBD_HID_SendReport(USBD_HandleTypeDef *pdev, uint8_t *report, uint16_t len, uint8_t report_id) { if (report_id == 0x01) { // Keyboard memcpy(aBuf, report, len); aBuf[0] = 0x01; // 强制置Report ID } else if (report_id == 0x02) { // Mouse memcpy(&aBuf[1], report, len); // 鼠标数据从第2字节开始(第1字节是ID) aBuf[0] = 0x02; } return USBD_LL_Transmit(pdev, 0x81, aBuf, 8); // 发送到EP1 IN }

逻辑说明:report_id参数由上层业务代码传入,决定填充哪个Report ID。aBuf统一为8字节,适配键盘(6字节状态+2字节修饰键)和鼠标(3字节:按钮+X+Y)。USBD_LL_Transmit调用前必须确保aBuf[0]是正确ID,否则主机解析错乱。


3. HAL层状态同步实现:键盘与鼠标的独立状态机如何共用同一套USB中断服务

3.1 USB中断服务函数(USBD_LL_DataInStage)的改造要点:解包Report ID并路由

CubeMX生成的usbd_conf.c中,USBD_LL_DataInStage()是EP1 IN传输完成后的回调。原始代码直接调用USBD_HID_SendReport(),但未传递report_id。我们必须在此处注入路由逻辑:

void USBD_LL_DataInStage(USBD_HandleTypeDef *pdev, uint8_t epnum) { /* Only handle EP1 IN */ if (epnum == 0x01) { // 检查上次发送的是键盘还是鼠标 —— 这里需维护一个状态变量 if (last_report_type == REPORT_TYPE_KEYBOARD) { // 键盘发送完成,可触发下一次键盘扫描 keyboard_scan_next(); } else if (last_report_type == REPORT_TYPE_MOUSE) { // 鼠标发送完成,可触发下一次鼠标采样 mouse_sample_next(); } } }

但更健壮的做法是:在发送时记录last_report_type,并在中断中据此调度。我们在USBD_HID_SendReport()开头添加:

static uint8_t last_report_type = REPORT_TYPE_NONE; uint8_t USBD_HID_SendReport(USBD_HandleTypeDef *pdev, uint8_t *report, uint16_t len, uint8_t report_id) { last_report_type = (report_id == 0x01) ? REPORT_TYPE_KEYBOARD : REPORT_TYPE_MOUSE; // ... 后续缓冲区填充与发送逻辑 }

这样,中断到来时就能明确知道该唤醒哪个外设的采集任务,避免键盘扫描和鼠标采样互相抢占。

3.2 键盘状态机:6键无冲+修饰键的位图管理与防抖策略

STM32F103C8T6的GPIO资源有限,我们通常用矩阵扫描(如4×4)或直接IO(最多8键)接按键。无论哪种,状态管理必须满足HID键盘Report格式:1字节修饰键(Ctrl/Shift/Alt等)+ 6字节普通键码(Key Code Array)。关键设计点:

  • 修饰键位图:定义uint8_t keyboard_modifiers,每位对应一个修饰键(Bit0=Left Ctrl, Bit1=Left Shift...),按下置1,松开清0;
  • 键码数组去重:定义uint8_t keyboard_keys[6],每次扫描将新按下的键码填入空位(值为0x00的位置),禁止重复填入同一键码,防止Windows报“键卡住”;
  • 硬件防抖:在GPIO初始化时开启输入滤波(GPIO_InitStruct.Pull = GPIO_PULLUP;+GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;),软件层再加10ms定时器消抖——某导师实测,仅靠软件延时在USB 1ms帧间隔下易丢键。
// 键盘扫描主循环(放在main while(1)中,非中断) void keyboard_scan_next(void) { static uint32_t last_scan_ms = 0; if (HAL_GetTick() - last_scan_ms < 10) return; // 10ms防抖 last_scan_ms = HAL_GetTick(); uint8_t new_keys[6] = {0}; uint8_t new_mod = 0; // 扫描所有按键,更新new_keys和new_mod scan_physical_keys(&new_mod, new_keys); // 仅当状态变化时才发送Report if ((new_mod != keyboard_modifiers) || memcmp(new_keys, keyboard_keys, sizeof(keyboard_keys)) != 0) { memcpy(keyboard_keys, new_keys, sizeof(keyboard_keys)); keyboard_modifiers = new_mod; // 构造Report Buffer:[ID][Mod][Res][K1]...[K6] uint8_t report_buf[8] = {0x01, new_mod, 0x00}; // ID=0x01, Mod, Reserved memcpy(&report_buf[3], new_keys, 6); // K1~K6 from index 3 USBD_HID_SendReport(&hUsbDeviceFS, report_buf, 8, 0x01); } }

参数说明:report_buf[8]严格对齐HID键盘Report Descriptor定义;0x00为Reserved字节,必须存在;memcpy(&report_buf[3], new_keys, 6)确保6个键码从第4字节开始(索引3),因前3字节已被ID、Mod、Res占用。

3.3 鼠标状态机:相对位移的增量压缩与按钮状态同步

HID鼠标Report格式为:1字节按钮(左/右/中)、1字节X位移、1字节Y位移(有符号)。难点在于:

  • 位移压缩:原始ADC或编码器数据是12位,但Report只给1字节(-128~127),需做缩放(如x_delta = raw_x / 16);
  • 按钮去抖:与键盘同理,但鼠标按钮(尤其微动)抖动更剧烈,建议硬件RC滤波+软件15ms延时;
  • 状态同步:鼠标移动和按钮不能异步发送,必须打包进同一Report——即每次USBD_HID_SendReport(..., 0x02)必须包含当前按钮状态+最新位移。
typedef struct { uint8_t buttons; // Bit0=Left, Bit1=Right, Bit2=Middle int8_t x_delta; // -128 ~ 127 int8_t y_delta; // -128 ~ 127 } mouse_report_t; static mouse_report_t current_mouse = {0}; void mouse_sample_next(void) { static uint32_t last_sample_ms = 0; if (HAL_GetTick() - last_sample_ms < 15) return; last_sample_ms = HAL_GetTick(); mouse_report_t new_mouse = {0}; // 采样按钮(GPIO读取) new_mouse.buttons = (HAL_GPIO_ReadPin(BTN_LEFT_GPIO_Port, BTN_LEFT_Pin) ? 0 : 1) | (HAL_GPIO_ReadPin(BTN_RIGHT_GPIO_Port, BTN_RIGHT_Pin) ? 0 : 2); // 采样位移(假设用TIM编码器,此处简化为ADC差分) int16_t raw_x = read_adc_x(); int16_t raw_y = read_adc_y(); new_mouse.x_delta = (int8_t)(raw_x >> 4); // /16 new_mouse.y_delta = (int8_t)(raw_y >> 4); // 仅当任一字段变化时发送 if ((new_mouse.buttons != current_mouse.buttons) || (new_mouse.x_delta != current_mouse.x_delta) || (new_mouse.y_delta != current_mouse.y_delta)) { current_mouse = new_mouse; uint8_t report_buf[4] = {0x02, new_mouse.buttons, new_mouse.x_delta, new_mouse.y_delta}; USBD_HID_SendReport(&hUsbDeviceFS, report_buf, 4, 0x02); } }

逻辑说明:report_buf[4]中0x02是Report ID,后3字节严格对应鼠标Descriptor定义的Usage Page: Generic Desktop→Usage: Pointer→Collection: Physical→Usage: Button/Usage: X/Usage: Y。位移用int8_t直接赋值,HAL自动处理符号扩展。


4. 避坑指南:复合HID开发中五个必踩的“Windows静默失败”陷阱

4.1 现象:设备管理器只显示“HID键盘”,鼠标不出现,且无错误提示

原因:Configuration Descriptor中鼠标Interface的bInterfaceProtocol未设为0x02,或bInterfaceNumber重复为0x00。Windows枚举时,若Protocol非0x02,会跳过该Interface,不创建设备实例。
解决:用USBlyzer或Wireshark抓包,查看枚举阶段返回的Descriptor。确认鼠标Interface段的第7字节(bInterfaceProtocol)为0x02,第5字节(bInterfaceNumber)为0x01。

4.2 现象:键盘能用,鼠标指针乱跳或不动

原因:鼠标Report Descriptor中缺失Logical Minimum/Maximum定义,或位移字段未声明为Signed。导致主机将0xFF(-1)解析为255,指针狂奔。
解决:检查鼠标Report Descriptor,必须包含:

0x15, 0x81, // Logical Minimum (-127) 0x25, 0x7F, // Logical Maximum (127) 0x75, 0x08, // Report Size (8 bits) 0x95, 0x02, // Report Count (2: X and Y) 0x81, 0x06, // Input (Data,Var,Rel) ← Rel表示Relative,关键!

0x81, 0x06中的0x06(Constant, Variable, Relative)是鼠标位移的黄金标记,缺之必翻车。

4.3 现象:按下键盘后鼠标失灵1秒,或反之

原因:键盘和鼠标共用同一EP1 IN,但HAL发送函数未做互斥保护。例如键盘发送中途被鼠标采样打断,导致aBuf被覆盖,发出错误Report。
解决:在USBD_HID_SendReport()入口加临界区:

HAL_NVIC_DisableIRQ(USB_LP_CAN1_RX0_IRQn); // 禁用USB中断 // ... 填充aBuf HAL_NVIC_EnableIRQ(USB_LP_CAN1_RX0_IRQn); return USBD_LL_Transmit(...);

或更优:用osMutex(若用FreeRTOS),但裸机环境禁用中断最直接。

4.4 现象:Windows提示“设备描述符请求失败”,设备反复断连

原因:wTotalLength计算错误,或Descriptor数组未__ALIGN_BEGIN/__ALIGN_END对齐。STM32F1系列USB外设要求Descriptor地址4字节对齐,否则DMA读取错位。
解决:检查USBD_FS_CfgDesc声明,确认有__ALIGN_BEGIN宏;用调试器查看该数组地址是否为4的倍数;用计算器重算wTotalLength:键盘Interface 9B + 键盘HID Class 9B + 键盘Report Desc(含ID)N B + 鼠标Interface 9B + 鼠标HID Class 9B + 鼠标Report Desc(含ID)M B + 配置头9B = 总长。

4.5 现象:MacOS能识别双设备,Windows只认键盘

原因:Windows对复合HID的Report ID校验更严。若键盘Report Descriptor中0x85, 0x01后紧跟0x95, 0x06(Report Count=6),但实际发送时只填了3个键码,剩余为0,Windows可能拒绝。
解决:确保Report Descriptor中Report Count与实际发送字节数一致;发送时report_buf必须严格按Descriptor定义填充,不可留空。例如键盘Report定义6个Key Code,则report_buf[3]到report_buf[8]必须全写,哪怕填0x00(空键码)。


5. 实战验证与进阶技巧:用Python脚本自动化检测Report流,以及低功耗下的USB挂起恢复

5.1 用PyUSB实时捕获并解析HID Report流:验证Report ID是否生效

光看设备管理器不够,必须看到真实数据流。在Ubuntu或Windows WSL中,用PyUSB直接读取HID接口:

pip install pyusb
# verify_reports.py import usb.core import usb.util dev = usb.core.find(idVendor=0x0483, idProduct=0x5710) # STM32默认VID/PID if dev is None: raise ValueError("Device not found") # 获取HID接口(Interface 0 = Keyboard, Interface 1 = Mouse) keyboard_intf = 0 mouse_intf = 1 # Claim键盘接口 usb.util.claim_interface(dev, keyboard_intf) # 设置配置(如果未配置) dev.set_configuration() print("Listening for Keyboard Reports (ID=0x01)...") while True: try: # 读取8字节(键盘Report大小) data = dev.read(0x81, 8, timeout=1000) # EP1 IN if data[0] == 0x01: print(f"✅ Keyboard Report: Mod={data[1]:02X}, Keys={data[3:9]}") elif data[0] == 0x02: print(f"⚠️ Mouse Report on Keyboard EP? Data={data}") except usb.core.USBTimeoutError: continue except KeyboardInterrupt: break usb.util.release_interface(dev, keyboard_intf)

技巧说明:dev.read(0x81, 8, ...)直接读EP1 IN,data[0]即Report ID。若看到data[0]==0x01,证明键盘ID生效;若data[0]==0x02,说明鼠标Report误发到键盘EP——此时应检查USBD_HID_SendReport()中report_id参数是否传错。此脚本是排查“ID混淆”的后悔药。

5.2 Windows设备管理器深度诊断:启用USB枚举日志定位Descriptor错误

当设备不识别时,Windows日志比任何抓包都准。启用方法:

  1. 管理员权限运行CMD:
    cd /d %windir%\inf mkdir usbdebug echo [Version] > usbdebug.inf echo signature="$WINDOWS NT$" >> usbdebug.inf echo [DefaultInstall] >> usbdebug.inf echo AddReg=DebugReg >> usbdebug.inf echo [DebugReg] >> usbdebug.inf echo HKLM,SYSTEM\CurrentControlSet\Control\UsbFlags,EnableLogging,0x00010001,1 >> usbdebug.inf
  2. 设备管理器 → “查看” → “显示隐藏设备” → 右键“通用串行总线控制器” → “更新驱动程序” → “浏览我的电脑” → “让我从列表选择” → “从磁盘安装” → 指向usbdebug.inf。
  3. 重启后,日志位于%SystemRoot%\inf\setupapi.dev.log,搜索USB\VID_0483&PID_5710,看是否有Failed to parse configuration descriptor。

5.3 低功耗场景:USB挂起(Suspend)期间的GPIO与定时器管理

STM32F103在USB挂起时,内核可进入Sleep模式,但需注意:

  • USB中断必须唤醒:在USBD_LL_SetupStage()中,收到SET_FEATURE DEVICE_SUSPEND后,调用HAL_PWR_EnterSLEEPMode(PWR_MAINREGULATOR_ON, PWR_SLEEPENTRY_WFI);
  • 唤醒后重初始化USB:USBD_LL_Resume()中必须调用HAL_PCDEx_PMAConfig()和HAL_PCD_Init(),否则EP1 IN无法再用;
  • GPIO状态保持:挂起前,用HAL_GPIO_WritePin()保存所有按键/LED状态;唤醒后,用HAL_GPIO_ReadPin()恢复,避免状态丢失。
void USBD_LL_Suspend(USBD_HandleTypeDef *pdev) { // 保存GPIO状态 saved_btn_state = HAL_GPIO_ReadPin(BTN_GPIO_Port, BTN_Pin); // 进入Sleep HAL_PWR_EnterSLEEPMode(PWR_MAINREGULATOR_ON, PWR_SLEEPENTRY_WFI); } void USBD_LL_Resume(USBD_HandleTypeDef *pdev) { // 重初始化USB外设 HAL_PCDEx_PMAConfig((PCD_HandleTypeDef*)pdev->pData, 0x00, PCD_SNG_BUF, 0x1000); HAL_PCD_Init((PCD_HandleTypeDef*)pdev->pData); // 恢复GPIO HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, (saved_btn_state ? GPIO_PIN_SET : GPIO_PIN_RESET)); }

经验总结:从那以后我每次做USB复合设备,都强制走一遍“PyUSB抓包验证Report ID”+“Windows setupapi.dev.log查枚举错误”+“USBlyzer看Descriptor二进制”,三者交叉印证。少走一半弯路。希望帮到你。

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

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

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

立即咨询