2024-02-29

使用 RP2040-Zero 改裝遊戲控制器

某日在下在 新高登電腦廣場 遊逛時發現一個模仿 PS遊戲控制器 設計的 USB遊戲控制器
忽發奇想,想將 USB遊戲控制器 改裝成可以能夠自行修改功能的遊戲控制器
因此購買嘗試改裝

USB遊戲控制器

外觀

這個遊戲控制器與 PS遊戲控制器 的外觀及部局基本上相同,使用 12個按鈕 及 6個類比轉軸 設計

內部

拆開外殼,內部有一塊主電路板,再配搭3塊小型電路板,同樣與 PS遊戲控制器 相同

USB連接

使用 4線芯USB線路
與 USB配色 相同,因此很容易分辨用途,電路板上亦有標示 USB引腳,方便還原

晶片

由於 USB遊戲控制器 的 晶片使用 板上晶片封裝(Chip on Board (COB封裝)) 技術
晶片被 黑色樹脂 保護著,無法得知使用哪種控制晶片
但根據體積及線路的連接,估計 體積較小的COB 屬於 USB轉TTL晶片體積較大的COB 則是 微控制器
當 微控制器 偵測到訊號變化後,將訊號傳送到 USB轉TTL晶片,再向 宿主系統 提交對應的 USB訊號

在下不想破壞原本的功能,有需要時可以將 USB線路 焊接回原本的通孔
唯有了解電路的運作原理,並在原本的電路板上加裝額外的 微控制器 達成目的

經檢查後,找到 12個按鈕4個類比按壓4個類比轉軸焊接點

按鈕

第1、第2、第3、第4 按鈕
對應 PS遊戲控制器 的 三角、圓形、交叉、正方

第5、第7 按鈕
對應 PS遊戲控制器 的 L1、L2

第6、第8 按鈕
對應 PS遊戲控制器 的 R1、R2

第9 按鈕
對應 PS遊戲控制器 的 SELECT

第10 按鈕
對應 PS遊戲控制器 的 START

類比按壓

第1、第2、第3、第4 類比按壓
對應 PS遊戲控制器 的 上、左、下、右

類比按壓 實際是使用按鈕方式操作,可以透過按壓力度控制訊號強弱,但這款 USB遊戲控制器 沒有這個效果

類比轉軸

類比轉軸 包含 1個按鈕2個10KΩ電位器

第11按鈕 及 LX、LY 類比轉軸 與 第12按鈕 及 RX、RY 類比轉軸
對應 PS遊戲控制器 的 L3、LX、LY 及 R3、RX、RY

分析

12個按鈕 及 4個類比按壓 都是使用 上拉電阻 連接,使用 萬用錶 測量出 未按下時為 5V按下時為 0V

當 類比轉軸 保持中央時大約為 2.45V 至 2.55V
X軸越接近左 或 Y軸越接近上 時,越接近 0V ; X軸越接近右 或 Y軸越接近下 時,越接近 5V

由於總共需要連接 16個按鈕 及 4個類比轉軸,即是 微控制器 最少要有 16支數碼引腳 及 4支類比引腳
因此打算使用 Raspberry Pi Pico 作為改裝遊戲控制器的 微控制器
然而遊戲控制器內的空間很小,但 Raspberry Pi Pico 體積相對較大,在下需要體積更小的微控制器開發板
因此在下改用與 Raspberry Pi Pico 相同使用 RP2040微控制晶片 的 RP2040-Zero開發板
另外在下還想能夠方便修改操作內容,因此在下使用 CircuitPython
能夠像 USB儲存裝置 能夠即時修改程式碼而不需要特別工具

焊接點連接
USB D- USB D+ USB GND USB VCC GP0 GP1 GP2 GP3 GP4 GP5 GP6 GP7 GP8 GP9 GP10 GP11 GP12 GP13 GP14 GP15 GP26 GP27 GP28 GP29 GND VCC

在下打算以下列方面接駁焊接點到 RP2040-Zero 的引腳
次序並不重要,只是 4個類比轉軸 必須連接到支援 類比數碼轉換 (Analog-to-Digital Conveter (ADC)) 的引腳

