Z80マイコンボードの製作

2015年夏に偶然Z80(正確にはTMPZ84C00)を見つけた事をきっかけに、今更ながらCP/Mマシンを製作することにしました。とりあえず試作回路で上手く行きそうな感触がつかめたので、試作回路にあった無駄な部分を省くと共にデータ通信の高速化を目指した基板を新しく作成しました。基板の設計データはGitHub上に公開しています。

CP/Mマシンに必要な仕様

CP/Mを動かすために必要な仕様は次のとおりです。

  • CPUはi8080とマシン語レベルで互換性があること。
  • メモリはO番地から最低20KiBのRAMがあること。
  • 入出力のためのコンソール端末があること。
  • 最低一台のディスクドライブがあること。
  • 割り込み機能は使用していない。

ちなみにCP/Mが全盛期だった頃のクロック周波数は数MHzと現在の1/1000という世界でした。

以下に各項目を具体的に検討していきます。

CPU

CP/Mが全盛時代にはi8080よりもZ80が一般的に使われていました。よってZ80はi8080とのマシン語レベルでの互換性に問題がないことは確かです。

今回はZ80と言っても現在手に入るZ80の後継チップであるZ84C00と通信機能などを集積したZ8S180を使用します。これらもソフトウェア的にはZ80と互換性があるのでCP/Mを動かすことに問題はありません。

Z80の時代はクロック周波数は数MHzでしたが、Z84C00とZ8S180は20MHz版1が手に入るのでより高速なCP/Mマシンを実現できそうです。

メモリ

必要なメモリー容量は今となっては誤差のような容量です。むしろ少容量すぎて入手できない可能性さえあります。しかし現在Z80の後継CPUが入手可能なように小容量なメモリーも種類が少ないながらもちゃんと販売されています。

秋月電子のページを見ると、小容量とは言っても昔とは比べられ物にならないアクセス速度のSRAMが販売されています。容量についても128KiBで十分すぎます。

HM678127UHJ-12(12ns)とM68AF127B(55ns)のどちらを使うかですが、55nsはほとんど20MHzの1サイクルの相当にするのでこれで十分かと余り考えずにM68AF127B(55ns)を選択しました。

タイミングの検討

余り考えずに選んだメモリですが、M68AF127B(55ns)で最高どのくらいの周波数まで使用できるかデータシートのタイムチャートを確認してタイミングを検討してみました。

Z80のメモリアクセスで一番厳しいのは命令を読み込む時です。通常メモリーを読み込むときはT3の立ち下がりに読み込むのですが、命令を読み込む場合は半クロック早いT3の立ち上がりで取り込まれます。

命令読み込みのタイミングチャート。Z8400/Z84C00 Z80 CPU Product Specificationより引用。
命令読み込みのタイミングチャート。 Z8400/Z84C00 Z80 CPU Product Specificationより引用。

Z84C00とZ8S180を比べるとZ84C00の方が時間がかかるようなので、Z84C00のデータを使ってタイミングを検討します。

Z84C00のデータシートを見るとタイミングは次のようになっています。

  • アドレスの値が確定するのは、T1の立ち上がりから最大57ns。
  • MREQとRDがLになるのは、T1の立ち下がりから最大40ns。
  • データは、T3の立ち上がりの最低12ns前に確定している必要がある。

またメモリのタイムチャートは次のようになっています。

  • tAVQV(アドレスの確定からデータの確定までの時間)は最大55ns。
  • tELQV(チップ選択からデータの確定までの時間)も最大55ns。
  • tGLQV(出力ゲートが開いてからデータが確定するまでの時間)は最大25ns。
メモリーのタイミングチャート。M68AF127Bのデータシートより引用。
メモリーのタイミングチャート。 M68AF127Bのデータシートより引用。

よって確定が一番遅いMREQとRDがLになってからT3が立ち上がるまでの時間は、次の式になります。

40ns(T1の立ち下がりからMREQがL)+ 55ns(EがLからデータが確定) + 12ns(T3立ち上がり前の余裕) = 1.5T(wait無し)

この等式を計算するとT = 71nsとなります。よって最大の周波数は約14MHzとなります。

また同様にZ8S180の場合を計算すると

