概要
JIS配列のPS/2キーボードをほぼNICOLAキーボードとして使うための準備を始めた。
そのためには、PS/2キーボードを(SparkFun) Pro Microに接続したり、PS/2キーボードから上がってくるスキャンコードを扱うための仕組みを作る必要がある。今回は、以下の3点について。
- PS/2キーボードを接続するための仕組み。
- PS/2キーボードのキーコードを得る。
- PS/2キーボードにコマンドを送ってLEDを点灯させる。

LEDのついたPS/2キーボードを持ってなかったので、amazonで666円(送料込)で売っていたサンワサプライのSKB-L1BKを購入して使った。
仕組み
まず、PS/2キーボードは+5V電源を必要とする。そして、ホスト側とキーボードの接続には、+5Vレベルの2本の信号線を使う。なので、前回と同じ+3.3V動作のPro Microではダメで、+5V動作のものが必要になる。
とりあえず手元に+5V版のPro Microがないし、注文して届くまで2週間程度はかかりそうなので、手持ちのArduino PRO MINI (コピー品のThe Simple)に接続していろいろやってみることにした。

回路図
今回は以下のような回路を使った。

PRO MINIにMINI DINソケットを接続し、PCとPRO MINIは秋月電子のUSB-シリアル変換モジュールAE-FT231Xを使って接続する。そしてPS/2キーボードのケーブルは、MINI-DINソケットに接続する。2本の信号線にはショートしたときの電流を制限するため、それそれ直列に抵抗を入れるべきなのだが今回は省略した。
MINI-DINソケットについても、秋月電子のMINI-DIN DIP化キット(ミニDINコネクタピッチ変換キット)を使ったので、ブレッドボードにそのまま載せることができた。

PRO MINiおよびキーボードの電源はAE-FT231Xの+5V端子を使うが、このモジュールのUSB-VBUSと+5V端子の間には350mAでトリップするポリスイッチが入っているから、過大な電流を消費するデバイスは接続できない。PS/2キーボードの最大消費電流の仕様は100mAのようなのでまず大丈夫だろう。
キーボードとホスト間の通信
キーボードとPRO MINI間で接続する信号線はDATAとCLKの2本のみで、今回はCLKをArduinoのD3 (INT1)に、DATAをD5につないだ。
CLKにはデータ伝送時のみ10K~16.7KHzのクロック信号が、DATAにはCLKに同期したシリアルデータ(1スタートビット、8ビットデータ、奇数パリティ、1ストップビット)が流れる。データはLSBからMSBの順。以下通信をコントロールする側(今回はPRO MINI、一般的にはPC)を「ホスト」と称することにする。
キーボード – ホスト間は双方向の通信が可能になっていて、キーボードからホストにはおもにキー入力データが、ホストからキーボードにはリセットやLED点灯などの制御情報(コマンド)が送信される。DATAの所有権(どちらが送信側になるか)はホスト側が決めるが、いずれの方向も同期クロックの発生はキーボード側が行う。これは、デバイス側の実装を容易にするためだろう。
通信のチャートやタイミングについては、PS/2 Mouse/Keyboard Protocol (This article is Copyright 1999, Adam Chapweske) を参考にし、以下のタイミングは今回実装した回路やプログラムを動かして実際に観測したものを掲載した。
電気的な概略
2本の信号線の駆動はオープンコレクタで行うことになっているので、信号線はホスト側で電源電圧にプルアップする(キーボード側にプルアップ抵抗が入っているのかどうかは不明)。両端ともに駆動していない場合はHIGHレベルになり、いずれかが駆動中は、プルアップ抵抗を介した電流を吸い込んでLOWレベルとなる。

概念図ではこんな具合か。OUTPUTをHIGHにしてトランジスタのベースに電流を流せば、プルアップ抵抗を介してコレクタに電流が流れ込むので信号線(図ではDATA or CLK)はLOWになるし、OUTPUTをLOWにすればコレクタがハイインピーダンスとなって信号線はHIGHになる。おのおののINPUTはハイインピーダンスな入力ポートを想定しており、任意の時点で信号線の論理値を読み取ることができる。
Arduinoを使うとき、トランジスタやOCドライバICを使ったり、各信号線ごとにポートピンを2本使うのも面倒なので、非駆動中は pinMode(port, INPUT);(ハイインピーダンス)にし、駆動するときは pinMode(port, OUTPUT); として0を出力(シンク)することで代用した。
今回は、回路図に示すように4.7KΩでプルアップすることにした。+5V電源で4.7KΩならば、駆動中に流れ込む電流は1mA程度ということになる。抵抗値を小さくするとクロックやデータの立ち上がりが素早くなるが、電流は余計に流れることになる。
キーボードからホストへの通信
通常の場合、キーボードのキーを打ったら対応するスキャンコードが送信される。キーボードは、少なくとも50usecの期間CLKがHIGHならば送信可能(ホスト側が所有権を持たない)と判断してCLKの駆動を開始しデータを送信する。キーボードは、以下のような形式で1バイト分のデータを送信してくる。なお、1バイト分の送信が終わるとクロック送信を停止するのでCLKはHIGHに戻る。

