🐱

Picoシリーズで作るUSB接続テンキーパッド~USB HIDの実装

に公開

このアーティクルは2025年3月をもって終了したエンジニア向け情報サイトfabcrossに寄稿し掲載された初心者向けの記事をfabcrossの許可を得て再掲しています


Picoシリーズを使ったUSB接続のテンキーパッドを取り上げる最後となる今回は、PicoシリーズにUSB HIDキーボードを実装する方法を紹介していきます。

ちなみに、キーボード自作の世界では、QMK Firmwareなどオープンソースのキーボード用ファームウェアが盛んに利用されています。なのでHIDキーボードの実装法を知らなくてもキーボードの自作はできます。しかし、HIDキーボードの実装方法を知っておくことで、さまざまな自作機器にキーボードの機能をもたせることが可能になります。たとえば、ロータリーエンコーダーにキーボード入力の機能をもたせるなどということも可能になるので知っておいて損はないでしょう。また、キーボードは割と楽に実装できますから、USBデバイス自作の入門にも最適です。

USBデバイスを自力で作成しようとするのであれば、多少なりともUSBデバイスについての知識が必要になります。ただ、USB規格は非常に規模が大きく、すべてを把握するとなると一筋縄では行きません。本腰を入れて説明しようとすれば分厚い本1冊にもなりかねないので、本稿ではHIDキーボードを実装するために知っておきた最小限の説明に留めることにします。

そのうえで、Picoシリーズの純正C SDKでサポートされているマイコン向けのUSBスタックTinyUSBを用いたHIDキーボードの実装を紹介しましょう。

なお、前回に取り上げている通り本稿で実装したUSBキーパッドの全ソースコードを筆者のリポジトリに公開しています。ソースコードを参照しながら読み進めてください。

USBデバイスの基礎知識

USBの最初の規格であるUSB 1.0が登場したのは1996年で、そろそろ30年近くが経とうとしている歴史と伝統のある規格です。もはや死語になりつつあるプラグアンドプレイ……PCに接続すればすぐに使えるデバイスを実現した初期の仕様のひとつです。現在はつなげば使えるデバイスなど当たり前ですが、当時は割と画期的でした。

ここで作ろうとしている「USB HIDキーボード」は、USBに定義されているUSB Device Classの中でも、もっとも広く普及しているHuman Interface Device(HID)Classに仕様が含まれます。Device Classというのは、業界団体USB-IF(USB Implementors Forum)が、USB仕様とともにプロトコルなどを定義しているデバイスです。標準規格なので、Device Classとして実装されているUSB機器ならば、メーカーを問わず同じドライバで利用できるのがユーザーにとっての利点です。現在ではキーボードやマウスを始めオーディオやプリンタ、映像入力機器、USB接続ディスプレイといった幅広いデバイスがDevice Classとして定義されています。

Device Classのカバー範囲はUSBのバージョンとともに増えていますが、キーボードを含むHID ClassはDevice Classの概念が提案されたUSB 1.0仕様書に含まれていた原初のDevice Classのひとつでした。つまり、そろそろ30年近くが経とうとしている、これまた歴史と伝統のあるDevice Classというわけですね[1]

USBのディスクリプタ

USBの特徴はプラグランドプレイです。USBデバイスは、USBバスに接続されるとホストにUSBディスクリプタと呼ばれる一連の情報を送りつけます。ディスクリプタでデバイスが何でありどのような機能を持っているのかを、ホストに通知するくらいの理解でいいでしょう。これでプラグアンドプレイが実現されています。

接続時にホストに通知するディスクリプタに含まれる主な情報、主としてキーボードを含むHIDがホストに送るディスクリプタを表1にまとめておきます。

表1 : USBのディスクリプタ

