4. USB HID多按键组合
任务说明
本案例实现一个USB HID键盘设备,支持标准按键与组合键(如Ctrl+Alt+Del)上报。设备枚举为键盘类(bInterfaceClass=0x03, bInterfaceSubClass=0x01, bInterfaceProtocol=0x01),通过端点1 IN以中断传输方式上报8字节键盘报告。组合键通过同时置位修饰符位与按键码实现,由主机端负责解析。
HID报告描述符
键盘报告格式为8字节:第0字节为修饰符位(Bit0=Left Ctrl, Bit1=Left Shift, Bit2=Left Alt, Bit3=Left GUI),第1字节保留,第2~7字节为同时按下的按键码(无按键时为0)。
const uint8_t hid_report_desc[] = {
0x05, 0x01, // Usage Page (Generic Desktop)
0x09, 0x06, // Usage (Keyboard)
0xA1, 0x01, // Collection (Application)
0x05, 0x07, // Usage Page (Key Codes)
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) - Modifier byte
0x95, 0x01, // Report Count (1)
0x75, 0x08, // Report Size (8)
0x81, 0x01, // Input (Const) - Reserved byte
0x95, 0x06, // Report Count (6)
0x75, 0x08, // Report Size (8)
0x15, 0x00, // Logical Minimum (0)
0x25, 0x65, // Logical Maximum (101)
0x05, 0x07, // Usage Page (Key Codes)
0x19, 0x00, // Usage Minimum (0)
0x29, 0x65, // Usage Maximum (101)
0x81, 0x00, // Input (Data, Array) - Key codes
0xC0 // End Collection
};
设备配置与端点描述符
// 配置描述符(含接口与端点)
const uint8_t config_desc[] = {
0x09, 0x02, 0x22, 0x00, 0x01, 0x01, 0x00, 0x80, 0x32, // Config
0x09, 0x04, 0x00, 0x00, 0x01, 0x03, 0x01, 0x01, 0x00, // Interface: HID Keyboard
0x09, 0x21, 0x10, 0x01, 0x00, 0x01, 0x22, 0x3F, 0x00, // HID descriptor
0x07, 0x05, 0x81, 0x03, 0x08, 0x00, 0x0A // EP1 IN, Interrupt, 8 bytes, 10ms
};
组合键上报逻辑
以下代码演示如何构造并发送Ctrl+Alt+Del组合键报告。按键码定义遵循USB HID Usage Table(0x04=键盘A,0x2C=空格,0x4C=Delete,0x53=Keypad Enter等)。
#include <stdint.h>
// 修饰符位定义
#define MOD_LCTRL 0x01
#define MOD_LSHIFT 0x02
#define MOD_LALT 0x04
#define MOD_LGUI 0x08
// 按键码(USB HID Usage ID)
#define KEY_DELETE 0x4C
#define KEY_A 0x04
#define KEY_SPACE 0x2C
// 8字节键盘报告结构
typedef struct {
uint8_t modifier;
uint8_t reserved;
uint8_t keycodes[6];
} keyboard_report_t;
// 发送报告到端点1 IN(由具体USB硬件层实现)
extern void usb_send_report(const uint8_t* data, uint8_t len);
// 发送组合键报告
void send_combo_key(uint8_t modifier, const uint8_t* keys, uint8_t key_count) {
keyboard_report_t report = {0};
report.modifier = modifier;
for (uint8_t i = 0; i < key_count && i < 6; i++) {
report.keycodes[i] = keys[i];
}
usb_send_report((uint8_t*)&report, sizeof(report));
}
// 发送Ctrl+Alt+Del
void send_ctrl_alt_del(void) {
uint8_t keys[] = {KEY_DELETE};
send_combo_key(MOD_LCTRL | MOD_LALT, keys, 1);
}
// 发送Ctrl+A(全选)
void send_ctrl_a(void) {
uint8_t keys[] = {KEY_A};
send_combo_key(MOD_LCTRL, keys, 1);
}
// 发送Ctrl+Space(输入法切换)
void send_ctrl_space(void) {
uint8_t keys[] = {KEY_SPACE};
send_combo_key(MOD_LCTRL, keys, 1);
}
// 释放所有按键(必须发送空报告)
void release_all_keys(void) {
keyboard_report_t report = {0};
usb_send_report((uint8_t*)&report, sizeof(report));
}
按键状态机与防抖处理
实际应用中需检测物理按键的按下与释放。以下为基于GPIO扫描的简化状态机,支持同时检测多个按键并生成对应报告。
#define KEY_CTRL_PIN 0 // 假设GPIO引脚编号
#define KEY_ALT_PIN 1
#define KEY_DEL_PIN 2
// 读取引脚电平(由硬件层提供)
extern uint8_t gpio_read(uint8_t pin);
// 按键状态记录
static uint8_t prev_ctrl = 0, prev_alt = 0, prev_del = 0;
void keyboard_scan(void) {
uint8_t cur_ctrl = gpio_read(KEY_CTRL_PIN);
uint8_t cur_alt = gpio_read(KEY_ALT_PIN);
uint8_t cur_del = gpio_read(KEY_DEL_PIN);
// 检测到组合键按下(Ctrl+Alt+Del同时为低电平)
if (cur_ctrl == 0 && cur_alt == 0 && cur_del == 0) {
if (!(prev_ctrl == 0 && prev_alt == 0 && prev_del == 0)) {
send_ctrl_alt_del(); // 边沿触发,只发送一次
}
}
// 检测到任意键释放,发送空报告
else if (prev_ctrl == 0 || prev_alt == 0 || prev_del == 0) {
release_all_keys();
}
prev_ctrl = cur_ctrl;
prev_alt = cur_alt;
prev_del = cur_del;
}
主循环与USB事件处理
int main(void) {
// 初始化USB设备(由硬件层提供)
usb_init();
while (1) {
keyboard_scan(); // 扫描按键并发送报告
usb_poll(); // 处理USB中断与主机请求
}
}
验证方法
硬件连接:将MCU的USB D+/D-引脚连接至主机USB口,按键分别接至GPIO引脚与地线(按下为低电平)。
测试步骤:
- 将固件烧录至设备,插入主机USB口;
- 在主机设备管理器中确认枚举为“USB输入设备”或“HID键盘设备”,VID/PID与描述符一致;
- 同时按下Ctrl、Alt、Del三个按键,观察主机是否弹出“安全桌面”或任务管理器(Windows)或对应系统功能;
- 单独按下Ctrl+A组合,在文本编辑器中验证是否触发全选;
- 释放所有按键后,确认无按键粘连(无重复触发)。
判定标准:组合键功能与单键功能均正常响应,释放后无卡键;使用USB分析仪(如Wireshark+USBPcap)抓包可见报告数据:Ctrl+Alt+Del时第0字节为0x05(Ctrl+Alt),第2字节为0x4C(Delete);释放后收到全零报告。
关键实现说明
本案例的核心在于:组合键并非特殊报文类型,而是修饰符位与普通按键码在同一报告中的组合。主机端HID驱动根据修饰符位(Byte 0)与按键码(Byte 2~7)自动识别组合键。发送组合键后必须发送全零空报告以表示按键释放,否则主机将认为按键持续按住。
上述代码中send_combo_key()函数接受修饰符与按键码数组,可灵活构造任意组合键。实际产品中建议将按键扫描与报告发送解耦,使用队列缓存按键事件,避免在中断中直接发送USB报告。