制作一个ESP32 Fps游戏雷达

前言

最近的具身智能是真火,AI这一波不光带火了大模型,还顺手把机器人也带飞了。AI的爆发不仅催生了语言模型和图像生成,更让物理世界中的小机器人开始走进大众视野——从桌面机械臂到开源机器鸭,形态各异、层出不穷。最近刷抖音,Hugging Face的机器鸭是真好看,我也用ESP32 整个小活,用ESP32给FPS游戏做个硬件雷达玩玩。

ESP32

做一个能实时显示敌人位置的硬件雷达。那主控芯片选谁?本来想玩树莓派,但是想想这玩意完全性能过剩,要啥自行车呢 ,干脆20元的ESP32来的直接。

买的时候还送我了一个0.96寸的OLED的显示器,刚开始还能凑合着显示,但是用来显示雷达可玩性太低了,后来又买了一个1.8英寸的显示器显示,才用了雷达的味道

ESP-IDF开发环境

ESP-IDF(Espressif IoT Development Framework)是乐鑫官方为其 ESP32 系列芯片推出的物联网开发框架,相当于esp的sdk环境。

windows环境可以在官网可以下载在线安装包或者离线安装包进行配置,笔者直接下载了离线的安装包,在线的有点担心网络问题,下载地址:https://dl.espressif.com/dl/eim/,离线下载包大小在3.19G

这里我直接选用VS Code + ESP-IDF 插件进行开发,安装之后配置一下环境,这里的屏幕需要安装ESP LCD ST77916的驱动依赖,可以使用idf.py进行一键安装,依赖可以在乐鑫的仓库找到,这里建议依赖装上LVGL的依赖优化UI比较舒服

屏幕端口连接数据主要如下

数据读取

采用驱动读写物理内存获取人物坐标、API获取人物信息等,为什么不用DMA,那玩意太贵买不起,不过对于一个内核驱动开发者来说,也丧失了内核对抗的乐趣,这里采用读取物理内存+UDP传输给服务的形式进行转发数据。这块关键在于学习UE4引擎相关反射机制,学习了一些UWORD、GNAME相关结构体,就能获取一些坐标数据了,这里顺便简单的绘制了一下火柴人哈哈哈。

刚开始采用0.96英寸的屏幕进行雷达显示,太鸡肋了,效果如下:

鼠标移动Makcu(两个ESP32)

既然要做硬件,偶然间看到网上有卖的鼠标盒子能够解析鼠标数据,合成鼠标移动数据,一看咸鱼20块钱,二话不说买了一个,买回来分析玩玩。仔细看了看,Makcu鼠标盒子是由两个ESP32组成的,一个负责移动鼠标,一个负责合成鼠标数据,网上开源的版本很早之前的了,这里直接提取固件丢入IDA进行分析,分析之前需要将esp32烧录的固件提取成elf文件,使其可以IDA分析。

提取factory(app 分区) :先用改过的解析器把分区表读出来,确认 app 镜像在 0x10000 偏移处、声明 1MB(实际固件只占了前 328KB)。网上现成的 esp32_image_parser 只认老 ESP32,直接跑会把 S3 的段解析错,照着 ESP32-S3 的镜像结构补了段映射——DRAM、BYTE_ACCESSIBLE_DRAM 归到 .dram0.data,RTC_IRAM 归到 .rtc.text(这些上游原版都是 TODO),芯片类型改成 esp32s3

拖入IDA分析, 架构是ESP32-S3 (Xtensa LX7)、通过字符串可以确定USB 栈是CherryUSB Host(usbh_core/usbh_hid/hub/dwc2)进行鼠标操作,主要是进行HID报告的操作

线上帧格式(右 → 左)

[0]    0x5A        帧头魔数
[1]    0xA2        类型:HID 报告帧
[2:3]  uint16 LE   长度 = payload_len + 4(即 [4] 到帧尾的字节数)
[4]    传输结构体 +0x8F 的字节(接口/报告属性)
[5]    端点字节(低 4 位 = 端点号)
[6]    payload_len
[7]    0x00
[8:]   原始 HID 报告(按钮/X/Y/滚轮,由 USB 控制器直接 DMA 写入)

简单的看了看剩下的交给AI连接IDA进行逆向了,具体报告如下:

ESP32-S3 鼠标数据合成分析报告

一、固件身份

通过横幅字符串 0x3C043370 确认:

MAKCU 2024-2025
Designer: .ihack
www.makcu.com
Firmware version: 3.8

这是 MAKCU(makcu.com)固件 v3.8 —— 一个基于 ESP32-S3 的 USB HID 鼠标仿真/自动化设备(游戏辅助鼠标控制,含锁轴/按键捕捉等)。源码路径内嵌:main/components/parse/parse.c、main/components/Injection_Queue/Injection_Queue.c、main/components/UART/uart0.c、main/components/RGB/RGB.c。USB 栈为 CherryUSB 风格 usbd + DWC2 控制器(字符串 usbd_core、usbd_ep0、g_usbd_core[busid].usbd_ep0_mq 等)。

二、总体架构(数据流向)

NVS配置(baud_rate/screen_w=1920/screen_h=1080)
        │
命令层:  esp_console 命令 (mouse/buttons/wheel/move/click/moveto/lock_*/catch_*/mask/remap...)
        + UART 二进制协议 (uart_events: 0xA1/0xA4/0xA5/'.' 前缀分发)
        │                     │
        ▼                     ▼
状态机:  hid_dev 结构 (obj+0x210) ──► mouse_report_data (0x1224字节, hid_dev+0x2B8)
        motion_state 位于 +0x1218~0x1220
        │
        ▼
