今後の展望

先週は忙しいのもあって工作にあまり手をつけられませんでした…。「Z80コンピュータを作ろう」企画の現状は、CPU・ROM・RAM・PIOという最小構成でシステムを動かせたところ。しかし本格的にゲーム機なりのシステムを作ろうと思ったら勉強すべきことが山積みです。例えばまだ試してないCTC・DMA・SIOというZ80ファミリの周辺LSIがあり、割り込みがあり、ユーザ入力や映像出力の課題があり…(サウンド関係もか?)。

それで、次の目標は「現状ブレッドボードで組んでいるシステムを基板に固定化する」に決めました。まず、上に挙げた諸々について学習するには、Z80で動かすプログラムをサクサクとトライ&エラーできる環境が必要です(ROM抜き差し開発では辛い…)。そこでArduinoを使って外部からブートロードをかける仕組みを考えているのですが、システムがブレッドボード上にあるままではバスへの接続ができません。穴が足りないし、一旦まっさらなボードを中継させようとすると今度はジャンパ線が足りなくなります。ジャンパ線を大量に追加するか、回路をブレッドボードからユニバーサル基板上に移植するか…ならば、はんだ付けのある後者だ!というわけです。

そして、ここで工作初心者の悲しい現実「部品ストックが足りないせいで製作が止まる」にぶち当たっています。今回はArduinoと拡張回路を接続するために全てのバスを外に出してやる必要がありますが、そのためのケーブルとコネクタがありません。今週末にまた仕事で東京行く予定があるので、それまで設計を練りつつ必要部品を念入りにリストアップしていくとしましょう。ブログ的にはまだしばらく動きなしです。


f:id:marlesan:20161114001236j:plain:w400
これはZ80と関係あるようなないような、MSXで使えるように改造したSFCコントローラです。なぜか今、手元にMSXがあるんですよね。

f:id:marlesan:20161114002818j:plain:w400
あっ、黒の線が左キーの接点に干渉してる…

f:id:marlesan:20161114002519j:plain:w400
カラフルなワイヤが基板下から折り返してるところ、コントローラ裏蓋の突起で潰されてる可能性あり…

SFCコンは制御(本体)側からクロックを入れてやるとキー入力状態をシリアルに取り出せるつくりのようでした。MSXゲームパッドはパラレル接続なので、シリアル化に使っているIC(たぶんシフトレジスタ)を引っぺがして必要なボタンのランドにケーブルの線を接続しておしまい。

完全に素人仕事ですが、こんなでもちゃんと動いたので「ジャンクを組み合わせて欲しい道具を作る」という電子工作を覚えてやりたかった事リストの1項目を潰せた満足感はありますね。

RAMが仲間になった!

f:id:marlesan:20161106002630p:plain:w400

東芝SRAM、TC55257DPL-85Lです。これ1個で32kBあり、アクセスタイムも85nsとおそらく速い部類で、やはりZ80基準だと未来のデバイスなのでしょう。まぁ、気にせず組み込んでいきます。

ROMとRAMでチップが1個ずつだけあって、それぞれに32kBのアドレス空間を持たせたいので、アドレスデコードは単に最上位bitのA15を両者のCEに振り分ければよいですね。CEというのはチップイネーブル、あるいはCSでチップセレクトと呼ばれる信号で、メモリチップはこの信号がアクティブ(普通はLレベル)の時だけアクセスが可能です。

ROMのCE# =  A15 | MREQ#
RAMのCE# = !A15 | MREQ#

これでアドレスが0000H~7FFFH(A15が0)ならROM、8000H~FFFFH(A15が1)ならRAMへのアクセスとなります。MREQはZ80CPUがメモリアクセスを行うときにアクティブにする信号で、メモリがROM1個の時はこれとCEを直結するだけでOKでした。逆にメモリチップが多くなる場合、CPUから出るMREQとアドレスを元にどうやって各チップのCEを作り出すか(これをアドレスデコードと呼ぶ)がシステム設計の重要なポイントになるのだと思います。ここにさらに踏み込むと、Z80アドレス空間を超える容量のメモリを扱ったり(バンク切り替え)、I/Oポートをメモリのアドレス空間マッピングしたりなどが可能のようです。

プログラムは、RAMを無理やり使ってみる処理と、RAMがないと不可能な処理を試してみます。

PIOAD:  equ     0
PIOAC:  equ     1

RAM:    equ     0x8000
STACK:  equ     0xf000

        org     0
;
;       CPU初期設定
        ld      sp,STACK
;
;       PIO初期設定
        ld      a,0b11001111 ;ビットモード
        out     PIOAC,a
        ld      a,0b00001111 ;出力4bit/入力4bit
        out     PIOAC,a
