USB 设备开发入门:用 CH559 制作自定义 HID 设备

USB 设备开发入门:用 CH559 制作自定义 HID 设备

USB 设备开发入门:用 CH559 制作自定义 HID 设备

大家好,我是 MakerOnsite 的老朋友。

昨天我们聊了 CH55x 系列的选型,今天我们来玩点实际的——用 CH559 做一个自定义的 USB HID 设备。

什么是 HID 设备?简单说,就是键盘、鼠标、游戏手柄这类”人机接口设备”。Windows、Linux、macOS 都原生支持 HID,不需要额外驱动,插上就能用。这对于我们做嵌入式开发来说,简直不要太友好。

今天的目标是:用 CH559 做一个自定义的 HID 设备,可以发送特定的按键组合。比如按一下按钮,自动发送 Ctrl+Alt+Delete,或者宏命令。

准备好了吗?我们开始。

需要准备什么?

这次的硬件清单稍微多一点,但总成本依然控制在百元以内。

物品型号/规格价格
CH559 开发板带 USB Host+Device¥25
微动开关12mm 自锁开关¥2
LED 指示灯5mm 红色¥0.5
电阻220Ω 1/4W¥0.2
USB 转 TTL 模块CH340/CP2102¥5
杜邦线公对母 20cm¥3
面包板400 孔¥8
USB 数据线Micro-USB¥5
总计¥48.7

如果你已经有第 1 篇文章里的材料,这次只需要额外买微动开关和 LED,成本不到 3 块钱。

HID 协议基础

在开始写代码之前,我们先简单了解一下 HID 协议。

HID(Human Interface Device)是 USB 协议中的一种设备类。它的核心是描述符——一段数据结构,告诉主机”我是什么设备,能做什么”。

一个典型的 HID 设备需要以下描述符:

  1. $1

  2. $1

  3. $1

  4. $1

  5. $1

  6. $1

听起来很复杂?别担心,我们直接用现成的模板修改就好。

环境搭建

如果你已经完成了第 1 篇文章的环境搭建,这一步可以跳过。如果没有,按照下面的步骤来:

# 安装 SDCC 编译器
sudo apt-get update
sudo apt-get install sdcc

# 安装 ch552tool 烧录工具
git clone https://github.com/arpruss/ch552tool.git
cd ch552tool
make
sudo make install

# 安装依赖(如果遇到问题)
sudo apt-get install libusb-1.0-0-dev

配置 USB 权限

# 创建 udev 规则
sudo tee /etc/udev/rules.d/70-ch552.rules > /dev/null 

// 设备描述符
__code UINT8 DevDesc[] = {
    0x12, 0x01, 0x10, 0x01, 0x00, 0x00, 0x00, 0x08,  // 设备描述符头
    0x48, 0x43, 0x59, 0x55, 0x00, 0x01, 0x01, 0x02,  // 厂商 ID、产品 ID
    0x00, 0x01, 0x01, 0x00, 0x00, 0x01               // 版本信息
};

// 配置描述符(包含 HID 描述符)
__code UINT8 CfgDesc[] = {
    // 配置描述符头
    0x09, 0x02, 0x22, 0x00, 0x01, 0x01, 0x00, 0x80, 0x32,

    // 接口描述符
    0x09, 0x04, 0x00, 0x00, 0x01, 0x03, 0x01, 0x01, 0x00,

    // HID 描述符
    0x09, 0x21, 0x10, 0x01, 0x00, 0x01, 0x22, 0x20, 0x00,

    // 端点描述符(中断端点)
    0x07, 0x05, 0x81, 0x03, 0x08, 0x00, 0x0A
};

// 报告描述符(定义键盘数据格式)
__code UINT8 ReportDesc[] = {
    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, Variable, Absolute)
    0x95, 0x01,        //   Report Count (1)
    0x75, 0x08,        //   Report Size (8)
    0x81, 0x01,        //   Input (Constant)
    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, Variable, Absolute)
    0x95, 0x01,        //   Report Count (1)
    0x75, 0x03,        //   Report Size (3)
    0x91, 0x01,        //   Output (Constant)
    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)
    0xC0               // End Collection
};

