The last mile: pixel → picture最後一哩:像素 → 畫面

Deep dive #5 · a roadmap, not a finished build. Our switch-level 2C02 is bit-exact, but it stops at palette indices — a number per pixel. A real NES doesn't output numbers; it outputs a wobbly analog composite waveform that a 1990 television turns back into a fuzzy, colour-bleeding, dot-crawling picture. This article maps that whole missing stretch — the on-die video DAC, the board amplifier, the composite signal itself, and the television's decode chain — and lays out how we'd model it: intercept the DAC's digital control nodes, synthesise the analog waveform at 21.477 MHz, and write our own NTSC decoder from first principles (no borrowed libraries). Two hardware-engineer consults inform it; the honest caveats are throughout.

深入專文 #5 · 這是路線圖,不是已完成的實作。我們的開關級 2C02 是 bit-exact,但它停在調色盤索引 —— 每像素一個數字。真的 NES 不輸出數字;它輸出一段搖搖晃晃的類比 composite 波形,由一台 1990 年的電視變回一張模糊、色彩溢出、點狀蠕動的畫面。這篇把那整段缺的路標出來 —— 晶片內視訊 DAC、主機板放大器、composite 訊號本身、電視的解碼鏈 —— 並鋪陳我們會怎麼模擬:截斷 DAC 的數位控制節點、在 21.477MHz 合成類比波形、然後從第一性原理自己寫 NTSC 解碼器(不借用函式庫)。內容來自兩次硬體工程師諮詢;誠實的但書貫穿全文。

S1a · deep dive #5roadmap2026-07-20

The whole chain整條鏈Where the model stops, and everything after it模型停在哪,以及它之後的一切

Four stages sit between the last digital node we simulate and a picture on a screen. Two live on the 2C02 die, one is a handful of parts on the board, and one is the entire television. The good news up front: the RF modulator does not need modelling — the composite baseband signal is fully formed before it, so we can tap the composite directly.

在我們模擬的最後一個數位節點與螢幕上的畫面之間,有四個階段。兩個在 2C02 晶粒上,一個是主機板上幾顆零件,一個是整台電視。先講好消息:RF 調變器不用模擬 —— composite baseband 訊號在它之前就完全成型,所以我們可以直接接 composite。

2C02 pixel → picture: the four stages (RF modulator skipped) ON-DIE BOARD TV CRT digital colour logic Sync·Luma·Chroma·Burst·Emph ← we stop here 12-phase + video DAC R-divider + NMOS → Pin 21 board amp Q1+Q2 2SA937 + 2SC2021 → CVBS TV NTSC decoder sync-sep · Y/C notch · burst-PLL demod · YIQ→RGB CRT scanline · blur · gamma tap point → synthesise CVBS at 21.477 MHz → our own software decoder
The four stages after the digital pixel. We tap between stage 1 and 2 (the DAC control nodes), synthesise the analog composite, and model the rest in software.數位像素之後的四個階段。我們在第 1、2 階段之間(DAC 控制節點)接出、合成類比 composite,其餘用軟體模擬。

Stage 1 — the 2C02 generates階段一 —— 2C02 產生A resistor ladder, not a real DAC一條電阻梯,不是真的 DAC

The NES has no R-2R DAC and no sine oscillator. Its composite waveform is crude switching between fixed DC voltages — a square-wave stack that the television's low-pass filter smooths into colour. On the die: a series resistor divider between VCC and GND, and NMOS pass transistors that connect one tapped node to the output (Pin 21, VOUT) each moment.

NES 沒有 R-2R DAC、也沒有正弦振盪器。它的 composite 波形是粗暴地在固定 DC 電壓間切換 —— 一疊方波,由電視的低通濾波器平滑成色彩。晶粒上:VCC↔GND 間一條串聯電阻分壓,加上 NMOS 傳輸閘,每一刻把某個抽頭節點接到輸出(Pin 21,VOUT)。

Then a tiny board amplifier — the 2C02's VOUT is too weak to drive a 75 Ω TV. On an NES-CPU-11 board that's Q1 (PNP 2SA937 / 2SA1015) for level-shift and Q2 (NPN 2SC2021 / 2SC1815) as an emitter follower, plus 220/510/110 Ω and a 220 µF DC-block. Its non-linearity is already baked into the measured voltage table below, so we can skip a SPICE model of it.

