Ben Eater Builds Breadboard Video Card to Render 64-Color VGA Image

World's worst video card? The exciting conclusion — Ben Eater

Ben Eater

A breadboard video card successfully drives an analog VGA monitor using an EEPROM and a six-resistor DAC, though 150-nanosecond memory access delays produce visible artifacts during rapid scan transitions.

AI-assisted summary of World's worst video card? The exciting conclusion — original source. Check the original for full context.

Source published:

Key ideas

  • Ben Eater completes a breadboard VGA video card by driving an AT28C256 EEPROM directly from horizontal and vertical timing counters to look up pixel data. (2:41)
  • A simple 2-bit passive resistor digital-to-analog converter per channel drops 5 V TTL logic down to standard 0.7 V VGA levels, generating a 64-color palette using 1.5 kΩ and 680 Ω resistors. (9:37)
  • Address line routing constraints and pixel aspect ratio corrections reduce the usable display image to a native 100×75 resolution padded inside a 32 KB binary. (4:44)
  • Vertical black artifacts appear on the rendered image because the 150 ns access latency of the AT28C256-15 EEPROM spans up to six 25 ns monitor pixel intervals during address transitions. (22:12)

Detailed notes

## Background Published by Ben Eater on YouTube on July 13, 2019, this video serves as the conclusion to his two-part series on constructing a discrete breadboard video card. According to the description, the project focuses on how VGA uses analog RGB signals, employing an AT28C256 EEPROM to store graphic data and a simple passive digital-to-analog converter to display an image on a standard monitor.

## TL;DR Building upon the VGA sync pulse generator created in part one, Ben Eater routes binary counter lines into an AT28C256 parallel EEPROM configured for a scaled 100×75 image resolution. He designs a 2-bit resistor ladder digital-to-analog converter for each color channel using 1.5 kΩ and 680 Ω resistors against the monitor's 75 Ω load, generating 64 distinct RGB colors alongside NAND-based blanking interval protection. Eater formats a photograph using Photoshop and a custom Python script to pad scanlines into a 32 KB raw binary file, flashes the memory chip, and successfully renders the image onto a VGA screen while analyzing minor timing artifacts caused by the EEPROM's 150 ns access delay.

## Atmosphere & style The presentation maintains Ben Eater's signature calm, methodical, and accessible instructional tone, transforming complex digital logic and circuit design into clear, step-by-step physical demonstrations. Eater speaks with relaxed modesty and understated humor, unbothered by hardware constraints and candidly acknowledging the low resolution and minor visual glitches inherent to a breadboard setup. The viewing experience feels like sitting alongside an expert bench technician, capturing the quiet satisfaction of incremental troubleshooting as inverted gate logic, Python padding errors, and memory latencies are diagnosed and resolved.

## Bulletin - [02:41](https://www.youtube.com/watch?v=uqY3FMuMuRo&t=161s) Ben Eater completes a breadboard VGA video card by driving an AT28C256 EEPROM directly from horizontal and vertical timing counters to look up pixel data. - [09:37](https://www.youtube.com/watch?v=uqY3FMuMuRo&t=577s) A simple 2-bit passive resistor digital-to-analog converter per channel drops 5 V TTL logic down to standard 0.7 V VGA levels, generating a 64-color palette using 1.5 kΩ and 680 Ω resistors. - [04:44](https://www.youtube.com/watch?v=uqY3FMuMuRo&t=284s) Address line routing constraints and pixel aspect ratio corrections reduce the usable display image to a native 100×75 resolution padded inside a 32 KB binary. - [22:12](https://www.youtube.com/watch?v=uqY3FMuMuRo&t=1332s) Vertical black artifacts appear on the rendered image because the 150 ns access latency of the AT28C256-15 EEPROM spans up to six 25 ns monitor pixel intervals during address transitions.

## Key takeaways - [00:00](https://www.youtube.com/watch?v=uqY3FMuMuRo&t=0s) Synchronized horizontal and vertical binary counters track the exact raster coordinates currently being scanned by the monitor. - [01:15](https://www.youtube.com/watch?v=uqY3FMuMuRo&t=75s) Tapping counter bits directly through current-limiting resistors into RGB pins creates basic colored bars, confirming hardware synchronization. - [02:41](https://www.youtube.com/watch?v=uqY3FMuMuRo&t=161s) The AT28C256 parallel EEPROM provides 32 KB of static storage, mapped by counter addresses to output an 8-bit color byte per pixel. - [02:59](https://www.youtube.com/watch?v=uqY3FMuMuRo&t=179s) Because the circuit runs on a 10 MHz clock instead of standard 40 MHz, pixels are quadrupled in width, requiring vertical line doubling and address bit dropping to maintain proper aspect ratio. - [04:44](https://www.youtube.com/watch?v=uqY3FMuMuRo&t=284s) Hardware address constraints leave 7 bits for X and 7 bits for Y, limiting the display image to a 100×75 pixel canvas. - [05:51](https://www.youtube.com/watch?v=uqY3FMuMuRo&t=351s) VGA color inputs require analog voltages between 0.0 V and 0.7 V into an internal 75-ohm monitor termination load. - [06:55](https://www.youtube.com/watch?v=uqY3FMuMuRo&t=415s) Calculating series resistance using a voltage divider with the monitor's 75-ohm internal load determines the resistor values needed for intermediate brightness steps. - [09:37](https://www.youtube.com/watch?v=uqY3FMuMuRo&t=577s) A 2-bit resistor DAC combining 1.5 kΩ and 680 Ω resistors produces four discrete voltage levels (0 V, ~0.23 V, ~0.47 V, ~0.70 V) per color channel, yielding 64 possible colors. - [12:22](https://www.youtube.com/watch?v=uqY3FMuMuRo&t=742s) Asserting color signals during horizontal and vertical blanking intervals can corrupt sync detection or potentially damage CRT monitors. - [13:46](https://www.youtube.com/watch?v=uqY3FMuMuRo&t=826s) A NAND gate fed by display interval flip-flops controls the active-low Output Enable (/OE) pin of the EEPROM to suppress output during blanking intervals. - [18:03](https://www.youtube.com/watch?v=uqY3FMuMuRo&t=1083s) Because 7 horizontal address bits span 128 bytes, each 100-pixel scanline in memory must be padded with 28 zero bytes to keep line addressing aligned. - [20:43](https://www.youtube.com/watch?v=uqY3FMuMuRo&t=1243s) Flashing software requires the binary image to fully match the EEPROM's 32,768-byte capacity, necessitating zero-padding across all unused vertical scanlines up to line 255. - [22:12](https://www.youtube.com/watch?v=uqY3FMuMuRo&t=1332s) The AT28C256-15's 150 ns propagation delay lags behind the monitor's 25 ns pixel sampling rate, causing transient black vertical lines during address transitions. - [23:22](https://www.youtube.com/watch?v=uqY3FMuMuRo&t=1402s) Commercial graphics adapters avoid latency artifacts and support real-time rendering by using fast video RAM rather than EEPROMs.

## Detailed walkthrough ### [00:00](https://www.youtube.com/watch?v=uqY3FMuMuRo&t=0s) Generating RGB Signals and Testing Counter Taps

Ben Eater recaps the foundation established in his previous video, where he designed a breadboard circuit capable of producing a compliant VGA signal. When plugged into a display, the monitor correctly identified the incoming feed as an 800×600 mode signal because the circuit generated precise horizontal and vertical synchronization pulses: a 3.2 µs horizontal sync pulse roughly 38,000 times per second, and a 0.1056 ms vertical sync pulse 60 times per second. This was accomplished using binary counters clocked at exactly 10 MHz. The horizontal counter cycles repeatedly from 0 to 264 counts to define line timing, while the vertical counter tracks scanlines from 0 to 628.

To move beyond blank synchronization and actually render an image, the circuit must drive the VGA interface's red, green, and blue (RGB) analog lines. Because the counters are locked in lockstep with the monitor's raster scan, they track the exact coordinates currently being painted from left to right (0 to 200 counts horizontally) and top to bottom (0 to 600 lines vertically). As Eater puts it: “In other words: because we're synchronized, we can use our counters to know exactly what pixel the monitor is painting at the moment.”

```mermaid flowchart LR accTitle: VGA Timing and Direct Counter Output accDescr: Diagram showing how horizontal and vertical counters synchronize with the monitor and directly drive color lines through resistors. subgraph Counters ["Timing Counters"] HC["10 MHz Horizontal Counter<br/>(0 to 264 counts)"] VC["Vertical Counter<br/>(0 to 628 scanlines)"] end subgraph MonitorPaint ["VGA Beam / Screen"] SYNC["H-Sync & V-Sync<br/>(800x600 Mode Sync)"] RGB["Analog RGB Lines<br/>(0 to 0.7 V)"] end HC -->|"H-Sync Pulses"| SYNC VC -->|"V-Sync Pulses"| SYNC VC -.->|"Bit 4 (16s place)"| R1["Resistor"] -->|"Green signal"| RGB VC -.->|"Bit 3 (8s place)"| R2["Resistor"] -->|"Red signal"| RGB ```

To demonstrate this coordination on the physical breadboard, Eater taps individual output bits directly from the vertical counter and routes them through current-limiting resistors into the color pins: - Connecting the 16s place bit (bit 4) to the green line causes the signal to toggle every 16 scanlines, producing thick horizontal green bands across the display. - Adding the 8s place bit (bit 3) to the red input generates alternating red and green bars alongside yellow stripes where the two colors overlap. - Bringing the blue input into the counter mix yields 16 distinct colored horizontal stripes across the display.

---

