SNSへはこちら

STM32でUSBをベアメタル(8) - HIDを構成

ひとまずマウスを作ることにしました。

USB HIDとは

そもそも HID とは Human Interface Device のことであり、人間がホストに対して情報を入力するためのデバイスを指します。身近な例だと

  • キーボード
  • マウス
  • ジョイスティック

がメジャーです。HID クラスを実装するには独自の HID Class Descriptor をコンフィギュレーションディスクリプタの一部として送る必要があります。
実際にコンフィギュレーション等が完了して、デバイスとして動くためには Report Descriptor を言うのを送ります。ここには押したボタンの情報や、マウスカーソルの移動情報が含まれるようです。
なお、HID の規格に関しては、USB.org の規格書を参考にしました。

USB CDC に比べてゴチャゴチャしていないので、全体像がつかみやすく、モヤモヤせずにデバイス開発ができるかなあと思いました。

ディスクリプタ作成

とはいっても実際にはネット上にはそれほど情報が落ちていないので(僕のググリティが足りないだけかも)、誰かの情報を半ばコピペする形でやっていきます。
こちらのサイトキーボードとマウスのコンポジットデバイスを解析しているようなので、その内のマウスのみを見て行きます。

そしてなるべく、手打ちコピーしながらその意味を理解していくようにしました。

実際のディスクリプタ

まずは全体像。当然ながら、Report Descriptor は含まれません。また、wbcd で始まるメンバは 16bit であり、USB はリトルエンディアンなので、下位8bit -> 上位8bit の順で記述していることに注意してください。

// For USB HID(Mouse)
const uint8_t __attribute__((aligned(2))) conf_desc[] = {
        // Configuration Descriptor
        CONFIG_DESCRIPTOR_LENGTH,
        CONFIG_DESCRIPTOR_TYPE,
        34 & 0xFF, // .wTotalLength
        34 >> 8, // same above
        1, // .bNumInterfaces
        1, // .bConfigurationValue,
        0, // .iConfiguration
        0x80, // .bmAttributes
        0x32, // MaxPower

        // Interface Descriptor
        INTERFACE_DESCRIPTOR_LENGTH,
        INTERFACE_DESCRIPTOR_TYPE,
        0, // bInterfaceNumber
        0, // bAlternateSetting
        1, // bNumEndpoints
        0x03, // .bInterfaceClass
        0x01, // .bInterfaceSubClass(Boot Interface Subclass)
        0x02, // bInterfaceProtocol(Mouse)
        0, // iInterface

        // HID Class Descriptor
        0x09, // bLength
        0x21, // bDescriptorType
        0x00, // bcdHID
        0x10, // L> 1.00
        0, // bCountryCode
        1, // bNumDescriptors
        0x22, // bDescriptorType
        50, // wDescriptorLength(The length of Report Desc)
        0x00,

        // Endpoint Descriptor
        ENDPOINT_DESCRIPTOR_LENGTH,
        ENDPOINT_DESCRIOTPR_TYPE,
        0x81, // .bEndpointAddress(IN, EP1)
        0x03, // Interrupt Transfer
        64, // .wMacPacketSize
        0,
        0x0A // bInterval
};

Configuration Descriptor

bNumInterfaces ですが、HID の場合は単純でインターフェイスが1つのみなので、1 と指定します。
また、用意するエンドポイントも1つだけなので楽ですね〜〜〜

bmAttributes は Bus Powered な感じなので、そこだけビットを立てておきました。MaxPower は適当です()

Interface Descriptor

bInterfaceNumber はインターフェイスの通し番号です。zero-oriented らしいので、0 としています。

bInterfaceClass は HID である 0x03 指定。bInterfaceSubClass は Boot Interface とします。よく分からないですが、どうやら「BIOS を操作するのにも使える、お行儀の良いデバイスだよ!」ということを示すそうです。

HID Class Descriptor

特に言うこと無し。

Endpoint Descriptor

