惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

TaoSecurity Blog
TaoSecurity Blog
L
LINUX DO - 最新话题
Help Net Security
Help Net Security
N
News | PayPal Newsroom
www.infosecurity-magazine.com
www.infosecurity-magazine.com
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
The Last Watchdog
The Last Watchdog
S
Security @ Cisco Blogs
W
WeLiveSecurity
C
CXSECURITY Database RSS Feed - CXSecurity.com
Webroot Blog
Webroot Blog
T
Troy Hunt's Blog
V
Vulnerabilities – Threatpost
Google Online Security Blog
Google Online Security Blog
N
News and Events Feed by Topic
T
Threat Research - Cisco Blogs
Security Archives - TechRepublic
Security Archives - TechRepublic
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
T
Tor Project blog
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
D
Darknet – Hacking Tools, Hacker News & Cyber Security
PCI Perspectives
PCI Perspectives
Google DeepMind News
Google DeepMind News
T
Tailwind CSS Blog
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
Apple Machine Learning Research
Apple Machine Learning Research
IT之家
IT之家
S
SegmentFault 最新的问题
J
Java Code Geeks
P
Privacy & Cybersecurity Law Blog
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
博客园 - 【当耐特】
博客园_首页
H
Hacker News: Front Page
T
Threatpost
Jina AI
Jina AI
博客园 - Franky
月光博客
月光博客
L
LINUX DO - 热门话题
The Cloudflare Blog
H
Heimdal Security Blog
博客园 - 司徒正美
酷 壳 – CoolShell
酷 壳 – CoolShell
Cloudbric
Cloudbric
雷峰网
雷峰网
Hugging Face - Blog
Hugging Face - Blog
S
Secure Thoughts
T
Tenable Blog
I
Intezer
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻

Lobsters

CIFSwitch: a non-universal Linux local root vulnerability RIPE NCC session fixation: poaching logins with an Atlas probe GNOME 2.20 but its Web Components Agentic Search for Context Engineering – Leonie Monigatti Garnix is shutting down [not OC] akashina.tngl.sh/jjc Concerning Emacs (and Jazz) Nitpicking the shell history scene in ‘Tron: Legacy’ What's cooking on SourceHut? Q2 2026 The tenth OpenPGP email summit Package managers that package package managers Clojure on Fennel part three: parsing WordPress at 23 Finding Miscompiles for Fun, Not Profit GitHub - creusot-rs/creusot: Creusot helps you prove your Rust code is correct. Announcing Rust 1.96.0 | Rust Blog A Love Letter to Neovim sqlite AGENTS.md Am I a Bad Friend? CSS vs. JavaScript • Josh W. Comeau Erlang Ecosystem Foundation - Supporting the BEAM community A brief note about slot access cost in Common Lisp Keyboard latency probe Rethinking the GNOME clipboard issues Back to the Building Blocks’ Building Blocks Tech Notes: Theseus: translating win32 to wasm Fast is better than slow Content-addressed Rust builds (or, what kache actually caches) Intent to Prototype: Embedding API Canada’s Bill C-22 and the security cost of collecting more data 5 PostgreSQL locking behaviors that trip people up okmij.org Stop advertising in your commits! | AksDev GitHub - mplsllc/macsurf: A modern web browser for Classic Mac OS 9 PowerPC. Real CSS3, ES5 JavaScript, native HTTPS — built with CodeWarrior on the Carbon API. Introducing DoomBench - Can Your Data Stack Run DOOM? What are some of your favourite developer tools? Building a Scalable Ingestion Pipeline with Temporal (Part 1) Converting shallow Git bundles into normal repositories Are you a member of any professional associations? What is a harmonic? An interactive comic about additive synthesis How Virtual Tables Work in the Itanium C++ ABI Using SwiftUI to Build a Mac-assed App in 2026 Rust (and Slint) on a jailbroken Kindle. ~jack/lambda-on-lambda - Serverless Haskell on AWS - sourcehut git Human proof for FOSS contributions Extremely simple internet radio controlled via IRC Announcing BABLR Splitting Konsole views from Helix to run tools | AksDev GitHub - yugr/rust-slides Serving files over HTTP three ways: synchronous, epoll, and io_uring update docs with information about building with build.py (#979) · astral-sh/python-build-standalone@c9c40c5 A Simple Makefile Tutorial On C extensions, portability, and alternative compilers Switching to Colemak | Pedro Alves Just How Bad Was The Intel IAPX432? Nix's Substituter List Is Not a Routing Table Accelerating copy_if using SIMD Lambda on Lambda: Serverless Haskell on AWS | Blog Announcing feed-repeat v1.0 Scaling Akvorado BMP RIB with sharding EYG news: A host of CLI improvements, new guides and new effects The social contract of writing JS Crossword C array types are weird; and related topics Flatpak will depend on systemd – OSnews Migrating from Go to Rust | corrode Rust Consulting A portentous reunion Vivado Licensing Options How my minimal, memory-safe Go rsync steers clear of vulnerabilities the entropy layer of a wavelet codec, on its own GitHub - nferhat/fht-compositor: A dynamic tiling Wayland compositor. Debian SE Linux and PinTheft Does bulk memmove speed up std::remove_if? (No.) 声明式部分更新 | Blog | Chrome for Developers Fully in-browser container builds Dianne Skoll's Web Site - Remind The Architecture of Open Source Applications (Volume 1)Berkeley DB Pardon MIE? - ironPeak Blog “Long-Term Support” doesn’t mean what you think Jira IS Turing-Complete May I recommend thinking of Emacs as your Fortress of Solitude hershey Floodgap Gopher-HTTP gateway gopher://thelambdalab.xyz/1cuneiforth/ HP QuickWeb, Singular And Pointless That one time I used Go panics for flow control A new suite of modern tools coming for editing and publishing RFCs From the Tabletop… The Digital Antiquarian Building a Host-Tuned GCC to Make GCC Compile Faster Are we self-sovereign PKI yet? Claw Patrol: an open-source security firewall for agents | Deno Revised^7 Report on Scheme, Large: Procedural Fascicle Draft is now public A Network Allow-List Won't Stop Exfiltration — André Graf From AFSK to Goertzel – µArt.cz Software For My New Home Server Introducing Neptune: Direct3D virtualization for QEMU AI Agent Bankrupted Their Operator While Trying to Scan DN42 - Lan Tian @ Blog mimalloc: A new, high-performance, scalable memory allocator for the modern era Making wl_shm fast The Soul of Maintaining a New Machine - Third Draft | Books in Progress What is Git made of?
The Card That Made the Apple II Serious — Wise Owl
Brent Rector · 2026-06-28 · via Lobsters

Part 1 of 2. This part covers the Videx VideoTerm’s hardware architecture, the MC6845 CRTC, and why slot 3 is architecturally special. Part 2 covers the rendering pipeline, C8 ownership, and the Pascal boot hang.


In 1977, the Apple II shipped with 40 columns of text. That was fine for BASIC programs and games. It was not fine for WordStar, VisiCalc, or the CP/M business software that was reshaping the industry. A serious word processor needed 80 columns. A spreadsheet with real data needed 80 columns. Anyone who wanted to use an Apple II for actual work needed 80 columns.

The Videx VideoTerm, introduced in 1980 and manufactured in Corvallis, Oregon, was the card that solved this problem — and dominated the market until Apple built 80-column support into the IIe in 1983. It was the most widely supported 80-column card for the Apple II and II+, compatible with Apple Pascal, WordStar, and virtually every serious business application of the era. Understanding why it worked the way it did requires understanding a chip that has largely faded from memory: the Motorola MC6845 CRT Controller.

The original 1981 Videx VideoTerm card (top) alongside the A2FPGA Tang Nano 20K board that emulates it (bottom)

The Display Problem

Before getting to the Videx card itself, it helps to understand what Apple II video actually looked like.

The Apple II+ outputs NTSC composite video — a single analog signal designed for 1970s consumer televisions. It works, in the sense that you can see the screen. But NTSC composite has color bleeding, chroma artifacts on text, and a horizontal resolution that is fundamentally limited by the bandwidth of the signal. The 40-column text mode that the stock Apple II produces is legible on a period-correct TV. On anything else it is a reminder of how much display technology has changed.

The A2FPGA bypasses composite output entirely. Rather than capturing and upscaling the analog signal, it monitors the Apple II’s video memory — the text pages and hires graphics pages that the Apple II’s video circuitry reads to generate its composite output — and generates HDMI directly from those memory contents. The FPGA reads the same bytes the Apple II would use to build its composite signal, and renders them into a clean 720×480 digital frame. The result is text and graphics at correct pixel boundaries, with no analog smear.

