VitaでZAVASやりたくて(4)
1年前のことを思い出しながらまとめるのは時間がかかりますなあ。
よいこのみんなはその時その時でちゃんとドキュメント書こうね!
まずは前回のおさらい。
ArduinoのSoftwareSerialを拡張してPC88のキーボードプロトコルである20800bps13ビットのシリアル通信ができるように書き換えたのでした。(だいぶ強引に)
あまりにやっつけすぎてちゃんと動くか怪しい代物ですけどとりあえずやってみましょう。
しつこいようですがキーボードプロトコルの復習。
調歩同期式については前回クリアしたので(したはず)、次は具体的なデータの形式を見てみます。
送信するデータのフォーマットはこうなっています。
R0 R1 R2 R3 D0 D1 D2 D3 D4 D5 D6 D7 P
RとDは上記ページの表にあるRow/Data(意味は後述)でPはパリティー(偶数パリティー)です。
見ての通り下位ビットから順に送信することになっています。
ただしSoftwareSerialに送信データを投げると勝手に下位ビットから送信してくれるので、プログラム上ではこれを反転させたデータを用意しておいて、それをSoftwareSerialに投げれば良いということになります。
要はRowもDataも普通に整数型の並びのまま扱って問題ないということです。
P D7 D6 D5 D4 D3 D2 D1 D0 R3 R2 R1 R0
では具体的なデータを見てみましょう。
初見だと表の見方がちょっとわかりにくいですが、RowはそのままR0~R3の値になっていて、DataはD0~D7のどのビットが0になっているかという情報です。
キーを押していない場合にDataは全てのビットが1(つまり0xFF)で、キーを押すとその箇所だけビットが0になります。
横一列8個のキー(8ビット分)を1セットにして表現する、いわゆるキーマトリックスというものですね。
例として「A」を押した場合を見てみると、Row=0x02、Data=1(ビット目が0)とあり、Dataをビット位置から実際の値に直すと11111101=0xFDとなるので、送信するデータは0xFD2となります。(あとこれにパリティーがつきます)
さて、そもそも何がしたかったか。
既に忘れていた頃だと思いますが、PC88の画面をテレビに映すためにまずは「F・10」を押さねばならないのでした。
表を見ると「F・10」を押したときはちょっと特殊な動きをしていて「F・10」「SHIFT」「F・5」が連続して送信されるとあります。(古いキーボードとの互換性のためみたい)
この3つのスキャンコードをPC88に強引に流し込めばいけるはず。
それぞれスキャンコードは、
「F・10」Row = 0xC, Data = 4 (0xEF)
「SHIFT」Row = 0x8, Data = 6 (0xBF)
「F・5」Row = 0x9, Data = 5 (0xDF)
なので、こんな感じのスケッチを書いてPC88にねじ込んでみます。
#include <SoftwareSerial.h>
// 出典:PC-8801FH以降のキーボードの通信プロトコル
// http://www.maroon.dti.ne.jp/youkan/pc88/kbd.html
const int pc88Kbd_DataPin = 8;
SoftwareSerial pc88Kbd(9, pc88Kbd_DataPin); // Rx(no used), Tx
void setup() {
// 20800bpsに設定
pc88Kbd.begin(20800);
}
void loop() {
for (;;) {
// PC88のスキャンコードを伝送路の形式で見ると、
// | Row0 .. Row3 | Data0 .. Data7 | Parity |
// SoftwareSerialは勝手に下位ビットから送信するのでプログラム上では反転したデータを扱えばよい。
// つまり次の形式にデータ成型したものをSoftwareSerialに放り込めばOK。
// | Parity | Data7 .. Data0 | Row3 .. Row0 |
// 表中のDataはビット位置表記になっているのでキーマトリックス(押下時0)に変換が必要。
// 「F10」アタックをかけてビデオディスプレイモードにする。
// 「F10」make時は「F10 SHIFT F5」のスキャンコードが連続して送信される。
// 表をそのまま「Row:Data」の形式で読むと「C:4 8:6 9:5」、
// Dataをビット位置表記からキーマトリックスに変換すると「C:EF 8:BF 9:DF」(make時)、
// これに偶数パリティを付加したものが送信データになる。
// 送信するごとに2.4msほど間隔をあける必要があるらしいので、念のために2倍の5ms待機。
pc88Kbd.writeEx((1 << 12) | (0xEF << 4) | 0xC, 13);
delay(5);
pc88Kbd.writeEx((0 << 12) | (0xBF << 4) | 0x8, 13);
delay(5);
pc88Kbd.writeEx((1 << 12) | (0xDF << 4) | 0x9, 13);
delay(5);
delay(100);
}
}
Arduinoへ書き込んで、コネクタをつなぎ、PC88の電源投入…
どうだ。
![](https://assets.st-note.com/img/1646841948109-Uox3TudAoH.jpg)
映らない!
コネクタの加工が雑すぎて接触が悪いのかな??
(全体的にやっつけ仕事なのでプログラム的な問題もあるかもしれない)
不安に駆られながら何度か抜き差ししては電源再投入を繰り返していると…
![](https://assets.st-note.com/img/1646841539524-A5HoF3PMIP.jpg?width=1200)
キタ━━━━(゚∀゚)━━━━!!
のか?
V2?
V2モードかな?
とにかくなんかテレビ画面に出ましたぞ。
よくわからんけど一旦リセットしてみます。
![](https://assets.st-note.com/img/1646841717656-ZQEfN627b5.jpg?width=1200)
キタ━━━━(゚∀゚)━━━━!!
今度こそほんと!
PC88 FEの起動画面とはじめてのご対面!
案外あっさり「F・10」を押せました。
この調子で色んなキーのスキャンコードをねじ込んでいけばひとまず操作はできるはず。
ただ、プログラム上で強引にねじ込むのはエレガントじゃないのでどうせならキーボードで操作したいところ。
幸いArduinoにPS/2キーボードのライブラリがあるのでそれを使います。
ちょっと理由を忘れてしまいましたけど、今回はこちらのライブラリを使用しました。
普通のキーボードっぽく使いたいのでキーのUp/Downをリアルタイムに監視しなければなりませんが、そうするとPS/2キーボードのスキャンコードを自前で処理しなければなりません。
こちらのページを参考にしてPS/2のスキャンコードをPC88のスキャンコードに変換するスケッチを作成します。
PS/2の話まですると長いしそろそろ書くのが疲れたので省略。
興味がある人は調べてみてください。
ざっくり書いたスケッチがこちら。(同時押しは一部不可)
容量がデカすぎてMegaとかでしか動かない気がする。
#include <SoftwareSerial.h>
#include <PS2Keyboard_stm32.h>
const int pc88Kbd_DataPin = 8;
const int ps2Kbd_ClockPin = 3;
const int ps2Kbd_DataPin = 4;
SoftwareSerial pc88Kbd(9, pc88Kbd_DataPin); // Rx(no used), Tx
PS2Keyboard ps2Kbd;
typedef struct _SCANCODE {
char *name;
char *mk;
char *brk;
} SCANCODE;
typedef struct _SCANCODE_MAP {
SCANCODE ps2;
SCANCODE pc88;
} SCANCODE_MAP;
SCANCODE_MAP scanCodeMap[] = {
// PS/2キーボードとPC88キーボードのマッピング情報。
// 以下のページを参考にした。
// http://www3.airnet.ne.jp/saka/hardware/keyboard/109scode.html
// http://www.maroon.dti.ne.jp/youkan/pc88/kbd.html
// SHIFT等のMODキーと同時押しでスキャンコードが変わるキーについては基本コードのみサポート。
//
// 処理の単純化のために全部文字列にしちゃうという離れ業を採用。
// PS/2 : スキャンコードのバイト列を単純に16進表現
// PC88 : 参考サイトの「Row:Data」表記を「RD」の形で単純化表記。[例]Aの場合:Row=02h,Data=1 -> 02h:1 -> 単純化して 21
// breakコードはDataを「*」で表記してみたけどあまり意味がない上にたぶん同時押しの時に困るのでmakeと同じ表記に直した方がよいような気がする。
{{"ZENKAKU","0E","F00E"},{"ZENKAKU","D3","D*"}},
{{"1","16","F016"},{"1","61","6*"}},
{{"2","1E","F01E"},{"2","62","6*"}},
{{"3","26","F026"},{"3","63","6*"}},
{{"4","25","F025"},{"4","64","6*"}},
{{"5","2E","F02E"},{"5","65","6*"}},
{{"6","36","F036"},{"6","66","6*"}},
{{"7","3D","F03D"},{"7","67","6*"}},
{{"8","3E","F03E"},{"8","70","7*"}},
{{"9","46","F046"},{"9","71","7*"}},
{{"0","45","F045"},{"0","60","6*"}},
{{"-","4E","F04E"},{"-","57","5*"}},
{{"^","55","F055"},{"^","56","5*"}},
{{"\\","6A","F06A"},{"\\","54","5*"}},
{{"BACKSPACE","66","F066"},{"BS","C583","C*8*"}},
{{"TAB","0D","F00D"},{"TAB","A0","A*"}},
{{"Q","15","F015"},{"Q","41","4*"}},
{{"W","1D","F01D"},{"W","47","4*"}},
{{"E","24","F024"},{"E","25","2*"}},
{{"R","2D","F02D"},{"R","42","4*"}},
{{"T","2C","F02C"},{"T","44","4*"}},
{{"Y","35","F035"},{"Y","51","5*"}},
{{"U","3C","F03C"},{"U","45","4*"}},
{{"I","43","F043"},{"I","31","3*"}},
{{"O","44","F044"},{"O","37","3*"}},
{{"P","4D","F04D"},{"P","40","4*"}},
{{"@","54","F054"},{"@","20","2*"}},
{{"[","5B","F05B"},{"[","53","5*"}},
{{"CAPSLOCK","58","F058"},{"CAPS","A7","A*"}},
{{"A","1C","F01C"},{"A","21","2*"}},
{{"S","1B","F01B"},{"S","43","4*"}},
{{"D","23","F023"},{"D","24","2*"}},
{{"F","2B","F02B"},{"F","26","2*"}},
{{"G","34","F034"},{"G","27","2*"}},
{{"H","33","F033"},{"H","30","3*"}},
{{"J","3B","F03B"},{"J","32","3*"}},
{{"K","42","F042"},{"K","33","3*"}},
{{"L","4B","F04B"},{"L","34","3*"}},
{{";","4C","F04C"},{";","73","7*"}},
{{":","52","F052"},{":","72","7*"}},
{{"]","5D","F05D"},{"]","55","5*"}},
{{"ENTER","5A","F05A"},{"RETURN","E017","E*1*"}},
{{"LSHIFT","12","F012"},{"LSHIFT","E286","E*8*"}},
{{"Z","1A","F01A"},{"Z","52","5*"}},
{{"X","22","F022"},{"X","50","5*"}},
{{"C","21","F021"},{"C","23","2*"}},
{{"V","2A","F02A"},{"V","46","4*"}},
{{"B","32","F032"},{"B","22","2*"}},
{{"N","31","F031"},{"N","36","3*"}},
{{"M","3A","F03A"},{"M","35","3*"}},
{{",","41","F041"},{",","74","7*"}},
{{".","49","F049"},{".","75","7*"}},
{{"/","4A","F04A"},{"/","76","7*"}},
{{"\\","51","F051"},{"_","77","7*"}},
{{"RSHIFT","59","F059"},{"RSHIFT","E386","E*8*"}},
{{"LCTRL","14","F014"},{"CTRL","87","8*"}},
{{"LALT","11","F011"},{"GRPH","84","8*"}}, // 「GRPH」
{{"SPACE","29","F029"},{"SPACE","96","9*"}},
{{"RALT","E011","E0F011"},{"","",""}},
{{"RCTRL","E014","E0F014"},{"","",""}},
{{"INSERT","E070","E0F070"},{"INS","C68683","C*8*8*"}},
{{"DELETE","E071","E0F071"},{"DEL","C783","C*8*"}},
{{"LEFT","E06B","E0F06B"},{"LEFT","A2","A*"}},
{{"HOME","E06C","E0F06C"},{"HOMECLR","80","8*"}}, // 「HOMECLR」
{{"END","E069","E0F069"},{"HELP","A3","A*"}}, //「HELP」
{{"UP","E075","E0F075"},{"UP","81","8*"}},
{{"DOWN","E072","E0F072"},{"DOWN","A1","A*"}},
{{"PAGEUP","E07D","E0F07D"},{"ROLLUP","B0","B*"}},
{{"PAGEDOWN","E07A","E0F07A"},{"ROLLDOWN","B1","B*"}},
{{"RIGHT","E074","E0F074"},{"RIGHT","82","8*"}},
{{"NUMLOCK","77","F077"},{"","",""}},
{{"NUM_7","6C","F06C"},{"NUM_7","07","0*"}},
{{"NUM_4","6B","F06B"},{"NUM_4","04","0*"}},
{{"NUM_1","69","F069"},{"NUM_1","01","0*"}},
{{"NUM_/","E04A","E0F04A"},{"NUM_/","A6","A*"}},
{{"NUM_8","75","F075"},{"NUM_8","10","1*"}},
{{"NUM_5","73","F073"},{"NUM_5","05","0*"}},
{{"NUM_2","72","F072"},{"NUM_2","02","0*"}},
{{"NUM_0","70","F070"},{"NUM_0","00","0*"}},
{{"NUM_*","7C","F07C"},{"NUM_*","12","1*"}},
{{"NUM_9","7D","F07D"},{"NUM_9","11","1*"}},
{{"NUM_6","74","F074"},{"NUM_6","06","0*"}},
{{"NUM_3","7A","F07A"},{"NUM_3","03","0*"}},
{{"NUM_.","71","F071"},{"NUM_.","16","1*"}},
{{"NUM_-","7B","F07B"},{"NUM_-","A5","A*"}},
{{"NUM_+","79","F079"},{"NUM_+","13","1*"}},
{{"NUM_ENTER","E05A","E0F05A"},{"NUM_RETURN","E117","E*1*"}},
// PS/2にはPC88側の「NUM_=」「NUM_,」が存在しない。どこに割り当てたらいいんだろう?
//{{"","",""},{"NUM_=","14","1*"}},
//{{"","",""},{"NUM_,","15","1*"}},
{{"ESC","76","F076"},{"ESC","97","9*"}},
{{"F1","05","F005"},{"F1","91","9*"}},
{{"F2","06","F006"},{"F2","92","9*"}},
{{"F3","04","F004"},{"F3","93","9*"}},
{{"F4","0C","F00C"},{"F4","94","9*"}},
{{"F5","03","F003"},{"F5","95","9*"}},
{{"F6","0B","F00B"},{"F6","C08691","C*9*8*"}}, // F6-F10はbreak順序が特殊(SHIFTが最後)
{{"F7","83","F083"},{"F7","C18692","C*9*8*"}}, // F6-F10はbreak順序が特殊(SHIFTが最後)
{{"F8","0A","F00A"},{"F8","C28693","C*9*8*"}}, // F6-F10はbreak順序が特殊(SHIFTが最後)
{{"F9","01","F001"},{"F9","C38694","C*9*8*"}}, // F6-F10はbreak順序が特殊(SHIFTが最後)
{{"F10","09","F009"},{"F10","C48695","C*9*8*"}}, // F6-F10はbreak順序が特殊(SHIFTが最後)
{{"F11","78","F078"},{"","",""}},
{{"F12","07","F007"},{"","",""}},
{{"PRINTSCREEN","E012E07C","E0F07CE0F012"},{"COPY","A4","A*"}}, // 「COPY」
{{"SCROLLLOCK","7E","F07E"},{"STOP","90","9*"}}, // 「STOP」
{{"PAUSE","E11477E1F014F077",""},{"","",""}},
// 本来なら「PAUSE」は「STOP」に割り当てるべきだと思われるがこのキーはbreakを発行しない&リピートしない特殊な動きをするのでやめとく。
{{"MUHENKAN","67","F067"},{"KETTEI","D196","D*9*"}},
{{"HENKAN","64","F064"},{"HENKAN","D096","D*9*"}},
{{"KANA","13","F013"},{"KANA","85","8*"}},
{{"LWIN","E01F","E0F01F"},{"PC","D2","D*"}}, // 「PC」
{{"RWIN","E027","E0F027"},{"","",""}},
{{"APP","E02F","E0F02F"},{"VT_BREAKALL","","0*1*2*3*4*5*6*7*8*9*A*B*C*D*E*F*"}}, // キーを離したときに全breakを発行する仮想キー(振る舞いがおかしくなった時のリセット用)として割り当てておく。
};
int h2i(char c) {
if ('0' <= c && c <= '9') return c - '0';
if ('A' <= c && c <= 'F') return c - 'A' + 10;
if ('a' <= c && c <= 'f') return c - 'a' + 10;
return -1; // あまりよろしくないがとりあえず変なのを返しておく。よいこのみんなはまねしないでね。
}
uint32_t Pc88RD2Packet(char R, char D, bool isMake) {
uint8_t row = h2i(R);
uint8_t dataAtBit = h2i(D);
uint8_t data = isMake ? ~(1 << dataAtBit) : 0xFF;
uint32_t scancode = (data << 4) | row; // D7-D0,R3-R0 -> 送信時は LSB first
bool parity = false;
for (int i = 0; i < 12; i++) if ((1 << i) & scancode) parity = !parity;
return (scancode | (parity << 12));
// 余談:
// http://www.maroon.dti.ne.jp/youkan/pc88/kbd.html
// ここの「■謎」の記述によるとR=0xEの場合はdataのbit7は常に0でないとダメかもしれない。一応動いてる。
}
void Pc88Sendkey(char R, char D, bool isMake) {
uint32_t packet = Pc88RD2Packet(R, D, isMake);
pc88Kbd.writeEx(packet, 13);
delay(5); // 2.4msほど待つ必要があるらしい。念のため2倍待つ
}
void Pc88Sendkeys(char *RDs, bool isMake) {
for (int i = 0; RDs[i] != '\0'; i += 2) {
uint8_t R = RDs[i];
uint8_t D = RDs[i + 1];
Pc88Sendkey(R, D, isMake);
}
}
void setup() {
Serial.begin(9600);
pc88Kbd.begin(20800);
ps2Kbd.begin(ps2Kbd_DataPin, ps2Kbd_ClockPin);
}
char scanCodeBuf[64] = "";
void loop() {
// キー入力がない場合はさっさと抜ける
if (!ps2Kbd.available()) return;
int c = ps2Kbd.getRcevData();
if (!c) return;
// キー入力あり!
// 受信したデータをバッファに追加して一致するPS/2スキャンコードがあるか探す。
// あれば対応するPC88スキャンコードを送信、なければ次のデータに乞うご期待。
int len = strlen(scanCodeBuf);
// 8バイト(文字列的には16文字)超えの場合は何かが間違ってるのでバッファをクリア。
// 入力を取りこぼしたことになるがそれは忘れる。
if (len > 16) len = 0;
sprintf(scanCodeBuf + len, "%02X", c);
int scanCodeMapNum = sizeof(scanCodeMap) / sizeof(scanCodeMap[0]);
// PS/2スキャンコードを総当たりするぜえ
for (int i = 0; i < scanCodeMapNum; i++) {
SCANCODE ps2 = scanCodeMap[i].ps2;
SCANCODE pc88 = scanCodeMap[i].pc88;
if (strcmp(scanCodeBuf, ps2.mk) == 0) { // 一致するスキャンコード発見!
Serial.print("[MK ] "); Serial.print(ps2.name); Serial.print(" "); Serial.print(ps2.mk); Serial.print(" -> "); Serial.print(pc88.name); Serial.print(" "); Serial.println(pc88.mk);
Pc88Sendkeys(pc88.mk, true);
scanCodeBuf[0] = '\0';
break;
}
if (strcmp(scanCodeBuf, ps2.brk) == 0) { // 一致するスキャンコード発見!
Serial.print("[BRK] "); Serial.print(ps2.name); Serial.print(" "); Serial.print(ps2.brk); Serial.print(" -> "); Serial.print(pc88.name); Serial.print(" "); Serial.println(pc88.brk);
Pc88Sendkeys(pc88.brk, false);
scanCodeBuf[0] = '\0';
break;
}
// make/breakで何か処理が変わるかなと思ってなんとなく.mk/.brkに分けたのだが何も変わらん。
// 結局コピペでごり押しするという壮大なる無駄を犯してしまった。よいこのみんなはまねしないでね。
}
}
PS/2のClockとDataをなんで3番ピンと4番ピンにしたのかはまるで思い出せません。(普通2と3だろって感じだけどなんでだろ。変えても大丈夫なはず)
コネクタの接触の問題なのかはたまたバグか、ちょいちょいおかしな動きしますのでご了承ください。
例によってやっつけ仕事なのでバグってる自信はとてつもなくある。
さて、
Arduinoへ書き込んで、コネクタをつなぎ、PC88の電源投入…(セカンドシーズン)
どうだ。
![](https://assets.st-note.com/img/1646843431431-9SMOYhT7Qw.jpg)
動かないんだなこれが。
コネクタぐにぐにすると反応したりするのでやっぱ接触だろうかねえ。
そんなこんなであせあせしながら何度かリトライしてみると…
![](https://assets.st-note.com/img/1646843521452-FfXfI0YuFp.jpg?width=1200)
こいつ動くぞ!
入力できましたよ!
なんかすげえ!
やればできる!
いやこれ尋常じゃなくテンションあがりますね。
死ぬまでに一回はやってみた方がいいよほんと。
というわけで、若干挙動があやしいものの、一応キーボードが使えるようになりました。
これで念願のパソコンとの通信ができるぞ!
次回はいよいよフロッピー吸出しの冒険の旅。