6502tinymachines

ベンチの計画

2026-09-06 に、ファームウェアを一行も書く前に書いた。コンソールのスケッチの機械 側のマイルストーン N0 から N8 は閉じており、その報告に残る未決の項目はすべて ベンチか机の項目だ。この計画はベンチをワークステーションが駆動する機械にする。 そうすればそれらの項目は無人で閉じ、繰り返せて、一回の座り込みではなくゲートに なる。

手元の装備: 純正のパッドとカートリッジ付きの NES-001、LAN にある Rigol DS1054Z (1 チャンネルをコンソールの映像に当ててある。2026-09-02 の捕捉はこれから来た)、 カートリッジを吸い出すための Open Source Cartridge Reader、ESP32 の基板、 Raspberry Pi 4、リレー。

ループ

コンソールが手から学ぶことのすべては、コントローラポートの二本の線を通って来る: ラッチのパルス一発、それからパッドの中の 4021 から 8 ビットを押し出す 8 発の クロックパルスだ。模型のコントローラ (nes-glue::controller) はまさにそこから 書き下ろされている: OUT0 が高の間 4021 は読み込み、その立ち下がりがボタンを ラッチし、$4016 の読み出しごとに 1 ビットが出て行く、A が最初。だから入力の履歴 は、リセットからのラッチ数で番号を振ったバイト列であり、実機と模型に同じことを 意味する。

ブリッジはコンソールのポートとパッドの間に座る:

  • 傍受。 リセットからのラッチ数と、ラッチあたりのクロック数をハードウェアで 数える。各ラッチでシフトレジスタが保持していたバイトを記録する。
  • 注入。 パッドのバイトの代わりに、ラッチ番号で引いた台本のバイトを保持する。
  • トリガ。 選んだラッチ番号で線を上げ、スコープの外部トリガに渡す。そうすれば 捕捉は、既知の入力履歴の既知のフレームに着地する。

リレー二つ、リセットと電源が、セッション全体を台本の下に置く。ヘッドは LAN 上の Raspberry Pi 4 で、ワークステーションから台本を受け取り、USB シリアルで ブリッジを駆動し、自分のピンでリレーを駆動し、SCPI でスコープと話す。 ワークステーションは同じ台本で模型を走らせ、比べる。

なぜソフトウェアではなくシフトレジスタなのか

ポート上のクロックパルスの幅は数百ナノ秒で、次のビットは約 7 マイクロ秒後の 次の読み出しより前にデータ線に乗っていなければならない。74HCT165 はそれを数十 ナノ秒でやり、一度も外さない。ESP32 の割り込みは RTOS の下で 2 から 3 マイクロ秒の遅れがあり、たいていは間に合うだろう。ときどき混じる誤ったビットは、 実機についての発見とまったく同じ顔をする。だからレジスタがパッドであり、 コンソールは 4021 を叩くのと同じようにそれを叩き、ESP32 はポーリングの合間に レジスタの 8 本の入力を書くだけ、そしてパルス計数の周辺回路でパルスを数える。 これなら経路に CPU は要らない。

純正のパッドはコンソールではなくブリッジに差す。ESP32 は自分のラッチとクロックで キロヘルツの速さでそれをポーリングし、パッドは 3.3 V で電源を取って 4021 が 3.3 V のロジックを話すようにしてあり、状態をレジスタに写す。ボタンを押せば 1 ミリ秒のうちにコンソールへ届き、コンソールが各ポーリングで読んだものは、構成上、 レジスタが保持していたものになる。

マイルストーン

それぞれ、ここに述べたゲートで閉じる。コードが存在する前に述べてある。許容は ゲートと一緒に述べてあり、ゲートが守るべきものが無いのに通るゲートは壊れた ゲートだ (一族のどこでもそうであるように、MUTATE)。

B0: 傍受

ブリッジはパスモード、コンソールにはゲームのカートリッジ、純正のパッドには手。 記録はラッチごとに 1 行: ラッチ番号、バイト、その区間のクロック数。

