ジョイスティックポートでRS-232C送受信

更新情報

[2020/03/31] もう少し分かりやすい説明ページを用意しました / ジョイスティックポートでRS-232C受信 見直し版

前置き

PC-6001のジョイスティックポートは接続されたスティックの状態を読み取るだけではなく、汎用的な+5Vの入出力ポートとして利用することが出来ます。 ジョイスティックの接続口はD-SUB 9pinのコネクタで、PC-6001内部のAY3-8910に接続されています。 AY3-8910は音源ICですが、2つの8bit I/Oポートを持っています。2つのジョイスティックポートはAY3-8910に接続されているのですが、 単純に2つのD-SUBコネクタの信号線がAY3-9810につながっているのではありません。

このページでは、PC-6001のジョイスティックポートとPC等のシリアルポート(RS-232C)を接続する簡単な機器を製作し、 PC-6001に接続されたデータレコーダからテープを読み取りWindows PCに送るプログラムと、 Windows PCからデータを送り、それをPC-6001に接続されたデータレコーダに書き出すプログラムを作成してみます。

今回はPC-6001に接続する相手側のマシンとしてWindows PCを利用していますが、RS-232Cが使える機器であれば、Macintosh、UNIXマシン、MSX、X680x0等々も可能です。

PC-6001のジョイスティックポートについて

PC-6001のジョイスティックポートとAY3-8910までの回路はこのようになっています。[引用:I/O誌 1983年4月号]

ジョイスティック周りの回路図

ジョイスティックポートを扱うには、この回路を読み解いておく必要があります。74xx系ICの動きがわかれば、それほど複雑ではありません。詳細な説明は長くなるので省略しますが、 9pinコネクタのうちの1つはGND、1つは+5Vで残りの7つが入出力用途です。ただ、全てのピンが入出力として扱えるのではなく、ピンによってある程度の制限があります。

RS-232Cについて

RS-232Cについていまさら説明することは特にありませんし、ググった方がより正しく正確な情報にたどり着けると思います。 ここでは、今回の回路に必要な事だけを説明します。

RS-232Cは1本の信号線の電圧変化で1Bitの情報をやり取りします。 一般的にパソコンはBit単位ではなくByte単位でデータを扱いますから、Bit情報からByte情報への変換をしなければなりません。 また、CPUの動作速度に比べるとRS-232Cの通信速度は何百倍も遅く、CPUがRS-232Cを処理するのは無駄が多いので、 通常はRS-232C専用のLSIに処理を任せて、CPUはそのLSIから割り込みなどによってデータを受け取るのが一般的です。 Z80であれば、Z80 SIOや8251といった専用LSIがあります。 今回はそれらの専用LSIを使わずに、PC-6001のZ80だけで処理させます。Bit情報の受け取りやByte情報への変換もZ80が行います。

変換処理で問題になるのが、PC-6001がBit情報を受け取った後にByte情報へ変換する処理をしている間、相手側のPCは次のBit情報を送ろうとしてしまうという事です。 PC-6001がByte情報への変換作業や変換後のデータをメモリに保存している間、もっというと、PC-6001が画面表示処理などを行っている間は Bitデータを読み取ることができませんから、その間に信号線が変化してしまうのです。RS-232Cの仕様上では、相手が情報を受け取ったかどうかの確認をとるといった制御も可能なのですが、 PC-6001が他の作業をしている状態では、そのRS-232C上での確認信号のやり取りすら正しく行えません。

この問題を解決するために、PC-6001側から「今忙しいからデータを送らないで!」という信号を相手に伝えるようにします。 厳密には逆の考えで「今ならデータを受け取れるから送っていいよ」ということを相手側のPCに伝えるようにします。この信号はデータ信号線とは別の信号線を使います。 一般的に、この「今ならデータを受け取れるから送っていいよ」という処理はフロー制御と呼ばれ、今回はこれを信号線で処理します。 RS-232Cでは、Request To Send(RTS)と呼ばれる信号で、通信相手に対して送信可能である事を伝える出力信号です。

相手から送られてくるRTS信号を受け取るのがCTS信号線です。つまり、自分のRTSが相手のCTSに、相手のRTSは自分のCTSに接続されます。 自分のCTSが有効になったということは相手がデータを送ってもいいよ、と伝えてきているという事です。

もちろん、フロー制御はPC-6001側だけでなく、相手側のWindows PC側が受信する場合にも必要です。 つまり、WindowsPC側がデータを受け取れるか(=PC-6001からWindowsPCへデータを送信してもいいか)をPC-6001側で確認する必要があるのですが、 Windows PC側はRS-232Cに専用のLSIを使っていますし、PCの処理速度も十分すぎるほど高速でフロー制御をしなくてもよさそうなので、PC-6001からWindowsへデータを送る場合には フロー制御を行わないようにします。それだけ処理が簡単になります。

通信速度(転送速度)ですが、ざっくりと計算したところ、9600bps程度であればPC-6001のZ80の処理速度でも処理できそうです。 データレコーダの速度は1200bpsですので、RS-232C通信をしつつ、データレコーダの読み書きをするには充分でしょう。