編號 按鈕 引腳 按鈕代號
遊戲控制器按鈕 對應PS按鈕 電路板引腳 RP2040引腳
B0 按鈕1 按鈕三角 K1 GP0 TR
B1 按鈕2 按鈕圓形 K2 GP1 CI
B2 按鈕3 按鈕交叉 K3 GP2 CR
B3 按鈕4 按鈕正方 K4 GP3 SQ
B4 按鈕L1 按鈕L1 K5 GP4 L1
B5 按鈕R1 按鈕R1 K6 GP5 R1
B6 按鈕L2 按鈕L2 K7 GP6 L2
B7 按鈕R2 按鈕R2 K8 GP7 R2
B8 按鈕SELECT 按鈕SELECT K9 GP8 SE
B9 按鈕START 按鈕START K10 GP9 ST
B10 按鈕L3 按鈕L3 K11 GP10 L3
B11 按鈕R3 按鈕R3 K12 GP11 R3
B12 方向上 方向上 AU GP12 UP
B13 方向左 方向左 AL GP13 LT
B14 方向下 方向下 AD GP14 DN
B15 方向右 方向右 AR GP15 RT
A0 左類比X軸 左類比X軸 LX GP26 LX
A1 左類比Y軸 左類比Y軸 LY GP27 LY
A2 右類比X軸 右類比X軸 RX GP28 RX
A3 右類比Y軸 右類比Y軸 RY GP29 RY

根據剛才的配對,將跳線焊接到 USB遊戲控制器 的焊墊

在下用不同顏色的跳線區分用途,方便跟進不良情況

由於正面的焊墊有機會防礙按鈕,因此改為焊接到電路板背後的的連接線路

USB 連接到 RP2040

雖然在下將 USB遊戲控制器 的 USB線路 除焊,但由於 RP2040-Zero 使用 USB Type-C
其引腳實在太細,以在下目前的能力及工具,無法在不影響其他引腳的情況下焊接到 USB Type-C 的焊墊上

在下嘗試使用 USB Type-C 連接器

焊接前先確認引腳的功能,從左至右分別為:

  1. GND (黑)
  2. D+ (綠)
  3. D- (白)
  4. VCC (紅)

將 USB引腳 焊接到 連接器

便可以用相對簡單及安全的方法連接到 RP2040-Zero

線路設計

解決 USB 連接問題,便開始構思線路

線路原型

實際焊接所有線路後,為了免線路被擠壓而損壞
在下將線路繞到外殼邊緣,再用原本的 USB線 固定,防止移位

(在下為了增加空間,因此將原本的震動摩打除焊
但因為重量平衡問題,因此左右摩打都除焊,但對 遊戲控制器 並沒有影響)

將線路從 RP2040-Zero 的背後穿過通孔

將所有線路焊接到 RP2040-Zero ,再將多餘的線段剪除

在下發現按下按鈕沒有反應,估計是因為 USB 的 電源 及 接地 只是連接到晶片,並沒有貫通整個電路板
因此將 RP2040-Zero 的 3V3 及 GND 分別焊接到電路板的 電源 及 接地
結果如在下所料,所有按鈕都能夠接地;4個類比轉軸亦能夠顯示 ADC數值

編寫 CircuitPython

硬件連接已經完成,基本的程式已經測試,並正確無誤,便開始製作自訂的功能

載入 HID 函式庫

雖然 RP2040 能夠模擬 HID 功能,但 CircuitPython 預設函式庫並沒有相關功能
因此需要到 CircuitPython 的官方網頁 下載 函式庫組合包

根據需要下載對應版本的函式庫組合包
(在下使用 8.x的編譯版本)

由於 RP2040 的 記憶體不可能載入所有 CircuitPython 函式庫,因此只將需要使用的函式載載入到 CURCUITPYlib目錄 中即可
例如在下這個專案只需要使用 HID 功能,只需要將函式庫組合包的 adafruit_hid 載入到 CURCUITPY 的 lib目錄

