The phenomenon現象A bus that remembers會記憶的匯流排
When the 6502 executes a read, it puts an address on the bus and expects something to drive the eight data lines back. For most addresses a chip answers — RAM, ROM, a PPU register. But some addresses map to nothing. On a chip with pull-up resistors an unanswered read would return all 1s; the NES has no such resistors, so the data lines are simply left floating. And a floating wire is not empty: it is a tiny capacitor, still holding whatever voltage was last forced onto it. So the CPU reads back the last byte that any device drove — the "open bus" value.
當 6502 執行一次讀取,它把位址放上匯流排,期待某個東西把八條資料線驅動回來。大多數位址有晶片回應 —— RAM、ROM、PPU 暫存器。但有些位址對應不到任何東西。在有上拉電阻的晶片上,沒被回應的讀取會回全 1;NES 沒有那種電阻,所以資料線就這樣被留成浮接。而一條浮接的線並不是空的:它是一顆微小的電容,還保持著最後被強壓上去的電壓。所以 CPU 讀回的是最後任何裝置驅動過的位元組 —— 「open bus」值。
Two facts make this a hard case for a switch-level simulator, and they are the whole reason this shim exists:
兩個事實讓這對開關級模擬器成為困難個案,而它們正是這個 shim 存在的全部理由:
- The holding node is off-die. The capacitance that remembers the byte is the package leads + PCB traces + cartridge edge + the input gates of the chips that share the bus. None of that is in our
segdefs— those describe polygons on the two silicon dies. The node that does the work is on the board, invisible to every on-die mechanism. - 保持值的節點在晶粒外。記住那個位元組的電容是封裝引腳 + PCB 走線 + 卡帶邊緣 + 共用匯流排的晶片輸入閘。那些都不在我們的
segdefs裡 —— 它們描述的是兩顆矽晶粒上的多邊形。做這件事的節點在板子上,對每個晶粒內機制都是隱形的。 - The value decays. Charge leaks away. Left alone, the held byte rots to 0 over roughly 600 ms — and there is a hardware test that watches it happen. So it is not enough to replay the last byte; we have to model its slow death.
- 值會衰減。電荷漏掉。放著不管,保持的位元組會在大約 600 ms 內爛成 0 —— 而且有硬體測試在看它發生。所以只重播最後那個位元組不夠;我們得把它的緩慢死亡也模型化。
The first fact is why this shim is the shim ledger's one permanent CEILING: no on-die mechanism (charge arbitration, latch verdicts, delay) can reach a board node. It is chartered to M5e, the board-level bus-hold. This article is about the second fact — the decay — because that we can compute.
第一個事實正是為什麼這個 shim 是 shim 總帳裡唯一永久的 CEILING:沒有晶粒內機制(電荷裁決、閂鎖判決、延遲)碰得到板子節點。它被立案到 M5e,板級 bus-hold。這篇講的是第二個事實 —— 衰減 —— 因為那個我們算得出來。
The decay is linear, not RC衰減是線性,不是 RCWhy the curve is a straight ramp為什麼曲線是直斜坡
The instinct is to model a decaying capacitor as an RC discharge — the familiar e−t/RC exponential. For this node that instinct is wrong, and the reason is worth stating precisely. A floating MOS node loses its charge not through a resistor but through reverse-biased PN-junction leakage into the grounded substrate (plus a little subthreshold leakage, also to ground). And reverse-bias junction leakage is, to first order, constant current — it barely depends on the voltage across it. A constant current draining a fixed capacitance gives a constant slope:
直覺會把衰減的電容當成 RC 放電 —— 那個熟悉的 e−t/RC 指數。對這個節點,那個直覺是錯的,而原因值得講精確。一個浮接的 MOS 節點失去電荷不是透過電阻,而是透過反偏 PN junction 漏電流進接地的基板(加一點 subthreshold 漏電,也是流到地)。而反偏 junction 漏電,一階近似下是定電流 —— 幾乎不依賴它兩端的電壓。定電流放掉固定電容,給出固定斜率:
dV/dt = −Ileak / Cbus (constant slope)
V(t) = VOH − (Ileak / Cbus) · t (a straight ramp down)
The switch-level model does not carry a voltage; it carries a 0 or a 1. So all we need from this ramp is the single instant it crosses the input-low threshold and the digital bit flips 1 → 0. (It only ever goes 1→0: leakage is to ground, there is no pull-up, so a 0 never rises to a 1.) That crossing time is the whole model:
開關級模型不帶電壓;它帶 0 或 1。所以我們從這個斜坡只需要一個東西:它越過輸入低閾值、數位位元翻 1 → 0 的那一瞬間。(它永遠只 1→0:漏電到地、沒有上拉,所以 0 不會升成 1。)那個穿越時間就是整個模型:
Computing the constant算那個常數From picofarads to half-cycles從皮法到半週期
Here is the whole derivation, every step, with the numbers we can defend from first principles.
這是完整推導,每一步,附上我們能從第一性原理辯護的數字。
Step 1 — the bus capacitance第一步 —— 匯流排電容
| contribution來源 | ≈ pF |
|---|---|
| DIP-40 package lead | 4 |
| PCB trace (~4 in × 1.5 pF/in)PCB 走線(~4吋 × 1.5pF/吋) | 6 |
| cartridge edge connector卡帶邊緣連接器 | 3 |
| die pad + ROM input gate晶粒 pad + ROM 輸入閘 | 7 |
| Cbus total | ≈ 20 pF |
Cbus = 20 pF Ileak ≈ 80 pA (reverse-bias diode leak, 3µm NMOS @ 25 °C, summed over the pins on the bus)
VOH ≈ 4.5 V → VIL ≈ 1.5 V ⇒ ΔV ≈ 3.0 – 3.5 V
Step 3 — the decay time (constant-current, so t = C·ΔV / I)
tdecay = Cbus · ΔV / Ileak
= (20 pF · 3.5 V) / 80 pA
= (70 × 10−12) / (80 × 10−12) s = 0.875 s (with 3.0 V → 0.75 s; the band is ~0.6–0.9 s)
Step 4 — convert to half-cycles (1 hc = 23.28 ns)
threshold_hc = tdecay / thc = 0.7 s / 23.28 ns ≈ 30 million hc
So the hand-set "~600 ms" that our timestamp-decay used is not a magic number — it is C·ΔV / I with defensible board values, landing in the 0.6–0.9 s band. The empirical value we tuned to sits right inside the first-principles range. The constant is grounded; only its exact position within the band is calibration (and, as we'll see, that position is not even fixed).
所以我方時戳衰減用的手調「~600 ms」不是魔術數字 —— 它是 C·ΔV / I 帶上有依據的板級數值,落在 0.6–0.9 秒帶。我們調到的經驗值正好坐在第一性原理範圍內。這個常數是有依據的;只有它在帶內的精確位置是校準(而且我們接著會看到,那個位置甚至不是固定的)。
It rots bit by bit它一位一位地爛Eight lines, eight clocks八條線,八個時鐘
A crucial subtlety: the byte does not decay all at once. The eight data lines are eight independent capacitors, each with a slightly different trace length, via count, and — most of all — a slightly different junction-leakage current from manufacturing spread. So each line has its own decay clock, and a held 0xFF does not snap to 0x00; it rots one bit at a time — something like 0xFF → 0xEF → 0xEE → 0x40 → 0x00 over a window of tens of milliseconds. The hardware open-bus decay tests watch exactly this waterfall, and a ROM can check the order the bits drop. So a correct model needs a per-bit timestamp and threshold, not one for the whole byte.
一個關鍵細節:位元組不是一次全衰減。八條資料線是八顆獨立的電容,各自走線長度、via 數略有不同,而最重要的是製程散佈讓 junction 漏電流略有不同。所以每條線有自己的衰減時鐘,一個保持的 0xFF 不會瞬間變 0x00;它一次爛一個位元 —— 像 0xFF → 0xEF → 0xEE → 0x40 → 0x00 這樣,在幾十毫秒的窗裡。硬體 open-bus 衰減測試看的正是這道瀑布,而 ROM 可以檢查位元掉落的順序。所以正確模型需要逐位元的時戳與閾值,不是整個位元組一個。
How we implement it我們怎麼實作Lazy timestamps, off the hot path懶惰時戳,離開熱路徑
The engine has no analog voltage and no future-event scheduler, so we do not simulate the ramp. We use lazy-evaluated timestamps: only ever compute the decay the moment a floating line is looked at. This is correct here precisely because the NES samples its data bus synchronously (on φ2 / read-enable) — the value is only observed at clock edges, so evaluating it there matches the hardware without scheduling a wake-up 30 million cycles in the future (which an event wheel would need, then have to cancel every time the bus is driven again).
引擎沒有類比電壓、也沒有未來事件排程器,所以我們不模擬那條斜坡。我們用懶惰求值的時戳:只在一條浮接線被看的那一刻才算衰減。這裡這樣做是對的,正因為 NES 是同步取樣它的資料匯流排(在 φ2 / read-enable)—— 值只在時鐘邊沿被觀察,所以在那裡求值就對上硬體,不用排一個 3000 萬週期後的喚醒(event wheel 要那樣,而且每次匯流排被重新驅動都得取消它)。
It is a tiny post-pass over just the eight external data-bus nodes, run after the switch-level network has settled for the half-cycle — never inside the hot inner solver, so the golden benchmark is untouched:
它是一個小小的 post-pass,只跑八條外部資料匯流排節點,在開關級網路 settle 完該半週期之後 —— 絕不進熱內迴圈,所以金 benchmark 不受影響:
// after wire_compute() has settled this half-cycle — test-mode only struct OpenBusBit { uint64_t last_driven_hc; uint32_t threshold_hc; uint8_t last_val; } bus[8]; for (i = 0; i < 8; i++) { if (node[DB0+i].is_driven) { // had a path to VDD/GND this cycle → refresh bus[i].last_driven_hc = now_hc; bus[i].last_val = node[DB0+i].state; } else if (bus[i].last_val == 1) { // floating, was high → is it still holding? uint64_t age = now_hc - bus[i].last_driven_hc; node[DB0+i].state = (age > bus[i].threshold_hc) ? 0 : 1; // the flip, 1→0 only } else { node[DB0+i].state = 0; // was driven low → stays low } }
The refresh rule is the load-bearing detail: a line's clock resets iff there was an active path to VDD or GND during the settled half-cycle. A write drives it → refresh. A read of mapped RAM/ROM/PPU drives it → refresh. A read of open bus does not drive it → no refresh (the pass-transistors do briefly charge-share the huge board cap against the tiny internal bus, but that drops the voltage by microvolts and never recharges it, so a discrete model ignores it). And one hazard to keep straight: these eight nodes are the board bus. The 2C02 has its own internal open-bus latch (reading $2002 returns the last value on the PPU's internal data bus) — a different capacitance, modelled separately; do not let the two leak into each other.
refresh 規則是承重的細節:一條線的時鐘重置當且僅當該 settle 半週期裡有到 VDD 或 GND 的 active path。寫入驅動它 → refresh。讀已映射的 RAM/ROM/PPU 驅動它 → refresh。讀 open bus 不驅動它 → 不 refresh(pass-transistor 確實會短暫讓巨大板電容對微小內部匯流排 charge-share,但那只掉幾微伏、且從不充回,所以離散模型忽略它)。還有一個要分清楚的雷:這八個節點是板匯流排。2C02 有它自己的內部 open-bus latch(讀 $2002 回傳 PPU 內部資料匯流排的最後值)—— 是不同的電容、分開模型;別讓兩者互相滲漏。
From tuned to computed — our plan從調到算 —— 我們的計畫And the floor we cannot cross以及我們跨不過的地板
The implementation above still seeds threshold_hc from a hand-set number. Replacing it with a computed one is a short, concrete plan:
上面的實作仍然用手設的數字種 threshold_hc。把它換成算出來的,是一個短而具體的計畫:
- Compute the base. Use the Step 1–4 derivation:
threshold_hc = C·ΔV / (I·t_hc)with C = 20 pF, I = 80 pA, ΔV = 3.5 V → ≈ 30 M hc. Hardcode the derivation in the source as its own documentation, so the constant is a computed result, not a magic literal. - 算基準值。用第 1–4 步推導:
threshold_hc = C·ΔV / (I·t_hc),C = 20 pF、I = 80 pA、ΔV = 3.5 V → ≈ 30M hc。把推導本身寫進原始碼當文件,讓這個常數是計算結果、不是魔術字面值。 - Seed the per-bit spread. Apply a fixed ±10–20% deterministic spread across the 8 bits at init, so the byte rots in a fixed, reproducible order — reproducing the hardware waterfall while staying bit-exact run-to-run.
- 種逐位元散佈。初始化時對 8 個位元套一個固定的 ±10–20% 決定論散佈,讓位元組以固定、可重現的順序爛 —— 重現硬體瀑布,同時逐 run bit-exact。
- Calibrate from a bench measurement (optional, for accuracy). On real hardware: write
0xFFto an open-bus address, wait N ms, read it back out to the controller port, sweep N from 100 → 1000 ms on a scope. The waterfall you capture (bit 2 drops at ~510 ms, bit 7 at ~580 ms, …) gives the exact per-bit thresholds to seed. - 從實機量測校準(選配,為了精度)。在真硬體上:寫
0xFF到一個 open-bus 位址、等 N ms、讀出到手把埠、在 scope 上把 N 從 100 → 1000 ms 掃過去。你抓到的瀑布(bit 2 在 ~510ms 掉、bit 7 在 ~580ms、…)就是要種的精確逐位元閾值。
Ileak ∝ T³ · exp(−Eg/kT). A real NES in a 30 °C room decays its open bus almost twice as fast as one at 20 °C. So there is no single "true" threshold — the hardware itself does not have one. The honest model computes the base constant, documents the derivation, and exposes a temperature-scaling parameter for the leakage; the ±10% per-bit spread does the rest. That is not a defeat of the model — it is the model correctly refusing to pretend a temperature-dependent analog quantity is a fixed digital constant. This is what the CEILING fate means in the ledger: an off-die, physically-real behaviour we can characterise and bound, but not reduce to one on-die number.誠實的地板。有一件事我們永遠算不出單一普世數字,而它值得講清楚:反偏漏電是溫度指數相關的,Ileak ∝ T³ · exp(−Eg/kT)。30 °C 房裡的真 NES 衰減它的 open bus 比 20 °C 的快近兩倍。所以沒有單一「真」閾值 —— 硬體自己就沒有。誠實的模型算出基準常數、把推導寫成文件、對漏電開一個溫度縮放參數;±10% 逐位元散佈做剩下的。這不是模型的失敗 —— 是模型正確地拒絕假裝一個溫度相關的類比量是固定的數位常數。這就是總帳裡 CEILING 命運的意思:一個晶粒外、物理真實、我們能刻畫並界定、但無法化約成一個晶粒內數字的行為。Interactive — the same byte, at different room temperatures互動 —— 同一個位元組,在不同室溫下
Slide the temperature. This is the whole reason there is no single "true" decay time — the leakage current, and so the decay, moves by more than 100× across a normal range of ambient temperatures. Warm room: the byte is gone in a tenth of a second. Freezer: it lingers for minutes.拖動溫度。這就是為什麼沒有單一「真」衰減時間 —— 漏電流、進而衰減,在正常室溫範圍內就變動超過 100 倍。溫暖的房間:位元組十分之一秒就沒了。冷凍庫:它逗留好幾分鐘。
Model: t(T) = t₀ · exp( Ea/k · (1/T − 1/298 K) ), generation-limited junction leakage Ea ≈ 0.56 eV (≈ halves every 10 °C), anchored at t₀ = 600 ms at 25 °C. Illustrative — the numbers move; the point is that they must.模型:t(T) = t₀ · exp( Ea/k · (1/T − 1/298 K) ),generation-limited junction 漏電 Ea ≈ 0.56 eV(≈ 每 10 °C 減半),錨定在 25 °C 時 t₀ = 600 ms。示意用 —— 數字會動;重點是它們必須動。
← back to deep dives · the M5e charter · shim ledger← 回深入專文 · M5e 立案 · shim 總帳