概要
SparkFun Pro Micro(コピー品)に小さなキーパッドを付けて小規模なHIDキーボードを作ってみたが、今度はふつうのUSBキーボードをつないでみた話。
ArduinoにUSBキーボードを接続するためにはUSBホスト機能を追加する必要がある。今回は、mini USB Host Shield を使ってみることにした。

この2つのボードを組み合わせて使うことでPro MicroにいろいろなUSB周辺装置を接続できるようになるのだが、話の流れとしてキーボードを接続することにした。Pro Micro自体をHIDキーボードとして振る舞わせることは既に出来ているから、Pro MicroにつないだキーボードをPCの入力装置として使うこともできるだろう。ただつなぐだけで、どういう意味があるのか?
mini USB Hostシールドのこと
今回使った製品は中国から取り寄せたもので、USDで$4~$5程度の価格だった。USB HostシールドにはArduinoシールドとして普通サイズのものや、Arduino Pro MiniやPro Microとほぼ同サイズのミニ版がある。詳細については開発元のCircuits@Homeのページが詳しい(ちょっと読みづらいけど)。pdf版の回路図はこちら。

miniUHSの構成など
今回購入した製品には、”HW-244 UHS mini v2.0″と書かれているが、本家の方の”UHS mini r1.1″と、抵抗値が違っていたりするものの中身は同じと考えてよさそうだ。ROMやフラッシュにプログラムが焼かれているわけではないので。
ボード中央に、MAXIM社のMAX3421Eが載っていて12MHzの発振器が接続されている。MAX3421Eは SPIインタフェース付き、USBペリフェラル/ホストコントローラ と称されていて、SPIホスト(今回はATmega32u4)とSPIで接続する。メーカーのページによれば、SPIホストからのコントロールによりUSB周辺機器用にもUSBホスト用にもなるとのこと。
動作電源(VCC)は+3.3Vなので、+5V動作のArduinoと組み合わせる際には電源だけでなく、SPIの各ラインについてもレベルシフトが必要になる。+3.3V動作のものと組み合わせるのが簡単だろう。
Arduinoとの接続には、SPIの4本(SCLK, MISO, MISO, SS) と割込み通知用の(INT)が必要になる(割り込みを使うかどうかはスケッチ次第)。miniUHSの外周にある20個のスルーホール端子はArduino PRO MINIと重ねて使うことができるよう配列されているが、Pro Microと重ねるときには若干の相違があるので注意が必要。
なお、miniUHS内で意味をもつ端子は、VCCとGNDを除けば6端子(SCLK, MISO, MOSI, SS, INT, RST) しかない。多くのスルーホールは、ボード内のどこにも配線されていない。
シルクの誤りに注意