CIRCUITPY/
CIRCUITPY/lib/
CIRCUITPY/lib/adafruit_hid/
CIRCUITPY/lib/adafruit_hid/__init__.mpy
CIRCUITPY/lib/adafruit_hid/consumer_control.mpy
CIRCUITPY/lib/adafruit_hid/consumer_control_code.mpy
CIRCUITPY/lib/adafruit_hid/keyboard.mpy
CIRCUITPY/lib/adafruit_hid/keyboard_layout_base.mpy
CIRCUITPY/lib/adafruit_hid/keyboard_layout_us.mpy
CIRCUITPY/lib/adafruit_hid/keycode.mpy
CIRCUITPY/lib/adafruit_hid/mouse.mpy

函式庫結構應該與樹狀圖相同

遊戲控制器 函式庫

CircuitPython 雖然有提供 HID 函式庫,能令 微控制器 模擬成 鍵盤、滑鼠 等輸入裝置,但沒有提供 遊戲控制器 函式庫
因此需要自行製作 遊戲控制器 的函式庫

報告描述器
import usb_hid
GAMEPAD_REPORT_DESCRIPTOR = bytes(
	(0x05, 0x01, 0x09, 0x05, 0xA1, 0x01, 0x85, 0x04, 0x05, 0x09, 0x19, 0x01, 0x29, 0x10, 0x15, 0x00, 0x25, 0x01, 0x75, 0x01, 0x95, 0x10, 0x81, 0x02, 0x05, 0x01, 0x15, 0x81, 0x25, 0x7F, 0x09, 0x30, 0x09, 0x31, 0x09, 0x32, 0x09, 0x35, 0x75, 0x08, 0x95, 0x04, 0x81, 0x02, 0xC0,)
)
gamepad = usb_hid.Device(
	report_descriptor = GAMEPAD_REPORT_DESCRIPTOR,
	usage_page = 0x01,
	usage = 0x05,
	report_ids = (4,),
	in_report_lengths = (6,),
	out_report_lengths = (0,),
)
usb_hid.enable(
	(
		usb_hid.Device.KEYBOARD,
		usb_hid.Device.MOUSE,
		usb_hid.Device.CONSUMER_CONTROL,
		gamepad
	)
)

網上已經有開發者製作 報告描述器 ,將內容儲存並將存放在內容儲存並將檔案內容儲存並將檔案 CIRCUITPY/boot.py

遊戲控制器類別
import struct, time
from adafruit_hid import find_device
class Gamepad:
	def __init__(self, devices):
		self._gamepad_device = find_device(devices, usage_page = 0x01, usage = 0x05)
		self._report = bytearray(6)
		self._last_report = bytearray(6)
		self._buttons_state = 0
		self._joy_x = 0
		self._joy_y = 0
		self._joy_z = 0
		self._joy_r_z = 0
		try:
			self.reset_all()
		except OSError:
			time.sleep(1)
			self.reset_all()
	def press_buttons(self, *buttons):
		for button in buttons:
			self._buttons_state |= 1 << self._validate_button_number(button) - 1
		self._send()
	def release_buttons(self, *buttons):
		for button in buttons:
			self._buttons_state &= ~(1 << self._validate_button_number(button) - 1)
		self._send()
	def release_all_buttons(self):
		self._buttons_state = 0
		self._send()
	def click_buttons(self, *buttons):
		self.press_buttons(*buttons)
		self.release_buttons(*buttons)
	def move_joysticks(self, x = None, y = None, z = None, r_z = None):
		if x is not None:
			self._joy_x = self._validate_joystick_value(x)
		if y is not None:
			self._joy_y = self._validate_joystick_value(y)
		if z is not None:
			self._joy_z = self._validate_joystick_value(z)
		if r_z is not None:
			self._joy_r_z = self._validate_joystick_value(r_z)
		self._send()
	def reset_all(self):
		self._buttons_state = 0
		self._joy_x = 0
		self._joy_y = 0
		self._joy_z = 0
		self._joy_r_z = 0
		self._send(True)
	def _send(self, always = False):
		struct.pack_into(
			"<Hbbbb",
			self._report,
			0,
			self._buttons_state,
			self._joy_x,
			self._joy_y,
			self._joy_z,
			self._joy_r_z,
		)
		if always or self._last_report != self._report:
			self._gamepad_device.send_report(self._report)
			self._last_report[:] = self._report
	@staticmethod
	def _validate_button_number(button):
		if not 1 <= button <= 16:
			raise ValueError("Button number must in range 1 to 16")
		return button
	@staticmethod
	def _validate_joystick_value(value):
		if not -127 <= value <= 127:
			raise ValueError("Joystick value must be in range -127 to 127")
		return value