接著一個小小的主機板放大器 —— 2C02 的 VOUT 太弱、推不動 75Ω 電視。NES-CPU-11 板上是 Q1(PNP 2SA937 / 2SA1015) 做位準平移、Q2(NPN 2SC2021 / 2SC1815) 當射極隨耦器,加 220/510/110Ω 與 220µF 隔直流。它的非線性已經含在下面的量測電壓表裡,所以可以省掉它的 SPICE 模型。

Stage 2 — the composite signal階段二 —— composite 訊號One scanline, as a waveform一條掃描線,當成波形

One NTSC scanline of composite (CVBS) volts blank 0 sync −0.286 white ~1.0 H-sync colourburst ~9 cyc, phase ref active video — luma = pedestal height · chroma = 3.58 MHz swing, phase = hue luma
A composite scanline: sync tip below blanking, the colourburst phase reference, then active video where the pedestal height is luma and the 3.58 MHz swing's phase is the hue. Measured levels (lidnariq, relative to 1 Vpp): sync −0.286, blank 0, black $0D −0.11, Luma0 0.22/0.61, Luma3 0.71/1.09.一條 composite 掃描線:低於消隱的 sync tip、colourburst 相位基準,然後 active video —— 底座高度是亮度、3.58MHz 擺動的相位是色相。量測電壓(lidnariq,相對 1Vpp):sync −0.286、blank 0、黑 $0D −0.11、Luma0 0.22/0.61、Luma3 0.71/1.09。

Stage 3 — the television decodes階段三 —— 電視解碼Sync → Y/C → demodulate → RGB同步 → 亮色分離 → 解調 → RGB

A real set runs the composite through four stages, historically discrete chips:

真電視把 composite 過四個階段,以前是分立晶片:

Stage階段What it does做什麼Classic chip經典晶片
Sync separation同步分離slice below blanking → H-sync (4.7 µs) & V-sync via RC切低於消隱 → RC 分 H-sync(4.7µs)/ V-syncLM1881
Y/Csplit luma from chroma — notch/bandpass, or a comb filter using a line-delay (glass → CCD → digital)亮度/色度分離 —— notch/BPF,或用行延遲(玻璃→CCD→數位)的 comb filterdelay line / SAA7118延遲線 / SAA7118
Demod + PLL解調 + PLLlock a PLL to the colourburst each line, multiply chroma down to I/Q每行用 colourburst 鎖 PLL,乘回 chroma 得 I/QCXA1621S
Matrix矩陣YIQ → RGB, drive the electron gunsYIQ → RGB,推電子槍TDA3562A · TA7698AP

The NES comb-filter trap.NES 的 comb-filter 陷阱。 Standard NTSC flips the subcarrier phase 180° per line, so adding two adjacent lines cancels chroma — a comb filter works. But the 2C02, to keep hardware simple, shifts phase only 120° per line (a 3-line cycle). A standard 1-line comb filter does not cancel on NES — colour aliases badly. Famously, expensive comb-filter TVs looked worse on a NES than cheap notch-filter sets. So a faithful model must use a notch filter, which is also what ~90% of 1980s–90s televisions used — and it reproduces the colour bleed and dot crawl correctly.標準 NTSC 每行副載波相位翻 180°,相鄰兩行相加即抵銷色度 —— comb filter 有效。但 2C02 為了硬體簡化,每行只偏 120°(3 行一循環)。標準 1-line comb 在 NES 上不抵銷 —— 色彩嚴重混疊。有名的是:昂貴的 comb-filter 電視看 NES 反而比廉價 notch 電視更糟。所以忠實模型必須用 notch filter —— 那也是 80/90 年代約 90% 電視的做法 —— 且能正確重現色彩溢出與 dot crawl。

Stage 3, our way階段三,我們的做法Our own decoder, no borrowed code自己寫解碼器,不借用程式碼

We will write the decoder ourselves — not nes_ntsc, not a copy of any emulator's filter. The maths is short; the difficulty is timing and filter design. Sampling at 6× the colourburst makes it clean: every colour cycle is exactly 6 samples, so the sin/cos tables are constant and no interpolation is needed. The stages:

我們會自己寫解碼器 —— 不用 nes_ntsc、不抄任何模擬器的濾波器。數學很短,難在時序與濾波器設計。用 colourburst 的 6 倍取樣讓它很乾淨:每個色彩週期正好 6 樣本,sin/cos 表是常數、不用插值。各階段:

