STM32F103C8T6USB HIDPS2无线手柄踩坑复盘

PS2 无线手柄 → USB HID 游戏手柄
踩坑全复盘

从"游戏不认 COM 口"到 DCS World 实飞可用,两天闭环。四个坑:USB 时钟、GPIO 位操作、HID 描述符、GET/SET_REPORT。

2026-08-05 ⏱ 两天 DCS World 实飞可用

一、缘起:一种另类的"买手柄"

二维云台项目折腾结束,知道了云台的特点,也就没有再折腾——毕竟没达到预期目的,下一步计划是找无刷电机云台。但买这个舵机云台的时候,还附送了一个无线手柄(2.5GHz)。原来它接在 STM32C8T6 上,占着仿真器的引脚。我把它和 C8T6、接收器单独拼凑起来,用 C8T6 模拟 USB-HID 输出手柄数据——这样,我一直心心念念的游戏手柄,也算另一种方式"买了"。

说干就干,先找好所有引脚,让 WorkBuddy 评估可能性,随后开始调试。期间遇到一点小问题,比如 PA12 必须上拉——我把陈年的色环电阻拿出来,用飘逸的手法飞了一个 1.5kΩ 到 3.3V。期间也看到 WorkBuddy 排查方向不对,进行了提醒和多次测试。等于我一直把控测试方向,代码都是 AI 写的。

用了大概一个晚上(2 个小时)+ 一个早上(1 个小时),终于调试通了。

晚上下班回来,用手柄控制心心念念的 DCS World——游戏手柄控制飞机,感觉还真的不太一样。尝试 3 分钟,然后退出游戏,继续折腾别的项目了。这个无线手柄算是定型了。

跑通 STM32 模拟手柄之后我就想:既然能模拟手柄,那 STM32 能模拟的东西有很多,这样就衍生出很多玩法——等于打开一扇门。DIY 就是这么有意思。

二、项目背景与硬件链路

为什么做 USB HID:游戏软件不认 COM 口,所以"PS2 手柄 → 串口 → 上位机"的方案一开始就废了。要的是 USB HID 游戏手柄——插上 Windows 即插即用,游戏直接认。

PS2 无线手柄 (2.4G)
无线

无线接收器 ── PA4/5/6/7 ──► STM32F103C8T6 ── PA11/12 USB ──► Windows HID 驱动 ──► 游戏
GPIO 位操作读 PS2   USB Device (HID Class)   joy.cpl / DCS World

引脚:PA4=ATT / PA5=CLK / PA6=DAT / PA7=CMD(PS2);PA11=DM / PA12=DP(USB,PA12 外部 1.5kΩ 上拉到 3.3V——色环电阻飞线);USART1 调试。

2.5G接收模块
图 12.5G 无线接收模块
C8T6和接收端
图 2STM32F103C8T6 与接收端接线
STM32C8T6模块
图 3STM32F103C8T6 最小系统模块

四、坑 1:USB 时钟 USBPRE 配置错误 → 枚举失败

