Z80でLチカ!

f:id:marlesan:20161104012238j:plain:w400
思ったより簡単に動いてくれました。手こずったのはICのGNDつなぎ忘れなどの凡ミスばかり。

Lチカプログラムはとてもシンプルです。

PIOAD:  equ     0
PIOAC:  equ     1

        org     0
;
;       PIO初期設定
        ld      a,11001111b ;ビットモードで動作
        out     PIOAC,a
        ld      a,00001111b ;出力4bit/入力4bit
        out     PIOAC,a
;
;       LEDチカチカ
        ld      a,0
        ld      b,0
BLINK:  inc     a
        out     PIOAD,a
DELAY:  inc     b
        jp      nz,DELAY
        jp      BLINK

これでPIOのAポート最上位ビットにLEDを繋ぐと秒間5回くらいの速度でチカチカします。せっかくなので計算してみましょうか。

000C: 3C       [ 4]     BLINK:  inc     a
000D: D300     [15]             out     PIOAD,a
000F: 04       [ 4]     DELAY:  inc     b
0010: C20F00   [14|14]          jp      nz,DELAY
0013: C30C00   [24]             jp      BLINK

Aレジスタが1回INCするのに15+14*256+10=3609クロック。この256倍が1回の明滅の周期で、923904クロック。これに1クロック=250nsをかけて秒に直すと、230976000/1000000000=0.230976秒。秒間約4.33回チカで体感に近いです。手持ちのテスターでLチカ部分の周波数を計ると…

f:id:marlesan:20161104015141j:plain:w400

バッチリですね!

今回、一番の壁はZ80PIOの扱いだと思ってたんですが、さすがファミリLSI。回路はCPUと直結、プログラムはたかだか4行の設定で何事もなく動いてくれました。次はRAMを組み込むのが順当でしょう。そこまでならギリギリ、ブレッドボードで組めそうです。

あ、Lチカの喜びで忘れてましたが、電源投入時の動作に難があったんでした。写真の回路ではリセットを何回かポチポチしないとCPUが動き始めないのです。さすがに無視できない問題なので、RAMに取り組む前に調査したいと思います。

UbuntuでZ80アセンブラを書く

選択肢は色々あるようですが、次のサイトの zasm というZ80専用アセンブラを使うことにしました。

オプションでCPUサイクル付きのリストが出せるのが面白そうです。導入方法は、Archiveから実行バイナリをダウンロードして動くならそれが一番簡単です。私の環境(Ubuntu 16.04.1 LTS 64bit)は zasm-4.0.15-Linux64.zip が問題なく使えそうですが、一応ビルドも試したので、手順をメモしておきます。

$ cd ~/workshed/gitprojects /* 適当な作業フォルダ */
$ git clone http://k1.spdns.de/Git/zasm-4.0.git
$ git clone http://k1.spdns.de/Git/Libraries.git
$ cd zasm-4.0
$ ln -nfs ../Libraries Libraries
$ cd Linux
$ make
$ sudo cp zasm /usr/local/bin

早速、簡単なプログラムをアセンブルしてみます。

        org     0000h
        ld      a,0
INC:    inc     a
        jp      nz,INC
LOOP:   jp      LOOP

これを test.asm として保存して…

$ zasm -uwy test.asm
assemble: 6 lines
time: 0.0202 sec.
zasm: no errors
$ 

問題なさそうです。test.lst と test.rom ができています。.rom が機械語バイナリで、これをROMに焼いてZ80に実行させます。使ったオプションの意味は次の通り(作者のオススメ組み合わせっぽい)

  • -u ... オブジェクトコード(機械語を16進数表記したもの)を .lst に含める
  • -w ... ラベルのリストを .lst に含める
  • -y ... CPUのクロックサイクルを .lst に含める

では test.lst がどうなっているか見てみましょう。

$ cat test.lst
                        ; --------------------------------------
                        ; zasm: assemble "test.asm"
                        ; date: 2016-11-03 03:26:01
                        ; --------------------------------------


