TinyGoから理解するHID

Rinrin — @rin2yh

自己紹介

  • 名前: Rinrin
  • 所属: 株式会社スリーシェイク
  • 職種: フルスタックエンジニア
  • 趣味: アニメ、ゲーム、キーボード
  • ひとこと: 親知らず全部抜いてクリーンに。ジャンクフード食べたい!

目次

  1. 導入
  2. TinyGoで入力が送信されるまで
  3. 送信されたデータの意味を理解する
  4. HIDがPCに届くまで

SECTION 01

導入

ゴール

TinyGoの実装を通して、HIDデバイスからUSBホストへ入力が伝わるまでの基本的な仕組みを理解する

ポイント

  • 入力がどのようなデータとして表現されるか
  • TinyGoの中で入力がどのように処理されるか
  • HIDデバイスからUSBホストへどう伝わるか

おことわり

  • HIDは深遠かつ時間の関係上、今回は一部をピックアップして話す
    • 今回はHIDの音量調節のみ
    • 他のHIDも、同様の方法で理解できるはず!
  • TinyGoの実装とHIDの仕様にフォーカスし、OS内部の実装までは追わない
    • デバイス側はReport生成〜USB送信まで
    • USBホストのPC側はHIDドライバによる解釈まで

※ 筆者はまだHIDに明るくなく、全てを理解しているわけではありません。誤りを含む可能性があります。
なるべく技術的な正確性を担保するよう努力していますが、もし誤りがあれば後でこっそり教えてください。

HIDとは

Human Interface Devicesの略。

  • 人の操作をPCへ伝えるデバイスなどを、USB上で共通の方法で扱うための仕様。
  • 1996年、USB-IFがHID over USB仕様を承認
    • USB-IF: USB Implementers Forum。USBの仕様を策定/管理する非営利団体
  • 代表例:キーボード、マウス、ゲームパッド

USB-IF: Human Interface Devices (HID) Specifications and Tools

Consumer Controlとは

HIDの中で、音量や再生・停止などのメディア関連の操作を扱うための分類。
一般にMedia Keyと呼ばれる。

例

  • 音量調節: Volume Increment / Decrement, Mute
  • 再生・停止: Play / Pause

SECTION 02

TinyGoで入力が送信されるまで

ロータリーエンコーダーから音量を操作する実装例

func DispatchVolume(ev Event) {
    kb := keyboard.Port()

    switch {
    case ev.Delta > 0:
        kb.Press(keyboard.KeyMediaVolumeInc)
    case ev.Delta < 0:
        kb.Press(keyboard.KeyMediaVolumeDec)
    }
}

ev.Deltaをロータリーエンコーダーの回転量として扱い、正負で回転方向を判定する。
KeyMediaVolumeInc / KeyMediaVolumeDecで音量を調節している。

tiny-deck: media.go

KeyMediaVolumeInc / Dec がTinyGo内部でどう扱われるか

