Pro Micro + キーパッドにキーボードLEDを実装

概要

前回作ったPro Micro(コピー) + 秋月電子の4×3キーパッドに、キーボードLEDの点灯/消灯を行う機能を追加し、PCのキーボードと同じように、ロックキー(CapsLock, NumLock, ScrollLockなど)の操作によって点灯させてみる。

A_PRO_MICRO_4x3_KEY_1
Pro Micro + KeyPad.

1台のPCに複数のUSBキーボードをつないでいると、ある1つのキーボードでたとえばNumLockキーを押すと、すべてのキーボードのNumLock LEDが点灯する。これは、「キーボードLEDを点灯せよ」という通知がUSBを介してすべてのキーボードに届いているからだろう。今回はこの通知に応じてLEDを付けたり消したりしてみた。

オンボードLEDを利用

ブレッドボードを組み替えるのも面倒だったので、Pro MicroのオンボードLED(RXLEDとTXLED)を使うことにした。これらのLEDはUSB送受信のたびにチカチカしてしまうがそこには目をつぶることにした。

前にも書いたがPro Microには3つの表面実装型のLEDが実装されていて、

  • 赤(LED1) : VCC直結の電源インジケータ
  • 緑(RXLED) : PD5に接続。LOWで点灯。USBコネクタを上にしたとき、オシレータの左側。
  • 黄(TXLED) : PB0に接続。LOWで点灯。同じく右側。

となっている。今回利用しているコピー品ではRXLED, TXLEDともに緑色LEDが使われていた。

これらのLEDをスケッチから利用するための定義は、SpakFun Pro Micro用の pins_arduino.h に書かれている ([user_directory]\AppData\Local\Arduino15\packages\SparkFun\hardware\avr\1.1.12\variants\promicro\pins_arduino.h )。Arduino IDEで対象ボードとしてSparkFun Pro Microを選択していれば、ビルド時にこのヘッダファイルが使われる。

上記の pins_arduino.h を見ると、Pro Micro 上の2つのLED(RXLED, TXLED)のために以下のようなマクロが用意されている。

#define TX_RX_LED_INIT  DDRD |= (1<<5), DDRB |= (1<<0)
#define TXLED0          PORTD |= (1<<5)
#define TXLED1          PORTD &= ~(1<<5)
#define RXLED0          PORTB |= (1<<0)
#define RXLED1          PORTB &= ~(1<<0)

#define LED_BUILTIN 13
#define LED_BUILTIN_RX 17					
#define LED_BUILTIN_TX 30

TX_RX_LED_INIT は呼び出し済のはずなので、TXLEDを点灯するためには、

TXLED1;
// あるいは、
digitalWrite(LED_BUILTIN_TX, 0);

と書けばよいことがわかる。

LEDを点灯させるための修正

HIDレポートのこと

USBキーボードからPCに対するキー入力情報はHID仕様のインプットレポートによって通知され、PCからキーボードに対するLED制御はアウトプットレポートによって通知される。

ある機器がどのようなレポートを扱うのか、レポートの中身がどんなふうなのかは、機器ごとのHIDレポート記述データ(HID Report Descriptor) に定義しておく必要がある。たとえば前回使ったKeyboardライブラリのKeyboard.cppには、