除了 報告描述器 ,開發者亦提供 遊戲控制器 類別,令開發更簡單
將內容儲存並存放到 CIRCUITPY/lib/adafruit_hid/hid_gamepad.py
從 原始碼 可以了解到 遊戲控制器 類別 能支援 16個按鈕 及 4個類比轉軸,而 類比轉軸 範圍為 -127 至 127

編寫程式
import digitalio, analogio, board, usb_hid
from adafruit_hid.hid_gamepad import Gamepad
def remap(value):
	return int((value / 65535) * 254) - 127
buttons = {
	"TR": {"PIN": board.GP0, "KEY": 4},
	"CI": {"PIN": board.GP1, "KEY": 2},
	"CR": {"PIN": board.GP2, "KEY": 1},
	"SQ": {"PIN": board.GP3, "KEY": 3},
	"L1": {"PIN": board.GP4, "KEY": 5},
	"R1": {"PIN": board.GP5, "KEY": 6},
	"L2": {"PIN": board.GP6, "KEY": 7},
	"R2": {"PIN": board.GP7, "KEY": 8},
	"SE": {"PIN": board.GP8, "KEY": 9},
	"ST": {"PIN": board.GP9, "KEY": 10},
	"L3": {"PIN": board.GP10, "KEY": 11},
	"R3": {"PIN": board.GP11, "KEY": 12},
	"UP": {"PIN": board.GP12, "KEY": 13},
	"LT": {"PIN": board.GP13, "KEY": 15},
	"DN": {"PIN": board.GP14, "KEY": 14},
	"RT": {"PIN": board.GP15, "KEY": 16},
}
axes = {
	"LX": board.GP26,
	"LY": board.GP27,
	"RX": board.GP28,
	"RY": board.GP29,
}
for key in buttons:
	buttons[key]["GPIO"] = digitalio.DigitalInOut(buttons[key]["PIN"])
	buttons[key]["GPIO"].direction = digitalio.Direction.INPUT
	buttons[key]["GPIO"].pull = digitalio.Pull.UP
for key in axes:
	axes[key] = analogio.AnalogIn(axes[key])
gamepad = Gamepad(usb_hid.devices)
while True:
	for key in buttons:
		if not buttons[key]["GPIO"].value:
			gamepad.press_buttons(buttons[key]["KEY"])
		else:
			gamepad.release_buttons(buttons[key]["KEY"])
	gamepad.move_joysticks(
		remap(axes["LX"].value),
		remap(axes["LY"].value),
		remap(axes["RX"].value),
		remap(axes["RY"].value)
	)

這個程式只是顯示 16個按鈕 及 4個類比轉軸 直接操作效果
這個檔案需要儲存到 CIRCUITPY/code.py

在下將 CircuitPython 的 HID 函式庫,包含 遊戲控制器的報告描述器 、 Gamepad類別、 boot.py 、 code.py 製作成 zip檔案
方便使用及分享,有興趣可以自行下載及使用

circuitpython-gamepad.zip
基本測試

RP2040 能夠模擬成傳統遊戲控制器

高速連按

將程式碼修改,將按鈕按下的操作更改為高速連按

但需要注意連按的速度,即是每次模擬按動按鈕的間隔,如果太快反而會失效
例如測試的遊戲平台是 Super Famicom (SFC) ,大部分遊戲內容的更新速度為 60 FPS (少數 3D 遊戲為 50 FPS)
即是 每幀16毫秒 ,因此間隔不應少於 16毫秒

組合招式

亦可以修改成組合招式功能

編寫組合招式需要了解觸發原理
例如 Street Fighter 的波動拳是 + P (假設面向右方)
但方向按鈕沒有右下,而且方向操作必須連貫;因此不能獨立按:下、右、拳;亦不是同時按:下、右、拳 便完成
而是需要有步驟地按:

  1. 按著下
  2. 延遲1幀
  3. 按著右
  4. 延遲1幀
  5. 釋放下
  6. 延遲1幀
  7. 釋放右
  8. 延遲1幀
  9. 按著拳
  10. 延遲1幀
  11. 釋放拳

