2025-6-2 13:56:52 只看该作者
968浏览
查看: 968|回复: 0
打印 上一主题 下一主题

[教程] 口袋版迷你任天堂手柄

[复制链接]
本帖最后由 fibx 于 2025-6-2 13:56 编辑

口袋版迷你任天堂手柄





大家好,欢迎回来! 今天,我要分享一个既有趣又小巧的项目——口袋版超级任天堂手柄 (Pocket SNES)!这是一个完全从零开始打造的经典超级任天堂手柄迷你版,使用了定制 PCB 板3D 打印外壳

这个项目经历了不少改进。最初我做了一个超大号版本,核心是 XIAO SAMD21 微控制器,装在一个特大的外壳里。那时我不得不把外壳分成三部分打印,再组装成一个巨大的手柄单元。对于第二个版本,我的目标是在保留所有功能的前提下,把它缩小到比手掌还小的尺寸。

为了实现这个小巧版本,我打算使用 RP2040-Tiny 迷你开发板,它是树莓派 PICO 的贴片模块版本。我把这块 RP2040 Tiny 模块安装在一个手柄形状的 PCB 板上:正面装有小型贴片微动按钮,背面则固定着 RP2040 主控制器。为了提升握持手感,我在 PCB 板底部加装了一个外壳,使其握持起来更舒适。

别看它体积小,口袋版 SNES 手柄表现相当出色。我们首先用它玩了《Broforce》,需要把手柄按键映射到游戏角色的动作上。简单来说,这个手柄可以用于任何游戏,我们只需要在游戏里设置好按键映射就行。

接着,我们又在 MAC Pi 游戏机上测试了这个手柄。我在 PPSSPP 模拟器上玩了几款游戏,包括《Moto GP 3》和《生死格斗天堂》。同样地,我们也在 PPSSPP 模拟器的设置里映射了手柄按键。

总的来说,这个迷你版的 SNES 手柄的功能和标准尺寸的 SNES 手柄(拥有 12 个按键)是一样的。这个项目最棒的一点是,它只需要把一块 PICO 和 12 个按钮连接起来,简单几步就能完成

在这篇教程里,我会详细介绍打造这个小巧又简洁的游戏手柄的全过程。让我们开始吧!

所需材料

本项目用到的材料如下:

  • 定制 PCB 板(由 PCBWAY 提供服务)
  • RP2040 Tiny 开发板(带 USB 适配板)
  • 贴片微动按钮
  • 3D 打印外壳
  • 插件式直角微动开关
  • M2 螺丝

第一步:设计

我们从创建项目的模型开始,这一步挑战很大。

在开发这个迷你版本之前,我们做过一个超大号版本(XL)。XL 版本空间充裕,元器件可自由布局,基本不受限制。

但在迷你版本中,最初想把所有元件都放进一个外壳的想法行不通。于是我设计了一块可双面安装元件的 PCB 板,并在底部加装了一个框架结构。这个框架既保护了 USB 适配板,也大大改善了握持手感。

  • 贴片按钮安装在 PCB 板的顶面
  • 直角微动开关RP2040 Tiny 开发板安装在 PCB 板的底面
  • USB 适配板安装在框架背面,用集成在框架上的螺丝柱固定。

模型定稿后:

  • 我们将模型导出为网格文件,使用透明 PLA 材料进行 3D 打印(这样板载的 RGB LED 灯光就能透出来)。
  • 同时导出了 PCB 板的 DWG 文件,用于后续的 PCB 设计。

第二步:RP2040-Tiny 微型开发板

我们使用的是基于树莓派 RP2040 核心的 RP2040-Tiny 迷你开发板。它附带一个独立的编程适配板,将 Type-C USB 接口与主电路分开,有效减小了整体厚度和尺寸,方便用户集成到自己的项目中。

技术规格方面: 它本质上就是一个带有 RP2040 微控制器芯片的 Pico,但做了一些改进:

  • 板载 8PIN FPC 软排线接口,用于连接 Type-C 适配板。
  • 增加了半孔焊盘 (Castellated holes/pads),可以直接焊接到载板上。
  • 板上还添加了一颗 WS2812B 2020 封装的 LED 灯(连接到 GPIO16)。

总体而言,核心参数与 Pico 相同:

  • RP2040 双核 Arm Cortex M0+ 处理器
  • 高达 133 MHz 的灵活时钟频率
  • 264KB SRAM
  • 2MB 板载闪存

