Raspberry Pi Picoを始めよう! 週末工作でステップアップ
ラズパイPicoで作るUSB接続テンキーパッド——USB HIDキーボードの実装

Raspberry Pi Picoシリーズ(以下、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に接続すればすぐに使えるデバイスを実現した初期の仕様の1つです。現在はつなげば使えるデバイスなど当たり前ですが、当時は割と画期的でした。
ここで作ろうとしている「USB HIDキーボード」は、USBに定義されているUSB Device Classの中でも、もっとも広く普及しているHuman Interface Device(HID)に仕様が含まれます。
Device Classというのは、業界団体USB-IF(USB Implementers Forum)が、USB仕様とともにプロトコルなどを定義しているデバイスです。標準規格なので、Device Classとして実装されているUSB機器ならば、メーカーを問わず同じドライバで利用できるのがユーザーにとっての利点です。現在ではキーボードやマウスをはじめオーディオやプリンター、映像入力機器、USB接続ディスプレイといった幅広いデバイスがDevice Classとして定義されています。
Device Classのカバー範囲はUSBのバージョンとともに増えていますが、キーボードを含むHIDはDevice Classの概念が提案されたUSB 1.0仕様書に含まれていた原初のDevice Classの1つでした。つまり、そろそろ30年近くがたとうとしている、これまた歴史と伝統のあるDevice Classというわけですね※1。
※1 USB 1.0の段階では、まだDevice Classが概念定義にとどまっていたこともあり、本格的にDevice Classの実装や製品化がスタートするのは1998年に公開されたUSB 1.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ドル(約90万円)の料金が必要ですから、アマチュアが取得するのは現実的ではありません。一つの方法として、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の実装ならサンプルのディスクリプタを流用できます。自分でゼロからディスクリプタを書く必要はないので安心してください。
※2 USBデバイスを市販したり頒布したりする場合、IDの衝突が起こるので原理的に問題が発生します。
HIDキーボード
ここで実装しようとしているHIDキーボードは、HIDという大きなくくりの中に含まれる1つのデバイスです。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キー以上が同時に押されることは考慮しなくても良いかもしれません。
※3 HIDキーコードと呼ぶのが正しいですが、このシリーズでは4×4キーパッドのキースキャンデータをキーコードと呼んでいるので、それと区別するためにPCの伝統にのっとったキースキャンコードという名称を使います。本文の用語は正式なものではないので注意してください。
※4 ただし、筆者がWindowsでテストしている限りGET_REPORTを観測していません。TinyUSBのHID CompositeサンプルもGET_REPORTを実装していないので事実上必要ないのかもしれません。とはいえ、USB HIDの仕様ではGET_REPORTに対してキーボードレポートを送信せよとなっています。
インジケーターの制御情報(ホスト→デバイス)
キーボードがホストから受け取る唯一のレポートが、インジケーターを制御する情報です。コントロールリクエスト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の機能を併せ持つデバイスの例です。後で触れますが、このサンプルを叩き台にすれば楽ができます。
※5 HIDのプロトコルを使った任意のデバイスのこと。
TinyUSBの基本
PicoシリーズでUSBを利用する場合、CMakeLists.txtのtarget_include_directoriesコマンドにtinyusb_deviceまたはtinyusb_hostとtinyusb_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本体の初期化であるtud_init()を呼び出します。tud_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_idがHID_REPORT_TYPE_INPUT(キーボード→ホスト)ではないとか、reqlenがsizeof(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_enがtrueなら、tud_remote_wakeup()で復帰させられるようです。
休止状態に入ったなら7ミリ秒以内に平均消費電流を2.5mA未満に抑える必要があると記されていますが、特に何もしていない実装が多いので、空でも構わないものと思われます。
USBキーパッドの実装
では、最後に本稿で作製したUSBキーパッドの実装をざっくり見ておくことにしましょう。USBキーパッドは、公式サンプルのdev_usb_compositeを元に実装しています。
tusb_config.h
usb_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
1.3.2 ディスクリプタを返すコールバック関数
TinyUSBでは、ディスクリプタを返すコールバック関数を用意する必要があります。先述のようにディスクリプタは数が多く自分で用意するのは面倒なので、dev_usb_compositeのディスクリプタを流用します。ディスクリプタはusb_descriptors.hとusb_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のキースキャンコードの変換テーブルkeymapをkeymap.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); } } } }
※6 UEFI(BIOS)設定でPC起動時にNum LockモードをオンにできるPCもありますが、Windowsが起動時にオフに戻してしまうので起動時オフが標準的と思われます。
コールバック関数
コールバック関数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つのキーに複数キーの同時押しを割り当てるのは少し工夫がいりそうですが、これもそう難しいことではありません。読者自身で色々と改造して楽しんでください。