USB HID键盘实现详解:从硬件扫描到USB通信的完整解决方案
前言
USB HID(Human Interface Device)是USB设备中最常见的设备类型之一,键盘、鼠标、游戏手柄等都属于HID设备。本文将详细介绍如何基于CH559单片机实现一个完整的USB HID键盘,包括按键扫描、USB描述符配置、HID报告描述符设计等核心技术。
1. 项目架构概述
1.1 系统整体架构
┌─────────────────┐    ┌─────────────────┐    ┌─────────────────┐
│   按键矩阵扫描   │───▶│   按键处理逻辑   │───▶│   USB HID通信   │
│   (KEY_Scan)    │    │ (Key_Process)   │    │    (USB)       │
└─────────────────┘    └─────────────────┘    └─────────────────┘
         │                       │                       │
         ▼                       ▼                       ▼
   硬件按键检测              按键码转换              发送到主机1.2 核心文件结构
- USB.H/USB.C: USB协议栈实现,包含设备描述符、配置描述符等
- KEY_Scan.H: 按键扫描相关定义
- Key_Process.H: 按键处理逻辑
- USB_Keyboard.H: USB键盘特定功能
2. USB描述符详解
2.1 设备描述符(Device Descriptor)
设备描述符是USB设备的"身份证",告诉主机这是什么设备。
            
            
              c
              
              
            
          
          // 设备描述符 - 18字节固定长度
UINT8C DevDesc[18] = 
{
    0x12,       // bLength: 描述符长度为18字节
    0x01,       // bDescriptorType: 设备描述符类型
    0x00, 0x02, // bcdUSB: USB协议版本2.0
    0x00,       // bDeviceClass: 在接口描述符中定义设备类
    0x00,       // bDeviceSubClass: 在接口描述符中定义子类
    0x00,       // bDeviceProtocol: 在接口描述符中定义协议
    0x08,       // bMaxPacketSize0: 端点0最大包大小8字节
    0x89, 0x11, // idVendor: 厂商ID(自定义)
    0x90, 0x88, // idProduct: 产品ID(自定义)
    0x00, 0x01, // bcdDevice: 产品版本号
    0x01,       // iManufacturer: 厂商字符串索引
    0x02,       // iProduct: 产品字符串索引  
    0x03,       // iSerialNumber: 序列号字符串索引
    0x01        // bNumConfigurations: 配置数量为1
};关键参数说明:
- 厂商ID和产品ID: 用于系统识别设备,开发测试可使用自定义值
- 端点0包大小: 控制传输的基础,通常为8、16、32或64字节
- 字符串索引: 指向设备名称、厂商等描述信息
2.2 配置描述符(Configuration Descriptor)
配置描述符定义了设备的功能配置,包含接口描述符、HID描述符和端点描述符。
            
            
              c
              
              
            
          
          // 配置描述符 - 总长度66字节
UINT8C CfgDesc[66] =
{
    // 配置描述符头部 (9字节)
    0x09,       // bLength: 配置描述符长度
    0x02,       // bDescriptorType: 配置描述符类型
    0x42, 0x00, // wTotalLength: 整个配置描述符总长度66字节
    0x02,       // bNumInterfaces: 接口数量为2个
    0x01,       // bConfigurationValue: 配置值标识
    0x00,       // iConfiguration: 配置字符串索引
    0xA0,       // bmAttributes: 总线供电,支持远程唤醒
    0x32,       // bMaxPower: 最大功耗100mA (0x32*2mA)
    
    // 接口描述符 - 键盘接口 (9字节)
    0x09,       // bLength: 接口描述符长度
    0x04,       // bDescriptorType: 接口描述符类型
    0x00,       // bInterfaceNumber: 接口编号0
    0x00,       // bAlternateSetting: 备用接口编号
    0x01,       // bNumEndpoints: 端点数量1个
    0x03,       // bInterfaceClass: HID设备类
    0x01,       // bInterfaceSubClass: 启动接口子类
    0x01,       // bInterfaceProtocol: 键盘协议
    0x00,       // iInterface: 接口字符串索引
    
    // HID描述符 (9字节)
    0x09,       // bLength: HID描述符长度
    0x21,       // bDescriptorType: HID描述符类型
    0x00, 0x01, // bcdHID: HID版本1.0
    0x21,       // bCountryCode: 国家代码(中国)
    0x01,       // bNumDescriptors: 报告描述符数量
    0x22,       // bDescriptorType: 报告描述符类型
    sizeof(KeyRepDesc), 0x00, // wDescriptorLength: 报告描述符长度
    
    // 端点描述符 - 中断输入端点 (7字节)
    0x07,       // bLength: 端点描述符长度
    0x05,       // bDescriptorType: 端点描述符类型
    0x81,       // bEndpointAddress: 端点1,输入方向
    0x03,       // bmAttributes: 中断传输类型
    0x40, 0x00, // wMaxPacketSize: 最大包大小64字节
    0x0A,       // bInterval: 轮询间隔10ms
    
    // 第二个接口描述符 - 自定义HID接口
    // ... (类似结构,用于扩展功能)
};配置要点:
- 接口数量: 键盘通常需要1个接口,扩展功能可增加接口
- 端点配置: 键盘至少需要1个中断输入端点
- 轮询间隔: 10ms是键盘的标准轮询间隔
3. HID报告描述符深度解析
3.1 键盘报告描述符结构
HID报告描述符定义了设备发送给主机的数据格式,是HID设备的核心。
            
            
              c
              
              
            
          
          // 键盘HID报告描述符 - 65字节
