搖桿(Joystick)和遊戲手把(Gamepad)是電玩遊戲常見的兩種人機介面裝置(HID),底下是一款飛行搖桿,飛行桿可控制飛行器的X, Y, Z軸姿態,飛行桿上面的幾個按鍵可控制武器系統,其中的HAT(帽子開關,也稱「苦力帽」)是個小搖桿或方向鍵。

底下是任天堂Switch Pro遊戲手把的外觀和按鍵編號,它具有14個按鍵、2個類比搖桿(搖桿本體可下壓)以及稱為D-Pad或HAT的十字鍵。

遊戲控制器的HID Report Descriptor(報告描述器)
USB人機介面裝置和主機之間傳送的訊息,稱作「報告(report)」,每當使用者操作控制器,例如,按下A鍵,控制器就會發送所有按鍵和搖桿的狀態報告給主機。

報告內容是一連串2進位資料,以Switch Pro控制器為例,報告的第3個位元代表A鍵的狀態,若該位元值為1,代表A鍵被按下了。
初次連接主機時,人機介面裝置會傳送一個HID報告描述器(Report Descriptor)給主機,報告描述器相當於「資料對照表」,讓主機知道HID報告資料的格式,例如,第1個位元是Y鍵、第2個位元是B鍵…等。
不同USB人機介面裝置的元件數量和組成結構不盡相同,像鍵盤、滑鼠和搖桿的組成方式差別很大,不同廠牌型號也不一樣,所以每個HID裝置都要準備一個報告描述器。
HID報告描述器本身也要按照USB組織協會制定的格式編寫,請參閱《超圖解ESP32深度實作》第16章的「人機介面裝置(HID)程式庫的原理說明」。
本單元採用的Joystick程式庫(參閱第一篇文章,這個程式庫應該命名成Gamepad比較貼切)可讓採用ATmega32U4微控器的開發板被主機識別為Switch Pro控制器。HID報告描述器寫在Joystick.cpp原始檔,定義成名叫_hidReportDescriptor的字元常數陣列。
Arduino程式開發工具內建的HID程式庫不像ESP32的BLE(低功耗藍牙)程式庫(HIDTypes.h檔)有定義報告描述器的關鍵字常數,像USAGE_PAGE, REPORT_ID, HIDINPUT, … 等等,所以Joystick.cpp裡的HID報告描述器直接用USB組織定義的16進位代碼編寫。例如,USAGE_PAGE(用途類型)要寫成0x05,而“0x05, 0x01”代表「通用桌面控制類型(Generic Desktop)」。
例如,Joystick.cpp原始碼當中的這段程式,描述了這個HID裝置是一種“Gamepad”(遊戲手把):
#define JOYSTICK_REPORT_ID 0x03 #define JOYSTICK_STATE_SIZE 7 static const uint8_t _hidReportDescriptor[] PROGMEM = { // Joystick 0x05, 0x01, // USAGE_PAGE (Generic Desktop) 0x09, 0x05, // USAGE (Gamepad) - Very important for Switch 0xa1, 0x01, // COLLECTION (Application) 0x85, JOYSTICK_REPORT_ID, // REPORT_ID (3)
Gamepad(遊戲手把)類型的USAGE(用途)編號是0x05(參閱USB組織官方HID Usage Tables文件第48頁),REPORT_ID(報告識別碼)預設為0x03。若是Joystick(搖桿)類型,USAGE(用途)編號要改成0x04。
底下的程式描述了這個遊戲控制器共有16個按鍵,其中的“(Button 32)”註解是筆誤,應該是“(Button 16)”:
// 16 Buttons 0x05, 0x09, // USAGE_PAGE (Button),「按鍵」 0x19, 0x01, // USAGE_MINIMUM (Button 1),範圍最小值1 0x29, 0x10, // USAGE_MAXIMUM (Button 32),範圍最大值16 0x15, 0x00, // LOGICAL_MINIMUM (0),邏輯最小值0 0x25, 0x01, // LOGICAL_MAXIMUM (1),邏輯最小值1 0x75, 0x01, // REPORT_SIZE (1),1個位元 0x95, 0x10, // REPORT_COUNT (16),共16位元 0x55, 0x00, // UNIT_EXPONENT (0),單位指數(0) 0x65, 0x00, // UNIT (None),單位(無) 0x81, 0x02, // INPUT (Data,Var,Abs)
對照上文的Switch Pro外觀圖片,這個遊戲控制器實際只有14個按鍵,但因為報告資料的基本單位是位元組(8位元),所以這裡額外定義了兩個按鍵來補成16位元。多餘的按鍵定義不會造成影響,因為沒有實質作用。
上面的報告描述器定義了按鍵值的單位(UNIT)和單位指數(UNIT_EXPONENT),這兩個參數留待下文說明。其實這兩個參數的預設值分別就是None(無)和0,所以可以省略不寫。
筆者把上面的報告改成底下的敘述,明確指出這個裝置有14個按鍵,另外補上2個沒有作用的位元、刪除單位以及單位指數描述。
// 14個按鍵 0x05, 0x09, // USAGE_PAGE (Button) 0x19, 0x01, // USAGE_MINIMUM (範圍最小值:1) 0x29, 0x0E, // USAGE_MAXIMUM (範圍最大值:14) 0x15, 0x00, // LOGICAL_MINIMUM (0) 0x25, 0x01, // LOGICAL_MAXIMUM (1) 0x75, 0x01, // REPORT_SIZE (1) 0x95, 0x0E, // REPORT_COUNT (14個按鍵) 0x81, 0x02, // INPUT (Data,Var,Abs) // 補上兩個空白 0x95, 0x01, // REPORT_COUNT (1) 0x75, 0x02, // REPORT_SIZE (2個位元) 0x81, 0x01, // INPUT (保留)
HID裝置的Logical(邏輯)、Physical(實體)值、Unit(單位)和Unit Exponent(單位指數)
電玩控制器的十字鍵,通常定義了8個狀態,每個方向用一個數字代表,例如:0代表「上」、2代表「右」、5代表「左下(同時按「左」和「下」)…等等。