0000:                           org     0000h
0000: 3E00     [ 7]             ld      a,0
0002: 3C       [ 4]     INC:    inc     a
0003: C20200   [14|14]          jp      nz,INC
0006: C30600   [10]     LOOP:   jp      LOOP


; +++ segments +++

#CODE :        start=0     len=9

; +++ global symbols +++

INC     = $0002 =      2          test.asm:3
LOOP    = $0006 =      6          test.asm:5


total time: 0.0199 sec.
no errors
$

例えば ld a,03E 00 という機械語アセンブルされています。間にある数字がその行の命令の実行時間(クロック数)のようです。これ、Z80を手動クロックで動かす実験をしたおかげでよく理解できます。まず、各命令は必ずM1サイクルから始まります。ここでオペコード(命令の種類を示す)をメモリから読み出して命令の実行準備を整えるのですが、基本4クロックかかります。その後、命令によって次のどれかのサイクルに進みます。

  • M1のうちに実行完了(5クロック以上かかることもある)
  • もう1回M1サイクル(Z80で追加された新しい命令)
  • メモリ・リード・サイクル(3クロック)
  • メモリ・ライト・サイクル(3クロック)
  • I/O・リード・サイクル(4クロック)
  • I/O・ライト・サイクル(4クロック)
    • メモリ・I/Oサイクルは必要数繰り返す

ld a,0 なら、3E で「アキュムレータ(Aレジスタ)に値をロードする」という命令を判別するのがM1サイクルで4クロック、ロードする値 00 をメモリ・リード・サイクルで取ってくるのに3クロック使って計7クロック。inc a3C だけで「アキュムレータの値を1増やす」と判別して実行するのでM1サイクルの4クロックで終了、という具合です。

jp LOOP ではジャンプ先アドレスを2バイトで指定するため、メモリ・リード・サイクルが2回必要です。つまり4+3+3=10クロック必要。残った jp nz,INC[14|14] は、分岐ジャンプでは分岐する時としない時でクロック数が変わる場合があるためこのような表記になっているのだと思いますが、14という数字自体が不思議です。手元の本でもググってみても jp nz,nn[10|10] となっています。なにか必要な設定が抜けてるのだろうか…。とりあえず、吐かれるバイナリ自体は間違ってないようなのでよしとしますか。この点も確認しときましょう。

$ od -t x1 test.rom
0000000 3e 00 3c c2 02 00 c3 06 00
0000011
$

オッケー!

というわけで、遂にZ80のシステムを実際に組んでいきます! まずは、CPU・ROM・PIOの最小構成を試す予定。

追記

jp nz,INC のクロックサイクルは [10|10] じゃないの?問題について。test.asm にちょっと手を加えてアセンブルしてみたら [14|14] の理由がわかりました。以下、リストファイルの抜粋

0000:                           org     0000h
0000: 3E00     [ 7]             ld      a,0
0002: 3C       [ 4]     INC:    inc     a
0003: 3C       [ 8]             inc     a
0004: 3C       [12]             inc     a
0005: 3C       [16]             inc     a
0006: 3C       [20]             inc     a
0007: 3C       [24]             inc     a
0008: 3C       [28]             inc     a
0009: 3C       [32]             inc     a
000A: C20200   [42|42]          jp      nz,INC
000D: C30D00   [10]     LOOP:   jp      LOOP

カッコ内の数字はその行の実行サイクルではなくて、ラベル間の実行サイクルを足し合わせながら出力される値のようです。

ROM(EN29F002)ライタ完成

RubyでPC側のソフトも作って、ROM書き込み環境が整いました!

64kBまでのバイナリファイルをROMに書き込めます。64kBというのはPC側ソフトを簡単に実装にするための制限(かつ、Z80で使うには十分な容量)で、ファームウェアの方は256kBフルに利用可能です。

使い方はこんな感じで。