HID に関して、具体的にどのようなエンドポイント構成にすれば良いのかわかりませんが、ここを見る限り、送信方向のインタラプト転送を実装するエンドポイントを1つ用意すれば良いようです。今回は 0x81 とし、EP1 をそのように設定することにしました。これって USB->EP1R に書き込むアドレスのことなのかなあ?

つうしん!

以上を書き込んでホストと通信させてみましょう。いつものようにログを取ります。

(省略)
** SETUP
bmResuestType: 0x80
bRequest: 6
wValue: 0x300
wIndex: 0x0
wLength: 0xff
IN
STATUS_OUT
** SETUP
bmResuestType: 0x80
bRequest: 8
wValue: 0x0
wIndex: 0x0
wLength: 0x1
IN
STATUS_OUT
** SETUP
bmResuestType: 0x81
bRequest: 6
wValue: 0x2200
wIndex: 0x0
wLength: 0x32
IN
STATUS_OUT
** SETUP
bmResuestType: 0x80
bRequest: 6
wValue: 0x301
wIndex: 0x411
wLength: 0xff
IN
STATUS_OUT
** SETUP
bmResuestType: 0x80
bRequest: 6
wValue: 0x303
wIndex: 0x411
wLength: 0xff
IN
STATUS_OUT

このように、まずは SET_CONFIGURATION が働き(未だに謎ですが)、ついで LANGID を含んだストリングディスクリプタの送出が行われます。

特筆すべきはその次の GET_DESCRIPTOR です。bmRequestType が 0x81 になっています。これまでは EP0 とコンフィギュレーションをやり取りしていただけだったので、このフィールドは 0 or 0x80 でしたが、今回は異なります。
USB の規格書によると、この通信要求はこれまでと異なり、インターフェイスに対しての情報を要求しているらしいです。

そして、このサイトに依ると、wValue == 0x2200 に対応するディスクリプタは HID のレポートディスクリプタだと言うのです!興奮してきました。

レポートディスクリプタを組むぞ

ということで、ジャンジャンやっていきましょう!

の前に

まずは EP1 をしっかりと設定せねばなりません。USB RESET 時の初期化処理に以下を追加しておきます。あと、若干プログラム記述に不適切な箇所があったのでついでに直しています。

    if( flag & USB_ISTR_RESET ) {
        iprintf("\nRESET\n");
        usb_is_ready = 0;
        USB->ISTR &= ~USB_ISTR_RESET;
        USB->CNTR |= USB_CNTR_CTRM | USB_CNTR_RESETM;
        memset((void *)&pma_allocation_info, 0, sizeof(PMAInfo));
        memset((void *)ep_info, 0, sizeof(USBEndpointInfo) * 2);
        USB->BTABLE = alloc(sizeof(PacketBuffer) * 2);
        USB->DADDR = USB_DADDR_EF; // enable the functionality

        // initialize EP0
        ep_info[0].fsm = ST_RESET;
        ep_info[0].packet_size = 64;
        ep_info[0].pb_ptr = (PacketBuffer *)((uintptr_t)USB->BTABLE + USB_PMAADDR);
        ep_info[0].pb_ptr->addr_rx = 0;
        USB->EP0R = USB_EP_CONTROL;
        pma_allocation_info.offsets.setup_packet = alloc(64);
        usb_ep_receive(0,
                pma_allocation_info.offsets.setup_packet,
                ep_info[0].packet_size
        );
        // initialize EP1
        ep_info[1].packet_size = 64;
        ep_info[1].pb_ptr = (PacketBuffer *)((uintptr_t)USB->BTABLE + USB_PMAADDR + sizeof(PacketBuffer));
        USB->EP1R = USB_EP_INTERRUPT | 0x01;
        addr_report = alloc(3); // size of report, in this case.
        pma_in(addr_report, (const void *)report, 3);
        usb_ep_send(1,
                addr_report,
                3
        );
    }

「report って何だ?」とか詳細は以下でちょびちょび述べていくことにします。

レポートディスクリプタについて勉強...しようと思ったんだ

