ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

CH552实现USB HID键盘模拟器:从枚举到中断传输的完整实操

2026/9/28 22:52:18 拓冰建站 浏览量
CH552实现USB HID键盘模拟器:从枚举到中断传输的完整实操 做USB开发早晚都会想尝试一个项目让单片机插上电脑之后被系统识别成一只标准键盘自动帮你敲字、触发快捷键。我最近用沁恒CH552单片机把这个HID键盘模拟器完整做了一遍从画最小原理图到最终看USB抓包数据整个流程走下来体验比想象中顺。CH552这芯片之所以适合是因为它自带全速USB控制器不需要外挂CH340或CP2102这类USB转串口芯片一颗几块钱的芯片加一个晶振、几个电容就能和被识别成标准HID键盘。它想解决的核心问题也很有意思USB设备是怎么被电脑“认”出来的键盘按键又是以什么格式传到主机端的做完这个项目USB协议的枚举流程、控制传输、中断传输、描述符这些概念都会落地不是纯看文档空想。这篇文章适合正在学单片机、想做USB外设或者想搞宏键盘/自动输入的电子爱好者也适合准备蓝桥杯单片机方向、需要快速上手USB应用的人。我会从方案选型讲起把硬件搭建、HID协议、完整源码、烧录调试的坑一次说清楚。1. 为什么拿CH552做HID键盘模拟器而不是STM32或ESP321.1 CH552的硬件底子到底够不够用很多人一听到8051单片机第一反应是“太老了吧”。但CH552不一样它是增强型的8051内核最高主频24MHz内置16KB Flash和1KB左右的RAM。单看算力确实不豪华但是做USB全速设备刚刚好因为USB HID键盘本身不需要复杂运算它只做一件事把按键状态打包成8字节报告再通过USB中断端点发给主机。8051处理这个负载绰绰有余甚至还有点富余。CH552最值钱的部分是内部集成了一个USB2.0全速控制器和PHY支持全速12Mbps和低速1.5Mbps两种模式。这颗芯片还内置了3.3V LDO外部用5V供电可以直接从USB口取电。它还有片上复位、看门狗、ADC、触摸按键等功能扩展性不错。真正决定单片机适不适合USB开发的关键不只是CPU频率而是有没有好用的USB控制器以及寄存器文档是否清晰。CH552在这两点上都做得很好WCH官方提供CH554/CH552的参考代码资料相对齐全踩坑成本低。选型的时候我还对比过STM32F103和ESP32。STM32做USB设备也很强但工程复杂度高CubeMX配一堆外设起步成本大ESP32虽然有USB外设但大多数入门板还要外接USB转串口芯片才能下载程序对初学者来说容易把“USB调试口”和“USB设备口”搞混。CH552的定位就非常简单粗暴这颗芯片就是为USB应用准备的下载程序走USB bootloader跑起来又自己是USB设备一个USB口通吃。1.2 HID键盘模拟器为什么值得做HID是Human Interface Device人机交互设备。鼠标、键盘、游戏手柄都属于HID设备。HID协议在USB协议栈里算是最简单的一种不需要装驱动Windows、macOS、Linux都自带支持枚举成功后立刻被识别。对于刚接触USB的人从HID键盘入手是最优路径它把控制传输、描述符、端点、中断传输这些概念全部串起来但又避开了大容量存储、UVC这类复杂协议。键盘模拟器的应用场景也很多。最简单的是做一个宏键盘一键打开文件夹、一键输入常用文本进阶一点可以做自动化录入面板给重复性工作按下一个按键触发一串组合操作。还可以把它当成USB协议学习板因为键盘报告的格式是公开稳定的随便抓个普通键盘的数据就能对比你的设备是否在按相同方式工作。我见过有人用它做无障碍辅助工具也见过用它做游戏外设但底线是别拿来做入侵或绕过认证这属于安全红线犯不上。做这个项目另一个价值在于影响范围。USB协议是无处不在的但你平时很少能直接控制数据链路底层。通过CH552自己实现一个HID设备等于把电脑输入端口的“黑盒”打开了一条缝之后再做自定义U盘、USB转串口、USB MIDI设备都是同一套思路只是描述符和端点配置不同。2. 硬件准备最小系统与下载方式一次搞明白2.1 元件清单与接线说明我用的材料很少如果手头有成品的CH552开发板连焊接都省了。以CH552G为例裸芯片方案大致需要这些元件参数/规格数量作用CH552G/T芯片本体1主控内置USB控制器晶振12MHz1USB时钟/系统时钟来源陶瓷电容22pF2晶振负载电容陶瓷电容0.1uF2芯片VCC和3.3V去耦电解电容10uF/16V1电源滤波电阻10kΩ1复位上拉轻触按键6x6mm1触发宏按键USB母座TYPE-C或Micro-USB1连接电脑发光二极管任意1状态指示选配接线有几点要注意。CH552的USB数据引脚是UDM和UDP对应到USB插座是D-和D别接反。USB座子的D/D-信号线要尽量短而且最好在靠近芯片的位置加小电阻或者磁珠实测下来能减少电磁干扰。VCC接5VCH552内部会自己产生3.3V给USB PHY和内核用但V33引脚一定要接一个0.1uF电容到地不然USB枚举会不稳定。晶振我建议选12MHz不要为了省事用内部RC。CH552的USB全速模式对时钟稳定性有要求内部振荡器精度不够的话主机端可能时好时坏尤其在高负载USB总线上更容易掉线。晶振的两个引脚X1、X2接上22pF电容电容另一端接地晶振本体尽量靠近芯片引脚走线短一点。触发按键我接在P1.1上内部上拉按键另一端接GND。LED接P1.7高电平点亮还是低电平点亮要看你的LED接法代码里我会按“低电平点亮”来写如果你的板子相反把digitalWrite反过来就行。2.2 下载固件的两种方式CH552有内置的USB bootloader很多开发板出厂时就刷好了。下载固件最爽的方式是先把USB线插到电脑按住下载按键再上电或复位电脑会识别出一个USB下载设备然后用WCH官方的WCHISPTOOL工具选中生成的hex文件点下载几秒完成。这种方式不需要额外的USB转TTL下载器因为bootloader本身就是一个USB设备CH552一边被电脑当作下载接口一边在下载完成后复位进入用户程序。如果没有现成开发板、也没有内置bootloader的CH552芯片那就用串口ISP方式。CH552支持通过UART引脚下载例如使用UART0的RXD/TXD引脚配合任意USB转串口模块常见FT232R、CP2102、CH340都行连接把下载工具的DTR/RTS配置成复位/下载时序也可以刷进去。跑USB开发板我强烈建议选带bootloader的核心板省去很多不必要的折腾。有个容易踩的坑有些CH552开发板上有两个USB接口一个接板载串口芯片一个接CH552自身的USB设备引脚。下载的时候要插到“CH552 USB口”不是“串口下载口”。我一开始试的时候直接插到USB转串口那侧结果设备管理器里始终只看到一个COM口没有USB下载设备搞了半天才反应过来。3. 开发环境用ch55xduino在半小时内跑通编译3.1 为什么要用ch55xduino而不是纯SDCCCH552官方支持用SDCC或者Keil C51做开发。SDCC是开源免费的编译器Keil C51是经典商业编译器两个都能用。但如果你第一次接触8051打开官方datasheet里那一堆寄存器定义比如UEP0CTRL、UEP1CTRL、UEP0DAT、USB_ISR很可能会劝退。这些寄存器不是不好只是学习曲线比较陡。我的做法是先用ch55xduino这套Arduino第三方核心把功能跑通同一个项目里既能获得Arduino那种简洁API也能随时插入汇编或寄存器操作。ch55xduino底层其实还是用SDCC编译只是把很多重复劳动封装好了比如USB描述符、端点配置、HID报告描述符、中断处理逻辑都帮你预置了。你用键盘API只管发press/release底层会帮你打包成HID报告再由CH552的USB控制器发出去。对刚接触USB的人来说这是性价比最高的切入方式。等你跑通之后如果还想深入再去看ch55xduino源码里的usb.c和有hid.c那些文件你会发现它基本上就是把官方例程的寄存器配置翻译成了更简洁的C代码。这时候再回去看官方SDCC裸机例程就不会觉得那么懵了。3.2 Arduino IDE下安装ch55xduino我用的环境是Arduino IDE 1.8.x步骤很简单。第一步打开Arduino IDE在“文件 - 首选项 - 附加开发板管理器网址”里添加ch55xduino的板管理器JSON地址。第二步打开“工具 - 开发板 - 开发板管理器”搜索CH55x或者ch55x找到WCH CH55xDuino Boards点安装。第三步选择开发板列表里能找到的CH552芯片型号选CH552如果没有特殊要求其余参数默认就行。安装完之后还需要安装ch55xduino的USB驱动吗一般不需要。如果你的系统能识别到CH552的bootloader设备Windows会自动装好驱动。部分老版本驱动可能会出现未知设备这时候可以从WCH官网下载CDC或WinUSB驱动但这是个例。还有一个关键点ch55xduino的GPIO引脚编号并不是芯片物理引脚编号它有一套Arduino化的映射。你需要在代码里查一下当前板子的pinout图比如我在文章代码里写#define KEY_PIN 11这对应的是P1.1逻辑引脚。如果你的板子丝印不同可能要对到实际引脚。写程序烧录前最好先用LED闪烁例程做引脚自检避免硬件和代码对不上。4. USB HID键盘协议精讲电脑是怎么认出键盘的4.1 枚举过程从设备插入到驱动安装完成一块USB键盘插上电脑之后主机端做了很多动作其中第一个关键过程叫“枚举”。枚举可以理解成新同学到宿舍报到宿管阿姨先问你是谁设备描述符然后分配房间号Set Address再问你在哪个班、有什么生活习惯配置/接口/端点和HID描述符最后说“你住这儿吧”Set Configuration。数据层面USB枚举的起点是主机复位设备让设备回到地址0状态。之后主机发送GET_DESCRIPTOR请求设备首先返回18字节的设备描述符里面包含了设备类型、厂商ID、产品ID、端点最大包长等。紧接着主机发送SET_ADDRESS给设备分配一个全局地址可能是1也可能是其他数字。接着主机再次读取设备/配置/HID等描述符特别是一大串配置描述符里面嵌套了接口描述符、HID描述符和端点描述符。最后主机选择配置设备正式进入工作状态。HID设备有个特殊的地方它还要通过HID描述符中的报告描述符来告诉主机“我这个设备怎么解释数据”。报告描述符与普通描述符不同它不是固定字段结构而是一串“指令”比如这是键盘设备、按键报告长度为8字节、按键码的范围是0到255等。操作系统根据这些指令来解析后续端点收到的手写报告。这一段是初学者最容易困惑的但搞清楚之后USB开发的一大半就算是通了。4.2 键盘报告一次按键到底发了几个字节标准的USB HID键盘使用中断传输每次中断IN传输的数据负载通常是8字节。这个8字节就是键盘报告Boot Protocol Keyboard Report格式如下字节索引含义0修饰键Modifier1保留字节固定为02按键1键码3按键2键码4按键3键码5按键4键码6按键5键码7按键6键码修饰键字节是位图每一位对应一个键。第0位是左Ctrl第1位是左Shift第2位是左Alt第3位是左GUIWindows键或Command键第4到7位是右边对应键。比如同时按CtrlShift修饰键值就是0x02 | 0x01 0x03。普通键则是在键码位置填对应的Usage ID比如字母A是0x04字母B是0x05数字1是0x1E回车是0x28。这里有个经常弄混的点HID键码不是ASCII码也不是扫描码。我在初学时总想把ASCII的0x41发给电脑来代表字母A结果电脑没反应。正确做法是发0x04。如果要发大写字母A需要同时设置修饰键Shift0x02并在键码位置写0x04主机端收到报告后才解释成一个大写A。很多键盘库内部做的就是把ASCII码转换成“修饰键键码”的组合比如HID Usage Table里列得清清楚楚。空闲的时候不能一直什么都不发吗协议上更推荐设备定期返回空报告这样主机知道键盘仍然在线、按键已释放。常见做法是每10ms或1ms发送一个全0的报告。如果在按下按键后没有收到释放报告主机会一直认为某个键处于按住状态这也是做模拟器时一个隐蔽的坑。4.3 主机如何拿到你的按键中断传输USB有四种传输类型控制、中断、批量、等时。键盘和鼠标用的是中断传输Interrupt Transfers注意这里的“中断”不是指设备主动触发硬件中断而是主机在固定的时间间隔内轮询设备。设备端有数据就在端点缓冲区里放着主机来查一次拿一包数据没数据或者空状态也回一个空报告。在HID键盘上这个轮询间隔通常由端点描述符里的bInterval字段决定。全速设备一般是10ms也就是100Hz轮询。如果有特殊需求可以设置成1ms换来更低的输入延迟。但没必要为好几百毫秒延迟折腾10ms对手键盘输入完全够用。在CH552上端点1或者端点2可以被配置为中断IN端点将键盘报告填入端点缓冲区再由USB硬件根据主机轮询自动发出。节点说来又像流水线我想用一个快递柜类比USB主机是取件的人端点缓冲区是放包裹的柜子中断传输就是每隔一段时间来敲一次柜门。设备线程往柜子里塞一份8字节的键盘报告主机取走。如果设备一直不往里放东西主机每次敲开柜门看到的还是上一个包有初学的朋友遇到过类似问题按键一直重复其实是没有及时发送空的释放报告。5. 完整源码一个可以直接烧录的HID键盘模拟器5.1 工程结构与接线约定整个工程其实就是一个.ino文件依赖ch55xduino自带的USBHIDKeyboard库。我写了两个功能按下P1.1上的按键后CH552先模拟“Windows键R”打开运行窗口然后自动输入“notepad”并按回车把记事本调出来。这个功能虽然简单但它演示了发送组合键、发送单个普通键、发送字符序列三种HID键盘操作。在接线方面我按前面的设计引脚连接作用P1.1轻触按键-另一端GND触发宏按键P1.7LED限流电阻到GND或VCC状态指示灯UDM/UDPUSB插座D-/DUSB通信如果你的板子引脚映射不一样直接把代码顶部的KEY_PIN和LED_PIN改成对应Arduino引脚号。如果不确定先跑一个LED闪灯例程来确定pin编号。5.2 完整源码注释版下面这段代码是我在ch55xduino下测试通过的核心逻辑。为保持可读性我把关键步骤注释写得很详细/* * CH552 USB HID Keyboard Simulator * 功能按下按键后模拟 WinR打开运行框 * 自动输入 notepad 并回车启动记事本。 * * 环境: ch55xduino, Arduino IDE * 硬件: CH552开发板, 按键接P1.1, LED接P1.7 */ #include ch55xduino.h #include USBHIDKeyboard.h // 根据你的板子实际引脚调整 #define KEY_PIN 11 // P1.1, 按键到GND #define LED_PIN 13 // P1.7, 状态LED #define DEBOUNCE_DELAY_MS 5 USBHIDKeyboard Keyboard; void setup() { pinMode(KEY_PIN, INPUT_PULLUP); pinMode(LED_PIN, OUTPUT); digitalWrite(LED_PIN, HIGH); // 初始高让LED灭低电平点亮情况 // 初始化USB等待主机枚举 Keyboard.begin(); // 简单状态提示连闪两次表示设备已就绪 for (int i 0; i 2; i) { digitalWrite(LED_PIN, LOW); delay(80); digitalWrite(LED_PIN, HIGH); delay(80); } } // 执行“打开记事本”的完整动作 void openNotepadWithHotkey() { // 第一步按下 Win R Keyboard.press(KEY_LEFT_GUI); Keyboard.press(r); delay(20); Keyboard.release(KEY_LEFT_GUI); Keyboard.release(r); // 等待运行窗口打开 delay(300); // 第二步输入 notepad 命令 const char* cmd notepad; while (*cmd) { Keyboard.write(*cmd); cmd; } // 第三步按回车执行 Keyboard.write(KEY_RETURN); } void loop() { static uint8_t lastState HIGH; uint8_t curState digitalRead(KEY_PIN); // 检测下降沿避免按下一次触发多次 if (lastState HIGH curState LOW) { delay(DEBOUNCE_DELAY_MS); // 再次读取确认防抖动 curState digitalRead(KEY_PIN); if (curState LOW) { openNotepadWithHotkey(); // LED 闪一下提示执行成功 digitalWrite(LED_PIN, LOW); delay(80); digitalWrite(LED_PIN, HIGH); } } lastState curState; }这段源码的出发点是能真正烧录观察而不是一个空壳示例。你把它复制到Arduino IDE选好开发板下载完按一下P1.1上的按键几秒后电脑会弹出记事本窗口。如果没反应优先检查引脚映射和bootloader下载方式。5.3 代码里最关键的三处细节第一处是Keyboard.begin()。这行代码负责USB控制器初始化、描述符注册、端点配置并开始等待主机枚举。如果它没有执行后面所有键盘操作都白做。ch55xduino库里已经包含了设备描述符、配置描述符、HID报告描述符等一堆数据你不需要手动写但可以后面打开库源码看。第二处是Keyboard.press和Keyboard.release之间加的那20ms延时。修饰键和普通键的顺序非常敏感我们要求按住Win和R之后先把Win释放再释放R。如果同时释放大多数电脑也能识别但不同操作系统行为略有差异加一点延时可以让主机把“WinR”识别得更稳定。实际测试中有些无延迟的极端情况会漏掉组合键所以这种小延时不是多余是实战经验。第三处是边沿检测加软件消抖。单片机读GPIO时机械按键按下前后会有电平抖动如果不处理可能一次按键被识别成十几次触发记事本会一连串打开好几个。传统Arduino里用millis()做延时消抖但这里为了简洁我用了delay()。缺点是delay的时候键盘没响应不过在宏键盘这种简单场景完全可接受。如果你想做得更专业就把按键扫描放进中断再用定时器做多任务。5.4 如果不想输文本想自己发裸键盘报告怎么办ch55xduino的USBHIDKeyboard已经把键盘打包得很舒服但在某些场合你可能需要自己构造标准8字节报告比如同时发送组合键或者处理非标准键。可以这样处理先调用Keyboard.releaseAll()清空所有按键状态然后填充一个8字节数组调用replacement函数发送。可惜不同版本的库API名略有差异我建议直接看库源码找到SendReport或hidDevice::send。如果做更底层的学习我建议换到SDCC裸机工程里看官方CH554例程。官方例程里有一份HID键盘的配置文件里面有完整的报告描述符数组例程大概长这样伪代码DeviceDesc[] { 0x12, 0x01, 0x00, 0x02, 0x00, 0x00, 0x00, 0x40, ... }; CfgDesc[] { 0x09, 0x02, ... }; ReportDesc[] { 0x05, 0x01, 0x09, 0x06, 0xA1, 0x01, ... };这些数组就是你在前面协议部分看到的描述符和报告描述符的编码表现。理解它、会改它才算真正掌握USB HID。6. 编译、烧录与抓包调试实录6.1 在Arduino IDE里完成编译上传选择好ch55xduino的CH552开发板后把源码粘进去。如果你的板子已经预置了USB bootloader且当前插的就是CH552的USB口直接点上传就行。上传过程中Arduino会调用工具链编译生成hex再通过bootloader烧进去。如果板子没有bootloader编译器会提示找不到设备。解决办法是先用USB转TTL把串口ISP的引导程序刷进去或者一次用WCHISPTOOL直接烧录hex文件。第一次烧hex的流程是按住下载按键同时插上USB线电脑里会出现“WCH MCU ISP”或者未知设备打开WCHISPTOOL选择CH552加载hex点下载。下载完成后拔掉USB线重新上电就会运行你的用户程序。烧录成功未必马上能用还需要看设备管理器。正常情况下电脑会识别出一个HID键盘设备设备类型里能找到“USB输入设备”或“键盘”。如果你在设备管理器里看到黄色感叹号说明枚举阶段有问题优先检查晶振、USB线、SHELL时钟配置。6.2 用USB抓包工具验证数据到底发没发对调试USB设备光靠“按下按键有没有反应”还不够因为你要知道是枚举失败了还是报告内容错了。USB抓包是直接看数据链路的方法。Windows下可以用Bus Hound或Wireshark加USBPcapLinux下推荐usbmon加Wireshark。抓包重点看两个阶段。第一阶段是插上设备后的枚举过程能看到主机发GET_DESCRIPTOR设备返回设备描述符再往下能看到配置描述符和HID报告描述符的返回。如果这个阶段有描述符长度不对、CRC错误基本就是个软件配置问题。第二阶段是点击按键之后找到对应设备的中断IN传输展开URB数据应该看到8字节。比如没按键时是00 00 00 00 00 00 00 00如果代码里发送了回车键第二或第三个字节会变成28。我实测下来最常见的问题是报告描述符定义了Report Count是6但代码只发送了2字节主机会拒绝处理整个报告键盘完全没反应。所以用抓包工具一眼就能定位不用盲猜。6.3 常见问题速查表做这个项目最容易被卡住的问题我整理成一个表现象可能原因解决办法电脑完全没识别到USB设备供电不足、D/D-接反、晶振没起振换短USB线核对UDM/UDP接线用示波器/万用表量晶振频率设备管理器出现未知设备bootloader没进、驱动异常按住下载按键再上电使用WCHISPTOOL重刷枚举成功但按按键无反应HID报告描述符格式错误对比官方例程ReportDesc数组检查长度和Report Count字符输出大小写颠倒ASCII转键码时没有处理Shift检查库的ASCII映射大写A要同时发Shift和0x04按键一次变成多次触发缺少消抖逻辑代码里加下降沿检测和延时消抖拔掉再插或复位后失效没有重新初始化USB或断了上拉确认Keyboard.begin()在setup里执行复位后能重新枚举6.4 实测心得和小技巧CH552做USB HID用的USB线别太长、别用品质太差的线很多奇怪问题其实都是供电和信号完整性问题。我自己测试时用过一根2米的普通充电线插上后偶尔“无法识别的设备”但换一根0.5米的短线就稳定了。后来把CH552的V33电容从0.1uF换成10uF也没有更好反而恢复短线才解决。调试时多利用LED状态灯。我在代码里把初始化成功时闪两下执行成功时闪一下。这样不接串口也能知道程序跑到哪里。等以后想在电脑上看日志可以再引一路UART到USB转TTL工具但即便不加也足够完成这个项目。最后一个实用技巧如果你要调小写和大写混合文本别自己手动拼凑键码尽量用Keyboard库的write接口库会负责把ASCII转成“Shift键码”的组合。库也不是万能的碰到中文输入法里的输出需要先把系统输入法切换到英文不然模拟出来的是拼音字母而不是预期结果。做这个项目对我来说最大的收获是USB协议听起来很玄但把它分解成“枚举、报告、端点轮询”三件事之后一切都有章可循。CH552和HID键盘组合起来既是低成本的USB入门载体也是方便实用的外设基板。以后再做USB鼠标、USB游戏手柄或者自定义U盘走的都是同一套叙事。你先从这段源码开始把它跑通再慢慢改描述符和报告USB开发的大门就对你敞开了。