$ ruby romwr.rb test64k.rom
requesting connection..
connected!
erasing the chip...done in 2.7771766820078483 sec.
programming the romfile...done in 8.397580000993912 sec.
reading back the romfile to verify...done in 8.40169953199802 sec.

SUCCEEDED!!

dumped the readback image to "_test64k.rom"
$ cmp test64k.rom _test64k.rom 
$ /* ROMから読み戻したバイナリと差分なし! */

MegaのI/Oでごり押ししたパラレル制御のおかげで、64kBフルに書き込んでもベリファイ含めて20秒で終わります(ごり押してる割りには遅いのかもしれないけど…)。ピンアサインは次の通り。

Mega2560 EN29F002 用途
22-29 A0-A7 アドレスバス
37-30 A8-A15 アドレスバス
41,40 A16,A17 アドレスバス
49-42※1 DQ0-DQ7 データバス(Arduino→ROM方向)
analog 8-15 DQ0-DQ7 データバス(ROM→Arduino方向)
analog 0 RESET リセット信号
21 CE チップセレクト信号
20 OE 読み出し信号
analog 2 OE@74HC541※2 バス制御信号
18 WE 書き込み信号

※1) 74HC541を通してROMのDQ0~DQ7に接続する
※2) OE(あるいはG)は2本あるので、両方に繋ぐか、片方はGNDに落とす

f:id:marlesan:20161031075937j:plain:w400
私の手元で完成したシールドはこんな感じです。aitendoのMega用プロトタイプシールドを利用しました(リセットボタンつきなのが嬉しい)。ジャンパー配線があるのは28ピン化したEN29F002も扱えるようにするためなので、素直に32ピンDIPに変換して使うだけなら上の表通りに直結すればOK。ちなみに28ピン化は頓挫しています。

f:id:marlesan:20161031075940j:plain:w400
配線面。せっかく揃えんだし、ということで多色ふんだんに使いました。実は信号乗り移り系のバグが取れてないんですが…(指で線を弄ってたら現象が消えた)。とりあえずICにパスコンくらいはつけようか。

以上、システムとして動く初めての自作物だったので、雑ですが解説してみました。次はZ80アセンブラの準備です。ようやくCPUをまともに動かせるイメージが湧いてきています。

秋葉原!

f:id:marlesan:20161031010522j:plain:w400

ドヤッ

初受験なりの感覚でも900点はないな、という手応えだったので、まぁ想定通りの実力でした。リスニングは精度、リーディングは速度が課題のようですね。改善して来年のいつかにまた受けたいと思います。ちなみに、会社の奨励金が満額もらえる点数に5点足りてません…。


さて先週、仕事で東京に行ったついでに秋葉原を散策できました。日ごろ通販でお世話になっている各店にお礼参りしてきましたよ。

f:id:marlesan:20161031010459j:plain:w400

全体的な感想として、人の多さに驚きです。あんなに熱量のある世界だとは思わなかった…。台北に全然負けてなくてちょっと嬉しさを感じました。秋月電子の狭い店舗を人、人、人がひっきりなしに循環している光景が特に印象的。

肝心の自分自身の買い物は、今回特に喫緊で必要なものがなかったので目に付いたものを適当に。ラジオデパート2階で最近大好物の600mil-DIPなICを見つけましたが、欲しいものはほとんどありませんでした。心が動いたのは32ピンDIPFlash-ROMとHD6840くらいで、前者は1500円という値段に躊躇してスルー、後者も900円と値が張ったので1個だけ購入。74HC181も見つからなかった…。ただ、通販だと探すのに苦労したUV-EPROMがどっさり置いてあったのはさすが。カードエッジのソケットなんかも通販でなかなか見かけませんが、ラジオデパートに来れば買えることがわかりました。

他、千石電商、マルツ本店、若松通商など回ってぼちぼち買い足し、最後に寄ったのがaitendo。

f:id:marlesan:20161031020727j:plain:w400

