第四章 第一个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。

小技巧: 如果你不想手写报告描述符,可以用Zephyr自带的宏定义。在include/zephyr/usb/class/usb_hid.h里,有现成的键盘报告描述符模板。我建议新手先手写一遍理解原理,再用宏简化。

4.4 主程序——初始化与发送按键

主程序的核心逻辑其实就三步:

  1. 初始化USB设备栈
  2. 注册HID回调函数
  3. 循环发送按键报告
#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栈能力的地方。咱们下章见。