回路の制作

制作する回路ですが、出来るだけパーツを減らして極力、PC-6001のソフトで制御するようにします。今時であればPICやAVRを使って、 RS-232C制御の全てを一つのチップで処理させることも可能ですが、PICやAVRの書き込み環境が必要になってしまいます。

また、8251を使った純正のRS-232Cボードと同等の回路を安価に作成する事は難しくはないのですが、ハードウェア制作の手間を出来るだけ減らす方向で進めます。

RS-232Cを採用した理由は、動作原理が単純なのと、PC-6001の向かい側(Windows PC)のマシンのプログラムを作成せずに、通信ソフトを使って データのやり取りを済ませられるからです。ハードウェアの単純さを求めるのであれば、FT232RというUSBハードウェア制御ICを使うと 非常に簡単にWindows側から+5VのI/Oを読み書きできるのですが、Windows側のソフトを作成しなければなります。Windows側のアプリをC#、C++、VBなどを使って作る手間と PC-6001でZ80のアセンブラを書く手間を考えたら・・・やはり、後者を取るべきでしょう。

ただ、最近のWindowsPCにはシリアルポートがないので、所有しているWindowsPCによってはUSB-シリアル変換器を用意しなければならなくなるかもしれません。

RS-232Cでは送受信の両方を行えますが、今回は1つのプログラムで1つの方向だけを扱うことにします。プログラムの一つはWindows PCからのデータを受け取って データレコーダに書き出すプログラム、もう一つはPC-6001に接続されたデータレコーダからテープを読み取り、Windows PCに送るというプログラムです。

制作する回路ですが、事前に説明した通り、PC-6001のジョイスティックポートは+5Vの入出力として使えるので、送信用(出力)と受信用(入力)に それぞれ1本の信号線を使います。また、フロー制御用に送受信それぞれ1本ずつ必要ですので、合計で送信(出力)に2本、受信(入力)の合計4本の信号線を使う事になります。 ただし、フロー制御の1つは前述の理由によりWindowsPC側のフロー制御を考慮しなくてもよさそうなので今回のプログラムでは使用しません。

PC-6001の信号線はTTLで0Vか+5Vなのですが、PC側シリアルポートは-12Vと+12Vで信号をやり取りするため、電圧の変換(レベル変換)が必要になります。 この変換は専用のICを用います。MAX232Cというのが昔からよく知られている変換ICで、これと互換のあるICがいくつか発売されています。MAX232CはICと数個のコンデンサの組み合わせで使用します。 電圧変換用ICの駆動には+5Vの電源が必要なので、ジョイスティックポートの+5VとGNDを利用します。

概念図

簡単概念図

PC-6001のジョイスティックポートはD-SUB 9pinですが、一点だけ注意しなければならない事があります。接続口をみるとわかるのですが、コネクタ部が 本体の奥まったところにあるので、電子パーツ店で売られている一般的なD-SUBコネクタではネジ止めの板があり、接続できないのです。少々心苦しいのですが、 MSXやメガドライブなどのジョイスティックやパッドを分解して利用するのがよいでしょう。

PC-6001ジョイスティックケーブル

RS-232C側のコネクタもD-SUB 9pinです。25pinタイプのものもありますが、コネクタの信号線さえあっていれば25pinだろうがなんだろうが問題ありません。

作りを簡単にするのであれば、RS-232Cケーブルの片側を切り取り、レベル変換回路に直接つなげてしまっても構いません。RS-232Cケーブルを安価に入手出来れば、 D-SUBコネクタ1つと配線材を減らすことが出来るので、その方が無駄がないと思います。 RS-232Cで注意しなければならない事は、接続口のオスメス形状です。また、RS-232Cケーブルにはストレートタイプとリバースタイプがあります。今回は ストレートタイプを使用します。一般的にPC同士を接続する時にはリバースケーブルを用います。片方のPCの出力をもう片方のPCの入力につなげなければ 通信できませんから、リバースタイプでなければならないのです。ですが、リバースタイプのケーブルにも色々と種類があり、その中にはフロー制御が出来ないものもあるようです。 ですので、変換回路で信号の出入り口を交換する事にして、ケーブルは普通のストレートケーブルを使用するようにします。

電圧レベルの変換ICはICとコンデンサの部品を個別に揃えてもいいですし、秋月電子などではレベル変換キットが販売されています。秋月電子のWebページで「RS232C」か 「ADM3202AN」を検索すると出てきます。ADM3202ANはMAX232Cと互換のあるレベル変換ICで、2010年現在、秋月電子では変換ICとコンデンサのセットで300円です。 秋月電子ではUSB-RS232C変換ケーブルも900円程度で販売していますので、Windows PCにシリアルポートが無いのあれば、一緒に注文するとよいでしょう。

[回路図]

回路図