入り口がこんな感じで面食らいましたが、明らかに通っぽいおじさんが颯爽と扉をくぐっていったので、それに乗っかって常連風に入店成功。結局、ここで一番お金を使いました。今取り組んでいるROMライタ製作にぴったりなArduino Mega用プロトタイプシールドとか、32ピンのZIFソケット、PLCC引き抜き工具などなど。特にZIFソケットは種類豊富で異様に安い。それから、ICコーナーの棚の上に処分品としてUV-EPROMが@200円で大量に置いてありました(たぶん何かの基板から引っぺがしたもの)。ROMは不要なのでスルーして、同じコーナーからHD6301、HD6305というワンチップマイコンを購入。使うかは分かりません…。

そんなこんなで秋葉原電気街を堪能させていただきました。期待以上の世界だったので隙あらば立ち寄りたいです。

ROMライタのファームウェアひとまず完成

EN29F002用ROMライタ ファームウェア

ファームウェアのソースだけあってもしょうがないんですが、共有してみます。書き込み、読み込み、チップ全消去できるのは確認済み。

ROMに対する各操作シーケンスのタイミングがイメージしやすいよう、特に構造化もせずゴリゴリ書きました。単純なコマンドで最低限の動作をするように作ってあり、あとはPC側の書き込みソフトを目的に応じて作りこむ想定です。

コーディング上ハマった点など。

  • Arduinoのピンモードを動的に切り換えて双方向データバスにしたかったが上手く行かなかった。入力と出力のピンを分け、出力はROMの出力と衝突しないよう541で制御。
  • が、最後の瞬間まで541のつもりで540(541とは出力が反転する)を使っていた…動くわけねえよ! そもそも541のつもりで540を買っていたようなので根が深い。
  • ポートレジスタで入力を受ける場合、PINXを使う。PORTXで入出力いけるってどこかで読んだ気がするけど、ダメだった。
  • 書き込みはビットを1→0にする操作しかできないので、消去(ビットをすべて1にする)してから書き込む。
  • Serial.readStringUntil(char terminator);でコマンドのやり取りをするとき、受信データに terminator で指定した文字が現れるほか、タイムアウトでも受信を打ち切ってしまうのにしばらく気付かなかった。タイムアウトの場合は受信データをバッファして繋げて…というコードを書き出すとこの関数使う意味がないような気がしたので、Serial.setTimeout(-1);として回避(引数の型が unsigned long なので無制限の待ちになってるわけではないはず)。
  • Serial.readStringUntil(char terminator);で取得した文字列の最後に terminator は含まれない。
  • PC側ソフトからシリアルポートをopenすると、Arduinoはリセットがかかる。これに気付かずミニマルなコマンド送受信の実験するのにも苦労した…。open後2秒くらい待てばいいらしい。

あとはハマったのではないですが、書き込み時のDQ7ポーリングは簡略化できそうです。Arduinoからの制御が遅いので最初にDQ7を見に行く時すでにタイムアウトしてる気がします。つまりこの時点で書き込んだデータの7bit目==DQ7なら成功だし、違うなら失敗と判断するだけでOKのような。まぁとりあえずはデータシート通りに。

次はRubyで書き込みソフトを作る…ではなく、書き込むべきデータがまだ存在しないので、Ubuntuで使えるZ80アセンブラ入手が課題です。前にやったbinutilsをクロス開発用にコンパイルする手順でいけそうですが、ちょっと大げさな感じ。お手軽なものがないか調査してみます。

アセンブラをクリアしてZ80バイナリをROMに焼けたとしても、見てください。
f:id:marlesan:20161024003823j:plain:w400
この状態ではROMの抜き差しが不自由すぎるので、ライタ回路のシールド化が必要です。半田付けヤッター!

