Snake を一命令ぶん深く
中が見えるチップにゲームを書く、その連載の第一回。
tools/export-walk.py が生成したもの。以下の数字はすべて、本物の Snake
ROM をシミュレーションで走らせて実測した。回路図はすべて実ページの回路図
から引き出したもので、そのページはスイッチ網からそれを描いた。ここに間違い
があるなら、チップが変わったということだ。
始める前に: はっきりさせておきたい四つのこと
RAM と ROM、そしてこのコンソールに一方しか無い理由
ROM は部品が作られた時に固定されたメモリだ。プログラムはシリコンの中に あり、変えられない。RAM は電源が落ちれば忘れ、何でもそこに書ける。
6502 は二つを区別できない。16 本のピンにアドレスを載せ、バイトを読むか書く
かするだけで、向こう側にあるものが書かれることを承知しているかどうかは
プロセッサの仕事ではない。このコンソールでは全部が RAM だ。
snake.rom は 351 バイトで、$0200 からメモリに読み込まれる。リセット
ベクタがそこを指しているから、チップは最初の命令をそこから取ってくる。
プログラムが自分自身を上書きするのを止めるものは何も無い。それは実機を
単純化したものではない。ROM があるはずの場所に RAM を配線したとき、実機が
やるのがこれなのだ。
Snake のメモリは三つに分かれていて、リストからそのまま読み取れる。変数のた
めの $0000-$00FF ゼロページ (アドレス指定が安く、2 バイトではなく 1
バイトで済む)、ハードウェアが要求する $0100-$01FF スタック、そして
$0400 から先の画面。画面はたまたまホストが描いているメモリの 1 ページ
だ。
ゲート、動的ノード、パス
ここで特定の意味を持つ三つの言葉。
ゲートは、値を計算して配線をその値へ駆動する数個のトランジスタだ。この チップはそれを一つの形でしか組まない。プルアップが配線を高く保ち、グランド へ向かうトランジスタの網がそれに打ち勝てる。だから網が導通したときに出力は 低くなる。このダイに AND ゲートも OR ゲートも無い。
動的ノードは、何も駆動していない配線だ。クロックによって充電され、それ から引き下げられるか放っておかれるかで、その電荷を保つことで記憶する。 これ以上安いメモリは無く、そして漏れる。だからこのプロセッサには最高クロック だけでなく最低クロックもある: 6502 を長く止めすぎると、何をしていたかを 忘れる。
パスはパストランジスタだ。ゲートに制御線が付いた一つのトランジスタで、 制御が高くなると二本の配線をつなぐ。何も計算せず、向きも持たない。このチップ の中でデータを動かしているように見えるものは、ほとんどがパスが開くこと だ。
この三つの個数は docs/idioms.md にある。
アドレス: 何がどこにあるかの言い方
このダイの配線、トランジスタ、接続にはそれぞれ一つのアドレスがあり、郵便番号 のように後ろから前へ読む:
regs:x : bus : #1169
\____/ \_/ \___/
which what which
part kind one
regs:x はコンテナだ。X レジスタ、チップが分けられた 132 グループの一つ。
bus は、この配線を直接駆動するものが無いことを言う。#1169 はダイ自身が
それに付けた番号で、前の二つの求め方を改善しても決して変わらない。前置き
だけで集合を名指すのも正しい: regs: はレジスタ全部、alu:bit3 は加算器の
一つのスライスだ。docs/atlas.md が完全な規則書になっている。
割り込み、そしてプレイヤーがキーを押す仕組み
6502 には自分を割り込む 3 本のピン、res、irq、nmi がある。どれかを低く
引くと、チップはやっていることを終え、どこにいたかをスタックに積み、メモリ
上端近くの決まったアドレスを経て飛ぶ。
この仕掛けは知っておく価値がある。あまりに安いからだ: チップには割り込み
シーケンサがまったく無い。 プリデコードが命令レジスタを $00 に強制し、
それは BRK で、あとは BRK のシーケンスがやる。割り込みとは、ハードウェア
が挿し込んだ BRK なのだ。
Snake はこれを一切使わない。 入力はホストがメモリに書き込むバイトとして
届き、プログラムはそれを他の変数と同じように読む。$0245 のリストを見て
ほしい:
$0245 A5 0D LDA $0D
$0247 D0 FC BNE $0245
これがフレーム同期の全部だ。バイトを読み、ゼロでなければ戻る。外の何かが それを変えるまで、いつまでも。ここでのポーリングは劣った技法ではない。走って いるプログラムは一つで他にやることも無いのだから、スピンループは何の費用も かからず、どの二命令の間にも着地しうる割り込みよりはるかに考えやすい。
その命令
Snake は三つの命令で画面を消す。これはそのループの真ん中だ:
$021F 9D 00 04 STA $0400,X
$0222 E8 INX
$0223 D0 FA BNE $021F
アキュムレータを $0400 プラス X に書き、X を進め、X がゼロに戻るまでもう
一周する。256 回で画面は空になる。これはゲームがやることのうち最小で役に
立つものだ。そして 5 サイクル、このシミュレーションが実際に刻む半サイクル
で言えば 10 個かかる。
見ているのは X が $02 の周回なので、アドレスの計算にもやることがある。
まず命令の全体を、それから 1 サイクルずつ。
| 半サイクル | 位相 | T | R/W | アドレス | データ | 何が起きているか |
|---|---|---|---|---|---|---|
| 123 | phi2 | - | R | $021F | $9D | オペコード $9D が読まれる。sync が高く、チップがこのバイトは命令だと言っている |
| 124 | phi1 | T2 | R | $0220 | $00 | アドレスの下位バイト $00 が $0220 から読まれる |
| 125 | phi2 | T2 | R | $0220 | $00 | |
| 126 | phi1 | T3 | R | $0221 | $04 | 上位バイト $04 が $0221 から読まれる |
| 127 | phi2 | T3 | R | $0221 | $04 | |
| 128 | phi1 | T4 | R | $0402 | $00 | 加算器が $02 を出した。アドレスバスは $0402 と読める |
| 129 | phi2 | T4 | R | $0402 | $00 | |
| 130 | phi1 | T0 | W | $0402 | $00 | 書き込み。 R/W が低くなり、アキュムレータのバイトが出て行く |
| 131 | phi2 | T0 | W | $0402 | $00 | |
| 132 | phi1 | T1 | R | $0222 | $E8 | また sync: 次の命令 $E8 (INX) がもう読まれている |
中に入る前に、二つ気づいてほしいことがある。
10 個の半サイクルのうち 2 個が書き込みだ。 残りは全部、命令を取り、その オペランドを取り、アドレスの算術をやっている。ストアは、バイトをどこに置くか を割り出すことに生涯のほとんどを使う。
この命令が終わる前に、次の命令がもう読まれている。 最後の行を見てほしい。
ストアがまだ落ち着いていないうちに sync は高く、$E8 がデータバスに乗って
いる。6502 は常に重ね合わせる。だから、間違った瞬間に読んだレジスタは、
シリコンについては真でありあなたには役に立たない答えを返す。
サイクル 1: オペコードが届く
プログラムカウンタがアドレスピンに乗り、メモリが $9D で答えた。そのバイトは命令レジスタに入り、そこからデコード格子に載って、ある行に一致するか一致しないかになる。これが合法な命令だと決めるものは何も無い。パターンが積項を発火させるか、させないかだけだ。
A $00 0000 0000 X $02 0000 0010
Y $00 0000 0000 S $FF 1111 1111
P $34 nv-BdIzc <- capital means set PC $021F
AB $021F DB $9D R/W R SB $01 ADL $20 ADH $02
ALU $01 = A-side $02 B-side $FF IR $D0 T-state -
half-cycle 123, phi2
サイクル 2: アドレスの下位バイトを取る
カウンタが進み、$0220 が読まれている。返ってきた $00 は $0400 の下半分だ。それは入力データラッチに入る。メモリから来たバイトは、他の何かが使う前に必ずそこに着く。
A $00 0000 0000 X $02 0000 0010
Y $00 0000 0000 S $FF 1111 1111
P $34 nv-BdIzc <- capital means set PC $0220
AB $0220 DB $00 R/W R SB $01 ADL $20 ADH $02
ALU $01 = A-side $01 B-side $01 IR $9D T-state T2
half-cycle 124, phi1
サイクル 3: 上位バイトを取り、加算を始める
$04 が $0221 から届く。その間に X を下位バイトへ足さねばならないが、X は自力で加算器に届かない。制御線がパスを開く必要がある。XSB を高くするのがこの回路で、トランジスタ 3 個とクロックでできている。この線は自分のプルアップを持たない。それが動的な作法だ: 高く充電され、それからデコード格子がそう言ったときに引き下げられる。
X を特殊バスへ開くもの。回路図のページから引き出し、そのページはスイッチ網から描いた。
A $00 0000 0000 X $02 0000 0010
Y $00 0000 0000 S $FF 1111 1111
P $34 nv-BdIzc <- capital means set PC $0221
AB $0221 DB $04 R/W R SB $02 ADL $21 ADH $02
ALU $02 = A-side $02 B-side $00 IR $9D T-state T3
half-cycle 126, phi1
サイクル 4: アドレスが揃う
加算器は出力に $02 を持ち、アドレスバスは $0402 と読める。まだ何も書かれていない。このサイクルは、オペランドとインデックスを足さねばならなかったからだけで存在する。ただの STA $0400 にはこれが無い。
加算器の答えを下位アドレスバスに載せるもの。回路図のページから引き出し、そのページはスイッチ網から描いた。
A $00 0000 0000 X $02 0000 0010
Y $00 0000 0000 S $FF 1111 1111
P $34 nv-BdIzc <- capital means set PC $0222
AB $0402 DB $00 R/W R SB $FF ADL $02 ADH $04
ALU $02 = A-side $00 B-side $04 IR $9D T-state T4
half-cycle 128, phi1
サイクル 5: 書き込み
R/W が低くなる。アキュムレータが下の線を通して内部データバスを駆動し、出力レジスタがそれを受け取り、パッドがそれをピンに載せる。画面の 1 バイトが、いま A の持っていた値になった。
アキュムレータをデータバスへ駆動するもの。回路図のページから引き出し、そのページはスイッチ網から描いた。
A $00 0000 0000 X $02 0000 0010
Y $00 0000 0000 S $FF 1111 1111
P $34 nv-BdIzc <- capital means set PC $0222
AB $0402 DB $00 R/W W SB $05 ADL $FF ADH $05
ALU $05 = A-side $05 B-side $00 IR $9D T-state T0
half-cycle 130, phi1
加算器が実際にやっていたこと
インデックス付きストアの面白いところは、6502 にアドレス用の加算器が無い
ことだ。8 ビットの ALU が一つあるだけで、それは ADC が使うのと同じもので、
インデックス付きアドレスは他のどんな和とも同じようにそこで計算される。それ
に入る二本の線を追ってほしい:
特殊バスを加算器の A 入力として選ぶもの。回路図のページから引き出し、そのページはスイッチ網から描いた。
SBADD が特殊バスを一方の入力として選ぶ。もう一方の入力は、いま取ってきた
バイトだ。答えは ALU 自身の配線に出てきて、それからまた動かされねばならず、
そのためにあるのが ADDADL だ。二つの数を足すのに制御線 3 本とバスを 2
回渡ること: チップに加算器が一つしか無く、みんなが順番待ちをしなければ
ならないとき、それがかかる費用だ。
そのサイクルが書き込みだと決めるもの
このサイクルが書き込みだと決めるもの。回路図のページから引き出し、そのページはスイッチ網から描いた。
ラベルを読んでほしい。ir2 から ir7 と irline3 はオペコードそのもので、
書き込みの制御は命令を見ている。t2 と t4 はタイミングチェーンだから、
いつも見ている。そして #440 と #1258 は二つのストアデータラッチで、
これはストアが進行中だとチップがサイクルをまたいで覚えていることだ。
書き込みはプログラマが頼むものではない。正しい T ステートに届いて、たまたま
このゲートに達したオペコードのビットの並びだ。 ここにある何も、STA が何を
意味するかを知らない。
条件がもう一つあって、それはこの絵の中に無い。暗に示すのではなく、言って
おく価値がある。書き込みは、チップが ready であってリセット中でないことにも
依存する。それらの入力は、ここに描いた 2 段より奥にある (3 段にしても届か
ない)。読み出しの間に rdy を低く引けば 6502 は止まるが、書き込みの間に低く
引いても止まらない。書き込みの途中で止まれば、半分だけ書かれたバイトがどこ
かに残ってしまうからだ。
サイクルが終わったとき、何が残っているか
これは、プロセッサについて考えることとチップについて考えることを分ける問い だ。サイクルは、きれいな結果が片付けられて終わるのではない。ダイの配線が 一本ずつ何らかのレベルを保っている状態で終わり、次のサイクルはそこから 始まる。
上の 10 個の半サイクルにわたって、実際に変わったものと変わらなかったものは こうだ:
| オペコードのフェッチ時 | 次のオペコードのフェッチ時 | |
|---|---|---|
| A | $00 | $00 |
| X | $02 | $02 |
| Y | $00 | $00 |
| S | $FF | $FF |
| PC | $021F | $0222 |
| P | $34 | $34 |
| フラグ | nv-BdIzc | nv-BdIzc |
ストアはレジスタもフラグも一つも変えない。 動いたのはプログラムカウンタ と、この表にまったく載っていないメモリの 1 バイトだけだ。他はすべて、あった ところにそのままある。
だが配線は別の話で、ここが腰を据えて考える価値のある部分だ。チップマップは ダイを 132 のコンテナに分ける。ひとつの半サイクルの間に、チップのおよそ 10 分の 1 がレベルを変える。レジスタが値を保つのは、何かがそれを守っているから ではなく、二つのインバータの環がまだ回り続けているからで、そしてそれが 続くのはクロックが届き続ける間だけだ。
だから、命令の境界ではレジスタは意味を持ち、読んでよい。命令の途中では
持たないし、バスも持たない。 上の書き込みのブロックを見てほしい: 半サイクル
130 で下位アドレスバスは $FF と読めるが、ピンは $0402 を保っていて、$FF
はこの命令が触れるどのアドレスの下位バイトでもない。ADL はこのチップの意味
でのバスだ (docs/idioms.md、4): 誰のものでもない一本の線で、クロックによって
high にプリチャージされ、すべての供給元へパストランジスタでつながる。その半
サイクルではそこへ何も開かれていないので、読み出しはプリチャージであって、
何かがそこへ置いた値ではない。チップの中を覗くつもりなら、最初の規律は、
読み出しがいつ意味を持つかを知ることだ。
半サイクル、そしてこのシミュレーションがそれを数える理由
6502 の文書はたいていサイクルを数える。ここでは半サイクルを数える。チップは 両方のエッジで仕事をし、二つの半分は別の仕事をするからだ:
- phi1 ではアドレスラッチがピンを駆動し、前のサイクルで読み込まれた値が 読めるようになる。
- phi2 ではバスがプリチャージされ、デコード格子が落ち着き、次の phi1 の ための制御線がラッチされる。
サイクルを丸ごとで数えると、話の半分が見えない。上の書き込みは一サイクル ではない。あるサイクルの phi1 の半分であり、書き込む先のアドレスは、その前の phi2 の半分の間にピンに載せられたものだ。
位相は二つ、決して同時に高くならない。その非重複は取り決めではない。クロック 生成器の中の二つのトランジスタが、もう一方が去るまで各位相を押さえている のだ。
いちばん下まで
上のどの回路図も記号のところで止まる。記号は底ではない。
dpc2_XSB は X を特殊バスへ開く線で、描かれている三角形の下では
トランジスタ 4 個だ。そのアドレス、ゲート、そしてシリコン上の実寸を
並べる。
| アドレス | ゲートは | つなぐ先 | 両端の幅 / 長さ | 面積 |
|---|---|---|---|---|
t:regs:x.XSB:pullup:#2468 | #133 | vcc | 23 x 23 / 19 | 457 |
t:regs:x.XSB:pulldown:#2761 | #602 | vss | 230 x 229 / 14 | 3333 |
t:regs:x.XSB:pulldown:#2793 | #1247 | vss | 229 x 227 / 14 | 3200 |
t:regs:x.XSB:pulldown:#2932 | cclk | vss | 62 x 62 / 18 | 1154 |
抜けている一つ
静的なゲートには、その出力を高く保つものが要る。その表にそんなトランジスタ
は無いし、見落としでもない。このダイでは負荷が空乏型のデバイスで、
トランジスタの一覧の行ではなくポリゴンのフラグとして記録されている。1018
個のノードがそのフラグを持つ。 dpc2_XSB はその一つではない。
そうなると、あの表の最初の行は誤解を招く。黙って直すのではなく、なぜかを言う
価値がある。あそこの pullup という分類は素朴な読みだ。片端が vcc にある
トランジスタ、ということ。それが形の話であって、仕事の話ではない。t2468 は
プリチャージのデバイスで、恒久的な負荷を持たない配線に電荷を載せるため、
サイクルに一度開かれる。空乏型の負荷とプリチャージのトランジスタは、端子の
一覧では見分けがつかず、まったく違う仕事をする。ダイデータはその違いを
知らないし、一度に一つのトランジスタしか見ない規則もどれも知らない。
つまりこの配線には、それを上に保つものが何も無い。t2468 を通して充電され、
それから何かが引き下げるか電荷が漏れて失われるまで、ただ充電されたままに
なる。それが先に出てきた動的な作法で、ここではそれが 4 個のデバイスになって
いる: 電荷を載せる小さいのが 1 個、それを取り去る大きいのが 3 個。6502 の
最低クロック速度が来るのはここだ。 誰かが書き留めた設計規則ではない。上に
保つものが何も無い配線なのだ。
トランジスタはどのくらい大きいか
ダイデータは実座標を持っていて、MOS の青写真のシート 1 はダイをスクライブ レーンを含めて 168 mil と記す。描かれたダイは 8769 単位にわたるので、 1 単位はおよそ 0.487 マイクロメートルだ。
| ダイ単位 | マイクロメートル | |
|---|---|---|
| チャネル長の中央値 | 16 | 7.8 |
t2468 のチャネル | 長さ 19、幅 23 | 9.2 x 11.2 |
t2761 のチャネル | 長さ 14、幅 229 | 6.8 x 111.7 |
7.8 マイクロメートル。 6502 は 8 ミクロンのプロセスで作られたが、この 数字は調べてきたものではない。1975 年に誰かが青写真に書いたダイ幅に対して 測ったポリゴンの座標だ。人の髪はおよそ 70 マイクロメートルなので、髪 1 本の 幅にチャネル長が 9 つほど並ぶ (デバイス全体はそのチャネルより大きいので、 まるごとのトランジスタならそれより少ない)。
それが 3,510 個ある。それがプロセッサの全部だ。マイクロコードも、隠れた 層も、この下には何も無い。4 個で一本の制御線ができ、その制御線がパスを開き、 そのパスが X をバスへ運ぶ。そうやって蛇は動く。
この先の行き先
これは一命令ぶんだった。これが属する連載は:
- これ。 ストア一つ、5 サイクル、そして語彙。
- ループ。
INXとBNE、それが立てて読むフラグ、そして分岐が成立する と 1 サイクル、ページを跨ぐと 2 サイクルかかる理由。 - フレーム。
$0Dでのスピン、チップが回っている間ホストは何をしている のか、そして 1 フレームが半サイクルでいくらかかるのか。 - 描画。 メモリとしての画面、そして乗算器なしで x と y をアドレスに変える 算術。
- 入力。 ポーリングされるバイト、それから同じことを
irqでやったもの、 そして実測した差。 - 自分のカートリッジ。
games/README.mdが一つ鋳造し、API がそれを走らせ る。
上のすべては生きている。チップマップは 132 のコンテナすべてを描き、トレーサは 走っているコードの横でそれらを半サイクルごとに光らせ、API はこの同じ ROM を 半サイクルずつあなたのために進めてくれる。