[RS-232Cコネクタ番号]
ピン番号略称方向用途
1今回は未使用  
2RxDIn受信データ
3TxDOut送信データ
4今回は未使用  
5GND  
6今回は未使用  
7RTSOut送信要求
8CTSIn送信可能(一応接続しますが今回は未使用)
9今回は未使用  

[この回路でのPC-6001ジョイスティックポートの割り当て]
ピン番号略称方向用途
1 RxD In 受信データ
2 CTSIn 送信可能(一応接続しますが今回は未使用)
3 今回は未使用   
4今回は未使用  
5+5V  
6今回は未使用  
7RTSOut送信要求
8TxDOut送信データ
9GND  

[部品リスト]
部品名個数
MAX232C(互換ICでもOK)1
コンデンサ0.1μF5(*1)
D-Sub 9pinストレートケーブル1
D-Sub 9pin端子1(*2)
PC-6001にささるD-SUB 9pin端子1

*1 私がセット購入したMAX232C互換ICはコンデンサの耐圧が接続場所によって異なりました

*2 D-SUB 9pinストレートケーブルの片方を切って回路につなぐ場合は不要

製作上の注意点は、D-SUBコネクタの端子番号の位置です。コネクタ接続側なのか半田側から見るのかで番号が反転しますから注意してください。 また、送受信信号とフロー制御信号は入れ替えて配線する必要がありますから、端子の用途と番号に注意してください。この回路では、Windows PC側のD-Sub端子部分で入出力を 入れ替えています。

最初に作った時はRTS信号を考えていなかったので、あとからワイヤーストラップでRTS/CTSを追加したのでした...

PC-6001のジョイスティックポート1に接続します。PC-6001シリーズは機種によってジョイスティックポートの接続口の左右が異なるので注意してください。写真左側はWindowsに接続するUSB-RS232C変換器です。

完成図

プログラムの制作

通信速度9600bps、パリティなし、スタートビットは1Bit、ストップビットも1Bit、データは8Bitとします。PC-6001は正確なタイマ割り込みがないので、9600bpsで 信号線を正確に変化させるためにZ80 CPUクロックと命令数から計算して測定します。PC-6001のCPUは約3.9936MHzで動作しています。周波数の逆数が時間になるので、 1/3.9936MHz = 約0.0000002504秒です。例えばOR A命令は4クロック命令なので、0.0000002504×4 = 0.0000010016秒ですから、OR命令を1回実行するのに 約0.0000010016秒かかることになります。実際はもう少し計算の桁を考慮しつつ、いちいち秒に換算せずに周波数のままで計算していきます。

ここでPC-6001のZ80アセンブラプログラミングで最も注意しなければならないことがあります。それは、PC-6001のZ80は各命令のM1ステートに1ClockのWAITが入るということです。 また、I/Oポートへのアクセス時にも1ClockのWAITが入ります。Z80の回路に触れることがないとM1という用語を聞くことがあまりないので簡単に説明します。

例えば先のOR A命令の実行には4Clock分の時間がかかると説明しました。これがなぜ4Clockなのかというと、Z80の命令実行には段階があり、大きく分けると

  1. 命令自体をメモリから読み込み解釈する
  2. 命令によってメモリから読み込んだりメモリに書き込んだりI/Oを変化させる

という動作をしています。この[1]の段階をM1ステートと呼びます。

OR A命令は1Byte命令で実行によりAFレジスタを変化させますがメモリには影響しないので[1]段階だけで終了する命令です。OR A命令を実行するのに 4Clockだけの時間を使うのですが、この4Clock分の動きを細かくみると、Z80はまず最初にメモリから命令自体を取り出さなければなりませんから、 当然、メモリにアクセスすることになります。Z80がメモリにアクセスする時は、Z80から接続されたメモリに対して読み出し番地を伝えると、メモリがその内容を Z80に伝えます。それをZ80が読み取り、その命令を実行するので、これらの処理に4Clockを要するのです。PC-6001の頃の時代のメモリは高価で低速でしたので、Z80の速度に メモリの応答速度がついてこれない事はよくありました。また、Z80は通常のメモリアクセス処理と比べると命令取り出し時のメモリアクセスが厳しく設計されているCPUでした。 そこで、Z80に低速なメモリを接続できるように、通常は4Clockかかるところを5Clockでアクセス...必要で あればいくらでもメモリアクセスを遅くすることが出来るようにZ80は作られています。Z80の信号線でWAIT信号というものがあり、これをを変化させる事によって メモリアクセスに余裕を持たせることが出来ます。通常は1Clockでメモリから読み取るところを2Clock,3Clock...というようにすることが出来るのです。

この理由によって、PC-6001では命令取り出し時には常に1Clock追加されるようになっています。Z80には2バイト命令がありますが、この場合、M1ステートが2つあることになり、 2Clockが追加される事になります。たかが1Clockとも思えますが、1ClockのWAITがかかったZ80が4命令を実行する間にWAITが入らないZ80は5命令を実行できるので、 短時間でたくさんの命令を実行できる分、大きな差になることがわかると思います。

