読者です 読者をやめる 読者になる 読者になる

USB規格を理解しなくてもできるUSBジョイスティックの作り方(PIC)

タイトルでこんなこと書いてますが「これ使え」で終わりです
Microchipさん本当にありがとう

Microchip Libraries for Applications | Microchip Technology Inc.

自分は秋月電子のPIC18F14K50のボードでやりました(mla ver. = v2015_08_10)

下準備

  1. MPLAB X,XC8 をインストール
  2. mla インストールフォルダ内の
    mla\v2015_08_10\apps\usb\device\hid_joystickプロジェクトをMPLAB Xで開く
    (自分は元ファイルを残しておきたかったのでhid_joystickをコピーして名前を変えたものを編集します)
  3. プロジェクトウィンドウからHIDプロジェクトを右クリしてSet as Main Project
  4. プロジェクトのプロパティから自分の環境にインストールされているXC8コンパイラを選択する(そうしないとこの記事みたいになります)
  5. 左上のプルダウンメニューから自分のPICを選択してビルド成功することを確認する

オリジナルデバイス作成手順

プロジェクトには様々なファイルが沢山入っていますが自分用HIDジョイスティックを作るときに変更する必要があるのはapp_device_joystick.c usb_descriptors.c usb_config.hの3ファイルのみです
これらはプロジェクトのSource Files\appに入っています

usb_descriptors.c

最初にして最難関です
このファイルはパソコンとPICが通信した時にボタンが何個あるかとか、アナログ軸が何本あってどの用途で使うのかとかを通知するためのデータを定義しています
変更するのはこのファイルの一番下に有るこいつ(レポートディスクリプタ)です

const struct {
    uint8_t report[HID_RPT01_SIZE];
} hid_rpt01 = {{
・・・省略
}};

デフォルトのコードを改造して、これを自分は以下のようにしました
128ボタン ハットスイッチなし 10bit-8軸のゲームパッド(ジョイスティック?)です
ボタン数、軸数の根拠はここ

const struct {
    uint8_t report[HID_RPT01_SIZE];
} hid_rpt01 = {
    {
        0x05, 0x01,      //USAGE_PAGE (Generic Desktop)     Globalアイテム
        0x09, 0x05,      //USAGE (Game Pad)                 Localアイテム
        0xA1, 0x01,      //COLLECTION (Application)         Mainアイテム

        //押しボタン
        0x15, 0x00,         //  LOGICAL_MINIMUM(0)             Globalアイテム
        0x25, 0x01,         //  LOGICAL_MAXIMUM(1)             Globalアイテム
        0x35, 0x00,         //  PHYSICAL_MINIMUM(0)            Globalアイテム
        0x45, 0x01,         //  PHYSICAL_MAXIMUM(1)            Globalアイテム
        0x75, 0x01,         //  REPORT_SIZE(1)                 Globalアイテム
        0x95, 0x80,         //  REPORT_COUNT(128)              Globalアイテム
        0x05, 0x09,         //  USAGE_PAGE(Button)             Globalアイテム
        0x19, 0x01,         //  USAGE_MINIMUM(Button 1)        Localアイテム
        0x29, 0x80,         //  USAGE_MAXIMUM(Button 128)      Localアイテム
        0x81, 0x02,         //  INPUT(Data,Var,Abs)            Mainアイテム

        //ハットスイッチとアナログ軸との共用部分に注意
        0x05, 0x01,         //  USAGE_PAGE(Generic Desktop)    Globalアイテム
        0x65, 0x14,         //  UNIT(Eng Rot:Angular Pos)      Globalアイテム

        //アナログ軸 各軸16bitでレポートするけど内容は10bitデータ
        0x26, 0xFF, 0x03,   //  LOGICAL_MAXIMUM(1023)          Globalアイテム
        0x46, 0xFF, 0x03,   //  PHYSICAL_MAXIMUM(1023)         Globalアイテム
        0x09, 0x30,         //  USAGE(X)                       Localアイテム
        0x09, 0x31,         //  USAGE(Y)                       Localアイテム
        0x09, 0x32,         //  USAGE(Z)                       Localアイテム
        0x09, 0x33,         //  USAGE(Rz)                      Localアイテム
        0x09, 0x34,         //  USAGE(Ry)                      Localアイテム
        0x09, 0x35,         //  USAGE(Rz)                      Localアイテム
        0x09, 0x36,         //  USAGE(Slider)                  Localアイテム
        0x09, 0x37,         //  USAGE(Dial)                    Localアイテム
        0x75, 0x10,         //  REPORT_SIZE(16)                 Globalアイテム
        0x95, 0x08,         //  REPORT_COUNT(8)                Globalアイテム
        0x81, 0x02,         //  INPUT(Data,Var,Abs)            Mainアイテム

        0xC0            //END_COLLECTION                    Mainアイテム
    }
};

簡単に説明すると
COLLECTION ~ END_COLLECTION内で定義されたデバイスをどんな用途に使うか最初の2行で定義して、
COLLECTION内では各入力デバイスの情報をそれぞれ定義しているということですね

例えば押しボタンなら

  • ボタンが取りうる最大値、最小値、(LOGICAL_MINIMUM,LOGICAL_MAXIMUM,PHYSICAL_MINIMUM,PHYSICAL_MAXIMUM)
  • ボタン一つあたりのレポートに使うビット数(REPORT_SIZE)
  • いくつボタンが有るか(REPORT_COUNT)
  • 用途は何か(USAGE_PAGE(Button))
  • ボタン番号何番から何番として使用するか(USAGE_MINIMUM,USAGE_MAXIMUM)
  • 送られてくるデータはどんなものか(INPUT(Data,Var,Abs):定数でなくて絶対値のデータを送るという意味(相対値もある) - 後述する[padding--app_device_joystick.cではfiller]ではダミーの定数を送りつける)