关于这块开发板的详细信息,可以访问其官方 Wiki 页面

这块板子我是从 PCBWAY 的礼品商店 (Gift Shop) 购买的。这是他们的在线商城,提供正品价格的电子模块、开发板和传感器,也可以通过 PCBWAY 的积分系统“豆子 (beans)”兑换。

第三步:PCB 设计

如前所述,游戏手柄的 PCB 设计其实非常简单:本质上就是将 RP2040 Tiny 开发板连接到 12 个贴片按钮。每个按钮都连接到一个 RP2040 的 GPIO 引脚上。所有贴片按钮的引脚都连接到地线 (GND)。当一个按钮被按下时,与之相连的 GPIO 引脚就接地了,我们的微控制器就能检测到这个状态变化。

绘制好原理图后,我们导出网络表,并使用 3D 模型中的 DWG 文件来定义 PCB 的形状。所有贴片按钮(包括两个扳机按键)都严格按 3D 模型的位置摆放。PCB 板上还开了四个安装孔,用于将框架固定到 PCB 板上。

RP2040 开发板安装在 PCB 板的底面

第四步:PCBWAY 服务

PCB 设计完成后,我们选择了白色阻焊层 + 黑色丝印的工艺,并在 PCBWAY 的报价页面上传了 Gerber 文件。

一周内就收到了 PCB 板,质量非常棒! 我们在 PCB 的丝印层添加了一些设计元素来提升项目的美观度。PCBWAY 完美地实现了这些定制细节,充分展现了他们卓越的 PCB 制造能力。

在过去的十年里,PCBWay 凭借提供出色的 PCB 制造和组装服务而闻名,成为全球无数工程师和设计师信赖的合作伙伴。

如果你需要高性价比的优质 PCB 服务,可以去看看 PCBWAY

第五步:PCB 组装 - 贴片按钮安装

  1. 点焊锡膏: 我们用点胶针头,将普通的 Sn/PB 63/37 焊锡膏逐个点在 PCB 顶面每个按钮焊盘上。
  2. 放置按钮: 用防静电镊子拾取并安装所有贴片微动按钮。
  3. 回流焊接: 把电路板放到我们的 Miniware 回流焊加热台上加热。加热台从底部加热 PCB,直到焊锡膏熔化,这样所有元件就一次性焊接到位了。

第六步:PCB 组装 - RP2040 Tiny 安装

RP2040 Tiny 的安装过程和前面的回流焊工艺略有不同。因为 PCB 顶面已有贴片元件,无法再用加热台从底部加热来焊接背面的元件。

  • 使用电烙铁: 我们在这里使用电烙铁。首先,在烙铁头上蘸点助焊剂,然后在 RP2040 Tiny 焊盘中的一个焊点上熔化一点焊锡。
  • 定位焊接: 用镊子夹起 RP2040 Tiny 模块,精确放到它的位置上。接着用电烙铁加热刚刚上锡的那个焊点。熔化的焊锡会把模块初步固定住。
  • 焊接对侧焊点: 再焊接模块对角的焊点,确保模块完全固定。最后,用电烙铁把所有剩下的焊点都焊好。

第七步:PCB 组装 - 插件开关安装

  1. 安装开关: 将插件式 (THT) 开关从电路板的底面插入。
  2. 焊接引脚: 在电路板的顶面用电烙铁焊接开关的引脚。
  3. 剪掉多余引脚: 最后用剪线钳剪掉顶面露出的插件开关引脚。

第八步:连接适配板与控制器

PCB 板组装完成后,开始组装连接 USB 适配板和主控板。

  1. 连接排线: 我们使用随附的柔性排线 (FPC)。先将排线连接到 Type-C 适配板上。
  2. 连接主控板: 接着将排线的另一端连接到 RP2040 Tiny 板上的接口。排线的连接方向很重要:蓝色面(通常标识为第1脚)应朝上,并且要确保排线完全插入接口内。
  3. 整理排线: 原配的排线有点长,我们把它弯曲折叠一下,使其更紧凑。

第九步:框架组装

  • 穿入适配板: 先将 USB 适配板穿过框架部件。
  • 对齐固定: 将 PCB 板的安装孔与框架上的安装孔对齐。
  • 固定框架: 用四颗 M2 螺丝将框架部件紧固到 PCB 板上。
  • 固定适配板: 将适配板放到框架背面预留的螺丝柱上,再用两颗 M2 螺丝将其固定。

