ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

免驱USB键盘模拟开发:HID描述符与STM32固件实战解析

免驱USB键盘模拟开发:HID描述符与STM32固件实战解析 开发一个USB键盘模拟设备或者自拍器、快捷键小键盘、按键录制器时最容易被吓住的就是“驱动”两个字。很多人一上来就想写Windows驱动觉得非得搞出一个.sys文件不可。实际上Windows从很早的系统版本开始就内置了完整的USB HID类驱动你的设备只要枚举成符合HID规范的键盘插上去就能用根本不需要任何第三方驱动。这篇文章我就完整讲一下怎么利用Windows内置驱动快速实现USB键盘模拟从USB描述符到报告描述符从STM32固件到总线抓包排查把我在实际开发中踩过的坑一个个摊开来讲。1. 先搞懂Windows那套内置驱动栈才知道“免驱”到底免的是什么很多人问我做USB键盘模拟是不是得写驱动。我的回答是如果你用到的HID报告格式是标准的Windows里早就给你准备好了完整的驱动链一个.sys都不用写。但前提是你得理解这套驱动链的工作方式否则设备枚举出来不对你会以为是驱动问题实际上可能是描述符写错了。1.1 从设备插入到键盘能用系统后台发生了什么当你的设备通过USB口插到电脑上系统后台做的事情比你想象的多。刚开始USB主机控制器EHCI/XHCI检测到D上的上拉知道有设备接入于是启动复位过程。设备被复位后主机向下发Get_Descriptor请求拿到设备描述符。注意这里有个细节主机先只拿前8字节其中就包含bMaxPacketSize0——端点0控制端点的最大包长。拿到之后主机会分配一个地址给设备然后用新地址把设备描述符完整拿一遍再依次拿配置描述符、接口描述符、HID描述符、端点描述符。真正决定“设备是谁”的是配置描述符集合里的接口描述符。如果你的接口描述符声明bInterfaceClass3、bInterfaceSubClass1、bInterfaceProtocol1Windows就会认定这是一个HID Boot Keyboard然后自动加载hidusb.sys、hidclass.sys和kbdhid.sys这一整套类驱动不需要你提供任何inf文件。整个过程在设备管理器的“键盘”分类下就能看到新设备名字通常带HID Keyboard Device。1.2 hidusb、kbdhid、hidclass各自管哪一段Windows内置的USB HID驱动链可以理解为三层。最下面是hidusb.sys它负责和USB总线打交道处理URB、中断传输、枚举请求这些原始总线事务。中间是hidclass.sys它属于HID设备的类驱动负责解析HID描述符和报告描述符把设备抽象成HID设备。最上层是kbdhid.sys这个需要单独说一下它是HID键盘的类驱动从HID协议层接收报告把键盘按键转成Windows的输入事件。这三层的分工决定了你开发的设备只需要满足一个要求能响应USB标准请求能正确上报HID报告描述符和输入报告。剩下的事情系统全包了。这也是为什么很多单片机产品、DIY自拍器、媒体遥控器都能做到“插上就用”因为它们本质上就是一个标准的USB HID键盘。1.3 内置驱动能覆盖的设备边界以及覆盖不到的内置驱动能覆盖的是符合HID规范的设备而且主要是键盘、鼠标、消费控制设备音量、播放暂停、媒体控制这些“标准用途”。如果你的设备被识别为HID键盘那Windows按键输入这部分完全不用管。如果你做的是自定义的HID vendor-defined设备比如给软件发送私有数据Windows同样有内置的HID类驱动但上层没有“键盘”“鼠标”对应的输入处理你需要自己写一个应用软件通过hid.dll或ReadFile去读取报告这就不是“免驱”了。我在实际项目里就吃过亏一开始把产品做成了自定义HID设备Windows识别正常但上层软件不写的话设备就是一块砖。后来改成“标准HID键盘消费控制页”的方案用户插上就能用这才达到免驱的目标。所以决定要不要自己写驱动其实取决于你要不要用Windows已经定义好的用途。键盘模拟这条路上绝大多数需求都不需要自己写驱动。2. 枚举阶段最考验描述符一份能用且不犯错的配置集合USB设备能被Windows认成键盘靠的不是什么神秘代码而是配置描述符集合里一串字段。我在帮客户调设备时见过大量枚举失败的案例最后定位下来基本都是描述符写错。下面我把一份标准键盘的配置描述符集合拆开逐个字段过一遍并指出最容易被忽略的地方。2.1 四条核心描述符设备、配置、接口、端点设备描述符重点关注这几个bDeviceClass0表示类定义在接口描述符里这个很重要别想当然填成3bMaxPacketSize0控制端点最大包长FS下填64idVendor和idProduct自定义即可Windows不管但别用0x0000这种明显非法的厂商标识配置描述符集合包含三个结构级联配置描述符 接口描述符 HID描述符 端点描述符。最容易错的是wTotalLength它必须等于整个描述符集合的总字节数多一个字节、少一个字节都可能导致Windows枚举失败或者驱动加载异常。我在调试时发现很多人在这个字段上直接复制别家的值结果接口数不同、端点不同整个集合的长度也就不同设备就成了“无法识别的USB设备”。// 以STM32 HAL库的USBD_HID为例配置描述符集合示例 __ALIGN_BEGIN static uint8_t USBD_HID_CfgDesc[USB_HID_CONFIG_DESC_SIZ] __ALIGN_END { 0x09, /* bLength */ USB_DESC_TYPE_CONFIG, /* bDescriptorType */ USB_HID_CONFIG_DESC_SIZ, 0x00, /* wTotalLength */ 0x01, /* bNumInterfaces */ 0x01, /* bConfigurationValue */ 0x00, /* iConfiguration */ 0x80, /* bmAttributes: bus-powered */ 0x32, /* bMaxPower: 100mA */ /* 接口描述符 */ 0x09, /* bLength */ USB_DESC_TYPE_INTERFACE, 0x00, /* bInterfaceNumber */ 0x00, /* bAlternateSetting */ 0x01, /* bNumEndpoints */ 0x03, /* bInterfaceClass: HID */ 0x01, /* bInterfaceSubClass: Boot */ 0x01, /* bInterfaceProtocol: Keyboard */ 0x00, /* iInterface */ /* HID描述符 */ 0x09, /* bLength */ 0x21, /* bDescriptorType: HID */ 0x11, 0x01, /* bcdHID: 1.11 */ 0x00, /* bCountryCode */ 0x01, /* bNumDescriptors */ 0x22, /* bDescriptorType: Report */ sizeof(USBD_HID_ReportDesc), 0x00, /* wDescriptorLength */ /* 端点描述符 */ 0x07, /* bLength */ USB_DESC_TYPE_ENDPOINT, 0x81, /* bEndpointAddress: IN, EP1 */ 0x03, /* bmAttributes: Interrupt */ 0x08, 0x00, /* wMaxPacketSize: 8 */ 0x01, /* bInterval: 1ms */ };这里还有两个细节。一个是HID描述符里的wDescriptorLength必须和报告描述符数组的实际字节数一致否则Windows加载驱动时拿到的报告描述符是截断的后面会出现“设备能识别但按键错乱”的怪毛病。另一个是bIntervalFull Speed中断端点的单位是毫秒填1表示主机每1ms查询一次IN端点响应更快如果对响应速度不敏感填10也可以。2.2 Boot SubClass与Protocol要不要声明接口描述符里bInterfaceSubClass和bInterfaceProtocol这两个字段新手经常不知道该怎么填。这里直接给结论你可以按这个表来选集合bInterfaceSubClassbInterfaceProtocol行为表现标准键盘Boot0x010x01Windows下正常BIOS/UEFI阶段也可用标准键盘无Boot0x000x00Windows下正常BIOS阶段大概率不可用鼠标0x010x02鼠标设备和键盘相反我一般的建议是除非有特殊原因否则按Boot Keyboard来做。原因是这个声明不会影响Windows下的正常功能反而多了一层兼容性尤其适合那些需要在开机时用键盘操作选项的产品比如按键录制器要在系统引导阶段使用。但要注意声明了Boot之后固件要能正确处理Set_ProtocolBOOT_PROTOCOL/REPORT_PROTOCOL请求。这是个不大不小的坑。HAL库的usbd_hid.cST官方其实处理了Set_Protocol请求但有些精简代码或早期版本没处理这会导致系统报“无法启动设备”。我后面排查章节会再展开。2.3 用Bus Hound确认枚举交互是否符合预期描述符写没写对直接看现象判断是最慢的。我强烈建议你在第一次调通之前就装上Bus Hound免费或者USBPcapWireshark把枚举过程抓下来看。Bus Hound开抓之后能看到主机依次发出Get_Descriptor、Set_Address、Get_Configuration等请求以及设备的返回数据。对着返回值逐字段核一遍比反复插拔瞎试要快得多。具体操作很简单Bus Hound选择你的设备点Capture然后插拔一次USB停掉捕获在事件列表里找到GET DESCRIPTOR相关的URB。如果设备返回的数据和预期不符问题基本就在那一线。枚举阶段出问题90%都是描述符字段错用抓包工具直接“看”设备的返回比靠Windows的错误提示猜要直接。3. 报告描述符才是键盘模拟的灵魂从标准8字节到音量键扩展枚举成功只代表Windows认识这个“键盘”了真正决定按键能不能送到系统的是HID报告描述符Report Descriptor。报告描述符描述的是报告的结构哪个字节是修饰键哪几个字节是普通键码哪些位是LED输出。Windows会严格按照这段描述符来解析你发送的数据所以这里任何一个字节写错键盘都会表现得很诡异——有时候是按键错乱有时候是按下没反应有时候是某个键一直卡住。3.1 逐字节拆解标准键盘报告描述符先给出我一直在用的标准键盘报告描述符这段可以从成熟项目里抄但你必须看懂它。const uint8_t USBD_HID_ReportDesc[] { 0x05, 0x01, // Usage Page(Generic Desktop) 0x09, 0x06, // Usage(Keyboard) 0xA1, 0x01, // Collection(Application) // 修饰键 Ctrl/Shift/Alt/Win 0x05, 0x07, // Usage Page(Keyboard/Keypad) 0x19, 0xE0, // Usage Minimum(224) 0x29, 0xE7, // Usage Maximum(231) 0x15, 0x00, // Logical Minimum(0) 0x25, 0x01, // Logical Maximum(1) 0x75, 0x01, // Report Size(1) 0x95, 0x08, // Report Count(8) 0x81, 0x02, // Input(Data, Var, Abs) // 保留字节 0x95, 0x01, // Report Count(1) 0x75, 0x08, // Report Size(8) 0x81, 0x01, // Input(Const) // 6个普通按键使用Array方式 0x95, 0x06, // Report Count(6) 0x75, 0x08, // Report Size(8) 0x15, 0x00, // Logical Minimum(0) 0x25, 0x65, // Logical Maximum(101) 0x05, 0x07, // Usage Page(Keyboard/Keypad) 0x19, 0x00, // Usage Minimum(0) 0x29, 0x65, // Usage Maximum(101) 0x81, 0x00, // Input(Data, Array) // 输出报告LED状态NumLock/CapsLock/ScrollLock等 0x95, 0x05, // Report Count(5) 0x75, 0x01, // Report Size(1) 0x05, 0x08, // Usage Page(LEDs) 0x19, 0x01, // Usage Minimum(1) 0x29, 0x05, // Usage Maximum(5) 0x91, 0x02, // Output(Data, Var, Abs) 0x95, 0x01, // Report Count(1) 0x75, 0x03, // Report Size(3) 0x91, 0x01, // Output(Const) 0xC0 // End Collection };解析一下开头是Generic Desktop页里的Keyboard集合然后前面8个bit是修饰键。修饰键的物理含义是“按住状态”每个bit代表一个键左Ctrl、左Shift、左Alt、左Win、右Ctrl、右Shift、右Alt、右Win。接着8个bit保留系统要求必须保留通常发0。最后6个字节是普通按键区采用Array方式所以它可以同时容纳最多6个键码。3.2 按键码、修饰键和组合键的数据组织发送一个按键时发送的是一段8字节报告Byte 0修饰键位图按CtrlShiftA时Byte 0 0x02左Shift| 0x01左Ctrl 0x03同时Byte 2 0x04AByte 1保留恒为0Byte 2~7普通按键键码从0x00到0x650表示空位常用的键码表我直接放一份可以抄的A0x04B0x05C0x06一直到Z0x1D数字键10x1E、20x1F、30x20一直到00x27Enter0x28、Esc0x29、Tab0x2B、Space0x2CF10x3A、F20x3B一直到F120x45方向键Up0x52、Down0x51、Left0x50、Right0x4FInsert0x49、Home0x4A、PageUp0x4B、Delete0x4C、End0x4D、PageDown0x4E。剩下的小键盘和媒体键可以在USB HID Usage Table文档里查这是公开标准网上搜一下就有完整PDF。发键容易犯一个错误只发按下状态不发释放状态。键盘协议是“状态快照”——你发的报告描述的是“当前哪些键处于按下状态”。如果只发按下A的报告Windows会认为A一直按住然后自动重复直到你发一份“没有A”的报告。所以最小可靠的点击动作是发送含A的报告等待20ms左右再发送全0报告。3.3 音量键/媒体键加字节还是加报告ID如果只是发字母数字用标准8字节报告就够了。但很多产品还需要音量调节、静音、播放暂停这些媒体键。媒体键不在键盘Usage Page里而在Consumer PageUsage Page0x0C所以报告结构就要变。最简单的做法是把报告从8字节扩成9字节多出来的第9字节放媒体键的位图。对应报告描述符要在6键数组后面追加一段Consumer Page描述0x05, 0x0C, // Usage Page(Consumer) 0x09, 0x01, // Usage(Consumer Control) 0xA1, 0x01, // Collection(Application) 0x0A, 0xE9, 0x00, // Usage(Volume Increment) 0x0A, 0xEA, 0x00, // Usage(Volume Decrement) 0x0A, 0xE2, 0x00, // Usage(Mute) 0x15, 0x00, 0x25, 0x01, 0x75, 0x01, 0x95, 0x03, 0x81, 0x02, // Input(Data, Var, Abs) 0x95, 0x05, 0x81, 0x01, // 剩余5bit填充 0xC0但这里有个坑如果第9字节加入同一份报告那么报告就不是标准的8字节Boot格式了。如果设备又声明了Boot Keyboard在Boot协议下会冲突Boot协议只认前8字节。另外Windows对8字节和9字节键盘报告的容忍度不一样某些终端程序或老式软件可能解析异常。更稳妥的方案是用报告ID分两套报告Report ID 1是标准键盘Report ID 2是Consumer Control。这样键盘部分保持Boot兼容媒体键单独走一套。代价是固件发送时每个报告都要带一个报告ID字节。我的项目里目前的方案就是这样一个HID接口里定义两个报告ID实测Windows、macOS、Linux都没问题。3.4 首次调试最好先用工具校验报告描述符报告描述符语法复杂纯靠眼睛看容易漏。我强烈建议在烧录前把写好的报告描述符用HID报告描述符分析工具网上可以搜到“hid报告描述符分析工具v1.7”这类小工具或在线解析器跑一遍。工具会输出每个字段的解析结果比如“全局项Usage Page Keyboard/Keypad”、“主项目Input Data, Variable, Absolute”这种。如果你看到Report Count×Report Size换算出的总位数不是8的倍数报告就没法正常解析这就是典型的“设备能枚举但按键无效”的根源。4. 固件端发键的完整套路从初始化到一次可靠敲击描述符对完之后就到固件了。下面我以STM32为主讲套路因为网上做USB HID键盘的教程里STM32和Arduino各占半壁江山。Arduino LeonardoATmega32U4的HID库封装更简单几步就能发键STM32则更适合做产品流程可控性更高。4.1 以STM32为例的HID工程改造用STM32CubeMX生成一个USB Device HID的工程。生成后默认的报告描述符是鼠标的长度19字节接口描述符里鼠标协议也和键盘不一样。所以要做三处修改。第一处替换报告描述符数组。把上面第三章的标准键盘报告描述符整体换成数组赋值注意长度要和HID描述符里wDescriptorLength字段一致否则枚举阶段报告描述符会截断Windows很容易报设备无法启动。我遇到过几次都是在wDescriptorLength上写死了宏结果报告描述符改长之后忘了同步。第二处修改端点最大包长。HAL库里键盘端点的wMaxPacketSize默认可能写的是0x4064字节或者0x044字节鼠标端点建议改成0x08。虽然理论上大包长也能用但保持和报告实际长度一致可以减少兼容性问题。第三处配置描述符里接口描述符的bInterfaceSubClass和bInterfaceProtocol改成0x01、0x01Boot Keyboard这个按第二章说的来。4.2 按下与释放的时序控制以及为什么不能只有按下发送按键核心就两个操作把目标报告填好调用USBD_HID_SendReport发出去。但这里有一个HAL库经常被忽略的点USBD_HID_SendReport是异步的它把报告挂到IN端点真正传输要等主机下一次IN令牌查询。如果连续发送两份报告第二份可能在端点缓冲还没清空时被丢弃返回USBD_BUSY。我实际测试中用1ms间隔发送也会偶发丢包所以稳妥的节奏是“发送一份等上一小段时间再发送下一份”。标准敲击动作的代码逻辑如下uint8_t keyReport[8] {0, 0, 0, 0, 0, 0, 0, 0}; void send_key(uint8_t keycode, uint8_t modifier) { // 按下 keyReport[0] modifier; keyReport[2] keycode; USBD_HID_SendReport(hUsbDeviceFS, keyReport, 8); HAL_Delay(20); // 释放 keyReport[0] 0; keyReport[2] 0; USBD_HID_SendReport(hUsbDeviceFS, keyReport, 8); HAL_Delay(20); }HAL_Delay(20)的意义是保证Windows能看到按下和释放两个状态20ms是一个平衡点。太短比如2ms时USB传输和系统输入线程还没处理完可能丢状态太长比如200ms则感觉卡顿。这个数值可以根据实测微调我的经验是10~30ms表现都不错。还一个细节如果你想模拟“长按”比如快捷键按住不放就不要在按下后立刻发释放报告而是保持当前报告状态一段时间靠外部条件定时器、GPIO释放事件再发送清零报告。这也说明报告结构天然支持长按不需要像老式串口那样做重复发送。4.3 组合键、长按、宏按键的代码组织项目做大了以后持续调用send_key会非常碎。我的做法是定义一个简单的“按键事件”结构体把一次动作抽象成“按下某几个键、保持多久、释放”然后上层只管拼装。typedef struct { uint8_t keys[6]; uint8_t modifier; uint16_t hold_ms; } KeyAction; void execute_action(const KeyAction *act) { uint8_t report[8] {0}; report[0] act-modifier; memcpy(report[2], act-keys, 6); USBD_HID_SendReport(hUsbDeviceFS, report, 8); HAL_Delay(act-hold_ms); memset(report, 0, 8); USBD_HID_SendReport(hUsbDeviceFS, report, 8); HAL_Delay(20); }组合键就是填充modifier和keys数组比如CtrlShiftS对应modifier0x03、keys[0]0x15S。宏按键就是把一串KeyAction按顺序执行。注意执行宏时不要在主循环里用长时间HAL_Delay堵死USB中断服务最好在状态机或定时器里逐条执行否则设备整体响应会卡。音量键的发送逻辑类似但走的是Consumer Control报告。我用报告ID方案时发送的是9字节数组第一个字节是报告ID2第二个字节是音量键位图发送完成后第二个字节清零第一字节保持2。如果用的是单报告扩展方案则在标准键盘报告的第9字节置位、延时、清零。5. 设备认了但不出字完整排查链路与典型陷阱这是最让新手崩溃的环节设备管理器里明明出现了HID Keyboard Device没感叹号但按下去就是不出字。这种“半成品”问题通常不是枚举挂了而是报告描述符或数据组织错了。我把它按优先级排一下排查顺序。5.1 先把问题分类枚举失败、驱动异常、报告无效我会把问题分成三类来定位现象可能原因优先排查方向插上毫无反应Windows弹无法识别硬件电路、D上拉、描述符严重错误供电、线材、抓包看枚举设备管理器有未知设备或感叹号配置描述符、HID描述符、报告描述符长度设备管理器错误代码、Kernel-PnP日志设备管理器显示HID Keyboard Device但按键无效报告数据与报告描述符不匹配核对发送的矩阵与描述符字段第三个场景最容易误判成“驱动问题”其实问题完全在固件和报告结构上。5.2 设备管理器事件查看器Kernel-PnP日志先用设备管理器看设备状态。右键设备属性里的“设备状态”会给出错误代码代码10一般是驱动无法启动代码43多数是硬件上报错误代码28是未安装驱动——但“HID Keyboard Device”出现这个大概率是描述符集合里的接口信息不对。接下来打开事件查看器Windows日志-系统筛选事件源Kernel-PnP能看到设备安装的详细过程和失败原因。这个日志会直接告诉你“设备未迁移”还是“驱动返回错误”比瞎猜强太多。有些细节设备管理器是看不到的比如主机请求报告描述符时固件返回的是什么。这时候需要抓包。5.3 Bus Hound抓包定位是描述符错还是数据错Bus Hound抓枚举过程的方法前面提过现在说怎么用数据区分故障。如果抓包看到主机的GET_DESCRIPTOR请求后面设备返回的数据是乱的或者在配置描述符请求之后主机就停止对话那问题就在描述符。如果枚举过程完全正常主机也read到了你发送的报告但系统没有按键反应那就要对比报告内容与报告描述符是否匹配。还有一个特别容易踩的坑某些精简的HID库不支持Set_Protocol(REPORT_PROTOCOL)或Get_Protocol请求。Windows在加载kbdhid.sys之后会发送Set_Protocol请求把设备切到Report协议。如果固件没实现这个控制请求可能返回STALLWindows会直接判定设备启动失败但设备管理器又不定时刷新出感叹号。这种问题你用Bus Hound抓一遍控制传输立刻就能看到是哪个请求被STALL了。5.4 几个我见过的高频坑描述符长度、端点大小、发送频率列一个容易出问题的小清单HID描述符里的wDescriptorLength和报告描述符实际长度不一致截断导致解析失败。端点描述符wMaxPacketSize和实际发送报告长度不匹配报告发送被截断。连续发送时没有等上一个发送完成报告互相覆盖现象是“按键偶尔失灵”。报告数组没清零就填入键码出现“一直按着一个键”的假象。使用了报告ID但发送时忘带ID字节全部数据错位按键乱跳。这些坑我全踩过每个都能让一个看起来正确的工程白折腾一下午。6. 硬件链路里那些和协议无关、但能折腾半天的坑软件调通之后很多人会栽在硬件上。协议和代码都对了但设备还是各种识别失败问题往往出在USB物理链路。6.1 D/D-走线、上拉电阻与USB识别USB 2.0全速设备在D线上要有1.5k上拉到3.3V主机靠这根上拉判断设备插入。很多MCU内部已经集成上拉比如STM32F4的OTG_FS但F1系列内部没有需要外部接1.5k电阻。如果上拉没接对设备插入时主机根本检测不到。D/D-是差分对布线上要保持等长尽量短远离晶振和电源噪声。我之前在一块紧凑的PCB上把D/D-绕了个长蛇形结果枚举成功率只有70%改短之后立刻稳定。如果你只是飞线做样机务必用双绞或按紧的两根短线别用杜邦线甩一长串高速信号很容易被干扰。6.2 供电、线材和接触不良的问题USB口能提供的电流有限全速设备在枚举阶段属于低功耗要求但如果你的板子一上来就带动大电流外设比如LED灯条、马达主机可能拉到过流保护瞬间端口断电设备插上又被踢下来。我习惯在调试时把负载和USB供电隔离开用独立的LDO给主控Vbus只做信号和基础供电。线材是一个低频但非常实际的坑很多廉价充电线只接了电源正负极D/D-没接或接触不良。插上后Windows一点反应都没有换上带数据传输的线就好了。另外USB-A口的弹片磨损后接触电阻很大设备在运行中掉线排查时先换个口或换根线别急着怀疑固件。6.3 低功耗、热插拔与意外掉线的处理设备进入低功耗模式时USB模块的时钟和D上拉状态如果处理不当会出现“电脑休眠后设备消失”或“唤醒后需要重新拔插”。我做过一个自拍器进入低功耗时把USB时钟关了唤醒后没有重新初始化USB外设结果每次都要重插。后来改成只在USB挂起时保持时钟、降低主频彻底解决了。热插拔场景下设备端要注意上电时序。MCU程序还没跑起来时D被提前拉高主机可能枚举到一半就拿不到应答所以有些板子会用GPIO控制上拉电阻程序初始化完成后再拉高D。这个细节不太起眼但能避免大量“插10次只有8次识别”的诡异现象。最后分享一个我自己的习惯改完报告描述符第一次上电先别急着接业务逻辑直接用一个循环发送A键然后在记事本里看有没有输出。这个最基础的验证通过再往上报媒体键和组合键。很多项目卡住都是因为一上来就把全部功能堆进去出了问题根本不知道是哪一层不对。先最小闭环再逐步加这套思路在USB HID开发里特别管用。
返回列表