今回入手したコピー品の裏面には、SPI用のシルクが印刷されているが誤りがあるので注意が必要だろう。上図下側の右からSS, CLK, MISO, MOSIと印刷されているが、この並びはPRO MINIやPro Micro のそれ(右から D10, MOSI, MISO, SCLK)と異なっている。
実際にはどうなのかと思ってパターンを追って導通チェックしてみたら以下のようになっていた(左側がシルク印刷、右側がMAX3421Eの端子)。
- SS-> MAX3421の#14 (/SS)
- SCLK -> MAX3421の#16 (MOSI)
- MISO -> MAX3421の#15 (MISO)
- MOSI -> MAX3421の#13 (SCLK)
つまり、シルクが間違いで実際はArduino PRO MINIやPro Microの端子並びと同じということ。なので、シルクに惑わされることなく、同じ位置の端子同士を接続すればよいだろう(SPI SSは、ArduinoのD10に接続される)。
ボードの小改造
+3.3V版Pro Microとの接続にあたり、以下のような改造を行った。miniUHSボードの改造についてはDEKOのアヤシいお部屋。の該当するページを参考にさせていただいた。
VBUSとRAW端子
miniUHSの回路図を見ても今回の製品の配線パターンを見ても、USB AコネクタのVBUSはVCCと直結されている。VCCは+3.3Vだけど大丈夫?と思った。今回は+5V近い電圧を与えたいので、+3.3V動作のPro MicroのRAW端子(元を正せば、PCやUSBハブが供給するVBUS) から電圧を得ることにした。ただ、miniUHSのRAW端子相当の外周スルーホールはどこにも接続されていないから自分で配線する必要がある。
RAWとVBUSを接続するならば、VCCとVBUS間のパターンをカットしておく必要がある。カットしておかないと、おそらくMAX3421Eが壊れます。
MAX_RSTとRST端子
いちおう、リセットボタンを押したらPro MicroもMAX3421Eもリセットしたい。しかしながら、miniUHSでMAX3421Eの/RESピンとつながっている端子は、Pro MicroではなぜかGNDになっている。PRO MINIではRST端子なのだが。そして、Pro MicroのRST端子に該当するminiUHSの端子のスルーホール(下図での (RST) )はどこにもつながっていないので、自分で配線してやる必要がある。
ただ、miniUHSとPro Microを重ね合わせて接続しない限り、RST-MAX_RST間のパターンカットの必要はない。ブレッドボードに並べるのならPro MicroのRSTとminiUHSのRSTをワイヤで接続すればよいだけ。
Pro Micro自体にはリセットボタンがないし、Pro Micro側もminiUHS側もRSTは入力端子である。なのでPro MicroのRSTとMAX_RSTをあえて接続する必要もないように思える。
注意すべき点は、MAX3421Eの仕様では電源投入後の動作状態では、MAX_RSTをHレベルに維持する必要があること。しかしながら、miniUHSのMAX_RST端子はVCCにプルアップされていないから、何も接続していない状態では状態が不定で正しく動作しなかった。
Pro Micro側のRSTはボード内でVCCにプルアップされているので両者を接続してやればよいし、あるいは、miniUHS内でMAX_RSTをVCCにプルアップする必要がある。
もしもVCC=+5VのPro MicroやPro MINIと(SPIのレベルシフタを介して)接続するならば、MAX_RSTはマイコン側と接続せず、+3.3Vにプルアップしておく必要がある。

まとめると、上図の斜線の2箇所でパターンをカットし、(RAW)-VBUSと(RST)-MAX_RSTを配線する。赤丸を付けている端子が、ボード内に接続相手がいるスルーホールを示している。
なお、PRO MINIにはRST端子が2つあるので、上のようなパッチをあてて下側のRSTをN.C.にしたminiUHSと組み合わせても動作に影響しないと思う。
※ PRO MINIやPro MicroのRAW端子の電圧は+5.25 V以下であること。
実際にやったらこんな具合になった。

配線面が薄いので簡単にカットできます。
当初は2つのミニボードを重ね合わせようと思っていたのだけど、また何かパッチが必要になると困ると思ってふつうのブレッドボードに並べることにした。

ヘナヘナなワイヤで接続している両ボードの端子位置は一致している。つまり重ね合わせて使うこともできる。そのときは写真と同じ向きでUSBコネクタが両側にでる形状となる。このボードはしっかりピンをはんだ付けしてしまったので、別途用意することになるが。いちおう、配線図を載せておきます。

USB Host Shield Library 2.0 を使う
動作確認のため、Circuits@Home がLGPLではなくGPL v2ライセンスで公開しているライブラリを使うことにした。このライブラリを使ったプログラムや製品は、ソースコードを公開すること(入手可能とすること)が義務付けられている。
ライブラリの導入
Arduino IDEのスケッチ/ライブラリをインクルードで開くメニューの先頭からライブラリを管理を選択し、ライブラリマネージャを開く。検索フィールドに、”USB Host” と入力すると、”USB Host Shield Library 2.0 by Oleg Mazurov…. “といった項目が表示されるので、最新バージョン(1.3.2)を選択してインストールした。
もしかしたら、以前にインストールしたことがあってこうなったのかもしれない。ライブラリマネージャに表示されない場合はgithubからzipをダウンロードしてArduinoライブラリとして追加すればよいだろう。
動作確認にはexamplesのUSBHIDBootKbd.inoを使う
ハードウェアやライブラリの動作を確認するため、ライブラリに付属のスケッチ(examples\HID\USBHIDBootKbd\USBHIDBootKbd.ino) をビルドして書き込んだところあっさり動いた。このスケッチは、Arduino IDEのファイル/スケッチ例の中から選ぶことができる。
ビルドしてPro Microに書き込むと、miniUHSの下流につないだキーボードのキーを押したり離したりしたとき、対象キーのキーコード(Usage ID) がシリアルモニタに表示される。

