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后,我们需要设计其外围电路。核心电路包括:

  1. USB接口电路 :采用Micro-USB或USB Type-C接口。需包含ESD保护二极管(如SRV05-4)和匹配电阻,确保信号完整性和设备安全。
  2. 电源管理 :MCU可通过USB总线取电(5V)。同时,需要一颗LDO(如AMS1117-3.3)将5V转换为3.3V,为MCU和电平转换电路供电。
  3. 与树莓派的通信接口 :这是设计的重点。我们预留两种方式:
    • UART (首选) :最简单、最通用的异步串口。需要在MCU和树莓派的GPIO之间加入 电平转换电路 (如TXS0108E或分压电阻),因为树莓派GPIO是3.3V电平,而STM32F103的IO口也是3.3V,两者可直接连接,但为保险起见,仍建议使用电平转换芯片以隔离和保护。我们使用USART1(PA9/PA10)。
    • SPI (备用/高速) :用于未来传输大量数据,例如模拟U盘传输文件时。使用SPI1(PA5/PA6/PA7)。
  4. 模式控制GPIO :预留1-2个MCU的GPIO连接至树莓派,用于控制桥接器的工作模式。例如,一个高电平信号可以让MCU从“HID注入模式”切换至“USB-UART透明桥模式”,此时设备变成一个简单的USB转串口适配器,方便调试或他用。
  5. 状态指示 :至少需要一颗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位),用于检测传输错误。

固件侧解析流程:

  1. 在UART中断服务程序中,将接收到的字节存入环形缓冲区。
  2. 在主循环中,不断从缓冲区搜索帧头 0xAA 0x55 。
  3. 找到帧头后,读取后续的“长度”字节,然后等待接收足够数量的后续字节。
  4. 数据收齐后,计算校验和并与接收到的校验和比对。如果一致,则根据“命令字”调用相应的处理函数(如 USB_HID_Send_Keyboard_Report )。
  5. 处理完毕后,继续搜索下一个帧头。

避坑指南 :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 系统集成与开机自启动

为了让桥接器随时可用,我们需要将其集成到树莓派系统中。

  1. udev规则 :创建 /etc/udev/rules.d/99-pihidbridge.rules 文件,内容如下:
    SUBSYSTEM=="tty", ATTRS{idVendor}=="0483", ATTRS{idProduct}=="5740", SYMLINK+="pihidbridge", MODE="0666"
    
    这条规则会在STM32设备(假设其USB VID/PID为0483:5740)连接到树莓派USB口时,自动在 /dev 下创建一个固定的符号链接 /dev/pihidbridge ,并设置所有用户可读写的权限,这样我们的软件就不需要关心具体的 ttyUSBx 编号了。
  2. 系统服务 :编写一个systemd服务单元文件 /etc/systemd/system/pihidbridge-daemon.service ,用于在开机时自动启动一个守护进程,或者至少确保库文件已就绪。更常见的做法是让用户脚本按需调用库,而非常驻进程。
  3. 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 硬件调试清单

  1. PC无法识别USB设备 :

    • 检查供电 :确保USB线能提供足够电流。尝试更换USB口或使用带外部电源的Hub。
    • 检查数据线 :有些USB线只能充电,无法传输数据。
    • 检查原理图和PCB :重点检查USB D+和D-线上是否有正确的上拉电阻(通常D+需要1.5k上拉到3.3V)。STM32需要正确配置 USB_DP 引脚的上拉内部或外部使能。
    • 使用USB分析仪 :如Saleae逻辑分析仪配合USB协议分析软件,可以直观看到枚举过程在哪一步失败,是硬件问题还是描述符问题。
  2. 树莓派与MCU通信失败 :

    • 确认电平 :确保双方串口TX/RX交叉连接,且电平匹配(都是3.3V)。
    • 确认波特率 :双方波特率必须严格一致(如115200)。STM32的时钟配置(HCLK, PCLK)会影响USART波特率生成器的精度。
    • 检查地线连接 :共地是通信的基础,务必确保树莓派和桥接板有良好的共地。

6.2 软件/固件调试清单

  1. 设备枚举成功但输入无反应 :

    • 检查报告描述符 :使用 lsusb -v 命令(Linux)或USBlyzer等工具(Windows)查看设备枚举出的报告描述符,与代码中的定义对比。
    • 检查报告发送函数 :确保 USBD_HID_SendReport() 函数被正确调用,且返回值为 USBD_OK 。发送频率不宜过快,需等待上一次发送完成。
    • 检查协议解析 :在MCU端,将接收到的UART数据通过调试串口打印出来,确认树莓派发送的协议帧格式完全正确。
  2. 输入延迟或丢帧 :

    • 优化协议 :确保协议帧足够精简。对于鼠标移动,可以设计一个“批量移动”命令,一次发送多个位移数据包,减少协议开销。
    • 提高波特率 :在布线质量允许的情况下,将UART波特率从115200提升到921600甚至更高。
    • 检查树莓派CPU负载 :如果树莓派正在执行高负载任务,可能会影响串口数据的实时发送。可以考虑提高脚本进程的优先级( nice 值)。

6.3 未来功能扩展思路

这个项目的框架具有很强的可扩展性。

  1. 复合设备增强 :除了键盘鼠标,可以增加游戏手柄(Joystick)、摇杆(Game Pad)甚至自定义控制面板的HID报告描述符。
  2. 模拟USB大容量存储设备(U盘) :这是原设想中提到的“动态文件系统”。MCU端实现USB Mass Storage类,树莓派通过SPI高速向MCU传输文件数据。MCU可以将这些数据虚拟成一个FAT32格式的磁盘映像呈现给PC。这可以实现“加密U盘”或“网络挂载盘”的效果。
  3. 无线化 :将树莓派替换为ESP32等带有Wi-Fi/蓝牙的MCU,直接通过无线网络接收指令,让桥接器完全摆脱线缆束缚。
  4. 集成传感器 :在桥接器板上集成陀螺仪、加速度计,将其变成一个体感鼠标或空中鼠标。

整个项目从构思到实现,最深的体会是“分而治之”的重要性。将复杂的“树莓派控制PC”问题,拆解为“树莓派与MCU通信”和“MCU模拟标准USB设备”两个相对独立且均有成熟方案的问题,极大地降低了难度。硬件上的一次性投入,换来的是软件层面无限的自动化可能性。当你第一次用自己编写的脚本,让远在几米外的电脑自动输入命令时,那种成就感远超乎想象。

更多推荐