LSI-C86 講座その3 (副題:21世紀に役立たない知識)

1から今のPC向けの BIOS ROM を作りたいという酔狂な人がいない限り、ほとんど役に立たない。

.data と .bss の順番

ROM image の転送部分を考えた結果、ソフトによって 40000h から a3fffh が ROM だったり RAM だったりするのでそれも全て RAM にすることにした。容量だけは余っている(並列動作の余裕はない)。

このため 00000h から a3ffffh まで RAM, a4000h-a7fffh を FPGA 内蔵 ROM か 8bit の flash memory を ROM とする。そもそもこの CPU は ffff0h がリセット直後のPCなので、後ろのアドレスを ROM, 頭のアドレスを RAM にするほうが普通なので 00000h を ROM から RAM にするのは対して問題ではないということに気づいた。

前回の通り .data には文字列のような read only (const) なデータを ROM に貼らないといけないので、 RAM と ROM のデータを同じ data segment 上でまたぐようにして、 RAM に .bss, ROM に .data を貼ろうとした。が、いろいろやってみても必ず .data -> .bss の順番になるみたいなので諦めた。

このため 00000h から 97fffh を RAM, 98000h から 9ffffh を ROM, a0000-a3fffh を RAM という変なまたぎ方になってしまった。

filesystem 取り込み

SD カードからデータを取るので IO port から filesystem にアクセスするようなコードを作る。毎度おなじみの petit fatfs を使った。ソースのコンパイルはそのままで通るが、 pff.c の中に32bit乗算と除算を行うコードがはいっていて、それらは _LMUL, _LDIVU が呼ばれることになる。これらは標準のライブラリに入っており (lib/s/doslib.lib)、リンク時にこれを明示すればライブラリから必要な部分だけリンクしてくれる。

ライブラリの説明をみると sprintf, memcpy, memset, memcmp, strncpy が OS なしでも使えるみたいなので結構便利かもしれない。strncpy はあるのに snprintf がないのがちょっと残念なところはある。

テスト向けのエミュレータ側は、こちらも毎度おなじみの MAME のソースをいじってやるのだが、 diskimage は chd とかでやろうかと思ったがこれは途中でやめた。というのも chd はエミュレータ側での大きいデータストレージの読み書きをサポートするもので、今回みたいに外部からディスクイメージの中のデータを更新する場合には chd は不便であるということ。

結局 fat のベタイメージを使うのだが、これの取得方法がわりと微妙だ。一番簡単な手段はその辺に転がっているメモリーカードを dd コマンドでひっぱり出す方法なんだが、手元のメモリーカードの最低容量が 2GB なのが普通だったりする。あまり大きい容量をすべてエミュレータに読み込ませるのは(気分的に)よろしくないのでいくつか探してみたが、OS 起動向けのフロッピーイメージ作成用(fat12)かバックアップ向けのGB単位(fat32)のものだけだった。

本当は8MBぐらい欲しかったのだが仕方がないので 1.44 MB のイメージを作ってファイルを追加できる fatimgen を使うことにした。
http://sourceforge.jp/projects/sfnet_fatimgen/releases/

1点だけ問題があって、offset 0x1fe のデータが 0xff, 0xff となっていて mount 時にエラーが出るので 0x55, 0xaa としておくこと。本気で使うならソースも公開されてるので自分で直してビルドすべきだろう。

データ転送