また、今回は影響ありませんが、PC-6001ではROM領域へのメモリアクセス時にも1ClockのWAITが入ります。

安定した9600bpsを再現するためにはZ80の命令実行自体をシンプルに毎ビットの動作をできるだけ同じにしなければなりません。 スタート/ストップビットの検出を行っている事を考えれば、ちょっとぐらいの誤差は大丈夫ですが、 できるだけ処理速度が同じなるようにプログラムを組み立てています。9600bpsで10bit分だけ送る毎にByte→Bit変換を通信中に行うとシフトや条件ジャンプなどの処理を行わなければ ならなくなるので、あらかじめByteをBit情報にメモリに展開しています。

PC-6001で自作のプログラムを実行する際に外的な要因となるのは、割り込みとVDGによるメモリアクセスのブロックです。特に後者によって長時間、Z80がメモリにアクセス 出来なくなり、実質、Z80を停止させられてしまうので、強制的にVDGがメモリアクセス出来ないようにしておきます。これにより、画面が表示されなくなってしまいます。

また、今回はジョイスティックポートを使って通信しますが、ジョイスティックポート自体はAY3-8910に組み込まれていてBASICは音楽を演奏する際にAY3-8910のレジスタを 変化させるので、その影響をジョイスティックポートが受けるので注意が必要です。音楽を再生させなければ影響なさそうに思えるのですが、PC-6001のBASICでは キーボードを押すとクリック音が出るようになっていて、この時にBASIC+割り込みによってレジスタが変化しますから、キークリック音が出ないようにして置いた方が 良いでしょう。

受信プログラムの実行

Windows PC上の通信ソフトからバイナリデータをRS-232C経由で送信して、それをPC-6001で受信してからカセットテープに書き出すプログラムです。


	ORG	0DF00H

	LD	HL,(0DFA0H)

	; CMT SAVE INIT.
	CALL	1AB8H
MAIN_L:
	PUSH	HL

	CALL	AYSET
	CALL	RTSOFF

	; 割り込み禁止&BUSREQ停止
	DI
	LD	A,02H
	OUT	(93H),A

	; 受信待ち~受信処理
	CALL	RECV

	; 割り込み許可&BUSREQ許可
	LD	A,03H
	OUT	(93H),A
	EI

	; 受信データを組み立てる
	CALL	BULDDATA

	; CMT 1文字出力
	CALL	01ACCH

	POP	HL
	DEC	HL
	LD	A,H
	OR	L
	JR	NZ,MAIN_L

	;CMT SAVE STOP
	CALL	1B06H

	RET

	;----------------------------
	; 1Byte受信処理
	; I/Oはプルアップされていることに注意する
RECV:
	; 9bit(データ8bit + STOP1bit)受信するための場所と回数をあらかじめ準備
	LD	B,9
	LD	HL,RECVAREA

	; ここでRTS信号をアクティブにする
	CALL	RTSON

	; portAから入力する
	LD	A,0EH
	OUT	(0A0H),A

WAIT_BIT:
	; ひたすらスタートビットを待つ
	IN	A,(0A2H)
	; Bit0を調べる
	AND	01H
	JR	NZ,WAIT_BIT
	CALL	WAITHALF	; クロックの半分だけ待って中心で取得するように調整
LOOP1:
	CALL	WAIT		; 17CLK [18CLK]
	IN	A,(0A2H)	; 11CLK [13CLK]
	LD	(HL),A		; 7CLK [8CLK]
	INC	HL		; 6CLK [7CLK]

	DJNZ	LOOP1		; 13CLK [14CLK]

	; RTSをOFFにする
	CALL	RTSOFF

	RET

	;----------------------------
	; 受信データの組み立て

BULDDATA:
	LD	HL,RECVAREA
	LD	B,8

	XOR	A
LOOP2:
	RR	(HL)		; 最下位ビットをCYに
	RRA			; CYをAreg最上位Bitに
	INC	HL
	DJNZ	LOOP2

	; 反転しないでいいっぽい。通信データがすでに反転してるから?
	;CPL			; プルアップされてるので反転してる

	RET

	;-------------------------------
AYSET:
	; AY3-8910入出力設定
	; I/OportAを入力に
	; I/OportBを出力に
	LD	A,07H
	OUT	(0A0H),A	; REG7
	LD	A,080H
	OUT	(0A1H),A

	; 初期BIT状態(アイドル状態に->HIGH)
	LD	A,0FH
	OUT	(0A0H),A
	LD	A,7FH
	OUT	(0A1H),A
	RET

	;-------------------------------
	; WAIT ルーチン
	; CPU 3.9936MHzなので、3993600 / 9600 = 416clock
	; ここがCALLされた時点で60CLKを消費
	; M1ステートで+1Clockであることに注意

	; PUSH, POP, RETで57CLKを消費
WAIT:
	PUSH	AF		; 11CLK [12CLK]
	PUSH	BC		; 11CLK [12CLK]

	; ここのループで299CLKを消費すること
	LD	B,19		; 7CLK  [8CLK]