口袋版 SNES 手柄的组装就完成了!接下来我们看看这个项目的代码。

第十步:代码

本项目使用的代码非常简单:

#include "Adafruit_TinyUSB.h" // 包含USB通信库
#include <Adafruit_NeoPixel.h> // 包含NeoPixel LED控制库

// === 设置 WS2812 LED ===
#define LED_PIN     16     // LED连接在GPIO16
#define NUM_LEDS    1      // 只有1个LED
Adafruit_NeoPixel pixel(NUM_LEDS, LED_PIN, NEO_GRB + NEO_KHZ800); // 创建NeoPixel对象

// === 设置游戏手柄 ===
#define NUM_BUTTONS 12     // 总共12个按钮
const uint8_t buttonPins[NUM_BUTTONS] = { // 按钮连接的GPIO引脚号
  0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11
};

// 定义USB HID报告描述符 (12个按钮的游戏手柄)
uint8_t const hid_report_descriptor[] = {
  0x05, 0x01, 0x09, 0x05, 0xA1, 0x01, 0x15, 0x00,
  0x25, 0x01, 0x35, 0x00, 0x45, 0x01, 0x75, 0x01,
  0x95, 0x0C, 0x05, 0x09, 0x19, 0x01, 0x29, 0x0C,
  0x81, 0x02, 0x75, 0x01, 0x95, 0x04, 0x81, 0x03,
  0xC0
};

Adafruit_USBD_HID usb_hid; // 创建USB HID对象
uint8_t report[2] = {0};   // 存储按钮状态报告的数组 (2字节 = 16位)

void setup() {
  // 1. 配置按钮引脚为上拉输入模式
  for (int i = 0; i < NUM_BUTTONS; i++) {
    pinMode(buttonPins[i], INPUT_PULLUP);
  }

  // 2. 初始化NeoPixel LED
  pixel.begin();          // 启动LED
  pixel.setBrightness(50); // 设置亮度为50 (范围0-255)
  pixel.show();           // 初始状态关闭LED

  // 3. 配置USB HID (游戏手柄)
  usb_hid.setReportDescriptor(hid_report_descriptor, sizeof(hid_report_descriptor)); // 设置报告描述符
  usb_hid.setPollInterval(2); // 设置轮询间隔(毫秒)
  usb_hid.begin();            // 启动USB HID

  // 4. 等待USB连接成功
  while (!USBDevice.mounted()) delay(10);
}

void loop() {
  // 1. 限制执行频率:每10毫秒运行一次
  static uint32_t last = 0; // 记录上次运行时间
  if (millis() - last < 10) return; // 如果不到10ms就跳过
  last = millis(); // 更新上次运行时间

  // 2. 清除按钮状态报告
  report[0] = 0; // 清零报告字节0
  report[1] = 0; // 清零报告字节1
  bool anyPressed = false; // 记录是否有按钮被按下

  // 3. 读取所有按钮状态
  for (int i = 0; i < NUM_BUTTONS; i++) {
    if (digitalRead(buttonPins[i]) == LOW) { // 如果按钮按下(引脚变低电平)
      // 计算该按钮在报告中的位置:i/8确定字节索引, i%8确定位索引
      report[i / 8] |= (1 << (i % 8)); // 将该按钮对应的位置1
      anyPressed = true; // 标记有按钮被按下
    }
  }

  // 4. 更新LED状态:如果有按钮按下则亮紫色,否则关闭
  if (anyPressed) {
    pixel.setPixelColor(0, pixel.Color(255, 0, 255)); // 设置LED为紫色 (R=255, G=0, B=255)
  } else {
    pixel.setPixelColor(0, 0); // 关闭LED (颜色值为0)
  }
  pixel.show(); // 应用LED颜色更改

  // 5. 通过USB HID发送按钮状态报告给电脑
  usb_hid.sendReport(0, report, sizeof(report));
}

代码解释:
这段代码的核心是使用 Adafruit TinyUSB 库将 RP2040 配置成一个 USB HID 游戏手柄。同时,我们还加入了控制 WS2812B LED 的代码,在按键按下时提供视觉反馈。

#include "Adafruit_TinyUSB.h"
#include <Adafruit_NeoPixel.h>
  • 开头引入必需的库:Adafruit_TinyUSB 负责 USB 通信,Adafruit_NeoPixel 负责控制 WS2812B LED。