キーボードはCLKがHIGHの期間(立上りから5usec以内)に送信すべきビットをDATAに出力し、次の立上り(から5usec以内)まで保持することになっている。なので、ホストはCLKの立下りエッジを検出したときにDATAを読めばキーボードがそのとき出したビットの論理値を得ることができる。
最初の立下りはSTARTビットなので常に0。ホストは、引き続く8回の立下りでデータのLSBから順に8ビット分を読み取る。その次がパリティビット(奇数パリティ)で、最後に常に1のSTOPビットがくる。つまり合計で11回の立下りエッジで1バイト分のデータが表現される。
奇数パリティなので8ビットのデータのうち、1のビットが偶数個ならパリティビットは1、奇数個なら0となる。データの8ビットとパリティビットをあわせて奇数個のビットが1になる。
CLKの論理はHIGH状態から開始するので、キーボードがSTARTビットをセットするタイミングはCLKだけではわからない。少なくともCLKの最初の立下りエッジでDATAに出ているのがSTARTビットということ。

CLKがHIGHまたはLOWの期間は、図に示したように約30~50usecなので、ホストは立下り検出後すみやかに読み取る必要がある。今回使ったキーボードでは1周期が約80usecだった。つまり、周波数は12.5KHz程度なのだろう。
PS/2キーボードでは、数バイトのコードで構成されているキーがあったり、プリフィックスコードによってキーのリリースを表したりするので、ホスト側は連続的に受信を行えるようにしておく必要がある。
今回使ったキーボードで実測したところ、複数バイトのスキャンコードを発生するキーの場合、最初のバイトのSTOPビットの後、約3.2msec後に次のバイトのためのCLKの駆動が開始していた。ただ、ホストからコマンドを送った場合には400usec程度で応答が帰ってくる。
ホストからキーボードへの通信
ホストは、キーボードをリセットするときや、LEDの点灯/消灯を行うときキーボードに対してコマンドデータを送信する。キーボードがコマンドデータを受け取ると、キーボードは速やかに応答データ(ACKまたは再送要求)を返してくる。
コマンドデータを送信するときには、まずDATAの所有権を得る必要がある。そのためにホストは、CLKを最小で100usecの期間LOWに保ち、その状態のままDATAをLOWにすることでキーボードに対して送信要求を行う。そしてDATAをLOWに駆動してから約50usec後にCLKの駆動を停止する。具体的には、ArduinoのポートピンをINPUTに設定し直すことで、ハイインピーダンスに戻す。