;
;       サブルーチンをRAMに転送
        ld      hl,_DELAY ; 転送元アドレス
        ld      de,RAM    ; 転送先アドレス
        ld      bc,0xff   ; 転送サイズ
        ldir
;
;       Lチカ速度設定(入力ポートから読み込む)
SPEED:  equ     RAM + 0x100
        in      a,PIOAD
        sla     a
        sla     a
        sla     a
        sla     a
        or      0b00001111
        ld      (SPEED),a
        ld      a,0
;
;       LEDチカチカ本体
BLINK:  out     PIOAD,a
        inc     a
        call    DELAY
        jp      BLINK
;
;       遅延サブルーチン(RAMに置く)
DELAY:  equ     RAM
_DELAY: push    af
        ld      a,(SPEED)
        ld      b,a
_LOOP:  djnz    _LOOP
        pop     af
        ret

ちょっと長くなってきました。やってることは変わらずLチカです。

  • RAMを無理やり使ってみる処理
    • 点滅を遅延させる処理をRAM領域にコピーし、そこで実行されるようにした
  • RAMがないと不可能な処理
    • サブルーチン(スタックが使えないとリターンができない。また、コール元のレジスタ退避にもスタックを使う。スタックは当然、RAMが必要)
    • メモリへのデータ保存(起動時にPIO入力ポートの信号を読み、点滅速度設定としてRAM領域に保存)

アセンブルして、ROMに焼いて、電源投入。無事にチカチカしていますが、果たして想定通りの動作になっているのでしょうか。また実行速度を調べて確認してみます。

                        ;
                        ;       LEDチカチカ本体
0027: D300     [11]     BLINK:  out     PIOAD,a
0029: 3C       [15]             inc     a
002A: CD0080   [32]             call    DELAY
002D: C32700   [42]             jp      BLINK
                        ;
                        ;       遅延サブルーチン(RAMに置く)
8000:                   DELAY:  equ     RAM
0030: F5       [11]     _DELAY: push    af
0031: 3A0081   [24]             ld      a,(SPEED)
0034: 47       [28]             ld      b,a
0035: 10FE     [ 8|13]  _LOOP:  djnz    _LOOP
0037: F1       [18]             pop     af
0038: C9       [28]             ret

djnzがちょっとややこしいですね。これはBレジスタをデクリメントして0なら次の行(8クロック)、0以外なら相対ジャンプ(13クロック)という命令です。(SPEED)に保存されている設定値を 0xff だとすると、DELAYサブルーチン1回のコールは 28+13*254+8+20=3358クロック。明滅1回が (42+3358)*256=870400クロック。1クロック=250nsから秒に直して逆数を取ると秒間約4.6チカの点滅です。プログラムが正しく動いていれば、この速度でLチカしてるはず。

f:id:marlesan:20161106022932p:plain:w400

お見事!
楽しすぎますね、これ。

もちろんプログラムは1発動作なんてことはなく、ROMが10回くらいライタとボードを往復しました。RAMが加わって本格的にコンピューターっぽいものが出来上がってきたので、ソフトウェアの開発環境をどうするかという問題が浮上してきたように思います。ブートローダーを作ってトライ&エラーの時間を短縮するか、もう割り切ってエミュレーターで動作確認してしまうか…悩みどころです。

水晶発振の罠(レベル1)

先日とりあえず完成したミニマルなZ80システムは電源投入時の動作に問題がありました。手持ちの測定器具であれやこれやと調べてるうち、Analog Discovery 2のオシロスコープで次のような波形が…

f:id:marlesan:20161105031044p:plain:w400

水色がRESET#信号で、パワーオンリセットが500msほど続いた後、非アクティブのHに立ち上がったところ。その前後で全く変化のない黄色の信号は、なんと、システムクロックです。これじゃ動くわけないですね。そしてこれはどこかで聞いたことがある現象だ、と思い当たって水晶に指で触れてみると…

f:id:marlesan:20161105031853p:plain:w400

ブワッ!とクロックが発振し始めました。この状態からリセットボタンをポチるとCPUが正常動作してLチカが始まります。水晶が発振するにはキッカケが必要なのです、というのは どこかで聞いたというか、発振回路を引用したページでまさに解説されています…。したがって、そのキッカケのために1MΩ程度の抵抗を回路に挿入することも書かれていて、私の手元の回路にも当然組み込んであるはz穴1個ぶんズレて挿してある!!

とまぁ、またもや凡ミスが発覚してこの件は解決いたしました。

ブレッドボードで端子の挿入位置を1列間違えるとVccとGNDが短絡することもあるわけですよ。これまでそういう致命的なエラーをやらかしても破壊的な事態に発展しなかったのは、普段使ってる電源に電流計がついているおかげです。このことはブログの最初のエントリでも書きましたが、作ってよかったなぁとしみじみ思います。

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というワンチップマイコンを購入。使うかは分かりません…。

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