#define LED_PIN 16
#define NUM_LEDS 1
Adafruit_NeoPixel pixel(NUM_LEDS, LED_PIN, NEO_GRB + NEO_KHZ800);
  • 定义连接到 GPIO16 的单个 WS2812 LED
  • 创建一个 Adafruit NeoPixel 对象 pixel 来管理这个 LED。
#define NUM_BUTTONS 12
const uint8_t buttonPins[NUM_BUTTONS] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11};
  • 定义 12 个按钮,它们分别连接到 GPIO 引脚 0 到 11。这些引脚用于读取玩家的按键输入。
uint8_t const hid_report_descriptor[] = { ... };
  • 定义 USB HID 报告描述符,描述这是一个 12 按钮的游戏手柄。这告诉电脑如何理解从手柄发过来的数据,即哪些按钮被按下了。
Adafruit_USBD_HID usb_hid;
uint8_t report[2] = {0};
  • usb_hid:初始化 USB HID 对象进行通信。
  • report[2]:一个数组(2字节=16位),用于存储当前被按下的按钮状态
void setup() {
  // 1. 配置按钮引脚 (上拉输入模式)
  for (int i = 0; i < NUM_BUTTONS; i++) {
    pinMode(buttonPins[i], INPUT_PULLUP); // 设置为上拉输入:默认高电平(松开),按下时变低电平
  }

  // 2. 初始化NeoPixel LED
  pixel.begin();          // 启动LED
  pixel.setBrightness(50); // 设置亮度(0-255)
  pixel.show();           // 初始关闭LED

  // 3. 配置USB HID (游戏手柄)
  usb_hid.setReportDescriptor(hid_report_descriptor, sizeof(hid_report_descriptor)); // 设置报告格式
  usb_hid.setPollInterval(2); // 设置电脑查询手柄状态的频率(毫秒)
  usb_hid.begin();            // 启动USB HID功能

  // 4. 等待USB连接成功
  while (!USBDevice.mounted()) delay(10); // 循环等待直到USB被电脑识别
}
  • 1. 初始化按钮: 每个按钮引脚配置为上拉输入 (INPUT_PULLUP)。这意味着按钮松开时,引脚读到的是高电平 (HIGH);按钮按下时,引脚通过内部电阻被拉到低电平 (LOW)
  • 2. 初始化 LED: 启动 WS2812B LED,设置初始亮度为 50(范围 0-255),并确保 LED 初始状态是关闭的。
  • 3. 配置 USB HID: 设置之前定义的报告描述符(告诉电脑这是一个游戏手柄),设置轮询间隔(电脑询问手柄状态的频率),然后启动 USB HID 功能
  • 4. 等待 USB 连接: 循环等待,直到电脑成功识别并挂载了这个 USB HID 设备,确保通讯正常。