將所有按動的操作分拆總共有 11個步驟

節奏連按

在下改裝 USB遊戲控制器 其中一個目的是能夠做到有節奏(Pattern)的連按功能
高速連按功能的遊戲控制器一早已經存在,但要有特定節奏的高速連按功能,在下則未發現

在下使用喜歡遊玩的舊版 Bio Hazard 展示效果
在沒有使用漏洞的情況下的射擊速度為 大約12秒 射擊15發手槍子彈
以特定節奏按動按鈕,使用漏洞的射擊速度為 大約6秒 射擊15發手槍子彈
(包括 射擊狀態 及 取消射擊狀態)

這個漏洞不是單靠高速連按而達成,而是需要在 特定時間 釋放再按著按鈕 才能觸發

如果能夠觸發漏洞,可以令每次手槍射擊的間隔由 750毫秒 減少至 333毫秒

補充資料

使用指令載入到起動模式

由於 RP2040開發板 每次載入到 起動模式 都需要 關閉RP2040電源,按著BOOT按鈕,啟動RP2040電源
如果 開發板 已經安置在 麵包板 或 焊接到 電路板 上,而 BOOT按鈕 在 開發板背面,要按著BOOT按鈕便非常麻煩
尤其在下已經將 RP2040-Zero 放進 遊戲控制器 中,每次更改 韌體 都需要拆開外殼更加不便,甚至有機會損壞 改裝的線路

在下發現 Arduino IDE 使用 Raspberry Pi Pico 或 RP2040 的開發模組時
能夠控制 RP2040 載入到 起動模式,再將 uf2檔案 上載至 RP2040

Arduino IDE 使用的指令:

"${HOME}/.arduino15/packages/rp2040/tools/pqt-python3/"*"/python3" -I "${HOME}/.arduino15/packages/rp2040/hardware/rp2040/"*"/tools/uf2conv.py" --serial "/dev/ttyACM0" --family "RP2040" --deploy "/tmp/arduino_build_"*"/"*".ino.uf2"

由於 --deploy 後的參數是 uf檔案,在下估計這個指令會將 RP2040 載入至 起動模式 後再將 uf檔案 上載
因此在下認為只需要將 --deploy 連同 參數 省卻,便只會載入到 起動模式,而不會立即上載 uf2檔案,結果正確
(--serial 的 參數 需要修改為合適的 連接埠)

使用這種方法便可以不需要物理接觸而載入到 起動模式
將整個 ${HOME}/.arduino15/packages/rp2040/hardware/rp2040/"*"/tools/ 目錄複製
便可以免除安裝 Arduino IDE 及 RP2040 開發模組

RP2040 的 ADC參考

CircuitPython 使用 16位元ADC , 即是 0 至 65535
當轉軸靜止是應該會保持在正中央位置,理論上數值應該 接近32768
(基於物理限制,不可能完美地保持在正中央)
但在下測試時,卻顯示 接近48000,即是數值無法保持在正中央
而且當 其中一個轉軸的數值 增加時, 其他轉軸的數值 亦同時增加
但 轉軸的數值 減少時,則沒有影響 其他轉軸的數值

在下使用另一塊 RP2040-Zero 測試,亦有相同問題;但使用 Arduino Nano 則沒有問題
在下懷疑是 CircuitPython 處理 ADC 有不正確,因此使用 Arduino IDE 測試,同樣無法正確顯示轉軸靜止時的數值
(Arduino IDE 使用 10位元ADC , 即是 0 至 1023 ,但轉軸數值顯示 接近780)
最後認為是 RP2040-Zero 的 ADC參考 不是 5V ,因此改為使用 3V3 ,結果輸出正確的數值

在下查看 RP2040 的規格書,才發現 RP2040 的 ADC參考 是使用 3.3V 而不是 5V
RP2040-Zero 的 5V 實際是 VBUS ,是由 USB 提供電源給 RP2040-Zero
因此當 轉軸的電源 連接到 5V 後,由於使用錯誤的 ADC參考 ,導致轉軸數值錯誤

在下估計是原本 USB遊戲控制器 的 微控制器 的 ADC參考 使用 5V ,因此使用 萬用標 量度到 5V

