Rapid Prototyping: Implementing an Apple II Emulator in LabVIEW

LabVIEW Apple II Emulator Tutorial: Emulate Classic Apple II HardwareThis tutorial shows how to create an Apple II emulator using LabVIEW. It covers architecture, CPU emulation, memory and I/O mapping, video and audio output, keyboard input, timing, and testing with classic software. The goal is a functional, well-structured emulator that runs simple Apple II programs and provides a platform for learning both vintage computer architecture and LabVIEW programming techniques.


Target audience and prerequisites

This guide is intended for engineers, hobbyists, and students with:

  • Basic familiarity with LabVIEW (VI structure, arrays, state machines, event loops).
  • Understanding of digital systems and CPU basics.
  • Interest in retro computing and emulation concepts.
  • Optional: some knowledge of 6502 assembly (Apple II CPU).

Software/hardware needed:

  • LabVIEW (2018 or later recommended).
  • Optional: LabVIEW FPGA / real-time modules for performance, but standard LabVIEW is sufficient for a basic emulator.
  • Apple II ROM images and disk images for testing (ensure you have legal rights to use them).

High-level architecture

An emulator reproduces the behavior of original hardware in software. Break the project into modular components:

  • CPU core (6502 instruction set and timing)
  • Memory subsystem (RAM, ROM, memory-mapped I/O)
  • Video generator (text and high-resolution graphics modes)
  • Keyboard and joystick input
  • Audio (speaker toggle behavior)
  • Peripheral devices (disk drives, cassette, printers) — optional
  • System bus/timing and synchronization
  • UI for loading ROMs, disks, and controlling emulation

Each component should be implemented as separate VIs (LabVIEW subVIs) with well-defined interfaces to simplify testing and reuse.


CPU emulation: 6502 basics

The Apple II uses a MOS Technology 6502 (or compatible) CPU. Core emulation responsibilities:

  • Implement the 6502 instruction set (ADC, SBC, LDA, STA, JMP, JSR, RTS, BRK, interrupts, etc.).
  • Maintain CPU registers: A (accumulator), X, Y, SP (stack pointer), PC (program counter), and processor status flags (N, V, B, D, I, Z, C).
  • Correctly model addressing modes (immediate, zero page, absolute, indirect, indexed, etc.).
  • Implement cycle counts for each instruction for timing-accurate behavior.

Implementation tips in LabVIEW:

  • Use a state machine VI that fetches opcode from memory, decodes it (lookup table/array of function pointers implemented as case structures), executes micro-operations, updates cycles.
  • Represent registers as numeric scalars; status flags can be a cluster or bitmask integer.
  • For decoding, create an array of clusters mapping opcode (0–255) to a VI reference or a case name string. Use dynamic VI calling (VI Server) or a large case structure keyed by opcode.
  • Optimize hot paths (fetch/decode/execute) by minimizing VI calls and using inlined code where possible.

Example opcode dispatch structure (conceptual):

  • Fetch byte at PC.
  • PC = PC + 1.
  • Lookup opcode entry: addressing mode, base cycles, operation.
  • Compute effective address via addressing-mode function.
  • Execute operation function (reads/writes memory, sets flags).
  • Subtract cycles and loop until cycles for frame exhausted.

Memory and I/O mapping

Apple II memory map (simplified):

  • \(0000–\)07FF: Zero page and stack (RAM)
  • \(0800–\)BFFF: Main RAM (varies by model)
  • \(C000–\)C0FF: I/O, soft switches, video text page pointers
  • \(C100–\)FFFF: ROM (BASIC, monitor, etc.)

Key points:

  • Memory is byte-addressable. Use a 64K array (0–65535) of U8.
  • ROM areas should be read-only — writes ignored or routed to shadow RAM depending on soft-switches.
  • I/O locations trigger side-effects (e.g., writing to certain addresses changes video mode). Implement soft-switch handling in memory write VI: if address in I/O range, call I/O handler instead of storing data.

LabVIEW implementation:

  • Central memory VI that provides Read(address) and Write(address, value) methods.
  • On Write, check address ranges and route to I/O handlers as needed.
  • Keep ROM data separate and mapped into read responses for ROM addresses.

Video: rendering text and hi-res graphics

Apple II produced video via a video generator driven by memory-mapped video pages. Two main modes matter:

  • Text (40×24) using character ROM
  • High-resolution graphics (bitmap, color artifacts due to NTSC)

Goals:

  • Recreate enough behavior to display text and simple hi-res graphics programs.
  • Optionally simulate NTSC color artifacting for authentic color output.

Steps:

  1. Video memory model:
    • Text: Character codes in video page memory map to glyphs in character ROM. Build a glyph ROM (array of 7–8 bytes per character) and render into a pixel buffer.
    • Hi-Res: Implement Apple II hi-res bitmap addressing (weird interleaved memory layout). Map bitmap bytes to pixel positions taking into account the 7-pixel-wide bytes and color artifact rules.
  2. Framebuffer:
    • Create a 280×192 (hi-res) or scaled framebuffer (e.g., 560×384) in LabVIEW as a 2D array of U32 (RGBA) or U8 triplets.
  3. Rendering loop:
    • Run video rendering on a timed loop at ~60.15 Hz (NTSC field rate).
    • At each frame, read current video memory, render glyphs/bitmap to framebuffer, and update a picture control or panel using LabVIEW’s image APIs.
  4. Performance:
    • Cache rendered glyphs and only redraw changed regions when possible.
    • Use LabVIEW’s IMAQ or .NET/Call Library for faster image blitting if available.

