从零实战:手把手教你编写USB键盘驱动

张开发
2026/4/19 15:26:41 15 分钟阅读

分享文章

从零实战:手把手教你编写USB键盘驱动
1. USB键盘驱动开发基础要开发一个USB键盘驱动首先需要理解USB HIDHuman Interface Device类设备的工作原理。USB键盘属于HID设备的一种它通过中断传输方式与主机通信。当你在键盘上按下或释放按键时键盘会通过USB接口向主机发送报告描述符格式的数据。USB HID设备的核心是HID报告描述符它定义了设备如何组织和使用数据。对于键盘来说报告描述符定义了哪些字节表示按键状态哪些位表示特殊功能键如Ctrl、Alt等。在Linux内核中USB HID子系统已经为我们处理了大部分底层细节但我们仍然需要了解这些概念。开发环境准备Linux内核源码建议使用3.x或更高版本开发板或PC机作为测试平台USB键盘设备交叉编译工具链如果是嵌入式开发2. USB设备识别与端点配置当USB键盘插入系统时USB核心会执行以下步骤设备枚举USB核心读取设备描述符、配置描述符等基本信息驱动匹配根据设备信息寻找合适的驱动程序端点配置确定数据传输使用的端点及其属性对于键盘这类HID设备通常使用中断传输端点。我们可以通过以下代码查看设备的端点信息static int usb_keyboard_probe(struct usb_interface *interface, const struct usb_device_id *id) { struct usb_device *dev interface_to_usbdev(interface); struct usb_host_interface *host_iface interface-cur_altsetting; struct usb_endpoint_descriptor *endpoint; int i; printk(KERN_INFO USB Keyboard detected\n); /* 遍历所有端点 */ for (i 0; i host_iface-desc.bNumEndpoints; i) { endpoint host_iface-endpoint[i].desc; if (usb_endpoint_is_int_in(endpoint)) { printk(KERN_INFO Interrupt IN endpoint found (addr 0x%02x, interval %d ms, max packet %d)\n, endpoint-bEndpointAddress, endpoint-bInterval, endpoint-wMaxPacketSize); } } return 0; }这段代码会打印出键盘设备的中断输入端点信息包括端点地址、轮询间隔和最大包大小。这些信息对后续的数据传输至关重要。3. HID报告描述符解析HID报告描述符是USB键盘驱动的关键部分它定义了设备如何向主机报告其状态。虽然Linux内核已经提供了HID解析功能但了解其工作原理有助于调试和定制驱动。一个典型的键盘报告描述符可能如下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 (Cnst,Arr,Abs) ; Reserved byte 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) ; LED report 0x95, 0x01, // Report Count (1) 0x75, 0x03, // Report Size (3) 0x91, 0x01, // Output (Cnst,Arr,Abs) ; LED report padding 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,Arr,Abs) ; Key array 0xC0 // End Collection在驱动中我们可以使用hid_parse_report()函数来解析这个描述符static int parse_hid_report(struct hid_device *hdev) { struct usb_interface *intf to_usb_interface(hdev-dev.parent); struct usb_device *dev interface_to_usbdev(intf); unsigned char *report_desc; int ret, size; /* 获取报告描述符长度 */ size le16_to_cpu(dev-config-desc.wTotalLength); report_desc kmalloc(size, GFP_KERNEL); if (!report_desc) return -ENOMEM; /* 读取报告描述符 */ ret usb_control_msg(dev, usb_rcvctrlpipe(dev, 0), USB_REQ_GET_DESCRIPTOR, USB_DIR_IN, (USB_DT_REPORT 8), 0, report_desc, size, USB_CTRL_GET_TIMEOUT); if (ret 0) { kfree(report_desc); return ret; } /* 解析报告描述符 */ ret hid_parse_report(hdev, report_desc, size); kfree(report_desc); return ret; }4. 中断传输与按键事件处理USB键盘使用中断传输来报告按键事件。在Linux中我们可以使用URBUSB Request Block来处理这些中断传输。以下是设置URB的示例代码static void usb_keyboard_irq(struct urb *urb) { struct usb_keyboard *keyboard urb-context; unsigned char *data keyboard-data; int status; switch (urb-status) { case 0: /* 成功 */ /* 处理按键数据 */ process_key_events(keyboard, data); break; case -ECONNRESET: /* 未连接 */ case -ENOENT: case -ESHUTDOWN: return; default: /* 错误 */ goto resubmit; } resubmit: /* 重新提交URB以继续接收数据 */ usb_fill_int_urb(urb, keyboard-udev, usb_rcvintpipe(keyboard-udev, keyboard-endpoint-bEndpointAddress), data, keyboard-endpoint-wMaxPacketSize, usb_keyboard_irq, keyboard, keyboard-endpoint-bInterval); status usb_submit_urb(urb, GFP_ATOMIC); if (status) dev_err(keyboard-udev-dev, cant resubmit intr, %s-%s/input0, status %d\n, keyboard-udev-bus-bus_name, keyboard-udev-devpath, status); } static void process_key_events(struct usb_keyboard *keyboard, unsigned char *data) { int i; unsigned int keycode; /* 第一个字节是修饰键状态Ctrl, Shift等 */ unsigned char modifiers data[0]; /* 后续字节是普通按键 */ for (i 2; i 8; i) { if (data[i] 0) continue; keycode usb_kbd_keycode[data[i]]; /* 上报按键事件 */ input_report_key(keyboard-input, keycode, 1); input_sync(keyboard-input); /* 模拟释放事件 */ input_report_key(keyboard-input, keycode, 0); input_sync(keyboard-input); } }5. 与Linux输入子系统集成要将键盘事件传递到用户空间我们需要将驱动与Linux输入子系统集成。以下是设置输入设备的代码static int setup_input_device(struct usb_keyboard *keyboard) { struct input_dev *input; int i, error; input input_allocate_device(); if (!input) return -ENOMEM; keyboard-input input; input-name USB Keyboard; input-phys keyboard-phys; input-dev.parent keyboard-udev-dev; input-id.bustype BUS_USB; input-id.vendor le16_to_cpu(keyboard-udev-descriptor.idVendor); input-id.product le16_to_cpu(keyboard-udev-descriptor.idProduct); input-id.version le16_to_cpu(keyboard-udev-descriptor.bcdDevice); /* 设置支持的事件类型 */ set_bit(EV_KEY, input-evbit); set_bit(EV_REP, input-evbit); /* 设置支持的按键 */ for (i 0; i 256; i) set_bit(usb_kbd_keycode[i], input-keybit); clear_bit(0, input-keybit); /* 注册输入设备 */ error input_register_device(input); if (error) { input_free_device(input); keyboard-input NULL; return error; } return 0; }6. 完整的USB键盘驱动实现结合以上各部分我们可以构建一个完整的USB键盘驱动框架#include linux/kernel.h #include linux/module.h #include linux/usb.h #include linux/hid.h #include linux/input.h #define DRIVER_AUTHOR Your Name your.emailexample.com #define DRIVER_DESC USB Keyboard Driver static struct usb_device_id usb_kbd_id_table[] { { USB_INTERFACE_INFO(USB_INTERFACE_CLASS_HID, USB_INTERFACE_SUBCLASS_BOOT, USB_INTERFACE_PROTOCOL_KEYBOARD) }, { } /* Terminating entry */ }; MODULE_DEVICE_TABLE(usb, usb_kbd_id_table); struct usb_keyboard { struct usb_device *udev; struct input_dev *input; struct urb *irq; unsigned char *data; dma_addr_t data_dma; struct usb_endpoint_descriptor *endpoint; char phys[64]; }; static void usb_kbd_irq(struct urb *urb) { /* 实现如前所述 */ } static int usb_kbd_open(struct input_dev *dev) { struct usb_keyboard *keyboard input_get_drvdata(dev); int error; /* 提交URB开始接收数据 */ error usb_submit_urb(keyboard-irq, GFP_KERNEL); if (error) return error; return 0; } static void usb_kbd_close(struct input_dev *dev) { struct usb_keyboard *keyboard input_get_drvdata(dev); usb_kill_urb(keyboard-irq); } static int usb_kbd_probe(struct usb_interface *intf, const struct usb_device_id *id) { struct usb_device *dev interface_to_usbdev(intf); struct usb_host_interface *interface intf-cur_altsetting; struct usb_endpoint_descriptor *endpoint; struct usb_keyboard *keyboard; struct input_dev *input; int error -ENOMEM; int i; /* 分配驱动数据结构 */ keyboard kzalloc(sizeof(struct usb_keyboard), GFP_KERNEL); if (!keyboard) goto err1; /* 查找中断端点 */ for (i 0; i interface-desc.bNumEndpoints; i) { endpoint interface-endpoint[i].desc; if (usb_endpoint_is_int_in(endpoint)) { keyboard-endpoint endpoint; break; } } if (!keyboard-endpoint) { error -ENODEV; goto err2; } /* 分配数据缓冲区 */ keyboard-data usb_alloc_coherent(dev, keyboard-endpoint-wMaxPacketSize, GFP_KERNEL, keyboard-data_dma); if (!keyboard-data) goto err2; /* 分配URB */ keyboard-irq usb_alloc_urb(0, GFP_KERNEL); if (!keyboard-irq) goto err3; /* 设置输入设备 */ input input_allocate_device(); if (!input) goto err4; keyboard-udev dev; keyboard-input input; usb_make_path(dev, keyboard-phys, sizeof(keyboard-phys)); strlcat(keyboard-phys, /input0, sizeof(keyboard-phys)); input-name USB Keyboard; input-phys keyboard-phys; input-dev.parent intf-dev; input_set_drvdata(input, keyboard); input-id.bustype BUS_USB; input-id.vendor le16_to_cpu(dev-descriptor.idVendor); input-id.product le16_to_cpu(dev-descriptor.idProduct); input-id.version le16_to_cpu(dev-descriptor.bcdDevice); input-open usb_kbd_open; input-close usb_kbd_close; set_bit(EV_KEY, input-evbit); set_bit(EV_REP, input-evbit); for (i 0; i 255; i) set_bit(usb_kbd_keycode[i], input-keybit); clear_bit(0, input-keybit); /* 设置URB */ usb_fill_int_urb(keyboard-irq, dev, usb_rcvintpipe(dev, keyboard-endpoint-bEndpointAddress), keyboard-data, keyboard-endpoint-wMaxPacketSize, usb_kbd_irq, keyboard, keyboard-endpoint-bInterval); keyboard-irq-transfer_dma keyboard-data_dma; keyboard-irq-transfer_flags | URB_NO_TRANSFER_DMA_MAP; /* 注册输入设备 */ error input_register_device(keyboard-input); if (error) goto err5; usb_set_intfdata(intf, keyboard); return 0; err5: input_free_device(input); err4: usb_free_urb(keyboard-irq); err3: usb_free_coherent(dev, keyboard-endpoint-wMaxPacketSize, keyboard-data, keyboard-data_dma); err2: kfree(keyboard); err1: return error; } static void usb_kbd_disconnect(struct usb_interface *intf) { struct usb_keyboard *keyboard usb_get_intfdata(intf); usb_set_intfdata(intf, NULL); if (keyboard) { usb_kill_urb(keyboard-irq); input_unregister_device(keyboard-input); usb_free_urb(keyboard-irq); usb_free_coherent(keyboard-udev, keyboard-endpoint-wMaxPacketSize, keyboard-data, keyboard-data_dma); kfree(keyboard); } } static struct usb_driver usb_kbd_driver { .name usbkbd, .probe usb_kbd_probe, .disconnect usb_kbd_disconnect, .id_table usb_kbd_id_table, }; module_usb_driver(usb_kbd_driver); MODULE_AUTHOR(DRIVER_AUTHOR); MODULE_DESCRIPTION(DRIVER_DESC); MODULE_LICENSE(GPL);7. 测试与调试完成驱动开发后需要进行测试和调试加载驱动模块insmod usbkbd.ko查看内核日志dmesg | tail测试按键输入cat /dev/input/eventX # X为你的键盘设备号使用evtest工具进行更专业的测试evtest /dev/input/eventX调试技巧使用printk输出关键变量值检查URB提交和完成状态验证HID报告描述符解析结果确认输入事件是否正确上报8. 进阶主题与优化对于更复杂的键盘驱动开发可能需要考虑以下进阶主题多语言键盘布局支持实现键码映射表支持不同国家的键盘布局特殊功能键处理多媒体键系统控制键睡眠、唤醒等LED状态控制static int usb_kbd_set_leds(struct usb_keyboard *keyboard, unsigned int leds) { unsigned char *data; int ret; data kmalloc(8, GFP_KERNEL); if (!data) return -ENOMEM; data[0] 0; /* 报告ID */ data[1] (leds 0x07); /* Num Lock, Caps Lock, Scroll Lock */ ret usb_control_msg(keyboard-udev, usb_sndctrlpipe(keyboard-udev, 0), USB_REQ_SET_REPORT, USB_TYPE_CLASS | USB_RECIP_INTERFACE | USB_DIR_OUT, (2 8) | data[0], /* HID_OUTPUT_REPORT */ intf-altsetting[0].desc.bInterfaceNumber, data, 8, USB_CTRL_SET_TIMEOUT); kfree(data); return ret; }电源管理实现suspend/resume回调处理远程唤醒功能性能优化使用DMA传输优化URB处理流程减少中断延迟在实际项目中你可能还需要考虑固件兼容性、错误恢复机制、热插拔处理等问题。这些都需要根据具体需求进行定制开发。

更多文章