static const uint8_t _hidReportDescriptor[] PROGMEM = {
  //  Keyboard
    0x05, 0x01,                    // USAGE_PAGE (Generic Desktop)  // 47
    0x09, 0x06,                    // USAGE (Keyboard)
    0xa1, 0x01,                    // COLLECTION (Application)
    0x85, 0x02,                    //   REPORT_ID (2)
    0x05, 0x07,                    //   USAGE_PAGE (Keyboard)
...

のようなデータが30行にわたって書かれている。ビルドしてPro Microに転送したバイナリにはこの内容が含まれているので、扱えるレポートもこのデータに従うことになる。

残念ながらこのレポート記述データには、キーボードLEDに関する項目が含まれていない。そのためPCにつながっている別のキーボードでCaps Lockキーを押しても、前回作ったキーパッドには何も通知されないのである。

LED用のアウトプットレポート

2013年から2014年にかけての話なのでちょっと前になるが、Arduino.cc のフォーラムにLeonardo keyboard leds emulation? ” というトピックがあるのを見つけた。投稿者の質問に対して、当初はよく分かってないような(?)リプライが続いていたが、しばらく経ったころ、当時のArduino HIDライブリにキーボードLED用のアウトプットレポートを受けるための修正パッチを載せてくれたありがたい方がいた。
そのパッチには、アウトプットレポート用のレポート記述データの追加と、レポートを受け、その内容をLED状態としてArduino側で記憶するためのコードが含まれていた。

今回使っているArduino 1.8.3では、その当時のライブラリとは構成が変わっているのでそのままでは使えない。新たに書き起こすのも面倒なので、現在のライブラリファイル(Keyboard.cpp, HID.cpp, HID.h)に修正を加えて使うことにした。
修正にあたっては、上記のパッチとusb.org が公開している、Device Class Definition for Human Interface Devices (HID) Version 1.11を参考にし、レポート記述データについてはその中の、Appendix E: Example USB Descriptors for HID Class Devices の E.6 Report Descriptor (Keyboard) にしたがった。または、キーやLEDのUsageコードについては、HID Usage Tables Version 1.12 を参照。

変更後のHIDレポート記述データ

Keyboard.cpp内のHIDレポート記述データを以下のようにし、説明のためのコメントを入れた。

// Keyboard.cpp hid report descriptor (modified).
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
};

変更点は、LED状態のアウトプットレポートに関する定義と、キーの押下情報の範囲を日本語キーボードに合わせてKeypad Hexadecimalまで拡張したこと。このKeypad Hexadecimalというキーの実体は不明なのだけど、上にリンクを書いたHIDのUsage Table ページ7 (Keyboard/Keypad) の修飾キー直前のコードがこれだったのでそうした。いっそ 0xff(255) までとしてもいいのかもしれないが。

この修正を適用したKeyboard.cppを含むzipをダウンロードするためのリンクを文末に用意した。

HID仕様書にあるデスクリプタ( E.6 Report Descriptor (Keyboard) )と比べると、0x85, 0x02, // REPORT_ID (2)  という行が多い。これは、Arduino付属のMouseライブラリと同時に使うことが想定されていてマウス側がID == 1、キーボード側がID == 2となる。つまり、Mouseライブラリも同じバイナリに含めた場合、2つのエンドポイントを持つデバイスになるようだ。PCのデバイスマネージャを見ると、Pro MicroはUSB Composite Deviceに見えている。

修飾キー通知

インプットレポートでの8つの修飾キーの内容は以下のようなビットパターンで表現する。

ビット キー
0 左CTRL
1 左SHIFT
2 左ALT
3 左GUI (Windowsキー)
4 右CTRL
5 右SHIFT
6 右ALT
7 右GUI (Windowsキー)
LED通知

アウトプットレポートのLED通知の内容は以下のようになっている。

ビット キー
0 NUM LOCK
1 CAPS LOCK
2 SCROLL LOCK
3 COMPOSE
4 KANA
5 (reserved)
6 (reserved)
7 (reserved)

COMPOSEキーというのは、ヨーロッパ言語などにある á や ß といった文字をキーボードから入力するための、2つ以上のキーのコンビネーションを開始するためのキー。COMPOSEを打ってから、a と ‘ を打つと á  になる、ということらしい。

KANAは、昔のPC9801などにあったKANAロックキーのことだろう。当初はメカニカルロックキーだったものが、いつからかふつうのキーになって、押下ごとにLEDがトグルしてカナ状態を示すようになった記憶がある。

実はKANAロックは現在のWindows 10でも有効で、うちのPCではCtrl + 英数 というコンビネーションでカナロックがかかる。カナロックしてしまうとIMEをローマ字入力にしていてもJISカナになるようだ。また、カナロックをかけたままスリープしてしまうと、回復時のパスワード入力時に配列がカナになっていて、何度打ってもはじかれて焦ることがある。

アウトプットレポートを受けるための修正