ゲート:

  • DMC の無いゲームでのラッチあたりのクロック数: 1 分間のプレイのあいだ、どの ポーリングでも 8。ほかの値の数を印字し、それがゼロであること。
  • フレームレートに対するラッチの割合: スコープで測った 1 分間のラッチ数を、 映像チャンネルのフィールド数と突き合わせる。1 フレームに 1 回ポーリングする ゲームで、比が数え上げ自身の分解能の内側で 1 であること。
  • 同じゲームを模型で走らせ、その $4016 のストローブの立ち下がりをフレームごとに 数える: 実機と同じフレームあたりポーリング数が、並べて印字されること。
  • DMC が鳴っているゲームで: 9 クロックを数えたポーリングをラッチ番号で並べ、 同じ入力履歴に対する模型の DMC フェッチ予定表と並べる (記録を再生する。B3 の 仕掛けを早めに使う)。一致することが測定であり、食い違いはどちらかについての 発見になる。
  • MUTATE: ブリッジのクロック計数器にラッチ線を食わせたら、ラッチあたり 8 の検査は 失敗しなければならない。

B1: 注入

台本: リセット、それからラッチ番号ごとのバイト、ラッチ T でトリガ。実機は ブリッジを通してそれを走らせ、模型は同じポーリングで set_pad を通して走らせる。 スコープは EXT TRIG の T でトリガされてフレームを捕捉し、ntsc-crt の復元が それを読み返し、同じポーリングでの模型のフレームが同じ連鎖を通して描かれる (N6 の往復。カラーバーのカートリッジで、四つの輝度の行すべてで 13 領域中 13 で 閉じたもの)。

ゲート:

  • まずカラーバーのカートリッジ、終端付きで: N6 のゲートの領域と許容を、実機で。 これは N6 が開けたままにしたベンチの項目であり、トリガ無しの捕捉の彩度 +28% がプローブのものか DAC のものかという問いへの答えだ。
  • 次にゲーム: T で復元したフレームを模型のそれと比べ、N6 が採点するやり方で採点 する (フレームの平らな領域について、領域ごとの輝度、色相、彩度。許容は N6 の 報告が述べるもの)。最初に落ちた領域を、名指しで。
  • リセットから最初のラッチまで: リセットのリレーが離れた後の最初のポーリングの ラッチ番号を、実機で 20 回のリセットにわたって模型のそれと比べ、ばらつきを 記録する。
  • MUTATE: 模型の側だけ台本を 1 ラッチずらしたら、絵が入力に依存するゲームで フレームの比較は失敗しなければならない。

B2: リレーと、無人でやる項目

リセットと電源のリレーを台本の下に。スコープの残りのチャンネルをマスタクロック、 M2、ALE に。

ゲート:

  • 電源投入をまたぐアライメント (クロックの位相の揃い方): 台本による電源投入 100 回、毎回、捕捉から読んだマスタクロックに対する CPU の位相、そして四つ (以上) の アライメントについてのヒストグラムを印字する。模型の Alignment::MEASURED (cpu_phase 4、ppu_phase 3) は一回の実測だった。これはその分布であり、 コンソールに好みのアライメントがあるかどうかが発見になる。
  • リセットの連鎖: リレーが離れてから最初のフェッチまでの保持時間を、捕捉の上で。 nes-glue の札の付いた仮の値を置き換える。
  • ミキサー ROM の下での AUDIO_OUT (N7 の項目) を、スコープの空きチャンネルを 音声ジャックに当てて、模型の段に対して採点する。
  • ここではやらない: スプライト DMA の衝突下での DMC のアドレスレジスタには アドレスバスが要り、16 チャンネル要る。これはロジックアナライザの項目のままで、 そう述べておく。
  • MUTATE: マスタクロックのチャンネルを 1 周期ずらした捕捉をアライメントの分類器に 食わせたら、ヒストグラムは動かなければならない。

