The ALERead muxALERead mux

Deep dive #1. Buried in the AccuracyCoin recipe is one line of magic numbers — MUX_HC = 13, 13, 25, 44, 52 — five hand-tuned constants without which the ALERead test drops the suite to 140/141. This article asks the honest question: where do those five numbers come from? We reconstruct the physics from scratch, verify the engine's own clock, and show that four of the five are not free knobs at all — they are a settle time you can compute and a phase error you can name. Full worked arithmetic, every step, with diagrams.

深入專文 #1。AccuracyCoin 配方裡藏著一行魔術數字 —— MUX_HC = 13, 13, 25, 44, 52 —— 五個手調常數,少了它 ALERead 測試就把套件從 141/141 拉到 140/141。這篇問一個誠實的問題:這五個數字到底從哪來?我們從頭重建物理、驗證引擎自己的時鐘,並證明五個裡有四個根本不是自由旋鈕 —— 它們是一段你算得出來的 settle 時間、和一個你叫得出名字的相位誤差。完整算式、每一步、附圖。

S1a · deep diveALERead mux2026-07-20

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 閂鎖那一刻資料是有效的。下圖就是這支舞:

One PPU access — ALE latches the address, then data rides the same pins ALE high — ADDRESS on AD[7:0] ALE low — DATA on AD[7:0] ALE falling edge → '373 captures AD[7:0] address low byte data byte (valid after rise settles) '373 Q held address capture window opens here $2007 read early! correct sample ≈ 1 CPU cycle = 3 dots early ⟵ this is the 25
The multiplexed AD bus. The engine's $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_abcpu.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_abcpu.ab 節點切開,讓位址與資料相位能乾淨取樣),和一個小的相位多工器,在三個時序窗 —— 內部叫 swallowreplayale-freeze —— 裡把某些節點壓 LOW,直到正確一瞬間到來。這些窗以半週期計,五個時長就是我們的 MUX_HC = 13, 13, 25, 44, 52

The honest framing. These five values were tuned: adjusted until the ALERead sub-test passed. The question this article answers is not "what does the mux do" (it re-times the read) but "are those five numbers arbitrary, or do they mean something?" To answer that we first have to know what a half-cycle is worth in real time.誠實的定調。這五個值是調出來的:一路調到 ALERead 子測試通過。這篇要答的不是「mux 做什麼」(它重定讀取時序),而是「那五個數字是任意的,還是有意義?」要回答它,得先知道一個半週期值多少真實時間。

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。這是一個速率:

hc_rate = 1,715,304,000 / 39.93 s = 42.96 MHz
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 master cycle = 2 hc = 46.6 ns
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。這就是我們接著要拿五個數字去比對的尺。

The five MUX_HC values laid on the hc ruler 0 81 dot 16 241 CPU cycle = 3 dots 32 40 482 CPU cycles 56 hc (1 hc = 23.28 ns · 1 dot = 186 ns · 1 CPU cycle = 559 ns) 13 ×2 physical 25 44 52 architectural
Green = physical settle; orange = the phase error, sitting right on the 1-CPU-cycle line; purple = the two architectural windows, straddling 2 CPU cycles. The pattern is the whole argument.綠 = 物理 settle;橙 = 相位誤差,正坐在 1-CPU-cycle 線上;紫 = 兩個架構窗,跨在 2 CPU cycles。這個 pattern 就是整個論證。

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 —— 結構就跳出來了。這五個不是同一種東西;它們分成三組。

hcnsCPU-cycdotswhat it is它是什麼
133030.541.6physicalAD-bus rising-edge settle (a weak pull-up into ~20 pF)AD 匯流排上升沿 settle(弱上拉驅 ~20pF)
133030.541.6physicalthe second settle window, same scale第二個 settle 窗,同尺度
255821.043.1the phase error itself1 CPU cycle = 3 dots = the "$2007 read is one cycle early"1 CPU cycle = 3 dots =「$2007 讀取早一個 cycle」
4410241.835.5architecturala swallow/replay scheduling window (µs-scale)swallow/replay 排程窗(µs 級)
5212102.176.5architecturalthe 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)就被「看成」高。

// rising edge through a weak depletion pull-up
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 下拉,不是弱負載:

// falling edge through a strong pull-down (~500 Ω)
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 的上升窗,並禁止一個對應的下降窗 —— 這正是那些手調數字的形狀。

Why the rise is 13 hc and the fall is not time (hc, 1 hc = 23.28 ns) node voltage 5.0 V 2.0 V V_IH 0 8 16 24 rise: weak depletion pull-up, RC≈600 ns 13.2 hc fall: strong pull-down, RC≈10 ns 0.8 hc
First-order RC threshold crossings. The rise crawls up a 600 ns time-constant and hits 2.0 V at 13.2 hc; the fall crashes down a 10 ns time-constant in under one hc. The mux only needs the slow one.一階 RC 閾值穿越。上升沿爬 600 ns 時間常數、在 13.2 hc 撞到 2.0 V;下降沿以 10 ns 時間常數在不到一個 hc 內崩落。mux 只需要慢的那個。
What's approximate here. R ≈ 30 kΩ and C ≈ 20 pF are order-of-magnitude estimates (we have no board extraction), and Elmore/first-order RC is a coarse model. So "13.2" is not a proof that the tuned value must be exactly 13 — it is a proof that the tuned 13 sits exactly where the physics puts a rising-edge settle, and nowhere near a wire delay or a clock count. That is the difference between a grounded constant and an arbitrary one.這裡哪些是近似。R ≈ 30 kΩ、C ≈ 20 pF 是量級估計(我們沒有板級萃取),Elmore/一階 RC 是粗模型。所以「13.2」不是證明手調值必須剛好是 13 —— 是證明手調的 13 正好落在物理放上升沿 settle 的位置,而離線延遲或時鐘計數都很遠。這就是「有依據的常數」與「任意常數」的差別。

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,遠高於任何線延遲。它們是 swallowreplay 排程窗的範圍:mux 把重建訊號 hold 多久,好讓離散引擎的時鐘追上類比現實。它們是架構性的 —— 是 mux 如何橋接兩個時鐘域的性質,不是任何單一網的性質 —— 所以不像 13 和 25,沒有乾淨的封閉式可算。在推導模型裡它們會表示成相位 offset 的算術(大致 2 · CPU_cycle ± 一段 settle),但釘死精確 hc 仍需一次量測。

Tally結算

13, 13computed physics (RC rising-edge settle, 13.2 hc)計算物理(RC 上升沿 settle,13.2 hc)

25a named constant (the 1-CPU-cycle phase error)一個命名常數(1-CPU-cycle 相位誤差)

44, 52architectural 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 命運走到完全計算的路 —— 而這篇文章就是它的地圖。

Why this stays opt-in. The node-split that makes ALERead work is a load-time graph change — it re-rolls the boot-alignment lottery for every other test if left on globally. So the mux is armed only under 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 總帳