上記のようなレポート記述を入れておくことで、Pro Micro側にもアウトプットレポートが届くようになるはずだが、受け取ったレポートをLED状態に反映してやる必要がある。今回は、HID.cppとHID.hを修正し、スケッチからLED通知用のコールバック関数をセットするようにした。

HID.h の修正
// Function prototype of led notification callback.
typedef void (*report_callback_t)(uint8_t);

class HID_ : public PluggableUSBModule
{
...
  report_callback_t led_notify;
public:
  void set_led_callback(report_callback_t fn) {
  	led_notify= fn;
  };
};

class HID_の前でコールバック関数用のプロトタイプを定義し、HID_クラス定義の最後に関数ポインタおよび関数ポインタ登録用のメソッドを追加した。

HID.cppの修正

コンストラクタ(HID_::HID_(void))内で、led_notify = 0; をやっておき、bool HID_::setup(USBSetup& setup) の中の、if (request == HID_SET_REPORT) {} の中を以下のようにした。

if (request == HID_SET_REPORT) {
	if (setup.wLength == 2 && setup.wValueL == 2) {
		uint8_t data[2];
		if (2 == USB_RecvControl(data, 2)) {
			if (led_notify) {
				led_notify(data[1]);
				return true;
			}
		}
	}
}

ベースクラスからHID_SET_REPORTが通知されてそれがキーボード用で、コールバック関数がセットされているならば、led_notify(); を呼んで通知内容を知らせている。

修正したHID.cpp, HID.hを含むリンクを文末に用意した。

LED動作の確認用スケッチ

LEDが意図通りに点くのかどうかを確認するためのスケッチを書いた。キーパッドの緑色のキーをScrollLockキーとし、青色のキーをNumLockキーにした。また、TXLEDをScroll LockLED用、RXLEDをNumLock LED用としている。

#include <Keyboard.h>
// A_PROMICRO_4x3_key_hid_led_1
const  uint8_t scan_port[] = { 2, 3, 4};
const  uint8_t data_port[] = { 5, 6, 7, 8};

uint8_t led_status = 0;
void led_func(uint8_t data) {
  led_status = data; 
}

void setup() {
  for(int i = 0; i < sizeof(scan_port); i++) {
    pinMode(scan_port[i], OUTPUT);
    digitalWrite(scan_port[i], HIGH);
  }
  for(int i = 0; i < sizeof(data_port); i++)
    pinMode(data_port[i], INPUT_PULLUP);
  Keyboard.releaseAll();
  HID().set_led_callback(led_func);
}

// 全キースキャンして、押下されているキーのビットを1にしたビットマップを返す。
int key_scan() {
  int key_pressed = 0;
  for(int col = 0; col < sizeof(scan_port); col++) {
    digitalWrite(scan_port[col], LOW);
    int row_data = 0;
    for(int row = 0; row < sizeof(data_port); row++) 
      row_data |= (digitalRead(data_port[row]) ? 0 : 8 >> row);
    key_pressed |= (row_data << (sizeof(data_port) * col));
    digitalWrite(scan_port[col], HIGH);
  }
  return key_pressed;    
}
// LEDs
#define KEY_LED_NUMLOCK    1
#define KEY_LED_CAPSLOCK  2
#define KEY_LED_SCRLOCK   4
#define KEY_LED_COMPOSE   8
#define KEY_LED_KANA    0x10
// Lock Keys.
#define KEY_SCROLL_LOCK    0xcf
#define KEY_NUMLOCK    0xdb

const uint8_t key_codes[] = {
  0x31, 0x34, 0x37, KEY_SCROLL_LOCK, 
  0x32, 0x35, 0x38, 0x30, 
  0x33, 0x36, 0x39, KEY_NUMLOCK};

void loop() {
  static int last_key_pressed = 0;
  int key_pressed = key_scan();
  int change = key_pressed ^ last_key_pressed;
  if (change) {
    int mask = 1;
    uint8_t code;
    for(int i = 0; i < sizeof(key_codes); i++) {
      if (change & mask) {
        code = key_codes[i];
        if (key_pressed & mask)
          Keyboard.press(code);
        else
          Keyboard.release(code);        
      }
      mask = mask << 1;          
    }
    last_key_pressed = key_pressed;
  }
  digitalWrite(LED_BUILTIN_TX, led_status & KEY_LED_NUMLOCK ? 0 : 1);
  digitalWrite(LED_BUILTIN_RX, led_status & KEY_LED_SCRLOCK ? 0 : 1);
  delay(20);
}