f:id:marlesan:20161024004325j:plain:w400
シールド回路のおおまかなレイアウト。表はZIFソケット、裏はArduinoとの接続部分に邪魔されてスマートな配線は無理そうです。写真撮るときには失念していましたが、Arduinoに対するリセットボタンも必要。チャタリング解消してデジタルなエッジを作ろうとしたら、スイッチ+IC+抵抗+コンデンサを載せないと…。

この辺の作業、完全にイージーモードだと思ってましたが結構厄介そう。

一番困ってるのはこれ。
f:id:marlesan:20161024005302j:plain:w400
足が太すぎる……。ZIFソケットは問題ないですが、Z80の基板上に載せる予定のICソケットには入りません。どう対策を打ったものか…

  • 足を細いものに付け替える。
    • 足を固定している穴がスルーホールっぽい。半田除去失敗しそう。
  • Z80の基板にもZIFソケットを乗せる。
    • もう1個持ってるのでできなくはないですが、イヤすぎますね…。
  • シールド上のコネクタからZ80基板上のROMソケットにジャンパ線で接続。
    • 現状の最善策はこれくらい。でもArduinoに電源が入らないようROMとして使うときはシールド外さないといけないのが面倒。

自分で変換基板を発注してみようかな。そしたらついでに28ピンにしちゃうぞ。

ROMライタ製作中

オーソドックスなROMライタというのがどんなものか分からないまま作ってます。データシートのタイミング通りに「コマンド書き込み→データ書き込み→書き込み完了をポーリング(異常検出したらリトライor終了)」を繰り返せばいいとは思うのですが。

  • ターゲットROM EN29F002T-70JI
  • PCからArduinoバイナリを送信→ArduinoがI/Oを通してROMに書き込む
  • PC側はRubyで通信ソフトを自作(serialportというgemでなんとかなりそう)
  • ArduinoはMega2560を使う(バスと制御信号を自前のI/Oで全てカバー可能)

Z80基準で考えるとどれもこれもオーバーテクノロジーですね…。現在はArduino側のコーディング中で、バスと制御信号を操作するところまでできています。

f:id:marlesan:20161019021532p:plain:w500
Analog Discovery 2 のロジックアナライザーで信号をキャプチャしたものです(線が足りないのでバスはそれぞれ下位4bit)。

  1. アドレス・データバスにそれぞれ 5555H, AAH の書き込みコマンドを出力
  2. WE#のアクティブパルスを入れてROMにコマンド書き込み
  3. 書き込み先アドレス、書き込みデータをバスに出力
  4. WE#のアクティブパルスを入れてROMにデータ書き込み

なんとなくなんとかなってる雰囲気あります。2回目のWE#が短いのが謎ですが…。まぁ最適化で何か起こってるんでしょうね。これはこれで興味深いトピックですが、Z80のシステムを作ってZ80アセンブラを書いていこうという時にAVRのバイナリコードに執着するのはやめておきます。

ちなみにタイミングについて。Arduinoが16MHz動作なので何をするにも1クロック周期の62.5nsはかかります。これだけでEN29F002T-70JIの書き込みに関わる主要なタイミング30~40nsより十分長いので、特に何も考えなくても(NOPを入れて動作を遅延させたりしなくても)書き込みは可能という見込みです。

f:id:marlesan:20161019024934j:plain:w400
便利です Analog Discovery 2。ほんとうに買っててよかった。

手動クロックで動かすZ80

ようやくZ80(LH0080A)の動作確認できました。
課題になっていた手動クロックは 74HC123 で無事解決。C=0.001μF(1000pF)× R=470Ω の時定数で、タクトスイッチを押したときに 1μsec 弱のLパルスが出る回路を作ってLH0080Aを動作させました。

f:id:marlesan:20161015210937p:plain:w400
角のギザギザが若干不安でしたが、原因も解消法も見当つかないので無視。