WAIT1:
	DJNZ	WAIT1		; B!=0 14CLK / B=0 9CLK::::残り291CLK.
        			; B!=0で282CLKを消費したいので19回のループ = 266CLK
	; 残り16CLK分
	OR	00H		; 7CLK [8CLK]
	OR	00H		; 7CLK [8CLK]

	POP	BC		; 10CLK [11CLK]
	POP	AF		; 10CLK [11CLK]
	RET			; 10CLK [11CLK]

	;-------------------------------
	; 半分の長さのWAIT ルーチン
	; 大雑把でいい

WAITHALF:
	PUSH	AF		; 11CLK [12CLK]
	PUSH	BC		; 11CLK [12CLK]
	LD	B,9		; 7CLK  [8CLK]
WAIT2:
	DJNZ	WAIT2		; B!=0 14CLK / B=0 9CLK

	POP	BC		; 10CLK [11CLK]
	POP	AF		; 10CLK [11CLK]
	RET			; 10CLK [11CLK]

	; RTS信号はBit3に割り当てられている。
	; ビット単位での状態変化は出来ないので、送信用TxDも変化してしまうことに注意
	; RTS ON, TxD 0 --> 00h
	; RTS ON, TxD 1 --> 77h
	; ここでTxDをLOWにしてしまうとSTART BITとみなされてしまい、向こう側の機器で
	; データの読み取りをしてしまうので、TxDは1にしておく事
RTSON:
	LD	A,0FH
	OUT	(0A0H),A
	LD	A,77H
	OUT	(0A1H),A
	RET

	; RTS OFF, TxD 0 --> 0000_1000
	; RTS OFF, TxD 1 --> 0111_1111
RTSOFF:
	LD	A,0FH
	OUT	(0A0H),A
	LD	A,7FH
	OUT	(0A1H),A
	RET

RECVAREA:
	DS	8

	END

説明が必要になりそうな箇所にはコメントを入れてあります。

信号レベルの判定ですが、上記の方法には少し問題があります。例えばスタートBitの検出は信号線がLOW状態になったタイミングを ループで常時監視しているのですが、それが本来の信号とは異なるノイズによるものだった場合、誤判定となります。本来であればハードウェア上でノイズ対策をするか、信号検出を1度ではなく 複数回にして判定するべきなのです。これはスタートBitに限らず、データ自体の判定でも同様です。今回は確率的に大丈夫だろうという素人プログラム的な考えで対応しないことにしました。

WindowsPC側から、どれだけのサイズのデータが転送されてくるのかを判定する方法がなかったので、ちょっとカッコワルイですが、あらかじめワークエリアに書いておく方法にしました。DFA0H~DFA1H番地に 16進数2バイトの値でデータサイズをあらかじめ書き込んでおきます。

キーボードを押下した時のクリック音はBASICのCONSOLE命令で消すことにしています。テープへの書き出しはBASIC ROMのルーチンをそのまま使用しているので、 テープ書き出しの初期化(1AB8H)、1バイト書出し(1ACCH)、終了(1B06H)のそれぞれのルーチンをCALLしているだけです。テープは常に走行しているデバイスですから、 書き込みが間に合わなくてもどんどん先に進んでしまいます。無駄な処理を実行している余裕はありません。

もう少し具体的な使い方を示しましょう。PC-6001ではROM&RAM拡張カートリッジを取り外してから、PAGE2で起動して、次のBASICプログラムを入力します。mkII以降の場合は、MODE1で起動してください。 (ROM&RAM拡張カートリッジを接続したままでもPAGE1にすれば大丈夫かも)


10 CONSOLE,,,0
20 CLEAR 300,&HDF00-1
30 FOR I=&HDF00 TO &HDF91
40 READ A$:POKE I,VAL("&H"+A$)
50 NEXT
60 POKE&HDFA0,&H00
70 POKE&HDFA1,&H00
80 END
110 DATA 2A,A0,DF,CD,B8,1A,E5,CD,59,DF,CD,89,DF,F3,3E,02
120 DATA D3,93,CD,2A,DF,3E,03,D3,93,FB,CD,4C,DF,CD,CC,1A
130 DATA E1,2B,7C,B5,20,E0,CD,06,1B,C9,06,09,21,92,DF,CD
140 DATA 80,DF,3E,0E,D3,A0,DB,A2,E6,01,20,FA,CD,77,DF,CD
150 DATA 6A,DF,DB,A2,77,23,10,F7,CD,89,DF,C9,21,92,DF,06
160 DATA 08,AF,CB,1E,1F,23,10,FA,C9,3E,07,D3,A0,3E,80,D3
170 DATA A1,3E,0F,D3,A0,3E,7F,D3,A1,C9,F5,C5,06,13,10,FE
180 DATA F6,00,F6,00,C1,F1,C9,F5,C5,06,09,10,FE,C1,F1,C9
190 DATA 3E,0F,D3,A0,3E,77,D3,A1,C9,3E,0F,D3,A0,3E,7F,D3
200 DATA A1,C9

プログラムを入力後、一応、テープに保存しておきましょう。DFA0HとDFA1番地に、これから転送されてくるファイルのサイズを16進数2バイトで書き込んでおきます。上位バイトと下位バイトが逆転することに注意してください。 例えば、Windows PC側から送ろうとするファイルのサイズが10000バイトの場合、10進数の10000を16進数に変換すると(アクセサリの電卓で変換できます)2710Hですから、10HをDFA0Hに、27HをDFA1Hに書き込みます。 具体的にはPOKE &HDFA0,&H10とPOKE &HDFA1,&H27をそれぞれ実行すればよく、上記BASICプログラムの60,70行目を書き換えます。(input命令から自動変換してくれるBASICプログラムを書けばいいのですが、 私がBASICをあまり知らないので...)

その後、RUNでプログラムを実行してOKが表示されたら、データレコーダーに空のテープを入れてREC状態にします。PC-6001とデータレコーダを3本の線でつないでいない、 つまり、黒のリモート端子を接続していない場合にはREC状態にしないでテープの準備だけしておきます。

Windows PC側では通信ソフトを起動します。XP以前であれば標準で通信ソフトのハイパーターミナルが付属していたのですが、Vista以降にはありませんし、 あまり機能性がよくないので、フリーソフトのacknowrichがオススメです。
acknowrich

メニューのファイル→シリアルデバイスを開く...から、COMポートを開きます。USB-RS232C変換器の場合でもCOMポートとしてデバイスが見えているはずですので、 WindowsのデバイスマネージャからCOMポートの番号を調べておきます。通信設定はウィンドウの右下部分の各項目をマウス右クリックすることで選択できますから、9600bps, Parity None, StopBit1.0, length 8bitに変更します。また、メニューの編集→バイナリモードを選択状態にします。

今回はハードウェアによるフロー制御を行うので、メニューの設定→デバイス設定から
フロー制御設定
通信設定を変更します。

基本的な設定はここまでで、あとは実際にWindows側からファイルを送信します。メニューのファイル→ファイル転送を選択して
ファイル転送
送信ボタンを押すとファイル選択のダイアログが開くのでファイルを選択すると送信待機状態になります。即座にファイルが転送されず待機状態になるのは、CTS信号がOFFだからです。PC-6001側でRTSをONにしないと、 送り手側は待機し続けます。

これで、PC-6001とWindowsPC側双方の準備が整いました。もし、PC-6001のデータレコーダに黒いケーブルのリモート端子を接続していない場合にはREC状態にしてください。 テープが回り始めるので、PC-6001側でEXEC&DF00を入力します。するとリレーのカチッという音と共に、Windows PC側の通信ソフトでCTSランプが点滅を始めます。 この時、PC-6001の画面もちらつきをし始めてデータレコーダーからいつもの音が聞こえますから、これで成功です。

送信プログラムの実行

PC-6001に接続されたテープを読み出し、それをWindows PC側に送り込むプログラムです。受信よりはシンプルです。


	ORG	0DF00H

MAIN:
	CALL	AYSET
	CALL	1A61H

ML_01:
	; CMT READ 1byte
	CALL	1A70H
	LD	B,A

	; 割り込み禁止
	; BUSREQ禁止
	DI				; F3
	LD	A,02H
	OUT	(93H),A

	LD	A,B

	CALL	SENDONE
	CALL	SENDSTRE

	LD	A,03H
	OUT	(93H),A
	EI				; FB

	JR	ML_01

	;-------------------------------
AYSET:
	; AY3-8910入出力設定
	; I/OportAを入力に
	; I/OportBを出力に
	LD	A,07H
	OUT	(0A0H),A		; REG7
	LD	A,080H
	OUT	(0A1H),A

	; 初期BIT状態(アイドル状態に->HIGH)
	LD	A,0FH
	OUT	(0A0H),A
	LD	A,7FH
	OUT	(0A1H),A

	RET

	;-------------------------------
	; 初期BIT状態(アイドル状態に->HIGH)
SENDSTRE:
	LD	A,0FH
	OUT	(0A0H),A
	LD	A,7FH
	OUT	(0A1H),A
	RET

	;-------------------------------
	; 1バイト送信(Areg)
SENDONE:
	LD	C,A

	; START BITの書き出し
	LD	HL,WORK
	LD	(HL),00H		; LOW SIGNAL
	INC	HL			; WORK+1

	; 書き出しデータのビット展開
	LD	B,08H
	LD	A,C
LOOP3:
	AND	01H
	OR	A
	JR	Z,LBL1
	LD	(HL),7FH
	JR	LBL0
LBL1:
	LD	(HL),00H
LBL0:
	INC	HL
	SRL	C
	LD	A,C
	DJNZ	LOOP3

	; STOP BIT
	LD	(HL),7FH		; HIGH SIGNAL

	; 1バイト分のデータをビット単位で送信する準備(START/STOPビットも含む)
	LD	B,10
	LD	HL,WORK