このスケッチのビルドや実行には、修正済のライブラリファイルが必要です。文末のリンクからダウンロードしたファイルを、Arduinoライブラリ内のファイルと置き換えて使う必要があります。

全体としては、前回載せたスケッチとほとんど同じになっている。LED関係の追加と、キー割り当ての変更を行った。

LED関係
// LEDs
#define KEY_LED_NUMLOCK    1
#define KEY_LED_CAPSLOCK  2
#define KEY_LED_SCRLOCK   4
#define KEY_LED_COMPOSE   8
#define KEY_LED_KANA    0x10

各LEDのビットマスクをマクロで定義した。

setup()の最後で、HID().set_led_callback(led_func); を呼んでアウトプットレポートをもらえるようにしている。

uint8_t led_status = 0;
void led_func(uint8_t data) {
  led_status = data; 
}

アウトプットレポートは、led_status という変数に格納する。

そしてloop() の最後で、led_statusの内容に応じてLEDを点けたり消したりしている。

digitalWrite(LED_BUILTIN_TX, led_status & KEY_LED_NUMLOCK ? 0 : 1);
digitalWrite(LED_BUILTIN_RX, led_status & KEY_LED_SCRLOCK ? 0 : 1);

TXLEDおよびRXLEDは、0の書き込みで点灯することに注意。

今回の実装では、led_func(uint8_t data) 内でLEDを制御するのではなく、loop() が回るたびにしつこくオン/オフするようにしている。TXLEDおよびRXLEDは、Pro Microに対するUSB通信があるごとにチカチカするから、LEDの点灯状態とled_status の対応するビットとの対応を維持するためにこのようにした。

キー割り当て

const uint8_t key_codes[] 内のキーコードデータを変更した。変更に使ったキーコードは以下のように定義している。

// Lock Keys.
#define KEY_SCROLL_LOCK    0xcf
#define KEY_NUMLOCK    0xdb

Arduino付属のKeyboardライブラリでは、文字以外のキー(修飾キーやファンクションキー)の定義方法が独特なので、それに合わせて HID Usage Table ページ7 (Keyboard/Keypad)に示されているキーコードに136を加えた値を定義した。

スケッチを見てもわかるように、今回変更した2つのキーを押してもLEDデータは変化しない。LEDの制御はあくまでもアウトプットレポートにしたがって行う。

動作状況

DELL PCのおまけについていたUSBキーボードを使って、キーパッドとキーボードのLEDが同時に光る様子を動画にした。DELLというロゴの上にキーボード側のLEDインジケータがある。

ふだんは、もっと小型のLEDのないキーボードを使っているので、この動画のために探し出して掃除して撮影した。

きょうのまとめ

思っていたよりLEDの追加は簡単だった。アウトプットレポートによってPC側からキーボードに何らかの通知を行うことができるので、LED以外のアウトプットレポートの追加も可能だろう。

次回は、WindowsのプログラムからLED通知を送ってみる話になる予定。

修正したライブラリファイル

今回の話で修正を加えたKeyboard.cpp, Keyboard.h,  HID.cpp, HID.h を含んだzipファイル(hid_and_keyboard.zip) はこちらからダウンロードできる。Arduinoライブラリ内の同名のファイルを置き換えて使うことを想定しているので、もしもビルドして使うならば所定のディレクトリにコピーすること。置き換える際には、オリジナルファイルをディレクトリごとコピーして保存することを強くお勧めします。

※ 改変したファイルの使用にあたっては、各ファイルの先頭に書いてあるLGPLの規定を読んでください。これらのファイルが役に立てばいいのだけれど害があっても知りません、ということです。

なお、Windows10で “C:\Program Files(x86)\Arduino\….” 内のファイルを書き換えて保存すると、ディレクトリから消えてしまう(VirtualStoreに移動される)ことがあるので、注意してください。