B3: 記録と再生

純正のパッドの上での人間の走行を B0 が記録し、B1 が実機と模型で再生する。実機を 自分自身の記録に対して再生するのが実機の決定性の検査で、模型を実機に対して 比べるのがゲートだ。

ゲート:

  • 実機と実機: 一つの記録を二度再生し、同じ K 個のラッチ番号でトリガした捕捉が、 B1 の許容の下で領域ごとに一致すること。一致しないゲーム (初期化されていない RAM やフレームのタイミングから種を取るもの) は、そういうものとして名前で記録 する。
  • 模型と実機: トリガ番号についての二分探索で、トリガした捕捉と模型のフレームが 食い違う最初のラッチ番号を特定し、名指しする。走行の長さいっぱい一度も食い違わ ないなら、それは一族が存在する目的そのものの結果だ。

部品と、その置き場所

  • firmware/: ブリッジのスケッチ (v1 は ESP32-C6 で bridge/、組んだとおりのものは UNO で bridge-uno/)。USB シリアル越しの行プロトコルで、形は halfwave のものと同じ: MODE PASS / MODE INJECT、SET hh (いま保持する バイト)、AT n hh (ラッチ n から保持するバイト)、TRIG n、RESET (計数器を ゼロに)、そしてラッチごとに 1 行の L n hh c の流れ。テキストなので、デーモンと 端末が同じものを読む。
  • head/: Pi のデーモン。ワークステーションから UDP で台本を受け取り、それを ブリッジとリレーの上で演奏し、SCPI でスコープを構えて読み、記録と捕捉を返す。 Pi のアドレスとスコープのアドレスは bench.local.md にあり、無視設定で、 コミットには決して入らない。
  • tools/: ワークステーションの側。台本の形式、模型のランナー (同じ台本の上の nes コンソールと、そのフレームを ntsc-crt で描いたもの)、比較、そして各 ゲートが名指しする検査。

2026-09-07 追加: 回路図、v2、パッドアダプタ

ベンチの電子回路レビューが四枚のシートと一つのビルド文書を返してきた。 docs/bench-build-v1-v2.md として、docs/bench-v1.svg、bench-v2.svg、 logical-timing.svg、pad-adapter.svg と一緒に取り込んだ。どれも tools/draw-schematics.py が描き、tools/check-sheets.py がこのリポジトリの 配線表に対して押さえている。計画に足すものは三つ:

  • v1 は現状の計画をそのまま回路図にしたもので、9 段の組み立て順序が付いて いる。5 V のものが測られる前に C6 と出会わないように選んだ順序だ。そしてタイミ ングのシートが見えるようにしたファームウェアの変更が一つ: レジスタは OUT0 が 高の間に読み込むので、その 8 本の入力はいま一回のストアで、しかも前後で OUT0 が 低に読めている間だけ書かれる。B0 は v1 でやる。
  • v2 は B0 から B3 が走った後のブリッジだ: バイトが一つのエッジで変わるように レジスタの前に 74HC595、二人ぶんの履歴のための二つ目のポート (AT2、L2)、 そして LM1881 の同期分離器を入れて、どのラッチも自分が着地したフィールドと 行を持つようにする。ハードウェアで測るので、B0 のフレームあたりポーリング数の ゲートがスコープ無しでブリッジの内側で閉じる。C6 の四つの計数器と 14 本のピンを 全部使う。まだマイルストーンではない: v1 の測定がそれを求めたときに マイルストーンになる (記録の中の裂けたバイト、二人プレイのゲーム、あるいは フレームの中の位置をスコープが与えられないポーリング)。
  • パッドアダプタは、ブリッジの poll_pad の後ろに無線を付けたものだ: 純正の パッドを、電話向けの BLE か USB HID のキーボードまたはゲームパッドにし、 ヘッドに餌を送るベンチモードも持たせる。キーボードの割り当てはプレイページの もの (A は x、B は z、Select は右の shift、Start は enter、十字は矢印) なので、 アダプタを付けた電話は追加のコード無しで /nes/play を動かせる。USB に使う 部品は ESP32-S3 で、手元の C6 は BLE ができる。これは自前のマイルストーンで、 ベンチの四つの後。(組んだとおりのもの: ESP32-P4-Module、USB、3.3 V の純正パッド、 2026-09-27 に Linux のホストですべてのキー。pad-usb-protocol.md。)