送信要求が成功するとキーボードがCLKの駆動を開始する。キーボードはCLKがHIGHの期間にDATAを読み取る。なので、CLKの駆動を開始する前(CLKがHIGHの期間)にSTARTビットを読み取ると思われる(送信要求のためDATAはLOWのまま)。ホストは引き続く立下りエッジでbit0を出力してやり、その後も立下りエッジごとに順次DATAにビットを乗せてやる。
図には「立上りエッジで読み取る」と書いたが、立上りエッジのタイミングまでにホストはDATAにビットを出しておく必要がある、という意味。
今回実装していて、送信要求を行うためにDATAをLOWにするタイミングを早くしたり遅くしたりすると、キーボードがCLKの駆動を開始しないことがあった。けっこう微妙なところ。
以下のスケッチの send_command(); という関数に送信要求のためのポート制御を書いてあるが、このあたりはもうちょっと検証したい部分である。
スケッチ
上記の回路にキーボードをつないで以下のスケッチを書き込んで動かすと、次のように動作する。
- 押下/リリースしたキーに対応するスキャンコードをシリアルモニタに表示する。
- F10, F11, F12キーを押下すると、それそれ NumLock LED, CapsLock LED, ScrLock LEDを点灯/消灯する。
- NumLockキーを押下すると、キーボードLEDがしばらくの間チカチカする。
最終的にはライブラリの形態にするが、今回は動作確認が目的ということでスケッチにベタッと書いた。また、押下したキーに対応した文字を出すことが目的ではないので、スキャンコード列から文字を得るような機能は入っていない。
#define LED 13 // on-board led. #define PS2_DATA 5 #define PS2_CLK 3 #define CLK_INT 1 void show_error(int ms) { digitalWrite(LED, 1); delay(ms); digitalWrite(LED, 0); delay(ms); } static const uint8_t KBD_BUFFER_SIZE = 12; volatile uint8_t kbd_in, kbd_out; uint8_t kbd_buffer[KBD_BUFFER_SIZE]; // fifo key buffer void clear_buffer() { cli(); kbd_in = kbd_out = 0; sei(); } bool put_buffer(uint8_t s) { uint8_t rx = kbd_in + 1; if (rx >= KBD_BUFFER_SIZE) rx = 0; if (rx == kbd_out) return false; // buffer is full. kbd_buffer[kbd_in] = s; kbd_in = rx; return true; } uint8_t get_buffer() { if (kbd_in == kbd_out) return 0; // empty. uint8_t s = kbd_buffer[kbd_out]; cli(); if (++kbd_out >= KBD_BUFFER_SIZE) kbd_out = 0; sei(); return s; } uint8_t cmd_to_send; typedef enum { None = 0, Idle, Receiving, WaitForStart, Sending } bus_state_t; volatile bus_state_t bus_state; void clk_interrupt() { volatile static uint8_t data = 0; volatile static uint8_t clocks = 0; volatile static uint8_t par = 0; switch(bus_state) { case Idle: clocks = 0; data = 0; digitalWrite(LED, 1); bus_state = Receiving; // found Start bit break; case Receiving: clocks++; if (clocks < 9) { data = data >> 1 | (digitalRead(PS2_DATA) ? 0x80 : 0); if (clocks == 8) // パリティとストップビットは無視。 put_buffer(data); // とりあえずバッファフルは見ない。 } else if (clocks == 10) { // STOP bit. bus_state = Idle; digitalWrite(LED, 0); } break; case WaitForStart: // Start bit will be fetched. (DATA is LOW) par = 0; clocks = 0; data = cmd_to_send; bus_state = Sending; break; case Sending: clocks++; if (clocks == 9) // parity digitalWrite(PS2_DATA, par & 1 ? LOW : HIGH); else if (clocks == 10) // STOP bit timing. pinMode(PS2_DATA, INPUT); // release DATA else if (clocks == 11) // ACK bit from keyboard. bus_state = Idle; else if (clocks > 0 && clocks < 9) { // send data bits. if (data & 1) { digitalWrite(PS2_DATA, HIGH); par++; } else digitalWrite(PS2_DATA, LOW); data = data >> 1; } break; } } bool send_command(uint8_t cmd, uint8_t resp_count = 1, uint8_t* resp = 0) { int timeout = 15; unsigned long start = millis(); while(bus_state != Idle && millis() - start < timeout) delayMicroseconds(200); clear_buffer(); cmd_to_send = cmd; bus_state = WaitForStart; pinMode(PS2_CLK, OUTPUT); // digitalWrite(PS2_CLK, 0); // drive LOW. delayMicroseconds(100); // at least 100usec. pinMode(PS2_DATA, OUTPUT); // drive LOW (START bit) digitalWrite(PS2_DATA ,0); delayMicroseconds(50); pinMode(PS2_CLK, INPUT); // release clock. CLK goes to HIGH. start = millis(); while(bus_state != Idle && millis() - start < timeout) delayMicroseconds(100); uint8_t ret = 0; for(int8_t i = 0; i < resp_count; i++) { char tmp[20]; start = millis(); if (cmd == 0xff && i == 1) timeout = 500; // for Basic Assuarance Test. while((ret = get_buffer()) == 0 && millis() - start < timeout) // タイムアウトチェックする ; if (resp) resp[i] = ret; sprintf(tmp, "cmd=%02X, resp=%02X", cmd, ret); Serial.println(tmp); } return (ret == 0xfa); } bool kbd_reset() { uint8_t tmp[2]; send_command(0xff, 2, tmp); // reset keyboard. if (tmp[0] == 0xfa && tmp[1] == 0xaa) return true; return false; } #define PS2_LED_CAPSLOCK 4 #define PS2_LED_NUMLOCK 2 #define PS2_LED_SCRLOCK 1 uint8_t kbd_led_state = 0; bool kbd_led(uint8_t led) { bool f = send_command(0xed); // LED if (f) { kbd_led_state = led; send_command(led); // LED parameter. } else kbd_reset(); return f; } void toggle_led(uint8_t led) { uint8_t new_led = kbd_led_state; if (new_led & led) new_led &= ~led; else new_led |= led; kbd_led(new_led); } const uint8_t pattern[] = {0, 2, 4, 1, 0, 1, 4, 2, 0, 2, 6, 7, 5, 1, 0, 7}; void led_demo() { kbd_reset(); delay(50); for(int j = 0; j < 5; j++) { for(uint8_t i = 0; i < sizeof(pattern); i++) { kbd_led(pattern[i] & 7); delay(200); } } kbd_led(0); } void setup() { Serial.begin(115200); delay(100); Serial.println("A_PROMINI_PS2_LED1 Start."); pinMode(LED, OUTPUT); digitalWrite(LED, 0); pinMode(PS2_DATA, INPUT); pinMode(PS2_CLK, INPUT); attachInterrupt(CLK_INT, clk_interrupt, FALLING); delay(50); if (!kbd_reset()) show_error(300); } void loop() { uint8_t scan_code = get_buffer(); static bool e0_prefix = false; static bool f0_prefix = false; if (scan_code) { char tmp[20]; sprintf(tmp, "%02X ", scan_code); Serial.println(tmp); if (scan_code == 0xf0) f0_prefix = true; else if (scan_code == 0xe0) e0_prefix = true; else if (!f0_prefix && !e0_prefix) { // LED. if (scan_code == 0x09) // F10 toggle_led(PS2_LED_NUMLOCK); else if (scan_code == 0x78) // F11 toggle_led(PS2_LED_CAPSLOCK); else if (scan_code == 0x07) // F12 toggle_led(PS2_LED_SCRLOCK); else if (scan_code == 0x77) // Num Lock led_demo(); } if (scan_code != 0xe0 && scan_code != 0xf0) { if (e0_prefix) e0_prefix = false; if (f0_prefix) f0_prefix = false; } } }
JIS配列PS/2キーボードのスキャンコードについては、 http://www.ne.jp/asahi/shared/o-family/ElecRoom/AVRMCOM/PS2_RS232C/KeyCordList.pdf を参考にさせていただいた。
void clk_interrupt() について
今回のスケッチでは、CLKの立下りエッジによる割込み処理内でデータの受信とデータの送信を行っており、割込み発生時に何をするかは、状態を示す bus_state という変数と、検出した立下りエッジ数をカウントする clocks という変数で決まるようにした。
Idle状態
ホストがDATAの所有権を持っておらず、データの受信待ちを表す。この状態で割込みが起きれば、それはキーボードがCLKの駆動を開始したことになる。
Receiving状態
(clocks++の後に)clocks が1~8ならば、データビットの受信中なので変数 data にLSBから順に取り込んでいく。8ビット目を取り込めたら、パリティビットやSTOPビットは無視して、受信バッファに格納する( put_buffer(data); )。パリティエラーくらいは見てもいいのだが。
そしてclocksが10ならば、それはSTOPビットを表している。受信完了したものとして、Idle状態に戻る。
WaitForStart状態
send_command(); でホストからの送信を開始するためにCLKをLOWにした時点で割込みが起きる。clocksの初期化や送信すべきデータの設定などを行う。
この時点ではキーボードによるCLKの駆動は開始していない。駆動が開始しない場合データの送信は行われない。今回のスケッチでは、かならず駆動は開始するものとした。
Sending状態
(clocks++の後に)clocksが1~8ならば、データビットの送信中なので変数 dataの内容をLSBから順に1ビットずつDATAに乗せる。clocks == 9 ならパリティビット、10ならSTOPビットを送る。STOPビットは常に1なので、この時点でDATAの所有権を放棄( pinMode(PS2_DATA, INPUT); )している。
clocks == 11 ならばキーボード側がDATAに乗せるACKビットを読むことができる。今回の実装ではDATAの読み取りは行わず送信が終了したものとして状態をIdleに戻している。
コマンド送信のための send_command();
この関数は、キーボードへのコマンドの送信と送信後に得られる応答の取得を行う。応答は通常1バイトなのだがリセット(0xff)を送ったとき、正常応答(0xfa)に引き続いてキーボード内でのセルフテストの実行結果が帰ってくるのでそれを得るようにした。
最初にキーボードから受信中かどうかを bus_state を見て判断し、Idleならば cmd_to_sendに送信データをセットし、bus_state = WaitForStartとしてからCLKやDATAの駆動を行う。CLKをLOWに駆動した瞬間に割込みがかかってclk_interrupt() に制御が移ることに注意。
その後は割込み関数が data (コマンドまたはパラメータ)を送信し終わるのを bus_state を見ながら待ち、送信終了したらキーボードからの応答データ(正常受信 : 0xfa、再送要求 : 0xfe)を待つ。このスケッチでは正常応答以外見ていない。
なお、コマンド送信の終了から応答データのためのCLK駆動開始までは約400usecだった。ただ、セルフテストの応答を得るまでは、400msec程度かかったので、一部タイムアウト時間を延長している。
スケッチ内で、送信したコマンドと応答データをシリアルモニタに表示して終了。
LEDコマンド(0xed)
一般的なPS/2キーボードには3つのLEDがあり、各LEDを表すビットをセット/リセットすることで点灯または消灯する。
LEDを制御するためにはまず0xedを送る。このコマンドバイトに対して正常応答が得られたら、LED状態を表すパラメータバイトを同じように送信する。各LEDを表すビットは以下のように定義している。
#define PS2_LED_CAPSLOCK 4 #define PS2_LED_NUMLOCK 2 #define PS2_LED_SCRLOCK 1
つまり、LSBがScrLock LED、bit1がNumLock LED、bit2がCapsLock LEDということ。対応するビットがセットされていれば点灯し、クリアされていれば消灯する。コマンドを送信するための bool kbd_led(uint8_t led) ; と、各LED状態をトグルするための void toggle_led(uint8_t led); を用意した。
リセットコマンド (0xff)
キーボードをパワーオンリセットした状態に戻す。内部でBAT ( BASIC Assurance Testというらしい)が走り、その結果を帰ってくる。リセットすると、キーボードにある3つのLEDが同時に点灯してすぐ消える。
リセットを行うための関数 kbd_reset() を用意し、setup() の最後に呼び出している。PRO MINIのリセットボタンを押してやると、シリアルモニタには以下のように表示される。

コマンドに対する応答(FA)と、BAT正常終了を表す AA が出力されている。
メインループ ( loop() )
PS/2キーボードのスキャンコードは、HIDキーボードと違って複雑怪奇である。今回はふつうに使われているスキャンコードを対象にしたが、どうやら数種類のスキャンコードのグループがあるらしい。
今回は、キーボードから送られてきたスキャンコードをシリアルモニタに表示するだけにし、特定のキーの押下に対応して以下のように動くようにした。
- F10キー(scan_code == 0x09):NumLock LEDをトグル。
- F11キー(scan_code == 0x78):CapcLock LEDをトグル。
- F12キー(scan_code == 0x07):ScrLock LEDをトグル。
- NumLockキー(scan_code == 0x77):配列 pattern[] の内容に応じて、約20秒間LEDを点けたり消したりする led_demo() で実行する。この間は、キーボードからのキー入力はほぼ受け付けられない。
一応、0xe0や0xf0といったプリフィックスコードを意識した処理にしたが、まだ不完全だろう(0xe0の方は不要な気もする)。たとえば、PauseキーのスキャンコードにはNumLockキーと同じ0x77が含まれるので、NumLockキーを押したときと同じように動作してしまう。
実際に動かすと以下のようになる。
ファンクションキーを押してLEDを点灯/消灯したあと、NumLockキーで led_demo() を動かしている。最後にPRO MINIのリセットボタンを押し、キーボードのBATによって全LEDが点灯、そして消灯しているところ。
PRO MINI上のLED (D13に接続されている)も、キーの押下やLEDコマンドの応答時に短く光っているのが分かる。
きょうのまとめ
LEDを点けるだけにしては大掛かりになってしまった。
もともとPS/2キーボードの動作を把握してプログラム化したかったのは、愛用している東プレ Realforce 91をほぼNICOLA化するため。+5V版のPro Micro相当品が届いたらライブラリ化してhoboNicolaプログラムに組み込むことになる。そのときにはまたブログに載せることになると思います。
秋月電子のmini DIN DIP化キットはとても便利なのだが、PS/2キーボードの長いケーブルをつないでいるので、ちょっと動かすとブレッドボードから抜けてしまう。ユニバーサル基板にハンダ付けして使うのが正しいだろう。miniUHSボードのように、mini Arduinoと2階建てにするのがいいのか、Pro Microもmini DINもユニバーサル基板に固定してしまうのがいいのか、考え中です。