# 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)