Keyboard and input

  • Map LabVIEW keyboard events to Apple II key matrix.
  • The Apple II reads a keyboard register; implement an input handler that updates memory-mapped keyboard state when the host keyboard events arrive.
  • For joystick/game paddle, map to mouse or external controller inputs if desired.

Implementation:

  • Use an event structure VI to capture key presses/releases.
  • On key press, set appropriate bits in a keyboard buffer; on read of the keyboard register (poll by CPU), return current buffer state and optionally clear or shift it per model behavior.

Audio: speaker and beeps

Apple II audio is simple: the CPU toggles a speaker output line by writing to a soft-switch. Emulation steps:

  • Track speaker state (on/off).
  • Produce a square wave (or buffered samples) when speaker toggles; for simplicity, map speaker state to toggling an audio sample buffer at a fixed sample rate.
  • Use LabVIEW sound VIs to output audio; for better timing, run audio generation in a separate timed loop or use the sound API’s buffer callbacks.

Timing and synchronization

Accurate timing determines whether software and peripherals run correctly.

  • Emulate CPU cycles and decrement cycle budget per video scan or per frame.
  • Typical approach: run the CPU for N cycles per frame where N ≈ CPU frequency (1.023 MHz for Apple II) divided by frame rate (~60.15 Hz) → about 17,000 cycles/frame.
  • Synchronize CPU execution with video rendering and I/O polls. Use a main loop that:
    1. Runs CPU for a frame’s cycle budget.
    2. Processes pending I/O (keyboard, disk).
    3. Renders a video frame.
    4. Sleeps or waits to maintain frame timing.
  • Implement interrupts (NMI, IRQ) according to video line or peripheral conditions if needed.

Disk and cassette support (optional)

  • Disk emulation: Implement a simple disk image loader (2IMG, DSK). Emulate Disk II controller or higher-level file system by intercepting BIOS/disk routines.
  • Cassette: Emulate cassette I/O by sampling/writing audio and interpreting rhythms—complex; optional for advanced accuracy.

Disk implementation advice:

  • Start by supporting reading disk images into an abstract file API that responds to read requests from DOS ROM routines.
  • Later add a Disk II controller state machine that responds to read/write sector commands.

Debugging, testing, and validation

  • Start small: get a ROM monitor running (so you can step/peek/poke memory and execute single instructions).
  • Use known test ROMs and Apple II demo programs to validate correctness.
  • Implement a debugger UI: registers display, memory viewer, breakpoints, single-step, instruction disassembly.
  • Compare behavior with reference 6502 emulators or test suites to validate instruction timing and flags.

Example LabVIEW project structure (folders & VIs)

  • /CPU
    • CPU_Main.vi (fetch-decode-execute loop)
    • AddrMode_*.vi (addressing mode helpers)
    • OpCode_*.vi (operation implementations)
    • Registers.lvclass
  • /Memory
    • Memory_Manager.vi (Read/Write)
    • ROM_Loader.vi
    • IO_Handler.vi
  • /Video
    • Video_Render.vi
    • Glyph_ROM.vi
    • HiRes_Mapper.vi
  • /Input
    • Keyboard_Event.vi
    • Joystick.vi
  • /Disk
    • Disk_Controller.vi
    • Disk_Image_Loader.vi
  • /UI
    • Main.vi (controls, load ROMs, run/stop)
    • Debugger.vi
  • /Utils
    • Timing_Manager.vi
    • Logger.vi

Performance tips

  • Minimize cross-VI calls in the CPU hot path; use a tight single-VI loop for fetch/decode/execute.
  • Use native data types (U8/U16) and arrays rather than variants/clusters for memory operations.
  • Precompute lookup tables for flag results (e.g., Zero/Negative) to reduce branching.
  • Consider using LabVIEW Real-Time or FPGA for cycle-accurate timing if host scheduling causes jitter.

Example development roadmap (milestones)

  1. Memory manager and ROM loader; display ROM boot messages in a basic UI.
  2. Implement minimal 6502 core supporting NOP, LDA/STA, JMP — get code execution flowing.
  3. Add full 6502 instruction set with addressing modes and basic timing.
  4. Implement text video rendering and keyboard input.
  5. Add more video modes (hi-res) and basic sound.
  6. Implement disk image support and DOS booting.
  7. Polish UI, add debugger, optimize performance.

Closing notes

Building a LabVIEW Apple II emulator is an excellent project to learn both 6502 architecture and LabVIEW system design. Start iteratively: get simple features working first, then expand toward full compatibility. Focus on modularity so you can replace or optimize components (e.g., swap in a native C 6502 core later) without rewriting the whole system.

Good luck with the build — tackle one subsystem at a time and keep testing with real Apple II programs as you go.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *