さて、前回はPS/2マウスを98のバスマウスに変換して利用してみました。
次にPS/2キーボードを98につなげて使えるようにしてみました。使う機材は前回と同じ格安のArduino Nano(の互換機)です。
ところで、最近やっとgitHub始めました(冷やし中華的な?)。いままでずっとローカルでやってましてgitは統合開発環境なんかはリポジトリの管理が標準でついているのでなんとなく使っていました。一人でやってるんでリモートリポジトリの重要性もあんま感じなく、gitHubを覚える時間があったらコードを書こうと思っていたのですが、公開するときにやっぱり便利なんで、初心者ですが始めてみました。公開第一号です。でもあんまよくわかってません!
コードを見ながらざくっと読んでいただければ…ざっくり解説を入れていきます。
ArduinoNanoとの接続
98側のコネクタはMiniDin8pin、PS/2キーボードはMiniDin6pinとなります。ピンアサインは下記のとおりです
※画像はyagura様コネクト資料室から転載させていただきました
1 2 3 4 5 6 7 8 9 |
//98側(miniDin8)ピン定義 #define RST 4 //リセット要求 #define RDY 5 //送信可能 #define RXD 9 //データ送信 #define RTY 6 //再送要求 //PS2側ピン定義 #define PS2_DATA A3 // PS2キーボードのDATAピンに接続 #define PS2_CLK 3 // PS2キーボードのCLKピンに接続(割込み可能PIN必須Nanoなら2or3) |
注意が必要なPinはPS2_CLKで割込み可能なPinしか使えません。Nanoであれば2または3ピンとなります。
あとはGND、5Vを接続すればOKです。電源はPC-98本体から取りますので別途用意する必要はありません。
コネクタは上の商品でいいと思います。AliExpressでも取り扱いがありますので好きなのをどうぞ
注意が必要なのがMiniDin8pinのオスオスケーブルで下記の商品
これ、意外に安くていいなと思ったのですが…。
クロスケーブルで結線がクロスされています!!
これに気づかなくて、しばらーーーくハマってました。ストレートケーブルは、なかなかないのでAliExpressで探した方がいいかもしれません。僕は泣きながらクロスケーブルのピンアサインを変えて使っています。
PC-98シリーズのキーボードの仕様
先に説明したMiniDin8ピンで接続します。接続プロトコルはシリアル通信(19200bps,8bit,奇数[odd]パリティ,ストップビット1bit)となっているようです。Nanoと接続するにはハードウエアシリアルかソフトウエアシリアルを使うことになると思います。ハードウエアシリアルはスケッチ書き込み、ログの読み出しで使いたいと思っているのでソフトウエアシリアルを用いることとしました。
しかし!標準のライブラリではパリティのオプションがつけられないとのこと。下のライブラリを利用することで利用可能となることがわかりました
98のキーボードインターフェイスの実装をしなければならないのですが、いかんせん古い情報ですので先人のコードを読んだり、ネットの海を探したりしました。特に
上のサイトのio_kb.txt
上のサイトには特にお世話になりました。
98のキーボードの通信処理は
- 送信処理(キーコード[何が押されたか離されたか]・問い合わせに対する反応[ACK,NACK])
- 受信処理(キーボード種類、LEDの状態問い合わせ、キーリピート情報などなど)
があり適切に処理しないと、特にwindows2000などにはキーボードして認識されません。まずは純正キーボードがどのように98本体とやり取りしているかをロジックアナライザやシリアル通信で観察することにしました。
使ったロジアナはWindowsでのセットアップのクセが強すぎる中華格安ロジアナです。ソフトはPulseViewを使っています。
98本体からキーボードへ送られるコマンドの処理
Rは98本体->キーボード、Sはキーボード->98本体を示します。
電源投入時には、R:FC R:9D S:FA R:70 S:FA R:00 R:9F S:FA S:A0 S:80 R:FF となっていました。R:9D S:FA R:70 S:FAはLED(Caps,カナ,Num)の設定、R:9F S:FA S:A0 S:80はキーボードの種類の問い合わせと応答(新キーボードはA0 80を返す)となっています。
また、98本体->キーボードのみのデータとなりますが
- Win98SE起動時: FF FF 96 96 96 96 96 96 96 9C 51 9C 70
- Win2Kでは起動時: 96 96 96 FF 9F 96 96 96 9C 70 9D 70 9D 9D 70 9D 70 9C 70 9D 72 9D 72 9D 72 9C 70 9D 72 9D 72 9D 72
- DOS6起動時: FF
が送られてきていました。起動時と同じように9DはLED設定、9Fはキーボード種類問い合わせです
96は「モード識別で自動変換モードではA0h,86hが返される。通常モードではA0h,85hが返される」とあります。なんのことかよくわかりませんがw、純正キーボードはA0 85を返しているようです。
9C 51 、9C 70はキーリピートの情報でio_kb.txtや先人のコードでは以下のようなデータ構造をしていることなんですが、ちょっとこれはなんか違うんじゃないかと思うわけです。詳しくは下のキーリピート問題に書いてあります。今回、どのみちPS/2キーボードにもキーリピートの設定があって98キーボードに近い設定を強制的にするようにしたのでキーリピート設定が送られてきてもACKを返しとけばいいのです(笑)
とまぁ、こう考えると 9C、9D、9F、96あたりに無難に応答してあげればOKということになります。全コードは最後に掲載しますが下のように処理します。num_fはNumLockのLEDなんですが、この機能自体はスケッチ上で処理するため状態の通知があっても無視しています。というか98にNumLockってありました?w
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 |
void pc98key_command(void){ uint8_t c,up_c; //cの上位4bit保持用 while(mySerial.available()>0){ c = mySerial.read(); up_c = c & 0xf0; //上位4bit #ifdef DEBUG Serial.print("Read : "); Serial.print(c,HEX); Serial.println(""); #endif //コマンドが0x9_以外はリセットかける。 ログでは0xFF,0x7_などが起動時に流れているが委細不明 if(up_c != 0x90){ kana_f = 0; caps_f = 0; num_f = 1; continue; } // コマンド処理 switch(c){ case 0x95: // Windows,Appliキー制御 pc98key_send(ACK); while(!mySerial.available()); c = mySerial.read(); // c=0x00 通常 // c=0x03 Windowsキー,Applicationキー有効 pc98key_send(ACK); break; case 0x96: // モード識別 通常モードを送信. pc98key_send(ACK); delayMicroseconds(500); pc98key_send(0xA0); delayMicroseconds(500); pc98key_send(0x85); // 0x85=通常変換モード, 0x86=自動変換モード break; case 0x9C:{ // キーボードリピート制御 pc98key_send(ACK); while(!mySerial.available()); c = mySerial.read(); #ifdef DEBUG Serial.print("9C-DATA : "); Serial.print(c,HEX); Serial.println(""); #endif pc98key_send(ACK); //キーリピート設定がなんであれACKで応答 break; } case 0x9D: // LED 制御 pc98key_send(ACK); while(!mySerial.available()); c = mySerial.read(); up_c = c & 0xf0; //上位4bit if(up_c == 0x70){ //LED状態の通知 //状態の記録 //num_f = ((c & 0x01) == 0x01); caps_f = ((c & 0x04) == 0x04); kana_f = ((c & 0x08) == 0x08); pc98key_send(ACK); }else if(up_c == 0x60){ // LED状態読み出し c = 0; if(num_f) c |= 0x01; if(caps_f) c |= 0x04; if(kana_f) c |= 0x08; pc98key_send(c); }else{ pc98key_send(NACK); } break; case 0x9F: // check keyboard type pc98key_send(ACK); delayMicroseconds(500); pc98key_send(0xA0); delayMicroseconds(500); pc98key_send(0x80); // 新キーボードを送信 break; default: // 不明コマンドは NACK で応答 pc98key_send(NACK); break; } return; } |
キーボードから98へ送られるコマンドの処理
キーボードから98へ送られるデータとして、前項の各種問い合わせの応答と、キーコードの送信になります。RDY(送信可能かどうか)、RTY(再送要求があるか)を確認して送るという関数となります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
void pc98key_send(uint8_t data){ while(digitalRead(RDY) == HIGH) delayMicroseconds(1); //送信不可より待機 if(digitalRead(RTY) == LOW){ // RTYオンなら直前のキーを再送信する mySerial.write(prev); }else{ // 現在キーを直前のキーに保存 mySerial.write(data); prev = data; #ifdef DEBUG Serial.print("Send : "); Serial.print(data,HEX); Serial.println(""); #endif } delayMicroseconds(500); } |
PS/2側キーボードのデータ送受信
次はPS/2側のデータ送受信です。これはArduinoとグローバルな器材のPS/2キーボードの接続という比較的メジャー命題なので情報も多く、ライブラリでササっとできると思っていました。
が有名どころです。結構機能満載で、スキャンコードをアスキーコードに変換処理などしてくれます。今回はPS/2のスキャンコードの取得だけできればいいのでオーバースペックですねぇ。また、LEDの制御(Caps,NumLock,ScrollLock)も行えるライブラリとなると急激に情報が減ります。
色々探していると
の情報を発見。非常にシンプルで分かりやすいです。しかもLEDの制御も徹底的に解説しており、サンプルコードまであり感謝しかありません。このコードでPS/2キーボードのデータ送受信と制御を行うこととしました。まずは必要な部分を別ファイル(ps2ScanCode.h)にまとめます
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 |
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; #ifdef DEBUG //sprintf(tmp, "cmd=%02X, resp=%02X", cmd, ret); //sprintfは遅くて処理落ちするので //Serial.println(tmp); Serial.print("PS2Send : cmd="); Serial.print(cmd,HEX); Serial.print(" resp="); Serial.println(ret,HEX); #endif } 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; } 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); } |
このヘッダファイル(実装も含んでますが…)を事前にメインのスケッチでincludeして、以下のコードで初期化を行います。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
// ~略~ //PS2側ピン定義 #define PS2_DATA A3 // PS2キーボードのDATAピンに接続 #define PS2_CLK 3 // PS2キーボードのCLKピンに接続(割込み可能PIN必須Nanoなら2or3) #define CLK_INT 1 // 割り込み順位 //PS2 LED値 #define PS2_LED_CAPSLOCK 0x04 //CapsLock時 #define PS2_LED_NUMLOCK 0x02 //NumLock #define PS2_LED_SCRLOCK 0x01 //スクロールロックはカナ #include "ps2ScanCode.h" //PS/2スキャンコード取得&LED制御コード // ~略~ void setup(){ #ifdef DEBUG Serial.begin(115200) ; Serial.println("Keyboard Debug :") ; #endif // ~略~ //PS/2キーボード初期化 pinMode(PS2_DATA, INPUT); pinMode(PS2_CLK, INPUT); attachInterrupt(CLK_INT, clk_interrupt, FALLING); delay(50); if (kbd_reset()){ #ifdef DEBUG Serial.println("kbd_reset OK"); #endif //キーリピートのセット if(setKeyRepeat()){ #ifdef DEBUG Serial.println("keyRepeat set OK"); #endif }else{ #ifdef DEBUG Serial.println("keyRepeat set Error"); #endif } delay(10); //キーボードのLEDのセット setLED(); }else{ #ifdef DEBUG Serial.println("kbd_reset ERROR"); #endif } } |
これでPS/2キーボードのスキャンコード取得とLED制御などのコマンド送信を行うことができます。次はPS/2スキャンコードを98キーボードのスキャンコードに変換する部分です。
スキャンコード変換
まず入力されたPS/2側のスキャンコードの詳細です。
この資料みてみると、ブレイクコード(キーを離したとき場合)は押したキーコードの前にF0コードを付ける。またE0で始まるスキャンコードはE0の後にF0コードを入れればよいことがわかります。が、例外もあります。
Pauseキーはブレイクコードがないですし、シフト・NumLockキー状態に左右されるキーでは押すときのキーコードにF0がある場合があります。
次は98のスキャンコードですが、先人のソースや下のサイト、純正キーボードをたたいてロジアナを観察することで情報を得ました
これらをまとめて、keyconst.hで定義しました。また特殊キーはコード内で処理しています。もっといい処理方法がある気もしますが、とりあえずこれで動いているので暇なとき整理したいと思います。メインはcodeArray、codeArrayE0でPS2->98キーコード変換をします。NumLock解除時にはcodeArrayNotLkを利用します。これでvfキー、テンキーをカーソル替わりに使うことを実現しています。とりあえずコードを追ってもらえればと思います。
キーリピート問題
キーリピートとは、あるキーを押し続けると一定時間後リピートする機能です。このキーリピート機能は98キーボード、PS/2キーボード共にキーボード側で実現しています。ちなみにUSBキーボードはソフトウエア(BIOSなど)やドライバ側で実現しています。98キーボードは先に触れたように9Cコマンドが送られてくるとリピートするまでのディレイとリピート間隔のデータを送ってきます。それに基づいてキーボードはキーリピートを行います。
ロジックアナライザで計測したところ、98純正キーボードのデフォルトではディレイ500msでリピート間隔40msでした。先人のコードをみるとディレイは500msなんですが、間隔が60msとなっていました。何度計測しても40msなので自分のデータを信じていきたいと思います。
次に調べたことが9Cコマンドに送られてくるパラメータの解釈です。
資料には上のように定義されているとのことです。実験として、Win98上でのコマンドプロンプト(DOS窓)を開いてみると9C 51が送られてきます。フォーカスがメモ帳などに切り替わると9C 70が送られてきます。
ちなみにロジックアナライザを見ていると9C 51の時はキーリピートをキーボードが行っています。が、9C 70の後ではドライバ側で行っているようでキーリピートさせてもロジックアナライザでは反応ありません。つまり70はキーリピート無効、51はキーリピート有効であると考察できます。
つまりWin2K、Win98SE起動時では最後の9Cコマンドでは70が送られてくることからキーリピートは無効になっていてドライバ管理ということですね。
そもそも、キーリピート無効という設定が上の資料からは読み取れません。先人のコードを見るとbit0-4が00000bだと無効としてあります。しかし、70h = 01110000bで00000bのデータ部がありません。うーん
ここからは僕の推測ですので話半分で読んでください。
70hの上下4bitを反転させると00000111bなります。こうするとbit0-4が00000bとなりキーリピートは無効となります。
51hの場合は00010101bとなります。リピート間隔はbit0-4が00010bとなりリピートは有効となります。ディレイはbit5-6ですので10bで上の資料の対応表から500msとなり正しくなります。bit0-4のリピート間隔の解釈ですが、これは最高速の間隔(00001b)を20msとすると 20ms×10b(2Dex) = 40msとなり実測値と同じくなります。
この仮説を検証しようにも9Cのデータが70と51しか観測できず推測の域を出ることができません。このへん詳しい人いませんかね?ツイッターで待ってますw
[2024-09-24] X(ツイッター)で情報提供がありデータ解釈が判明しました!
https://t.co/vRzZvGjCn9(@d_dinagon)を読んでいたところ、キーリピートの速度設定が https://t.co/UFBLwSeLj1と違うのではないかとの指摘があったので実際に 00h~FFhを送ってみた。
たぶんこうだろう pic.twitter.com/4YQgeQSw7f
— Ryoichi Fukushima (@netperfect) August 12, 2024
ちょっと忙しくてブログの更新が遅れてしましました。非常に有用な情報感謝です。時間ができ次第コードを書きなおしてみたいと思ってます!
でコードでの実装ですが、いろいろやってみてわかったことは、9C 70が送られて、キーリピート無効でも無視してキーボードでキーリピートをしてても問題なく動くのですw。というわけで常にデフォルトでキーリピートをオンにし続ければいいわけです。
1 2 3 4 5 6 7 8 9 10 11 12 |
//キーリピートをPC-98のデフォルト[delay=500ms Rate=24MakeCode/Sec(約40ms/Stroke)]で固定 // https://www.technoveins.co.jp/technical/keyboard/ bool setKeyRepeat(){ uint8_t tmp[1]; send_command(0xF3,1,tmp); if (tmp[0] != 0xfa) return false; send_command(0b00100010,1,tmp); if (tmp[0] != 0xfa) return false; send_command(0xF4,1,tmp); if (tmp[0] != 0xfa) return false; return true; } |
というわけで上記コードをPS/2キーボードに送り98のキーリピートと同一にセットし続けておくことで解決します。
ではなんで、くどくど9Cのデータのことを話したかというと、次に作るUSBマウスキーボード変換器(実はほぼ完成)のコード処理に欠かせない知識となったので忘備録的に書かせてもらいました。みなさんも興味ありますよね?(オイ)
最後に(セットアップメニュー起動とか)
あとの細かい部分はコードをみてほしいのです。自分好みにカスタマイズしてください。もう何を書こうと思ったのかすら忘れてしまってw。
起動時にキーを押すとセットアップメニューを起動できます。いろいろありますがHELPが有名どころですね
この変換器で…セットアップメニュー起動をできるかできないか!どっちなんだいっ?
できません!!
電源投入時に
R:FC R:9D S:FA R:70 S:FA R:00 R:9F S:FA S:A0 S:80 R:FF
というデータが純正キーボードやりとりされているのは上で説明しましたが、最初のFCを受信して次の9Dを受信する間に起動するキーコード(HELPキーなど)を送らないとダメなんですね。
しかし、この変換器は電源が入り、ソフトウエアシリアルの初期化が完了し、最速でキーコードを送ってもR:9F付近が限界でした。というわけで無理なんですw。別電源で予めNanoを起動させとくか、ハードウエアシリアルを使うかすればいいのかもしれません。
複数のハードウエアシリアルがあるATmega2560あたりと思いますが、持ってません!まぁESP32があるんですが、ロジックレベルが違うんだよなぁ・・・
とはいえ、次の予定ではESP32を使ってUSBキーボード・マウスを同時に変換する変換器を作っています。試験段階ですがセットアップメニュー起動はできていますが、癖が強い方法となりそうですw。こうご期待(誰得?)
続きのUSB編です~