名称 内容
デバイスディスクリプタ USBバージョン、Vendor ID/Product IDなどデバイス全体の情報
コンフィグレーションディスクリプタ 消費電力、インタフェースの数などの設定情報
インタフェースディスクリプタ USBデバイスに含まれるインタフェースのClassやSub Class、プロトコルなど
エンドポイントディスクリプタ エンドポイントの転送タイプ、最大パケットサイズ、ポーリング間隔など
ストリングディスクリプタ メーカー名、デバイス名、シリアル番号など製品の文字情報

よく知られているのは、デバイスを識別するVendor IDとProduct IDでしょう。Vendor IDは製造元がUSB-IFから取得する16bitの固有IDで、Product IDはその製造元が製品に与える16bitの固有IDです。USB機器は、Vendor IDとProduct IDの組み合わせをUSB-IFに登録しなければならないとされます。ただ、正式にVendor IDを取得するには法人格が必要で、さらに6000ドルの料金が必要ですから、アマチュアが取得するのは現実的ではありません。

一つの方法として、USB-IFがプロトタイプ向けに予約しているVendor ID=0x6666を使う方法が考えられます。ただ、0x6666もメーカー向けに予約されており、いくつかのProduct IDが実製品に使われているようです。アマチュアが自由に使えるVendor IDではないことに注意が必要でしょう。

あまり褒められた方法ではないものの、取得されていない適当なVendor IDを使ってしまうこともよく行われています。個人的に使うUSBデバイスであれば、とくに問題はありません[2]。Picoシリーズが採用するUSBスタックTinyUSBのサンプルプログラムや、その他の自作例ではVendor IDとして0xCafeを使う例が多いようです。取得されておらず、おしゃれですからね。

また、Product IDはデバイスの種類ごとに特定のビットを変える方法で衝突を避ける手法があります。

その他、表1ではエンドポイントという用語が出てきますが、これはUSBにおけるデータの仮想的な出入り口のことです。エンドポイントにはデータ転送の種類によって、制御情報を送るコントロールエンドポイントやバルクエンドポイント、インタラプトエンドポイントなどいくつかの種類があります。コントロールエンドポイントはすべてのUSBデバイスが持つエンドポイントです。

HIDでは非同期に小さなデータをやり取りするインタラプトエンドポイントを使ってデータのやり取りが行われます。キーボードやマウスといったHIDデバイスは、人間が操作するので、データのやり取りがいつ発生するか予測できません。そのような種類のデータのやり取りにインタラプトエンドポイントを使います。

USB接続の独自デバイスを自作するときにはエンドポイントに関する知識が必要になりますが、TinyUSBでHIDを実装するのであれば、エンドポイントに関する具体的な知識は必要ないでしょう。そういうものがあるという程度で十分です。あとでも触れますが、TinyUSBは非常によくできたUSBスタックで、USBのややこしい詳細を抽象化してくれます。

とはいえ、USBデバイスを自作するのなら、これらのディスクリプタを用意しなければりません。HIDではさらにHIDディスクリプタやレポートディスクリプタといったHIDで定義されているディスクリプタも必要です。

そう聞くだけで気が遠くなるかもしれませんが、幸いなことにPicoシリーズが採用しているUSBスタックであるTinyUSBには豊富なサンプルがあり、Device Classの実装ならサンプルのディスクリプタを流用できます。自分でゼロからディスクリプタを書く必要はないので安心してください。

HIDキーボード

ここで実装しようとしているHIDキーボードは、HIDという大きな括りの中に含まれる一つのデバイスです。HIDにはキーボードの他にマウスやゲームパッドなど人間とやり取りするデバイスが含まれます。まず大きくHIDの仕様があり、その中にキーボードの仕様があるという恰好で、仕様書はとてもわかりにくいものになっています。

そこで、ここではキーボードに焦点を当てて端折った説明を行うことにします。その分だけ用語などの正確性が犠牲になっていることを押さえておいてください。

HIDは、レポートと呼ばれる単位でデータをホストとやり取りします。HIDはレポートディスクリプタという、レポートの構造を記した一種のデータをホストに送信してレポートの送受信を開始します。

