第四章 第一个USB设备:HID键盘实现——从零搭建最小USB设备
好,咱们正式开始动手了。
前面几章讲了那么多理论,说实话,我自己当年学USB的时候,最烦的就是光看协议不看代码。看来看去,总觉得隔着一层纱。所以这一章,咱们直接上手——从零搭建一个HID键盘设备。
你想想看,一个USB键盘,说白了就是告诉电脑:「我按了哪个键」。就这么简单。但背后涉及到的枚举、描述符、报告描述符,嗯,这些才是真正要啃的骨头。
4.1 最小系统需要哪些文件?
我个人习惯,新建一个Zephyr项目时,先列清楚需要哪些文件。一个最小的USB HID键盘,至少需要这些:
- prj.conf:项目配置文件,打开USB和HID相关功能
- main.c:主程序,初始化USB,发送按键报告
- app.overlay(可选):如果开发板需要额外引脚配置
- CMakeLists.txt:构建文件,Zephyr会自动处理大部分
我在项目中遇到过不少新手,一上来就复制粘贴大段代码,结果编译报错都不知道去哪找原因。其实从最小系统开始,一步步加功能,反而更快。
4.2 配置prj.conf——打开USB设备栈
配置文件是第一步。说白了,就是告诉Zephyr:「我要用USB,我要用HID」。
# 启用USB设备栈
CONFIG_USB_DEVICE_STACK=y
# 启用HID类驱动
CONFIG_USB_DEVICE_HID=y
# 设置USB设备描述符中的厂商和产品信息
CONFIG_USB_DEVICE_VID=0x2FE3
CONFIG_USB_DEVICE_PID=0x0001
CONFIG_USB_DEVICE_MANUFACTURER="MyCompany"
CONFIG_USB_DEVICE_PRODUCT="Zephyr HID Keyboard"
# 启用控制台输出,方便调试
CONFIG_CONSOLE=y
CONFIG_SERIAL=y
这里要注意一点:VID和PID。0x2FE3是Zephyr的测试用VID,你如果做产品,一定要换成自己申请的。我曾经见过有人直接用别人的VID,结果被USB-IF找上门……嗯,那场面挺尴尬的。
4.3 编写HID报告描述符——键盘的灵魂
HID报告描述符,说白了就是一份「说明书」,告诉主机你的设备能发什么数据。键盘的报告描述符,核心就是8个字节:
- 第0字节:修饰键(Ctrl、Shift、Alt、Win)
- 第1字节:保留
- 第2-7字节:最多同时按下的6个普通键
为什么是6个?这是USB HID协议规定的。你想想看,正常人也就10根手指,6个键同时按已经够用了。
static 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)
// ... 中间省略部分标准描述符
0xC0 // End Collection
};
这段代码看着挺吓人,其实每个字节都有含义。我刚开始学的时候,也是对着HID Usage Table手册一行行查。后来发现,用工具生成更靠谱——比如USB.org官方的HID Descriptor Tool。
include/zephyr/usb/class/usb_hid.h里,有现成的键盘报告描述符模板。我建议新手先手写一遍理解原理,再用宏简化。
4.4 主程序——初始化与发送按键
主程序的核心逻辑其实就三步:
- 初始化USB设备栈
- 注册HID回调函数
- 循环发送按键报告
#include <zephyr/kernel.h>
#include <zephyr/usb/usb_device.h>
#include <zephyr/usb/class/usb_hid.h>
static const struct hid_ops ops = {
.int_in_ready = int_in_ready_cb,
};
void main(void)
{
int ret;
// 初始化USB设备
ret = usb_enable(NULL);
if (ret != 0) {
printk("USB enable failed: %d\n", ret);
return;
}
// 注册HID回调
usb_hid_register(&hid_device_data, hid_report_desc,
sizeof(hid_report_desc), &ops);
// 等待USB枚举完成
k_sleep(K_SECONDS(1));
// 模拟按下'A'键
uint8_t report[8] = {0};
report[2] = 0x04; // HID usage ID for 'A'
while (1) {
ret = usb_hid_send_report(&hid_device_data, report, sizeof(report));
if (ret != 0) {
printk("Send report failed: %d\n", ret);
}
k_sleep(K_SECONDS(2));
}
}
这里有个坑,我必须要提醒你:usb_hid_send_report必须在USB枚举完成之后才能调用。否则,你发送的数据会石沉大海。我在项目中就吃过这个亏,调试了半天才发现是时序问题。
4.5 编译与测试
编译命令很简单,以nRF52840 DK为例:
west build -b nrf52840dk_nrf52840 -t pristine
west flash
插上USB线,电脑应该会识别出一个HID键盘设备。你可以用dmesg(Linux)或设备管理器(Windows)查看。
如果没识别出来,别慌。先检查这几个地方:
- USB线是不是数据线?有些充电线不能传数据
- prj.conf里USB_DEVICE_STACK有没有打开?
- 报告描述符有没有写错?
我记得有一次,我折腾了一下午,最后发现是USB线的问题。嗯,从那以后我就在桌上贴了张纸条:「先换线」。
4.6 扩展:实现真正的按键输入
上面的示例只是循环发送'A'键,实际应用中肯定要检测物理按键。这里给个思路:
// GPIO按键中断回调
void button_pressed(const struct device *dev, struct gpio_callback *cb,
uint32_t pins)
{
uint8_t report[8] = {0};
report[2] = 0x04; // 'A'键
usb_hid_send_report(&hid_device_data, report, sizeof(report));
}
void button_released(const struct device *dev, struct gpio_callback *cb,
uint32_t pins)
{
uint8_t report[8] = {0}; // 全零表示释放
usb_hid_send_report(&hid_device_data, report, sizeof(report));
}
你想想看,这样就能实现一个真正的物理键盘了。当然,实际产品还要考虑防抖、多键同时按下等细节。但作为第一个USB设备,能跑起来就是胜利。
- 最小系统只需要prj.conf、main.c两个文件
- HID报告描述符是键盘的灵魂,8字节报告格式要记牢
- USB枚举完成后才能发送报告
- 按键需要按下和释放两个动作
好了,这一章的内容就到这里。下一章我们会讲复合设备——把键盘和鼠标合在一个USB设备里。说实话,复合设备才是真正体现Zephyr USB栈能力的地方。咱们下章见。