上記のようにキー操作内容がシリアルモニタに表示されてくる。ShiftやCtrlのような修飾キー単独での押下も表示されるし、修飾キーを押しながら文字キーを押したときもその内容が表示される。
文字キーが押下されると、void KbdRptParser::OnKeyDown(uint8_t mod, uint8_t key) が呼ばれ、リリースされると void KbdRptParser::OnKeyUp(uint8_t mod, uint8_t key); が呼ばれる。いずれも、keyにはキーコード(HID Usage ID)が、modにはその時点の修飾キーの情報が入っている。
なお、これらのメソッドは修飾キー単独での押下時には呼ばれない。たとえば Shiftキーが単独で押されたときの処理が必要ならば、void KbdRptParser::OnControlKeysChanged(uint8_t before, uint8_t after); を使う必要がある。beforeとafterには、ライブラリ側で維持している直前の修飾キー情報と、現在の修飾キー情報が入っている。文字キーの押下を伴わない場合も、この2つの値に相違があれば呼び出しが行われるようだ。
miniUHSの下流につないだキーボードのLEDは、ScrLockやNumLockの押下に応じて点いたり消えたりするので、ライブラリがLED用のアウトプットレポートを送っていると思われる。今回はキーボードLEDを利用しないことにした。
miniUHSにUSBキーボードをつなぎ、PCのキーボードとして使うスケッチ
上記の作例を参考に、miniUHSのUSBポートに接続したキーボードから送られてくるキー操作イベントをHIDインプットレポートとしてPCに送るスケッチを書いてみた。結果として普通のキーボードとして使えればオーケー。
#include "HID.h" #include <hidboot.h> #include <usbhub.h> #include <SPI.h> static const uint8_t _hidReportDescriptor[] PROGMEM = { 0x05, 0x01, // USAGE_PAGE (Generic Desktop) 0x09, 0x06, // USAGE (Keyboard) 0xa1, 0x01, // COLLECTION (Application) 0x85, 0x02, // REPORT_ID (2) --- Mouse.cppがID==1。 0x05, 0x07, // USAGE_PAGE (usage = keyboard page) // モデファイヤキー(修飾キー) 0x19, 0xe0, // USAGE_MINIMUM (左CTRLが0xe0) 0x29, 0xe7, // USAGE_MAXIMUM (右GUIが0xe7) 0x15, 0x00, // LOGICAL_MINIMUM (0) 0x25, 0x01, // LOGICAL_MAXIMUM (1) 0x95, 0x08, // REPORT_COUNT (8) 全部で8つ(左右4つずつ)。 0x75, 0x01, // REPORT_SIZE (1) 各修飾キーにつき1ビット 0x81, 0x02, // INPUT (Data,Var,Abs) 8ビット長のInputフィールド(変数)が1つ。 // 予約フィールド 0x95, 0x01, // REPORT_COUNT (1) 0x75, 0x08, // REPORT_SIZE (8) 1ビットが8つ。 0x81, 0x01, // INPUT (Cnst,Var,Abs) // LED状態のアウトプット 0x95, 0x05, // REPORT_COUNT (5) 全部で5つ。 0x75, 0x01, // REPORT_SIZE (1) 各LEDにつき1ビット 0x05, 0x08, // USAGE_PAGE (LEDs) 0x19, 0x01, // USAGE_MINIMUM (1) (NumLock LEDが1) 0x29, 0x05, // USAGE_MAXIMUM (5) (KANA LEDが5) 0x91, 0x02, // OUTPUT (Data,Var,Abs) // LED report // LEDレポートのパディング 0x95, 0x01, // REPORT_COUNT (1) 0x75, 0x03, // REPORT_SIZE (3) 残りの3ビットを埋める。 0x91, 0x01, // OUTPUT (Cnst,Var,Abs) // padding // 押下情報のインプット 0x95, 0x06, // REPORT_COUNT (6) 全部で6つ。 0x75, 0x08, // REPORT_SIZE (8) おのおの8ビットで表現 0x15, 0x00, // LOGICAL_MINIMUM (0) キーコードの範囲は、 0x25, 0xdd, // LOGICAL_MAXIMUM (221) 0~221(0xdd)まで 0x05, 0x07, // USAGE_PAGE (Keyboard) 0x19, 0x00, // USAGE_MINIMUM (0はキーコードではない) 0x29, 0xdd, // USAGE_MAXIMUM (Keypad Hexadecimalまで) 0x81, 0x00, // INPUT (Data,Ary,Abs) 0xc0 // END_COLLECTION }; #define REPORT_KEYS 6 typedef struct { uint8_t modifiers; uint8_t reserved; uint8_t keys[REPORT_KEYS]; } KeyReport; KeyReport keyReport; void sendReport() { HID().SendReport(2, &keyReport ,sizeof(KeyReport)); } void releaseAll() { memset(&keyReport, 0, sizeof(KeyReport)); sendReport(); } void report_press(uint8_t key, uint8_t mod) { if (key != 0) { bool already = false; int empty_slot = -1; for(int i = 0; i < REPORT_KEYS; i++) { if (keyReport.keys[i] == key) already = true; if (keyReport.keys[i] == 0 && empty_slot < 0) empty_slot = i; } if (empty_slot < 0) // error condition. return; if (!already) keyReport.keys[empty_slot] = key; } keyReport.modifiers = mod; sendReport(); } void report_release(uint8_t key, uint8_t mod) { if (key != 0) { for(int i = 0; i < REPORT_KEYS; i++) { if (keyReport.keys[i] == key) { keyReport.keys[i] = 0; break; } } } keyReport.modifiers = mod; sendReport(); } class KeyboardEvent : public KeyboardReportParser { protected: void OnControlKeysChanged(uint8_t before, uint8_t after); void OnKeyPressed(uint8_t key) {}; void OnKeyDown (uint8_t mod, uint8_t key) { report_press(key, mod); }; void OnKeyUp (uint8_t mod, uint8_t key) { report_release(key, mod); }; }; void KeyboardEvent::OnControlKeysChanged(uint8_t before, uint8_t after) { uint8_t change = before ^ after; if (change != 0) { if (change & after) report_press(0, after); else report_release(0, after); } } USB Usb; HIDBoot<USB_HID_PROTOCOL_KEYBOARD> HidKeyboard(&Usb); KeyboardEvent kbd; void error_blink(int period) { for(;;) { TXLED1; delay(period); TXLED0; delay(period); RXLED1; delay(period); RXLED0; delay(period); } } void setup() { static HIDSubDescriptor node(_hidReportDescriptor, sizeof(_hidReportDescriptor)); HID().AppendDescriptor(&node); delay(200); releaseAll(); if (Usb.Init() == -1) error_blink(400); delay( 200 ); HidKeyboard.SetReportParser(0, &kbd); } void loop() { Usb.Task(); }
Keyboardクラスは使わない
いままで、Arduino付属のKeyboardライブラリに手を入れながら使ってきたのだけど、今回は必要な要素を拾い上げてスケッチ内に記述することにし、Keyboardクラスを使わないことにした。
必要な要素は以下の4項目。
- Pro MicroをHID入力デバイスとするためのレポート記述データ( _hidReportDescriptor )。前回作ったLEDレポートを受けるものをそのまま使う。
- インプットレポート用の構造体およびデータ (KeyReport構造体 および 変数keyReport)。
- HIDクラスを使ってレポートを送信する関数 ( sendReport() )。
- setup() にて、HID().AppendDescriptor(&node); を使ってレポート記述データを登録。
USB Host Shield Library 2.0との連携
このライブラリはテンプレートのカタマリのような構造なので、読み解いていくのが面倒である。上で使ったサンプルスケッチと同じように、KeyboardReportParserクラスを継承したKeyboardEventクラスを定義し3つのvirtualメンバを実装した。
またサンプルと同様に、loop() 内では Usb.Task(); を呼んでMAX3421Eの状態との整合性を維持させるようにした。
void OnKeyDown (uint8_t mod, uint8_t key)
修飾キー以外の押下イベントを処理する。report_press() を呼んでPCに通知する。modおよびkeyの内容をそのまま通知することになる。
void OnKeyUp (uint8_t mod, uint8_t key)
修飾キー以外のリリースイベントを処理する。report_release() を呼んでPCに通知する。
void OnControlKeysChanged(uint8_t before, uint8_t after)
修飾キーが変化したときに呼ばれ、beforeに前回情報、afterに今回(呼び出し時)情報が格納されている。実装では、前回と変化しているキーを抽出し、オンになったのなら report_press(after, 0); で、今回オフになったのなら report_release(after, 0);でそれぞれ通知している。
OnKeyDown()とOnKeyUp()は修飾キー単独の押下時には呼ばれないので、たとえばGUIキー(Windowsキー)やAltキーのように、単独でWindowsデスクトップやアプリケーションに影響するキーのイベントを通知するために実装した。
オンになったかオフになったかによってreport_press()とreport_release() を呼び分けているが、ある2つの修飾キーがそれぞれ同時にオンからオフ、オフからオンと変化した場合、かならずreport_press() が呼ばれるように書いた。悪影響はないと思うのだけど。
void report_press(uint8_t key, uint8_t mod)とvoid report_release(uint8_t key, uint8_t mod)
今回のスケッチではキーボードからのイベントをそのまま上流側に渡すだけなので、KeyReport構造体のデータに適宜キーコードをセットし、sendReport() によって通知するだけ。
key == 0のときは修飾キーのみの変化なのでkeyreport.keys[] には触らずに修飾キー( keyReport.modifiers )のみを更新して通知を行っている。
きょうのまとめ
ビルドして書き込んだところ、意図通りにふつうのキーボードとして動作した。特に動画や写真を載せるような話でもないだろう。BIOS操作は試していないが、Windowsログオン時のCtrl + Alt + Deleteというシーケンスから同じように使えた。
簡単な応用例としては、キーボードの配列を変えるような処理が考えられる。OnKeyDown()とOnKeyUp()のそれぞれでキーコードを使ってテーブルを引き、テーブルの中身を通知に使えばいいだろう。その際、Shiftキーの状態を見たり操作したりするのがキモに思える。
別のもうちょっと高度な応用例としては、複数のキーを同時に打鍵した際のタイミングに応じて文字を出し分けるような入力方式(nicola方式の親指同時打鍵入力など)への対応だろう。このあたりをやってみようかな。
このスケッチをビルドしたときのメモリ使用状況は以下のようなものだった。
最大28672バイトのフラッシュメモリのうち、スケッチが13644バイト(47%)を使っています。 最大2560バイトのRAMのうち、グローバル変数が466バイト(18%)を使っていて、ローカル変数で2094バイト使うことができます。
USB Host Shield Library 2.0 ライブラリがフラッシュメモリをけっこう使っている。しかしながら、プログラム領域、データ領域ともにまだまだ余裕があるから、いろいろな応用に使えそうである。
追記
二階建てバージョンも作ってみました。小さくなるのと、ブレッドボード上の配線ワイヤが抜けたりする心配がないのがいいところ。
スケッチをちょっと拡張して、配列変換するような仕組みも入れてみました。こちらを参照。