简介:面向嵌入式开发者的CH376鼠标键盘控制资料包,围绕USB主机接口芯片与常见人机交互设备的通信展开,适合单片机及Arduino平台上需要控制键盘、鼠标的开发者。资源共二十二个文件,压缩包仅一百一十五KB,包含C语言源码、头文件、Keil工程文件、编译生成的固件、启动文件及列表等,能够完整还原一个CH376操作鼠标键盘的示例工程。目前已有四百九十七人学习,可作为USB设备控制入门的参考模板。内容覆盖芯片初始化、命令读写、键盘扫描码发送、鼠标移动与按键操作,以及中断错误处理等关键环节;通过源码可理解人机接口设备类在主机接口芯片上的调用方式,并在此基础上扩展自制USB键盘鼠标适配器或自动化测试工具。尤其适合需要将数据手册与代码对照学习、快速掌握CH376指令集的开发场景。
1. 用 CH376 接 USB 键鼠:把协议栈塞进芯片,MCU 只负责读懂报表
一块板子要接 USB 键盘鼠标,MCU 里却没有 USB 主机外设,这是很多嵌入式项目的真实痛点。我一般会直接让 CH376 把 USB 主机协议栈接走:MCU 只通过 SPI 发命令、读中断、解析 HID 报表。CH376 在键鼠场景里就像一个黑匣子,总线复位、枚举握手、中断事务调度都在芯片内部完成,应用层拿到的是干净的键值、位移和滚轮数据。这篇文章回答三类问题:CH376 模块到底能不能操作键鼠、最小命令流程怎么走、报表解析和轮询有哪些绕不开的坑。同时也说清楚一个边界:CH376 是单设备 USB 主机控制器,两个独立的有线键鼠不能同时挂在它下面,想同时用键盘和鼠标,要么选二合一接收器,要么改选多接口复合设备。适合正在做单片机外设扩展、主机输入设备改造的开发者阅读。
2. CH376 为什么能接管键鼠:内置固件、标准枚举与 Boot 协议的组合
2.1 内置固件与外置固件的区别:键鼠项目选内置固件就能干
CH376 有两种工作模式:内置固件模式和外置固件模式。内置固件模式下,芯片里跑着完整的 USB 主机控制器固件,能够自己处理总线复位、标准请求、令牌调度和数据重试,MCU 按命令集下发操作、读回结果。外置固件模式把 CH376 降级成一个纯粹的 USB 接口引擎,USB 协议栈全部要 MCU 自己实现,枚举状态机、控制传输的握手阶段、出错重试都要在中断里挤出来。做键鼠项目几乎没人选外置固件,除非 MCU 资源特别充裕而且有现成的协议栈代码。
很多人只知道 CH376 常用于读 U 盘,以为它只能操作 Mass Storage 类设备,这是个误区。U 盘读写只是内置固件里最出名的一项能力,芯片同样对外提供 USB 主机控制命令,允许 MCU 访问其他类别的设备。键盘鼠标属于 HID 类,走的端点类型是中断传输,CH376 对这类设备并没有特殊的自动识别逻辑,但它提供总线复位、读描述符、设置地址、设置配置、读端点数据等基础命令。只要按标准枚举流程把设备配置好,后续的端点轮询和报表读取完全可以做。
从硬件能力上看,CH376 工作在 USB 全速/低速范围,键鼠这种低速设备远低于它的上限,速度完全不是瓶颈。接口方面,它支持并口、SPI 和 UART 三种与 MCU 通信的方式,SPI 最省引脚,四根信号线加一根中断就能跑,适合大多数小型控制系统。选型上常见做法是:MCU 有硬件 SPI 就用 SPI,没有就用 GPIO 模拟,命令交互频率不高,IO 模拟也不会慢到影响键鼠体验。
2.2 一次完整枚举里,MCU 只需要关心 4 个节点
USB 设备上电后要经过标准枚举流程才会进入可用的配置状态。CH376 内置固件承担了底层事务,MCU 需要关心的节点不多:总线复位、获取设备描述符、设置地址、设置配置。总线复位之后设备地址回到 0,速度也在这时候被识别出来;获取设备描述符能拿到 VID、PID 和设备类信息;设置地址给设备分配一个非零地址,后续所有事务都基于这个地址;设置配置让设备进入 Configured 状态,这一步漏掉,设备不会正常上报数据。
HID 键盘鼠标的类信息不在设备描述符里,而在配置描述符下面的接口描述符中,这个细节经常让第一次做的人困惑。枚举时读到设备描述符,发现 bDeviceClass 是 0,以为设备不是 HID,其实 0 表示类定义在接口层。需要继续读配置描述符,在接口描述符里找到 bInterfaceClass 等于 0x03 的接口,再看 bInterfaceProtocol:键盘通常是 1,鼠标通常是 2。这个协议号决定后面能不能走 Boot 协议。
2.3 Boot 协议为什么是移植捷径
BOOT 协议是 USB HID 规范里为 BIOS 环境保留的一套简化模式,键盘和鼠标在 Boot 协议下使用固定格式的报表,不解析报告描述符。键盘在 Boot 协议下是强制要求支持的,报表固定 8 字节:1 字节修饰键、1 字节保留、6 字节按键槽位。鼠标的 Boot 支持是可选的,标准 Boot 鼠标报表固定 3 字节:1 字节按键位图、1 字节 X 位移、1 字节 Y 位移,带滚轮的鼠标通常会扩展成 4 字节。
选择 Boot 协议意味着不用写报告描述符解析器,对资源紧张的 MCU 非常友好。鼠标的情况要留个心眼:设备默认可能是 Report 协议而不是 Boot 协议,需要尝试下发 Set_Protocol 请求切到 Boot,或者直接打印报表看首帧数据是否符合预期。常见做法是先把枚举做完,读端点数据并打印,看长度和内容再决定解析策略。如果设备不支持 Boot 或者要读滚轮扩展数据,就得退回解析完整 HID 报告描述符,工作量会大一个量级。
3. 移植 CH376 操作键鼠:SPI 接线、上电自检与最小枚举序列
3.1 SPI 接线、复位时序与 3.3V/5V 电平处理
实际操作中我最常用的接线方式是四线 SPI 加中断。CH376 一侧需要片选、时钟、数据输入、数据输出四根信号,再加上 INT 中断引脚和 RST 复位引脚。片选低有效,SCK 时钟由 MCU 产生,INT 是芯片主动向 MCU 报告事件用的,必须接外部中断输入或者至少是能捕获电平变化的 GPIO。
| CH376 引脚 | 接 MCU 引脚 | 说明 |
|---|---|---|
| SCS | GPIO 输出 | 片选,低有效,命令/数据阶段由它圈定 |
| SCK | SPI 时钟 | 时钟速率按手册上限留一半余量 |
| SDI | MOSI | MCU 向 CH376 写命令和数据 |
| SDO | MISO | MCU 从 CH376 读状态和数据 |
| INT | 外部中断输入 | 芯片事件通知,下降沿/低电平触发 |
| RST | GPIO 输出 | 上电复位控制 |
电平匹配是移植里最常见的翻车点。CH376 的供电范围在手册里有明确标注,不同批次、不同封装对 3.3V 逻辑的支持程度不一样。常见做法是 MCU 侧用 3.3V,CH376 供电也按数据手册推荐值接,SPI 引脚选 3.3V 兼容的 MCU 端口,不确定时在 SDI/SDO 上串 33Ω 电阻做缓冲。RST 引脚建议直接由 MCU 控制,上电后先拉低保持一段时间再释放,比纯 RC 复位电路更好排查问题。
3.2 上电自检与 USB 主机模式切换
CH376 上电后第一件事不是插设备,而是确认 MCU 和芯片之间的通信链路是通的。芯片提供一条通讯自检命令,往里面写一个字节,它会返回按位取反的结果。这个自检必须放在所有初始化之前,它能区分出「SPI 没通」和「USB 枚举失败」两类问题,省掉大量无意义的排错时间。
// SPI 初始化完成后,先做芯片通讯自检 uint8_t tx_data = 0x55; ch376_cs_low(); ch376_write_cmd(CMD_CHECK_EXIST); // 命令字见手册命令集 ch376_write_data(tx_data); // 写入 0x55 ch376_cs_high(); // 读取芯片返回数据,期望是取反后的 0xAA uint8_t rx_data = ch376_read_data(); if (rx_data != (uint8_t)~tx_data) { // 检查 SPI 接线、时钟极性和相位 while (1); }这段逻辑很直白:自检返回取反值,0x55 对应 0xAA。如果读回来不对,先别急着查 USB 部分,用示波器量 SCS、SCK、SDO 三根线的电平,确认 GPIO 模式和 SPI 模式是否配错。参数上需要注意,命令阶段和数据阶段片选的低有效时序严格按手册来,有的固件批次要求命令字节后插入一个空时钟周期,遇到自检时通时不通的情况优先查这个时序。
自检通过后,把芯片切到 USB 主机模式。芯片支持从机和主机两种模式,操作键鼠必须选主机模式。模式切换命令带一个模式参数,常见写法是写入 0x07 表示带 SOF 输出的主机模式。切换后延时一段时间,再读状态寄存器确认模式切换成功,不要直接进入枚举流程。
// 将 CH376 切到 USB 主机模式 ch376_write_cmd(CMD_SET_USB_MODE); ch376_write_data(0x07); // 主机模式,详见手册模式参数 delay_ms(20); // 等待内部状态稳定 // 读中断状态,确认模式设置完成 uint8_t status = ch376_get_status(); if (status != USB_MODE_HOST_READY) { // 检查供电和复位,不要继续往下走 while (1); }模式切换成功后,芯片会持续监视 USB 总线。这时可以插上键盘或鼠标,芯片检测到设备连接会产生连接中断。收到连接中断后再做总线复位,这是枚举的第一步,不要跳过。
3.3 键鼠枚举的最小命令序列
枚举流程按标准 USB 顺序走,但每个节点的具体操作被 CH376 固件简化了。MCU 要做的就是:等设备插入、总线复位、等复位完成、设置地址、读设备描述符、读配置描述符、解析 HID 接口、设置配置。下面这段是常见的最小序列,函数名封装了具体命令交互,只保留状态机骨架。
// 等待设备插入,超时 500ms if (ch376_wait_interrupt(USB_INT_CONNECT, 500)) { // 1. 总线复位,设备地址回到 0 ch376_bus_reset(); if (!ch376_wait_interrupt(USB_INT_SUCCESS, 500)) { return ERR_BUS_RESET_TIMEOUT; } // 2. 给设备分配地址 1 ch376_set_device_address(1); delay_ms(5); // 3. 读设备描述符前 18 字节 uint8_t dev_desc[18]; int len = ch376_get_descriptor(USB_DESCR_DEVICE, dev_desc, 18); if (len < 18) return ERR_GET_DEV_DESC; // 4. 读配置描述符,解析 HID 接口和中断端点 uint8_t cfg_desc[64]; len = ch376_get_descriptor(USB_DESCR_CONFIG, cfg_desc, sizeof(cfg_desc)); hid_iface_t hid = {0}; if (hid_parse_config(cfg_desc, len, &hid) != 0) { return ERR_HID_IFACE_NOT_FOUND; } // 5. 设置配置,设备进入 Configured 状态 ch376_set_configuration(cfg_desc[7]); // bConfigurationValue ch376_wait_interrupt(USB_INT_SUCCESS, 200); }逻辑说明:总线复位是后续一切的前提,复位时间受设备上电初始化影响,常见坑是等待时间太短,500ms 超时不算夸张。设置地址后要留几个毫秒让设备切换地址,太快发读描述符有可能收不到响应。配置描述符是变长的,前 4 字节里有总长度字段,实际项目里先读前 4 字节拿到总长再读全量,示例中直接用 64 字节缓冲是为了简化演示。
hid_parse_config 要做的核心事情只有两件:找到 bInterfaceClass 为 3 的接口,记住它的 bInterval 和中断 IN 端点地址;再记下 bInterfaceProtocol,键盘为 1、鼠标为 2。这些字段后面轮询和解析都要用。设置配置时用的配置值取配置描述符第 7 字节,也就是 bConfigurationValue,一般设备都是 1,但不要写死,直接读出来更稳。
枚举到这里,设备已经处于可上报数据的状态。接下来就是按端点的轮询间隔去收报表,这部分的坑集中在报表解析和轮询节奏上,下一章详细拆。
4. 把报表变成输入事件:Boot 键盘映射、鼠标相对位移与轮询节奏
4.1 键盘 8 字节 Boot 报表:修饰键、键值表与释放判断
Boot 协议键盘报表固定 8 字节,第一个字节是修饰键位图,第二个字节保留,后六个字节是当前同时按下的按键 Usage ID。Usage ID 和 ASCII 没有直接关系,它是 HID 规范定义的键值,比如 0x04 对应字母 a,0x05 对应字母 b,0x28 对应回车。解析时要先查表,再叠加修饰键。
typedef struct { uint8_t modifier; // bit0:左Ctrl bit1:左Shift bit2:左Alt bit3:左GUI uint8_t reserved; // 保留字节,固定为 0 uint8_t keycodes[6]; // 同时按下的键值,0 表示空槽位 } boot_keyboard_report_t; // 仅列出常用字母键,完整表见 HID Usage Tables static const char keycode_map[] = { 0, 0, 0, 0, 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z' }; uint8_t shift_on = (rpt.modifier & 0x02) || (rpt.modifier & 0x20); for (int i = 0; i < 6; i++) { uint8_t usage = rpt.keycodes[i]; if (usage == 0) continue; char ch = keycode_map[usage]; if (shift_on && ch >= 'a' && ch <= 'z') { ch = ch - 'a' + 'A'; } input_key_event(ch); }逻辑说明:keycodes 数组里同一帧可能有多个键值,表示组合按键。大多数情况下六槽位只有第一个有值,但处理时还是要循环全部槽位。修饰键的 bit0 到 bit7 分别对应左 Ctrl、左 Shift、左 Alt、左 GUI、右 Ctrl、右 Shift、右 Alt、右 GUI,shift 同时包含左右两个 bit,代码里的 0x02 是左 Shift,0x20 是右 Shift。
按键释放的判断是键盘解析最容易出错的地方。USB 键盘不会发「释放事件」,它只在某帧报表里让对应的按键槽位变为 0。如果按键从槽位 1 换到槽位 2,槽位 0 变成 0,但槽位 2 出现了同一个键值,逻辑上仍然是按住状态。常见做法是拿上一帧的 keycodes 和当前帧做比对,只有新出现的 Usage ID 才触发按下事件,消失的触发释放事件。如果简化为「看到非 0 就叫按键」,长按键会重复触发。
键盘键值表是调试时频繁要查的东西,下面列几个最常用的:
| Usage ID | 按键 | Usage ID | 按键 |
|---|---|---|---|
| 0x04 | a | 0x28 | Enter |
| 0x05 | b | 0x29 | Esc |
| 0x1D | z | 0x2A | Backspace |
| 0x1E | 1 | 0x2B | Tab |
| 0x27 | 0 | 0x2C | Space |
| 0x3A | F1 | 0x52 | 上方向键 |
| 0x3B | F2 | 0x51 | 下方向键 |
4.2 鼠标 Boot 报表:int8_t 符号扩展与滚轮方向
标准 Boot 鼠标报表是 3 字节:按键位图、X 位移、Y 位移。带滚轮的鼠标厂商扩展成 4 字节,第四个字节是滚轮增量。这里的位移是相对量,表示「从上一帧到这一帧鼠标动了多少」,不是绝对坐标。解析时要做符号扩展,int8_t 的负值不能当成无符号数累加,否则光标会乱跳。
typedef struct { int8_t buttons; // bit0:左键 bit1:右键 bit2:中键 int8_t x; // X 位移,向右为正 int8_t y; // Y 位移,向下为正 int8_t wheel; // 滚轮,向上为正,可选字段 } boot_mouse_report_t; static int16_t cursor_x = 0; static int16_t cursor_y = 0; void mouse_parse(uint8_t *buf, uint8_t len) { if (len < 3) return; boot_mouse_report_t *rpt = (boot_mouse_report_t *)buf; // 关键:先转 int8_t 再累加,避免符号被无符号化 cursor_x += rpt->x; cursor_y += rpt->y; if (cursor_x < 0) cursor_x = 0; if (cursor_x > 1023) cursor_x = 1023; if (cursor_y < 0) cursor_y = 0; if (cursor_y > 767) cursor_y = 767; if (len >= 4 && rpt->wheel != 0) { // 正数向上滚,负数向下滚 input_scroll_event(rpt->wheel); } }逻辑说明:X 位移向右为正,Y 位移向下为正,这是屏幕坐标系约定。钳位到 0~1023 和 0~767 是为了防止累加器溢出后坐标突然跳变,实际边界按你的显示分辨率改。滚轮字段不是标准 Boot 报表的一部分,很多鼠标只有 3 字节,直接访问 buf[3] 会越界,所以判断 len >= 4 再处理。
初次调试鼠标时建议先把三个字段的原始值打印出来,手动移动鼠标观察数值变化。如果 x 和 y 的符号和预期相反,可能不是报表解析问题,而是设备坐标系与你的屏幕坐标系定义不同,等价于把 x/y 取反。有一类鼠标上报位移值特别大,单帧几百,多半是设备进入了高分辨率模式,需要在枚举后发送厂商特定的特性请求调整,这个只能对着设备的数据手册做。
4.3 端点轮询与单设备限制:键盘鼠标能不能同时用
CH376 是单设备 USB 主机控制器,同一时刻只能和一个物理设备交互。键盘和鼠标是两个独立设备时,不能同时挂在总线上并行读取。想同时使用键鼠,常见方案有两个:用二合一接收器的复合设备,接收器对 CH376 来说是一个物理设备,里面包含键盘和鼠标两个 HID 接口,枚举后按接口分别轮询;或者用 USB HUB 把两个设备合成一个拓扑,但 CH376 对 HUB 的支持有限,切换设备代价高,不建议走这条路。
轮询节奏上,每个中断端点描述符里都有 bInterval 字段,表示设备的轮询间隔。键盘一般是 10ms,鼠标常见是 10ms 或 8ms。MCU 侧常见做法是主循环里按设备精度的 bInterval 整数倍去发起读端点操作,不要比设备上报周期快太多,否则浪费 MCU 时间,也不要比它慢太多,否则快速操作会丢帧。
// 主循环里按节奏发起读端点 void hid_poll(hid_iface_t *iface) { // 向 CH376 发起中断 IN 传输读取 ch376_read_endpoint(iface->ep_in_addr, buf, &len); if (len > 0) { if (iface->protocol == HID_PROTOCOL_KEYBOARD) { parse_keyboard_report(buf); } else if (iface->protocol == HID_PROTOCOL_MOUSE) { mouse_parse(buf, len); } } }逻辑说明:读端点命令发出后,CH376 会代表主机发起 IN 令牌,设备有数据就返回,没数据则 NAK,芯片固件处理重试,MCU 侧拿到的是已经完成的事务结果。注意不要让读端点操作阻塞主循环,等不到数据时要在超时后返回,把 CPU 让给其他任务。参数上,端点号必须用配置描述符里解析出来的实际地址,不能假设是 0x81。
5. 避坑:键鼠接入最容易翻车的 5 个细节
5.1 枚举总是超时:先量 VBUS,再查复位时序
现象:插上键盘后,连接中断能等到,但总线复位后 USB_INT_SUCCESS 永远不来,或者根本等不到连接中断。反复上电偶尔成功一次,成功率极低。
原因:CH376 的 VBUS 输出给设备供电,供电电流不足时键盘内部的 USB 控制器在上电瞬间跌落,设备反复重启,总线复位自然完成不了。另一个常见原因是 RST 引脚复位时序不对,释放太快,芯片内部还没稳定。
解决:把 VBUS 的 5V 供电和系统供电分开,至少保证设备侧能稳定拿到 500mA 级别电流,VBUS 与地之间加 10uF 以上电容。复位时序改成上电后延时 50ms,拉低 RST,再延时 100ms 释放,释放后再等 200ms 才开始 USB 操作。排查时拿示波器量 VBUS 电压在设备插入瞬间的跌落幅度,如果低于 4.5V,先解决供电。
5.2 设备枚举成功但收不到数据:Set_Config 没下发
现象:设备描述符能读出来,地址也设置成功,但轮询端点永远超时,设备就像没接一样。
原因:漏了 CMD_SET_CONFIG 这一步。USB 设备只有收到 Set_Configuration 请求后才进入 Configured 状态,未配置的设备不会在中断 IN 端点上返回任何数据。很多人做完 Set_Address 就急着收报表,卡在设备已经 ready 的假象里。
解决:把设置配置作为枚举的强制步骤,配置值从配置描述符第 7 字节读取,不要写死 1。下发后确认返回成功中断,再进入轮询。更稳妥的做法是设置配置前先发 Set_Idle 请求,部分键盘对 Set_Idle 有依赖,设置空闲时间为 0 表示一直上报状态变化。
5.3 鼠标光标乱飞:把相对位移当绝对坐标
现象:鼠标轻轻一动,光标跳到屏幕边缘;快速移动时方向相反或者数值忽大忽小。
原因:报表里的 x/y 是相对位移,不是坐标值。有的实现直接把位移值赋给当前坐标,导致坐标被不断覆盖和跳变。更隐蔽的是符号扩展错误,int8_t 的 0xFF 表示 -1,如果被当作 uint8_t 累加,就成了 255,光标自然乱跳。
解决:位移必须累加到坐标上,累加变量用 int16_t 或 int32_t,并且钳位到显示范围。读报表时先转成 int8_t 再运算,不能先加再转。调试时把原始字节、转换后的值、累加后的坐标一起打印,能很快定位是哪一层出了问题。
5.4 按键松开后一直重复:没处理空键值报表
现象:按一次键盘,系统收到一串相同字符,直到按另一个键才停。
原因:键盘在按键释放时会上报一帧全零报表,表示所有键位为空。解析代码如果只关心「keycodes 里有非 0 值就触发按键事件」,那么按下、释放、再按下,每帧都当作新按键处理,实际上第二次的按下是重复事件。
解决:维护上一帧的按键状态,新出现才触发按下,消失触发释放,全零报表只做状态清零,不产生任何事件。六个槽位的处理要注意排序问题,同一帧里 keycodes 的顺序没有语义,不能假设槽位稳定对应同一个物理按键,必须用键值本身判断。
5.5 热插拔死机:掉线中断后没有重新枚举
现象:运行中拔掉键鼠再插回,设备再也无法识别,只能复位 MCU 才能恢复。
原因:代码忙等数据时只处理了「收到数据」中断,没有处理设备拔出产生的掉线中断。拔线瞬间 CH376 检测到总线断开,触发断开事件,但主循环还在等端点数据,状态机卡死在轮询里,后续插入事件被忽略。
解决:轮询循环里同时检测掉线事件,收到掉线中断就退回到「等待设备插入」的初始状态,并做一次总线复位。重新枚举前加 300ms 以上延时,防止设备供电还没稳定就再次枚举。如果产品有频繁插拔需求,重枚举次数也要限制,比如连续失败 5 次就退出报错,避免固件陷入死循环。
6. 进阶验证技巧:先把报表打印出来,再谈业务映射
6.1 用 Hex 打印报表打通数据链路
移植键鼠解析之前,先别急着写映射逻辑。把读到的端点数据用十六进制打印出来,手动按键盘、动鼠标,观察哪些字节在变化。这一步能把问题精确隔离到「枚举/轮询环节」还是「报表解析环节」,比对着代码猜高效得多。
void dump_hex(uint8_t *buf, uint8_t len) { for (uint8_t i = 0; i < len; i++) { printf("%02X ", buf[i]); } printf("\r\n"); }比如键盘按一下 a,后六字节里应该出现 0x04;松开后整帧变成 00 00 00 00 00 00 00 00。鼠标左键点一下,第一字节出现 0x01;向右移动,第二字节出现正值。打印几帧之后,设备的报表长度、数据含义、滚轮是否存在全部一目了然。我习惯把 dump 开关用编译宏控制,调试完直接关掉,不影响发布代码。
6.2 用一张映射表把键值变成业务动作
报表解析稳定之后,业务层不要散落一堆 if-else,用查表结构把按键映射到业务动作,后续增加按键只用改表。
typedef struct { uint8_t usage_id; uint8_t modifier_mask; void (*action)(void); } key_action_t; static const key_action_t key_actions[] = { { 0x3A, 0x00, action_open_menu }, // F1 { 0x3B, 0x00, action_switch_page }, // F2 { 0x52, 0x00, action_move_up }, // 上方向键 { 0x28, 0x02, action_shift_enter }, // Shift + Enter }; void handle_key_press(uint8_t usage, uint8_t modifier) { for (uint8_t i = 0; i < sizeof(key_actions)/sizeof(key_actions[0]); i++) { if (key_actions[i].usage_id == usage && key_actions[i].modifier_mask == modifier) { key_actions[i].action(); return; } } }映射表匹配时把修饰键也纳入条件,Shift+F1 和 F1 可以分到不同动作。没有匹配的键值默认忽略,业务侧不会因为未知按键崩溃。鼠标的映射方式类似,比如把滚轮事件映射成音量加减、把中键映射成某个快捷键。
我在一个模拟项目里就吃过报表长度的亏:想当然认为鼠标 Boot 报表一定是 4 字节带滚轮,结果打印出来只有 3 字节,滚轮另有高分辨率接口。那之后养成的习惯是,任何新设备接入,先 dump 报表再写解析,这个习惯帮我避开了不少玄学问题。希望帮到你。
本文还有配套的精品资源,点击获取