The problem問題One set of pins, two jobs一組接腳,兩份工作
To save pins, the 2C02 PPU multiplexes its address and its data onto the same eight external pins, AD[7:0]. A single control line, ALE (Address Latch Enable), says which is on the bus right now: while ALE is high the pins carry the low byte of the address; a board-level octal latch (a 74LS373, modelled in S1 at the transistor level) grabs that address on ALE's falling edge and holds it; then the pins flip to carrying data for the rest of the cycle.
為了省接腳,2C02 PPU 把它的位址和資料多工在同一組八根外部接腳 AD[7:0] 上。一條控制線 ALE(Address Latch Enable)說明現在匯流排上是哪個:ALE 為高時,接腳承載位址的低位元組;板上一顆八位元閂鎖(74LS373,在 S1 裡用電晶體級建模)在 ALE 下降沿抓住那個位址並保持;接著接腳翻成承載資料,度過該週期剩下的時間。
The CPU's $2007 read — the way software reads PPU video memory — has to sample the returned data at exactly the right instant in that dance. In real silicon the analog rise/fall times, the '373's propagation delay, and the CPU/PPU clock phases all line up so the data is valid when the CPU latches it. The figure below is that dance:
CPU 的 $2007 讀取 —— 軟體讀 PPU 影像記憶體的方式 —— 必須在這支舞的正確一瞬間取樣回傳的資料。在真矽上,類比的上升/下降時間、'373 的傳播延遲、CPU/PPU 的時鐘相位全都對齊,讓 CPU 閂鎖那一刻資料是有效的。下圖就是這支舞:
$2007 read strobe lands ~1 CPU cycle before the data has settled and the '373 window is right — the misalignment the mux exists to correct.多工的 AD 匯流排。引擎的 $2007 讀取脈衝落在資料 settle 完、'373 窗對齊之前約 1 個 CPU cycle —— 正是 mux 存在要修的錯位。Why the engine needs a fix at all為什麼引擎需要修Discrete time has no inertia離散時間沒有慣性
S1 is a discrete, event-driven switch-level simulator. It advances the clock one half-cycle at a time and settles the transistor network to a steady state before moving on. It has no notion of a signal rising over 300 nanoseconds; a node is 0, then on the next settle it is 1. Real silicon does not work that way: a node driven onto 20 picofarads of package-and-board capacitance climbs its voltage as an RC curve, and the '373 and the CPU sample it only once it has crossed a threshold. Those analog delays are exactly what makes the timing line up — and they are exactly what a zero-delay engine throws away.
S1 是一個離散、事件驅動的開關級模擬器。它一次推進時鐘一個半週期,把電晶體網路 settle 到穩態才前進。它沒有「訊號在 300 奈秒內上升」這種概念;一個節點是 0,下一個 settle 就是 1。真矽不是這樣運作:一個節點驅動到 20 皮法的封裝與板電容上,電壓沿 RC 曲線爬升,而 '373 和 CPU 只在它越過閾值後才取樣。那些類比延遲正是讓時序對齊的東西 —— 也正是零延遲引擎丟掉的東西。
So on the multiplexed AD bus the engine's CPU-side read fires a full CPU cycle before the data-side settle would have happened in analog reality. The ALERead mux is a test-mode model that re-inserts the missing timing. It does two things: a load-time node-split (it cuts the shared ppu.io_ab ↔ cpu.ab node so the address and data phases can be sampled cleanly), and a small phase multiplexer that holds certain nodes LOW across three timing windows — internally named swallow, replay, and ale-freeze — until the correct instant arrives. Those windows are measured in half-cycles, and their five durations are our MUX_HC = 13, 13, 25, 44, 52.
所以在多工的 AD 匯流排上,引擎的 CPU 側讀取比類比現實裡資料側該 settle 的時刻早了整整一個 CPU cycle。ALERead mux 是一個測試模式模型,把缺掉的時序補回去。它做兩件事:一個載入期 node-split(把共用的 ppu.io_ab ↔ cpu.ab 節點切開,讓位址與資料相位能乾淨取樣),和一個小的相位多工器,在三個時序窗 —— 內部叫 swallow、replay、ale-freeze —— 裡把某些節點壓 LOW,直到正確一瞬間到來。這些窗以半週期計,五個時長就是我們的 MUX_HC = 13, 13, 25, 44, 52。
Step 1 — the unit第一步 —— 單位What is one half-cycle worth?一個半週期值多少?
Everything downstream depends on converting hc (the engine's half-cycle tick) into nanoseconds. We do not assume it — we measure it from the engine's own run log. A control run recorded, at simulated second 39.93, a half-cycle count of hc = 1,715,304,000. That is a rate:
後面每一步都依賴把 hc(引擎的半週期 tick)換成奈秒。我們不用假設 —— 直接從引擎自己的 run log 量。一個對照 run 記錄到:模擬第 39.93 秒時,半週期計數 hc = 1,715,304,000。這是一個速率:
NTSC master clock = 21.477 MHz
hc_rate / master = 42.96 / 21.477 = 2.0000 → one hc is exactly half a master-clock cycle
1 hc = 1 / 42.96 MHz = 23.28 ns
So the engine ticks at twice the NTSC master clock — one hc per master half-cycle, 23.28 ns. From there the NES's fixed divider ladder gives every other unit for free:
所以引擎以 NTSC master clock 的兩倍在 tick —— 每個 master 半週期一個 hc,23.28 ns。從這裡,NES 固定的除頻階梯免費給出其他每個單位:
1 PPU dot = 4 master = 8 hc = 186 ns (the PPU clock is master/4)
1 CPU cycle = 12 master = 24 hc = 559 ns (the CPU clock is master/12)
and therefore 1 CPU cycle = 24 hc = 3 PPU dots
Keep that last line in view: 1 CPU cycle = 3 dots = 24 hc. It is the ruler we are about to lay the five numbers against.
記住最後一行:1 CPU cycle = 3 dots = 24 hc。這就是我們接著要拿五個數字去比對的尺。
Step 2 — decompose第二步 —— 拆解Five numbers, three meanings五個數字,三種意義
Now convert each value into physical time, into CPU cycles, and into dots — and a structure jumps out. The five are not one kind of thing; they fall into three groups.
現在把每個值換成物理時間、換成 CPU cycles、換成 dots —— 結構就跳出來了。這五個不是同一種東西;它們分成三組。
| hc | ns | CPU-cyc | dots | what it is它是什麼 |
|---|---|---|---|---|
| 13 | 303 | 0.54 | 1.6 | physical — AD-bus rising-edge settle (a weak pull-up into ~20 pF)AD 匯流排上升沿 settle(弱上拉驅 ~20pF) |
| 13 | 303 | 0.54 | 1.6 | physical — the second settle window, same scale第二個 settle 窗,同尺度 |
| 25 | 582 | 1.04 | 3.1 | the phase error itself — 1 CPU cycle = 3 dots = the "$2007 read is one cycle early"1 CPU cycle = 3 dots =「$2007 讀取早一個 cycle」 |
| 44 | 1024 | 1.83 | 5.5 | architectural — a swallow/replay scheduling window (µs-scale)swallow/replay 排程窗(µs 級) |
| 52 | 1210 | 2.17 | 6.5 | architectural — the second scheduling-window extent第二個排程窗範圍 |
The reason a magnitude check settles the whole question is that the three groups live at three utterly different time scales. A wire on this die switches in tens of nanoseconds; a scheduling window at 582–1210 ns is not a wire delay, it is a clock-phase count. So each group must be explained by a different mechanism — which is exactly why calling all five "calibration" undersells four of them.
為什麼一個量級檢查就能定案整個問題 —— 因為這三組活在三個天差地遠的時間尺度。這顆晶粒上一條線在幾十奈秒內切換;一個 582–1210 ns 的排程窗不是線延遲,是一個時鐘相位計數。所以每一組都得用不同機制解釋 —— 這正是為什麼把五個全叫「校準」低估了其中四個。
Step 3 — the 13 is physics第三步 —— 13 是物理Computing the rising-edge settle算上升沿 settle
The two 13s are a real analog settle time, and we can compute it from first principles. When the PPU drives the AD bus high, the pull-up is a depletion-load NMOS transistor — deliberately weak, always-on. It has to charge the whole external bus capacitance: the die pad, the DIP-40 lead, a few inches of PCB trace, the cartridge edge, and the ROM's input gate. Add those up and you get roughly 20 pF. A weak depletion load looks like a resistor of order 30 kΩ. The node climbs as a first-order RC charge and is "seen" as high once it crosses the NMOS logic input threshold, ~2.0 V out of a 5 V rail.
兩個 13 是真實的類比 settle 時間,而我們能從第一性原理算出它。當 PPU 把 AD 匯流排驅動高時,上拉是一顆 depletion-load NMOS 電晶體 —— 刻意做得弱、常開。它得對整個外部匯流排電容充電:晶粒 pad、DIP-40 引腳、幾吋 PCB 走線、卡帶邊緣、ROM 的輸入閘。加起來大約 20 pF。一個弱 depletion 負載看起來像 30 kΩ 量級的電阻。節點沿一階 RC 充電,一旦越過 NMOS 邏輯輸入閾值(5V 電軌裡的 ~2.0 V)就被「看成」高。
V(t) = VDD · (1 − e−t / RC)
solve for the threshold crossing V(t) = VIH = 2.0 V:
trise = −R · C · ln( 1 − VIH / VDD )
= −(30 kΩ)(20 pF) · ln( 1 − 2.0 / 5.0 )
= −(600 ns) · ln(0.6) = −(600 ns)(−0.511)
= 306 ns
306 ns / 23.28 ns per hc = 13.2 hc → MUX_HC's 13, to the tick
That is the whole point: the calibrated 13 is not free — it is 13.2 hc computed, the rising-edge settle of the bus. And it explains why there is a rising window at all and not a symmetric one. The falling edge runs through the strong enhancement pull-down, not the weak load:
這就是重點:校準出的 13 不是自由值 —— 它是算出來的 13.2 hc,匯流排的上升沿 settle。而它解釋了為什麼會有一個上升窗、而非對稱的。下降沿走的是強 enhancement 下拉,不是弱負載:
tfall = −(500 Ω)(20 pF) · ln( VIL / VDD ) = −(10 ns) · ln(0.8/5.0)
= 18 ns = 0.8 hc → far too fast to be one of the tuned windows
So the asymmetry is real and predicted: the fall is under one hc (invisible to the mux), the rise is ~13 hc (one of the two settle windows). The physics requires a ≈13-hc rising window and forbids a matching falling one — which is exactly the shape of the tuned numbers.
所以這個不對稱是真實且被預測的:下降不到一個 hc(mux 看不見),上升 ~13 hc(兩個 settle 窗之一)。物理要求一個 ≈13-hc 的上升窗,並禁止一個對應的下降窗 —— 這正是那些手調數字的形狀。
Step 4 — the 25 is the error itself第四步 —— 25 就是誤差本身Not a delay — a phase count不是延遲 —— 是相位計數
The 25 is the easiest of all, because we already named it. Recall the ruler: 1 CPU cycle = 24 hc = 3 dots. The 25-hc window is 1.04 CPU cycles, 3.1 dots — it sits right on the 1-CPU-cycle line. And the ledger's own description of the bug is "the $2007 read access arrives one CPU cycle early", which the campaign notes record independently as "pure M6 phase, 3 dots early". Those are the same statement (1 CPU cycle = 3 dots), and the mux window that compensates it is 25 hc ≈ 24 hc.
25 是全部裡最簡單的,因為我們已經幫它命名了。回想那把尺:1 CPU cycle = 24 hc = 3 dots。25-hc 的窗是 1.04 CPU cycles、3.1 dots —— 正坐在 1-CPU-cycle 線上。而總帳自己對這個 bug 的描述是「$2007 讀取存取早一個 CPU cycle」,戰役筆記獨立記成「純 M6 相位、早 3 dots」。那是同一句話(1 CPU cycle = 3 dots),而補償它的 mux 窗就是 25 hc ≈ 24 hc。
So the 25 is not a physical delay at all — it is the misalignment itself, expressed as a phase count. In a fully-derived model you would not tune it; you would write it as the known CPU/PPU divider offset. It is the orange arrow in the first figure.
所以 25 根本不是物理延遲 —— 它是錯位本身,以相位計數表示。在完全推導的模型裡你不會調它;你會把它寫成已知的 CPU/PPU 除頻 offset。它就是第一張圖裡那支橙色箭頭。
Step 5 — the 44 and 52第五步 —— 44 和 52The genuinely tuned pair真正靠調的一對
Which leaves 44 and 52 — the two that are honestly still calibration. At 1024 ns and 1210 ns they are ~2 CPU cycles, far above any wire delay. They are the extents of the swallow and replay scheduling windows: how long the mux holds the reconstructed signals while the discrete engine's clocks catch up to the analog reality. They are architectural — a property of how the mux bridges two clock domains, not of any single net — so, unlike the 13 and the 25, there is no clean closed form to compute them from. In a derived model they would be expressed as arithmetic on the phase offset (roughly 2 · CPU_cycle ± a settle), but pinning the exact hc still takes a measurement.
剩下 44 和 52 —— 誠實說仍然是校準的兩個。1024 ns 與 1210 ns,是 ~2 個 CPU cycles,遠高於任何線延遲。它們是 swallow 與 replay 排程窗的範圍:mux 把重建訊號 hold 多久,好讓離散引擎的時鐘追上類比現實。它們是架構性的 —— 是 mux 如何橋接兩個時鐘域的性質,不是任何單一網的性質 —— 所以不像 13 和 25,沒有乾淨的封閉式可算。在推導模型裡它們會表示成相位 offset 的算術(大致 2 · CPU_cycle ± 一段 settle),但釘死精確 hc 仍需一次量測。
Tally結算
13, 13 — computed physics (RC rising-edge settle, 13.2 hc)計算物理(RC 上升沿 settle,13.2 hc)
25 — a named constant (the 1-CPU-cycle phase error)一個命名常數(1-CPU-cycle 相位誤差)
44, 52 — architectural window extents, still tuned架構窗範圍,仍靠調
Five hand-tuned knobs reduce to two computed values + one named constant + two architectural extents. Four of the five are no longer free.五個手調旋鈕化約成 兩個計算值 + 一個命名常數 + 兩個架構範圍。五個裡有四個不再自由。
From tuned to derived從調到算The one measurement that would close it能封閉它的那一次量測
Could we remove the tuning entirely? Yes, in principle — but not with the on-die delay model (M3 Elmore). The values that matter here cross a boundary M3 cannot see: the PPU pad driver, the ~20 pF of package + board parasitics, and the '373's own propagation. The right tool is a small, targeted boundary micro-SPICE: export just the PPU AD-pad driver, a lumped 20 pF board capacitor, the '373 (or its datasheet numbers — LE-to-Q ≈ 18 ns), and the CPU receiver into an analog transient simulation (ngspice), stimulate the internal ALE, and measure the true continuous-time delay from ALE to the CPU node crossing 2.0 V. Divide by 23.28 ns and you have the physical windows in hc with no tuning; the architectural windows then fall out as phase_offset − physical_settle. That is the path from a CALIBRATED fate to a fully computed one — and this article is the map for it.
我們能完全拿掉調參嗎?原則上可以 —— 但不能用晶粒內延遲模型(M3 Elmore)。這裡重要的值跨過一條 M3 看不到的邊界:PPU pad 驅動器、~20 pF 的封裝 + 板寄生、以及 '373 自己的傳播。對的工具是一個小而精準的 boundary micro-SPICE:只把 PPU AD-pad 驅動器、一顆集總 20 pF 板電容、'373(或它 datasheet 的數字 —— LE-to-Q ≈ 18 ns)、和 CPU receiver 匯出到一個類比暫態模擬(ngspice),激勵內部 ALE,量從 ALE 到 CPU 節點過 2.0 V 的真實連續時間延遲。除以 23.28 ns,你就得到不含調參的物理窗(以 hc 計);架構窗則以 phase_offset − physical_settle 掉出來。這就是從 CALIBRATED 命運走到完全計算的路 —— 而這篇文章就是它的地圖。
ALEREAD_MUX=1 for the ALERead sub-test, and the golden benchmark checksum is untouched. It is a required mechanism with a real switch-level '373 behind it — its only impurity is that one line of tuned timing, four-fifths of which this article has now grounded.為什麼它保持 opt-in。讓 ALERead 運作的 node-split 是載入期圖變更 —— 若全域開著,會替其他每一顆測試重擲開機對齊的彩票。所以 mux 只在 ALEREAD_MUX=1 下、為 ALERead 子測試武裝,金 benchmark checksum 不受影響。它是一個必需的機制、背後有真開關級的 '373 —— 唯一的不純就是那一行手調時序,而其中五分之四這篇已經幫它接了地。← back to the special cases · shim ledger← 回特殊個案 · shim 總帳