修改 CIRCUITPY 的名稱

由於所有安裝 CircuitPython 的開發板,掛載時都會顯示 CIRCUITPY
如果同時掛載超過一塊 CircuitPython 開發板,很容易選擇錯誤的開發板而錯誤修改檔案

因此可以將名稱更改,方便管理不同用途的 CircuitPython 開發板
使用任何 磁碟管理軟件,將 CircuitPython 的 分割區標籤 修改即可

亮著 LED

由於 Analog按鈕 已經失效,顯示啟動 Analog 的 LED 亦失去作用
但在下不想浪費,打算將 LED 當作電源接駁的指示燈
即是當 遊戲控制器 連接電源後 LED 便會立即亮著

檢查線路後,發現 LED 原本已經接駁電源,只是沒有連接 接地
因此只需要將 LED負極 連接到 接地 便是完整電路
為了避免因為電壓過大而燒毀 LED ,因此先焊接 電阻 再焊接到 接地
由於空間所限,常用的 碳膜電阻 太大,因此改用 0603 SMD電阻
而且尺寸剛好不需要額外線路便可以直接將 LED負極 及 接地 連接
(在下使用的 2000 SMD電阻 ,電阻值為 200Ω)

通電後 LED 便會亮著
雖然效果比較暗,但亮度足夠顯示電源接通與否,而且偏暗亦可以令 LED 較耐用

使用 Arduino IDE

雖然 CircuitPython 能夠將 RP2040 當作 USB儲存裝置
但如果使用者胡亂修改或不慎將內容刪除,會影響操作效果或令操作失效
因此在下亦製作不容易被修改內容的 Arduino 版本

#include <Joystick.h>
#define BUTTON_COUNT 16
#define AXIS_COUNT 4
const PROGMEM byte BUTTON_PINS[] = {
	0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15
};
const PROGMEM byte BUTTON_KEYS[] = {
	4, 2, 1, 3, 5, 6, 7, 8, 9, 10, 11, 12, 13, 15, 14, 16
};
const PROGMEM byte AXIS_PINS[] = {
	26, 27, 28, 29
};
void setup() {
	for (byte i = 0; i < BUTTON_COUNT; i++) {
		pinMode(BUTTON_PINS[i], INPUT_PULLUP);
	}
	for (byte i = 0; i < AXIS_COUNT; i++) {
		pinMode(AXIS_PINS[i], INPUT);
	}
	Joystick.begin();
}
void loop() {
	for (byte i = 0; i < BUTTON_COUNT; i++) {
		if (!digitalRead(BUTTON_PINS[i])) {
			Joystick.button(BUTTON_KEYS[i], true);
		} else {
			Joystick.button(BUTTON_KEYS[i], false);
		}
	}
	Joystick.X(analogRead(AXIS_PINS[0]));
	Joystick.Y(analogRead(AXIS_PINS[1]));
	Joystick.Z(analogRead(AXIS_PINS[2]));
	Joystick.Zrotate(analogRead(AXIS_PINS[3]));
}

Arduino 版本 不會被當作 USB儲存裝置,因此相比 CircuitPython 較難被一般使用者修改內容

由於 RP2040 的 Arduino 開發模組已經包含 HID功能,因此不需要額外函式庫便可以立即製作 HID程式
程式碼亦與 CircuitPython 相似,但由於 Arduino 使用 AVR-C 能夠直接控制微控制器,因此效能比較快

在下將已經編譯的 uf2檔案 製作,只需要將 RP2040 載入到 起動模式,並將 uf2檔案 複製到 RPI-RP2 即可使用
減省編譯的操作及時間

arduino-gamepad.zip
裝置名稱及支援

原本的 USB遊戲控制器 連接到電腦後 會顯示

0810:0001 Personal Communication Systems, Inc. Dual PSX Adaptor

當使用 CircuitPython 及 Arduino 時,裝置名稱及支援亦會有變化

Vendor ID Product ID
CircuitPython 2E8A 101F
Arduino 2E8A 0103
Linux Windows Mac OS Android
CircuitPython Waveshare Electronics
RP2040 Zero
CircuitPython HID USB儲存裝置 部分有效
Arduino Waveshare
RP2040 Zero
RP2040 Zero RP2040 Zero 特殊