[02:41](https://www.youtube.com/watch?v=uqY3FMuMuRo&t=161s) Storing Image Data and Resolving Resolution Constraints

While colorful stripes confirm the timing works, rendering an arbitrary picture requires a dedicated memory buffer. In standard computers, video data resides in system RAM or dedicated VRAM so software can modify pixel values on the fly. For this standalone breadboard build, Eater opts to store a static graphic in an AT28C256 parallel EEPROM, which provides 32 KB of storage. Driving the EEPROM's address pins with the counter outputs allows the memory to look up and output an 8-bit color byte for every distinct X/Y screen coordinate.

Using the EEPROM introduces severe geometric and hardware addressing constraints: - **Pixel aspect ratio:** Because the circuit runs on a 10 MHz pixel clock rather than standard 40 MHz, the visible horizontal area contains 200 wide pixels across 600 vertical scanlines instead of 800×600. Every pixel is naturally four times wider than normal. - **Counter truncation:** To prevent the image from looking vertically stretched, each vertical line must be repeated four times to match the horizontal stretch. Dropping the lowest two bits of a binary counter divides its output count rate by four (0, 0, 0, 0, 1, 1, 1, 1, etc.). - **Address line limitations:** The AT28C256 EEPROM has only 15 address lines ($2^{15} = 32{,}768$ bytes). Storing raw coordinates directly would require more address lines than the chip possesses. Eater resolves this by discarding three bits from the vertical counter (Y) and one bit from the horizontal counter (X).

This reduction forces a compromise on resolution, prompting Eater's candid assessment: “So given what we've got with this EEPROM and to keep the right proportions end up having to drop three bits from our Y counter and one bit from the X counter and that leaves us with a final image resolution of 100 pixels by 75 pixels which you know, I don't know it's maybe not the most impressive resolution but I mean, what do you want for me? I'm trying to build a video card on bread boards.”

---

[05:04](https://www.youtube.com/watch?v=uqY3FMuMuRo&t=304s) Wiring the AT28C256 EEPROM to Counter Addresses

Eater places an additional breadboard adjacent to the counter logic and installs the AT28C256 EEPROM, hooking up $V_{CC}$ (+5 V) and ground. To keep the memory chip permanently configured for continuous reading, he wires the control pins directly to supply rails: - **Write Enable ($\overline{\text{WE}}$):** Tied high (+5 V) to prevent unintended write operations, since write enable is active-low. - **Output Enable ($\overline{\text{OE}}$) and Chip Enable ($\overline{\text{CE}}$):** Tied directly to ground (low) so the EEPROM's output bus remains constantly active and driving data.

He connects 14 address lines between the counters and the EEPROM to supply the X and Y raster coordinates: - **Horizontal (X) address lines:** Seven address lines are wired to the horizontal counter, omitting the least significant bit (bit 0) to scale the width down to 100 pixels. - **Vertical (Y) address lines:** Seven address lines are wired to the vertical counter, skipping the lowest three bits (bits 0, 1, and 2) to scale the height down to 75 lines. - **Unused address pin:** The 15th address line on the EEPROM is tied to ground.

With 7 bits for X and 7 bits for Y, the memory receives a unique 14-bit address for each pixel cell within the 100×75 matrix, outputting an 8-bit data byte on its D0–D7 data pins for every point on the screen.

```mermaid flowchart TD accTitle: Counter to EEPROM Addressing Scheme accDescr: Block diagram illustrating the address line routing from horizontal and vertical counters into the EEPROM to produce an 8-bit color byte. subgraph Counters ["VGA Pixel Counters"] HC["Horizontal Counter<br/>(Skip bit 0)"] VC["Vertical Counter<br/>(Skip bits 0, 1, 2)"] end subgraph Memory ["AT28C256 EEPROM"] A_X["Address A0–A6<br/>(7 bits for X: 100 pixels)"] A_Y["Address A7–A13<br/>(7 bits for Y: 75 lines)"] A14["Address A14<br/>(Tied to GND)"] DATA["Data Outputs D0–D7<br/>(8-bit Pixel Color Byte)"] end HC --> A_X VC --> A_Y A_X --> DATA A_Y --> DATA A14 --> DATA ```

---

[05:51](https://www.youtube.com/watch?v=uqY3FMuMuRo&t=351s) Converting 5 V Digital Logic to Analog 0.7 V VGA Signals

Standard VGA video inputs expect an analog voltage between 0.0 V (completely dark) and 0.7 V (maximum brightness) for each color channel. The EEPROM outputs standard digital CMOS/TTL logic levels (0 V for logic low and 5 V for logic high). Because this requires converting a higher voltage down to a maximum of 0.7 V, a voltage divider is the natural solution.

```mermaid flowchart TD accTitle: Passive Resistor Voltage Divider with Monitor Load accDescr: Schematic flow of the digital output passing through an inline series resistor into the internal 75 ohm load of the monitor. subgraph BreadboardDAC ["Video Card / Breadboard"] V_IN["Digital Output Pin<br/>(5 V High / 0 V Low)"] R_SERIES["Series Resistor (R)<br/>Calculated: ~460.7 Ω"] end subgraph MonitorLoad ["VGA Display Input"] V_OUT["Analog RGB Pin<br/>Target: 0.7 V Peak"] R_INT["Internal Load Resistance<br/>R2 = 75 Ω"] GND["Chassis Ground<br/>0 V"] end V_IN --> R_SERIES R_SERIES --> V_OUT V_OUT --> R_INT R_INT --> GND ```

A standard voltage divider equation determines output voltage based on the resistor ratio: $$\text{V}_{\text{out}} = \text{V}_{\text{in}} \times \frac{R_2}{R_1 + R_2}$$

The VGA specification dictates that monitor inputs feature a 75-ohm input impedance ($R_2 = 75\ \Omega$), which acts as an internal load tied to ground inside the display: - To find the series resistor ($R$) required to step 5 V down to full-scale 0.7 V: $$\frac{75}{R + 75} \times 5 = 0.7$$ - Solving the algebra: $$375 = 0.7R + 52.5$$ $$322.5 = 0.7R$$ $$R \approx 460.7\ \Omega$$ - Placing a $460.7\ \Omega$ series resistor between a 5 V output and the VGA input pin delivers the required 0.7 V.

To achieve varying brightness levels and rich color palettes rather than simple on/off primary colors, intermediate voltages must be produced between 0 V and 0.7 V. For four evenly spaced brightness steps per channel (zero, one-third, two-thirds, and full brightness): - **Full brightness (0.700 V):** Ideal resistor is $460.7\ \Omega$. - **Two-thirds brightness (~0.467 V):** Ideal resistor is $722.9\ \Omega$; Eater substitutes a commonly available $680\ \Omega$ resistor as a close match. - **One-third brightness (~0.233 V):** Ideal resistor is $1535.4\ \Omega$; Eater substitutes a standard $1.5\ \text{k}\Omega$ ($1500\ \Omega$) resistor. - **Zero brightness (0.000 V):** Formed when the digital pin stays low at 0 V.

[09:37](https://www.youtube.com/watch?v=uqY3FMuMuRo&t=577s) Constructing a 2-Bit Resistor DAC and 64-Color RGB Mapping

Ben Eater details how to turn digital outputs into the required analog VGA voltage levels (ranging from 0 to 0.7 volts) using a simple resistor digital-to-analog converter (DAC). By using two inputs (each driven at either 0V or 5V) per color channel, four distinct voltage states can be formed: 1. When both inputs are at 0V, the output is naturally 0V. 2. When only the input tied to the 1.5 kΩ resistor is high (5V) and the other is at 0V, the output yields approximately 0.23V across the monitor's internal 75Ω termination. 3. When the 1.5 kΩ input is low and the 680Ω resistor input is driven at 5V, the voltage reaches roughly 0.47V. 4. When both inputs are driven high (5V), the two resistors combine in parallel. Calculating their equivalent resistance via the sum of reciprocals formula:

$$\frac{1}{R_p} = \frac{1}{1500} + \frac{1}{680}$$

This yields an equivalent resistance of approximately 468Ω, resulting in an output of around 0.7V.

```mermaid flowchart LR accTitle: Two-bit Resistor DAC accDescr: Schematic showing how two digital inputs feed 1.5k and 680 ohm resistors in parallel to generate an analog voltage into a 75 ohm load.

subgraph Inputs["Digital Inputs (0V / 5V)"] Bit0["Bit 0 (LSB)"] Bit1["Bit 1 (MSB)"] end

subgraph Resistors["Resistor Network"] R1["1.5 kΩ Resistor"] R2["680 Ω Resistor"] end

subgraph OutputNode["Analog Output"] VOut["VGA Pin (0 to 0.7V)"] RTerm["75 Ω Monitor Load"] GND["Ground"] end

Bit0 --> R1 Bit1 --> R2 R1 --> VOut R2 --> VOut VOut --> RTerm RTerm --> GND ```

Replicating this simple 2-bit ladder across each of the three color channels (red, green, and blue) allows six bits of EEPROM data to define four intensity levels per channel. Ben points out: “So with just a couple resistors we can take 2 bits of binary data and convert that into one of 4 different voltage levels from 0 to 0.7 volts and if we do that for red green and blue we can use six bits of data coming out of our EEPROM to get four different shades of red green and blue which combine into 64 different colors”. In the 6-bit color byte, the lowest two bits control blue, the middle two control green, and the highest two control red.

- **Resistor values used:** Three 1.5 kΩ resistors and three 680 Ω resistors (one pair per color channel). - **VGA pin assignments:** Red, green, and blue connect to pins 1, 2, and 3 respectively on the standard 15-pin VGA connector, with sync and grounds connected as established previously.

---

[12:22](https://www.youtube.com/watch?v=uqY3FMuMuRo&t=742s) Wiring the DAC and Solving the Blanking Output Bug

Ben wires the six data outputs of the EEPROM into the resistor networks on the breadboard. Data bits 0 and 1 feed into the 1.5 kΩ and 680 Ω resistors for blue; bits 2 and 3 route through the pair for green; and bits 4 and 5 drive the pair for red. The output ends of each resistor pair are tied together to supply the analog signals to pins 1, 2, and 3 of the 15-pin VGA connector, alongside the horizontal sync, vertical sync, and ground lines.

```mermaid flowchart TD accTitle: EEPROM Blanking Logic accDescr: Block diagram illustrating horizontal and vertical display period signals feeding a NAND gate to control active-low Output Enable on the EEPROM.

HDisp["Horizontal Display Period Active"] VDisp["Vertical Display Period Active"] NAND["NAND Gate"] OE["EEPROM /OE (Output Enable)"]

HDisp --> NAND VDisp --> NAND NAND -->|Active Low during display| OE ```

Upon powering up and reconnecting the monitor, the screen stays dark. The monitor recognizes the sync signals, but shows nothing. Ben notes that the EEPROM installed is completely erased, meaning every memory byte holds all ones (0xFF). All ones should light every channel at maximum voltage and produce a solid white screen.

He realizes that driving full voltage constantly means pixels are being asserted even during horizontal and vertical blanking intervals: “And we really shouldn't be putting out any pixels in this blanking time because well it should be blank and in fact, this is one of those things that might actually damage a CRT monitor and hopefully we won't damage my monitor”.

To prevent this, the EEPROM outputs must be disabled whenever the beam enters a blanking interval: - The circuit already tracks the active horizontal and vertical display intervals via flip-flops. - Because the EEPROM's Output Enable (`/OE`) pin is active-low, Ben adds a NAND gate. When both the horizontal display period and vertical display period signals are high, the NAND output drops low, enabling the EEPROM. - After initially wiring the gate inputs to the wrong polarity outputs, he swaps them to the inverted flip-flop outputs: “And there we go now it actually looks like we're getting looks like a white screen”.

---

[15:27](https://www.youtube.com/watch?v=uqY3FMuMuRo&t=927s) Preparing an Image and Formatting Raw Binary Pixel Data

With the breadboard displaying a pure white field, Ben moves to displaying an actual full-color graphic. He finds a public domain photograph of a colorful bird and imports it into Photoshop to format it for the video card's specific constraints. He resizes and crops the canvas down to the hardware's native resolution of 100×75 pixels.

Next, he switches the color mode to indexed color using a custom 64-color palette matching the resistor DAC's voltage steps. Each index corresponds directly to the hardware's color byte encoding (where index 8 represents a specific shade of green, driven by the middle bits). Ben observes of the coarse 100×75 resolution: “So that is that is the best our wonderful breadboard video card can do so, let's go ahead and save this”.

Exporting to `image.png` introduces headers, chunks, and metadata that the raw EEPROM address lines cannot parse. To extract pure byte data, Ben writes a short Python script using the Pillow (`PIL`) library: - The script loads the image to read its 2D array of indexed color pixels. - It iterates across all 75 scanlines and 100 pixels per line. - It writes each raw byte directly into a binary file named `image.bin`.

Inspecting `image.bin` confirms that all byte values are under `0x40` (64 in decimal), matching the expected 6-bit color range.

However, Ben identifies a memory layout mismatch caused by the video card's address generation architecture. The circuit uses 7 bits for horizontal addressing and 7 bits for vertical addressing. While the display only shows 100 pixels per line, resetting the horizontal counter rolls the lower 7 bits from 99 back to 0, while the vertical counter increments bit 7 (the 128s place).

Therefore, every scanline in the EEPROM must span 128 bytes rather than 100 bytes: “so in the EEPROM really even though we only have a hundred pixels per line on the screen in the EEPROM we want to take up 128 pixels per line”. When he alters the inner loop bound from 100 to 128, Python promptly throws an `IndexError: image index out of range`, because the source image is only 100 pixels wide.

[19:41](https://www.youtube.com/watch?v=uqY3FMuMuRo&t=1181s) Padding the Image Data and Programming the EEPROM

When the Python script attempts to read pixel coordinates beyond the boundaries of the 100-pixel-wide image—such as pixel 100, 101, or 128—it throws an `IndexError`. To resolve this, Ben Eater catches the exception and simply writes a zero byte to the binary file whenever an out-of-bounds pixel is requested. Inspecting the generated output reveals the intended layout: a block of data representing the first 100 pixels of an image line, immediately followed by 28 zero bytes padding the line out to 128 bytes, which matches the 7-bit horizontal address space.

```mermaid flowchart LR accTitle: Binary Image Padding and Flashing Pipeline accDescr: Pipeline showing Python conversion with zero padding, flashing via MiniPro, and loading into the EEPROM. subgraph DataPrep ["Data Preparation (Python)"] direction TB A["100x75 Source Image"] --> B["Pad scanlines to 128 bytes"] B --> C["Pad scanlines 75–255 to 0x00"] C --> D["32,768-Byte (32KB) Binary"] end subgraph Hardware ["EEPROM Programming"] direction TB D --> E["MiniPro Software"] E --> F["Commercial Programmer"] F --> G["AT28C256 EEPROM"] end ```

Rather than using the DIY Arduino-based EEPROM programmer he built in an earlier series, Eater opts for a commercial USB programmer with the open-source `minipro` command-line utility, noting that having the raw file ready makes it significantly faster. However, his initial flashing attempt targeting the AT28C256 32-kilobyte EEPROM fails with an error stating `incorrect file size: 9600`. Because the vertical loop only iterated across 75 scan lines ($75 \times 128 = 9,600$ bytes), the file fell short of the full 32K (32,768 bytes) required by the programmer. Address line $A_{14}$ is tied low, leaving an 8-bit vertical address space ($Y = 0$ to $255$). Eater updates the script loop limit to 256, padding all remaining unused vertical lines with zeros until the file reaches exactly $8000$ hex (32,768 bytes). Re-running `minipro` completes successfully.

- The horizontal line length requires 7 bits ($2^7 = 128$ bytes), requiring 28 zeros of padding after the 100 image pixels. - The vertical resolution uses 8 address bits ($2^8 = 256$ lines), requiring lines 75 through 255 to be padded entirely with zeros. - The targeted EEPROM is the Microchip/Atmel AT28C256, a 256K-bit ($32\text{K} \times 8$) parallel EEPROM.

[22:12](https://www.youtube.com/watch?v=uqY3FMuMuRo&t=1332s) Displaying the Image and Diagnosing Timing Glitches

Eater seats the freshly flashed EEPROM back into the breadboard socket and powers on the display system. The monitor immediately syncs and displays the color image of the bird. Inspecting the output on the screen closely reveals faint, thin black vertical glitches scattered across the graphic. Eater observes that the circuit is functional despite these minor imperfections: “So this is our completed video card, but you know, it's producing an image from the ROM. So I'm going to call that a success.”

```mermaid flowchart LR accTitle: Video Card Address Timing Mismatch accDescr: Diagram illustrating clock timing mismatch causing output delay artifacts. subgraph Timers ["Clock & Scan Timing"] A["10 MHz Master Clock<br/>(100 ns per cycle)"] B["Monitor Scan Expectation<br/>(25 ns per 40 MHz pixel)"] end subgraph ChipDelay ["EEPROM Propagation"] C["AT28C256-15 Address In"] -->|tACC = 150 ns max| D["Output Data Valid"] end subgraph ScreenArtifact ["Display Result"] D --> E["Up to 6 Invalid Pixel Windows"] E --> F["Transient 0x00 Outputs (Black Streaks)"] end ```

The underlying cause of the black streaks traces back to EEPROM access latency. The system clock runs at 10 MHz, ticking every 100 nanoseconds, but the VGA monitor expects standard 800×600 @ 72 Hz timing based on a 40 MHz pixel clock—meaning the display scans through a pixel every 25 nanoseconds. Consulting the AT28C256 datasheet reveals that the specific chip model in use (the `-15` speed grade) has a maximum address-to-output access time ($t_{ACC}$) of 150 nanoseconds. Because the address lines change while the internal decoding circuitry takes up to 150 ns to present stable data at the outputs, the signal can remain invalid across up to six consecutive 25 ns monitor pixel intervals. During these transition periods, floating or unasserted data reads as zero volts, appearing on the monitor as narrow black vertical lines.

- Master circuit clock: 10 MHz (100 ns period). - Monitor pixel expectation: 40 MHz (25 ns period). - EEPROM access time ($t_{ACC}$): 150 ns maximum for the AT28C256-15. - Duration of invalid output: up to 6 monitor pixels ($150\text{ ns} / 25\text{ ns} = 6$).

[23:22](https://www.youtube.com/watch?v=uqY3FMuMuRo&t=1402s) Real Video Cards vs. EEPROM and Project Wrap-up

Eater contrasts this breadboard video generator with production computer graphics adapters. Standard graphics cards utilize high-speed video RAM, such as SDRAM, which is engineered specifically for rapid random-access throughput and low latency to avoid access-time artifacts. Furthermore, an operating system or software routine can freely rewrite the contents of video memory in real time to render animations or update desktop UI elements. On this breadboard card, updating an image requires physically extracting, erasing, reflashing, and reseating the EEPROM chip in the breadboard, an operation that is unavoidably slow even when sped up through video editing.

Eater emphasizes that achieving dynamic rendering was never the core objective of the project: “Really the goal was just to get some kind of image on the screen and I'd say this is a success.” He directs viewers interested in recreating or expanding the project to schematics, component lists, and datasheets hosted on his website. He concludes by thanking his Patreon community, noting that the extensive planning, testing, and production time required to design discrete hardware circuits from scratch would not be possible without continuous viewer support.

## Mentioned Ben Eater, AT28C256, Microchip, Atmel, VGA, Python, Pillow, PIL, Adobe Photoshop, MiniPro, Arduino, Patreon

## Video description For part 1, see this video: https://www.youtube.com/watch?v=l7rce6IQDWs Let's build a circuit that displays an image on a VGA monitor! In part 2, I talk about how VGA uses analog RGB signals to send different color pixels to the monitor. I use an EEPROM to store an image and build a simple digital-to-analog converter to generate the color signals to successfully display an image.

Support these videos on Patreon: https://www.patreon.com/beneater or https://eater.net/support for other ways to support.

------------------

Social media: Website: https://www.eater.net Twitter: https://x.com/beneater Patreon: https://patreon.com/beneater Reddit: https://www.reddit.com/r/beneater

Special thanks to these supporters for making this video possible: Name Alexander Catchpole Beau-James Erion Ben Ben Dyson Ben Kamens Ben Williams Christopher Blackmon Dean Winger Debilu Krastas Dušan Dželebdžić Eric Dynowski Erik Broeders fxshlein Gonzalo Belascuen HaykH Ian Tait Ivan Sorokin JavaXP Jay Binks Jayne Gabriele Jimmy Campbell Jordan Scales Justin Duch Manne Moquist Marcus Classon Mats Fredriksson Michael Burke Michael Garland Miguel Ríos Mirko Rener Nathan Wachholz Nicholas Moresco Nick Wrightsman Onion Sniffer Paul Pluzhnikov Randy True Robert Butler Sachin Chitale Sergey Ten SonOfSofaman Stefan Nesinger Steve Gorman Vladimir Kanazir Örn Arnarson

[Source](https://www.youtube.com/watch?v=uqY3FMuMuRo)

中文简报

Ben Eater搭建面包板显卡以渲染64色VGA图像 一块面包板显卡使用EEPROM和六电阻DAC成功驱动了模拟VGA显示器,但150纳秒的内存访问延迟在快速扫描转换期间产生了可见的伪影。 在第一部分制作的VGA同步脉冲发生器的基础上,Ben Eater将二进制计数器线路接入配置为缩放后100×75图像分辨率的AT28C256并行EEPROM。他利用1.5 kΩ和680 Ω电阻针对显示器的75 Ω负载,为每个颜色通道设计了一个2位阶梯电阻数模转换器,在基于与非门(NAND)的消隐间隔保护下生成64种不同的RGB色彩。Eater使用Photoshop和自定义Python脚本对一张照片进行格式化,将扫描线填充为一个32 KB的原始二进制文件,随后烧录至该存储芯片,并成功在VGA屏幕上渲染出图像,同时分析了由EEPROM的150 ns访问延迟引起的微小时序伪影。 Ben Eater通过直接利用水平和垂直时序计数器驱动AT28C256 EEPROM查找像素数据,完成了一块面包板VGA显卡。 (2:41) 每个通道使用简单的2位无源电阻数模转换器将5 V TTL逻辑电平降至标准0.7 V VGA电平,利用1.5 kΩ和680 Ω电阻生成64色调色板。 (9:37) 地址线走线限制和像素宽高比校正将可用显示图像缩小为填充在32 KB二进制文件内的原生100×75分辨率。 (4:44) 渲染图像上出现了垂直黑色伪影,因为AT28C256-15 EEPROM的150 ns访问延迟在地址转换期间跨越了多达六个25 ns的显示器像素间隔。 (22:12)

Detailed notes — zh

## Background 该视频由 Ben Eater 于 2019 年 7 月 13 日发布在 YouTube 频道上。视频简介表明这是自制 VGA 显卡项目的第二部分,主要讲解 VGA 如何使用模拟 RGB 信号传输不同颜色的像素,并演示如何利用 EEPROM 存储图像以及搭建简单的数模转换器(DAC)来最终点亮显示器。 ## TL;DR 本期视频承接上集搭建的面包板 VGA 同步电路,深入讲解了红、绿、蓝(RGB)模拟色彩信号的工作原理及 0~0.7V 的电平规范。Ben Eater 利用电阻网络搭建了一套简易的 2 位 DAC,使系统能够呈现 64 种色彩,并结合逻辑门在消隐期强制关断输出以保护显示系统。为了将静态图像存入 AT28C256 EEPROM,他妥协将分辨率设定为 100×75 像素,并编写 Python 脚本处理索引调色板映射与补零对齐。最终,电路成功在 VGA 显示器上还原出彩色鸟类图像,并对 ROM 访问延迟导致的屏幕微小噪点进行了时序成因分析。 ## Atmosphere & style 视频整体洋溢着纯粹硬核、严谨而又极具极客精神的手作质感,Ben Eater 的语调平静从容、条理清晰。面对面包板布线杂乱、分辨率严重受限以及初次点亮时屏幕全黑等硬件挫折,他以幽默自嘲的态度化解并耐心地层层排查,将复杂的工程权衡化繁为简。当最终在显示器上成功点亮那张低分辨率的彩色图像时,观众能深刻体会到底层硬件调试从无到有、从混乱走向秩序的巨大成就感与工匠乐趣。 ## Bulletin - [09:37](https://www.youtube.com/watch?v=uqY3FMuMuRo&t=577s) 利用二进制计数器输出驱动 VGA 同步信号后,可通过每色两个不同阻值的电阻(1.5 kΩ 与 680 Ω)并联构成简易 2 位加权 DAC,配合显示器内部 75 Ω 阻抗输出 0V 至 0.7V 之间的 4 档电压,实现 64 色显示。 - [02:41](https://www.youtube.com/watch?v=uqY3FMuMuRo&t=161s) 针对 AT28C256 EEPROM 仅有 15 根地址线的限制,电路通过在水平方向跳过 1 位、垂直方向跳过 3 位计数器输出,将分辨率妥协定为 100×75 像素,并借此修正 10 MHz 像素时钟带来的纵横比失真。 - [13:36](https://www.youtube.com/watch?v=uqY3FMuMuRo&t=816s) 行场消隐期间若输出高电平像素会导致基准失真并可能损坏 CRT 显示管,系统加入 74HC00 与非门对行场有效显示信号进行逻辑与操作,使 EEPROM 输出使能端在消隐期保持关闭。 - [22:12](https://www.youtube.com/watch?v=uqY3FMuMuRo&t=1332s) AT28C256-15 芯片具有高达 150 ns 的地址访问延迟(tACC),在标准 VGA 每 25 ns 期望一个新像素的时序下,地址切换瞬态会导致画面出现细微的黑色垂直虚线杂讯。 ## Key takeaways - [00:00](https://www.youtube.com/watch?v=uqY3FMuMuRo&t=0s) 基于 10 MHz 时钟的水平计数器(0~264)与垂直计数器(0~628)不仅生成同步脉冲,其计数值直接反映显示器电子束当前的屏幕绘制坐标。 - [01:02](https://www.youtube.com/watch?v=uqY3FMuMuRo&t=62s) 将计数器的高低电平二进制位直接连入 RGB 引脚,即可利用逐行扫描机制在屏幕上生成交替亮灭的彩条与 16 种混合色。 - [02:41](https://www.youtube.com/watch?v=uqY3FMuMuRo&t=161s) 由于 10 MHz 时钟使单像素在水平方向被拉长 4 倍,系统通过丢弃垂直计数器低位(除以 8)与水平计数器低位(除以 2)来重构近似正常的像素长宽比。 - [02:41](https://www.youtube.com/watch?v=uqY3FMuMuRo&t=161s) 受限于 32 KB EEPROM 的 14 根有效地址线,最终图像分辨率被折中设定为 100×75 像素,剩余第 15 根地址线直接接地。 - [05:51](https://www.youtube.com/watch?v=uqY3FMuMuRo&t=351s) VGA 模拟色彩通道具有标准的 75 Ω 输入阻抗,串联分压电阻即可将 5V TTL 逻辑电平精确缩放至 0~0.7V 满幅范围。 - [09:37](https://www.youtube.com/watch?v=uqY3FMuMuRo&t=577s) 通过 1.5 kΩ 与 680 Ω 电阻构成简易 2 位 DAC,并联等效电阻约 468 Ω,可产生 0V、0.23V、0.47V 与 0.70V 共 4 级阶梯电压。 - [09:37](https://www.youtube.com/watch?v=uqY3FMuMuRo&t=577s) RGB 三通道各分配 2 位数据(共 6 位),使面包板显卡能够输出总计 64 种不同的离散颜色。 - [11:35](https://www.youtube.com/watch?v=uqY3FMuMuRo&t=695s) EEPROM 芯片在全 1 擦除状态下若在消隐区间持续输出满幅白光,会严重干扰显示器的黑电平基准并危害老式显像管。 - [13:36](https://www.youtube.com/watch?v=uqY3FMuMuRo&t=816s) 利用双稳态触发器引出的行场显示有效信号驱动 NAND 门,在消隐期间将 EEPROM 的低电平有效输出使能(OE)拉高以实施黑电平强制关断。 - [15:27](https://www.youtube.com/watch?v=uqY3FMuMuRo&t=927s) 原始图像需预先经过 Photoshop 索引色量化并映射至与 DAC 电阻网络完全对应的 64 色硬件调色板中。 - [15:27](https://www.youtube.com/watch?v=uqY3FMuMuRo&t=927s) 硬件计数器每行从 0 计数到 99 后在 128 处进位,导致图像二进制数据每行必须按 128 字节边界进行补零对齐。 - [19:41](https://www.youtube.com/watch?v=uqY3FMuMuRo&t=1181s) 为满足商业烧录软件 minipro 对 AT28C256 物理容量的严格校验,Python 脚本必须将未使用的垂直行地址空间全零填充至 32,768 字节。 - [22:12](https://www.youtube.com/watch?v=uqY3FMuMuRo&t=1332s) EEPROM 150 ns 的读出延迟无法完美匹配高速像素时钟,导致显示边缘出现黑色瞬态杂讯,这也揭示了现代显卡必须采用高带宽 SDRAM 显存的物理成因。 ## Detailed walkthrough ### [00:00](https://www.youtube.com/watch?v=uqY3FMuMuRo&t=0s) 回顾先前的 VGA 信号与计数器同步机制 在上一期视频中,Ben Eater 制作了一个能够输出标准 VGA 信号的面包板电路。当将该电路接入显示器时,显示器立即识别出当前处于 800×600 模式。这背后的核心在于电路输出了正确的行同步脉冲(水平同步脉冲,Horizontal Sync Pulse)和场同步脉冲(垂直同步脉冲,Vertical Sync Pulse):每秒发送约 38,000 次(精确为 3.2 µs 脉冲宽度)的水平同步脉冲,以及每秒 60 次(脉冲宽度为 0.1056 ms)的垂直同步脉冲。 整个电路基于二进制计数器工作。水平计数器以正好每秒 1000 万次(10 MHz)的频率从 0 计数到 264 并不断循环;通过检测特定计数值即可精准匹配脉冲宽度与频率。垂直方向同样采用另一个计数器,对屏幕上的每一行从 0 计数到 628。由于电路完全与显示器维持严格的同步,计数器的当前值直接对应着显示器电子束当前正在“绘制”的位置。 - 水平计数器:0 至 264 循环计数,基准时钟为 10 MHz。 - 垂直计数器:0 至 628 循环计数,用于跟踪行号。 - 同步依据:“Because we're using the sync signals here to stay synchronized with the monitor we can use our horizontal and vertical counters to keep track of what part of the screen the monitor is currently 'painting' at the moment.”(正如 Ben Eater 所说,借由与显示器保持同步,计数器让我们能确切获知显示器在特定瞬间正在绘制屏幕的哪个像素)。 ```mermaid flowchart LR accTitle: VGA 计数器与显示器扫描同步机制 accDescr: 10 MHz 时钟驱动水平与垂直计数器,向显示器提供同步信号并确定当前像素位置 subgraph Counters["时钟与计数逻辑"] CLK["10 MHz 时钟"] --> H_CNT["水平计数器 (0~264)"] H_CNT -->|"行扫描溢出"| V_CNT["垂直计数器 (0~628)"] end subgraph SyncGen["同步脉冲与位置映射"] H_CNT -->|"3.2 µs 脉冲"| HSYNC["HSync (水平同步)"] V_CNT -->|"0.1056 ms 脉冲"| VSYNC["VSync (垂直同步)"] H_CNT --> X_POS["当前 X 坐标 (0~200)"] V_CNT --> Y_POS["当前 Y 坐标 (0~600)"] end HSync --> MON["VGA 显示器"] VSync --> MON X_POS -.->|"确定像素位置"| MON Y_POS -.->|"从左至右 / 从上到下"| MON ``` ### [01:02](https://www.youtube.com/watch?v=uqY3FMuMuRo&t=62s) RGB 信号测试与彩条显示 VGA 接口除了同步信号外,还有红(Red)、绿(Green)、蓝(Blue)三路原色信号,用于告诉显示器特定位置像素的颜色。显示器的扫描机制如同阅读书页一样:从左至右、从上到下。为了演示这一点,Ben Eater 直接从垂直计数器中提取特定的二进制位连接到彩色信号引脚上。 当提取垂直计数器的第 16 权重位(16's place bit)并通过一个限流保护电阻接到绿色信号引脚时,随着垂直计数器从 0 递增到 600,该位每隔 16 行就会翻转一次高低电平,屏幕上随即显现出规则的每 16 行交替亮灭的绿色横条。随后,他将第 8 权重位接到红色信号引脚,红绿混合产生了黄色横条纹;紧接着再接上蓝色信号,原色混合立即形成了 16 种不同颜色的条纹。 - 扫描规律:像阅读书页一样,从左到右、从上到下逐行扫描。 - 计数位映射:计数器输出的高低电平翻转直接映射为屏幕横向条纹。 - 原色混合:红、绿、蓝三种通道的组合在屏幕上直观呈现出包含黄色在内的 16 种基色图案。 ### [02:41](https://www.youtube.com/watch?v=uqY3FMuMuRo&t=161s) 引入 EEPROM 存储图像及分辨率妥协 条纹图案虽有趣,但真正的目标是呈现复杂图像。在计算机架构中,屏幕数据通常存放在 RAM 中,便于软件实时重写更新。然而 Ben Eater 并不打算立即连接计算机,而是使用一块 AT28C256 EEPROM 来静态存储图像。该芯片容量为 32 KB,足以存放一张小图像。 设计思路是将水平与垂直计数器的 X、Y 坐标信号直接作为 EEPROM 的地址输入,EEPROM 输出的单字节数据则用来定义该坐标的像素颜色。然而这里存在严重的物理与时序限制:由于上一集采用了 10 MHz 像素时钟(而非标准的 40 MHz),水平可见分辨率实际只有 200 像素,导致 200×600 的像素形状严重拉长。 为了恢复正常的像素纵横比,最简单的做法是在垂直方向将每行像素也重复 4 次。在二进制计数器中,舍弃最低位会使序列变成 0,0, 1,1, 2,2……相当于除以 2;若舍弃低 2 位,每个数值就会重复 4 次,相当于将计数频率除以 4。另一个硬性限制是 28C256 芯片只有 15 根地址线(A0~A14)。经过权衡与折中,Ben Eater 最终决定在垂直计数器端丢弃最低 3 位,在水平计数器端丢弃最低 1 位,从而在 14 根有效地址线内将画面比例调整为 100×75 像素。 Ben Eater 幽默地为这种超低分辨率辩护道:“Final image resolution of 100 pixels by 75 pixels which you know, I don't know it's maybe not the most impressive resolution. But I mean, what do you want for me? I'm trying to build a video card on bread boards.”(最终图像分辨率是 100×75 像素,虽然听起来不怎么惊艳,但你还能对我要求什么呢?我可是在面包板上造显卡啊)。 - 存储芯片:AT28C256 EEPROM(32 KB 容量,15 根地址线 A0~A14)。 - 时钟妥协导致的分辨率失真:10 MHz 时钟使原始像素在横向上被拉长 4 倍(由 800 变为 200 像素)。 - 地址缩减方案: - 水平(X)方向:舍弃最低 1 位,取接下来的 7 位(7 位对应 128,有效显示 100 像素)。 - 垂直(Y)方向:舍弃最低 3 位,取接下来的 7 位(有效显示 75 像素)。 - 未使用的第 15 根地址线直接接地。 ### [05:04](https://www.youtube.com/watch?v=uqY3FMuMuRo&t=304s) EEPROM 的面包板接线 Ben Eater 增加了一块新的面包板,放置 AT28C256 EEPROM,并布置电源线与地线。由于该存储器在此电路中仅用于数据读取,其控制引脚设置如下: 1. **写使能(WE / Write Enable)**:为低电平有效,因此将其直接接至高电平(+5V),严防误写。 2. **输出使能(OE / Output Enable)与片选使能(CE / Chip Enable)**:均为低电平有效,因此直接接地拉低,确保数据输出始终处于启用状态。 接线时,EEPROM 的前 7 根地址线跳过水平计数器的第 0 位接至高位;接下来的 7 根地址线跳过垂直计数器的前 3 位接至高位,总计占用 14 根地址线,最后第 15 根地址线(A14)直接接地。这样一来,每个坐标点都会对应输出 1 个字节(8 位)数据。 ```mermaid flowchart TD accTitle: EEPROM 地址连接结构 accDescr: 计数器低位被跳过以缩放分辨率,将 14 根地址线接入 EEPROM subgraph H_Logic["水平 X 坐标"] H_RAW["水平计数器输出"] -->|"跳过 Bit 0"| H_ADDR["7 根 X 地址线 (A0~A6)"] end subgraph V_Logic["垂直 Y 坐标"] V_RAW["垂直计数器输出"] -->|"跳过 Bit 0, 1, 2"| V_ADDR["7 根 Y 地址线 (A7~A13)"] end H_ADDR --> ROM["AT28C256 EEPROM\n(WE=5V, OE=0V, CE=0V, A14=0V)"] V_ADDR --> ROM ROM --> DATA["8 位像素数据输出 (D0~D7)"] ``` ### [05:51](https://www.youtube.com/watch?v=uqY3FMuMuRo&t=351s) VGA 电压标准与分压电阻设计 接下来的关键挑战在于将 EEPROM 输出的 8 位 TTL 数字逻辑电平(0V 或 5V)转换为 VGA 显示器所接受的模拟电压。VGA 规格规定,红、绿、蓝输入引脚的模拟电平范围为 0V 到 0.7V。电压高低决定了原色的明暗程度:0V 代表全黑,0.7V 代表最大亮度。 同时,VGA 规范明确规定各色彩通道具有 75 Ω 的输入阻抗(Input Impedance)。在电学结构上,这等同于显示器内部有一颗 75 Ω 的下拉电阻接地。利用这一特性,外部只需串联一颗电阻 $R$,便与显示器内部的 75 Ω 电阻构成了经典的分压电路(Voltage Divider)。分压公式为: $$V_{\text{out}} = V_{\text{in}} \times \frac{R_2}{R + R_2} = 5\text{V} \times \frac{75}{R + 75}$$ 当要求满幅满亮度 $V_{\text{out}} = 0.7\text{V}$ 时,代入计算: $$5 \times 75 = 0.7 \times (R + 75)$$ $$375 = 0.7R + 52.5$$ $$0.7R = 322.5 \implies R \approx 460.7\,\Omega$$ 然而,单纯输出 0V 和 0.7V 只能呈现开或关两种状态。为了显示细腻的色彩阴影与多级明暗过渡,需要产生介于 0V 与 0.7V 之间的多档中间电压。以实现 4 级均匀亮度(0、1/3、2/3、全亮度 0.7V)为例,对各档目标电压反推所需的分压电阻阻值: - 全亮度(0.700V):计算值为 460.7 Ω。 - 2/3 亮度(0.467V):计算值为 722.9 Ω。 - 1/3 亮度(0.233V):计算值为 1555.4 Ω。 现实中并不存在 460.7 Ω、722.9 Ω 或 1555.4 Ω 的标称电阻。Ben Eater 说明道:“And of course, you know, they don't make 1555.4 ohm resistors they make, you know, 1500 ohm resistors and they don't make 722.9 ohm resistors, but you know, I've got 680 ohms which is maybe close enough.”(现实中厂家并不生产 1555.4 Ω 电阻,通常只有 1500 Ω 的标称电阻;也不生产 722.9 Ω 电阻,不过手头有一颗 680 Ω 电阻,也许已经足够接近了)。通过选用相近的常用标称阻值,便能搭建简易且低成本的数模转换器(DAC)。 ### [09:37](https://www.youtube.com/watch?v=uqY3FMuMuRo&t=577s) 基于电阻分压网络构建 2 位数模转换器(DAC) 为了将 EEPROM 输出的数字逻辑电平(0V 或 5V)转换为显示器所需的模拟电压信号,Ben Eater 利用电阻并联分压的原理设计了一个极其简单的 2 位数模转换器(DAC)。当两个输入端均为 0V 时,输出端自然为 0V。如果仅将接有 $1.5\text{ k}\Omega$ 电阻的输入端置为 5V、另一端置为 0V,分压后在显示器端(考虑内部 $75\ \Omega$ 负载)大约能获得 $0.23\text{ V}$ 的电压;反过来,若仅让接有 $680\ \Omega$ 电阻的输入端接 5V,输出端大约会产生 $0.47\text{ V}$ 的电压。 当两个输入端同时接入 5V 高电平时,两个电阻并联工作。根据并联电阻的倒数之和计算公式: $$\frac{1}{R_{\text{parallel}}} = \frac{1}{1500} + \frac{1}{680}$$ $1500\ \Omega$ 与 $680\ \Omega$ 并联后的等效电阻约为 $468\ \Omega$,与显示器内部电阻分压后可得到约 $0.7\text{ V}$ 的峰值电压。通过这种方式,仅用两个电阻就能将 2 位二进制输入映射到 0V 至 0.7V 之间的 4 种离散电压台阶。Ben Eater 总结道:“So with just a couple resistors we can take 2 bits of binary data and convert that into one of 4 different voltage levels from 0 to 0.7 volts and if we do that for red green and blue we can use six bits of data coming out of our EEPROM to get four different shades of red green and blue which combine into 64 different colors.” ```mermaid flowchart LR accTitle: Two-bit DAC using parallel resistors accDescr: Schematic showing how two binary inputs combine through 1.5k and 680 ohm resistors to drive a 75 ohm VGA load. subgraph Inputs["数字输入 (0V / 5V)"] Bit0["最低位 (Bit 0)"] Bit1["最高位 (Bit 1)"] end subgraph Resistors["加权电阻网络"] R1["1.5 kΩ 电阻"] R2["680 Ω 电阻"] end subgraph Output["模拟端与负载"] VGA["VGA 模拟输入引脚<br/>(内部 75 Ω 接地下拉)"] OutLevel["输出模拟电压:<br/>00 -> 0.00 V<br/>01 -> 0.23 V<br/>10 -> 0.47 V<br/>11 -> 0.70 V"] end Bit0 --> R1 Bit1 --> R2 R1 --> VGA R2 --> VGA VGA --- OutLevel ``` - **三原色位分配**:6 位数据中,最低 2 位用于控制蓝色分量(Blue),中间 2 位控制绿色分量(Green),最高 2 位控制红色分量(Red)。 - **总色彩数**:每种原色支持 4 个亮度等级,三原色组合共可呈现 $4 \times 4 \times 4 = 64$ 种不同的颜色。 --- ### [11:35](https://www.youtube.com/watch?v=uqY3FMuMuRo&t=695s) 面包板硬件连线与初次点亮故障排查 明确电路原理后,Ben Eater 开始在面包板上组装电路。他拿出了 3 个 $1.5\text{ k}\Omega$ 电阻和 3 个 $680\ \Omega$ 电阻,分别对应红、绿、蓝三路信号。EEPROM 的前两个数据引脚(控制蓝色)分别接至 $1.5\text{ k}\Omega$ 和 $680\ \Omega$ 电阻;随后的两个引脚(控制绿色)同样接入一组 $1.5\text{ k}\Omega$ 和 $680\ \Omega$ 电阻;最后两个引脚(控制红色)依次连接最后一组电阻。每种颜色的两个电阻输出端在面包板上短接在一起,汇总出三路输出电压。随后,他将标准 15 针 VGA 接口的地线和水平/垂直同步信号接回原位,并将电阻网络汇整出的红、绿、蓝信号分别插入 VGA 接口的第 1、2、3 号引脚。 接好线路并给电路板通电、插上显示器后,屏幕如之前一样被唤醒并检测到了同步信号,但画面依旧一片漆黑。按理说,一片刚刚擦除完成的 EEPROM 其所有存储单元中的数据均被置为全 1,输出端应当向红、绿、蓝通道全部输出满幅高电平(0.7V),屏幕应当呈现纯白色。Ben Eater 敏锐地指出:“I think the problem is that if every address in the EEPROM is set to all ones then we're gonna be outputting a white for every position and that includes even in the blanking time here and we really shouldn't be putting out any pixels in this blanking time because well it should be blank.” 在消隐期间输出像素信号是严重的违规操作,在老式 CRT 显示器上有可能因为电子枪回扫未关断而损坏显像管。因此,必须在水平与垂直消隐期强制关闭 EEPROM 的输出。 - **VGA 引脚对应关系**:引脚 1 为红色(Red),引脚 2 为绿色(Green),引脚 3 为蓝色(Blue)。 - **擦除状态特征**:EEPROM(电可擦除可编程只读存储器)擦除后每个字节均为 `0xFF`(全 1)。 - **消隐危害**:行场消隐期间必须维持零电平,否则不仅显示器无法正确识别黑电平基准,严重时还会损伤 CRT 设备。 --- ### [13:36](https://www.youtube.com/watch?v=uqY3FMuMuRo&t=816s) 利用与非门(NAND)控制消隐期输出使能 为了确保仅在有效显示区域内输出色彩信号,Ben Eater 决定利用电路上监控水平与垂直消隐状态的双稳态触发器(Flip-flop)信号。EEPROM 的输出使能端(Output Enable, $\overline{\text{OE}}$)是低电平有效的(Active-Low)。这意味着只有当水平和垂直方向都处于有效显示期间(Display Period)时,$\overline{\text{OE}}$ 才应该被拉低以开启输出;而在任何消隐区间,$\overline{\text{OE}}$ 都必须为高电平以关断输出。 他在面包板上增加了一片 NAND 门(74HC00),将两路代表“处于显示期”的信号作为输入。当水平显示与垂直显示同时处于高电平时,NAND 门输出低电平驱动 EEPROM 的 $\overline{\text{OE}}$ 引脚。然而初次接线后,显示器仍然没有任何显示。Ben Eater 意识到自己可能混淆了触发器的正相输出 $Q$ 与反相输出 $\overline{Q}$,导致电路误在消隐期开启输出、而在显示期关闭输出。他将 NAND 门的两个输入端调换到触发器的反相端后,屏幕瞬间被点亮并稳定显示出纯白色画面。 ```mermaid flowchart TD accTitle: EEPROM Output Enable Gating Logic accDescr: Logic diagram showing horizontal and vertical display timing inputs feeding a NAND gate to control EEPROM active-low OE pin. H_Disp["水平有效显示信号"] V_Disp["垂直有效显示信号"] NAND["NAND 门 (与非门)"] OE["EEPROM OE 引脚 (低电平有效)"] H_Disp -->|高电平表示显示期| NAND V_Disp -->|高电平表示显示期| NAND NAND -->|"有效显示期输出 0V<br/>消隐期输出 5V"| OE ``` - **芯片特性**:EEPROM 的芯片使能($\overline{\text{CE}}$)与输出使能($\overline{\text{OE}}$)均为低电平有效逻辑。 - **故障排查**:信号极性接反导致有效显示窗口与消隐窗口颠倒,切换接至反相输出端后修复了全白屏幕显示。 --- ### [15:27](https://www.youtube.com/watch?v=uqY3FMuMuRo&t=927s) 图像准备、Photoshop 调色板映射与二进制导出 纯白屏幕验证了硬件链路的可行性,接下来的目标是显示一幅具有实际细节的彩色图像。Ben Eater 在网上找到了一张色彩丰富且属于公有领域(Public Domain)的鸟类照片。受限于显示卡硬件架构,屏幕分辨率仅为 $100 \times 75$ 像素。他将图片导入 Photoshop,先将其尺寸缩放至 $114 \times 75$,随后裁切(Crop)至精确的 $100 \times 75$ 像素大小。 针对硬件只支持 64 色的特性,Ben Eater 将图像的色彩模式更改为索引颜色(Indexed Color),并载入了与电阻分压网络严格对应的 64 色自定义调色板。调色板的索引顺序至关重要,例如索引值 8 必须精确对应分压电路在读入二进制数 8 时所输出的特定绿色。保存为 `image.png` 后,用文本/十六进制编辑器查看文件内容,会发现其中包含大量 PNG 文件头、XML 元数据及压缩数据,这些非像素数据绝不能直接写入 EEPROM。为此,Ben Eater 编写了一个使用 Python 图像处理库(PIL)的脚本,逐行读取图像的索引像素值并准备写入原始二进制文件: ```python from PIL import Image im = Image.open("image.png") pixels = im.load() out_file = open("image.bin", "wb") for y in range(75): for x in range(100): out_file.write(bytes([pixels[x, y]])) ``` 检查生成的 `image.bin`,所有字节值均严格小于十六进制的 `0x40`(十进制 64),证明所有像素都在 64 色调色板范围内。 然而,EEPROM 内部的数据排布与普通的线性文件并不相同。硬件电路上使用 7 位二进制计数器表示 X 坐标,7 位表示 Y 坐标。X 轴在显示完每行的 100 个像素(0 到 99)后,计数器复位回 0,同时 Y 计数器递增。由于 7 位计数器的高位进位发生在 128 处,地址会直接从 99 跳变至 128(即低 7 位归零,$2^7$ 权位置 1)。因此,EEPROM 中每一行像素必须按 128 字节进行对齐与填充。当 Ben Eater 尝试直接在 Python 脚本中将外层循环的 X 上限从 100 改为 128 时,程序抛出了 `IndexError: image index out of range` 异常,因为原始图像每行仅有 100 个像素点,直接越界访问未定义的像素导致了报错。 - **目标图像规格**:$100 \times 75$ 像素,64 色索引色模式。 - **调色板对齐**:色彩索引与硬件中红(2位)、绿(2位)、蓝(2位)的 DAC 电压组合形成一一映射。 - **地址对齐限制**:X 坐标占用 7 位地址线,每行在 EEPROM 中占据 128 字节的寻址空间,其中后 28 字节属于行填充区。 ### [19:41](https://www.youtube.com/watch?v=uqY3FMuMuRo&t=1181s) 解决像素越界与补零对齐 EEPROM 结构 在将图像数据转换为二进制文件的脚本中,当程序尝试去读取超出原始图像宽度的像素(例如第 100、101 或 128 个像素)时,由于这些像素并不存在于图像中,代码会直接抛出 `IndexError`。为了解决这个问题,Ben Eater 决定在代码中捕获这个异常:一旦发生越界,程序依然向输出文件写入数据,只不过写入的字符统一用 `0` 来填充。重新运行脚本后查看生成的十六进制数据,可以看到前 100 个像素为实际数据,随后是一连串用来填充至 128 字节的 `0`;接着是下一行的有效数据,后面同样紧跟着填满该行剩余空间的零。这种数据结构完全契合了硬件计数器和 EEPROM 地址线的映射需求。 尽管 Ben Eater 在之前的视频中曾经自己制作过一台 EEPROM 编程器,但由于当前所有数据已经预先处理成文件,他决定使用商业 EEPROM 烧录器来操作,因为这样不仅更加方便,而且烧录速度要快得多。他将 EEPROM 芯片从面包板上取下并放入烧录座,随后调用开源命令行烧录工具 `minipro`,指定芯片型号为 AT28C256,并尝试将生成的 `image.bin` 写入其中。然而,烧录程序报错提示“incorrect file size: 9600”。这是因为烧录工具要求写入文件必须与 32 KB(32,768 字节)的 EEPROM 物理容量完全一致,而当前文件只有 9600 字节。 ```mermaid flowchart TD accTitle: 图像数据填充与 EEPROM 映射流程 accDescr: 展示 Python 脚本如何将尺寸不足的原始图像数据补零扩展到 32KB 并烧录至 EEPROM subgraph S1["图像数据填充 (Python)"] A["读取原始像素<br/>(100 x 75)"] --> B{"X 范围判断<br/>(0 ~ 127)"} B -->|有效像素 0~99| C["写入图像颜色字节"] B -->|越界 100~127| D["补零写入 0x00"] C --> E{"Y 范围扩展<br/>(0 ~ 255)"} D --> E E -->|有效行 0~74| F["构建 9600 字节有效区"] E -->|越界行 75~255| G["全补零填充至 32,768 字节"] end subgraph S2["烧录与硬件载入"] F --> H["生成 32KB image.bin"] G --> H H --> I["Minipro 烧录到 AT28C256"] I --> J["插回面包板显卡点亮显示"] end ``` - 出现文件大小不匹配的原因是垂直方向的 Y 坐标只从 0 计数到 74(共 75 行),而垂直计数器实际提供了 7 位地址(可达 128),外加被接地拉低的 A14 地址引脚,实际上垂直方向共占用了 8 位地址空间(0 到 255)。 - 为了让文件满足 32 KB 大小,只需将脚本中 Y 轴的遍历循环上限扩大到 256,超出实际图像范围的行全部写入 `0` 即可。 - 重新转换后的二进制文件底部显示出一整行零,紧接着用通配符标示出全零区域一直延伸到 `8000`(十六进制,对应十进制 32768),此时再调用烧录器便顺利完成了烧录。 --- ### [22:12](https://www.youtube.com/watch?v=uqY3FMuMuRo&t=1332s) 点亮屏幕与 EEPROM 访问延迟分析 将烧录完毕的 EEPROM 插回面包板电路并重新通电后,VGA 显示器上成功呈现出了预期的鸟类彩色图片。Ben Eater 兴奋地表示:“This is our completed video card, but you know, it's producing an image from the ROM. So I'm going to call that a success.”(这就是我们做好的显卡,它确实从 ROM 里输出了图像,所以我认为这算是一次成功)。不过,仔细观察屏幕会发现画面上存在一些细微的黑色垂直虚线或噪点,Ben Eater 解释这主要是因为所使用的 ROM 速度较慢造成的时序问题。 显卡的时钟频率为 10 MHz,即每 100 纳秒计数一次;但 VGA 显示器接收的是按照 40 MHz 像素时钟标准解析的时序,显示器认为每 25 纳秒就应该有一个新像素到来。查阅 AT28C256 的芯片数据手册可以发现,从地址信号有效到输出数据稳定存在一个访问延迟(Address to Output Delay,即 $t_{ACC}$)。Ben Eater 所用的 `-15` 后缀芯片最大延迟高达 150 纳秒。在每 25 纳秒一个像素的节奏下,150 纳秒的延迟意味着在地址切换后最多可能有 6 个像素的时间内输出数据是无效的。如果此时数据线上恰好呈现为低电平(0),屏幕上就会闪现出这几个黑色的无效像素点。 - 现代计算机为了避免这种时延与动态画面问题,通常会将图像存储在专用的 SDRAM 显存中,其超高速的存取能力正是为了应对如此严苛的高频像素时钟。 - 计算机软件可以直接往显存高速写入新数据,使屏幕画面实时刷新变化;而在这块极简面包板显卡上,更换图像的唯一途径就是重新烧录或拔插更换 EEPROM,即使用视频剪辑加速后依然显得十分繁琐缓慢。 - Ben Eater 总结道:“Really the goal was just to get some kind of image on the screen and I'd say this is a success.”(其实真正的目标不过是在屏幕上显示出某种图像,在我看来这已经达成了目标)。 - 视频最后,他提到该项目的原理图、元器件数据手册及采购渠道均已整理发布在个人网站(eater.net)上,并向在 Patreon 上长期支持他制作这一耗时内容的所有赞助者表达了感谢。 ## Notable quotes > [00:00](https://www.youtube.com/watch?v=uqY3FMuMuRo&t=0s) Because we're using the sync signals here to stay synchronized with the monitor we can use our horizontal and vertical counters to keep track of what part of the screen the monitor is currently 'painting' at the moment. — Ben Eater ## Mentioned Ben Eater, VGA, AT28C256, EEPROM, 74HC00, Adobe Photoshop, Python, PIL, minipro, Patreon, CRT ## Video description For part 1, see this video: https://www.youtube.com/watch?v=l7rce6IQDWs Let's build a circuit that displays an image on a VGA monitor! In part 2, I talk about how VGA uses analog RGB signals to send different color pixels to the monitor. I use an EEPROM to store an image and build a simple digital-to-analog converter to generate the color signals to successfully display an image. Support these videos on Patreon: https://www.patreon.com/beneater or https://eater.net/support for other ways to support. ------------------ Social media: Website: https://www.eater.net Twitter: https://x.com/beneater Patreon: https://patreon.com/beneater Reddit: https://www.reddit.com/r/beneater Special thanks to these supporters for making this video possible: Name Alexander Catchpole Beau-James Erion Ben Ben Dyson Ben Kamens Ben Williams Christopher Blackmon Dean Winger Debilu Krastas Dušan Dželebdžić Eric Dynowski Erik Broeders fxshlein Gonzalo Belascuen HaykH Ian Tait Ivan Sorokin JavaXP Jay Binks Jayne Gabriele Jimmy Campbell Jordan Scales Justin Duch Manne Moquist Marcus Classon Mats Fredriksson Michael Burke Michael Garland Miguel Ríos Mirko Rener Nathan Wachholz Nicholas Moresco Nick Wrightsman Onion Sniffer Paul Pluzhnikov Randy True Robert Butler Sachin Chitale Sergey Ten SonOfSofaman Stefan Nesinger Steve Gorman Vladimir Kanazir Örn Arnarson [Source](https://www.youtube.com/watch?v=uqY3FMuMuRo)

Source quotes

[0:00] Ben Eater: Because we're using the sync signals here to stay synchronized with the monitor we can use our horizontal and vertical counters to keep track of what part of the screen the monitor is currently 'painting' at the moment.

Original transcript

[0:00] In a previous video we built this circuit that generates a valid VGA signal, and when we plugged a monitor in [0:05] we saw that the monitor recognized that it was in 800 by 600 mode. [0:08] And that's because we're sending at the right horizontal and vertical sync pulses for that mode. [0:13] We're sending a 3.2 µs horizontal sync pulse 38-whatever thousand times per second and [0:19] we're sending a 0.1056 ms sync pulse [0:24] 60 times per second. And the way this all works is we have a binary counter here that's counting from 0 to 264 [0:30] you know over and over again, and it's counting at exactly 10 million times per second. [0:35] So we can look for these particular values to get the pulse width and the pulse frequency [0:39] to match exactly; and then same thing in the vertical direction, you know [0:43] we've got another counter here that counts each line on the screen from 0 to 628 [0:46] and of course, you know [0:47] we measured all the timing and everything in the last video to make sure it matched the specs and it did and [0:52] so we saw the monitor recognize it as a valid signal. So if you want to know more about what's going on here, [0:57] I definitely recommend you check out the first video, but now you know [1:00] we probably want to actually display something on the screen. [1:02] And to do that, the VGA interface has a few more signals for red, green and blue. and [1:07] Because we're using the sync signals here to stay synchronized with the monitor [1:11] we can use our horizontal and vertical counters to keep track of what part of the screen the monitor is currently [1:16] "painting" at the moment. And so the 0 to 200 tells us where it is from left to right, and then the 0 to 600 [1:22] Will tell us where it is from top to bottom and so as the monitor paints the screen [1:25] You know, left to right, top to bottom: the same way you'd read a page in a book. [1:30] These red green and blue signals is how we tell the monitor what colour pixel we want at that particular location. [1:36] In other words: because we're synchronized, we can use our counters to know exactly what pixel the monitor is.painting at the moment [1:41] So for example if I take just one bit from one of these counters [1:44] (so this is the the vertical counter) if I take those, you know 1 2 4 8 16s place [1:49] Let's say this 16 s placed bit right here [1:52] you know as we count down the screen from 0 to 600 down the screen this bit is going to flip on or off every [1:58] 16 scan lines. [2:00] So if I take that bit and hook it up to the the green signal and I'm hooked it up through a [2:05] resistor (for reasons that I'll get into here in a minute) [2:08] There we go [2:09] You know [2:09] You can see every 16 lines the green flips on and off and that's because as our article counter counts from 0 to 600 [2:16] every 16 counts this [2:18] 16 places bit flips on and off because that's how binary counters work [2:22] Now if I hooked the red up here to the eights place in the same way [2:26] There we go [2:26] and now you now you can see the green and the red as well as [2:30] Yellow when the two mix and then of course, you know, I can also hook up blue [2:33] So, let me just do that for the sake of completeness [2:36] And now you can see the three primary colors mixed to give us actually 16 distinct colors [2:41] but you know as much fun as colorful stripes are what I really want to do is display a more complex picture and [2:47] for that to work [2:47] We need the pixel data stored in some memory somewhere [2:50] Normally in a computer the data for whatever's on the screen is stored in a part of RAM [2:54] that way the software running on the computer could just write new data to the same Ram location and the image on the screen will [3:00] Change right away, but I'm not gonna hook this to a computer at least not yet [3:03] So I'm just gonna store the image on an EEPROM and this is a 28 C [3:08] 256 EEPROM so it'll hold 32 k of data which should be enough for an image of some sort [3:13] and the way this will work is we already have our horizontal and vertical counters giving us an x and a y [3:18] position and if we feed all of those signals into the addresses of the [3:22] EEPROM [3:23] Then the EEPROM will give us a byte stored at that address and that byte could be the color of the pixel for that particular [3:29] XY location [3:30] So that's how we'll store the image in the EEPROM [3:32] But there's one weird issue which is because we went with the 10 megahertz pixel clock instead of 40 megahertz [3:38] we only have 200 pixels by 600 instead of 800 by 600 and [3:43] 200 by 600 is kind of a weird resolution [3:45] No [3:46] The pixels would be kind of stretched out because really what we're doing is we're repeating each pixel four times as we go across [3:51] So it'd be nice to maybe slow our vertical counter down. So we repeat each line four times as well [3:57] And it's actually pretty easy to do if we have our counter counting in binary like this [4:01] You know here we're just counting 0 1 2 3 all the way up to 16, you know, just counting in binary [4:06] Well, if we lop off that last bit [4:09] now it goes 0 [4:10] 0 1 1 [4:11] 2 2 and so forth up to 8 and if we lop off another bit [4:15] Then it repeat each number four times and only counts up to 4 instead of counting to 16 [4:19] So if we just ignore the bottom 2 bits of our Y counter that all divided the counter by 4 [4:24] We actually still have another problem which is the EEPROM. I have the [4:28] 22:56 it actually only has 15 address lines so we couldn't fit all these address lines here [4:33] Even if we wanted to so we've actually got to get it rid of at least three address lines in order [4:37] For it to actually fit in this EEPROM [4:40] So given what we've got with this EEPROM and to keep the right [4:43] proportions end up having to drop three bits from our Y counter and one bit from the X counter and that leaves us with a [4:49] Final image resolution of 100 pixels by 75 pixels which you know, I don't know it's maybe not the most impressive resolution [4:55] But I mean, what do you want for me? I'm trying to build a video card on bread boards [4:58] So 100 by 75 is is what we get. So let's hook the EEPROM up like this [5:04] I'll start by adding another breadboard and connecting power and ground to the EEPROM [5:10] Then I'll tie the right enable pin high since it will only be reading and it's active low and [5:15] Output enable and chip enable are both active low as well [5:18] So I'll tie them both low, so everything's enabled for output [5:21] Then the first seven address lines go down to our horizontal counter [5:24] and again [5:25] We're skipping the first bit and the next seven address lines go to the vertical counter and this time skipping the first three bits [5:32] So that's 14 address lines 7 for X 7 for Y. There is a 15th address line, which we're not going to use [5:38] So I'll just tie that to ground [5:41] So now we've got all of our address lines connected like this [5:44] So for each of our 100 by 75 pixels, we should be getting a byte of data out here [5:48] That'll presumably tell us what color that pixel should be [5:51] But how are we going to turn this 8-bit data into the signal that the VGA monitor expects? [5:56] well [5:57] the VGA interface has three pins red green and blue and each expects a voltage between zero and point 7 volts and [6:03] Depending on what that voltage is [6:04] Whether it's closer to 0 volts are closer to 0.7 volts [6:07] Determines how much of each color is mixed together to determine the color of the pixel? [6:11] But we've got eight bits here of data that are either 0 volts or 5 volts. How do we get something that's point 7 volts [6:19] well is the case where we have one voltage and we need a lower voltage and so we can use a voltage divider and [6:24] A voltage divider is just two resistors like this. So relative to ground down here [6:28] We've got 0 volts and up at the top. We've got 5 volts and in the middle here [6:32] It's going to be somewhere between 0 and 5 volts and how far it is from 0 volts to 5 volts [6:37] Depends on how big r2 is compared to the total resistance [6:41] So if r2 is half of the total resistance that is r1 and r2 or the same value then in the middle here [6:47] We're gonna have half the voltage. So two and a half volts if r2 were 10% of the total resistance [6:53] Then we'd have 10 percent of the total voltage or half a volt [6:56] So this expression just describes that? [6:59] You look at the proportion of r2 to the total resistance [7:02] And then that tells you how much of the total voltage you get [7:04] Now the VGA spec says that the red green and blue signals it says, you know zero to 0.7 volts [7:11] It also says it's got this 75 [7:13] ohm input impedance and [7:15] You can roughly think of that as meaning that inside the monitor [7:18] Those red green and blue inputs are kind of connected to ground like this through a 75 [7:22] Ohm load of some kind [7:24] so if we've got five volts and we want to get it down to [7:27] 0.7 volts as it's going into the monitor here. We can just add a resistor like this and that creates a voltage divider, right? [7:33] So we've got five volts at the top [7:35] We've got ground here at the bottom and we want this to be 0.7 volts here in the middle [7:39] Now if the monitors input impedance is 75, ohms [7:41] we can't change that but we can put whatever resistor we want here between our 5 volts and the input to the monitor and [7:47] You know to figure out what resistor that needs to be in order to get 0.7 volts [7:50] well, we could just look at this expression here and say you know [7:53] When would this expression equal 0.7 volts given that r2 is is going to be 75, ohms [7:59] well, we can put [8:01] 75 ohms in here for r2 and set it equal to 0.7 volts and we just need to solve for R [8:07] So 5 times 75 is is 375 and then R + 75 times 0.7 is gonna be 0.7 R + 52 and 1/2 [8:15] Subtract that 52 and a half from both sides we get 0.7 R [8:20] Equals 3 22 and a half we can divide that by 0.7 and we get R equal to four hundred and sixty point seven. Ohms [8:28] So if we take five volts and we put it through a four hundred and sixty point seven [8:31] Ohm resistor will have point seven volts here going into our monitor, you know given that there's a 75. Ohm load inside that monitor [8:38] But ideally we don't just want point seven volts or zero volts going into the monitor here, you know [8:43] ideally [8:43] we'd be able to have a range of voltages between zero and point seven volts so we can get different brightnesses of red green and [8:48] Blue, yeah, so that we're able to have different shades of colors [8:51] so for example [8:52] If we wanted four different voltages from zero at 0.7 [8:55] Evenly-spaced so we could get four different shades of red green and blue [8:58] We'd have to solve this equation essentially for each of those different voltages to find out what resistor we need to get that particular voltage [9:04] So that's what I've done here. That's what these different resistances are and [9:08] This is for you know, one-third brightness two thirds brightness, and then of course full brightness at 0.7 volts [9:13] Which is what we just we just figured out is four hundred sixty point seven [9:16] And we could use these different resistances to get these different voltages so that we end up with these different [9:21] Brightness levels for each of the colors and of course, you know, they don't make fifteen hundred fifty five point four [9:26] Ohm resistors they make you know fifteen hundred. Ohm resistors and they don't make [9:31] 720 two point nine. Ohm resistors, but you know, I've got 680 ohms which is maybe close enough [9:37] So if we use those resistors to build something like this that's got two inputs over here that can be you know [9:42] Either 0 or 5 volts and an output over here and check this out [9:45] You know if both inputs are zero like this then the output table. These are going to be 0 volts, right? [9:50] But if one input here is 5 volts [9:53] So the one hooked to the 1.5 cave' resistor is 5 volts and this is a zero then essentially we're kind of in this scenario [9:58] Here where we have, you know, approximately 1,500 [10:01] 1,500 ohm resistor and we're gonna get about [10:04] 0.23 volts over here [10:06] But if we flip that around and we we don't have anything here and we have five volts down here [10:10] Then we're going through the 680 [10:11] Ohm resistor and we're gonna have you know about 700 or well we're gonna have about 0.47 volts [10:16] But then if we have ones on both of these inputs, so both of these inputs are 5 volts [10:22] Well, then these resistors are going to combine in parallel and to combine resistors in parallel [10:26] You use this expression here, which is sort of the sum of the reciprocal of the sum of the reciprocals [10:32] So to combine 1,500, ohms and 680. Ohms in parallel like this [10:37] It's going to appear as a single resistance from 5 volts to this point over here of 468 [10:42] Which is pretty close to here and so we're going to get 0.7 volts, so [10:47] this takes two inputs that can either be 0 or 5 volts and [10:50] gives us the 4 different possibilities that give us these 4 different voltage levels from 0 up to 0.7 volts and [10:57] So with just a couple resistors [10:59] we can take 2 bits of binary data and convert that into one of 4 different voltage levels from 0 to 0.7 volts and [11:05] If we do that for red green and blue [11:08] we can use six bits of data coming out of our EEPROM to get four different shades of red green and blue which combine into [11:13] 64 different colors and so this is the mapping for those different colors [11:17] If you're familiar with these hex codes for colors [11:19] But you can see the last two bits here controls how much blue there is now the middle two bits controls [11:25] how much green there is and then the first two bits controls how much red there is and [11:29] Then this here is what those actual colors look like [11:31] And so those are the 64 colors that were able to generate with a circuit like this [11:35] So let's actually hook these resistors up to the outputs of our EEPROM and to here the 1.5 K resistors [11:40] And of course, there's three of them one for red green and blue and here are the 680 [11:44] Ohm resistors and so hook the first two data bits up for blue [11:48] And the first one is going to go to a 1.5 K resistor and then the next one will go to a 680 [11:53] Ohm resistor, then the next two bits will be for green. So go to a 1.5 K resistor and then to a 680 [12:01] Ohm resistor and then finally the last two bits are gonna be for red [12:05] The 1.5 k resistor and the 680 [12:08] Ohm resistor and then we just need to tie this side of the the resistors for each color together [12:13] So they'll do blue [12:15] Green and red. And so now over on this side we should be getting that voltage between zero and 0.7 volts [12:22] So now we can try hooking this up to the monitor again [12:24] So here's our 15 pin vga connector and i've got the the same sync signals before in the ground, of course [12:30] But now i've got the red green and blue hooked up to pins 1 2 & 3 [12:34] so we can hook up ground and hook up our sync signals as before so the horizontal sync and [12:40] The vertical sync and then the colors we could just hook up over on this side of the resistors [12:45] So there's blue there's green and there's red [12:50] now if we power up our circuit and plug in the monitor [12:56] So we basically see the same thing as before so the monitor comes alive [12:59] so it's detecting the sync signal but it's still a blank screen and [13:03] Of course, that could be because what's in the EEPROM is just blank [13:06] But I happen to know that this is an erased EEPROM and when you were to race an EEPROM it it just writes all one's [13:11] So we should actually see a white screen and we're not [13:15] but I think the problem is that if every address in the EEPROM is [13:19] Set to all ones then [13:20] We're gonna be outputting a white for every position and that includes even in the blanking time here [13:25] And we really shouldn't be putting out any pixels in this blanking time because well it should be blank [13:30] And in fact, this is one of those things that might actually damage a CRT monitor and hopefully we won't damage my monitor [13:36] But the way we can fix that [13:38] Is use these signals that were detecting here for the blanking intervals, right? [13:42] Because we're actually detecting when we're in this horizontal blanking interval [13:46] Right here with this flip-flop and we're detecting when we're in the the vertical blanking interval down here [13:52] So really what we need to do is we need to figure out are we in the blanking interval and those together? [13:56] and we could actually use the result of that for the output enable signal for our EEPROM so we can essentially turn the output of [14:03] The EEPROM off if we're in that blanking interval [14:05] so what I'm going to do is add a NAND gate here and I'm using a NAND gate so that the output is [14:10] inverted because our chip enable or not our tripping well actually both [14:13] But the output enable which is the one we're going to use the output enable is active low [14:17] So that way if if two inputs here are high [14:20] then the output will be low and then the two inputs that we'll use is well look at if we're in the display period for [14:27] horizontal and the display period for vertical [14:31] so, let me hook up power and ground for our NAND gate and [14:34] so the first input here is going to say are we in the horizontal display period [14:38] So in other words, this is saying are we in this time here? [14:41] and then the next input is going to say are we in the vertical display period [14:46] So that's going to say are we in this time here vertically? [14:49] If the answer is yes [14:50] if we're in both of those then what we want to do is we want to take the [14:55] Output of that and use that instead of just always tying our output enable low. So here we'll say output enable [15:03] Only when both of these conditions are true [15:06] So now let's reconnect our monitor and see what we get [15:09] And that's still not working [15:12] But it's possible that I've got these mixed up and so I'm actually outputting only during the blanking interval [15:17] So, let me just switch this over to [15:18] the [15:20] inverted outputs on both of these [15:23] And there we go now it actually looks like we're getting looks like a white screen [15:27] So if we want to display something a little more interesting then we've got to come up with a little more interesting image [15:32] Let's find an image. That's nice and colorful. This looks good public domain. That's perfect [15:37] So I'll download I guess, you know, we're gonna reduce the resolution here quite a bit. So it doesn't matter too much [15:43] So then if I load that image up in Photoshop, we can resize it [15:47] So we want our image to be 100 by 75 [15:50] So 100 by well make it 114 by 75 and then we can crop it to 100 by 75 [15:57] Okay, so there's our image with the resolution that we have on our screen. It's now we need to change the color modes [16:02] Who are using indexed color and we want a custom palette? [16:07] and [16:09] So these are the 64 colors that we can produce with our resistor voltage dividers and they're also in the in the right order [16:15] so for example an index of 8 is this [16:18] You know green color and if we have an 8 in our EEPROM then that's going to produce that same green color [16:23] So it's pretty important that we have the right colors in here and we have them in the right order [16:27] So if we apply that [16:29] This is the image we get so those are the 64 colors that we support and it's a 100 by 75 [16:34] So that is that is the best our wonderful breadboard video card can do so, let's go ahead and save this [16:40] We'll call it image dot PNG [16:42] And if we actually look at what's in that file if we see, you know, there's more than just a pixel data, right? [16:47] There's a header here PNG. There's you know appears to be some XML stuff in here [16:52] So there's a whole bunch of stuff in here that we definitely don't want to write to our EEPROM [16:56] so what we've got to do is we've got to convert this PNG data into the pixel bytes that we want to put into our [17:00] Indoor EEPROM and the best way I can think to do that is just with a little Python [17:04] Script, we can use a Python image library to make a little bit easier. So pale is this Python image library [17:13] And we can open up an image file [17:15] and then we can load the image and when we load the image what we get is we just get a [17:18] two-dimensional array of the pixels and [17:21] because we're using index colors with 8 bits and [17:25] You know, we've got our colors in the right order here with the right index values [17:30] the pixels that we end up loading are going to be the values that we want to actually [17:34] output from our EEPROM to get those particular colors [17:36] So really what we want to do is we just want to write all those pixels to a file without any of the PNG header [17:40] Information without any compression without anything like that [17:43] We just want to write the raw pixels to a file and then we can write that file the EPROM [17:47] That's what I'm gonna do is just go through all the pixels. And since our image is 100 by 75 [17:51] I'll just go through all 75 lines and then go through all hundred pixels per line [17:55] and then I just want to write the value of that pixel to a file so [18:00] I'll create an output file [18:01] just call it image bin and we'll just write to this as a binary file and [18:05] What we'll write to it is we'll write a character that has the value of the pixel [18:10] so the pixel is going to be coming from our pixels array and it's just gonna be at the [18:15] XY value that we're at [18:20] So if we run this that'll create that image bin file [18:27] And if we look at the image that bin file what we should see in here is we should see all of the pixels [18:32] so, I don't know if this looks like a bird to you, but [18:35] One sign that we're on the right track here is that all of these values in here are less than 64? [18:39] Or while they're in hex, but they're less than 40 hex which is which is 64 [18:43] So at least everything seems to be you know in the right range [18:46] But this isn't quite how we want to organize the data in the EEPROM [18:49] because remember we're using [18:50] 7 bits for the exposition and 7 bits for the Y position and so because we have a hundred pixels per row [18:55] We have this X counter that goes up to 99, but once it gets to 99 [18:59] It doesn't just go to 100 [19:01] it goes back to zero and then we increment our Y position and so the actual address in the EEPROM will jump from 99 to [19:08] 128 because these all go to zero and then this bit will be a 1 and that's the hundred twenty eights place [19:13] so in the EEPROM really even though we only have a hundred pixels per line on the screen in the EEPROM we want to take [19:18] Up 128 pixels per line, but that's easy enough. We can just for each line instead of a hundred we can just do 128 and [19:25] Then because we're writing that 128 times we'll end up writing 128 things to the file. So let's run the conversion again and [19:33] we get an error so image index out of range and that's because you know [19:38] our image only has only has 100 pixels across so [19:41] You know when we're trying to get pixel 128 or even pixel 101 or 100 that pixel is not actually in our image [19:48] So what we can do is we can just try to catch that condition [19:50] So we get an index error when we try to get that pixel. That's not actually part of our picture [19:55] What we can do is we can still write to the file [19:57] But the character will write to the file would just be I mean, it could be I guess we'll just write a zero [20:04] So let's run that and now if we look at our image [20:08] We still see the data but what we see is we see, okay [20:10] This must be the first hundred pixels, and then we have a bunch of zeros that Pat it out to [20:15] 128 and so you can see like this block here [20:17] That's the first line and then this next block here with all the zeros padding it out to the end [20:22] That's the next line [20:23] And so this should be the format of data that we want to put into the EEPROM [20:27] Now I suppose I could use the EEPROM programmer that I built in a previous video [20:32] But in this case because I've got all the data in a file and everything already [20:35] It's probably easiest just to use a commercially prom programmer. It's also a lot faster. Let's pop our EEPROM out put it in the programmer [20:43] Then I'll use Mini Pro which is just an open source EEPROM programmer tool that works with this programmer and [20:48] Will tell it we're using the 1828 C 256 which is the EEPROM [20:52] We've got and we want to write the image bin file to it and it says incorrect file size [20:57] 9600 so that's because I guess it's expecting a file. That's the exact size of the EEPROM and we have a 32 K EEPROM [21:03] So we need to give it a 32k file [21:05] And the reason our file is is smaller is because our Y is only counting from 0 to 74 even though you know [21:12] This is 7 bits so it could go up to 128 [21:14] And actually there's an a 14 right that's that's this extra address line that we just tied to zero [21:19] So there's actually another 8 bits here [21:21] So our Y value really could go from 0 to 255 [21:25] But we're only writing the first 70 75 values of that and that's just so that our you know image aspect ratio makes sense [21:32] We can do the same thing here. We can just change this to 256 and [21:36] Then anything that's out of range will just write a zero to the file [21:40] So let's reconvert and then if we take a look at our image file [21:45] you can see the image but then if we go all the way down to the [21:48] you know if we get all the way down at the bottom it says [21:51] we have got a row of zeros and then I think this star just means everything is zeros between there and [21:56] Eid 0 0 0 which is 32,000 [21:59] So let's try to write it again. And there we go. It's writing it [22:04] And so now let's put our EEPROM back in and see if we get the picture of that bird [22:12] Hey and there it goes and [22:15] So this is our completed video card, but you know, it's producing an image from the ROM. So I'm going to call that a success [22:22] Well, you might notice these thin black vertical lines and I think that's because the ROM I'm using is relatively slow. Yes [22:28] I'm using a 10 megahertz clock which means it's counting 10 million times per second or you know [22:33] Once every hundred nanoseconds remember the monitor still assumes I'm using a 40 megahertz clock [22:37] so really the monitor is expecting a pixel every 25 nanoseconds and [22:42] So here's the problem if we look at the data sheet for the EEPROM that we're using [22:46] There's a delay from when the address goes valid. The address lines are valid to when the output is valid [22:52] That's this TAC C or access time and if we look at access time here the addressed output delay [22:57] It's a maximum up to 150 nanoseconds for the dash 15, which is the one I'm using [23:04] Well 25 nanoseconds per pixel [23:06] 150 nanosecond delay could mean up to six pixels could be [23:10] invalid in this in this period from when the address changes to when the output is actually valid and you know [23:15] It makes sense that sometimes invalid might just mean that the output is zero [23:18] And so I think that's why we sometimes see a few black pixels like this [23:22] Now in a computer like I mentioned before the image would be stored in SDRAM [23:26] Which is designed to be very fast access for this very reason [23:30] And of course the you know the software running on the computer could write new data to that RAM and the image would change right? [23:35] away [23:36] whereas here the only way to change the image is to reprogram and/or swap the EEPROM which as [23:41] You can see even if I speed it up when I edit. The video is is pretty slow [23:45] But you know, that's not really the point is it, you know [23:48] Really the goal was just to get some kind of image on the screen and I'd say this is a success [23:52] And if you want to try this yourself, you can head over to my website [23:55] I've got schematics data sheets and where you can get all the parts and as always, you know [23:59] I want to thank my patrons for making it possible for me to create videos like this as you can imagine [24:04] these were rather time-consuming videos to plan out and produce and [24:08] without the support of these people and all my patrons who are just viewers like you [24:12] Making this kind of video just wouldn't be possible. So, thank you