UINT8C KeyRepDesc[65] =
{
    // 全局项:选择通用桌面页面
    0x05, 0x01, // USAGE_PAGE (Generic Desktop)
    
    // 局部项:指定为键盘设备
    0x09, 0x06, // USAGE (Keyboard)
    
    // 主要项:开始应用集合
    0xa1, 0x01, // COLLECTION (Application)
    
    // 修饰键部分(Ctrl、Alt、Shift等)
    0x05, 0x07, //     USAGE_PAGE (Keyboard/Keypad)
    0x19, 0xe0, //     USAGE_MINIMUM (Left Control)
    0x29, 0xe7, //     USAGE_MAXIMUM (Right GUI)
    0x15, 0x00, //     LOGICAL_MINIMUM (0)
    0x25, 0x01, //     LOGICAL_MAXIMUM (1)
    0x95, 0x08, //     REPORT_COUNT (8)
    0x75, 0x01, //     REPORT_SIZE (1)
    0x81, 0x02, //     INPUT (Data,Var,Abs)
    
    // 保留字节
    0x95, 0x01, //     REPORT_COUNT (1)
    0x75, 0x08, //     REPORT_SIZE (8)
    0x81, 0x03, //     INPUT (Const,Var,Abs)
    
    // 普通按键部分(6个按键码)
    0x95, 0x06, //   REPORT_COUNT (6)
    0x75, 0x08, //   REPORT_SIZE (8)
    0x15, 0x00, //   LOGICAL_MINIMUM (0)
    0x25, 0xFF, //   LOGICAL_MAXIMUM (255)
    0x05, 0x07, //   USAGE_PAGE (Keyboard/Keypad)
    0x19, 0x00, //   USAGE_MINIMUM (No Event)
    0x29, 0x65, //   USAGE_MAXIMUM (Keyboard Application)
    0x81, 0x00, //     INPUT (Data,Ary,Abs)
    
    // LED输出部分(Num Lock、Caps Lock等)
    0x25, 0x01, //     LOGICAL_MAXIMUM (1)
    0x95, 0x05, //   REPORT_COUNT (5)
    0x75, 0x01, //   REPORT_SIZE (1)
    0x05, 0x08, //   USAGE_PAGE (LEDs)
    0x19, 0x01, //   USAGE_MINIMUM (Num Lock)
    0x29, 0x05, //   USAGE_MAXIMUM (Kana)
    0x91, 0x02, //   OUTPUT (Data,Var,Abs)
    
    // LED填充位
    0x95, 0x01, //   REPORT_COUNT (1)
    0x75, 0x03, //   REPORT_SIZE (3)
    0x91, 0x03, //   OUTPUT (Const,Var,Abs)
    
    0xc0,        // END_COLLECTION
};3.2 键盘数据包格式
根据报告描述符,键盘发送的数据包格式为8字节:
字节0: 修饰键状态 (Ctrl/Alt/Shift等)
字节1: 保留字节 (固定为0)
字节2-7: 普通按键码 (最多6个同时按下的键)修饰键位定义:
            
            
              c
              
              
            
          
          // 修饰键位掩码定义
#define KEY_LEFT_CTRL   0x01  // 左Ctrl键
#define KEY_LEFT_SHIFT  0x02  // 左Shift键  
#define KEY_LEFT_ALT    0x04  // 左Alt键
#define KEY_LEFT_GUI    0x08  // 左Windows键
#define KEY_RIGHT_CTRL  0x10  // 右Ctrl键
#define KEY_RIGHT_SHIFT 0x20  // 右Shift键
#define KEY_RIGHT_ALT   0x40  // 右Alt键
#define KEY_RIGHT_GUI   0x80  // 右Windows键4. 按键扫描系统设计
4.1 按键矩阵扫描原理
键盘通常采用矩阵扫描方式,通过行列扫描减少IO口使用。
            
            
              c
              
              
            
          
          // 按键扫描相关定义
extern xdata UINT8 g_KEY_keydata[6][20]; // 6行20列按键矩阵
// 按键扫描服务函数
void KEY_TimerPlus_Service(void);  // 定时器扫描服务
void KEY_Init_Service(void);       // 按键初始化
void KEY_Run_Service(void);        // 按键运行服务扫描流程:
- 行扫描: 依次将每行设为低电平,其他行设为高电平
- 列检测: 读取所有列的电平状态
- 按键判断: 低电平表示该位置有按键按下
- 消抖处理: 通过多次采样确认按键状态
- 状态更新: 更新按键状态矩阵
4.2 按键处理逻辑
            
            
              c
              
              
            
          
          // 按键处理相关定义
extern xdata UINT8 SendHIDKey[8];    // 发送HID按键缓冲区
extern xdata UINT8 ReleaseHIDKey[8]; // 释放按键缓冲区
extern UINT8 HIDKey[6][20][9];       // HID按键映射表
// 按键处理函数
void Init_HIDKey();                   // 初始化HID按键映射
void Key_Send_DataMode(UINT8 row, UINT8 col); // 发送按键数据处理步骤:
- 按键映射: 将物理按键位置映射为HID键码
- 组合键处理: 检测Ctrl、Alt、Shift等修饰键
- 数据包构建: 按照HID格式构建8字节数据包
- USB发送: 通过中断端点发送给主机
5. USB通信实现
5.1 端点缓冲区配置
            
            
              c
              
              
            
          
          // USB端点缓冲区定义(指定内存地址)
UINT8X Ep0Buffer[THIS_ENDP0_SIZE] _at_ 0x0000; // 端点0缓冲区
UINT8X Ep1Buffer[MAX_PACKET_SIZE] _at_ 0x0008; // 端点1输入缓冲区  
UINT8X Ep2Buffer[2*MAX_PACKET_SIZE] _at_ 0x0050; // 端点2输出缓冲区
// USB状态变量
UINT8 SetupReq, SetupLen, Ready, Count, FLAGUpdata, UsbConfig;
PUINT8 pDescr;                    // USB描述符指针
USB_SETUP_REQ SetupReqBuf;        // Setup请求缓冲区5.2 USB字符串描述符
            
            
              c
              
              
            
          
          // 语言描述符
UINT8C MyLangDescr[4] = { 0x04, 0x03, 0x09, 0x04 };
// 厂商信息字符串
UINT8C MyManuInfo[18] = { 
    0x12, 0x03, 
    'B', 0, 'i', 0, 'l', 0, 'i', 0, 
    'B', 0, 'i', 0, 'l', 0, 'i', 0 
};
// 产品信息字符串  
UINT8C MyProdInfo[12] = { 
    0x0C, 0x03, 
    'h', 0, 'o', 0, 'o', 0, 'p', 0, '0', 0 
};
// 产品版本信息字符串
UINT8C MyProductIDInfo[14] = {
    0x0E, 0x03,
    'k', 0, 'e', 0, 'y', 0, '5', 0, '5', 0, '9', 0
};字符串格式说明:
- 第1字节:字符串长度
- 第2字节:描述符类型(0x03)
- 后续字节:Unicode编码的字符串内容
6. 实际应用示例
6.1 发送按键数据
            
            
              c
              
              
            
          
          // 发送HID按键数据到主机
void enp1IntIn(PUINT8 HIDKeyBuf) 
{
    // 将按键数据复制到端点1缓冲区
    memcpy(Ep1Buffer, HIDKeyBuf, 8);
    
    // 设置端点1为准备发送状态
    UEP1_T_LEN = 8;           // 设置发送长度为8字节
    UEP1_CTRL = UEP1_CTRL & ~MASK_UEP_T_RES | UEP_T_RES_ACK;
}6.2 按键扫描示例
            
            
              c
              
              
            
          
          // 按键扫描主循环