其實左上圖遺漏一個「全都未按下」的狀態,通常用-1表示。這些代表「哪些鍵被按下的狀態值」稱作邏輯(Logical)值,各個按鍵對應的實際角度,叫做實體(Physical)值。
上一節的14個按鍵只有定義邏輯值,省略定義實體值,代表實體值等同邏輯值,而按鍵開關實際上也只有「開」和「關」兩個狀態。
底下是十字鍵的報告描述內容,其中的PHYSICAL_MINIMUM和PHYSICAL_MAXIMUM用於定義實際的數值範圍,而UNIT則用於定義數值的單位,此處為「角度」。
// 8方向十字鍵(HAT) 0x05, 0x01, // USAGE_PAGE (General Desktop),通用桌上型 0x09, 0x39, // USAGE (Hat Switch),帽子開關(十字鍵) 0x15, 0x00, // LOGICAL_MINIMUM (0),邏輯最小值 0x25, 0x07, // LOGICAL_MAXIMUM (7),邏輯最大值 0x35, 0x00, // PHYSICAL_MINIMUM (0),實體最小值 0x46, 0x3B, 0x01, // PHYSICAL_MAXIMUM (315) ,實體最大值 0x65, 0x14, // UNIT (Eng Rot:Angular Pos),單位:英制角度 0x75, 0x04, // REPORT_SIZE (4),佔4個位元 0x95, 0x01, // REPORT_COUNT (1) 0x81, 0x02, // INPUT (Data,Var,Abs),絕對可變資料
上面的「實體最大值」設成315,16進制為0x013B,但HID報告描述器的資料排列順序採用小頭派(Little-Endian,也譯做小端序),也就是低位元組在前、高位元組在後,因此0x013B要寫成0x3B, 0x01。
USB組織定義了如下表的單位類型,請參閱官方Device Class Definition for Human Interface Devices (HID)文件第37頁,其中的「國標」代表國際標準(SI)。

單位值的定義,以半位元組(4個位元,稱為“nibble”)來劃分,第0個「半位元組」定義單位的制式(system)。對照上表,4代表「英制旋轉」;2代表「國標旋轉」。
第0個「半位元組」以外的位數代表單位的類型,例如,第1個「半位元組」位數代表「長度」、第3位數代表「時間」。請看看底下兩個例子:

左上圖的單位0x14值,代表這個單位是「角度」;右上圖的0x1001則代表這個單位是「秒」。由於國標和英制的時間單位都是「秒」,所以這個單位設定值的第0個「半位元組」可以是1~4任意數字。
有些單位定義需要用到「指數」,像「奈米」單位,因為奈米是10-9公尺,USB官方定義的長度單位是公分,所以我們要先把奈米換算成10-7公分。USB組織定義了如下的代碼來表示指數數字-8~1(正整數的0次方值都是1,所以忽略不計)。

因此,定義「奈米」單位的HID報告描述寫法如下,「單位」指定為公分(0x11)、「單位指數」設成0x09,代表「指數」為-7,而「底數」則固定為10。

如果要設定單位的指數,例如,面積單位的「平方公尺」,指數代碼要寫在UNIT(單位)值所在的位數,例如:

