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引脚与地线(按下为低电平)。

测试步骤:

  1. 将固件烧录至设备,插入主机USB口;
  2. 在主机设备管理器中确认枚举为“USB输入设备”或“HID键盘设备”,VID/PID与描述符一致;
  3. 同时按下Ctrl、Alt、Del三个按键,观察主机是否弹出“安全桌面”或任务管理器(Windows)或对应系统功能;
  4. 单独按下Ctrl+A组合,在文本编辑器中验证是否触发全选;
  5. 释放所有按键后,确认无按键粘连(无重复触发)。

判定标准:组合键功能与单键功能均正常响应,释放后无卡键;使用USB分析仪(如Wireshark+USBPcap)抓包可见报告数据:Ctrl+Alt+Del时第0字节为0x05(Ctrl+Alt),第2字节为0x4C(Delete);释放后收到全零报告。

HID键盘组合键上报时序与数据流 物理按键 Ctrl (GPIO0) Alt (GPIO1) Del (GPIO2) 按下为低电平 MCU固件 keyboard_scan() 检测边沿触发 构造8字节报告 修饰符+按键码 USB端点1 IN 中断传输 间隔10ms 最大包长8字节 报告速率100Hz 主机 HID 解析 执行 8字节键盘报告格式 Byte 0 修饰符 Byte 1 保留 Byte 2 按键1 Byte 3 按键2 Byte 4 按键3 Byte 5 按键4 Byte 6 按键5 Byte 7 按键6 组合键报告示例 Ctrl+Alt+Del: 0x05 0x00 0x4C (Del) 其余字节为0x00 释放按键: 0x00 0x00 0x00 全零报告

关键实现说明

本案例的核心在于:组合键并非特殊报文类型,而是修饰符位与普通按键码在同一报告中的组合。主机端HID驱动根据修饰符位(Byte 0)与按键码(Byte 2~7)自动识别组合键。发送组合键后必须发送全零空报告以表示按键释放,否则主机将认为按键持续按住。

上述代码中send_combo_key()函数接受修饰符与按键码数组,可灵活构造任意组合键。实际产品中建议将按键扫描与报告发送解耦,使用队列缓存按键事件,避免在中断中直接发送USB报告。