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:
- 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.
- 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.
- 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.
- 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:
- Runs CPU for a frame’s cycle budget.
- Processes pending I/O (keyboard, disk).
- Renders a video frame.
- 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)
- Memory manager and ROM loader; display ROM boot messages in a basic UI.
- Implement minimal 6502 core supporting NOP, LDA/STA, JMP — get code execution flowing.
- Add full 6502 instruction set with addressing modes and basic timing.
- Implement text video rendering and keyboard input.
- Add more video modes (hi-res) and basic sound.
- Implement disk image support and DOS booting.
- 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.