// 字符串描述符
__code UINT8 LangDesc[] = { 0x09, 0x04 };
__code UINT8 ManuInfo[] = { 0x0E, 0x03, \'M\', 0, \'a\', 0, \'k\', 0, \'e\', 0, \'r\', 0, \'O\', 0, \'n\', 0, \'s\', 0, \'i\', 0, \'t\', 0, \'e\', 0 };
__code UINT8 ProdInfo[] = { 0x0A, 0x03, \'H\', 0, \'I\', 0, \'D\', 0, \' \', 0, \'K\', 0, \'e\', 0, \'y\', 0 };

原理解析:

报告描述符是 HID 设备的核心。上面这段代码定义了一个标准的键盘设备:

  • 前 8 位表示修饰键(Ctrl、Alt、Shift 等)

  • 接下来 8 位保留

  • 然后 5 位表示 LED 状态(NumLock、CapsLock 等)

  • 最后 6 个字节表示按下的键值(支持 6 键无冲)

第三步:编写主程序

/* main.c - 主程序 */
#include 
#include 

// 定义按钮和 LED 引脚
#define BUTTON_PIN P3_0
#define LED_PIN P1_0

// USB 端点缓冲区
__at (0x0040) __xdata UINT8 Ep0Buffer[8];
__at (0x0048) __xdata UINT8 Ep1Buffer[8];

// 键盘报告数据结构
typedef struct {
    UINT8 modifiers;
    UINT8 reserved;
    UINT8 keys[6];
} KeyboardReport;

__xdata KeyboardReport KeyReport;

// 外部函数声明(来自 USB_DEV.C)
extern void USB_Device_Init();
extern void USB_ISR() __interrupt (USB_INT_VECTOR);

// 延时函数
void DelayMs(UINT16 ms) {
    UINT16 i, j;
    for(i = 0; i 
#include 

// USB 端点配置
void USB_Device_Init() {
    // 配置端点 0(控制端点)
    UEP0_DMA = 0x0000;
    UEP0_CTRL = UEP_T_RES_NAK | UEP_R_RES_ACK;

    // 配置端点 1(中断端点)
    UEP1_DMA = 0x0048;
    UEP1_CTRL = UEP_T_RES_NAK | UEP_R_RES_ACK;

    // USB 设备配置
    UDEV_EN = 1;      // 使能 USB
    UDEV_CTRL = 0x00; // 全速设备
    UINT_EN |= bUIE_SUSPEND;
    UINT_EN |= bUIE_TRANSFER;
    UINT_EN |= bUIE_BUST_RST;

    // 清除中断标志
    USB_INT_FG = 0xFF;
}

// USB 中断服务程序
void USB_ISR() __interrupt (USB_INT_VECTOR) {
    UINT8 intFlag = USB_INT_FG;

    if(intFlag & bUIF_TRANSFER) {
        // 传输完成中断
        if(UIF_EP0) {
            // 端点 0 处理
            UEP0_CTRL ^= UEP_T_TOG;
        }
        if(UIF_EP1) {
            // 端点 1 处理
            UEP1_CTRL ^= UEP_T_TOG;
        }
    }

    if(intFlag & bUIF_BUST_RST) {
        // 总线复位
        UEP0_CTRL = UEP_T_RES_NAK | UEP_R_RES_ACK;
        UEP1_CTRL = UEP_T_RES_NAK | UEP_R_RES_ACK;
    }

    // 清除中断标志
    USB_INT_FG = intFlag;
}

第五步:编译与烧录

# 编译所有源文件
sdcc -mcc59 --no-xinit-code --code-loc 0x0000 --data-loc 0x0030 main.c USB_DEV.C

# 烧录程序(按住 BOOT 键上电)
ch552tool write main.ihx

烧录小技巧: 和 CH552 一样,CH559 也需要进入 Bootloader 模式。按住 BOOT 键 → 插 USB → 松 BOOT 键 → 烧录。

测试验证

烧录完成后,把设备插到电脑上:

# Linux 下查看 USB 设备
lsusb | grep -i \"4348\"

# 查看设备详细信息
lsusb -v -d 4348:55e9

你应该能看到类似这样的输出:

Bus 001 Device 005: ID 4348:55e9 QinHeng Electronics CH559

现在按下开发板上的按钮,电脑应该会收到 Ctrl+Alt+Delete 组合键。在 Windows 上会弹出安全选项界面,在 Linux 上可能会注销或弹出对话框(取决于系统配置)。

注意: ⚠️ 在生产环境测试前,最好先在虚拟机里试一下,避免意外注销或重启。

常见问题排查

问题 1:电脑识别为”未知 USB 设备”

现象: 插上设备后,系统提示”USB 设备无法识别”

原因:

  • USB 描述符错误

  • 端点配置不正确

  • 时钟频率问题

解决:

  1. $1

  2. $1

  3. $1

  4. $1

问题 2:按钮按下没反应

现象: 烧录成功,电脑也识别了设备,但按按钮没反应

原因:

  • 按钮引脚配置错误

  • 中断未开启

  • USB 枚举未完成就发送数据

解决:

  1. $1

  2. $1

  3. $1

  4. $1

问题 3:发送的按键不对

现象: 按下按钮,电脑收到的是其他按键

原因:

  • 键值定义错误

  • 修饰键位设置错误

解决:

  1. $1

  2. $1

  3. $1

问题 4:编译报错”undefined symbol”

现象: SDCC 编译时报错,说某些符号未定义

原因:

  • 头文件包含顺序错误

  • 外部函数声明缺失

解决:

  1. $1

  2. $1

  3. $1

问题 5:设备频繁断开重连

现象: 设备插上后不断断开、连接

原因:

  • 电流不足

  • 代码中有死循环或看门狗复位

  • USB 线质量差

解决:

  1. $1

  2. $1

  3. $1

  4. $1

进阶扩展

自定义宏命令

你可以修改代码,让不同的按钮发送不同的宏命令:

// 定义多个按钮
#define BTN_1 P3_0
#define BTN_2 P3_1
#define BTN_3 P3_2

// 在 main() 中检测不同按钮
if(BTN_1 == 0) {
    SendKeyReport(0x01, 0x04);  // Ctrl+A (全选)
}
if(BTN_2 == 0) {
    SendKeyReport(0x01, 0x06);  // Ctrl+C (复制)
}
if(BTN_3 == 0) {
    SendKeyReport(0x01, 0x07);  // Ctrl+V (粘贴)
}

这样就做成了一个自定义的宏键盘!

添加更多按键

目前的报告描述符支持 6 键无冲。如果需要更多按键,可以修改报告描述符,增加 keys 数组的长度。不过要注意,端点缓冲区大小也要相应调整。

结合 Openterface 使用

如果你在用 Openterface Mini-KVM 进行远程开发,可以把这个 HID 设备插到 KVM 的 USB 口上,实现远程触发宏命令。这在批量部署或自动化测试时非常有用。

总结

今天我们用 CH559 做了一个自定义的 USB HID 键盘设备。整个过程涵盖了:

  • USB HID 协议基础

  • 描述符的编写

  • USB 设备驱动开发

  • 中断处理和端点通信

关键要点:

  • 报告描述符是 HID 设备的核心,定义了数据格式

  • CH559 同时支持 USB Host 和 Device,非常适合做 USB 相关项目

  • 测试时先在虚拟机里验证,避免意外操作

下一步可以试试:

  • 做成一个完整的宏键盘,支持多个自定义按键

  • 添加 OLED 显示屏,显示当前模式

  • 结合蓝牙模块,做成无线 HID 设备

  • 用 CH559 的 USB Host 功能,读取其他 USB 设备数据

USB 设备开发其实没有想象中那么难。有了 CH55x 这样便宜的芯片,加上开源的工具链,每个人都可以动手试试。

希望这篇博客文章对您有所帮助!


相关资源:

  • USB HID 协议规范

  • USB HID 键值表

  • CH559 数据手册

  • 在线 HID 测试工具

  • Openterface Mini-KVM