现象能看到 SOF 帧,但收不到 SETUP 包;ESOF 狂触发;主机报"设备描述符请求失败";最终 SUSPENDED
排查断点停在 USB 中断,wIstr=0x0900(SOF+ESOF);setup_cnt 一直是 0 → 控制传输根本没进来
根因PeriphClkInit.UsbClockSelection 用错宏:RCC_USBCLKSOURCE_PLL = RCC_CFGR_USBPRE(1) → USB 跑 72MHz(应 48MHz,超频 1.5 倍)
修复改用 RCC_USBCLKSOURCE_PLL_DIV1_5(USBPRE=0 → 72MHz÷1.5 = 48MHz
验证立即枚举成功,CONFIGURED,setup 计数正常递增

教训:F103 的 USB 外设必须 48MHz 时钟。而且 ST 宏的命名有误导性——RCC_USBCLKSOURCE_PLL 听起来像"对的",实际是 72MHz 超频;真正对的是带 _DIV1_5 那个。看 datasheet 寄存器位(USBPRE)最可靠。

五、坑 2:PS2 接收器必须 GPIO 位操作,SPI 外设不行

(最耗时的一个坑)

现象[RAW] 打印 9 字节全是 0xFF[PS2] OFF,怎么按手柄都没反应
排查① SPI 有时钟(如果 MISO 卡死低电平才是 0x00,全 0xFF = 线一直是高)→ 排除 SPI 本身没工作;② MISO 整个传输期间恒高 = 接收器根本没驱动 DAT 线;③ 试过配 Mode0/Mode3、改极性,全无果
关键转折用户一句话:"看看之前二维云台的源码是怎么驱动的" → 扒出厂程序 v3.5 的 SRC/ps2/y_ps2.c(杭州友辉)→ 原厂根本没用 SPI 外设,是 GPIO 位操作模拟
根因SPI 外设 Mode0 + MSB 高位先发时,命令字节 0x01 在线上完全反了(00000001 vs 10000000)→ 接收器解不出命令,状态机永不启动 → DAT 永不驱动
修复把原厂位操作原样移植到 PA4-7,DWT 微秒延时

原厂位操作时序(等价 SPI Mode3 + LSB first)

每 bit:
  ① 置 CMD 位(LSB 低位先发)  0x01<<i, i=0..7
  ② CLK 高  延时 6µs           ← 时钟空闲高 = CPOL=1
  ③ CLK 低  延时 6µs
  ④ CLK 高
  ⑤ 采样 DAT(上升沿后)         = CPHA=1
DAT 引脚配下拉输入 (GPIO_Mode_IPD)

教训克隆外设别想当然按"标准协议"去调 SPI 参数——先找原厂驱动看一眼,一分钟的事,省一天的事。位序(LSB/MSB)和空闲极性这种"软参数"反了,外设会一直沉默,永远不会给你"错数据",只给"没数据"。

六、坑 3:HID 接口描述符声明成 BOOT 鼠标

现象设备能被识别,但可能被 Windows 当鼠标类处理
修复bInterfaceSubClass = 0x00(去 BOOT)、bInterfaceProtocol = 0x00(去鼠标)——游戏手柄就该是纯 report 协议
教训ST 的 HID 模板默认是"BOOT 鼠标",改游戏手柄时必须清零这两个字段

七、坑 4:GET_REPORT / SET_REPORT 不处理 → STALL → Windows 重置循环

(最隐蔽的一个坑)

现象PS2 串口数据全通(摇杆/按键都在变),但 joy.cpl 完全没反应;且设备每 ~2 秒被 reset 一次(reset 计数递增、反复 DEFAULT→CONFIGURED)
排查① 报告描述符逐字节核对:合法(Game Pad + 4轴 + Hat + 16键 = 7 字节);② 端点/PMN/中断层全查:标准 ST 代码,配置无错;③ 加 hidin 计数(EP1 IN 传输完成计数)→ 稳定 CONFIGURED 3 秒,hidin 一次都没动(应 ~300 次)→ 主机根本没在轮询 EP1 IN;④ 打印 SETUP 包环形记录,发现主机在重置前发 HID 类请求
根因USBD_HID_Setup 只处理了 SET_PROTOCOL/SET_IDLE 等,没处理 HID_REQ_GET_REPORT(0x01) / SET_REPORT(0x09) → 落 default → USBD_CtlError = STALL → Windows 把 STALL 当设备异常 → 反复重置设备 → 每次重置打断 EP1 IN 轮询 → 报告永远送不出去
修复① 补 GET_REPORT:回 7 字节当前报告;② 补 SET_REPORT:收下丢弃;③ 报告改发精确 7 字节(与描述符一致)
验证hidin 涨到 ~100/秒(≈100Hz 报告率);reset 计数冻结不再涨;joy.cpl 按键/摇杆/十字键全部响应

教训ST 的 HID 模板类 USBD_HID_Setup 是不完整——GET/SET_REPORT 必须自己补。Windows 对设备 STALL 的处理不是"忽略",而是"重置重试",这会造成一个非常隐蔽的死循环:设备功能正常、枚举正常,但报告永远送不到主机。排查这种问题,靠"完成计数 + 看主机发了什么"两个诊断手段,比瞎猜快得多。

八、诊断方法论(可复用)

  1. 计数器体系reset / setup / susp / hidin(IN 完成) / ep1=armed/sent/stall(端点状态)——串口每 500ms 打一轮,直接看链路活没活、卡在哪。
  2. SETUP 包环形记录:中断里存最近 32 个 SETUP 包,主循环打印——直接看主机在 reset 前发什么,这是定位"主机行为"类问题的最快路径。
  3. 原始字节打点[RAW] 打 9 字节原始数据,区分"协议没通"(全 FF)vs"数据解析错"(有数据但不对)。
  4. 一次只加一个变量:每次改动只引入一个诊断量,烧录后看增量变化,避免变量互相干扰。

九、刷新率小知识

本项目现状~100Hz(主循环 11ms/帧 + USB bInterval=10ms)
USB 全速上限1000Hz(中断端点最小 1ms 间隔)
Xbox 360 / One~125Hz
PS4 / PS5~250Hz
高端电竞手柄500 ~ 1000Hz

提速路径(未做,够用)bInterval 0x0A→0x01 + 主循环延时 10→1ms + PS2 位操作 6µs→1~2µs(帧耗时 864µs→~150µs),可达 ~800Hz。

十、结论

电脑端连接最终样子
图 4电脑端最终连接:Windows 已识别为 HID 游戏手柄

全链路打通:PS2 无线手柄(2.4G)→ GPIO 位操作 → STM32F103 → USB HID Gamepad → Windows → DCS World 实飞可用(4 轴正好映射副翼/升降舵/方向舵/油门)。

最大的收获不是"手柄能用",而是四个坑背后的知识:

调试状态的样子
图 5调试状态实拍:串口打计数 + 逻辑分析

* 配套代码开源:gitee.com/gaokun3540/ps2-usb-gamepad —— README 记录了同样的四个坑。

← 返回