高速 2A03、組んで確かめた
N3 の報告。作業の後に書かれた。
計画は docs/n3-plan.md で、先に書いた。この報告は、各手順が測ったものを、手順が閉じた順に運ぶ。
手順 1: 食い違いの一覧 (2026-09-05 に閉じた)
走行スタンプ: rustc 1.97.1、halfphi v0.1.6、6502 リポジトリのピンのゴールデンは v6502-sim 0.1.0 nodes 1725 transistors 3510 が記録したもの (274 トレース)。REQUIRE_NETLIST=1 REQUIRE_PINS=1 cargo test --release -p v2a03-sim --test pin_lockstep: 両方の半分が約 12 s で緑。MUTATE=1: チップ側は赤 (フレームの中で R/W を反転させると、ベクタの取り込みが読み出しの検査に落ちる)、チップ間の側は、触れる最初の世話されたバイトで赤 (decimal-adc h=30: the differing byte is not a write's phi2)。
位相合わせ、実測
規約の h=0 は、リセットの手順が後に残すフレームであり、6502 のラング 0 ではそれが最初のオペコードの取り込みだ (power_cycle が、誰かが見る前にベクタの読み出しを走らせてしまう)。2A03 の power_on (リファレンスの initChip の手順) は、コアをもっと早い時点に残す: lockstep-probe が両方の流れを並べて印字したところ、リセットベクタの最初の同期高の取り込みは、試したどのトレースでも 2A03 の位相 17 に座っていた。その数字が pins::ALIGN_PHASES で、power_cycle は、着地したフレームが同期高のリセットベクタの読み出しであることを宣言する。だからリセットの手順が変わったら、すべての比較がずれるのではなく、名指しで失敗する。
結果
272 トレースを比較、2 つを名指しで拒否、130 がすべてのフィールドで全半サイクル厳密に一致。 残る 142 は、名前の付いた四つの類の内側でだけ違い、どの類もテストが宣言する規則で境界が付いている:
| 類 | フレーム数 | 規則 |
|---|---|---|
| stack | 2,088 | アドレスが違う。どちらもページ 1 の中で、下位バイトがちょうど $40 だけ違う |
| write-phi1 | 453 | 書き込みサイクルの phi1 側でデータバイトが違う |
| s-leak | 2 | 同じ $40 がページ 0 で。op-ba だけ |
| data | 17 | 世話されたバイトが違う: 十進の 9 個のストアと、リセットの窓の内側の 8 個 |
| ab、rw | 14、7 | リセットの窓の内側だけ |
スタックのページ。 シリコンは電源投入時に S を未定義で残す。6502 のシミュレーションはそれを $00 に落ち着かせ、2A03 のそれは $C0 に落ち着かせる (どちらも状態 0 から自分のリファレンスに対してビット厳密なので、それぞれが自分のダイデータの答えだ)。そしてリセットの三回の減算の後、一方のコアは $01FD に積み、もう一方は $01BD に積む。テストは h=0 で 2A03 のレジスタのノードから S を読み ($BD)、記録の最初の積み込みから S を読み ($FD)、その差 ($40) を導き、ページ 1 のすべての違いがちょうどそれであって、そのフレームの中の他は何も違わないことを要求する。二つのトレースは自分で S を書くもので (op-9a の TXS、op-9b の SHS)、それらのスタックのページはそのまま一致する。一覧にしてある。一つのトレースは S をページの外へ漏らす: op-ba は TSX で、その後に続く $34 $12 のバイトが NOP zp,X として走り、写された S でページ 0 を添字するので、二つの位相が $000F と $00CF になる。このトレースだけに許してある。
書き込みの phi1。 書き込みは clk0 が立ち上がるときに世話され、読み出しは落ちるときに世話される。書き込みサイクルの phi1 側では何も世話されず、外の世界は何も駆動しない。そこで 6502 のピンは最後に読んだバイトを示し (そのラングが再現するのはそれだ)、2A03 のピンは別のものを示す: INC の最後の書き込みでは新しいバイト ($41。6502 の $40 に対して)、DEC では $FF が着地しようとしているところで $55、BRK の積み込みでは $05 と $32 の前に $07 と $15。分類より先は調べていない: どの書き込みも自分の phi2 のバイトはデータとして比較され、272 トレースすべてで一致する。この類は数えられていて、ゼロでないままでなければならない。だからこれが消えることも一つの発見になる。
十進。 2A03 のダイは十進の補正が切り離されていて、三つの十進の鎖はちょうどそれだけを示し、それ以上は示さない。違っている世話されたバイトはどれも書き込みの phi2 で、テストの中でその横に算術を添えて一覧にしてある:
| トレース | アドレス | 6502 (補正あり) | 2A03 (2 進) |
|---|---|---|---|
| decimal-adc | $0080 | $47 | $41 (19+28) |
| $0081 | $10 | $0A (09+01) | |
| $0082 | $99 | $33 (99+99+1) | |
| $0083 | $00 | $A0 (50+50) | |
| $01FA (PHP) | $FD | $FC (C はクリア: $A0 は桁上げを運ばない。V はそのまま) | |
| decimal-sbc | $0080 | $29 | $2F (42-13) |
| $0081 | $05 | $0B (10-05) | |
| $0082 | $99 | $FF (00-01) | |
| decimal-mixed | $0080 | $26 | $20 (1F+01) |
9A-00 の減算、D の下での CMP、CLD の後の加算は、すでに一致していた。フラグも 2 進だ: 違っている一つの PHP は、十進の補正が作っていたはずの桁上げだ。
走行中のリセット。 RES を h=20 で 8 位相ぶん低に駆動すると (記録された台本)、6502 は飛行中の BRK をそのまま走らせ、ベクタの選択が 1 サイクル後に $FFFC へ回り、空転して、h=42 でベクタを読む。2A03 は RES の下でコアを止めたまま保ち (アドレスバス、R/W、データが、中断された積み込みのところで凍る。h=22 から 28)、解放されると二位相ぶんゴミのアドレスで空転し、同期付きで取り込み、リセットの三回のスタック読み出しを行い、h=42 でベクタを読む: 同じ半サイクルだ。reset-probe は保持を 8、12、16、24、48、96 位相で測った: ベクタの読み出しは毎回 42 + (保持 - 8) に着地するので、解放後の手順はどちらのダイでも 13 位相であり、違いはすべて保持された区間そのものだ。テストは窓を (最初の RES 低のフレーム、ベクタの読み出し - 6) に境界付け、ベクタの読み出しが同じ h であることを要求し、窓が空でないことを要求する。
名指しで拒否。 fixture-rdy-stall は RDY を駆動し、fixture-so-pulse は SO を駆動する。どちらも 2A03 のピンではない: RDY は DMA のユニットが所有する内部ノード (spr_dma_/rdy、pcm_dma_/rdy) で、SO はリファレンスが低に保つ未接続のパッドだ。CorePins は要求を記録し、テストは偶然を読む代わりに比較を拒む。
これが手順 2 に決めること
コアのラングは v6502-micro で、つまみは二つ。どちらも今は 6502 リポジトリにある (コミット dc2dada、tests/knobs.rs): 十進の補正を切り離すこと (セレクタが D を一度も見ないので、2 進の区間は 2 進のフラグで動く。上の表をちょうど作るようにそこへ押さえてある)、そして h=0 のスタックポインタを、このチップのラング 0 が実測する値で種付けすること。書き込みの phi1 のバイトは、コアのラングがこのダイについて再現しない唯一のものであり、手順 2 のゲートも同じやり方でそれを名指す。
手順 2: コアのラング (2026-09-05 に閉じた、持ち越し 1 件)
走行スタンプ: rustc 1.97.1、halfphi v0.1.6、v6502-micro と v6502-pins は 6502 リポジトリの dc2dada。REQUIRE_NETLIST=1 REQUIRE_PINS=1 cargo test --release -p v2a03-micro --test core: ビルドの後、約 9 s で緑 (v2a03-micro を初めてビルドするとき、その版の 6502 リポジトリをチェックアウトし、ネットリストを組んで、そのラング 0 を 256 オペコードにわたって走らせてラング 3 の表を記録する。約 80 s。この表は NC-SA 派生であり、OUT_DIR に留まる)。MUTATE=1 は十進の補正をつないだままにする: 三つの十進の鎖すべてが最初のストア (h=21) で赤になり、テストがそれを名指しで言う。
閉じたもの
-
crates/v2a03-micro: コアはv6502-microを、手順 1 が正当化した二つのつまみset_decimal_adjust(false)とset_stack_at_h0(Some(s))で構成したものだ。sはテストの中で、このチップのラング 0 のレジスタのノードから h=0 で読む ($BD)。打ち込まない。コアについて他に書き下ろしたものは何も無い。 -
ゲート: 記録済みの 6502 のプログラムと台本のすべて (272 個。RDY か SO を駆動する二つは手順 1 と同じく拒否) を、このチップのラング 0 の上で
CorePinsを通して、そしてコアのラングの上で同じrunの下で走らせる。272 プログラム、52,048 半サイクルを比較、133 個がすべてのフィールドで厳密に一致。457 個の write-phi1 のバイトが違い、他は何も違わない。 十進の鎖はそのまま一致する: 手順 1 の表の 2 進のバイトが、いまコアのラングのバイトでもある。 -
処理速度 (
examples/bench.rs、リファレンスのプログラム):エンジン 半サイクル/s ラング 0、メモリのハーネスを通したスイッチのレベル 9,796 コアのラング 32,807,382 (3 回の最良) ラング 0 の約 3,349 倍で、2A03 の実時間の 9.2 倍 (3,579,545 半サイクル/s。マスタクロックを 12 で割り、1 サイクルに 2 半サイクル)。6502 のラング 3 は自分のベンチで 39.0 M と測れた。違いはつまみではなくプログラムと種付けされた機械であり、APU の表が使うことになる量の内側だ。
持ち越し
- リセットの保持。 ラング 3 は 6502 の RES への応答を演じる: 飛行中の BRK はそのまま走り、ベクタの選択が回り、データパスが空転する。このダイは RES が低の間コアを止めたまま保ち (手順 1 の測定)、それから凍った状態から同じ 13 位相の解放の手順を演じるので、窓の内側のアドレスが違う (6502 の
5801/0057/00ffに対してeae9/00e9)。ゲートはfixture-reset-mid-runを名指しで食い違うものとして一覧に挙げ、違いをリセットの窓に境界付け、ベクタの読み出しが両方で同じ半サイクルであることを要求し、このトレースがきれいに再生してしまったら失敗する。これを閉じるには: このチップのラング 0 でのプローブ (6502 のreset-probeの形: RES の下での半サイクルごとの制御線とラッチ) で、止まったコアが何から再開するかを見て、それからv6502-microに三つ目のつまみをそこから書き下ろし、このトレースに押さえる。電源投入時のリセットは影響を受けない (コンソールは h=0 より前に RES をアサートする)。 - 書き込みの phi1 のバイトは、このダイのピンについてコアのラングが再現しない唯一のものであり続ける。手順 1 が記録したとおりだ。
手順 3: APU のプローブ (2026-09-05 に閉じた)
以下のどの数値も、成果物としてツリーに残る例 (crates/v2a03-sim/examples/apu-*-probe.rs) がラング 0 から読み取ったものだ。ここには文書から引用したものが何も無い。公表された表があるところでは後から比べていて、一つの項目が食い違う。下で名指しする。位置は、特記しない限り、レジスタ書き込み自身のバスサイクル (w40xx のストローブ) より後の CPU サイクル数だ。チップの単位は半ステップ。
フレームシーケンサ (apu-frame-probe、モード 0、1、そして書き込み無し)。$4017 への書き込みは 15 ビットの LFSR を再起動する (frm_t0..14。どちらのモードでもストローブ時に $7FE0)。4 ステップ: phase_a..d が 7458.5、14914.5、22372.5、29830.5、それから 29830 ごと。frame_irq は phase_d と一緒に立つ。5 ステップ: phase_a..c は同じ三つの位置、phase_d は無く、phase_e が 37282.5、周期は 37282、IRQ は無い。$4017 への書き込みがまったく無い場合、カウンタは電源投入時のリセットから同じ間隔で走り、phase_a は power_on が残す最初の半ステップから 7458 サイクル後になる (ピンの規約の h=0 はその 17 位相後だ)。エンベロープとスイープの更新はこのクロックに着地する: エンベロープの減衰は 7458/7456 を交互に置いた間隔で進み (四分フレーム)、スイープの周期の変更は 14916 間隔 (半フレーム)。
長さの表 (apu-length-probe): $4003 に 32 個の添字をそれぞれ書き、ストローブから 6 半ステップ後に sq0_len0..7 を読む。添字の順に: 9, 253, 19, 1, 39, 3, 79, 5, 159, 7, 59, 9, 13, 11, 25, 13, 11, 15, 23, 17, 47, 19, 95, 21, 191, 23, 71, 25, 15, 27, 31, 29。どの項目も公表された表の値から 1 引いたものだ: ダイのカウンタは n-1 を抱え、チャンネルは半フレームのクロック n 回ぶん鳴る。
矩形波 (apu-channel-probe duty|env|sweep)。デューティのシーケンサ sq0_c は 7..0 と数え下がり、1 ステップは 2(t+1) CPU サイクル (t=8 では 36 半ステップ)。sq0_out が 15 になるのは、デューティ 0..3 でステップ {6}、{6,5}、{6,5,4,3}、{7,4,3,2,1,0} のときだ: 公表された 12.5/25/50/75 パーセントの列そのもの。エンベロープ (周期 0、ループ無し): sq0_envc は $4003 の書き込みで 0、最初の四分フレームで 15 (ここでは +7396。start フラグによる再読み込み)、それから四分フレームごとに一段下がって 0 まで。スイープ (タイマー $100、シフト 1、周期 0): 正方向はどちらの矩形波でも半フレームで $100、$180、$240。負方向は矩形波 0 で $100、$07F、$03F (1 の補数)、矩形波 1 で $100、$080、$040 (2 の補数)。二つの矩形波の文書化された違いを、実測したものだ。
三角波 (tri): control を立て、リニアの再読み込み値は 127、タイマーは 8。最初のステップは $400B の書き込みから 7396 サイクル後、最初の四分フレームに着地する。リニアカウンタがそこで読み込まれ、シーケンサはそれがゼロでないことを必要とするからだ。それから 32 ステップの列 15..0、0..15 が 1 ステップ t+1 サイクル (18 半ステップ) で進み、0 と 15 が二重になるところは 36 として現れる。
ノイズ (noise)。$400E の添字ごとの周期の表を、LFSR のシフトの間の半ステップ数を 2 で割った CPU サイクルで: 4, 8, 16, 32, 64, 96, 128, 160, 202, 254, 380, 508, 964, 1016, 2034, 4068。15 個の項目はどの公表された NTSC の表とも一致する。添字 12 は 964 と実測され、公表された値は 762 だ。 ノイズのタイマー noi_t0..10 は読み込まれる数え下がりではない (シフトの間の値は、どの添字でも最大 $7FF まで及ぶ)。これは、リセットの模様が添字ごとの ROM の行になっている LFSR 型のタイマーだと読める。その構造の下では、行の中の 1 ビットの誤りがその項目を任意に動かすので、これは Visual 2A03 のあの行の転記の欠陥か、ダイの本物の癖のどちらかであり、決められるのはシリコンだけだ。ここから使う表はダイのもので、その項目は名指ししてある。LFSR noi_c0..14 は c14 の方向へシフトし、c0 には帰還が入る。帰還はモード 0 では c13 xor c14、モード 1 では c8 xor c14 だ (公表されたビット 1 とビット 6 のタップを、レジスタの番号付けを逆にしたもの)。どちらも連続する 24 状態から読み取った。電源投入は 011111111111111 を残し (c0 が左端)、有効化した後の最初のシフトは 8063 半ステップ待つ (タイマーが電源投入時の状態を走り切るぶん)。
DMC (dmc)。$4010 の添字ごとのレートの表を、シフトの間の CPU サイクルで: 428, 380, 340, 320, 286, 254, 226, 214, 190, 160, 142, 128, 106, 84, 72, 54。十六個すべてが公表された値だ。バッファが空の状態で有効化すると、$4015 の書き込みから 19 サイクル後に $C000 を取り込み、RDY は 5 半ステップ低になる。次の取り込み ($C001、8 回のシフトの後) では RDY が 7 半ステップ低になる。出力のカウンタは 1 ビットあたり 2 ずつ進む: %10100101 の標本の下で $40、$42、$40、$42。
スプライト DMA (apu-dma-probe): $4014 を二つの CPU サイクルのパリティで書く。RDY は書き込みのストローブで落ちる。次の命令のアドレスで一、二サイクル過ごした後、DMA が 256 回の読み書きの対を走らせ (phi1 で $02xx を読み、次のサイクルで同じバイトを $2004 へ書く。spr_addr が数え上げる)、RDY は 1029 か 1027 半ステップ (514.5 か 513.5 CPU サイクル) の後に戻る。公表された 513/514 に、書き込み自身の半分を足したものだ。RnWstretched はその間ずっと立つ。
コントローラのポート (io): $4016 のビット 0..2 は書き込みで OUT0..2 に着地する。$4016 の読み出しは joy1 (と /r4016) を、その読み出しの二半ステップぶん低に引き、$4017 は joy2 (/r4017) を引く。チップはそれらの読み出しでデータを駆動しない: コントローラのビットは基板のもの (N4 の 74LS368 二つ) だ。
これが手順 4 に決めること
ビルド時に測る表: 長さの表、ノイズと DMC の周期の表、デューティの列、そして両モードでのフレームシーケンサの位置。それ以外 (タイマー、エンベロープ、二つの補数を持つスイープ、リニアカウンタ、LFSR のタップ、DMC のデルタのカウンタと取り込み) は、上のプローブから書き下ろして札を付ける。ゲートは計画のままだ: CPU の半ステップごとの五つの出力コードを、全チャンネルを運動させるプログラムの上で、ラング 0 に対して。
手順 4: 表としての APU (2026-09-05 に閉じた)
走行スタンプ: rustc 1.97.1、halfphi v0.1.6。REQUIRE_NETLIST=1 cargo test --release -p v2a03-micro --test apu: ビルドの後、約 20 s で緑。そのビルドはいま、APU の表をラング 0 から約 1 分で記録する。MUTATE=1 はビルド時にデューティの表を逆順にし、ゲートは最初の矩形波のステップで赤になる。
ゲート
二つのレジスタプログラム (長い音の世界: 両方の矩形波にエンベロープとスイープ、一方は負方向。三角波は control を立てて。ノイズ。DMC は 33 バイトの標本をループ。短い音の世界: 長さは半フレーム 1 個と 3 個、ループ無しのエンベロープ、三角波は control をクリアしてリニアカウンタを 5、DMC は 17 バイトを一度だけ鳴らす) を、それぞれフレームシーケンサの両モードの下で、それぞれ 80,000 半ステップぶん、このチップのラング 0 の上で CorePins を通して、そしてラングの上で (コアに、自分の phi2 の書き込みから APU を与えたもの) 走らせる。四つの世界すべて: 80,001 半ステップ、五つの出力コード (sq0_out、sq1_out、tri_out、noi_out、pcm_out) とフレーム IRQ のフラグが、全半ステップでラング 0 と同一。 これが覆うのは、デューティの列とステップのタイミング、エンベロープの start と減衰、両方のスイープの補数からそのミュートまで、長さの満了、リニアカウンタの読み込みと満了、12,000 回のシフトにわたるノイズの LFSR、DMC のタイマー、バイトのサイクル、デルタのカウンタ、標本のループと終端、そしてフレーム IRQ だ。
ビルド時に実測 (build.rs、tables)
手順 3 の長さの表、デューティの列、フレームの位置、そして手順 3 が見ていなかった二つ:
- ノイズと DMC のタイマーはカウンタではなく LFSR だ。
noi_t(11 ビット) とpcm_t(9 ビット) は電源投入から自走し、二つのタップの XOR を入れながら左へシフトする。自走の全体に当てはめて、観測されたどのステップでも厳密だ: タップはノイズが (10, 8)、DMC が (8, 4)。それぞれ終端の状態が一つあり (1024 と 256)、次のティックでそこに添字ごとの値、つまりダイの周期の ROM が再読み込みされる: タイマーごとに 16 個の再読み込み状態を記録した。公表された周期の表は、それらの状態から終端までの距離だ。ノイズの ROM の添字 12 は 482 ティックのところに着地し、公表された 762 サイクルの項目なら 381 になるはずのところで、それが手順 3 の食い違いの中身だ。どちらもピンの規約の h=0 では 8 に立っていて、h = 0 mod 4 のフレームでティックする。矩形波と三角波はそれぞれ h = 2 mod 4 と、phi2 ごとにティックする。
プローブからの書き下ろしと、当てはめた定数 (apu::fit)
以下はすべて、ゲートを走らせ、コードの流れが最初に分かれたところを読み (APU_DUMP=1 がその周りの両方の流れの変化イベントを印字する)、ラング 0 の上でプローブでその仕組みを測り、定数を一つ釘付けして見つけたものだ。並び順は、ゲートが見つけた順だ。
- 矩形波の $4003 への書き込みは、そのタイマーには触らない。ダイのステップのカウンタ (
sq_c、7..0 と数え下がる) は、周期 0 のまま電源投入から 0 に留まり、周期がゼロでなくなった後の最初のティックで再読み込みして進む (seqのプローブ)。出力コードはステップから 2 半ステップ遅れる。 - 下位バイトの周期への書き込みは、タイマーの次のティックを減算ではなく再読み込みにする。 矩形波でも三角波でも同じだ: 三角波のティックのフレームはストローブのフレームなので、
tri_tはストローブそのもので周期を示す。矩形波の次のティックは三フレーム先で、デューティの落ちは、即座に読み込んだ場合より 1 ティック後に着地する。上位バイトの書き込みはタイマーに触らない。 - DMC の出力ユニットは、無音を設定した状態で電源投入からの完了回数を数える (
dmcseq): 有効化の後の 8 回目の完了でバッファがシフタに読み込まれ、次のバイトが取り込まれる。そしてそのときに初めて水準が動き、1 ビットあたり 2 ずつ動く。バイトの境界は読み込みと重なる。ノイズのシフトと DMC の完了は、タイマーを終端に着地させたティックから 3 半ステップ後に現れる。 - フレームの位相の立ち上がりは、エンベロープとスイープには 1 半ステップ後に届き (そのコードはそれを 2 半ステップの出力の遅れを通して示す)、三角波のリニアカウンタには 3 半ステップ後に届き (その出力に遅れは無い)、矩形波の長さのカウンタには 3 半ステップ後に届く (その満了は、同じクロックから見てノイズのそれより 2 半ステップ後に現れる)。
- 未実測で、公表された模型から書き下ろしたもの。どれもゲートのプログラムでは観測できない: $4003 の書き込みによるシーケンサのステップの再起動、エンベロープのループ、DMC の IRQ とその出力の 0 と 127 でのクランプ、そして $4015 のステータスバイト。
処理速度 (examples/bench.rs)
| エンジン | 半サイクル/s |
|---|---|
| ラング 0、メモリのハーネスを通して | 11,294 |
| コアのラングだけ | 41,926,421 |
| APU を付けたコアのラング | 32,067,032 |
APU を付けて ラング 0 の約 2,839 倍、2A03 の実時間の 9.0 倍。だから速さのために作ったものは何も無い。
持ち越し
- ストール (バスを止めること。手順 5): DMC の取り込みは RDY を 5 から 7 半ステップ保ち、スプライト DMA は 1027 か 1029 保つ。ラングのコアはまだそれを見ていない。
- 手順 2 からのリセットの保持。
$4015の読み出しと、その読み出しでのフレーム IRQ のクリアは書き下ろしで、運動させていない: ゲートのプログラムは一度も読まない。(2026-09-06 に閉じた:tests/reads.rs。README に註がある。)
手順 5: ストール (2026-09-05 に閉じた)
走行スタンプ: rustc 1.97.1、halfphi v0.1.6、v6502-micro は dc2dada。REQUIRE_NETLIST=1 cargo test --release -p v2a03-micro --test stalls: ビルドの後、1 秒未満で緑。MUTATE=1 はスプライト DMA を一対ぶん短くし、ゲートは最後の対で赤になる。
閉じたもの
src/rung.rs はピンのところでのチップ丸ごとだ: Rung は、コアのラング、コア自身の書き込みから与えられる APU、そして二つの DMA のユニットの上に、ピンの規約を実装する。DMA のユニットはコアからバスを取り、握っている間は自分のフレームを差し出す。コンソールが取り付くのはこれだ。
ゲート: Rung をラング 0 に対して CorePins を通し、全フレーム、全フィールド、RDY も含めて、三つのプログラムの上で比べる: $4014 のスプライト DMA を偶数の CPU サイクルで書いたものと奇数で書いたもの、そして DMC が自分の標本を取り込むもの。1,201 + 1,201 + 4,201 フレームが、全フィールドで同一 (write-phi1 のバイトは一つも生じない: これらのプログラムでは、比較の窓の中で書き込むのは DMA 自身だけで、そのバイトは一致する)。RDY が低なのは、そのうち 1,029、1,027、19 フレームだ。
実測 (v2a03-sim/examples/stall-probe.rs) と書き下ろし
- スプライト DMA。 RDY は $4014 の書き込み自身のフレームで落ちる。コアは次の読み出しサイクル、つまりオペコードの取り込みを、同期を高にして、少なくとも三フレーム先の最初の「get」のフレーム (h = 0 mod 4。偶数サイクルでの書き込みは二サイクル、奇数では一サイクル待つ) まで繰り返す。それから 256 対: get のサイクルで元のページを読み、その次の put のサイクルでそのバイトを $2004 へ書く。1,024 フレーム。RDY は最後の書き込みの次のフレームでまた高になり、保持されていた取り込みがそこで本当に一度走ってから、次のサイクルへ行く。
- DMC の取り込み。 RDY は、バッファを空にするバイト境界の完了から 1 フレーム後に落ちる。バッファが空で始まる場合は、有効化の書き込みから 6 フレーム後だ。コアは次の読み出しサイクルを繰り返す。標本のバイトは、少なくとも三フレーム先の最初の get のフレームで読まれる。RDY はその読み出しが始まってから 2 フレーム後に高になり、保持されていたサイクルが本当に走る。位相合わせによって 5 か 7 半ステップ。
- 二つのゲートが RDY について教えたこと。 ラング 3 は RDY が低のとき読み出しサイクルを保持し、解放されると次のサイクルをすぐに演じる。6502 リポジトリはいま、それを釘付けする台本を自分のラング 0 からさらに二つ記録していて (ストアの書き込みサイクルの内側で RDY が落ちる場合、phi1 のフレームで RDY が解放される場合)、ラング 3 はどちらもそのまま再生する。2A03 の DMA のユニットは一点で違う: 保持された読み出しは、RDY が立った後にもう一度走る。バスが RDY と一緒に戻ってくるからだ。
Rungはそのサイクルのあいだ RDY を高として差し出し、コアの解放を 1 サイクル後に与える。札を付けてある。 - コントローラのストローブと $4016/$4017 の読み出しは基板のもの (N4)。DMC の取り込みがスプライト DMA に重なる場合は運動させていない (両者のダイの上での調停は、両方が起きうるコンソールの層での測定だ)。
処理速度
DMA のユニットが待機しているラングは、手順 4 の 32.1 M 半サイクル/s に対して何も足さない。スプライト DMA は 1,024 フレームぶんの表引きだ。
スプライト DMA の内側の DMC の取り込み (2026-09-06、持ち越しの項目)
ラング 0 で実測した (tests/stalls.rs、衝突のプローブ): DMC が最速のレートで鳴っている間に $4014 の DMA が走り、NOP の橇で DMA を取り込みの上を越えて動かして、取り込みが DMA の十分内側、その開始の近く、そして最初のフレームに着地するようにした。書き込みの両方のパリティと、三つの標本アドレスで。ダイのやることは毎回同じだ: 標本の読み出しが、DMA が使おうとしていた get のサイクルを取り、コアの保持されたサイクルがその次のサイクルでバスに現れ、DMA は次の get のフレームで来る予定だった対から再開する。その間ずっと RDY は低だ。だから衝突の一回ごとに DMA が二サイクル延びる (1,027 か 1,029 半ステップの DMA が 1,031 か 1,033 になり、取り込みが二つ内側に着地すれば 1,035 か 1,037 になる)。ラングはいまそれをやり (SpriteDma::collision)、どの場合のどのフレームも、フィールドごとに一致する。
ダイが標本をどこから読むかは、規則ではなく発見だ。ラング 0 では、衝突によって DMC のアドレスレジスタが乱される: $C001 の標本は $8000 から読まれ、$C041 は $8040 から、$C0C1 は $8040 から読まれ、レジスタはそこに留まるので、DMA の後の次の取り込みは $8001 を読む (衝突が DMA の開始の近くだった場合は、また $8000 を読む)。文書化された実機は自分のカウンタから標本を読み、スプライト DMA を越えて標本を鳴らすゲーム、つまりほとんどのゲームがそう言っている: これをやる実機なら、どの衝突の後もゴミを鳴らすことになる。だからこれは、誰も文書にしていない本物の癖か、この隅でのスイッチレベルの模型自身のもの (ANC と ASR のバスの取り合いがそうであるように) のどちらかで、実機に当てたロジックアナライザが決める。ラングは文書化されたアドレスを保つ。ゲートは衝突のフレームの他のフィールドをすべて押さえ、アドレスを名前の付いた一つの類として数え、印字する。MUTATE_COLLISION=1 は取り込みにサイクルを取らせないようにし、その後の DMA の全フレームで赤になる。
DMC の取り込みの下でのジョイパッドのクロック (2026-09-06、ベンチのために)
コントローラのポートのクロックは 2A03 の /OE1 ピン、ダイの /r4016 で、$4016 の読み出しのあいだ低になる。ベンチの B0 のゲート (tinymachines/nes-bench) は実機のラッチごとのクロック数を数え、そこで訊かれるのは、DMC の取り込みがその読み出しに着地したら何が起きるかだ。ダイが先に答えた (v2a03-sim/examples/joy-clock-probe.rs: 最速のレートでループする標本、ストローブしたパッド、ループの中の LDA $4016):
- 取り込みが着地する読み出しでは、/r4016 が読み出しと一緒に落ち、停止のサイクルを通して低のまま (一本の連続したパルス: 停止のサイクルは再パルスしない)、取り込み自身の標本の読み出しの間に立ち、コアが RDY を高にして読み出しをやり直すときにまた落ちる。立ち上がりが二回なので、ポートの 4021 は二度シフトし、コアは自分が求めたビットの次のビットを取る。7 サイクルのループでは、7 回に 1 回の取り込みがそこに着地する。残る 6 回は JMP のサイクルに着地し、一度だけパルスする。
- ただし、標本のアドレスがポートの下位 5 ビットを持っているときは違う: $C016 からの取り込み (および $C036、$C0F6、$C196。二つのループの調子にわたって 20 個の別名) は /r4016 を取り込みを通して低のまま保ち、パッドは一度だけクロックされる。どの別名を普通に読んでも ($C016、$8016、$4116、$4036) 低には引かれず、コアが待機している状態で $C016 から取り込んでも引かれない。つまりこのストローブは、上位のアドレスビットをコアの保持されたアドレスから、下位 5 ビットを DMA が駆動するピンから解読している。これはダイのデコーダについての事実であって、パッドについての事実ではない。
ラングは二箇所でそれに従う。ラング 3 (tinymachines/6502 @ 89ae24f) は、保持された読み出しの phi2 ごとに自分のバスへ訊き直し、最後のバイトを保つ。DL がやることがそれだ。RungBus は、コアが保持されている間その訊き直しをメモから答え、やり直しのときに一つだけ通す。ただし取り込みのアドレスがポートの下位 5 ビットと別名になる場合は通さない。tests/joypad.rs は、ラング 0 での命令ごとの /r4016 の立ち下がり数と、同じプログラムでのラングでの命令ごとのバスへの問いかけ数を数え、両方の調子で (22 回目の取り込みが $C016 から、54 回目が $C036 から)、その列が等しいことを押さえる: {1: 2820, 2: 6} と {1: 2245, 2: 4}、命令ごとに。MUTATE_QUIET=1 (保持中の訊き直しがすべて外の世界に届き、衝突ごとに五つ) と MUTATE_HELD=1 (ラング 3 が最初のバイトを保ち、一つ) は、どちらも最初の衝突で赤になる。
これがコンソールに意味すること: その基板は、実機がパッドをクロックするのとちょうど同じときに $4016 を訊かれ、コアが読み込むバイトはやり直しのほうのものだ。ゲームが回避している「DMC の DMA がコントローラの読み出しを壊す」挙動 (二度読んで比べる) は、いま測定によって模型の中にある。しかも、文書が触れていない別名の例外付きで。実機自身の数、ポーリングで 9 クロックは、ベンチが取るものだ。
M2 ピンとコアのクロック (2026-09-07、ベンチのために)
ベンチの位相合わせの分類器 (tinymachines/nes-bench、B2) はスコープから M2 を読み、コンソールの Alignment はコアの clk0 の上で定義されているので、その間のオフセットは数字でなければならなかった。v2a03-sim/examples/m2-phase.rs: M2 は clk0 が落ちるまさにその半ステップで落ち、それは phi1 の始まりだ。そして clk0 が立ち上がる 3 半ステップ前に立つので、24 マスタ半ステップのうち 15 で高になる。それが、実機が文書化されている 62.5 パーセントのデューティであり、スイッチから読んだものだ。
N3 の現状
手順 1 から 5 が閉じた。チップは v2a03-micro だ: 十進の補正を切り離しスタックポインタを種付けしたラング 3 をコアとし、APU はラング 0 から測った表の周りに書き下ろし、どのタイミングもラング 0 のコードの流れに当てはめ、二つの DMA のユニットはフレームの測定から書き下ろし、その全体をピンのところと五つの出力コードのところでスイッチレベルのチップに押さえた。実時間の約 9 倍。持ち越し: リセットの保持 (手順 2)。$4015 の読み出しは 2026-09-06 に閉じ (tests/reads.rs)、スプライト DMA の内側の DMC の取り込みも同じ日 (上)。そのダイの上でのアドレスはベンチのための発見であり、DMC の取り込みの下でのジョイパッドのクロック (上) も、ダイで実測して tests/joypad.rs が押さえている。
ビルド時に 2a03/docs/n3-report.md から取り込まれる。写しはリポジトリの一つだけだ。報告は独自の作業用の言葉をいくつか使う: 報告書で使われる言葉。