CircuitPython 雖然能夠在 Mac OS 模擬 鍵盤 及 滑鼠
但無法模擬 遊戲控制器,只會當作 USB儲存裝置 掛載
Android 則部分有效(電話、平板),部分無效(電視)
同樣只會當作 USB儲存裝置 掛載

而 Arduino 的 遊戲控制器 雖然能夠在 Android 運作
但 控鈕 及 轉軸 的配對則與電腦不同,部分配對無效
同樣能夠正確模擬 鍵盤 及 滑鼠

總結

這個 改裝遊戲控制器 實際就是一種 巨集輸入裝置 (Macro Input Device)
早已存在 巨集鍵盤 或 巨集滑鼠 或 巨集遊戲控制器 等裝置
典型的 巨集輸入裝置 都是連按裝置,部分裝置能調節連按速度
而現今的 巨集輸入裝置 都需要在安裝專用的軟件才能修改 巨集 的內容
普遍都是一鍵輸入一連串操作,或不斷重覆指定的操作;但內容都無法超越 遊戲控制器 原本的功能
這些 巨集輸入裝置 價錢大約 100 至 1000港幣,功能越多、按鈕越多,價錢越貴

而在下只是以15元港幣購買 USB遊戲控制器 外,其他都是現存工具及零件
改裝及編寫程式的時間總共大約10小時
但回報就是在下能夠完全掌握所有功能及完整的自訂操作,這是無法用金錢買到的獎勵
例如要觸發 舊 Bio Hazard 的快速射擊漏洞,暫時沒有一個現有的 巨集輸入裝置 能完美執行

懶惰的代價

由於在下沒有預先計算跳線由焊墊至 RP2040-Zero 的通孔的長度
因此直接將跳線連同絕緣的外皮直接焊接到 RP2040-Zero 的通孔
但有部分跳線被絕緣的物料包裹而沒有焊接到通孔上,只是外皮熔解而黏附在通孔上
結果令跳線接觸不良,導致訊號時不穩定,最後完全沒有訊號
因此在下再次焊接不良的跳線,量度適合長度的跳線後開線,確保金屬部分完全焊接到通孔
最後訊號終於穩定

另外由於某些線路的位置影響按鈕按下時的準確度,同樣令訊號不穩定
結果同樣需要重新焊接來解決問題

改裝計劃

由於 RP2040-Zero 總共引出 25支數碼引腳,這個改裝只是使用 16支數碼引腳
因此原本在下還想使用 剩餘的9支數碼引腳 來連接 按鈕 或 切換器 來製作額外操作
(例如開關射按功能,切換按鈕功能模式)
但要安置額外的元件,即是需要改裝外殼,不過破壞性的改裝,失敗機會很高
而且 RP2040-Zero 背後 9支數碼引腳 的 焊墊 實在太細小,還有旁邊就是其他引腳的通孔、零件、微控制器
非常接近,焊接時稍有偏差便會破壞其他零件,還是先領取這次成功,留待將來再製作

這個改裝 遊戲控制器 其中一個想法主要來自 Steam Deck
Steam Deck 可以將額外的 L4, R4, L5, R5 按鈕設計成類似巨集的操作
然而,雖然能夠行執行點擊的操作,但無法執行高速連按、同時按下、按下指定時間後釋放等操作
因此在下開始構思這個改裝遊戲控制器的想法
而另一個想法是由於有些網頁遊戲,都必須使用鍵盤操作,而且無法修改操作配置
因此在下希望能夠改裝置遊戲控制器來突破操作配置的限制

另外在下將按鈕操作改為以中斷,令訊號反應提升

