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遊戲機。