他のディスクリプタとはだいぶ毛色が違うみたい。1つのディスクリプタ内で階層的な構成になっているらしい。勉強しようと思いましたが、色々調べている最中にマウス用のレポートディスクリプタが見つかってしまったので、以下に示します。

const uint8_t __attribute__((aligned(2))) ReportDescriptor[50] = {
    0x05, 0x01,                    // USAGE_PAGE (Generic Desktop)
    0x09, 0x02,                    // USAGE (Mouse)
    0xa1, 0x01,                    // COLLECTION (Application)
    0x09, 0x01,                    //   USAGE (Pointer)
    0xa1, 0x00,                    //   COLLECTION (Physical)
    0x05, 0x09,                    //     USAGE_PAGE (Button)
    0x19, 0x01,                    //     USAGE_MINIMUM (Button 1)
    0x29, 0x03,                    //     USAGE_MAXIMUM (Button 3)
    0x15, 0x00,                    //     LOGICAL_MINIMUM (0)
    0x25, 0x01,                    //     LOGICAL_MAXIMUM (1)
    0x95, 0x03,                    //     REPORT_COUNT (3)
    0x75, 0x01,                    //     REPORT_SIZE (1)
    0x81, 0x02,                    //     INPUT (Data,Var,Abs)
    0x95, 0x01,                    //     REPORT_COUNT (1)
    0x75, 0x05,                    //     REPORT_SIZE (5)
    0x81, 0x03,                    //     INPUT (Cnst,Var,Abs)
    0x05, 0x01,                    //     USAGE_PAGE (Generic Desktop)
    0x09, 0x30,                    //     USAGE (X)
    0x09, 0x31,                    //     USAGE (Y)
    0x15, 0x81,                    //     LOGICAL_MINIMUM (-127)
    0x25, 0x7f,                    //     LOGICAL_MAXIMUM (127)
    0x75, 0x08,                    //     REPORT_SIZE (8)
    0x95, 0x02,                    //     REPORT_COUNT (2)
    0x81, 0x06,                    //     INPUT (Data,Var,Rel)
    0xc0,                          //   END_COLLECTION
    0xc0                           // END_COLLECTION
};

勉強はまた後でかな()
これを EP0 から送ります。これによって、正式にホストが「このデバイスはマウスだ」と認識します。

ちなみに、レポートディスクリプタとレポートは異なるので要注意です。

EP1 でレポートを送るようにする

先程と同様のサイトですが、このサイト にマウスのレポートのフォーマットが載っています。

では、それに合わせてレポートを送信するように仕向けましょう。まずは以下のように変数を用意しておきます。突貫工事的ですが、後で整理します。

volatile int8_t __attribute__((aligned(2))) report[4] = {
        0, // Button
        0, // dX
        0, // dY
};
uint16_t addr_report;

そうして、受信割り込みに以下のように書けばいいでしょう。

if( ep_num == 0 ) {
...
}else if( ep_num == 1 ){
    USB_EPnR(1) = epreg & ~USB_EP_CTR_TX & USB_EPREG_MASK;
    lock = 1;
    pma_in(addr_report, (const void *)report, 3);
    usb_ep_send(1,
            addr_report,
            3
    );
    lock = 0;
}

あとは、別のスレッド(main 関数など)でこの report を書き換えるようにすればいいです。

ひとまず main はいじらずに、通信していきましょう。

** SETUP
bmResuestType: 0x21
bRequest: 11
wValue: 0x1
wIndex: 0x0
wLength: 0x0

bRequest == 11 は SET_INTERFACE だが、bmRequestType == 0x21 は標準リクエストに存在しない。HID の規格書に依ると、クラスに対するリクエストらしい。
HID においては、bRequest の意味は SET_PROTOCOL になります。どうやら Alternate 機能(?)に関するものらしいのですが、そんなものは存在しない(とディスクリプタですでに構成している)ので、無視で良いのです。

それで、ともかく EP1 に関わる割り込みがしっかり入っているっぽいのでいい感じだと思います。

さてさて、次回は実際に動かすためのコーディングをします。果たして動くのか?それとも...

今回実装したコードはこちらからどうぞ。