#include <Joystick.h>
#define BUTTON_COUNT 16
#define AXIS_COUNT 4
const PROGMEM byte BUTTON_PINS[] = {
	0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15
};
const PROGMEM byte BUTTON_KEYS[] = {
	4, 2, 1, 3, 5, 6, 7, 8, 9, 10, 11, 12, 13, 15, 14, 16
};
const PROGMEM byte AXIS_PINS[] = {
	26, 27, 28, 29
};
void buttonHandler(byte index) {
	Joystick.button(BUTTON_KEYS[index], !digitalRead(BUTTON_PINS[index]));
}
void button0Handler() {
	buttonHandler(0);
}
void button1Handler() {
	buttonHandler(1);
}
void button2Handler() {
	buttonHandler(2);
}
void button3Handler() {
	buttonHandler(3);
}
void button4Handler() {
	buttonHandler(4);
}
void button5Handler() {
	buttonHandler(5);
}
void button6Handler() {
	buttonHandler(6);
}
void button7Handler() {
	buttonHandler(7);
}
void button8Handler() {
	buttonHandler(8);
}
void button9Handler() {
	buttonHandler(9);
}
void button10Handler() {
	buttonHandler(10);
}
void button11Handler() {
	buttonHandler(11);
}
void button12Handler() {
	buttonHandler(12);
}
void button13Handler() {
	buttonHandler(13);
}
void button14Handler() {
	buttonHandler(14);
}
void button15Handler() {
	buttonHandler(15);
}
const unsigned int ANALOG_MAX_VALUE = 1024;
const unsigned int ANALOG_DEAD_ZONE = 16;
unsigned int axisHandler(byte index) {
	unsigned int value = analogRead(AXIS_PINS[index]);
	if (value < ANALOG_DEAD_ZONE) {
		value = 0;
	} else if (value >= ANALOG_MAX_VALUE - ANALOG_DEAD_ZONE) {
		value = ANALOG_MAX_VALUE - 1;
	} else if (ANALOG_MAX_VALUE / 2 - ANALOG_DEAD_ZONE < value && value <= ANALOG_MAX_VALUE / 2 + ANALOG_DEAD_ZONE) {
		value = ANALOG_MAX_VALUE / 2;
	}
	return value;
}
void setup() {
	for (byte i = 0; i < BUTTON_COUNT; i++) {
		pinMode(BUTTON_PINS[i], INPUT_PULLUP);
	}
	attachInterrupt(digitalPinToInterrupt(BUTTON_PINS[0]), button0Handler, CHANGE);
	attachInterrupt(digitalPinToInterrupt(BUTTON_PINS[1]), button1Handler, CHANGE);
	attachInterrupt(digitalPinToInterrupt(BUTTON_PINS[2]), button2Handler, CHANGE);
	attachInterrupt(digitalPinToInterrupt(BUTTON_PINS[3]), button3Handler, CHANGE);
	attachInterrupt(digitalPinToInterrupt(BUTTON_PINS[4]), button4Handler, CHANGE);
	attachInterrupt(digitalPinToInterrupt(BUTTON_PINS[5]), button5Handler, CHANGE);
	attachInterrupt(digitalPinToInterrupt(BUTTON_PINS[6]), button6Handler, CHANGE);
	attachInterrupt(digitalPinToInterrupt(BUTTON_PINS[7]), button7Handler, CHANGE);
	attachInterrupt(digitalPinToInterrupt(BUTTON_PINS[8]), button8Handler, CHANGE);
	attachInterrupt(digitalPinToInterrupt(BUTTON_PINS[9]), button9Handler, CHANGE);
	attachInterrupt(digitalPinToInterrupt(BUTTON_PINS[10]), button10Handler, CHANGE);
	attachInterrupt(digitalPinToInterrupt(BUTTON_PINS[11]), button11Handler, CHANGE);
	attachInterrupt(digitalPinToInterrupt(BUTTON_PINS[12]), button12Handler, CHANGE);
	attachInterrupt(digitalPinToInterrupt(BUTTON_PINS[13]), button13Handler, CHANGE);
	attachInterrupt(digitalPinToInterrupt(BUTTON_PINS[14]), button14Handler, CHANGE);
	attachInterrupt(digitalPinToInterrupt(BUTTON_PINS[15]), button15Handler, CHANGE);
	for (byte i = 0; i < AXIS_COUNT; i++) {
		pinMode(AXIS_PINS[i], INPUT);
	}
	Joystick.begin();
}
void loop() {
	Joystick.X(axisHandler(0));
	Joystick.Y(axisHandler(1));
	Joystick.Z(axisHandler(2));
	Joystick.Zrotate(axisHandler(3));
}

同樣在下將韌體編譯成 UF2 方便使用

arduino-gamepad-interrupt.uf2

參考資料

沒有留言 :

張貼留言