アナログ軸もだいたい同じです
気をつけるべきところはGlobalアイテムは一度定義されると値を変更して再定義されるまでその状態を引き継ぐということです
つまりLOGICAL_MINIMUM等は最初から最後まで生き残ってアナログ軸にも適用されています
逆にLOGICAL_MAXIMUM等はアナログ軸では1から1023に変更されて適用されています(押しボタンは0~1の範囲を取るけど、アナログ軸は0~1023の範囲を取る)

このために、真ん中辺りのハットスイッチがどうこうと言ってる部分があるわけです
(デフォルトコードでハットスイッチに適用されていたGlobalアイテムはアナログ軸でも使用する)
(UNIT(Eng Rot:Angular Pos) がなぜハットスイッチのInputの前にあったかは自分はわからないです)

この詳細はUSB.orgの資料 Hut_12v2.pdf, HID1_11.pdf に詳しく書いてあります(全然理解してません)
また、USB.orgで配布されているHID Descriptor Toolはアイテムを選択すると16進のバイト列を表示してくれるので資料を調べるより早いこともあります
まあ一番早いのはネットの海に転がってるコードを見ることですが
ルネサスUSB Human Interface Devices(HID)Class サンプルプログラムは日本語でサンプルコードもあるので大変役立ちました

usb_config.h

そんなこんなでお好みのデバイス構成を定義したら次はusb_config.h

#define HID_RPT01_SIZE          59

の値を自分の↑で定義したレポートディスクリプタの大きさに変更します
これはレポートディスクリプタの大きさより大きくても小さくてもパソコンに認識されません(尤も小さい場合はコンパイラがエラー吐いてくれますが大きいとビルドを通ってしまいます - 自分はこれで あれ? なんで動かないんだってなりました)
HID_RPT01_SIZEがレポートディスクリプタより大きい場合は、PICをパソコンに繋ぐと
ドライバーをインストールしています... → ドライバのインストールに失敗しました と言われるので、そうなったらHID_RPT01_SIZEを確認してください

app_device_joystick.c

最後にapp_device_joystick.cです
この中の

typedef union _INTPUT_CONTROLS_TYPEDEF
{
・・・省略
} INPUT_CONTROLS;

を先ほど定義したレポートディスクリプタのとおりに変数を並べるだけです

typedef union _INTPUT_CONTROLS_TYPEDEF
{
    struct
    {
        struct
        {
            uint8_t square:1;
            uint8_t x:1;
            uint8_t o:1;
            uint8_t triangle:1;
            uint8_t L1:1;
            uint8_t R1:1;
            uint8_t L2:1;
            uint8_t R2:1;
            uint8_t select:1;
            uint8_t start:1;
            uint8_t left_stick:1;
            uint8_t right_stick:1;
            uint8_t home:1;
            uint8_t :3;
            uint8_t ad[13];
            uint8_t a;
        } buttons;
        struct
        {
            uint16_t X;
            uint16_t Y;
            uint16_t Z;
            uint16_t Rx;
            uint16_t Ry;
            uint16_t Rz;
            uint16_t Slider;
            uint16_t Dial;
        } analog_stick;
    } members;
    uint8_t val[32];
} INPUT_CONTROLS;

デフォルトコードをそのまま使ってるのでちょっと汚いですね
別にボタン1bit毎に名前をつけたり、最後にuint8_t val[32]で全部一括で配列として扱えるようにしなくてもいいのですがそうするときっと便利です
この例ではレポートディスクリプタどおりbuttons構造体が計128bit、16ビットのアナログ軸が計8個となっています

最後の最後

app_device_joystick.c内のvoid APP_DeviceJoystickTasks(void)の中身でINPUT_CONTROLS構造体にボタンやら可変抵抗やら読み取った値を代入していく処理を書きます
そうすることであとはMicrochip USB Frameworkが適当にループで呼び出してくれます
あ、最後にちゃんと送信関数は呼んでね
lastTransmission = HIDTxPacket(JOYSTICK_EP, (uint8_t*) & joystick_input, sizeof (joystick_input));

おまけ padding filler

途中でpadding, fillerが出てきましたがこれはボタン数とかが8個単位じゃなくて変数にあまりができてしまう時にそれを埋めるものです
デフォルトのコードのレポートディスクリプタINPUT_CONTROLS構造体を見比べてみるとpaddingとfillerの位置と数が対応していることがわかるでしょう
まあ、Input(Const...なんちゃら)で埋めとけばいいんです

プログラムは大丈夫なはずなのに一部の軸が上手く動かない!

Windowsが軸構成をレジストリに覚えているそうです yotta0xff.hatenablog.com




以上の過程できっとそれなりに汎用のジョイスティックを作れるでしょう
アナログ軸をもっと増やしたい!とか Button is everything. More is better. みたいな人はUSAGE(BYTE_COUNT)とか使って自前のソフトウェアで受け取るしか無いと勝手に思ってます
HID規格で定義されていてWindowsのゲームなどで汎用的に同時に使用できる軸は8軸くらいなんじゃないかなということです

自分のジョイスティックがちゃんと機能しているか確認したい場合はusbhid-dumpが便利です

yotta0xff.hatenablog.com