目录

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. **设备描述符** – 告诉主机我是 USB 设备
2. **配置描述符** – 描述设备的配置信息
3. **接口描述符** – 描述接口类型
4. **HID 描述符** – HID 特有的描述符
5. **端点描述符** – 描述数据传输的端点
6. **报告描述符** – 最关键!描述数据格式

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

## 环境搭建

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

“`bash
# 安装 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 权限

“`bash
# 创建 udev 规则
sudo tee /etc/udev/rules.d/70-ch552.rules > /dev/null << 'EOF' SUBSYSTEM=="usb", ATTR{idVendor}=="4348", ATTR{idProduct}=="55e0", MODE="0666" SUBSYSTEM=="usb", ATTR{idVendor}=="4348", ATTR{idProduct}=="55e8", MODE="0666" SUBSYSTEM=="usb", ATTR{idVendor}=="4348", ATTR{idProduct}=="55e9", MODE="0666" EOF # 重载规则 sudo udevadm control --reload-rules sudo udevadm trigger ``` **注意事项:** ⚠️ CH559 的 USB 产品 ID 是 55e9,比 CH552 多了这一行。 ## 项目实现 ### 第一步:准备项目目录 ```bash # 创建项目目录 mkdir ch559-hid-keyboard cd ch559-hid-keyboard # 创建源文件 touch main.c touch USB_DEV.C touch USB_DESC.C ``` ### 第二步:编写 USB 描述符 这是最关键的部分。我们创建一个自定义的键盘设备,支持 6 键无冲。 ```c /* USB_DESC.C - USB 描述符定义 */ #include

// 设备描述符
__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 键无冲)

### 第三步:编写主程序

“`c
/* 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 < ms; i++) for(j = 0; j < 1000; j++); } // 发送键盘报告 void SendKeyReport(UINT8 modifier, UINT8 key) { KeyReport.modifiers = modifier; KeyReport.keys[0] = key; KeyReport.keys[1] = 0; KeyReport.keys[2] = 0; KeyReport.keys[3] = 0; KeyReport.keys[4] = 0; KeyReport.keys[5] = 0; // 通过端点 1 发送数据 Ep1Buffer[0] = KeyReport.modifiers; Ep1Buffer[1] = KeyReport.reserved; Ep1Buffer[2] = KeyReport.keys[0]; Ep1Buffer[3] = KeyReport.keys[1]; Ep1Buffer[4] = KeyReport.keys[2]; Ep1Buffer[5] = KeyReport.keys[3]; Ep1Buffer[6] = KeyReport.keys[4]; Ep1Buffer[7] = KeyReport.keys[5]; UEP1_T_LEN = 8; UEP1_CTRL = UEP1_CTRL & ~MASK_UEP_T_RES | UEP_T_RES_ACK; } // 清除按键状态 void ClearKeys() { SendKeyReport(0, 0); DelayMs(50); } void main() { // 配置系统时钟为 24MHz SAFE_MOD = 0x55; SAFE_MOD = 0xAA; CLOCK_CFG = (CLOCK_CFG & ~MASK_SYS_CLK_SEL) | 0x03; SAFE_MOD = 0x00; // 配置 GPIO P3_DIR &= ~(1 << 0); // 按钮输入 P3_PU |= (1 << 0); // 上拉电阻 P1_DIR |= (1 << 0); // LED 输出 P1_PU &= ~(1 << 0); // 初始化 USB USB_Device_Init(); EA = 1; // 开启全局中断 // 主循环 while(1) { // 检测按钮按下(低电平有效) if(BUTTON_PIN == 0) { DelayMs(20); // 消抖 if(BUTTON_PIN == 0) { // 按钮确认按下,点亮 LED LED_PIN = 0; // 发送 Ctrl+Alt+Delete // 修饰键:Left Ctrl (0x01) + Left Alt (0x04) SendKeyReport(0x05, 0x4C); // Delete 键值 DelayMs(100); ClearKeys(); // 等待按钮释放 while(BUTTON_PIN == 0) { DelayMs(10); } // 熄灭 LED LED_PIN = 1; } } DelayMs(10); } } ``` **原理解析:** 主程序做了这几件事: 1. 配置系统时钟和 GPIO 2. 初始化 USB 设备 3. 在主循环中检测按钮状态 4. 当按钮按下时,发送 Ctrl+Alt+Delete 组合键 5. 用 LED 指示当前状态 ### 第四步:USB 设备驱动代码 ```c /* USB_DEV.C - USB 设备驱动 */ #include
#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;
}
“`

### 第五步:编译与烧录

“`bash
# 编译所有源文件
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 键 → 烧录。

## 测试验证

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

“`bash
# 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. 检查 DevDesc 中的厂商 ID 和产品 ID 是否正确
2. 确认 CfgDesc 的总长度与实际长度匹配
3. 用 `lsusb -v` 查看详细错误信息
4. 尝试更换 USB 线或 USB 端口

### 问题 2:按钮按下没反应

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

**原因:**
– 按钮引脚配置错误
– 中断未开启
– USB 枚举未完成就发送数据

**解决:**
1. 用万用表测量按钮引脚电平变化
2. 确认 EA = 1 已开启全局中断
3. 在发送数据前加延时,等待 USB 枚举完成:
“`c
DelayMs(500); // 等待 USB 枚举
“`
4. 添加 USB 连接状态检测

### 问题 3:发送的按键不对

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

**原因:**
– 键值定义错误
– 修饰键位设置错误

**解决:**
1. 参考 USB HID 键值表:
– A = 0x04, B = 0x05, C = 0x06…
– Ctrl = 0x01, Alt = 0x04, Delete = 0x4C
2. 用在线 HID 测试工具验证:
“`
https://www.onlinetoolz.net/keyboard-test
“`
3. 先测试单个按键,再测试组合键

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

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

**原因:**
– 头文件包含顺序错误
– 外部函数声明缺失

**解决:**
1. 确保先包含 `` 再包含 ``
2. 检查所有外部函数都有声明
3. 编译时把所有源文件一起编译:
“`bash
sdcc … main.c USB_DEV.C
“`

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

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

**原因:**
– 电流不足
– 代码中有死循环或看门狗复位
– USB 线质量差

**解决:**
1. 换用带供电的 USB Hub
2. 检查代码中是否有耗时操作阻塞主循环
3. 禁用看门狗或及时喂狗
4. 换一根质量好的 USB 线

## 进阶扩展

### 自定义宏命令

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

“`c
// 定义多个按钮
#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 协议规范](https://usb.org/hid)
– [USB HID 键值表](https://www.usb.org/sites/default/files/documents/hut1_12v2.pdf)
– [CH559 数据手册](http://www.wch.cn/products/CH559.html)
– [在线 HID 测试工具](https://www.onlinetoolz.net/keyboard-test)
– [Openterface Mini-KVM](https://github.com/Openterface/Openterface_QT)

更多关于 的文章
关注创客出手公众号

关注创客出手