NEC 初代PC-8801において画面表示用の漢字ROMのデータを使ってプリンタPC-8821に漢字を出力するために、機械語の変換ルーチンを作成したというお話です。1983年ころのお話です。
初期のPC-8801は漢字ROMが別売りでした。同時発売だったのか、遅れて発売だったのか記憶がないですが、本体購入から1年以上たってから追加しました。当時ワープロなる機械も発売される前の話です。ワープロは英字・数字だけだが何アメリカでは作られていると聞いていましたので日本語でもなんとかならないか、そこまで行かなくとも、せめて毎回同じ文字を印刷する帳票ぐらいは漢字を使いたいと思ったわけです。
問題は値段です。漢字ROMの値段は約4万円。しかも本体とプリンタにそれぞれに必要だというのです。
そこで、本体用のみ購入してプリンタもそのデータを流用しようと考えました。
プリンタPC-8821は、18ピンのものです。それまでのプリンタは9ピンでypqなどの下に出る文字が上にずらされて印刷されるというものも多かったのです。英数字だけでも美しく印字したいと購入したお金をかけたプリンタでした。それでも漢字は16✕16で十分ではありません。24✕24が出るのはもう少し先です。
本体もプリンタも16✕16なら流用できると考えたわけです。
本体では漢字ROMからフォントデータを読みだして画面に描画します。
本来はBASIC内でPUT文で画面に表示するところですが、I/Oポートへアクセスするか、BASICのROM内ルーチンを使って取り出すことができます。例えば、「選」という字は、次のようなビットマップになっています。
実機が現在無いので昔のスクリーンショットから拾いました。余白が右に入っていたか左に入っていたかまではわかりません。とにかく1ビットが1ドットを表すビットマップです。
ROM内ルーチンを使ってデータを得る方法を使う場合は、16✕16ビットのビットマップデータが、8ビット✕32バイトとなって、32バイトの並びとしてメモリ上に得られます。その並びは、上記の「選」の場合、
40 00 27 BC 10 84 07 BC 04 20 04 A4 ...
となっています。
図との対応でわかる通り、左が上位ビット、右が下位ビットで、バイトの並びと文字内の対応は次のようになっています。
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 |
当時のプリンタは、本体から送られた文字コードにより内蔵のフォントを呼び出して印刷していました。それとは別に画像データのためにビットマップを送るコマンドも持っていました。
漢字ROMは内蔵フォントを漢字にまで拡張するものですが、今回見送りましたから、ビットマップとして描画することになります。
コマンドは、
LPRINT CHR$(27);"I";0016";
の後に2バイトを1列として16列分のデータ、つまり32バイトを送れば、一文字描画されます。漢字に16✕16は解像度不足ですが、当時はこれが精一杯でした。
バイトの並び順は次のようになっていて、各バイト内では、下が上位ビット、上が下位ビットになります。
1 | 3 | 5 | 7 | 9 | 11 | 13 | 15 | 17 | 19 | 21 | 23 | 25 | 27 | 29 | 31 |
2 | 4 | 6 | 8 | 10 | 12 | 14 | 16 | 18 | 20 | 22 | 24 | 26 | 28 | 30 | 32 |
当然のことながら、本体用つまりCRT画面用の32バイトのデータをそのままの順でプリンタに送ると、次のようになってしまいます。
1つだけ残っていた印刷物の拡大図です。
Gimpで加工して、正しい状態にしたもの。文字は「廠」。文字は適当に文字コードを入力したものらしいです。16✕16ですがピンが太くて滲んでいる様子がわかります。
メモリからデータを拾う順番を 2,1,4,3,6,5,...とすれば90度回転した文字を印刷することができます。句読点の位置は別途工夫が必要ですが、漢字だけならこれでいけます。
さて、ここからが本題です。文字を画面と同じ向きでプリンタに出したいのです。
横書きの文字にするには、次のようなデータの変換が必要になります。
40,27,10,07,04,04,07,E1
⇩
80,81,82,04,00,7A,4A,CA
下半分もありますから、バイトの順番はちょっと違いますが、横に並んだビットを縦に切り取ってプリンタ用のバイトの並びを作らなければなりません。。
BASICでビット演算をするのは時間がかかりそうです。そこで機械語で組んでみることにしました。
ビットの操作には、次の2つのローテイト命令を使います。
RLC (HL) はHLレジスタのアドレスにある1バイトを図のように1ビット回転させます。ビット0を1に、1を2に、2を3に...と順繰りに移動し、ビット7は0に移動すると同時にキャリービット(c)にコピーします。
(HL)はHLレジスタの値がE010だった時にはE010のアドレスのデータ(1バイトです)が操作対象であるという事です。
RRA はそのキャリーをAレジスタのビット7に移動し、7を6に、6を5に、...と移動してビット0をキャリーに移動します。
このとき、キャリーは共通なので、このビットを通してデータをコピーしていくという作戦です。
画面用のデータのビット7だけをAレジスタに集めます。
プログラムのキモの部分はこうです。
LD B,008H LD020H: RLC (HL) RRA INC HL INC HL DJNZ LD020H CALL 03ED4H
DJNZ LD020H は Bレジスタから1を引き、その結果が0でなければLD020Hへジャンプします。そのために最初にBに8を入れています。つまり1バイト分繰り返すという事です。
RLC (HL) と RRA の組み合わせで (HL)のビット7をAのビット7に押し込みます。
HLの値を2回+1して、次のアドレスに進みます。INC HL が2回なのは1行に2バイト使っているからです。
次の RLC (HL) と RRA でAレジスタには、2つ目のビットがビット7から押し込まれます。
こうして8ビットが押し込まれると、1バイトのデータが揃います。そのときDNJZで繰り返しを抜けます。
次のCALL 03ED4H はN88-BASIC ROM内のルーチンを呼び出します。これはAレジスタの1バイトをプリンタへ送信するというものです。
これで上記のプログラムはおしまいですが、この時、メモリ内のデータはビット6がビット7の位置に移動していますから、繰り返すことで次のプリンタ用の1バイトを得ることができます。
繰り返しになりますが、上の説明にあった繰り返しの一場面を図にしてみます。
5つ目の(HL)の第7ビットをRLC(HL)し、次に5回目のRRAをする直前の様子。左が縦に書かれたAレジスタ、真中cがキャリービット、右がメモリ上のデータです。黄色の背景のバイトのアドレスがHLの初期値で、図の状態では+8になっています。四角の中のabcdefghは0または1のどちらかというビットデータです。
これを8回繰り返したいところですが、ちょっと待ってください。
漢字は縦に16ドットで、今は上半分が終わったところです。下半分が残っているのでこのまま、HLを増やしながらもう1回、行います。
今度はCレジスタ(キャリー(c)とは別)に2を入れておき、DEC C (Cを-1)したときにゼロにならない(NZ)ときには繰り返すというジャンプ命令 JR NZ を使って繰り返します。Cが2の時と1の時の2回行います。
前回のプログラムリストに付け加える形で書いておきます。
LD C,002H
LD01EH:
LD B,008H
LD020H:
RLC (HL)
RRA
INC HL
INC HL
DJNZ LD020H
CALL 03ED4H
DEC C
JR NZ,LD01EH
プリンタに2バイト送った後の状態です。紫色背景の部分が送り終わったデータです。
これをあと7回繰り返せば、左半分が終わりになります。
ビット6が最初のビット7の位置にありますから、繰り返せばいいのですが、HLの値を元に戻す必要があります。最初にDEレジスタに値を入れておき、HLにその都度コピーすることにします。PUSH,POPを使って行います。
もうひとつ。再び8回の繰り返しですが、DJNZを使いたくなります。これはBレジスタを使いますが、Z80にはレジスタがもう一セットあって切り替えることができます。ちょっと使ってみたくなりました。
もう一セットのレジスタを裏レジスタと言いますが、裏か表かは判別できず、現在表に出ているレジスタが使われるという仕様です。EXXコマンドは、BC,DE,HLレジスタを表裏交換します。LD B,008H をEXXで挟んで、裏のBレジスタに8を入れ、最後にDJNZ LD019H もEXXで挟んで裏のBレジスタの値で、DJNZ を動作させます。
やはり、前回のプログラムリストに付け加える形で書いておきます。
EXX
LD B,008H ;裏
LD019H:
EXX
PUSH DE
POP HL
LD C,002H
LD01EH:
LD B,008H
LD020H:
RLC (HL)
RRA
INC HL
INC HL
DJNZ LD020H
CALL 03ED4H
DEC C
JR NZ,LD01EH
EXX
DJNZ LD019H ;裏
EXX
これで漢字の左半分の出力が終わりました。
ここまでくれば、右半分は簡単です。2回の繰り返しですから、裏レジスタ群のCレジスタを使いましょう。
HLの値は、初期値をさらに+1しておかなければなりません。表のDEレジスタの値を+1することでクリアします。
ここでも、前回のプログラムリストに付け加える形で書いておきます。
EXX LD C,002H ;裏 LD017H: LD B,008H ;裏 LD019H: EXX PUSH DE POP HL LD C,002H LD01EH: LD B,008H LD020H: RLC (HL) RRA INC HL INC HL DJNZ LD020H CALL 03ED4H DEC C JR NZ,LD01EH EXX DJNZ LD019H ;裏 EXX INC DE EXX DEC C ;裏 JR NZ,LD017H ;裏 EXX
前回と同じですが、改めて書き直します。裏レジスタを使っている部分の色を変えました。
;プログラム開始時点でDEにデータの開始アドレスが入っていること EXX LD C,002H LD017H: LD B,008H LD019H: EXX PUSH DE POP HL LD C,002H LD01EH: LD B,008H LD020H: RLC (HL) RRA INC HL INC HL DJNZ LD020H CALL 03ED4H DEC C JR NZ,LD01EH EXX DJNZ LD019H EXX INC DE EXX DEC C JR NZ,LD017H EXX
思ったよりもコンパクトに収まりました。
機械語のプログラムはおかれた環境に合わせるものなので、汎用な形にしておくのは難しい気がします。
今回のプログラムも、N88-BASICのROMがなければ、プリンタに1バイト送るルーチンを用意するか、別のメモリ領域に書きだすようなことを考える必要があります。ただ、今の時代に16ビットのビットマップを90度回転するという必要があるとも思えませんので、この程度でいいでしょう。
これに、漢字ROMからデータを読み出す部分を加えます。この部分についてはアスキー出版から出ていた「PC-Techknow8800vol1.1」という本から情報を得ました。CALL 07261H を使いますが、見えているROMの領域をROM5に切り替えなければなりません。8ビットのPC-8801は64KBで足りずに領域を切り替えるという部分が随所にあります。それがOUT命令で処理されています。切り替え中に割り込みがかかると、面倒なので切り替え中は割り込みを禁止する措置もとっています。
この07261Hから戻ってくると、DEレジスタにフォントデータのアドレスが格納されています。最初の4バイトは縦横のビット数なのですが、とりあえず16✕16に対象を絞って読み飛ばします。これが 4つ連続の INC DE です。
; z80dasm 1.1.3 ; command line: z80dasm -g0xd000h -lz ../../m09/xx2 ORG 0D000H LD E,(HL) INC HL LD D,(HL) DI LD A,0FEH OUT (071H),A CALL 07261H LD A,0FFH OUT (071H),A EI INC DE INC DE INC DE INC DE EXX LD C,002H LD017H: LD B,008H LD019H: EXX LD C,002H PUSH DE POP HL LD01EH: LD B,008H LD020H: RLC (HL) RRA INC HL INC HL DJNZ LD020H CALL 03ED4H DEC C JR NZ,LD01EH EXX DJNZ LD019H EXX INC DE EXX DEC C JR NZ,LD017H EXX RET END
メモリへの格納状況です
D000 5E 23 56 F3 3E FE D3 71 CD 61 72 3E FF D3 71 FB D010 13 13 13 13 D9 0E 02 06 08 D9 0E 02 D5 E1 06 08 D020 CB 06 1F 23 23 10 F9 CD D4 3E 0D 20 F1 D9 10 E9 D030 D9 13 D9 0D 20 E1 D9 C9
これを呼び出すBASICプログラムも掲載しておきます。
29990 '*** YOKOGAKI KANJI 1983.06.28 *** 30000 DEFINT A=Z:DEF USR=&HD000:DTSP=2 30010 LPRINT CHR$(27);"P"; 30020 INPUT "JIS";KC$:IF KC$="" THEN 30070 30030 KC=VAL("&H"+KC$) 30040 LPRINT CHR$(27);"I";"00016"; 30050 DUM=USR(KC) 30060 GOSUB *DTSP 30065 GOTO 30020 30070 LPRINT CHR$(27);"H"; 30080 END 30090 *DTSP LPRINT CHR$(27);CHR$(DTSP);:RETURN
16進文字列として一文字のJISコードを入力するたびにプリンタに送り、2ドット分のスペースを入れています。ただ最後がすべて";"で、改行を入れていないので、プログラム終了まで実際には印刷されなかったのではないかと思います。
プログラムを終了した後、ひょっとしたらダイレクトモードのLPRINT命令で1行まとめて印刷された可能性があります。
機械語呼び出しの要点は、
(1) DEF USR=開始アドレス で定義する。
(2) DUM=USR(引数); 残された資料からすると引数はポインタがHLレジスタに格納されて呼び出される模様。戻り値はない。
(3) 機械語ルーチン本体をD000H以下に格納するのは別途BLOADでやっていた模様。ここには書かれていない。
その他の解読ポイントは、
LPRINT CHR$(27);"P"; はプロポーショナルモード
LPRINT CHR$(27);"H"; はHDパイカモード
LPRINT CHR$(27);CHR$(DTSP); はDTSPドットだけヘッドを進める。DTSPは0から8まで。プロポーショナルモードの時有効。
資料は残っていないのですが、あらかじめ用意された漢字の文字列を1行まとめて印刷するプログラムを試作しています。データの変換は速いものの、プリンタとのデータのやり取りが遅く、1行書くたびにじっと待たされるので、開発が止まりました。
この後、程なくして24✕24ドットのプリンタが発売され、本体も9801シリーズに乗り換えたので、このプロジェクトはここで終わりです。
PC-9801シリーズに乗り換えたのは9801VMの時ですから、1985年ころです。