前回書いた far pointer を使うメモリ初期化は異様に遅いので書き直した。8086 には効率的に転送するストリング命令という Z80 でいう ldir みたいな、オペコードを見ても使うレジスタがさっぱりわからないやつがあるのでこれを使う(後半嫌味)。

	static const struct cleandata{
		uint16_t segment;
		uint16_t length;
	} LIST [] = {
		//{SETSEG(0x40000), 0x3f00 / 2}, //RAM
		{SETSEG(0xc0000), 0x0400 / 2}, //spritemap
		{SETSEG(0xc8000), 0x0c00 / 2}, //palette for sprite
		{SETSEG(0xcc000), 0x0c00 / 2}, //pallete for tile
		{SETSEG(0xd0000), 0x4000 / 2}, //tile #0 map 
		{SETSEG(0xd8000), 0x4000 / 2}, //tile #1 map
		//{SETSEG(0xe0000), 0x10000 / 2}, //z80 memory
		{NULL, 0}
	};

	const struct cleandata *d = LIST;

	port_write(0x02, 1 << 3); //sound cpu reset, video hide
	port_write(0x1a, 0x1bf);
	while(d->segment != NULL){
		_asm_fill(
			"mov es,ax\n"
			"mov ax,bx\n"
			"push di\n"
			//"mov di,0x0000\n"
			"xor di,di\n"
			"cld\n"
			"rep stosw\n"
			"pop di\n",
			d->segment /*ax*/, 0x5432/*bx*/, d->length /*cx*/
		);
		d++;
	}

stos[bw] は segment 内での memset なんだが、 word 単位でもできるので効率がよくかける。 一応バイトオーダー確認のために 0 ではなく 0x5432 をいれたらカラーパレットが偶然にもパステル調になったのでこのままにした。

LSI-C86 でのインラインアセンブラは通常の関数の呼び出し規約と同じ形になっていて、破壊してはいけない(push して使ってから pop で戻す)レジスタが書いてある。逆に push/pop がいらないレジスタが書いてなくて AX, BX, ES ということは覚えておくと便利。(誰が使うのか謎)

同様に movs[bw] 命令も作ったが大して変わらないので省略。下記 URL の文書を参考にさせていただいた。が、おそらく書いた人も有効に使われるとは思ってはいなかったと思う。
http://software.aufheben.info/kouza/beginner/kouza_asm.html

別件でアセンブラは定数を a000h と書くとエラーが出るので 0a000h と書かねばならぬかっこわるいやつがあるんだが、 0xa000 と書いたら通ったので16進数接尾辞はソースでは一切書かないことにした。

.data と .bss の割り振り

初期化していないグローバル変数は .bss に貼られるとしたのだが、どうもポインタや struct は初期化無しでも .data に貼られるようだ。 pff.c に struct のポインタがいて、邪魔だったので全ての関数で引数を渡すように直した。

今回は1つで簡単に直せたからいいものの、直せないとなるとちょっと難しくて、アセンブラソースで定義して .bss を明示することになるのだろうか。そうすると struct の容量指定が C ソースと連携がとれなくなるような...

リンカが出す map の中身も static な要素は出してくれないので、問題判別に static を外すのが不便。これは仕方ないいえば仕方ないのだが、 gcc 系だと static 関数は出してくれるのでちょっと不便。

stack と data segment

	uint16_t far *const tile = TILE0_OFFSET(12, 0x10);
	FATFS f;
	WORD read_byte;
	uint8_t buf[0x4000];
	int i, j;
	if(pf_mount(&f) != FR_OK){
		str_print("MOUNT ERROR", 25, tile);
		return;
	}
	if(pf_open(&f, "romimage") != FR_OK){
		str_print("OPEN ERROR", 25, tile);
		return;
	}

こんな感じに stack にでかいデータを作ったところ、SP 経由で BP を作ってデータ転送をしていてこけるようになった。どうもレジスタに収まりきらないローカルデータは stack 兼 data segment となるために、 SS と DS を同じ値にしたら直った。
というわけで RAM は厳しいですな。

このこけかたが盛大で前述の _LDIVU に初期化されてないデータ(エミュレータなので0)を渡されていて、 divide by zero が発生。この場合エミュだと PC が強制的に0000:0000 になった。

これらの問題分析にも手間がかかる。資料やツールの都合で 8086 のコードで記載しているものの、エミュレータは uPD70116 の互換命令(互換というかわざと言葉を換えたというほうが適切かも)でかかれるので分かりづらい。そもそもこれらの CPU の命令体系はよくしらんし。
画像はインラインアセンブラNEC 方言 でかかれてたもの。

今回の記述はわけてかいたが、全ての問題が複合して発生したので大変でしたとさ。