KeyMediaVolumeInc Keycode = 0xE9 | 0xE400
KeyMediaVolumeDec Keycode = 0xEA | 0xE400
// 省略...
func (kb *keyboard) Down(c Keycode) error {
    msb := uint8(c >> 8)
    // 省略...
    if 0xE4 <= msb && msb <= 0xE7 {
        // downCon()では値をkb.conに保持し、keyboardSendKeys(true)を呼び出す
        return kb.downCon(uint16(c & 0x03FF))
    }
    // 省略...

TinyGo: keycode.go · keyboard.go: Down()

Consumer Controlの値をUSB送信用のバイト列に変換する

func (kb *keyboard) keyboardSendKeys(consumer bool) bool {
    var b [9]byte
    // 省略...
    if consumer {
        b[0] = 0x03 // REPORT_ID
        b[1] = uint8(kb.con[0])
        b[2] = uint8((kb.con[0] & 0x0300) >> 8)

        return kb.sendKey(consumer, b[:3])
    }
    // 省略...

HIDでやり取りするデータをReportと呼ぶ。REPORT_ID は、Reportの種類を示すID。

TinyGo: keyboardSendKeys() / downCon() · SendUSBPacket()

ReportをUSBに送信する

func (kb *keyboard) sendKey(consumer bool, b []byte) bool {
    kb.tx(b)
    return true
}
// 省略...
func (kb *keyboard) tx(b []byte) {
    hid.SendUSBPacket(b)
}
// 省略...
func SendUSBPacket(b []byte) {
    machine.SendUSBInPacket(hidEndpoint, b)
}

sendKey() → tx() → SendUSBPacket() と処理が渡され、最後にReportがUSBへ送信される。

TinyGo: keyboard.go — sendKey() · hid.go — SendUSBPacket()

実際に送信されるReport

ロータリーエンコーダを回しながら、Reportをキャプチャしたログ。

# 右回し
reportID=3 bytes=03 E9 00
# クリア
reportID=3 bytes=03 00 00
# 左回し
reportID=3 bytes=03 EA 00
# クリア
reportID=3 bytes=03 00 00

ログを見てよくわからないもの

  • bytesのE9 00
    • 03はReport ID(スライド12枚目のkeyboardSendKeys()を参照)

SECTION 03

送信されたデータの意味を理解する

HID Usage Tables

Usageを定義した表。

  • Usage: HID上の操作や機能を識別する32bitの値
    • Usage Page: 上位16bit。関連するUsageをまとめるための値
    • Usage ID: 下位16bit。そのUsage Page内のUsageを識別する値
  • 今回扱う音量調節は、Consumer PageのUsageとして定義
    • Consumer Page: 音量や再生・停止などに関するUsageをまとめたUsage Page

Consumer PageのUsage

Usage ID Usage Name Usage Type Section
0xE9 Volume Increment RTC 15.9
0xEA Volume Decrement RTC 15.9

出典:HID Usage Tables 1.7, Consumer Page §15.9

E9 00の意味

バイト 読み方
03 Report ID 3
E9 00 16ビットのUsage値 0x00E9。Consumer PageではVolume Increment
EA 00 16ビットのUsage値 0x00EA。Consumer PageではVolume Decrement
00 00 Consumer Controlの押下なし

SECTION 04

HIDがPCに届くまで

今回読んだコードの範囲

USBホストがReportを解釈するまで

まとめ:HIDデバイスからUSBホストへ入力が伝わるまで

  1. ロータリーエンコーダー側で入力を発生させる
  2. マイコン側でTinyGoのHID実装がConsumer Control Reportを生成してUSBへ送る
  3. PC側でHIDドライバがReportを解釈する
  4. PC側でOSが音量操作として処理する

ご清聴いただき、
ありがとうございました

Rinrin — @rin2yh

参考文献

付録

Report Descriptorとは

Reportの形式と各フィールドの意味をUSBホストへ伝えるデータ構造。

  • 複数のItemを組み合わせて記述する
    • Item: Report Descriptor内に並ぶ、Usage PageやReport Sizeなどの設定項目
  • Usage PageやUsageでデータの用途を示す
  • Report SizeやReport Countでフィールドの大きさ・個数を示す

USBホストはReport Descriptorに記述された形式に従って、Reportを解釈する。

USB-IF: Device Class Definition for HID 1.11

Report DescriptorのItem

Item 役割
Usage Page Usageの分類を選ぶ(例:Consumer)
Usage フィールドやCollectionの用途を示す
Report Size 1フィールドあたりのビット数
Report Count フィールドの個数
Input デバイスからUSBホストへ送るデータを宣言

値の範囲を扱うItemとしてLogical Minimum / Maximumなどもある。

little-endianとは

複数バイトで1つの値を表すときに、下位バイトから順に並べる方式。(逆はbig-endian)

  • 16bit値 0x1234
  • little-endian: 34 12
  • big-endian: 12 34

KeyMediaVolumeInc の値は 0xE4E9。上位の 0xE4 はTinyGoが付けた「Consumer Controlのキー」という目印で、下位の 0xE9 はHIDで決められた Volume Increment の値。

Down() は上位バイトを見てキーの種類を判定する。0xE4〜0xE7 なら Consumer Control なので downCon() に渡す。

c & 0x03FF で目印を外し、0xE9 だけを kb.con(押下中の Consumer Control キーを保持する配列)に入れる。

kb.con[0] の値を下位バイト・上位バイトに分け、Report ID 03 と合わせて3バイトのReportを作る。

Reportのデータ構造: Report Descriptorは付録に入れています

scriptはswiftで実装 by codex

Consumer Page(Usage Page `0x0C`)から、今回扱うUsageを抜粋。

RTCとあるが、抜粋しただけなのと今回は見なくて良いので気にしないでください。RTC: Re-trigger Control, 「値が 1 の間、イベント完了後に再度イベントを発生させる」タイプの Control

HIDではReport内の複数バイトの値をlittle-endian(下位バイトが先)で並べる(HID 1.11 §5.8)。

TinyGoも b[1] に下位、b[2] に上位を入れているので、E9 00 は 0x00E9 と読む。

TinyGoのHID実装で、Media Keyの分岐からConsumer Control Reportの送信までのコードを読んだ。

USBホストがReportを受け取ったあと、HIDドライバがHIDの入力として解釈する。

その結果がOS側の入力処理に渡されて、最終的にVolume Incrementとして音量変更に反映される。

OS内部の具体的な実装はOSごとに異なるが、HID Usageを音量操作として扱う。