25ns(T1の立ち下がりからMREQがL)+ 55ns(EがLからデータが確定) + 10ns(T3立ち上がり前の余裕) = 1.5T(wait無し)

となり、T = 60nsとなります。よって最大周波数は16.6MHzとなります。

リセット直後のプログラム

Z80はリセットした時に0番地からプログラムをスタートします。そのためメモリの最初の方はシステムの初期設定やCP/Mをディスクから読み込むプログラムがリセット前に書き込まれている必要があります。そのため一般的にはその領域に電源を切っても内容が消えないROMが配置され、CP/Mが起動する時にはRAMに置き換えるようになっていました。

現在も紫外線で消去できるUV-ROMは手に入ります。しかしそれを使うには、書き込みと消去の装置が必要になります。さらにアクセス速度が遅いのでwait回路が必要になり、RAMとの切り替え回路も必要となります。

そこでメモリは全てRAMにし、リセット直後に必要なプログラムはスイッチを操作してメモリにマシン語を一ステップずつ書き込むというより古い手法を使うことにしました。もっともこれは面倒なので、一ステップずつメモリに書き込む操作は現代的なマイコンに肩代わりしてもらいます。

コントローラ

リセット直後に必要なプログラムの書き込みには使い慣れているAtmelのAVRマイコンを使用します。Arduinoに使われているATmega328を考えていたのですが、これだとピン数が足りずにアドレスなどの値をラッチしておく回路が必要になります。そこで少し値段は高くなりますがアドレスとデータバスを一対一で割り当てられるATmega64aを使用することにしました。

このコントローラは、初期プログラムの書き込みだけでなく、Z80の起動後は次のように端末やディスクドライブのインタフェイスとしても機能します。

ATmega64aはクロック回路を内蔵していますが、少しでも早いほうが良いかと思いZ80に供給するクロックをATmega64でも使用できるようにしました。この場合最大周波数がATmega64aの最大周波数16MHzに制限されます。

ATmega64aのハマりポイント

ATmega64aは、買ってきたままだとATmega64aではありません。

ATmega103というMMUとの互換モードになっています。mainルーチン内だけで済むプログラムだと気が付かないのですが、別の関数を呼び出すとフリーズしてしまい「あれ?」となります。

ATmega64aを使う前には、まずfuse設定を変更してATmega103互換モードをOFF(unprogrammed)にします。

またPFを使うときにも注意が必要です。

ATmega64aはJTAGインタフェイスを持っていますが、そのインタフェイスがPFに割り当てられており、入出力端子としてPFを使用できません。

これも必要ならばfuse設定を変更してOFF(unprogrammed)にするか、プログラム内でMCUCSRのJTDをセットします。MCUCSRのJTDは、誤動作で書き換えてしまうことを防ぐために、二回連続してセットする必要があります2

入出力

Z80の入出力もATmega64aが担います。メモリ上に実行したい入出力操作を書き込んでおき、準備が整ったところでATmega64aに実行を依頼します。

ATmega64aに入出力を要求するシグナルを送る簡単な出力回路も検討しましたが、Z80のHALT信号をATmega64aへのシグナルに使う回路3が試作で上手く行ったので外付け部品が不要なこの方法を使うことにしました。

具体的にはATmega64aに実行してもらいたい入出力操作をメモリに書いておきhalt命令を実行してHALT信号をLにします。ATmega64はHALT信号がLになったことを確認したらメモリ上に書かれた入出力操作を実行して、Z80を割り込みでhalt命令の次から実行を再開させます。RESETでもHalt状態から抜けられますが、通常のRESETと区別する必要や再開後の設定など面倒なので割り込みシグナルを使用することにしました。特にNMIは間違って割り込みを無効にしてしまう心配が無く好都合です。

ただしこのNMIを使ってHalt状態から抜けだすのは、NMIの割り込みベクタがCP/Mの予約領域にあるという問題点があります。そこでhaltを実行する前に割り込みベクタである0x66番地のデータを退避しておき、NMIでHaltから抜けだした後に書き戻すことにしました。またCP/Mでは他の割り込みを使用しませんが、一応入出力操作が完了するまで待つビジーウエイトを入れておきます。

実際のルーチンは次のようになります。

viotrap0:
	ld	hl,(NMI_VECT)	; save value on NMI_VECT
	ld	bc,045edh	; set RETN code
	ld	(NMI_VECT),bc
	halt