void loop() {
  // 1. 限制执行频率:每10毫秒运行一次主循环
  static uint32_t last = 0; // 存储上次运行loop的时间点
  if (millis() - last < 10) return; // 如果距离上次运行还不到10ms,直接返回(不执行后面代码)
  last = millis(); // 更新上次运行时间为当前时间

  // 2. 清除按钮状态报告
  report[0] = 0; // 清零报告字节0
  report[1] = 0; // 清零报告字节1 (共16位,对应12个按钮)
  bool anyPressed = false; // 标志位,记录本轮是否有按钮被按下

  // 3. 读取所有按钮状态
  for (int i = 0; i < NUM_BUTTONS; i++) {
    if (digitalRead(buttonPins[i]) == LOW) { // 如果检测到引脚是低电平(按钮按下)
      // 计算该按钮在report数组中的位置:
      // i / 8 => 确定该按钮状态存储在report[0]还是report[1] (0-7在report[0], 8-11在report[1])
      // i % 8 => 确定在该字节中的第几位(0-7)
      report[i / 8] |= (1 << (i % 8)); // 将该按钮对应的比特位置1 (|= 按位或操作, << 左移操作)
      anyPressed = true; // 设置标志位为true,表示有按钮按下
    }
  }

  // 4. 根据按键状态更新LED
  if (anyPressed) {
    pixel.setPixelColor(0, pixel.Color(255, 0, 255)); // 如果有按钮按下,设置LED为紫色 (R=255, G=0, B=255)
  } else {
    pixel.setPixelColor(0, 0); // 如果没有按钮按下,关闭LED (颜色值0)
  }
  pixel.show(); // 将颜色设置应用到LED上

  // 5. 发送按钮状态报告给电脑
  usb_hid.sendReport(0, report, sizeof(report)); // 发送报告ID 0,内容为report数组,长度为2字节
}
  • 1. 限制执行频率: 使用 millis() 计时确保 loop() 函数的核心逻辑大约每 10 毫秒运行一次,避免不必要的频繁更新占用资源。
  • 2. 清除报告: 每次循环开始时,将存储按钮状态的 report 数组清零(两个字节都设为 0),并重置 anyPressed 标志为 false
  • 3. 读取按钮状态: 遍历所有 12 个按钮:
    • 检查对应 GPIO 引脚是否为 LOW(低电平,表示按钮被按下)。
    • 如果按下,计算该按钮在 report 数组中的位置(哪个字节的哪一位),并将该位置设为 1
    • 同时将 anyPressed 标志设为 true
  • 4. 更新 LED 反馈: 检查 anyPressed 标志:
    • 如果有按钮被按下,将 WS2812B LED 设置为紫色 (R=255, G=0, B=255)
    • 如果没有按钮被按下,则关闭 LED (0)。
    • 调用 pixel.show() 将颜色更改实际应用到 LED。
  • 5. 发送报告: 调用 usb_hid.sendReport() 函数将包含当前按钮状态的 report 数组(2 字节)发送给连接的电脑。电脑根据之前定义的报告描述符将其识别为游戏手柄输入。

第十一步:成果展示

这就是我们这个简单小巧项目的最终成果:口袋版 SNES 游戏手柄!这是一款极致便携的游戏手柄,专为玩怀旧复古游戏设计,布局类似经典的 SNES 手柄。它也能用来玩一些不需要左右摇杆的现代游戏。

这款设备的尺寸仅为 74.5mm x 33.8mm x 11.5mm,堪称最小的 DIY 游戏手柄之一。

测试环节:

  1. Broforce (Windows): 我们首先在 Windows 上玩《Broforce》。这是一款充满动作元素的横版“跑轰”游戏,包含战役、奖励关卡、可解锁角色以及激烈的敌人战斗。Broforce 的成员是一群受到老牌电影和电视剧角色启发的超硬汉动作英雄,他们的任务是打击恐怖分子并营救被囚禁的兄弟。每个角色都有独特的技能、武器和特殊攻击,适应各种战斗场景。这款游戏非常精彩又好玩,尤其适合多人联机。我们在游戏设置里映射好手柄按键,就能用它操控角色了。
  2. MAC Pi (树莓派/PPSSPP 模拟器): 我们把这个手柄用在我们之前的项目 MAC Pi 游戏机上。通过 Pi 应用安装了 PPSSPP 模拟器,然后玩了几款 PSP 游戏,包括《Dead or Alive Paradise》和《Moto GP 3》。同样地,我们需要先在模拟器设置里映射好手柄按键。两款游戏运行都很流畅,我们的迷你手柄完全能胜任!

手感反馈: 那些小型贴片按钮按起来手感不算特别软,但整体功能表现良好。

第十二步:未来展望

在下一个版本中,我特别想做的一项改进是:用树莓派 PICO W 或 PICO 2W 替换掉 RP2040 Tiny。因为它们集成了 WiFi/蓝牙功能,这样就完全不需要 USB 线了。这将使设备实现真正的无线便携。

目前,这个项目已经圆满完成了。所有必要的细节,包括 PCB 文件、代码以及其他相关信息,都已整理好并随项目提供。

感谢您耐心把教程完整读完!

祝大家制作愉快![i][i][i][i]


附件:
下载附件Pocket SNES.rar

英文链接:Pocket SNES
英文作者:Arnov Sharma
中文翻译:fibx







MonJune-202506022503..png (244.23 KB, 下载次数: 5)

MonJune-202506022503..png
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

为本项目制作心愿单
购买心愿单
心愿单 编辑
[[wsData.name]]

硬件清单

  • [[d.name]]
btnicon
我也要做!
点击进入购买页面
上海智位机器人股份有限公司 沪ICP备09038501号-4 备案 沪公网安备31011502402448

© 2013-2025 Comsenz Inc. Powered by Discuz! X3.4 Licensed

mail