2026-09-08 追加: v1b、UNO のブリッジ、それに合うファームウェア

部品が届き、そのうちの一つが組み立て順序を決めた。74HC の系列は 5 V で走るとき 高として 3.5 V を要求し、ESP32-C6 が出すのは 3.3 V なので、図面どおりの v1 は レベル上げの変換器を足さないと棚のキットからは組めない。Arduino UNO の ATmega328P は 5 V の部品なので、それを中央に置くと三つの電圧領域が一つに潰れる:

  • 74LVC245 は要らない。UNO がコンソールの OUT0 と CLK を直接読む。
  • レジスタへのレベル上げも要らない。UNO が 74HC595 を 5 V で駆動する。
  • パッドはそれが作られた 5 V でポーリングされるので、4021 が 3.3 V で走るかを 先に測るという計画の項目は、もう何も止めない。

いまは v1b が最初に組むもので、docs/bench-v1b-1.svg と docs/bench-v1b-2.svg として描き、docs/bench-v1b-uno.md に文書化し、 firmware/bridge-uno/bridge-uno.ino として書いた。これは arduino:avr:uno 向けにコンパイルできる。v1 は、v2 の四つの計数器が必要になったときの C6 版として 一組の中に残る。マイルストーンとそのゲートは変わらない: B0 から B3 は、どの マイコンがレジスタを持っているかを気にしない。行プロトコルは同じであり、 head/headd.py とすべての道具は、基板ではなくプロトコルに対して書かれているから だ。

その結果、計画自身の主張が二つ動いた。どちらも、誰かが議論したからではなく、 コンパイラか計器がそう言ったからだ:

  • ブリッジの予定表はハードウェアの限界であって、一つの数字ではない。 C6 は 2048 個の AT 項目を持てる。ATmega328P は SRAM が全部で 2 KB しかなく、128 個 持てる。それより長い B3 の記録は、尻尾が欠けたまま再生されていたはずで、それは 実機についての発見の衣装を着た、間違った入力履歴だ。いまは三箇所が代わりに 拒む: 走行の前の tools/b3.py record、ハードウェア無しでその失敗を予習できる ようにする tools/fake-bridge.py、そして、ブリッジが # schedule full と 答えた走行をすべて止めるヘッドだ。
  • B0 の妨害にジャンパは要らない。 ラッチ線は D5 に着く。ここは Timer1 の外部 クロック入力でもあり PCINT21 でもあるので、クロック計数器にラッチ線を食わせる のはソフトウェアででき、コマンドで元に戻せる。ゲートは牙を保ち、ベンチは手を 出さずに済む。

これが閉じるもの、閉じないもの

一族の報告から閉じるもの: N5 のゲート 3 (本物のカートリッジ、棚から吸い出し済み)、 N6 の終端したカラーバーの捕捉とプローブか DAC かという問い、N7 のミキサー ROM の 下での音声、N8 のパッドが手元にあること、電源投入をまたぐアライメント、リセットの 連鎖の保持時間。足すもの: ジョイパッド読み出し時の DMC の二重クロックを、模型が 予測する測定として。

閉じないもの: 衝突下での DMC のアドレスレジスタ (アドレスバスが要る)、本物の画面 の上のシェル (これは机の項目で、ベンチの項目ではない)、そして映像信号が運ばない PPU の内部についてのすべて。

ビルド時に nes-bench/docs/bench-plan.md から取り込んだもの。リポジトリが唯一の写しだ。報告は独自の作業用の言葉をいくつか使う: 報告書で使われる言葉。