某日在下在 新高登電腦廣場 遊逛時發現一個模仿 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儲存裝置 能夠即時修改程式碼而不需要特別工具
焊接點連接
在下打算以下列方面接駁焊接點到 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 連接器
焊接前先確認引腳的功能,從左至右分別為:
- GND (黑)
- D+ (綠)
- D- (白)
- 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 函式庫,因此只將需要使用的函式載載入到 CURCUITPY 的 lib目錄 中即可
例如在下這個專案只需要使用 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檔案
方便使用及分享,有興趣可以自行下載及使用
基本測試
RP2040 能夠模擬成傳統遊戲控制器
高速連按
將程式碼修改,將按鈕按下的操作更改為高速連按
但需要注意連按的速度,即是每次模擬按動按鈕的間隔,如果太快反而會失效
例如測試的遊戲平台是 Super Famicom (SFC) ,大部分遊戲內容的更新速度為 60 FPS (少數 3D 遊戲為 50 FPS)
即是 每幀16毫秒 ,因此間隔不應少於 16毫秒
組合招式
亦可以修改成組合招式功能
編寫組合招式需要了解觸發原理
例如 Street Fighter 的波動拳是 + P (假設面向右方)
但方向按鈕沒有右下,而且方向操作必須連貫;因此不能獨立按:下、右、拳;亦不是同時按:下、右、拳 便完成
而是需要有步驟地按:
- 按著下
- 延遲1幀
- 按著右
- 延遲1幀
- 釋放下
- 延遲1幀
- 釋放右
- 延遲1幀
- 按著拳
- 延遲1幀
- 釋放拳
將所有按動的操作分拆總共有 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 即可使用
減省編譯的操作及時間
裝置名稱及支援
原本的 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
沒有留言 :
張貼留言