Z80カツドウ再開!
仕事落ち着いてきたようなそうでもないような。いろいろ中途半端にぶん投げておりますので、順に消化していきたいと思います。
まずは「Z80コンピュータを作ろう」企画から。
前回、といっても半年…!?近く前ですが、ブレッドボードで作っていた最小構成を基板で実装しよう、というところで止まっておりました。これは新居に机も無事導入でき、先週から作業再開してなんとか完成。
RAMは32KByteを2階建てにしてアドレス空間分フルに用意しました。
右のスペースは拡張用で、DMAと外部ボードへのコネクタを置く予定でいます。
Arduinoからのコネクタのそばに並んでいるのは74HC541で、Arduinoがバス権を持ってる間だけ開通するようになってます。これArduino側のボードに置くべきだったと思いますが、「Z80CPUにバス権がある時に出力と出力でぶつかる」ということに気付くのが遅れまして…。ただ、これについてはやはりArduinoの方で、出力しない時はピンを入力に設定しておくことで3ステート的なIOを実現できるんじゃないかと思ってます。やはりと言ったのは、一応試してはみたらしいんですよね。改めて勉強してみます。
ブランク明けでしたが2ヶ所の結線ミスで済んだのは、火入れをする前のチェックで気付けたことも含めて良し。でも、CPUがちゃんと動いているか不安です。RAMがブランクなので電源入れたらNOPを実行し続けるはず。つまり、4クロックのM1サイクルがひたすら繰り返されるはず。そして、クロック4MHzなので、M1端子の周波数を調べると1MHzが計測されるはず。なんですが、1MHzピタッと出ることもあれば、300KHzくらいだったり500KHzくらいだったり……。この周波数は電源入れ直すと変化しますが、ホットリセットをかけた場合はそのままです。また、M1の周波数が何であっても、CLKにはきっちり4MHzが入っています。なんなんでしょうね~。「RAMがブランク=全部0」という前提が勘違い、というのが最も穏やかな真実です。不定なコードが実行される場合はM1の周期が1MHzにはならないわけですね。まぁ他の電気的なミスがあるとしても現状の知識ではわからないので、Arduino側のRAM書き込みソフトの実装に移っています。こちらは週末完成が目標。
Arduinoプログラミングで1ヶ所詰まったのでシェアいたします。
pinModeでピンをOUTPUTに設定する時、なんもしないと最初はLOWが出力されます。このデフォルト出力をHIGHにしたい場合は、pinModeする前にdigitalWriteでHIGHを出力しましょう。こうするとピンがプルアップされて、OUTPUTに設定した瞬間からHIGHになるようです。ポートレジスタを弄る場合でも理屈は同じで、DDRxで出力設定する前にPORTxの目的ピンのビットに1を書き込めばOKです(私はこっちでやってます)。
これに気づかず、BUSRQ出す前にZ80さんからBUSAKアクティブが来てるんですけど?ってなりました。BUSRQをHIGHにするコードが一切なかったので当たり前だったんですが、少し考えてpinMode(BUSRQ, OUTPUT);
のあとすぐにdigitalWrite(BUSRQ, HIGH);
としてもその間でBUSRQ=LOWが拾われちゃうよね、と思い当たって調べた結果が上記です。
プルアップ関係でもうひとつ。オンボードで13番ピンにLEDが繋がっているArduinoでpinMode(13, INPUT_PULLUP);
としてもピン開放時にHIGHになりません。ピンは開放してても回路完成しているので、LEDの電圧降下+電流調整抵抗の分圧ぶんでLOWレベルの電圧になってしまうようです(台湾で買った怪しいMega互換ボードの実測で、約1.8V)。
Haskell 勉強中
引越し後、一向に工作環境が整わないのでZ80プロジェクトも絶賛放置中。ベッドもまだないけど、いい加減に作業机買わないと……。
そんなわけで?最近の家での時間潰しはPCにかじりついてソフトウェア関係の勉強です。「Deep Learning やりたい!」から始まったはずが「なんか関数言語触っておかなきゃ」という所に落ち着いて Haskell 弄ってます。すごいH本も買いましたよ。
手応えとしては、普段 JavaScript で Function を投げつけ合うコードを書いているせいか、関数型言語の世界観に意外とすんなり入れている気がします。もちろんこの先に壁は有るんでしょうが。
再帰によるリスト処理
すごいH本は現在1/4を過ぎたあたり。ここまでだと標準のリスト処理関数を再帰で実装するくだりのところがパズルを解いてるみたいで面白かったです。「カリー化された関数」という世界観に慣れるため JavaScript でも実装しながら理解を確かめました。以下、メモ的にそのコードを載せてみます。
まずはコードからノイズを減らすためにちょっとしたイディオムを定義。
// リストの先頭要素を返す Object.defineProperty(Array.prototype, 'head', { get : function _head () { if (this.length <= 0) throw new Error("empty list!"); return this[0]; } }); // 先頭を除いた残りのリストを返す Object.defineProperty(Array.prototype, 'tail', { get : function _tail () { if (this.length <= 0) throw new Error("empty list!"); return this.slice(1); } }); // リストが空かどうかを返す Object.defineProperty(Array.prototype, 'isEmpty', { get : function _isEmpty () { return this.length <= 0; } });
次のように使います。
["Miho", "Saori", "Hana", "Yukari", "Mako"].head; //=> "Miho" ["Miho", "Saori", "Hana", "Yukari", "Mako"].tail; //=> ["Saori", "Hana", "Yukari", "Mako"] ["Miho", "Saori", "Hana", "Yukari", "Mako"].isEmpty; //=> false [].isEmpty; //=> true
sum
まずは小手調べ、リストの要素の合計値を出す sum です。
function sum (list) { if (list.isEmpty) return 0; return list.head + sum(list.tail); } sum([1,2,3,4,5,6,7,8,9,10]); //=> 55
sum に渡すリストがどんどん短くなっていき、最後にsum([]) = 0
で再帰呼び出しが止まって計算の解決が始まります。
実行順はそうなんですが、考え方としてはまず「空のリストの合計値はどう考えても 0 だよね」という自明な部分から出発するのがポイントのよう。
再帰の部分は問題を分解して考えます。 「リストの合計値は、先頭の要素と先頭以外の要素からなるリストの合計値を足したもの」と考えて、 【先頭以外の要素からなるリスト】に対して再び「リストの合計値は、~」を適用していくことで全体の合計値を計算します。
maximum
リスト中の最大値となる要素を返す。リストを引数に取って値を返す点では sum と同じです。
function maximum(list) { if (list.isEmpty) throw new Error("empty list!"); if (list.length === 1) return list.head; return Math.max(list.head, maximum(list.tail)); } maximum([6,1,3,10,8,2,50000,7,9,4]); //=> 50000
「空のリストに最大値も何もねーだろ(エラー)」「リストに要素が1個しかなければそれが最大値」が、自明な部分。 「リストの最大値は、先頭の要素と、先頭以外の要素からなるリストの最大値のうち、大きい方」が、再帰の部分。
とりあえずここまで。1引数関数の実装だけだったのでカリー化がまだ未登場、つまり Haskell っぽいことはほとんど出てきてなくて、ただ JavaScript を書いただけです。何だこの記事。次回ではその辺りを……。
似非RAMディスク(2)
前回: 似非RAMディスク(1)
第一の誤算はこれです。
M68AF127Bの幅が広くて変換基板のランドに乗りません…。
仕方なく足を畳みます。撮影時は回路図を読み違えていてA14を伸ばしていますが、CS以外畳んでOKです。これでなんとか変換基板には収まります。こて先に初めて細いコーン形を使い、がんばってはんだ付けしました。
第二の誤算は、恐らくM68AF127Bが縦にも厚いために、亀の子接続するには足の長さが足りなかったことです。
仕方なく鍍銀線の芯線を使って上下の足を繋ぎました。
- 上亀(上に乗ってるIC)の足に予備半田をする
- 下亀の足とランドの接続部にある半田を使って芯線の先を接合
- 芯線を上亀の足に沿って巻きつけ、予備半田に押し付けている状態でこて先を当てて接合させる
- ひっぱって強度を確認後、余分な芯線をカット
なんか手順まとめてしまいましたが、全くお勧めしません……あ、変換基板の裏にはSSOP用のランドがあるので、写真のようにカートリッジ基板と密着させるならポリイミドテープなどで絶縁しましょう。いや、お勧めはしませんよ。
第三の誤算は(まだあるぞ!)同じくパッケージの縦の厚さのせいで、ケースが閉まらないことです。仕方なく、ケースに収めることは諦めました。
完全に敗北ですね、今回は……。
一応動作は問題なさそうで、MSX-DOSをインストールして似非RAMディスクからDOS起動できていますが、反省を活かして再チャレンジしたいです。
おまけの裏面。
似非RAMディスク(1)
仕事が忙しすぎて趣味に手を付けられていませんでした。1ヶ月以上空いてしまうとは……。仕事は一応落ち着いたところですが、プライベートでは引越しをせねばならず、電子工作への本格復帰はまたまた先延ばしです。
とはいえ、ちょっと半田こて握る時間くらいはあるはず!
ということで今日は「似非RAMディスク」なるものを作ってみました。MSXのスロットに挿すとディスクドライブとして使える、バッテリーバックアップ付きのSRAMです。256KBあります。MSXにこの容量を扱わせるにはメモリとの間に制御回路を置く必要がありますが、その部分を「メガロム」と呼ばれる大容量ゲームカートリッジを改造することで楽しちゃおうというのが似非RAMディスクの特徴のようです。たぶん。
参考にしたサイト: 似非RAMディスクの作り方(256kB)
とりあえず、こちらが完成品です。
見ての通りアルカノイド2がベース。これはまぁまぁ安く済んで助かりました。
このほか材料調達のネックになるのはSRAMかと思われます。まず、DIPで1Mbit(128KB)のSRAMというと、通販ですぐ買えそうなのは鈴商のHM628128BLP-8しかありませんでした。でも、1個800円は高いですよねぇ……。2個必要なら予備含めて4個は欲しいので躊躇する価格帯です。ちなみに、若松で628128で検索して出てくるのはTSOPと思われます。今回の用途では買っちゃダメなやつですね。しかも1800円する!
最終的に秋月電子にあるSOPの128KB SRAMとSOP→DIP変換基板を使うことに決めました。単価150円なのは文句なし。さらに、オリジナルではカートリッジ基板を挟んでDIPを亀の子にするところ、SOPなら表面だけで亀の子にできて配線がちょっと楽になるのでは?という狙いもありました。が!この辺の目論みは全部裏目に出てしまいました……。
続く:似非RAMディスク(2)
今後の展望
先週は忙しいのもあって工作にあまり手をつけられませんでした…。「Z80コンピュータを作ろう」企画の現状は、CPU・ROM・RAM・PIOという最小構成でシステムを動かせたところ。しかし本格的にゲーム機なりのシステムを作ろうと思ったら勉強すべきことが山積みです。例えばまだ試してないCTC・DMA・SIOというZ80ファミリの周辺LSIがあり、割り込みがあり、ユーザ入力や映像出力の課題があり…(サウンド関係もか?)。
それで、次の目標は「現状ブレッドボードで組んでいるシステムを基板に固定化する」に決めました。まず、上に挙げた諸々について学習するには、Z80で動かすプログラムをサクサクとトライ&エラーできる環境が必要です(ROM抜き差し開発では辛い…)。そこでArduinoを使って外部からブートロードをかける仕組みを考えているのですが、システムがブレッドボード上にあるままではバスへの接続ができません。穴が足りないし、一旦まっさらなボードを中継させようとすると今度はジャンパ線が足りなくなります。ジャンパ線を大量に追加するか、回路をブレッドボードからユニバーサル基板上に移植するか…ならば、はんだ付けのある後者だ!というわけです。
そして、ここで工作初心者の悲しい現実「部品ストックが足りないせいで製作が止まる」にぶち当たっています。今回はArduinoと拡張回路を接続するために全てのバスを外に出してやる必要がありますが、そのためのケーブルとコネクタがありません。今週末にまた仕事で東京行く予定があるので、それまで設計を練りつつ必要部品を念入りにリストアップしていくとしましょう。ブログ的にはまだしばらく動きなしです。
これはZ80と関係あるようなないような、MSXで使えるように改造したSFCコントローラです。なぜか今、手元にMSXがあるんですよね。
あっ、黒の線が左キーの接点に干渉してる…
カラフルなワイヤが基板下から折り返してるところ、コントローラ裏蓋の突起で潰されてる可能性あり…
SFCコンは制御(本体)側からクロックを入れてやるとキー入力状態をシリアルに取り出せるつくりのようでした。MSXのゲームパッドはパラレル接続なので、シリアル化に使っているIC(たぶんシフトレジスタ)を引っぺがして必要なボタンのランドにケーブルの線を接続しておしまい。
完全に素人仕事ですが、こんなでもちゃんと動いたので「ジャンクを組み合わせて欲しい道具を作る」という電子工作を覚えてやりたかった事リストの1項目を潰せた満足感はありますね。
RAMが仲間になった!
東芝の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チカしてるはず。
お見事!
楽しすぎますね、これ。
もちろんプログラムは1発動作なんてことはなく、ROMが10回くらいライタとボードを往復しました。RAMが加わって本格的にコンピューターっぽいものが出来上がってきたので、ソフトウェアの開発環境をどうするかという問題が浮上してきたように思います。ブートローダーを作ってトライ&エラーの時間を短縮するか、もう割り切ってエミュレーターで動作確認してしまうか…悩みどころです。
水晶発振の罠(レベル1)
先日とりあえず完成したミニマルなZ80システムは電源投入時の動作に問題がありました。手持ちの測定器具であれやこれやと調べてるうち、Analog Discovery 2のオシロスコープで次のような波形が…
水色がRESET#信号で、パワーオンリセットが500msほど続いた後、非アクティブのHに立ち上がったところ。その前後で全く変化のない黄色の信号は、なんと、システムクロックです。これじゃ動くわけないですね。そしてこれはどこかで聞いたことがある現象だ、と思い当たって水晶に指で触れてみると…
ブワッ!とクロックが発振し始めました。この状態からリセットボタンをポチるとCPUが正常動作してLチカが始まります。水晶が発振するにはキッカケが必要なのです、というのは どこかで聞いたというか、発振回路を引用したページでまさに解説されています…。したがって、そのキッカケのために1MΩ程度の抵抗を回路に挿入することも書かれていて、私の手元の回路にも当然組み込んであるはz穴1個ぶんズレて挿してある!!
とまぁ、またもや凡ミスが発覚してこの件は解決いたしました。
ブレッドボードで端子の挿入位置を1列間違えるとVccとGNDが短絡することもあるわけですよ。これまでそういう致命的なエラーをやらかしても破壊的な事態に発展しなかったのは、普段使ってる電源に電流計がついているおかげです。このことはブログの最初のエントリでも書きましたが、作ってよかったなぁとしみじみ思います。