viowait:
	ld	a,(virtio_command)
	or	a
	jr	NZ,viowait
	ld	(NMI_VECT),hl

LED

電子工作で動作確認の基本はLチカなので、Z80でLチカを出来るようにするためLEDを点灯できるようにします。

ディスクドライブ

ディスクドライブには、実物のフロッピーディスクドライブやSDメモリカードを使用する方法を考えましたが、データのやり取りが面倒そうなのでパソコン上のディスクイメージに直接アクセスすることにしました。これによりパソコン上で作成したバイナリやテキストファイルをディスクイメージにコピーするだけでZ80側から使用することができるようになります。

実際のディスクイメージへのアクセスは、Z80からATmega64aにディスクの読み書きを依頼し、ATmega64aがパソコン上のプログラムにディスクイメージのデータを読み書きする命令を送ります。

パソコンとATmega64aは、電源供給も兼ねてUSBで接続します。ただしATmega64aはUSBに直接接続できないので、FTDIのFT240XというUSB-FIFO変換チプを使用しました。FT240XはZ80のデータバスに直接接続できるので、データを直接メモリに書き込むことができ最高1Mbyte/secのデータ転送が可能です4

FT240Xは、ATmega64aから見ると8bitパラレルのFIFOデバイスですが、パソコンからはシリアル端末に接続したように見えます。そのためパソコン側のプログラムには特別なライブラリは不要です。例えばターミナルソフトを開いてAという文字を送ると、そのままTF240XのデータバスにAのASCIIコードが現れます。

コンソール端末

コンソール端末は簡略化のため自前でキーボードやディスプレイを持たずにパソコンを昔のディスプレイターミナルとして使用します。この通信にもディスクドライブと同じUSBを使用します。

コンソール端末はATmega64aの要求で動くディスクドライブと違ってパソコン側から非同期でデータが送られます。そのためディスクでのデータと混じってしまわないように非常に簡単なプロトコールでディスクのデータとコンソールのデータを多重化します。

リセット

リセットスイッチはZ80に直接接続しないで、ATmega64aに接続したスイッチが押されたらZ80のリセット端子をLにすることにしました。これによりチャタリング回避やリセット時間を確保する回路をなくすことができます。

基板の設計と組み立て

そうして完成したZ84C00とZ8S180ボードの回路図は次のようになります。

Z84C00を使ったZ80ボードの回路図。
Z84C00を使ったZ80ボードの回路図。
Z8S180を使ったZ80ボードの回路図。
Z8S180を使ったZ80ボードの回路図。

基板はKiCadを使って設計し、プリント基板の製造は格安なElecrowに注文しました5

必要な部品は、秋月電子Mouser千石電商から購入しました。

回路図や基板の設計データはGitHub上に公開しています。これをフォークしてより素晴らしい物を作る基板としてもらえると幸いです。

Z84C00用のプリント基板

Z8S180用のプリント基板

プリンプ基板は10枚単位での注文なため何枚か基板が余っています。もし欲しいという方がおりましたら送料のみでお分けいたします。ただし基板制作後にも回路図と基板設計に手を入れているので、GitHub上のデータとは多少パターンなどが異なります。

次は

Z80ボードが完成したので、次はATmega64aのプログラムとCP/MのBIOSを作成してCP/Mを動かします。

脚注

  1. Z8S180は20MHz版の他に30MHz版も存在します。ただし値段が急に高くなるので今回は20MHz版を購入しました。

  2. JTDビットは、正確には4CPUサイクル以内に2回セットする必要があります。

  3. halt命令をトラップに使用するアイデアは我ながら冴えた方法だと思ったのですが、既にネット上に公開されていました。T:(@T_colon) 部品を減らす工夫 – neko Java Home Page – GMOとくとくbb

  4. 試作段階ではパソコンとの接続に一般的なUSB-シリアル変換チップを使用しましたが、これでもデータ転送がそれほど遅くはありませんでした。

  5. 数千円と約1週間(国際宅急便使用)でプリント基板を製造してもらうことができます。精度などでは国内のしっかりした業者には及ばないのかも知れませんが、個人的に使用するには全く問題がないレベルです。手配線や自分でプリント基板を作る手間や時間を考えれば、このサービスを使わない手はありません。