実験回路全景(いい加減、回路図作成ツールを覚えないと…)
f:id:marlesan:20161015214647j:plain:w400
ごちゃっとしてますが、大半はLEDなど状態表示のための配線になります。
基本の考え方は単純で「RDがアクティブのタイミングでデータバスに適切な信号が乗ってさえいれば動作確認はとれるだろう」という発想です。この操作は手動クロックならDIPスイッチでのんびり行えるので、メモリが不要になって作業量を大幅に省略できます。メモリを使う場合、配線の手間はもちろんですが「そもそもメモリに動作確認用のプログラムを書いておかなきゃいけない問題」が発生してしまうのですね。とりあえずはCPUの動作確認をしたかったので、メモリと連携した動作は後回しにしました。

実際の回路はLH0080Aのアドレス出力ピンから台湾Mega2560に向かってジャンパ線がもりもり伸びていますが、これはアドレスバスの内容を確認するためのものです。台湾Megaの外部割込みを使って、クロック立ち上がり→アドレスバスの信号をキャプチャ→16進数でシリアルに出力→PCでアドレス内容を確認、とできるようにしてます。

実験回路の操作手順は次のような感じで。

  1. リセットをアクティブ(スライドスイッチを右)にした状態で電源投入
  2. クロックを3回以上入れる。制御信号が全て非アクティブ(点灯)になる
  3. リセットを非アクティブにする
  4. 好きなタイミングでクロックを入れてCPUを動作させる
  5. RD がアクティブになったら、DIPスイッチを操作してデータバスに適切な信号を乗せる

上の写真はこの手順でLD A,(4321H); LD (1234H),A;と続けて実行したところです。上述の通り実際はメモリがないので(4321H)(1234H)のアドレス指定は無意味で、1回目のLDの最後のメモリ・リード・サイクルでデータバスに乗っていた値(10101010)をアキュムレータに取り込み、2回目のLDの最後のメモリ・ライト・サイクルで先ほど取り込んだ値をデータバスに出力する、という動作になっています(写真はこのサイクルのT3で止めている)。
f:id:marlesan:20161015225415j:plain:w400
Z80ファミリ・ハンドブック』p23 より

ほか「JP命令でプログラムカウンタを適当な番地に設定した後、DIPスイッチを00Hに固定してクロック連打」という実験もしてみました。00HはNOPなのでM1サイクルがただ繰り返されますが、台湾Megaでキャプチャしているアドレスを確認すると、2つの値がインクリメントしながら交互に出力されます。片方はNOPを実行しながら増えていくプログラムカウンタです。もう片方はなんでしょう?
おお、これがZ80自慢の(?)DRAMリフレッシュ機能なのですね。「M1サイクル中、オペコードをフェッチした後、命令を解釈する間の時間を使ってリフレッシュ用のアドレスをバスに出力する」動作が大変よくわかります(もちろんこの時、RFSH信号がアクティブ=消灯になる)。


【追記】リフレッシュ用アドレスはバスの下位7bitに出力されるようです。インクリメントしていくと7FHの次は00Hに戻ります。DRAMは行アドレスと列アドレスを順次指定してアクセスするようにできていて、リフレッシュは行単位で行われるので7bitで間に合う、ということかな。ちなみに7FHとか00Hという値は上位9bitが全て0の場合の話です。こちらのサイトでマニュアルを引用しつつ解説されてますが、上位8bitはIレジスタの中身が出力されるとのこと。引用部分をさらに読むと、下位から8bit目はLD R,AでRレジスタにロードされた値が残り続けるようです(ただし、通常はプログラムでRレジスタを操作する意味はないとも書いてある)。


準備中は「こんな手間かけるより小規模なシステム組んじゃった方が話が早いんじゃないか」とも思ってましたが、やはり実際に手を動かして損することはないですね。実作業半日程度の実験でZ80の基本動作が体感できました。

さて、次は再びメモリです。前回の実験から大幅に飛躍して、Z80を動かすためのプログラムを書き込めるようにする必要があります。ROMライタを作るか、ブートロードの仕組みを作るかですが、スタンドアロンなシステムも視野に入れるならROMを扱えた方がいいのかな。