void KEY_Run_Service(void)
{
    static UINT8 scan_row = 0;
    
    // 设置当前扫描行为低电平
    set_row_low(scan_row);
    
    // 延时稳定
    mDelayuS(10);
    
    // 读取所有列状态
    for(UINT8 col = 0; col < 20; col++) {
        if(read_col_status(col) == 0) {
            // 检测到按键按下
            if(g_KEY_keydata[scan_row][col] == 0) {
                // 新按键按下,开始消抖
                g_KEY_keydata[scan_row][col] = 1;
            } else if(g_KEY_keydata[scan_row][col] < 255) {
                // 按键持续按下,增加计数
                g_KEY_keydata[scan_row][col]++;
                
                // 消抖完成,发送按键
                if(g_KEY_keydata[scan_row][col] == 3) {
                    Key_Send_DataMode(scan_row, col);
                }
            }
        } else {
            // 按键释放
            if(g_KEY_keydata[scan_row][col] > 0) {
                g_KEY_keydata[scan_row][col] = 0;
                // 发送按键释放
                Key_Release_DataMode(scan_row, col);
            }
        }
    }
    
    // 切换到下一行
    scan_row = (scan_row + 1) % 6;
}7. 调试与优化建议
7.1 常见问题排查
- 
设备无法识别 - 检查设备描述符中的VID/PID
- 确认USB时钟配置正确
- 验证端点0响应是否正常
 
- 
按键无响应 - 检查HID报告描述符格式
- 确认按键扫描时序
- 验证端点1数据发送
 
- 
按键重复或丢失 - 调整消抖时间参数
- 检查扫描频率设置
- 优化中断处理时序
 
7.2 性能优化
- 
扫描频率优化 - 键盘扫描频率建议1000Hz
- USB报告频率建议125Hz
- 平衡响应速度与功耗
 
- 
内存使用优化 - 合理分配端点缓冲区
- 优化按键状态存储
- 减少不必要的数据复制
 
8. 扩展功能实现
8.1 多媒体键支持
            
            
              c
              
              
            
          
          // 多媒体键HID报告描述符
UINT8C MediaKeyRepDesc[] = {
    0x05, 0x0C,        // Usage Page (Consumer)
    0x09, 0x01,        // Usage (Consumer Control)
    0xA1, 0x01,        // Collection (Application)
    0x19, 0x00,        //   Usage Minimum (0)
    0x2A, 0x3C, 0x02,  //   Usage Maximum (572)
    0x15, 0x00,        //   Logical Minimum (0)
    0x26, 0x3C, 0x02,  //   Logical Maximum (572)
    0x95, 0x01,        //   Report Count (1)
    0x75, 0x10,        //   Report Size (16)
    0x81, 0x00,        //   Input (Data,Array,Abs)
    0xC0,              // End Collection
};8.2 自定义功能键
通过第二个HID接口实现自定义功能:
            
            
              c
              
              
            
          
          // 自定义HID报告描述符(用于宏功能、RGB控制等)
UINT8C HIDRepDesc[33] = {
    0x05, 0x01, // USAGE_PAGE (Generic Desktop)     
    0x09, 0x00, // USAGE (Undefined) - 自定义用途
    0xa1, 0x01, // COLLECTION (Application)
    
    // 输入报告:8字节自定义数据
    0x15, 0x00, // LOGICAL_MINIMUM (0)
    0x25, 0xff, // LOGICAL_MAXIMUM (255)
    0x19, 0x01, // USAGE_MINIMUM (1)
    0x29, 0x08, // USAGE_MAXIMUM (8)
    0x95, 0x08, // REPORT_COUNT (8)
    0x75, 0x08, // REPORT_SIZE (8)
    0x81, 0x02, // INPUT (Data,Var,Abs)
    
    // 输出报告:64字节配置数据
    0x09, 0x02, // USAGE (2)
    0x15, 0x00, // LOGICAL_MINIMUM (0)
    0x25, 0xff, // LOGICAL_MAXIMUM (255)
    0x75, 0x08, // REPORT_SIZE (8)
    0x95, 0x40, // REPORT_COUNT (64)
    0x91, 0x06, // OUTPUT (Data,Var,Abs,Wrap)
    
    0xc0        // END_COLLECTION
};9. 总结
本文详细介绍了USB HID键盘的完整实现方案,涵盖了从硬件按键扫描到USB通信的各个环节。关键技术点包括:
- USB描述符设计: 正确配置设备、配置、接口、HID和端点描述符
- HID报告格式: 理解并实现标准键盘HID报告描述符
- 按键扫描算法: 实现高效的矩阵扫描和消抖处理
- USB通信协议: 掌握HID设备的数据传输机制
通过本文的学习,读者可以完全理解USB HID键盘的工作原理,并能够独立开发出功能完整的USB键盘产品。在实际项目中,还可以根据需求扩展多媒体键、宏功能、RGB灯效等高级特性。
开发建议:
- 先实现基础功能,再逐步添加扩展特性
- 重视USB协议的标准化,确保兼容性
- 充分测试各种使用场景,保证产品稳定性
- 关注功耗优化,特别是无线键盘应用
希望本文能够帮助大家深入理解USB HID技术,在嵌入式开发道路上更进一步!