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を扱えた方がいいのかな。

台湾旅行

連休は台湾旅行に行っておりました。もちろん光華國際電子廣場などの部品屋街を巡ってきましたよ。

f:id:marlesan:20161013020657p:plain:w400
一番嬉しかったのはこれです。鍍銀線。10色揃えてしまいました。

f:id:marlesan:20161013020900p:plain:w400
500ft(約150m)で230元(1元≒3円だったので690円)。右は国内通販で買った100ft巻で、900円。5倍長くてさらに安いなんて…。

f:id:marlesan:20161013021537p:plain:w400
ワイヤストリッパも安かったので衝動買い。鍍銀線にジャストフィットで豆腐を切るように被覆が剥けます。

でまぁ、これくらいで浮かれてるわけなので有意義な買い物ができたとは言い難いですね…。電子工作力が低すぎて台湾のポテンシャルに全くついていけてません。

f:id:marlesan:20161013022845j:plain:w400
これはビックリして買ったArduinoMega互換ボード?です。440元。よく見ると印刷は雑だしベタベタするし「副廠」の意味的にも非純正品で間違いないとは思いますが、見た目をここまで似せて安く売る意味がわかりません。でも、Megaは手持ちにないので動いてくれるだけで万々歳(まだ試してない)。

あとは国内通販での相場が分かっていて、明らかに安いもののうち使いそうな部品をこまごまと買いました(コネクタのハウジングとか)。また台湾に行く気は満々なので、それまでに堂々と散在できるようスキルアップしておきたいです。あ、その前に秋葉原も行きたいな…。電子工作を始めてからは1回も行ってなかったりします。

クロックまわり下調べ

Z80の動作の理解にあたって手動クロックで各マシンサイクルを追いかけていこうと思っているので、そのために調べたことをメモ。


資料を読んでいると「nMOS版は最低クロック周波数がある」という記述あり。WikipediaによるとnMOS版はダイナミック・ラッチという機構を使っているのでクロックを止められないとのこと。

Unlike the original nMOS version, which used a few dynamic latches, and therefore could not be stopped for more than a few thousand clock cycles.

で、ダイナミック・ラッチというのはDRAMのメモリセルのようなものらしい。要するにコンデンサを使って情報保持してるからリフレッシュが必要? データシートでAC特性を調べると、tw(φL) max = 2000[nsec] がこの辺の事情を表してそう(電荷が抜けるので一定時間以上Lにできない)。

f:id:marlesan:20161005022034p:plain

でもパルスをHにできる時間は∞なので、一応クロックを止められるような気はする。ボタンを押すごとに1μsecくらいのパルスを出す回路を作って実験してみよう(555が使えそうだけど、こんなに短い時間のパルスも出せるのかな…)。

  • 【追記】555だと出力パルス幅より短い入力トリガを与えないとダメっぽい(時定数コンデンサが充電されて内部F/FのRがHになる時、トリガパルスが終わってないとSもHのままなので)。74HC123がデータシートによるとnsオーダーの単発パルスを出せるようなので買おう。

ちなみに↑のZ80のデータシートは2.5MHzのものですが、4MHzでもmax側の値は同じのようです。

メモリまわり下調べ

f:id:marlesan:20161002023911j:plain:w250
Z80写真再掲。
これは純粋にCPUというだけのICで、メモリもありません。メモリがなければプログラムもありません(ストアドプログラム方式ってヤツだ!)。というわけで、まずはメモリについて学ばなければいけません。

「メモリ」というだけだと相当に意味の広い言葉ですが、ここでは「コンピュータ基板に直接乗せるICとしてのメモリ≒CPUがアドレス指定で直接アクセスできるメモリ」という文脈で使います。すると、メモリはROMとRAMの2種類に大別でき、以下のように使い分けるようです。

  • ROM (Read Only Memory)
    • 電源を切っても内容が保存される(不揮発性)
    • 内容の書き換えに制限あり(回数・方法など)
  • RAM (Random Access Memory)
    • 電源を切ると内容が消える(揮発性)
    • 読み書きが自由自在
    • ROMもランダム(任意位置)アクセス可能なので、慣習的な呼称

典型的にはプログラムをROMに置いて、プログラムが利用する一時的なデータをRAMで読み書きします。ここで唐突にスーパーファミコンのカートリッジを開けてみると、ROMとRAMが載った基板が出てきます。ROMには今言ったようにゲームのプログラムが書き込まれていますが、RAMは一時データの読み書き用ではなく(そのためのRAMは本体側にある)セーブデータの保存用です。RAMにバックアップ用電池を繋げることで不揮発性と自由な読み書きをなんとか両立させているのですね。

f:id:marlesan:20161003020555j:plain:w300
ドラクエ5の中身)

さて、このRAM、スーファミカセットの部品としてはセーブデータのバックアップ用ですが、RAMはRAMなのでひっぺがせばZ80のメインメモリとしても普通に使えるはず。何事も手を動かすのが大切ということで、早速弄ってみましょう。