レポート本体に含まれるデータはおもにUsage PageとUsage IDという2つの値です。Usage PageはHIDデバイスの種類……マウスであるとかキーボードであるといった種類を示し、Usage IDが情報です。キーボードの場合、Usage Pageが0x07、Usage IDはキースキャンコード[3]です。Usage IDはまとめて送信することができます。

ここでいうキースキャンコードとは、キーを示す符号なしの1バイトの数値のことです。USB HIDキーボードがホストに送るキースキャンコードはHIDの仕様に定義されており、TinyUSBではhid.hに定数として定義されています。抜粋しておきましょう。

//--------------------------------------------------------------------+
// HID KEYCODE
//--------------------------------------------------------------------+
#define HID_KEY_NONE                        0x00
#define HID_KEY_A                           0x04
#define HID_KEY_B                           0x05
#define HID_KEY_C                           0x06
……

符号なし1バイトですから、キーボードに搭載できるキーの数は最大256個になりますが、実際にはキースキャンコード0x00が「キーが押されていない」状態に割り当てられ、0x01~0x03までがエラーに割り当てられているほか、いくつか予約されているキースキャンコードがあるので、総計は256個に届きません。キースキャンコードは、Microsoftなど関係各社の提案により現時点で200ほどのキーが割り当て済です。

キーボードが送受信するレポートは基本的に次の2つだけです。

キーボードレポート(デバイス→ホスト)

キーボードレポートは、現在のキーの状態をホスト送信するレポートで、いわばキーボードの主要なレポートです。送信するタイミングは、キーボードのキーに変化があったとき(キーがオンになったりオフになったとき)と、ホストからコントロールリクエストGET_REPORTを受け取ったときです[4]

キーボードレポートの構造をTinyUSBのヘッダファイル(hid.h)の定義から抜粋しておきます。

typedef struct TU_ATTR_PACKED
{
  uint8_t modifier;   /* 修飾キー(Shift、Ctrlなど)の状態 */
  uint8_t reserved;   /* 予約(常に0) */
  uint8_t keycode[6]; /* いま押されているキースキャンコード */
} hid_keyboard_report_t;

先頭1バイトのmodifierは、修飾キーの状態を示します。各ビットが個々の修飾キーの状態、つまりオンなら1、0ならオフを示しています。定義をTinyUSBのヘッダファイルから抜粋しておきましょう。

/// Keyboard modifier codes bitmap
typedef enum
{
  KEYBOARD_MODIFIER_LEFTCTRL   = TU_BIT(0), ///< Left Control
  KEYBOARD_MODIFIER_LEFTSHIFT  = TU_BIT(1), ///< Left Shift
  KEYBOARD_MODIFIER_LEFTALT    = TU_BIT(2), ///< Left Alt
  KEYBOARD_MODIFIER_LEFTGUI    = TU_BIT(3), ///< Left Window
  KEYBOARD_MODIFIER_RIGHTCTRL  = TU_BIT(4), ///< Right Control
  KEYBOARD_MODIFIER_RIGHTSHIFT = TU_BIT(5), ///< Right Shift
  KEYBOARD_MODIFIER_RIGHTALT   = TU_BIT(6), ///< Right Alt
  KEYBOARD_MODIFIER_RIGHTGUI   = TU_BIT(7)  ///< Right Window
}hid_keyboard_modifier_bm_t;

このように左右のCtrl、Shift、Alt、Windowsキーがビットフィールドとして定義されています。

余談になりますが、2024年初頭にMicrosoftが「Copilotキー」の新設を提案したニュースを目にした人がいるかもしれません。前出のようにmodifierの8ビットは使い切っているので、Copilotキーは修飾キーではないわけです。実際にはキーコードも割り当てられないそうで、Copilotキーが押されたら左Shift+Windows+F23(キースキャンコード0x72)のキーボードレポートを送信するとのことです。つまりCopilotキーはHIDの仕様で定義されたキーではないわけですね。

modifierの次の1バイトは予約で常にゼロにします。レポートのサイズを8バイトに合わせるための隙間埋め(パッディング)と考えていいでしょう。

末尾の6バイトが現在のキーの状態を格納したキーの配列です。配列にはオンになっているキースキャンコードを格納します。USB HIDキーボードの仕様では、キーボードレポートの6バイトに含まれないキーはすべてオフです。レポートの6バイトがすべて0ならば前述のように全キーがオフという意味になります。

配列に含まれる順番は意味を持ちません。つまり、{4,0,0,0,0,0}をキーボードが送信したあと、次に{0,4,0,0,0,0}を送っても[A]キーがオンの状態に変わりはないということです。

なお、フィールドが6バイトですからUSB HIDキーボードで同時にオンにできるキーの数は、最大6キーです。Nキーロールオーバーを謳う高級キーボードやゲーマー向けキーボードでは、6キーをはるかに超える同時オンをサポートする製品が多いですが、この種のキーボードはHID標準から外れた実装を行っています。TinyUSBのHIDスタックは標準仕様に基づいた実装なので、最大6キーまでしかオンにできません。

ちなみに、仕様では6キー以上のキーがオンになっているときにErrorRollOver(キースキャンコード0x01)を含めるとなっています。ですが、6キー以上のオンを単純に無視しても結果は変わらないようなので、無理にErrorRollOverを送信する必要はないでしょう。もっとも、本稿で製作する4×4キーパッドで6キー以上が同時に押されること考慮しなくてもいいかもしれません。

インジケータの制御情報(ホスト→デバイス)

キーボードがホストから受け取る唯一のレポートが、インジケータを制御する情報です。コントロールリクエストSET_REPORTによりホストからキーボードに送られてきます。レポート本体は1バイトで、TinyUSBにおける定義は次のとおりです。

typedef enum
{
  KEYBOARD_LED_NUMLOCK    = TU_BIT(0), ///< Num Lock LED
  KEYBOARD_LED_CAPSLOCK   = TU_BIT(1), ///< Caps Lock LED
  KEYBOARD_LED_SCROLLLOCK = TU_BIT(2), ///< Scroll Lock LED
  KEYBOARD_LED_COMPOSE    = TU_BIT(3), ///< Composition Mode
  KEYBOARD_LED_KANA       = TU_BIT(4) ///< Kana mode
}hid_keyboard_led_bm_t;

このようにインジケータをオン・オフする情報がビットフィールドとして定義されています。Num Lock、Caps Lock、Scroll Lockはおなじみでしょう。Composition Modeは、かつてワークステーションで一世を風靡した旧Sun Microsystemsが定義した「Composeキー」で切り替わるモードのインジケータです。UNIX系OS向けですが、現在では事実上利用されていません。最後の「Kana Mode」は、日本語JISキー配列におけるかな入力モードです。

なお、キーボード側は、ホストから受け取った情報に基づいてインジケータの表示を変えるだけです。たとえば、Caps LockやNum Lockがオンになったからといってキーの制御を変える必要はないことに注意してください。モードに応じた入力動作の変更は、ホストOS側の仕事というのがHIDキーボードの基本的な考え方になっています。少し話がそれますが、キーを押し続けると発生するリピート入力も同様で、キーボード側でリピートする必要はなく、下手にキーボード側でリピートすると異常動作の引き金になりかねません。リピート入力を行うのはOS側の役目です。

TinyUSBでHIDキーボード

駆け足でUSB HIDキーボードの概要を説明してきました。TinyUSBを使ってHIDキーボードを実装するだけなら以上の知識でなんとかなります。

Picoシリーズが採用しているUSBスタックであるTinyUSBは、マイコン向けでありながら非常に高レベルなライブラリで、USBデバイスやUSBホストを、低レベルなハードウェアを意識することなく実装できます。ただ、公式ドキュメントはごく簡単な導入編しかなく、あとは自身でサンプルや本体のソースを見てなんとかするしかないという、とても硬派なライブラリですからハードルは高めといっていいでしょう。

幸い豊富なサンプルが用意されているほか、Picoシリーズ以外にArduinoやESP32シリーズ、STM32シリーズなど他のマイコンで利用されているので、実装例が豊富に見つかります。それらを見ながら開発すればなんとかなりそうです。

HIDの公式サンプルとしては、HIDコンポジットデバイス(複数の機能を持つHID複合デバイス)の例があり、Picoシリーズの公式サンプルにも含まれています。このサンプルはマウス、ゲームパッド、キーボード、コンシューマー制御デバイス[5]の機能を併せ持つデバイスの例です。あとで触れますが、このサンプルを叩き台にすれば何とかなります。

TinyUSBの基本

PicoシリーズでUSBを利用する場合、CMakeLists.txtのtarget_include_directoriesコマンドにtinyusb_deviceまたはtinyusb_hosttinyusb_boardの追加が必要になります。

target_link_libraries(
    .....
    tinyusb_device
    tinyusb_board
    ...
)

tiny_deviceはUSBデバイスを作成するためのライブラリで、USBホストを作成するなら代わりにtinyusb_host追加します。tinyusb_boardはマイコンボード依存部のライブラリで、デバイス、ホストともに必須です。本稿ではデバイスに絞って話を進めていきます。

TinyUSBの初期化は定型で、次のようにな記述します。

    board_init();                   // マイコンボード依存の初期化
    tud_init(BOARD_TUD_RHPORT);     // TinyUSB本体の初期化
    if (board_init_after_tusb) {    // マイコン依存の初期化後の処理
        board_init_after_tusb();
    }

初期化は2段階で、まずマイコンボードに依存するboard_init()を呼び出したあと、TinyUSB本体の初期化であるtuid_init()を呼び出します。tuid_init()の引数はマイコン依存で、複数のUSBポートを備えるマイコンで2番目のポート以降を使うのであれば0以外の指定が必要になるようです。PicoはUSBポートが1基しかないので、0が定義されているデフォルトのBOARD_TUD_RHPORTを指定します。

tud_init()はソースコードツリーに用意するtusb_config.hの内容に従って初期化を行います。HIDのようなデバイスクラスを実装する場合、内部のクラスドライバの初期化もここで行われます。したがって、tusb_config.hが必須ですが、サンプルの同ファイルを必要に応じて変更して流用すれば楽です。

board_init_after_tusb()もマイコン依存で、TinyUSB初期化後の処理が必要なボードのみ実装されています。

初期化を終えたら、メインループ内でtud_task()関数を定期的に呼び出します。

    while(true) {
        tud_task();     // TinyUSBのジョブを実行
        // ここでなにかする
    }

tud_task()はTinyUSBの本体に当たる関数で、USBインタフェースに生じているイベントに応じて適切な処理を行います。また、イベントによってはアプリケーション本体に記述したコールバック関数を呼び出します。TinyUSBを使用するアプリケーションは、コールバック関数に適切な応答を記述する形になります。

なお、tud_task()を呼び出す間隔が空くと、USBのイベント処理が間に合わなくなり、USBホスト側でデバイスの応答がないと判断されエラーになってしまいます。

tud_task()をどのくらいの頻度で呼び出すべきかは、扱う転送モードに依存するので一概に言えません。ただ、USB 1.1 Full-Speed(12Mbps)のデータフレームが1ミリ秒間隔であるため、tud_task()は1ミリ秒以下の頻度で呼び出すことが推奨されているようです。メインループは、tud_task()以外の処理が1ミリ秒以内になるよう記述する必要があると気に留めておくといいでしょう。

HIDに実装するコールバック関数

tud_task()の内部処理で検出したUSBのイベントに応じて、ユーザープログラム中のコールバック関数が呼び出されます。コールバック関数は末尾に_cbという関数名がつけられています。

実装すべきコールバック関数は、作成するUSBデバイスによって変わりますが、HIDであれば次のコールバック関数を実装するのが一般的な形のようです。

