BLE Nano を使っていた自作キーボードだったが、Mac のアップデートとともにまた不安定になってしまい、使う気を失ってしまった (数時間ごとに再ペアリングが必要に)。直す気力もなくてしばらく放置していたが、そろそろ観念して USB 接続のキーボードに作りかえることとした。

作りかえるといっても、キーボードの部分は I2C GPIO のモジュールとして動くように作ってあるので、マイコンまわりを載せ変えて実装を書くだけになる。

ということで、タイトルの通り LPC11U35 を使ってキーボードを実装しなおした。

コード: https://github.com/cho45/keyboard-lpc11u35

mbed official の USBDevice

mbed official に存在するライブラリの USBDevice の中に USBKeyboard というのがある。USBHID を継承していて、レポートデスクリプタとかを予め設定してくれる便利クラスとなっている。これは LPC11U35 でもちゃんと動くので基本的にハマるようなところはない。

標準で便利なメソッドがいくつか定義されているが、実際にキーボードを作る場合はこれらは使わない。USBKeyboard に定義されているメソッドはデモ用と考えていいと思う。

普通に使う場合は、レポートデスクリプタ定義はそのまま使いつつ (特に変更する必要がないので)、USBHID#send() を直接呼んで HID_REPORT を自分で構成して送ることになる。

基本的なコード

#include "mbed.h"
#include "USBKeyboard.h"

#include "mcp23017.h"

#define REPORT_ID_KEYBOARD 1
#define REPORT_ID_VOLUME   3

USBKeyboard keyboard;
DigitalOut led(LED1);
DigitalIn key1(P0_4, PullUp);

int main() {
    HID_REPORT report;
    
    uint8_t modifier = 0;
    uint8_t usage = 0;

    report.data[0] = REPORT_ID_KEYBOARD;
    report.data[1] = modifier;
    report.data[2] = 0;
    report.data[3] = usage;
    report.data[4] = 0;
    report.data[5] = 0;
    report.data[6] = 0;
    report.data[7] = 0;
    report.data[8] = 0;
    report.length = 9;
   
    
    while(1) {
        bool isKey1Pressed = key1.read() == 0;
        if (isKey1Pressed) {
            if (report.data[3] != 0x04) {
                report.data[3] = 0x04 /* a */  ;
                keyboard.send(&report);
            }
        } else {
            if (report.data[3] != 0x00) {
                report.data[3] = 0x00;
                keyboard.send(&report);
            }
        }
        wait_ms(10);
    }
}

基本的にはこういう形です。USBKeyboard のメソッドではキーの「押しっぱなし」ができない実装なので、ちゃんとしたキーボードにするにはレポートを自分で管理する必要があります。実用的にはリポート内のキーの状態を管理するクラスが必要になるでしょう。

といっても、USBKeyboard をほとんど使わないようなら直接 USBHID を継承して MyUSBKeyboard を作ったほうがいい気がするので (レポート定義もいじれるようになるし)、実際はそうしています。

ハマりポイント

sleep() がうまくいかない

ハマることはないと書いたが、メインループで sleep() (Active Sleeep) を使ってデバイスを割り込み待ちにするコードを書いたところ、USBHID#send() が失敗するという状態になった。どうやらUSBの状態まで狂わすようだったのでビジーループに変えた。

なんでおかしくなるのか、実装を読んだりマニュアルを読んだりして調べてみてもよくわからない。USB のクロックは有効だし、USB まわりの電源も sleep で切れるようなものはないように思える。

割り込み用のピンのプルアップが弱い

内部プルアップ時の電流がスペックだと 50μA となっている。電源電圧 3.3V なら 66kΩ 相当のプルアップとなる。

I2C を 2kΩでプルアップ動かしていると、この内部プルアップのピンをかなり動かしてしまうようだった。とりあえずは大丈夫そうだったが、今後誤動作の原因となりそうだったので、こちらも同様に 2kΩ で外部プルアップとした。

その他くだらないハマり

  • 一部のLANケーブルと相性がなぜか悪い
  • キーボード側で断線
    • かなり細いパターンの部分が見えないレベルで断線しており、割込みがかからない状態であった
  • sleep をやめたことによるバグ
    • キー入力がないときは I2C バスをやすませるような動作にしていたが、条件判定用の数値が sleep をやめたことによりアンダーフローしていた。
  • 時々キーが二重入力される
    • USB の通信遅延?
    • DEBUG 用に printf してるのが同期出力なのが原因っぽいので、本番で使う場合は必ず全 printf をオフにするように

接続

キーボード左右+USBコントローラという構成になった。USBコントローラとキーボードの左右はLANケーブルで接続する。BLE 版だとコントローラー基板はキーボードの左に付属していて、左右のキーボード同士をLANケーブルで接続していたが、実際はこのように配線を変えても動くような構成にしていたので、回路自体はすんなり変更できた。

電池がなくなったり、コントローラ基板を別にしたことで、キーボード全体の座高を低く、かつフラットにすることができた。今まで地味にキー位置が高くて、キーボード前にクッションが必要だったんだけど、その必要がなくなった。

筐体の作りなおし

3Dプリンタを得たのでより剛性の高い筐体になった。

  1. トップ
  2. tech
  3. LPC11U35 でUSBキーボードを作る

NKRO (N-key Rollover) というのは、おおざっぱに言うとキーボードのキーを同時押ししたとき、いくつ認識するか?ということです。

NKRO の理想的には以下の挙動になります。

  • いくつキーを押しても全て同時に押されていることがコンピュータから認識される

これを NKRO として定義すると、全ての USB HID キーボードは NKRO ではありません。なぜなら、USB HID キーボードは修飾キーを除くと6キーまでしか押されていることを送信できないからです。

USB HID での NKRO

そうすると妥協した次点として以下のような仕様を NKRO とするほかありません。

  • 6キーまでは同時押しがコンピュータから認識される
  • 7キー目を押すと、最初に押したキーが離されたと認識される

完全な同時押しは6キーまでですが、入力をとりこぼすということはありません。USB HID で NKRO と呼ばれているものはおそらくこの挙動をすると思います。

まぁ実際のユースケースとして、そもそも7キー以上の同時押しは極めて稀です。ということでさらに妥協して以下のような実装も考えられます

  • 6キーまでは同時押しがコンピュータから認識される
  • 7キー目を押しても無視される

これはこれでほとんど問題ないでしょう。単純に 6KRO になります。押した順番という状態を持つ必要が減るのでファームウェアの実装は簡単になります (バグを少なくできます)。一方で、キーを離さないクセを持つ人がものすごい高速タイピングをした場合にはキーをとりこぼすかもしれません。

PS-2 キーボードではどうなのか?

PS-2 キーボードのプロトコルは単純で、シリアル通信で

  • キーを押すと Make 信号を発生させる
  • キーを離すと Break 信号を発生させる

というものです。つまり NKRO になるかはOS側の実装次第です。

ゲーマー向けのマザーボードには PS-2 端子がついていることが多いですが、これは

  • NKRO 対応のため
  • レイテンシ削減のため

であると思われます。USB はホストからのポーリングで成りたっているため、キーを押した瞬間にコンピュータにデータが送られてくるわけではなく、コンピュータ側からのポーリングを待つ必要があります。ポーリング間隔はデバイス側から通知され、最小で1msまで設定可能ですが、OS 側に最終決定権があり、例えば Windows では最小で 8ms です。つまりこの時点で最大で8msの遅延が発生します。

PS-2 の場合、キーが押された瞬間に Make 信号を送るため、理論的にはこちらのほうが早いことになります。

  1. トップ
  2. tech
  3. USB キーボードでの NKRO