PC-6001: August 2008アーカイブ

とうとう、mkIIを落札してしまいました...
実際に入手できるのは、次回の日本出張時なので9月中旬になりますが.

なんだか深みにはまっていくような...これでよかったのだろうか....

いま、PC-6001のPSGでPCM再生を試みています。
全体的にかなりいい感じで進んではいるのですが、クロック計算がなかなか合わないというのが困りものです。

カタログ上のPC-6001のクロック周波数は3,993,600Hzです。半端に見えますが、因数分解すると
3993600 = 2^12 * 5^2 * 3 * 13
となります。カセット入出力やRS-232Cなどで作られている1,200Hzの2の累乗倍で考えると、
3993600 = 1200 * 256 * 13
となり、4MHzのZ80のスペックに従いつつかなり切りのいい数字なんですね。

さて、PCM再生には正確なタイミングが不可欠です。これまでの実験で、とりあえずCD音質の44,1KHzは不可能ではないのですが、S/N比が悪くバランスが悪いことが判りました。なにせPSGの音量コントロールだと、4bitでの再生になりますからね。

その1/4の11.025KHzだと、音量に12bitを割くことができ、音質的にもかなりいい感じです。
ああ、この辺は説明してませんね。PSGを3チャンネル使うことにより可能になるんですが、私も理解が追いついていないのでこれはまた後ほどということで。

とりあえずこれで実験を続けてみました。計算だと、

3993600/11025 ~= 362.23

となります。まぁこの際小数点以下は無視するとして、3チャンネルのPSG音量をセットアップするのに362T(-state)かけられる、ということです。もちろん、外部影響を無視するため、VDGからのDMAはoffにし、割り込みも止めています。

ところが、これで再生すると、遅いんです。当然音程も低いんです。そりゃもう、一青窈が平井堅になるほどに(嘘)。

処理には余裕があるので、16Tほど削減してみました。すなわち346Tです。すると、まぁいい感じです。あ、今回はM1サイクルのT-stateをちゃんとカウントしています。
でも、完璧じゃないと言うか、やっぱり少しはずれるんです。大体、削減した16Tというのも適当に削減するウェイト用の命令を選んだだけで、なんら根拠はありません。
.......これは何かがあるに違いない、とは思うのですが、確実な原因はわからないのです。

いくつか気になることは以下のとおりです。
  • CPUの周波数が3993600Hzなのに対し、PSG(AY-3-8910)の周波数は3578545Hzです。そしてこれはMSXのCPU周波数と同じです。もっともPSGにクロックいっぱいいっぱいで出力を与えられるわけはないのですが。
  • 仮に周波数をPSGの持つ3578545Hzとして計算すると、362Tで出る周波数は約9885Hz、修正後の346Tで出る周波数は10343Hzです。
  • MSX版でPSG再生するプログラムを参考にしたのですが、そこでは
    out (c), r
    にかかるステートがなぜか15となっていました(M1 wait込み)。Z80の仕様上は12です。M1ウェイトを入れても13です。
    そして、そのMSXのCPU周波数はPC-6001のPSGと同じ、3578545Hzなのです。
どなたか教えて、Z80 Geekな人!

#注記
上記の記事を書いているときは、実は再生時にdiを忘れると言う大ポカをやってました。なので、そのときのタイミングはかなり当てになりませんでした。ただし、割り込みを禁止して実験してみたところ、やはり計算どおりには行かず、平井堅です。どうしたものでしょうね??

プリンタポートの場合

カードリーダをプリンタポートに接続する場合のコードを考えてみました。昔のパソコンの大部分は入力がBUSY信号のみですが、幸いなことにSPIで必要とする入力信号は1本なので、これで間に合います。

PC-6001のプリンタ関連のポートは以下のとおりです。
	0x91	data output
	0xc0	busy(bit1)
  
なお、回路では出力の際、インバータを通しているので、ビットは反転出力する必要があります。

では、早速コードを見てみましょう。例により、レジスタEを出力としています。
-- 
	ld	d, #0xff