因為十字鍵的資料只有4個位元,所以要填補4個空白位元,湊成一個位元組。
0x65, 0x00, // UNIT (無),這一行可刪除。 0x95, 0x01, // REPORT_COUNT (1) 0x75, 0x04, // REPORT_SIZE (4),共4個位元 0x81, 0x01, // INPUT
在實際測試中,刪除底下設定「實體值」以及「單位」的3行敘述,十字鍵在Switch遊戲機仍可正常運作:
0x35, 0x00, // PHYSICAL_MINIMUM (0),實體最小值 0x46, 0x3B, 0x01, // PHYSICAL_MAXIMUM (315) ,實體最大值 0x65, 0x14, // UNIT (Eng Rot:Angular Pos),單位:英制角度
類比搖桿與2的補數
Switch Pro控制器包含兩個類比搖桿,由x, y以及z, rz軸構成,每個方向軸的值分別用8個位元表示,介於0~255,所以類比搖桿的報告佔4個位元組。底下是類比搖桿的報告描述內容:
// X, Y和Z軸 0x15, 0x00, // LOGICAL_MINIMUM (0) 0x26, 0xff, 0x00, // LOGICAL_MAXIMUM (255) 0x75, 0x08, // REPORT_SIZE (8) 0x09, 0x01, // USAGE (Pointer),游標 0xA1, 0x00, // COLLECTION (Physical),游標的座標資料集合 0x09, 0x30, // USAGE (x) 0x09, 0x31, // USAGE (y) 0x09, 0x32, // USAGE (z) 0x09, 0x35, // USAGE (rz) 0x95, 0x04, // REPORT_COUNT (4) 0x81, 0x02, // INPUT (Data,Var,Abs) 0xc0, // END_COLLECTION
其中的Pointer(指標)代表:能產生多軸(如:X, Y, Z和反向Z)方向值來驅動應用程式物件的東東,而這個指標的所有控制軸都歸納在COLLECTION (Physical)類型的集合裡面。
其中邏輯最大值LOGICAL_MAXIMUM (255)的描述採用兩個位元組:0x00FF(寫成0xFF, 0x00),這是因為HID報告的資料值採2的補數格式,若最高位元為1,則該數字將被視為負值,例如,0xFF代表-1,而非255。下表列舉用2的補數法表示的-8~7:

2的補數的「負數」轉換方式為:先把2進位數字反相再加1。以數字2為例,經過這個步驟得到的1110(0xE),代表-2:

在0xFF的前面加上0,它就不是負數了,所以255在此寫成0x00FF。附帶說明,2的補數的0x00~0x7F,代表10進位的0~127;0xFF~0x80代表-1~-128。
完整的Switch Pro遊戲控制器的報告描述器
綜合以上說明,修改後的Switch Pro遊戲控制器的報告描述器(Report Descriptor)內容如下:
static const uint8_t _hidReportDescriptor[] PROGMEM = { // 遊戲控制器(Gamepad) 0x05, 0x01, // USAGE_PAGE (Generic Desktop) 0x09, 0x05, // USAGE (Gamepad) 0xa1, 0x01, // COLLECTION (Application) 0x85, 0x03, // REPORT_ID (3) // 14個按鍵 0x05, 0x09, // USAGE_PAGE (Button) 0x19, 0x01, // USAGE_MINIMUM (範圍最小值:1) 0x29, 0x0E, // USAGE_MAXIMUM (範圍最大值:14) 0x15, 0x00, // LOGICAL_MINIMUM (0) 0x25, 0x01, // LOGICAL_MAXIMUM (1) 0x75, 0x01, // REPORT_SIZE (1) 0x95, 0x0E, // REPORT_COUNT (14個按鍵) 0x81, 0x02, // INPUT (Data,Var,Abs) // 補上兩個空白 0x95, 0x01, // REPORT_COUNT (1) 0x75, 0x02, // REPORT_SIZE (2個位元) 0x81, 0x01, // INPUT (保留) // 8方向十字鍵(HAT) 0x05, 0x01, // USAGE_PAGE (General Desktop),通用桌上型 0x09, 0x39, // USAGE (Hat Switch),帽子開關(十字鍵) 0x15, 0x00, // LOGICAL_MINIMUM (0),邏輯最小值 0x25, 0x07, // LOGICAL_MAXIMUM (7),邏輯最大值 0x35, 0x00, // PHYSICAL_MINIMUM (0),實體最小值 0x46, 0x3B, 0x01, // PHYSICAL_MAXIMUM (315) ,實體最大值 0x65, 0x14, // UNIT (Eng Rot:Angular Pos),單位:英制角度 0x75, 0x04, // REPORT_SIZE (4),佔4個位元 0x95, 0x01, // REPORT_COUNT (1) 0x81, 0x02, // INPUT (Data,Var,Abs),絕對可變資料 // 填補4個空白位元 0x95, 0x01, // REPORT_COUNT (1) 0x75, 0x04, // REPORT_SIZE (4),共4個位元 0x81, 0x01, // INPUT // X, Y和Z軸 0x15, 0x00, // LOGICAL_MINIMUM (0) 0x26, 0xff, 0x00, // LOGICAL_MAXIMUM (255) 0x75, 0x08, // REPORT_SIZE (8) 0x09, 0x01, // USAGE (Pointer),游標 0xA1, 0x00, // COLLECTION (Physical),游標的座標資料集合 0x09, 0x30, // USAGE (x) 0x09, 0x31, // USAGE (y) 0x09, 0x32, // USAGE (z) 0x09, 0x35, // USAGE (rz) 0x95, 0x04, // REPORT_COUNT (4) 0x81, 0x02, // INPUT (Data,Var,Abs) 0xc0, // END_COLLECTION 0xc0 // END_COLLECTION };