// 6 samples per colour cycle (Fs = 6·fsc)
sin6[] = {0, .866, .866, 0, -.866, -.866};   cos6[] = {1, .5, -.5, -1, -.5, .5};

// 1. per-line burst phase (software clock is perfect, so no jitter-fighting PLL needed —
//    just measure the burst; a "cheat" MVP can derive it from (frame+line)%3 instead)
burst_phase = atan2( Σ burst·sin6 , Σ burst·cos6 );

// 2. Y/C — NOTCH, never a comb (the 120°/line trap): FIR band-pass around 3.58 MHz
C = FIR_bandpass(composite);   Y = composite − C;

// 3. demodulate against the locked phase, then low-pass off the 2× (7.16 MHz) image
I = LPF( C · cos(2πn/6 − (burst_phase+π)) );   Q = LPF( C · sin(...) );   // I~1.5MHz, Q~0.5MHz

// 4. FCC YIQ → RGB, clamp 0..1
R = Y + 0.956·I + 0.621·Q;  G = Y − 0.272·I − 0.647·Q;  B = Y − 1.106·I + 1.703·Q;

Then the CRT, by contribution: scanlines (~40 %, NES is 240p — leave the even field dark), a horizontal Gaussian phosphor blur (~30 %, also softens the dot-crawl noise), gamma ≈ 2.2 (~15 %, or the darks die), and optionally a shadow-mask and bloom. Build order (each step is a working picture): cheat-phase MVP → software burst-lock PLL → tune the FIR orders → CRT filters.

然後 CRT,依貢獻度:scanline(~40%,NES 是 240p —— 偶數場留黑)、水平高斯 phosphor blur(~30%,順便柔化 dot-crawl 噪音)、gamma ≈ 2.2(~15%,否則暗部死黑),以及選配的 shadow mask 與 bloom。建置順序(每步都是能看的畫面):作弊相位 MVP → 軟體 burst-lock PLL → 調 FIR 階數 → CRT 濾鏡。

Where it plugs in接在哪The AprVisual integration — honestly, a planAprVisual 的整合 —— 誠實說,是個計畫

  1. Tap the DAC control nodes. In the switch-level 2C02, find the digital nodes driving the video-DAC pass transistors: Luma 0–3, the Chroma / 12-phase select, Sync, Colourburst enable, Emphasis R/G/B. That is the clean cut between "bit-exact digital" and "analog behavioural".
  2. 接出 DAC 控制節點。在開關級 2C02 裡找到驅動視訊 DAC 傳輸閘的數位節點:Luma 0–3Chroma / 12 相位選擇、SyncColourburst enableEmphasis R/G/B。那就是「bit-exact 數位」與「類比行為」之間的乾淨切點。
  3. A 21.477 MHz sidecar — a lookup get_composite_voltage(luma, chroma, phase, emphasis, sync) called once per master-clock tick, using lidnariq's measured voltages. No SPICE for the board amp — its non-linearity is in the table.
  4. 21.477MHz sidecar —— 一個查表 get_composite_voltage(luma, chroma, phase, emphasis, sync),每主時脈 tick 呼叫一次,用 lidnariq 的量測電壓。板上放大器不跑 SPICE —— 非線性在表裡。
  5. Our own decoder on the resulting ~1364-sample-per-line stream, as above.
  6. 在得到的每行 ~1364 樣本流上跑我們自己的解碼器,如上。

Honest status. This is a roadmap, not a shipped feature — none of it is built in the switch-level engine yet. It is also strictly a presentation layer: it changes how the picture looks, not a single bit of the logic the golden checksum guards. For comparison, the AprNes reference emulator already has a CRT-side (NesCore/NTSC_CRT/) we can check against; what's new here is modelling the analog signal generation from the transistor-level PPU, and decoding it with code we wrote ourselves.誠實狀態。這是路線圖、不是已出貨功能 —— 開關級引擎裡一點都還沒做。它也純粹是呈現層:改變畫面長相,不動金 checksum 守護的任何一個邏輯位元。對照:AprNes 參考模擬器已有 CRT 端(NesCore/NTSC_CRT/)可比對;這裡新的是從電晶體級 PPU 模擬類比訊號產生,並用我們自己寫的碼解碼。

← back to deep dives← 回深入專文