LOOP2:
	; データ送信; CLOCK数 [M1ステート+1WAITでのクロック数]
	; M1で+1, OUT命令はさらに+1される

	LD	A,0FH			; 7CLK [8CLK]
	OUT	(0A0H),A		; 11CLK [13CLK]
	LD	A,(HL)			; 7CLK [8CLK]
	OUT	(0A1H),A		; 11CLK [13CLK]
	INC	HL			; 6CLK [7CLK]

	CALL	WAIT			; 17CLK [18CLK]
	DJNZ	LOOP2			; 13CLK [14CLK]

	RET

	;-------------------------------
	; WAIT ルーチン
	; CPU 3.9936MHzなので、3993600 / 9600 = 416clock
	; ここがCALLされた時点で81CLKを消費
	; M1ステートで+1Clockであることに注意

	; PUSH, POP, RETで57CLKを消費
WAIT:
	PUSH	AF		; 11CLK [12CLK]
	PUSH	BC		; 11CLK [12CLK]

	; ここのループで278CLKを消費すること
	LD	B,18		; 7CLK  [8CLK]
WAIT1:
	DJNZ	WAIT1		; B!=0 14CLK / B=0 9CLK
				; 残り278CLK. B!=0で269CLKを消費したいので18回のループ = 252CLK
	; 残り17CLK分
	OR	00H		; 7CLK [8CLK]
	OR	00H		; 7CLK [8CLK]
	; 1CLK余る・・・

	POP	BC		; 10CLK [11CLK]
	POP	AF		; 10CLK [11CLK]
	RET			; 10CLK [11CLK]

	;-------------------------------
	; 送信の場合、この領域にI/Oポートに書き出す値を準備しておく
	; 受信の場合は、受信したデータをこの領域に書き出しておき、
	; あとで合成する
	; 最初の1バイトはSTART BIT
	; 最後の1バイトはSTOP BIT
WORK:
	DS	1
	DS	8
	DS	1

	END

具体的なプログラム例です。


10 CONSOLE,,,0
20 CLEAR 300,&HDF00-1
30 FOR I=&HDF00 TO &HDF73
40 READ A$:POKE I,VAL("&H"+A$)
50 NEXT
60 END
100 DATA CD,1D,DF,CD,61,1A,CD,70,1A,47,F3,3E,02,D3,93,78
110 DATA CD,37,DF,CD,2E,DF,3E,03,D3,93,FB,18,E9,3E,07,D3
120 DATA A0,3E,80,D3,A1,3E,0F,D3,A0,3E,7F,D3,A1,C9,3E,0F
130 DATA D3,A0,3E,7F,D3,A1,C9,4F,21,74,DF,36,00,23,06,08
140 DATA 79,E6,01,B7,28,04,36,7F,18,02,36,00,23,CB,39,79
150 DATA 10,EF,36,7F,06,0A,21,74,DF,3E,0F,D3,A0,7E,D3,A1
160 DATA 23,CD,67,DF,10,F3,C9,F5,C5,06,12,10,FE,F6,00,F6
170 DATA 00,C1,F1,C9

プログラムをRUNした後にEXEC&DF00を実行します。この状態でPC-6001はデータレコーダからの音を待機しています。データレコーダに何かテープを入れて再生すると読み取り、WindowsPC側に1バイト単位で送ります。

受信プログラムと比べるとRTS周りが手抜き、というかRTSを考慮する前に作ったプログラムなのでWindows PC側でCTSを不規則に受信してます。実害はないです、たぶん。

色々と試してみるとわかるのですが、テープの途中、つまり、プログラムの途中からは読み取りません。これはピーガッガーの最初の部分には必ず先頭判別用のマーカーデータ(マーカー音声)が埋め込まれていて、 それを読み取りの基点しているからです。このマーカー処理はデータレコーダからの音声データをバイトデータに変換しているSUB CPUの仕事なので、Z80は関与できませんし、通常は意識する必要がありません。 また、2つのプログラムが連続している場合、つまり、よくあるBASIC+マシン語構成のテープの場合は、間の空白部分は何も送られてきませんし、BASICプログラムの転送後にマシン語プログラムがちゃんと転送されてきます。 これもBASICプログラムの先頭部分だけでなく、マシン語部分の先頭部分にもマーカーが入っているからです。 読み込みの途中で停止した場合はSUB CPUが読み込みエラーとして内部的に処理しているようで、その後、再びテープを先頭から読み込ませればマーカーが検出される限りは転送されてきます。

テープ読み取りについて補足

市販ゲームを実機PC-6001とデータレコーダで読み取り、それをWindows側に転送するというのはテープをイメージ化する上でかなり理想的な方法です。実機で読み取るのですから、間違いがないのです。 ただ、今回のプログラムで実際に取り込んでみてエミュレータで使おうとすると、なかなか一筋縄ではいきません。 エミュレータに読み込ませても動かない可能性の方が遙かに高いです。この原因はいくつかあります。

まず、実機のデータレコーダとケーブルの劣化です。また、テープ自体の劣化もあります。これはもう、実機でも読み取れないのでイメージ化できなくて当然とも言えます。