合成器:  CombinedCommitTask (0x4200EDE4, 轮询任务)
        └─► sub_42011280 (核心合成: 按键位图+XY/wheel提取+限幅)
             └─► sub_4201538C (入队+任务通知)
                  │
                  ▼
发送:    hid_data 任务 (0x420151B8)
        └─► sub_42016C70 → 直接编程 DWC2 寄存器 (DIEPCTL/DIEPTSIZ/DIEPDMA)
             └─► USB IN 端点 → 主机PC

三、合成链路关键函数

1. 报告描述符解析(parse.c)
  • sub_420133D8— 解析 HID 报告描述符:主项/全局项/局部项分别交给 sub_42012D7C / sub_420130B4 / sub_4202DF3C(条目解析器 sub_4202DE2C),建立每个接口的按键位图表(hid_dev+0x50/0x54/0x58/0x5C/0x60/0x64)

  • sub_4201326C — finalize_parse(调试串 ep->mouse_report_data->motion_state @ parse.c:0x584):分配 0x1224 字节的 mouse_report_data,清零 motion_state,计算每接口报告字节数(hid_dev+0x76、ep+0x61)

2. 核心合成器 sub_42011280(0x42011280,最重要的函数)

接收 (obj, iface, report_ptr, size):

  1. 校验报告长度 == hid_dev+0x76(预期报告大小)

  2. memcpy 原始报告到栈缓冲区

  3. sub_420111C8 应用按键位图;sub_4202D900 转换

  4. 从解析表提取按键/X/Y/wheel 位字段(sub_4202D888),原子累加到 hid_dev+0x2CC/0x2D0/0x2D8

  5. 按屏幕尺寸(dword_42000BA8+0xC/+0x10,来自 NVS screen_w/screen_h)限幅绝对坐标到 +0x2C2/+0x2C4

  6. sub_4202DD00 把 0x10 字节 motion 结构写入 state+0x2FC(motion_state)

  7. 触发三个监控任务(sub_42010228/sub_42010304/sub_42010420,带节流:+0x19C/0x1A4/0x2E0 存时间戳、+0x19A/0x1A2/0x2DE 存周期,按 raw/mut/off/abs/rel/act 模式打印)

  8. sub_4201538C 将合成报告入队并通知 hid_data 任务(通道号 = 接口号,xTaskNotify 位 0~5)

3. 注入队列(Injection_Queue.c)
  • sub_4200FB6C = injection_queue_init:创建互斥锁 + 0x264 字节 click_queue(611 条 × 0x1C 事件)+ ClickTask
  • sub_4200F388— 按时间戳二分插入排序入队(事件含调度时间、动作类型 1/2/3、参数)

  • sub_4200F4F0 — ClickTask:阻塞接收队列,按 按下→N次重复(带延迟)→释放 的时序注入按键状态

4. 发送链路
  • sub_420151B8— hid_data 任务:等待通知位,逐通道调用获取回调 → sub_42016C70 发送,失败走释放回调,维护每通道计数器

  • sub_42016C70]— 直接写 DWC2 USB 设备控制器 MMIO 寄存器(传输大小 0x13 位移字段 = DWC2 DIEPTSIZ 布局;0x800/0x900 偏移 = IN/OUT 端点)

  • sub_42015BEC— usbd_core 每总线传输完成轮询(事件 0=数据/2=复位,字节序转换拷贝,错误 "Request buffer too small")

5. 低层状态 API(0x4202DB9C~0x4202DD8C,成对的 get/set)

函数

功能

状态偏移

sub_4202DB9C

按键状态查询(16键位测试)

+0x210+button*4+0xF8

sub_4202DBD4 / sub_4202DC0C

读/写 buttons命令状态(u8+u16+时间戳)

+0x199/+0x19A/+0x19C

sub_4202DC9C / sub_4202DCD4

读/写 mouse命令状态

+0x1A0/+0x1A2/+0x1A4

sub_4202DC38 / sub_4202DC70

读/写 wheel命令状态

+0x2DC/+0x2DE/+0x2E0

sub_4202DD00 / sub_4202DD8C

写/读 0x10字节 motion 结构

+0x2E8(原始) / +0x2FC(合成)

6. 控制台命令处理(0x4200DBF8~0x4200E6E8 区域)
  • [sub_4200DCD8]— buttons(raw|mut|off, 周期) 处理器

  • sub_4200DF4C— mouse(raw|mut|off, 周期) 处理器

  • sub_4200E1D0— wheel(abs|rel|act|off, 周期) 处理器

  • 命令全集(字符串表 0x3C043688~0x3C04378C):move/wheel/left/right/middle/side1/side2/click/moveto/getpos/screen/axis/mouse/version/down/press/string/isdown/multidown/multiup/multipress/mask/remap/silent/serial/echo + lock_mx/my/ml/mr/mm/ms1/ms2(+/-) + catch_ml/mr/mm/ms1/ms2(锁轴/按键捕捉 = 游戏无后坐力类功能)

7. 初始化(app setup,sub_4200C430 @ 0x4200C430)

打印横幅 → 创建 hid_data 任务 → LedFlashTask(RGB.c) → uart_event_task/uart1_worker_task(uart0.c) → ClickTask+注入队列 → CombinedCommitTask → 载入 NVS 配置(sub_42013B54,screen_w 默认 1920、screen_h 默认 1080)

移动效果:


结语

赶在赛季结束打到了水晶段位,这游戏上了水晶挂是真TM多啊,真是应景了一段话,小透不算挂!

 归根到底,就是三个ESP32组成的一个小玩具,或许有一个3D打印的小盒子就更完美,但折腾的过程更是一种享受!

发表评论