Joystick.h標頭檔宣告的Joystick_類別定義了下列公用方法,本文將解析它們的原始碼,並且透過一個簡易的任天堂Switch遊戲機的相容控制器範例,介紹它們的用法。
- void begin(bool initAutoSendState = true):設定是否自動傳送按鍵狀態報告,預設為true(是)。
- void end():結束;無實質作用
- void pressButton(uint8_t button):按下某按鍵;把代表該按鍵的資料值設成1。
- void releaseButton(uint8_t button):放開某按鍵;把代表該按鍵的資料值設成0。
- void setButton(uint8_t button, uint8_t value):設定某按鍵的值,若輸入值為0,代表則執行releaseButton()方法,否則執行pressButton()方法。
- void setHatSwitch(int16_t value):設定十字鍵的狀態值。
- void setXAxis(int8_t value):設定左搖桿X軸值。
- void setYAxis(int8_t value):設定左搖桿Y軸值。
- void setZAxis(int8_t value):設定右搖桿X軸值。
- void setZAxisRotation(int16_t value):設定右搖桿Y軸值。
- void sendState():傳送遊戲控制器的HID報告。
透過bitSet()與bitClear()設置與清除位元
Joystick程式庫原始碼定義了一個uint32_t(無號32位元整數,也就是正整數)類型的buttons變數,用來儲存手把上的14個按鍵的狀態(其實14個按鍵只需要16位元空間儲存,所以改用uint16_t類型也行)。
buttons變數的特定位元值,可透過Arduino程式語言內建的bitSet()和bitClear()兩個函式來設定或清除:
bitSet(資料變數, 編號):把「資料變數」的「編號」位置值設定成1,「資料變數」為整數類型。例如,底下敘述將把buttons變數的第2個位元(代表A鍵)設成1,其餘值不變:
bitSet(buttons, 2);
bitClear(資料變數, 編號):清除「資料變數」的「編號」位置,也就是將該值設定成0。例如,底下敘述將把buttons變數的第7個位元(代表ZR鍵)設為0,其餘值不變:
bitClear(buttons, 7);
底下是Joystick程式庫的pressButton(), releaseButton()和setButton()的原始碼:
void Joystick_::pressButton(uint8_t button){ // 按下按鍵 bitSet(buttons, button); // 如果執行begin()時,沒有傳入false參數值, // 則底下的條件式將被執行,代表每次按下按鍵, // 就傳送此遊戲控制器的報告 if (autoSendState) sendState(); } void Joystick_::releaseButton(uint8_t button) { // 放開按鍵 bitClear(buttons, button); if (autoSendState) sendState(); } void Joystick_::setButton(uint8_t button, uint8_t value) { if (value == 0) { releaseButton(button); // 放開按鍵 } else { pressButton(button); // 按下按鍵 } }
設定十字鍵(HAT)的值
十字鍵的狀態值存在帶正負號16位元整數類型的hatSwitch變數中:
int16_t hatSwitch;
其預設值為-1,代表「未按下」,請參閱上一篇貼文說明。設定十字鍵狀態值的方法是setHatSwitch(),它將把收到的值存入hatSwitch變數:
void Joystick_::setHatSwitch(int16_t value) { hatSwitch = value; if (autoSendState) sendState(); }
設定兩個類比搖桿的值
兩個類比搖桿的X, Y方向值分別暫存在底下4個變數,類型是帶正負號的整數,可接受-127~127的輸入值:
int8_t xAxis; // 左搖桿X int8_t yAxis; // 左搖桿Y int8_t zAxis; // 右搖桿X int16_t zAxisRotation; // Z軸旋轉;右搖桿Y。
設定這些軸的方法的原始碼如下,處理後的每個軸的資料值都是介於0~255(參閱下一節)。
void Joystick_::setXAxis(int8_t value) { // 左搖桿X,接受正負整數值。 xAxis = value; if (autoSendState) sendState(); } void Joystick_::setYAxis(int8_t value) { // 左搖桿Y yAxis = value; if (autoSendState) sendState(); } void Joystick_::setZAxis(int8_t value) { // 右搖桿X zAxis = value; if (autoSendState) sendState(); } void Joystick_::setZAxisRotation(int16_t value) { // 右搖桿Y zAxisRotation = value; if (autoSendState) sendState(); }
傳送遊戲控制器的HID報告
傳送報告的sendState()方法原始碼如下:
void Joystick_::sendState(){ // JOYSTICK_STATE_SIZE(報告數據大小)的值為7 uint8_t data[JOYSTICK_STATE_SIZE]; uint32_t buttonTmp = buttons; // 暫存14個按鍵的資料 // 14個按鍵資料拆分成兩個位元組 data[0] = buttonTmp & 0xFF; // 低位元組存入data[0] buttonTmp >>= 8; // 把高位元組右移到低位元 data[1] = buttonTmp & 0xFF; // 高位元組存入data[1] if (hatSwitch < 0) // 若十字鍵的值為-1… hatSwitch = 8; // 設成8 // 不管十字鍵的高4位元的值為何,存入data[2]。 data[2] = (B00001111 & hatSwitch); // 類比搖桿值+127,讓資料介於0~255。 data[3] = xAxis + 127; data[4] = yAxis + 127; data[5] = zAxis + 127; data[6] = zAxisRotation + 127; HID().SendReport(data[0], data + 1, JOYSTICK_STATE_SIZE); }
上面的程式中,原本兩個類比搖桿、四個軸的值介於-127~127(參閱下文「Switch Pro相容控制器的Arduino原始碼說明」),加上127之後變成介於0~254。最後一行透過Arduino IDE內建的HID程式庫物件的SendReport()方法送出HID報告,其語法格式如下:
HID物件. SendReport(報告編號, 報告資料陣列, 長度)
若要在Windows系統上使用此遊戲控制器,請將送出HID報告的敘述改成:
HID().SendReport(JOYSTICK_REPORT_ID, data, JOYSTICK_STATE_SIZE);
要在任天堂Switch遊戲機上使用此控制器,送出HID報告的敘述的「報告編號」要改成第1筆報告數據,報告資料從第2筆開始:
HID().SendReport(data[0], data + 1, JOYSTICK_STATE_SIZE);
Switch Pro相容控制器的Arduino原始碼說明
Switch Pro相容控制器的Arduino原始碼引用了兩個程式庫,一個負責讀取Wii Classic Controller(經典手把)的狀態,一個將手把的資料轉成HID報告傳給Switch遊戲機。
Wii經典手把的類比搖桿比較特別,兩個搖桿的數值範圍不一樣,而且中間值也不同。
這個搖桿程式的原始碼透過底下的函式,將左類比搖桿的水平和垂直軸以15, 25, 35和45為分界,對應成這些值:-127, -64, 0, 64和127。
int leftStick(uint8_t input) { if (input > 45) return 127; else if (input > 35) return 64; else if (input < 15) return -127; else if (input < 25) return -64; return 0; }
右類比搖桿的水平和垂直軸以8, 12, 20和24為分界,對應成這些值:-127, -64, 0, 64和127。
int rightStick(uint8_t input) { if (input > 24) return 127; else if (input > 20) return 64; else if (input < 8) return -127; else if (input < 12) return -64; return 0; }
一般的類比搖桿模組(像下圖這一款),垂直軸往上感測值會遞減、往下遞增(假設類比數位轉換器的解析度是10位元,最高值為1023)。
經典手把的類比搖桿則相反:垂直軸往上感測值會遞增、往下遞減。所以loop()函式中,設置左右搖桿Y軸的值要乘上-1:
Joystick.setXAxis(leftStick(classic.leftJoyX())); Joystick.setYAxis(leftStick(classic.leftJoyY()) * -1); // 反相垂直軸的值 Joystick.setZAxis(rightStick(classic.rightJoyX())); Joystick.setZAxisRotation(rightStick(classic.rightJoyY()) * -1); // 反相垂直軸的值
自製任天堂Switch遊戲機的相容控制器
我們可以用其他元件替換這個Switch Pro相容控制器的手把,像是自己組裝的類比搖桿和按鍵。Arduino也曾在2012年底發表遊戲控制器造型的開發板“Arduino Esplora”,當年也有一款同樣採用Atmega32u4微控器的”MaKey MaKey”開發板上市(參閱「寓教於樂的Arduino:MaKey MaKey, Arduino Leonardo以及Arduino Esplora控制板」),它們都可以透過Joystick程式庫連接任天堂Switch遊戲機。
底下是簡單的Switch遊戲機相容控制器實驗,連接兩個微觸開關當作A和B鍵,一個類比搖桿模組當作「左」搖桿。兩個微觸開關分別數位2和3腳,開關的另一腳接地。類比搖桿的X和Y分別接到A0和A1腳。
這個實驗只需要引用Joystick程式庫,程式碼如下。由於開關接腳啟用內部上拉電阻,所以平時輸入值是高電位,按下開關時變成低電位。類比搖桿的原始值介於0~1023,透過map()函式轉換成0~255。
#include <Joystick.h> void setup() { pinMode(2, INPUT_PULLUP); // 啟用上拉電阻 pinMode(3, INPUT_PULLUP); Joystick.begin(false); // 不要自動送出HID報告 } void loop() { int leftX = analogRead(A0); int leftY = analogRead(A1); Joystick.setXAxis(map(leftX, 0, 1023, 0, 255)); // 左搖桿X Joystick.setYAxis(map(leftY, 0, 1023, 0, 255)); // 左搖桿Y,不用乘上-1 Joystick.setZAxis(127); // 右搖桿X,保持在中間。 Joystick.setZAxisRotation(127); // 右搖桿Y,保持在中間。 Joystick.setButton(2, !digitalRead(2)); // A鍵,取相反值 Joystick.setButton(1, !digitalRead(3)); // B鍵,取相反值 Joystick.setButton(0, 0); // Y鍵,始終輸入0。 for (byte i = 3; i < 11; i++) { // 其餘11個按鍵,始終輸入0。 Joystick.setButton(i, 0); } Joystick.setHatSwitch(-1); // 十字鍵,保持「未按下」。 Joystick.sendState(); // 送出手把的HID報告。 delay(1); }
上文提到,Joystick程式庫的類比搖桿輸入值介於-127~127,程式庫會將它加上127轉換成0~254。我覺得這個步驟有點多餘,所以把程式庫的sendState()方法的這段敘述:
data[3] = xAxis + 127; data[4] = yAxis + 127; data[5] = zAxis + 127; data[6] = zAxisRotation + 127;
改成:
data[3] = xAxis; data[4] = yAxis; data[5] = zAxis; data[6] = zAxisRotation;
Joystick.h標頭檔的這些儲存類比軸的私有屬性的類型,也從原本的int8_t改成正整數類型:
uint8_t xAxis;
uint8_t yAxis;
uint8_t zAxis;
uint8_t zAxisRotation;
Joystick.cpp程式中,使用到這些變數的程式碼也要調整,例如,設定Z軸旋轉的setZAxisRotation()方法,原本接收int16_t類型值,改成uint8_t:
void Joystick_::setZAxisRotation(uint8_t value) { zAxisRotation = value; if (autoSendState) sendState(); }
修改完畢後,編譯並上傳程式到Leonardo板,即可透過類比搖桿以及A和B鍵操控Switch遊戲機。