次に問題となるのが、テープから送られてくる音の始めの部分です。ごくたまに、イメージ化されたファイルの先頭に本来はないはずの「ゴミ」データが出てくることがあります。 本来は無いはずのデータを読み取ってしまうのですから、それ以降、ゴミデータ分だけアドレスがずれてしまいます。これは実機でも起きえる現象ですが、 ちゃんとしたローダープログラムは、先頭にゴミが含まれていても回避できるように作られています。例えばBASICのCLOAD命令はテープから 送られてくるデータがD3Hかどうか判別してD3Hが10個並んでいたらBASICプログラムであると判定しています。D3の前にゴミデータが入っていても、ちゃんと読み捨てるのです。 市販のゲームによっては、マシン語部分の先頭に特定のデータや文字列を入れて置いて、それで判断しているものがあります。

テープ読み取り開始時と同じような現象が読み取りの終わりの部分でも起きます。ピーガッガ~~・・・ガッの最後のところで音が小さくなるのですが、 ここでノイズがデータとされてしまう事があります。これもプログラムの終端をちゃんと検出しているプログラムであれば問題ありません。例えばBASICであれば00Hが10個並んで送られてきたら終端と見なしますし、 市販のゲームなどでは、あらかじめ読み込みバイト数を決めているので余計なデータを読み込むことはないのです。テープの走行も最後の音が小さくなった箇所よりも少しあとのところで停止しますから、 次のプログラムをテープから読み取る時には影響を受けないのです。 エミュレータではテープイメージ内のデータを1バイト単位で読み取って処理していますから、終端マーカー以降のゴミデータを次の先頭データと見分けることができません。 終端のゴミデータが次のプログラムの先頭データとなってしまうのです。

では、p6datrecで変換した方が確実なのかというと難しいところです。市販ゲームをp6datrecで変換しても同様に本来はないはずのデータが含まれて変換される事があり、 実際は変換後のテープイメージを解析(逆アセンブル)してエミュレータで動くように修正しているのが実情です。

せっかくなので、今回のテストでT&Eのハイドライドのテープを読み取ってみましたので、問題箇所を特定して修正してみました。

Windows PC側に送られてきたデータをファイル化して保存し、バイナリエディタで開きます。

ハイドライドテープイメージ先頭

データの先頭に、FC 01という2バイトのデータがありますが、これが不要なデータです。なぜ不要なのかわかったのかというと、先にも説明したとおり、BASICのCLOADはD3Hが10個並んでいたらそれがBASICプログラムの開始であると判断します。 ハイドライドはまず最初にBASICのプログラム部をCLOADで読み取りますから、これで予測がつくのです。ただ、この不要なデータがあっても、CLOAD命令は読み捨ててしまうので影響がありません。問題は次です。

ハイドライドテープイメージ中間

39AHからの4バイトが不要なデータでした。これを探し当てるのは難しいです。まず、このテープイメージをエミュレータを起動してからCLOAD命令で読み込ませると39AHバイト分だけ読み取ったところで止まります。データをみても、39AHから 前の10個が00Hですから、ここがBASICプログラムの終端であるという事がわかります。この4バイトのゴミデータを気にせずにRUNすると、一見、問題なくマシン語データ部を読み込んでいるように思えるのですが、最後でエラーとなってしまいます。 この時点でテープイメージのファイルサイズとマシン語読み取り処理によって読み込まれたサイズとの差から予想がつきそうですが、テープイメージの最後、つまり、マシン語データ部の最後にもゴミデータがついていることもあるので、それだけでは 判断できないのです。

結果的には、BASICプログラム内のマシン語部を解析してマシン語ローダー処理から予測してゴミデータを取り除きました。P6datrecを使った場合でも、1バイトの00Hが入ってしまうので、やはりバイナリエディタで取り除かなければならない事に代わりはありませんから、 やはり、ある程度の解析と経験による調整となってしまいます。

ハイドライドの変換はもう少し簡単な解決方法があります。BASICローダー部分とマシン語データ部分を別々のファイルとしてWindows側で受信/保存すればよいようです。 つまり、BASIC部分の読み込みが終わったら一旦、データレコーダを停止してそこまでの受信データをファイルに保存します。すると、4バイトのゴミデータがBASICプログラム部の最後についたものだと気が付きます。 BASICローダーは00Hを10個検出した時点で止まりますから、それ以降は不要なのです。 もちろん、先の例のように、マシン語データ部の先頭にゴミデータがつくこともあるのですが、なぜかマシン語データ部を分離して受信/保存した時にはゴミがつきませんでした。 もしかして送信プログラムや今回制作した回路の不具合かな?という感もあるのですが、p6datrecを使った場合でも、プログラムが分離している場合は別々に変換して結合するのが鉄則ルールなので、 このルールに沿った方がよさそうです。

まとめ

送受信部分のプログラムを加工すればBASIC ROMの転送や、FDD読み書きなどの応用も可能だと思います。