自製Switch Pro相容遊戲控制器(二):Gamepad手把的HID Report Descriptor(報告描述器)格式說明

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

飛行搖桿

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

Switch Pro遊戲手把

遊戲控制器的HID Report Descriptor(報告描述器)

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

USB HID報告(report)

報告內容是一連串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代表「左下(同時按「左」和「下」)…等等。

Logical(邏輯)、Physical(實體)值、

其實左上圖遺漏一個「全都未按下」的狀態,通常用-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的補數的「負數」轉換方式為:先把2進位數字反相再加1。以數字2為例,經過這個步驟得到的1110(0xE),代表-2:

2轉換成負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
};
Posts created 470

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *

Related Posts

Begin typing your search term above and press enter to search. Press ESC to cancel.

Back To Top