uint16_t tud_hid_get_report_cb(uint8_t instance, uint8_t report_id, hid_report_type_t report_type, uint8_t* buffer, uint16_t reqlen);

コントロールリクエストGET_REPORTによって呼び出されるコールバック関数です。すでに触れている通り、キーボードではキー押下状態にかかわらずキーボードレポートを送る必要があります。bufferにキーボードレポートを格納し、キーボードレポートのサイズ(sizeof(hid_keyboard_report_t))を返します。

report_idHID_REPORT_TYPE_INPUT(キーボード→ホスト)ではないとか、reqlensizeof(hid_keyboard_report_t)未満という場合にどうしたらいいのか悩ましいところですが、何か返さないといけない関数なので0を返しておけばいいでしょう。

void tud_hid_set_report_cb(uint8_t instance, uint8_t report_id, hid_report_type_t report_type, uint8_t const* buffer, uint16_t bufsize);

コントロールリクエストSET_REPORTによって呼び出されるコールバック関数です。すでに触れている通り、キーボードではインジケータの制御情報がbufferの先頭に格納されています。この関数では、インジケータの点灯をbuffer[0]に従って変更します。

void tud_hid_report_complete_cb(uint8_t instance, uint8_t const* report, uint16_t len);

レポートの送信が終わると呼び出され、必要に応じて次のレポートを送るためのコールバック関数です。キーボードではとくに何もする必要はなく、空の関数で構いません。

void tud_mount_cb();
void tud_unmount_cb();

USBデバイスがホストに接続されて初期化が完了するとtud_mount_cb()が呼び出され、USBから切り離されるとtud_unmount_cb()が呼び出されます。必要に応じて関数内を記述しますが、何もしなくても特に問題はありません。

void tud_suspend_cb(bool remote_wakeup_en);
void tud_resume_cb()

USBバスが休止状態になるとtud_suspend_cb()が呼び出され、復帰するとtud_resume_cb()が呼び出されます。remote_wakeup_entrueなら、tud_remote_wakeup()で復帰させることができるようです。

休止状態に入ったなら7ミリ秒以内に平均消費電流を2.5mA未満に抑える必要があると記されていますが、とくに何もしていない実装が多いので、空でも構わないものと思われます。

USBキーパッドの実装

では、最後に本稿で作成したUSBキーパッドの実装をざっくり見ておくことにしましょう。USBキーパッドは、公式サンプルのdev_usb_compositeを元に実装しています。

tusb_config.h

tusb_config.hは、サンプルプログラムを変更なしに利用できるので、そのまま流用しています。USBデバイスクラスでは、tusb_config.hの次の部分を変更して作成するクラスドライバを有効化しますが、dev_usb_compositeはHIDのサンプルなので変更する必要はありません。

//------------- CLASS -------------//
#define CFG_TUD_HID               1   // HIDを利用する
#define CFG_TUD_CDC               0
#define CFG_TUD_MSC               0
#define CFG_TUD_MIDI              0
#define CFG_TUD_VENDOR            0

ディスクリプタを返すコールバック関数

TinyUSBでは、ディスクリプタを返すコールバック関数を用意する必要があります。先述のようにディスクリプタは数が多く自分で用意するのは面倒なので、dev_usb_compositeのディスクリプタを流用します。ディスクリプタはusb_descriptors.husb_descriptors.cに実装されています。

変更を加えるのはusb_descriptors.cの中でHIDリポートディスクリプタを定義している次の部分で、キーボード以外のリポートをコメントアウトします。

uint8_t const desc_hid_report[] =
{
  TUD_HID_REPORT_DESC_KEYBOARD( HID_REPORT_ID(REPORT_ID_KEYBOARD         )),
//  TUD_HID_REPORT_DESC_MOUSE   ( HID_REPORT_ID(REPORT_ID_MOUSE            )),
//  TUD_HID_REPORT_DESC_CONSUMER( HID_REPORT_ID(REPORT_ID_CONSUMER_CONTROL )),
//  TUD_HID_REPORT_DESC_GAMEPAD ( HID_REPORT_ID(REPORT_ID_GAMEPAD          ))
};