_main:
	ld	a, d
	out	(#0x91), a
	dec	a
	out	(#0x91), a
	in	a, (#0xc0)
	add	a, d
	rl	e
	; _mainからを8回繰り返す
	ret
-- 
  
ここではハードウェアの設定として、以下を仮定しています。
  • CLKはビット0とします。
  • DIはビット1~7のどこでもかまいません。
ここでのトリックは、

	add	a, d
です。ポート0xc0からの入力は、ビット1以外は常に0なので、これで入力がある(a=0x02)場合は0xffを足すとキャリーが立ちます。しかも、レジスタDを使いまわせるので、利用するレジスタがA, F, D, Eの4つのみです。

結構これは重要で、このルーチンの外側でループを使ってバッファに書き込みを行うので、

	ld	(hl), e
  
のようにバッファへのポインタ(この場合HL)とループカウンタを利用します。ジョイスティックの場合はバイト入力でAFDEHLを使ってしまい、残りのレジスタは2つ。ループカウンタ用のレジスタをBにしてdjnzでまわしても、ぎりぎりレジスタが足りないので、実は裏レジスタを利用しています。惜しい...。

プリンタポートだとその必要がないのでexxと裏表のレジスタ転送の分が浮きます。バイトごとにこの処理が必要なので、1セクタあたり12T*512回分。合計6,144Tで、意外に馬鹿になりません。

もし、SDカードからの入力信号が1のときにポート0xc0の値が0になるようであれば、上記の代わりに

	ld	c, #0x02
を準備しておいて、

	sub	c
とするとよいでしょう。

T-stateは1bitあたり53、1バイトで441となります。裏レジスタを利用しなくて済む分も考慮して、1セクタあたりの時間は約260,000T、期待できる転送レートは3kB/s(VDG on)または6kB/s(VDG off)となります。前回よりさらに1.5倍のスピードになりますね。

なお、これは現在のSDカードアダプタを、ケーブル配線のみ変更すれば適用可能です。ただ、テストはしていませんのでご注意ください。

高速読み出しの工夫
第1回で説明したように、ジョイスティックポートからのデータ入力はかなり手間がかかります。

高速化は最もたくさん実行する部分から行うのが定石なので、心臓部であるバイト入力をする部分を最適化するのが最も効果的です。
(ポート0xa0)<-0x0f	レジスタ0x0fをラッチ
(ポート0xa1)<-0x10	DIを書き込み
(ポート0xa1)<-0x11	クロックを反転
(ポート0xa0)<-0x0e	レジスタ0x0eをラッチ
A<-(ポート0xa2)	データ読みこみ、ビット0にデータが入る
    
これで1ビット。1バイト入力するにも8倍かかります。これを限界まで高速にする挑戦をします。

最初のコード

まず、Z80の入出力は

in	a, (n)
out	(n), a
    
がそれぞれ11 T-state(以下11Tと表記します)で最速ですので、これを使います。
同じポートから連続してバイト列を読み取りたい場合などは
ini
inir
outi
otir
でブロック転送するのがいいのですが、ここでは残念ながら適しません。
入力結果はレジスタeに入れると仮定します。
-- 
	ld	e, #0x00	; 結果
	ld	b, #0x08	; ループカウンタ
_loop:
	sla	e
	ld	a, #0x0f
	out	(#0xa0), a
	ld	a, #0x10
	out	(#0xa1), a
	ld	a, #0x11
	out	(#0xa1), a
	ld	a, #0x0e
	out	(#0xa0), a
	in	a, (#0xa2)
	and	a, #0x01
	jr	nz, _zero
	set	0, e
_zero:
	djnz	_loop
	ret
-- 
  
最初は上記のような感じでした。実際には入力ポートの判別を動的にやっていたので、もっと複雑だったのですが。
上記のコードのT-stateを計算すると、1bitあたり123T~126Tで、1バイトあたり1,003T~1,027Tとなります。

PC-6001のM1サイクルにはウェイトがあるらしいです。
海外のMSX関連の資料(たとえばこことか)を見たところ、やはりM1に1サイクル追加されるようです。よって、すべてのオペレーションのT-stateはZ80の仕様のドキュメントにあるT-stateから1増加させた数値になります。おそらくPC-6001も同じだと思われます。
ただし、この記事の中の数値はM1ウェイトを入れていない数値です。
これにセクタリード用のコマンド送信やらループ管理やらを入れて、1セクタ(512バイト)のリードにかかる時間はおよそ580,000T、実効速度2MHzとすると290msecということになります。実際にはさらにFATの管理コードの実行時間があるので、もっとかかります。

最適化

ここで、時間が短縮できるものがあります。それが以下の3種類です。
  1. Aレジスタのセットアップ
    上記のソースでは、いちいちAレジスタに即値代入をしていましたが、これは7Tかかります。あらかじめ別のレジスタに値を準備しておけばレジスタ間転送で4Tで済みます。
  2. Eレジスタの値の代入
    最初はandを使ってゼロフラグ経由で入力を判断していました。しかしうまいことに、入力がアキュームレータの第0ビットに入ってくるので、4Tのrrca一発でキャリーフラグに結果を追い出せます。また、8回シフトするのでEレジスタへ最初の0代入は実は不要です。
  3. ループ
    ループを展開すればサイズは大きくなりますが、djnzの分の時間は浮きますね。
というわけで、次のようなコードになりました。
-- 
	ld	hl, #0x0e0f
	ld	d, #0x10
_main:
	ld	a, l
	out	(#0xa0), a
	ld	a, d
	out	(#0xa1), a
	inc	a
 	out	(#0xa1), a
	ld	a, h
	out	(#0xa0), a
	in	a, (#0xa2)
	rrca
	rl	e
	; _mainからを8回繰り返す
	ret
-- 
  
inでデータを読み取ったあと、rrcaでビット0をキャリーに追い出して、それをeのビット0から入れています。
これでT-stateは1bitあたり83、1バイトあたり691と、最初のコードの70%ほどになります。さらに外部でも工夫して、1セクタあたり約390,000T、2MHzで200msec以下となりました。

計算上ではなくて実測してみると、およそ2kB/s出ているので、そちらから考えれば1セクタ250msです。FAT管理などのオーバヘッドを考えるとおよそ妥当なところでしょう。

なお、よく知られているようにVDGからのBUSREQを止めると、画面は乱れますが速度は約2倍、約4kB/sとなります。

ちなみに、バイト出力はもっと高速です。inする必要がなく、レジスタの再ラッチが不要なためです。

もしプリンタポートで入出力したとすれば、やはり同じ理由で入出力が少し高速になるでしょう。ただし、プリンタポートには電源が来ていないので、ジョイスティックポートよりも物理的な接続が面倒になります。

これがバイト入力に関して私が考え付いた限界です。もし上記でもっと最適化、高速化できる方法があったら教えてください。