f:id:marlesan:20161003223721j:plain:w300

特殊ドライバーでガワを開け、半田こてと半田吸い取り線で外し、HY6264A という8kBytesのSRAMを入手。新単語ですね。SRAMDRAMの違いについてまとめておきます。

  • DRAM(Dynamic RAM)
    • メモリセル(1bitの記憶回路)が1個ずつのFETとコンデンサで構成される
    • メモリセルの構造が単純なので面積あたりの容量を稼げる
    • コンデンサに貯まった電荷で0/1を区別するが、そのままだと放電してデータが消えるため絶えず「リフレッシュ」という動作が必要(Z80にはこのための回路が内蔵されている)
  • SRAM(Static RAM)
    • メモリセルがフリップフロップで構成される
    • DRAMに比べメモリセルの構造が複雑で、集積度が低い
    • その代わりリフレッシュのような面倒な手間がかからない

勉強のために是非めんどくさそうなDRAMを使ってみたいのですが、今回の用途に丁度いいもの(DIPで~64kB)は入手困難なよう。SRAMは普通に買えます。そして実は、ハードオフでジャンクカートリッジを買ってきて分解するより普通に買った方が安いです(例えばこれは32kBで@294)。

閑話休題
ひっぺがしたRAMに対してデータの読み書きをしてみます。基本は以下の2点。

  • 書き込み
    • アドレスバスに書き込み先アドレスを、データバスに書き込むデータを乗せてWE(Write Enable)信号をアクティブにする
  • 読み込み
    • アドレスバスに読み込み元アドレスを乗せ、データバスを読み込んだデータの出力先に接続してOE(Output Enable)信号をアクティブにする

実際にコンピュータシステムの一部として使う場合は上記操作のタイミングが超!重要になるようですが、手でぽちぽち実験するなら次のような適当な回路で動作しました。

f:id:marlesan:20161004002757j:plain:w400

データバスの取り扱いだけ要注意。データ入力の信号をバスに直結させると、読み込み時にRAMから出てきたデータとスイッチからの入力データが衝突してしまうため、間に74HC540を入れてます。このICはバッファ(ある出力信号が駆動できるIC数=ファンアウト数を増やすために通す)ですが、入力ピンのG1かG2をアクティブにすると出力がハイインピーダンス(Hi-Z)になる機能もついてます。Hi-Zは「すごく抵抗高い」状態なので回路的には開放しているのと同じです。スマートとはいい難いですが、G1の入力をスイッチで切り替えられるようにし、WEを押すときは信号を通し、OEを押すときはHi-Zにして入力信号をバスから切り離し衝突を回避しています。ちなみにこの74HC540はTD4の部品のひとつで、製作当時は間違ってHi-Zにしてしまう配線ミスをして「ていうか3ステート(出力がH/L/Hi-Zの3状態ある)って何に使うの…」とか思ってました。こういうときに使うんです。

写真では入力をデータバスまで通しているので、この状態で青ボタンを押すと現在設定中のアドレス(上位5bitはGNDに落としているので 0b0000011111111)にデータ 0b10010101 を書き込みます。逆にRAM内のデータを読み込むには、540をHi-Z状態にし、読み込みたいアドレスを設定して赤ボタンを押します。このRAMはWEもOEも非アクティブだとデータピンはHi-Zになるため、赤ボタンを押している間だけ指定アドレスのデータ信号でLEDが光ります。ぱちぱち。

もうSRAMは極めたね。

いや、実はチップセレクト信号という超重要な項目が残ってました。が、今回はもう長く書きすぎたのでまたの機会に……。

12ステップ完遂

久々の更新です。
12ステップで作る組み込みOS自作入門』を完遂。「そもそもOSって何のためにあるの?」というところから学べて、とてもためになりました。RubyJavascriptでイチャコラとカジュアルなプログラミングに親しんでいただけだった私が、今や「OSのソースを読む」とか「デバイスドライバを書く」とかいうハッカーめいた領域に手を出せそうな気になっています(気分だけか!)。

さて次は、『はじめる組込みLinux』に興味津々です。CPU自作、OS自作を体験してコンピュータ世界の俯瞰図ができあがってきたところで、既製システムにガッツリ触れるのはかなり勉強になりそうです。

ですが!

そろそろ「はんだ付けしたい」「LEDチカチカさせたい」欲求が溜まってきたので、いったん物理層に帰りたいと思います。
題材はこれです。

f:id:marlesan:20161002023911j:plain:w400

Z80

私が生まれる前から存在するCPUです。これを使って何がしかのシステムを作りたいのです(欲を言えばゲーム機を…)。Z80高専時代に教育用ボードで触れたはずですが全く覚えてないし、メモリや周辺コントローラを自分で配線してコンピュータシステムを作るなど、どんな学校でもやらないでしょう。おそらくハンドクロックでプリミティブな動作を勉強するところから始まるので、かなり気長なプロジェクトになりそうですが、飽きるまでは突っ走ってみます。