その他はお好みに応じて変えればいいでしょう。Vendor IDとProduct IDはdesc_deviceに定義されています。また、ベンダー名、製品名といった情報はストリングディスクリプタstring_desc_arrに定義されています。オリジナルに変えてもいいでしょう。

メインループ

USBキーパッドのメインはusb_keypad1.cです。また、キーパッドをPIOでキースキャンして得たキーコードと、HIDのキースキャンコードの変換テーブルkeymapkeymap.hに記述しています。keymap.hを書き換えればキーパッドの16個のキーに任意のキーを割り当てることが可能です。

現状では、下記のように10キー風のキー割当を行っています。ちなみに、10キーパッド上の数字キーを示すキースキャンコードHID_KEY_KEYPAD_1~が定義されていますが、このキースキャンコードで数字が入力できるのはPC側でNum Lockモードがオンになっているときだけです。一般的に、Num Lockキーを押さないと切り替わらない[6]ですから、本サンプルでは10キーパッド上の数字キーではなくメインキー側のキースキャンコードを割り当てています。

// keymap.hのキー割当
const uint8_t keymap[16] = {
    HID_KEY_7,              // S1
    HID_KEY_8,              // S2
    HID_KEY_9,              // S3
    HID_KEY_KEYPAD_ADD,     // S4

    HID_KEY_4,              // S5
    HID_KEY_5,              // S6
    HID_KEY_6,              // S7
    HID_KEY_KEYPAD_SUBTRACT,// S8

    HID_KEY_1,              // S9
    HID_KEY_2,              // S10
    HID_KEY_3,              // S11
    HID_KEY_KEYPAD_MULTIPLY,// S12

    HID_KEY_0,              // S13
    HID_KEY_PERIOD,         // S14
    HID_KEY_KEYPAD_DIVIDE,  // S15
    HID_KEY_KEYPAD_ENTER,   // S16
};

キーボードレポートに含まれるkeycodeフィールドに対応するkey_report配列をグローバルに用意しています。メインループでは、前回のPIOキースキャンで得たキーをget_key()で取得して、先のkeymapテーブルでキースキャンコードに変換し、キーボードレポートを送信するユーティリティ関数tud_hid_keyboard_report()で送信するという形です。

tud_hid_keyboard_report()の第1引数はレポートディスクリプタで定義したID、第2引数は修飾キー(hid_keyboard_modifier_bm_t)、第3引数がキーボードレポートに含めるキースキャンコードの6バイトの配列です。

ここまでの説明でわかると思いますが、ホストOSにキースキャンコードを送ると、そのキーがオンになり、次にそのキーのキースキャンコードが含まれないキーボードレポートが送られてくるまでオンの状態が保持されます。したがって、キーがオフになったとき、キーボードレポートから確実にそのキーのキースキャンコードを取り除かないと、キーが押されっぱなしになることに注意が必要です。

uint8_t key_report[KEYBOARD_REPORT_COUNT] = {0,0,0,0,0,0};

...

int main()
{
  ... いろいろ初期化

  while (true) {
      tud_task();     // TinyUSB定期的に呼び出す必要がある

      key_t key = get_key();
      if(tud_mounted()) {     // USBデバイスとしてマウントされていれば
          if(key.state != KEYPAD_INVALID) {  // キー状態に変化あり
              uint8_t scancode = keymap[key.code];
              if(tud_suspended())     // サスペンド状態なら起こす
                  tud_remote_wakeup();
                
              if(key.state == KEYPAD_PUSH) {    // キーが押された
                  for(int i = 0; i < KEYBOARD_REPORT_COUNT; i++) {
                      if(key_report[i] == 0) {
                          key_report[i] = scancode;
                          break;
                      }
                  }
              }
              else {                     // キーオフ
                  for(int i = 0; i < KEYBOARD_REPORT_COUNT; i++) {
                      if(key_report[i] == scancode)
                          key_report[i] = 0;
                  }
              }
              // キーボードレポートをOLEDに表示する
              display_report();
              // 空きがあればキーボードレポート送信
              if(tud_hid_ready())
                  tud_hid_keyboard_report(REPORT_ID_KEYBOARD,0, key_report);
          }
      }
  }
}

