基于STM32的USB HID桥接器:树莓派远程控制PC的硬件方案
1. 项目概述与核心需求解析
最近在折腾一个远程办公的自动化场景,遇到了一个挺有意思的需求:如何在不依赖目标PC上安装任何软件的前提下,让一台树莓派(Raspberry Pi)能够模拟成键盘、鼠标或者游戏手柄,向PC发送输入指令。简单来说,就是打造一个“USB HID桥接器”。这个想法源于几个实际痛点:比如,如何远程为一台设置了全盘加密启动密码的服务器输入密码;或者,在会议室里用树莓派控制讲台上的电脑播放PPT,而无需插拔线缆或安装远程控制软件;再比如,构建一个长距离的“键盘延长线”,将输入设备放在更符合人体工学的位置。
市面上确实有一些基于Arduino的类似项目,但它们往往功能单一,定制化程度高,缺乏一个通用、稳定且开源的解决方案。大多数方案要么是单向的,要么需要复杂的驱动配合。因此,我的目标是设计一个硬件+固件+软件库的完整方案。硬件上,它是一块独立的PCB板,一端通过USB连接到PC,被PC识别为标准HID设备(键盘/鼠标);另一端通过UART或SPI与树莓派通信,接收来自树莓派的指令。这样一来,树莓派上运行的程序就能通过一个简单的API,随心所欲地“敲击”PC的键盘,“移动”PC的鼠标。这个项目的核心价值在于其“无侵入性”和“高灵活性”——PC端无需任何额外驱动或软件,完全将其视为一个普通外设;而树莓派端则可以利用Python、C等丰富的生态,实现复杂的自动化逻辑。
2. 硬件方案设计与核心芯片选型
硬件是整个桥接器的基石,需要在成本、性能、开发难度和灵活性之间取得平衡。核心是一个微控制器(MCU),它需要扮演两个关键角色:一是作为USB设备,与PC进行符合USB HID规范的通信;二是作为串行设备,与树莓派进行高速、可靠的数据交换。
2.1 微控制器选型:PIC32与替代方案深度对比
项目发起者提到了PIC32,这是一个非常稳妥的选择。Microchip为其提供了成熟的USB协议栈(MLA或Harmony),大大降低了开发USB HID设备的门槛。PIC32MX或PIC32MZ系列通常内置USB OTG或Host/Device控制器,性能足以流畅处理键盘、鼠标的输入报告。其优势在于生态成熟、资料丰富,对于有相关经验的开发者来说,可以快速上手。
然而,从社区活跃度、开发便利性和成本角度考虑,我们有必要审视其他方案。 STM32系列 ,特别是STM32F0/F1/F3系列,是极具竞争力的替代者。它们同样拥有强大的USB外设和丰富的社区资源(如STM32CubeMX和HAL库)。基于ARM Cortex-M内核的STM32,其开发工具链(如Keil, IAR, 以及免费的STM32CubeIDE)对初学者更友好。社区内已有大量成熟的USB HID示例,例如使用STM32F103的“Blue Pill”开发板实现USB键盘/鼠标的案例比比皆是,这可以极大缩短原型开发周期。
RP2040 (树莓派Pico的核心)是另一个“黑马”选择。它双核Cortex-M0+的设计,允许一个核心专用于处理USB通信,另一个核心处理与树莓派的数据协议,并行效率更高。其独特的PIO(可编程输入输出)状态机甚至可以用于实现自定义的高速串行协议,为未来扩展(如模拟USB大容量存储设备)预留了硬件层面的可能性。更重要的是,如果你整个系统都是树莓派生态,使用RP2040在工具链和调试上会有一定的一致性优势。
选型心得 :对于追求快速验证和社区支持的项目, STM32F103 或 RP2040 是更优的起点。它们的开发板价格低廉(通常20-50元人民币),且有海量的开源参考项目。如果项目对USB协议栈的稳定性和厂商支持有极高要求,且团队熟悉Microchip生态,那么PIC32仍是可靠的选择。本项目后续的讨论将基于 STM32F103C8T6 (即Blue Pill)展开,因其性价比和社区资源最为突出。
2.2 电路设计与接口定义
确定了MCU后,我们需要设计其外围电路。核心电路包括:
- USB接口电路 :采用Micro-USB或USB Type-C接口。需包含ESD保护二极管(如SRV05-4)和匹配电阻,确保信号完整性和设备安全。
- 电源管理 :MCU可通过USB总线取电(5V)。同时,需要一颗LDO(如AMS1117-3.3)将5V转换为3.3V,为MCU和电平转换电路供电。
-
与树莓派的通信接口
:这是设计的重点。我们预留两种方式:
- UART (首选) :最简单、最通用的异步串口。需要在MCU和树莓派的GPIO之间加入 电平转换电路 (如TXS0108E或分压电阻),因为树莓派GPIO是3.3V电平,而STM32F103的IO口也是3.3V,两者可直接连接,但为保险起见,仍建议使用电平转换芯片以隔离和保护。我们使用USART1(PA9/PA10)。
- SPI (备用/高速) :用于未来传输大量数据,例如模拟U盘传输文件时。使用SPI1(PA5/PA6/PA7)。
- 模式控制GPIO :预留1-2个MCU的GPIO连接至树莓派,用于控制桥接器的工作模式。例如,一个高电平信号可以让MCU从“HID注入模式”切换至“USB-UART透明桥模式”,此时设备变成一个简单的USB转串口适配器,方便调试或他用。
- 状态指示 :至少需要一颗LED连接到MCU,用于指示电源、枚举成功、数据收发等状态。
原理图设计应追求清晰、模块化。USB数据线(D+, D-)的走线需尽可能等长、短粗,并做好包地处理,这在后续的PCB设计中至关重要。
3. 固件开发:让MCU成为标准HID设备
固件是桥接器的“灵魂”,它决定了PC如何看待这个设备。我们的目标是让PC即插即用地识别出一个键盘和/或鼠标。
3.1 USB HID设备描述符配置
这是最关键的一步。设备描述符告诉PC:“我是一个复合设备,包含一个键盘和一个鼠标。” 在STM32CubeMX中配置USB设备为“HID (Human Interface Device)”类后,我们需要精心编写报告描述符(Report Descriptor)。
对于键盘,报告描述符需要定义8字节的输入报告:第一个字节是修饰键(Ctrl, Shift, Alt, GUI),第二个字节保留,后续6个字节是普通按键的键码。同时还需要定义输出报告(用于控制键盘LED,如大小写锁定灯)。
对于鼠标,我们可以定义一个简单的3字节输入报告:第一个字节表示按键(左、中、右),第二、三个字节分别表示X轴和Y轴的相对移动量。如果需要滚轮,可以增加第四个字节。
在CubeMX生成的代码框架中,我们需要修改
usbd_hid.c
中的
HID_MOUSE_ReportDesc
和
HID_KEYBOARD_ReportDesc
(如果创建了复合设备),或者自定义一个包含两者的单一报告描述符。
// 示例:一个简单的鼠标报告描述符(相对坐标,3字节)
__ALIGN_BEGIN static uint8_t HID_MOUSE_ReportDesc[] __ALIGN_END =
{
0x05, 0x01, // Usage Page (Generic Desktop)
0x09, 0x02, // Usage (Mouse)
0xA1, 0x01, // Collection (Application)
0x09, 0x01, // Usage (Pointer)
0xA1, 0x00, // Collection (Physical)
0x05, 0x09, // Usage Page (Buttons)
0x19, 0x01, // Usage Minimum (Button 1)
0x29, 0x03, // Usage Maximum (Button 3)
0x15, 0x00, // Logical Minimum (0)
0x25, 0x01, // Logical Maximum (1)
0x95, 0x03, // Report Count (3)
0x75, 0x01, // Report Size (1)
0x81, 0x02, // Input (Data, Var, Abs) ; 3个按钮位
0x95, 0x01, // Report Count (1)
0x75, 0x05, // Report Size (5) ; 5位填充
0x81, 0x03, // Input (Const, Var, Abs) ; 常量填充
0x05, 0x01, // Usage Page (Generic Desktop)
0x09, 0x30, // Usage (X)
0x09, 0x31, // Usage (Y)
0x15, 0x81, // Logical Minimum (-127)
0x25, 0x7F, // Logical Maximum (127)
0x75, 0x08, // Report Size (8)
0x95, 0x02, // Report Count (2)
0x81, 0x06, // Input (Data, Var, Rel) ; 相对坐标X,Y
0xC0, // End Collection
0xC0 // End Collection
};
3.2 通信协议解析与实现
MCU通过UART接收来自树莓派的指令,并解析执行。我们需要设计一个简单、高效且容错的轻量级协议。
协议帧结构设计: 为了可靠地区分每一帧指令,我们采用“帧头+长度+命令+数据+校验”的结构。
[帧头 0xAA] [帧头 0x55] [数据长度 N] [命令字 CMD] [数据区 DATA...] [校验和 SUM]
- 帧头 (2字节) :固定的0xAA, 0x55,用于帧同步。
- 数据长度 (1字节) :表示CMD+DATA的总字节数。
-
命令字 (1字节)
:定义操作类型,例如:
-
0x01:键盘按键按下 -
0x02:键盘按键释放 -
0x03:鼠标移动 -
0x04:鼠标按键 -
0x05:切换工作模式
-
- 数据区 (N-1字节) :根据命令字变化。例如,对于键盘按键,数据区可包含修饰键字节和键码字节;对于鼠标移动,包含两个有符号的8位位移量。
- 校验和 (1字节) :通常为从“长度”字节到“数据区”最后一个字节所有字节的简单累加和(取低8位),用于检测传输错误。
固件侧解析流程:
- 在UART中断服务程序中,将接收到的字节存入环形缓冲区。
-
在主循环中,不断从缓冲区搜索帧头
0xAA 0x55。 - 找到帧头后,读取后续的“长度”字节,然后等待接收足够数量的后续字节。
-
数据收齐后,计算校验和并与接收到的校验和比对。如果一致,则根据“命令字”调用相应的处理函数(如
USB_HID_Send_Keyboard_Report)。 - 处理完毕后,继续搜索下一个帧头。
避坑指南 :UART通信必须考虑数据粘包和断帧。使用环形缓冲区是标准做法。另外,校验和虽然简单,但能过滤掉绝大部分因干扰导致的错误数据,避免MCU执行乱码指令,造成鼠标乱飞或键盘乱打字的灾难性后果。建议在协议设计初期就加入。
4. 树莓派端软件库开发
桥接器的另一半大脑在树莓派上。我们需要开发一个软件库,让Python、C甚至Shell脚本都能方便地调用,将“按下A键”、“移动鼠标”这样的高级指令,转换成通过串口发送的低层协议帧。
4.1 核心库(C语言)实现
首先用C实现一个核心库(例如
libpihidbridge.a
或
.so
),提供最基础的API。
// pihidbridge.h
#ifndef PIHIDBRIDGE_H
#define PIHIDBRIDGE_H
#include <stdint.h>
// 初始化库,打开指定的串口设备(如 /dev/ttyAMA0 或 /dev/ttyUSB0)
int pihidbridge_init(const char *serial_port, int baudrate);
// 清理资源,关闭串口
void pihidbridge_cleanup();
// 发送键盘按键事件
// modifiers: 修饰键位掩码 (e.g., CTRL_MASK, SHIFT_MASK)
// keycode: USB HID 键码 (e.g., KEY_A, KEY_ENTER)
// is_press: 1为按下,0为释放
int pihidbridge_keyboard_event(uint8_t modifiers, uint8_t keycode, int is_press);
// 发送鼠标移动事件
// rel_x, rel_y: 相对移动量(有符号8位整数)
int pihidbridge_mouse_move(int8_t rel_x, int8_t rel_y);
// 发送鼠标按键事件
// button_mask: 按键掩码 (e.g., MOUSE_LEFT, MOUSE_RIGHT)
// is_press: 1为按下,0为释放
int pihidbridge_mouse_button(uint8_t button_mask, int is_press);
#endif
库的内部实现,就是按照第3.2节定义的协议,将API调用参数组装成帧,通过串口(使用
termios
库配置)发送出去。它负责处理串口的打开、配置、读写以及协议帧的封装。
4.2 Python绑定与高级封装
有了C核心库,我们可以用
ctypes
轻松创建Python绑定,使其在树莓派上更易用。
# pihidbridge.py
import ctypes
import time
class HIDBridge:
def __init__(self, port='/dev/ttyAMA0', baudrate=115200):
self.lib = ctypes.CDLL('./libpihidbridge.so') # 或指定路径
self.lib.pihidbridge_init.argtypes = [ctypes.c_char_p, ctypes.c_int]
self.lib.pihidbridge_init.restype = ctypes.c_int
if self.lib.pihidbridge_init(port.encode(), baudrate) != 0:
raise Exception(f"Failed to initialize HID Bridge on {port}")
def __del__(self):
self.lib.pihidbridge_cleanup()
def key_press(self, keycode, modifiers=0):
self.lib.pihidbridge_keyboard_event(modifiers, keycode, 1)
time.sleep(0.01) # 短暂延时模拟人手
self.lib.pihidbridge_keyboard_event(modifiers, keycode, 0)
def mouse_move(self, dx, dy):
self.lib.pihidbridge_mouse_move(ctypes.c_int8(dx), ctypes.c_int8(dy))
def mouse_click(self, button='left'):
button_map = {'left': 0x01, 'right': 0x02, 'middle': 0x04}
mask = button_map.get(button, 0x01)
self.lib.pihidbridge_mouse_button(mask, 1)
time.sleep(0.05)
self.lib.pihidbridge_mouse_button(mask, 0)
# 使用示例
if __name__ == '__main__':
bridge = HIDBridge('/dev/ttyUSB0', 115200)
bridge.mouse_move(50, 0) # 鼠标右移50像素
bridge.key_press(0x04) # 按下并释放‘a’键 (假设0x04是键码)
bridge.mouse_click('right')
更进一步,可以基于这个库封装更高级的功能,例如
type_string("Hello World")
、
move_mouse_to(x, y)
(通过相对移动迭代实现)等。
4.3 系统集成与开机自启动
为了让桥接器随时可用,我们需要将其集成到树莓派系统中。
-
udev规则
:创建
/etc/udev/rules.d/99-pihidbridge.rules文件,内容如下:
这条规则会在STM32设备(假设其USB VID/PID为0483:5740)连接到树莓派USB口时,自动在SUBSYSTEM=="tty", ATTRS{idVendor}=="0483", ATTRS{idProduct}=="5740", SYMLINK+="pihidbridge", MODE="0666"/dev下创建一个固定的符号链接/dev/pihidbridge,并设置所有用户可读写的权限,这样我们的软件就不需要关心具体的ttyUSBx编号了。 -
系统服务
:编写一个systemd服务单元文件
/etc/systemd/system/pihidbridge-daemon.service,用于在开机时自动启动一个守护进程,或者至少确保库文件已就绪。更常见的做法是让用户脚本按需调用库,而非常驻进程。 - GPIO模式控制 :树莓派端的库还可以扩展一个函数,通过控制连接MCU模式引脚的那个GPIO(需在硬件设计时确定),来动态切换桥接器的工作模式(HID模式 vs. 透明串口模式)。这可以通过RPi.GPIO或gpiod库实现。
5. 应用场景实例与脚本编写
有了硬件和软件基础,我们就可以实现一些开篇提到的酷炫应用了。以下是一些具体的脚本示例。
5.1 场景一:远程加密磁盘解锁
假设你的服务器设置了GRUB加密,每次启动都需要在本地键盘输入密码。现在,你可以将树莓派和桥接器一直连接在服务器上,树莓派通过网络接收指令。
# unlock_server.py
import socket
import threading
from pihidbridge import HIDBridge
import hashlib
import time
def listen_for_unlock_command(bridge):
# 简化的socket服务器,接收解锁指令
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(('0.0.0.0', 65432)) # 注意安全,应使用更安全的认证
server.listen(1)
while True:
conn, addr = server.accept()
# 这里应加入加密认证
password = conn.recv(1024).decode().strip()
# 模拟键盘输入密码并回车
bridge.type_string(password)
bridge.key_press(0x28) # KEY_ENTER
conn.send(b"Unlock command sent.\n")
conn.close()
if __name__ == '__main__':
bridge = HIDBridge('/dev/pihidbridge')
# 启动监听线程
thread = threading.Thread(target=listen_for_unlock_command, args=(bridge,))
thread.daemon = True
thread.start()
print("Unlock daemon is running...")
while True:
time.sleep(1)
在办公室的电脑上,你可以通过一个简单的客户端脚本,安全地(务必使用SSH隧道或VPN!)向树莓派发送密码,触发解锁流程。
5.2 场景二:长距离输入与媒体控制
将树莓派和桥接器放在客厅,连接客厅的HTPC(家庭影院电脑)。在书房用树莓派Zero W(更小巧)通过SSH登录到客厅的树莓派,运行控制脚本。
# media_controller.py
from pihidbridge import HIDBridge
import time
bridge = HIDBridge('/dev/pihidbridge')
def play_pause():
# 发送多媒体“播放/暂停”键 (HID用法页0x0C,用法0xCD)
# 需要先在键盘报告描述符中支持多媒体键,或模拟系统快捷键,如Win+Shift+空格
bridge.key_press(0xCD) # 假设已扩展支持
def volume_up():
bridge.key_press(0xE9) # 音量+
def volume_down():
bridge.key_press(0xEA) # 音量-
def next_track():
bridge.key_press(0xB5) # 下一曲
# 可以绑定到物理按钮或网络API
if __name__ == '__main__':
# 例如,每按一次物理按钮(通过GPIO检测),就执行一次
play_pause()
time.sleep(2)
next_track()
5.3 场景三:自动化测试与演示脚本
对于需要重复进行UI操作或演示的场合,这个桥接器是绝佳的工具。
# demo_script.py
from pihidbridge import HIDBridge
import time
bridge = HIDBridge('/dev/pihidbridge')
def open_terminal():
# 模拟按下 Ctrl+Alt+T (Linux下打开终端)
bridge.keyboard_event(0x05 | 0x04, 0x00, 1) # 按下Ctrl+Alt
bridge.key_press(0x17) # 按下t键
bridge.keyboard_event(0x05 | 0x04, 0x00, 0) # 释放Ctrl+Alt
time.sleep(1) # 等待终端打开
def type_and_run_command(command):
bridge.type_string(command)
bridge.key_press(0x28) # 回车
time.sleep(0.5)
print("Starting automated demo...")
time.sleep(3) # 给操作者切换到目标窗口的时间
open_terminal()
type_and_run_command("echo 'Hello from Raspberry Pi HID Bridge!'")
type_and_run_command("ls -la")
type_and_run_command("exit")
print("Demo finished.")
6. 调试技巧、常见问题与未来扩展
在实际制作和使用的过程中,你肯定会遇到各种各样的问题。这里分享一些踩过的坑和解决方法。
6.1 硬件调试清单
-
PC无法识别USB设备 :
- 检查供电 :确保USB线能提供足够电流。尝试更换USB口或使用带外部电源的Hub。
- 检查数据线 :有些USB线只能充电,无法传输数据。
-
检查原理图和PCB
:重点检查USB D+和D-线上是否有正确的上拉电阻(通常D+需要1.5k上拉到3.3V)。STM32需要正确配置
USB_DP引脚的上拉内部或外部使能。 - 使用USB分析仪 :如Saleae逻辑分析仪配合USB协议分析软件,可以直观看到枚举过程在哪一步失败,是硬件问题还是描述符问题。
-
树莓派与MCU通信失败 :
- 确认电平 :确保双方串口TX/RX交叉连接,且电平匹配(都是3.3V)。
- 确认波特率 :双方波特率必须严格一致(如115200)。STM32的时钟配置(HCLK, PCLK)会影响USART波特率生成器的精度。
- 检查地线连接 :共地是通信的基础,务必确保树莓派和桥接板有良好的共地。
6.2 软件/固件调试清单
-
设备枚举成功但输入无反应 :
-
检查报告描述符
:使用
lsusb -v命令(Linux)或USBlyzer等工具(Windows)查看设备枚举出的报告描述符,与代码中的定义对比。 -
检查报告发送函数
:确保
USBD_HID_SendReport()函数被正确调用,且返回值为USBD_OK。发送频率不宜过快,需等待上一次发送完成。 - 检查协议解析 :在MCU端,将接收到的UART数据通过调试串口打印出来,确认树莓派发送的协议帧格式完全正确。
-
检查报告描述符
:使用
-
输入延迟或丢帧 :
- 优化协议 :确保协议帧足够精简。对于鼠标移动,可以设计一个“批量移动”命令,一次发送多个位移数据包,减少协议开销。
- 提高波特率 :在布线质量允许的情况下,将UART波特率从115200提升到921600甚至更高。
-
检查树莓派CPU负载
:如果树莓派正在执行高负载任务,可能会影响串口数据的实时发送。可以考虑提高脚本进程的优先级(
nice值)。
6.3 未来功能扩展思路
这个项目的框架具有很强的可扩展性。
- 复合设备增强 :除了键盘鼠标,可以增加游戏手柄(Joystick)、摇杆(Game Pad)甚至自定义控制面板的HID报告描述符。
- 模拟USB大容量存储设备(U盘) :这是原设想中提到的“动态文件系统”。MCU端实现USB Mass Storage类,树莓派通过SPI高速向MCU传输文件数据。MCU可以将这些数据虚拟成一个FAT32格式的磁盘映像呈现给PC。这可以实现“加密U盘”或“网络挂载盘”的效果。
- 无线化 :将树莓派替换为ESP32等带有Wi-Fi/蓝牙的MCU,直接通过无线网络接收指令,让桥接器完全摆脱线缆束缚。
- 集成传感器 :在桥接器板上集成陀螺仪、加速度计,将其变成一个体感鼠标或空中鼠标。
整个项目从构思到实现,最深的体会是“分而治之”的重要性。将复杂的“树莓派控制PC”问题,拆解为“树莓派与MCU通信”和“MCU模拟标准USB设备”两个相对独立且均有成熟方案的问题,极大地降低了难度。硬件上的一次性投入,换来的是软件层面无限的自动化可能性。当你第一次用自己编写的脚本,让远在几米外的电脑自动输入命令时,那种成就感远超乎想象。
更多推荐

所有评论(0)