Adding the Videx emulation extends this pipeline. The Videx card has its own 2KB of video RAM, separate from the Apple II’s memory map, that stores the 80-column character grid. When 80-column mode is active, the FPGA substitutes its Videx VRAM rendering for the Apple II’s native 40-column text — producing sharper 80-column output on a modern HDMI display than any physical Videx card connected through the Apple II’s composite output ever produced.

The MC6845

The MC6845 is a programmable CRT controller from 1977. Its job is to generate the timing signals that drive a CRT display: horizontal sync, vertical sync, and cursor position. It is not a video chip in the modern sense — it generates no pixel data itself. It tells the host system where the electron beam is and trusts the host to supply the right character data at the right time.

The 6845 exposes 18 registers through a two-port interface. You write a register number to the address register, then read or write the data register. The registers control everything from the number of characters per line to the vertical total to the cursor blink rate. The registers are largely write-only on the original MC6845, though the Hitachi HD6845SP used on the Videx VideoTerm is a Type 1 variant that allows full read-back — a detail that matters for software that checks its own configuration.

If you look at the physical Videx board, the HD46505SP is the largest chip, front and center. The two 2716 EPROMs above it — labeled “INVERSE (C)1981 VIDEX” and “STD. 7X9 E (C)1981 VIDEX” — are the two halves of the character ROM. The “VI-FIM-500 (C)1982 VIDEX” chip is the firmware ROM. A small army of 74LS-series logic chips handles address decoding, bus buffering, and timing.

The FPGA emulation replaces all of it. Every chip on that board — the HD46505SP, both character ROM EPROMs, the firmware ROM, and all the glue logic — lives in about 250 LUTs and 160 registers in the GW2AR-18C fabric.

Why Slot 3 Is Different

The Apple II has 7 expansion slots. Electrically, they are almost identical — each provides the same set of bus signals plus a slot-specific I/O select and device select. But slot 3 has a hardware quirk that makes it unlike every other slot in the machine: it controls the INTCXROM internal ROM mechanism.

