初代PC-8801の漢字ROMデータの変換

何の話かというと

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)にコピーします。

RLC (HL) の動作

(HL)はHLレジスタの値がE010だった時にはE010のアドレスのデータ(1バイトです)が操作対象であるという事です。

RRA はそのキャリーをAレジスタのビット7に移動し、7を6に、6を5に、...と移動してビット0をキャリーに移動します。

RRA の動作

このとき、キャリーは共通なので、このビットを通してデータをコピーしていくという作戦です。

まずは1バイト

画面用のデータのビット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のどちらかというビットデータです。

5回目のRLCの後の図

2バイトで16列中の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回繰り返せば、左半分が終わりになります。

左側の8列に範囲を拡大

ビット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から呼び出す

これを呼び出す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年ころです。