コールバック関数

コールバック関数tud_hid_set_report_cb()はレポートの1バイト目に従ってOLEDインジケータの表示を変えているだけなので、実際のコードを見てもらえばいいでしょう。

悩ましいのはtud_hid_get_report_cb()で、すでに触れている通り筆者が調べている限り、このコールバックが呼び出される状況を観察していません。なので、実装が正しいかが確認できないですが、次のように記述しています。

uint16_t tud_hid_get_report_cb(uint8_t instance, uint8_t report_id, hid_report_type_t report_type, uint8_t* buffer, uint16_t reqlen)
{
    (void) instance;
    (void) report_id;
    (void) report_type;

    if(reqlen < sizeof(hid_keyboard_report_t) ) {
        buffer[0] = 0;
        return 1;       // とにかく何か返さないと駄目
    }
    else {
        hid_keyboard_report_t   report;
        report.modifier = 0;        // 修飾キー情報
        report.reserved = 0;
        memcpy(report.keycode, key_report, KEYBOARD_REPORT_COUNT );
        memcpy(buffer, &report, sizeof(hid_keyboard_report_t));

        return sizeof(hid_keyboard_report_t);
    }
}

キーボードなので、GET_REPORTはキーボードレポートの要求しか来ないという前提で、hid_keyboard_report_tを作成しbufferにコピーしてhid_keyboard_report_tのサイズを返す形です。これで問題ないはずですが、実際に呼び出されたらどうなるかは未確認ということに注意してください。

実用キーパッドに仕立てよう

以上、ざっくりとUSBキーパッドの実装を紹介してきました。制作したキーパッドはkeymap配列を書き換えるだけで、16個のキーに任意のキーを割り当てることができます。なので、読者の工夫で実用的に使えるキーパッドに仕立てることができるでしょう。

たとえば、アプリでよく使うショートカットを16のキーに割り当てるのなら、CtrlやAltキーも含める必要があるかもしれません。ud_hid_keyboard_report()でレポートを送るときに、第2引数にhid_keyboard_modifier_bm_tを加えればいいだけですから、keymap配列を2次元にするだけで割と簡単に対応が可能です。

1つのキーに複数キーの同時押しを割り当てるのは少し工夫がいりそうですが、これもそう難しいことではありません。読者自身で色々と改造して楽しんでください。

脚注
  1. USB 1.0の段階では、まだDevice Classが概念定義にとどまっていたこともあり、本格的にDevice Classの実装や製品化がスタートするのは1998年に公開されたUSB 1.1仕様以降でした。 ↩︎

  2. USBデバイスを市販したり頒布する場合IDの衝突が起こるので原理的に問題が発生します。 ↩︎

  3. 本来ならキーコードと呼ぶのが正しいかもしれませんが、このシリーズでは4×4キーパッドのキースキャンデータをキーコードと呼んだりしているので、それと区別するためにPCの伝統に則ったキースキャンコードという名称を使います。ややこしいので注意してください。 ↩︎

  4. ただし、筆者がWindowsでテストしている限りGET_REPORTを観測していません。TinyUSBのHID CompositeサンプルもGET_REPORTを実装していないので事実上必要ないのかもしれません。とはいえ、USB HIDの仕様ではGET_REPORTに対してキーボードレポートを送信せよとなっています。 ↩︎

  5. HIDのプロトコルを使った任意のデバイスのこと。 ↩︎

  6. UEFI(BIOS)設定で起動時にNum LockモードをオンにできるPCもあるので一概には言えません。 ↩︎

Discussion