When the CPU accesses memory in the $C300–$C3FF range (the slot 3 ROM space), the Apple II hardware checks a soft switch called SLOTC3ROM. If SLOTC3ROM is 0 (the default), accessing $C300 sets a flag called INTC8ROM, which routes the $C800–$CFFF expansion ROM space to the Apple IIe’s internal 80-column firmware instead of external cards. On the Apple ][+, SLOTC3ROM doesn’t exist, so it’s always 0 — meaning any $C3xx access would set INTC8ROM and block all expansion ROMs from every card.

This is why Videx must be in slot 3. The SLOTC3ROM/INTCXROM mechanism exists specifically for the 80-column card slot. The Videx firmware is written to work within this constraint — it accesses $CFFF at specific points to release C8-space ownership and let other cards’ expansion ROMs operate. The slot 3 hardware architecture is, in effect, the 80-column card architecture baked into silicon.

Emulating this correctly means understanding the ownership protocol, which turned out to be one of the more subtle parts of the implementation.

The Address Map

The Videx card responds to three address regions:

$C0B0–$C0BF (device I/O) — This is the slot 3 device select range. Address bits [3:2] select one of four 512-byte VRAM banks. Address bit [0] selects between the MC6845’s address register (even) and data register (odd). Writes go to the CRTC. Reads return CRTC register values or VRAM contents.

$C300–$C3FF (slot ROM) — The 256-byte slot firmware ROM. Accessing this range also sets the expansion ROM ownership flag, giving the Videx card control of the $C800–$CFFF space.

$C800–$CFFF (expansion ROM + VRAM window) — Once the card owns C8-space, it serves its 2KB firmware from $C800–$CFFF. Within this range, $CC00–$CDFF is a VRAM window: the 512 bytes of VRAM currently selected by the bank bits in the device I/O address.

The VRAM window is clever. The Apple II’s video RAM on the Videx card needs to be 2KB (80 columns × 24 rows = 1920 bytes, rounded up). But the slot ROM is only 256 bytes and device I/O is only 16 bytes. The expansion ROM space is 2KB — and by carving a 512-byte window into it at $CC00, the Videx firmware can access the full VRAM through the C8 space, bank-switching between four 512-byte pages via the device I/O address bits.

The Firmware ROM

The Videx firmware is a 2KB ROM that serves two address ranges: the 256-byte slot ROM at $C300 and the full 2KB expansion ROM at $C800. It contains the Apple II character output hook, the 40/80-column switching logic, and interrupt handler patches.

The key entry point is at $C300, where the first two bytes are $2C $CB — a BIT $CBxx instruction. This is the standard Apple II slot ROM idiom: the first byte is a BIT opcode, which executes harmlessly but uses the next two bytes as an address operand. This lets the firmware signal to the system that this ROM has a Pascal-compatible entry structure, and it allows CALL 771 (which is $C303) to be a valid Pascal peripheral card protocol entry point.

$C313 contains 4C 00 C8JMP $C800 — which hands control to the expansion ROM for the main initialization path.

CardCat utility performing a blind ROM dump of the Videx emulation's $C300 slot ROM, returning correct bytes

That CardCat screenshot is reading $C300–$C3FF directly from hardware. It has no knowledge of the A2FPGA — it’s a generic ROM examination utility. The bytes it returns match the original Videx firmware exactly, because that’s what the emulation is serving.

The VRAM Architecture

The Videx VideoTerm uses 2KB of video RAM to store the 80×24 character grid. In the original hardware, this is implemented as static RAM with dedicated address multiplexing between the CPU and the MC6845’s display scan. The 6845 reads character codes sequentially as it scans the display, while the CPU writes new character data through the device I/O window.

In the FPGA implementation, the 2KB VRAM is a single Gowin SDPB block — a simple dual-port block RAM with asymmetric port widths. The write port is 8 bits wide (CPU access through the $CC00–$CDFF window). The read port is also 8 bits wide and connects to the rendering pipeline’s character lookup logic.

The bank addressing scheme maps cleanly: the device I/O address bits [3:2] select which 512-byte bank the $CC00–$CDFF window points to, giving the firmware access to all 2048 bytes of VRAM across four bank accesses.

The Character ROM

The Videx character ROM stores the pixel patterns for each character: 256 characters, each 16 rows tall (though only 9 rows are active in the standard 7×9 font), 8 pixels wide. In the original hardware, that’s two 2KB EPROMs — which is why the physical card has two chips, labeled “STD. 7X9 E” (the standard character set) and “INVERSE” (the inverse video characters $80–$FF, which are simply the bit-inverted versions of characters $00–$7F).

In the FPGA, storing both halves would cost 2 BSRAM blocks (pROMs) for 4KB of data. But the inverse relationship is exact — character $80 is character $00 with every pixel bit flipped — which means we can store only the lower 128 characters and generate inverse pixels in logic. One XOR gate per pixel, triggered by bit 7 of the character code. This saves one full BSRAM block, which matters: the GW2AR-18C has only 46 BSRAM blocks total, and the complete five-card build uses 45 of them.

The implementation uses only the low 7 bits of the character code for the ROM address, then XORs the output with a mask derived from bit 7 and the cursor active signal:

// ROM lookup uses only low 7 bits (128 unique characters)
videxrom_a_r <= {char_code[6:0], scan_line[3:0]};

// Inverse and cursor combined into a single XOR at pixel capture
pix_buffer_r[6:0] <= videxrom_d_r[6:0] ^
    {7{videx_data_r[7] ^ (cursor_active && cursor_byte == 2'd0)}};

The cursor blink and inverse video merge into a single operation. On real Videx hardware, cursor inversion and character inversion are separate mechanisms implemented with discrete logic. In the FPGA, they collapse to one XOR.

Proof of Identification

Software that knows how to identify expansion cards will find exactly what it expects:

CardCat identifying the Videx VideoTerm in slot 3, alongside SSC, Mockingboard, and Disk II

CardCat reads the firmware ROM signature bytes from each slot and matches them against a database. Slot 3 comes back as “Videx 80 Column Text Display Card” because the firmware ROM bytes are identical to what a real Videx card would return. The emulation is indistinguishable from hardware to software that doesn’t know to look for an FPGA.

Part 2 covers how the character data gets from VRAM to pixels on the screen: the rendering pipeline, the C8-space ownership state machine, and the Pascal boot hang that took the longest to diagnose.