Developers

Open hardware, open firmware.

Everything the card does is on GitHub under GPL-3.0: the FPGA logic, the ARM firmware, the Apple II demo sources and the schematic. This page is the map, for people writing Apple II software that targets the card and for people who want to change the card itself.

Source & license

The repository is github.com/hasseily/appletini-one. Releases with a ready-to-flash FIRMWARE.BIN are on the Releases page; the Downloads page points at the latest.

DirectoryWhat is in it
hdl/SystemVerilog and VHDL sources, the active source manifest, and board pin and timing constraints under hdl/constraints/. The Apple-side logic lives in hdl/apple/: bus wrapper, cycle capture, soft-switch manager, every virtual card, the W65C02 core and the TransWarp bus engine.
ps_sources/Bare-metal ARM code: the golden updater, the frontend (menu, storage, USB, coprocessors, printer, networking), the CPU1 renderer, and shared libraries.
scripts/Vivado, Vitis, image-generation, programming and regression tools.
software/Apple II demos, diagnostics, ACME assembly sources and disk images, including the demo volume.
tools/mcp/A serial control server that exposes a live Apple II to MCP clients.
schematics/The Rev. A3 board schematic, as PDF.
third_party/External components with their own license terms.

The project is licensed GPL-3.0. Bundled components keep their own licenses and attribution: the XTulator 8088 CPU core is GPL-2.0-or-later, the USB stack is CherryUSB, the 6502 network demos use IP65, and the Super Serial Card and SmartPort ROM sources come from the a2retronet project.

Architecture

The card is a Xilinx Zynq-7020: FPGA fabric plus two ARM Cortex-A9 cores at 766 MHz. The split is strict. The fabric (PL) owns everything cycle-sensitive on the Apple bus: capturing every cycle, answering as each virtual card, the soft 65C02 and its posted-write shadow, video timing, the Disk II sequencer, the 6522s and the SSI-263 speech synthesis. The two ARM cores (PS) provide what a cycle does not have to wait for: SD storage, the video renderer and HDMI compositor, USB host services, the menu and configuration, the Z80 and 8088 machines, the ImageWriter interpreter, FTP and the network stack.

Emulation is driven by the real bus, not by a software model of the Apple. That is why timing-sensitive demos, copy protection and raster tricks behave as they do on the original cards. The PS talks to the PL through a small AXI register bank (card-control registers) and DDR-backed shadows of Apple memory; the PL reports its RTL version at runtime alongside the firmware and golden-updater versions so a mixed build is visible immediately.

Virtual slot map

Software sees the card as several original cards in fixed slots. A physical card in a slot a virtual card uses must be removed; slot 7 must always be free.

SlotVirtual card
1Super Serial Card driving a color ImageWriter II, and the Uthernet II (W5100S). They share the slot with disjoint decodes and work at the same time.
2Apple Mouse Card, fed by a USB mouse.
3TransWarp accelerator, as its configuration presentation. The 80-column firmware is the //e’s own.
4Phasor: dual Mockingboard with dual SSI-263 speech chips. Slot 4 is slowed down under acceleration while the Phasor is active.
5PCPI Appli-Card (Z80) or ALF AD8088 Plus (8088), one at a time. Physical slot 5 must be empty.
6Disk II controller with two drives, WOZ-accurate.
7The boot menu card at power-on, which hands off to the SmartPort controller, with the text overlay on the slot’s DEVSEL block; or the SuperSprite, which takes slot 7 exclusively and disables both.

Off the slot bus, the card also provides RamWorks-style auxiliary memory (8 MB under acceleration), a no-slot clock, and the VidHD-compatible video controls used for Super Hi-Res and SHR4.

Detecting the card

The supported way for Apple II software to know it is running with an Appletini is a standard SmartPort STATUS call: status code $03 (GETDIB) to unit $00, the controller, in slot 7. The Device Information Block carries an ID string: Appletini SP from the controller, Appletini HD from a device unit. Compare the first nine characters. No soft-switch probing, no ROM tricks, and the controller answers even with no images mounted.

One caveat: slot 7 presents the SmartPort ROM only after the boot menu hands off. On power-up and after every Ctrl-Reset, slot 7 is the boot-menu card, which advertises itself as a Disk II ($C707 reads $3C) and will not answer GETDIB. Application code loaded from SmartPort or Disk II is past the handoff already; a reset-vector hook is not. Check the ROM signature first, as the sample does. With SuperSprite enabled, SmartPort is off and this detection is unavailable.

AddressValueMeaning
$C701$20SmartPort/ProDOS signature
$C703$00SmartPort/ProDOS signature
$C705$03SmartPort/ProDOS signature
$C707$00SmartPort device ($3C means boot-menu phase, not ready)
$C7FF$0AProDOS entry offset, so the ProDOS entry is $C70A
$C70DSmartPort dispatch entry (ProDOS entry + 3)

The call uses the standard inline-parameter convention: JSR the dispatch address, then the command byte and a pointer to the parameter list. Carry clear is success. The controller DIB puts the ID length ($0C) at offset 8 and the text at offsets 9–20, with firmware major and minor versions at offsets 27 and 28. A device DIB (unit $01 and up) puts the length at offset 4 and the text at offset 5.

The complete routine, in ACME syntax, is software/detect_appletini.a65:

; detect_appletini.a65 -- detect an Appletini from 6502 assembly.
; Returns:  carry CLEAR -> Appletini present
;           carry SET   -> not present / slot 7 not yet handed off
; Assemble: acme -o detect_appletini detect_appletini.a65

!cpu 6502
* = $0800

SLOT        = 7
ROM         = $C000 + (SLOT * $100)   ; $C700  slot-7 ROM
SIG5        = ROM + $05               ; $C705  -> $03 on a SmartPort card
SIG7        = ROM + $07               ; $C707  -> $00 on a SmartPort card
SP_ENTRY    = ROM + $0D               ; $C70D  SmartPort dispatch

SETSLOTCXROM = $C006                  ; IIe: map peripheral-card ROM at $Cx00

buffer      = $0900                   ; DIB result buffer (>= 32 bytes)
SIG_LEN     = 9                       ; compare just "Appletini"

detect
                sta SETSLOTCXROM        ; harmless on a II/II+

                ; 1. Is slot 7 presenting the SmartPort ROM yet?
                lda SIG5
                cmp #$03
                bne not_present
                lda SIG7
                bne not_present         ; must be $00

                ; 2. SmartPort STATUS / GETDIB to unit 0 (controller)
                jsr SP_ENTRY
                !byte $00               ; CMD = STATUS
                !word params
                bcs not_present         ; carry set -> call failed

                ; 3. Compare the controller ID string to "Appletini"
                ldx #0
cmp_loop
                lda buffer + 9, x
                cmp sig_text, x
                bne not_present
                inx
                cpx #SIG_LEN
                bne cmp_loop

found
                clc                     ; carry clear = Appletini detected
                rts

not_present
                sec                     ; carry set = not detected / not ready
                rts

params
                !byte $03               ; parameter count
                !byte $00               ; unit 0 = SmartPort controller
                !word buffer            ; status-list (result) pointer
                !byte $03               ; status code $03 = GETDIB

sig_text
                !text "Appletini"       ; plain low ASCII, as the firmware emits it

Compare against low ASCII; the firmware does not set the high bit. For portability across SmartPort cards, derive the dispatch address from $C7FF rather than hard-coding $C70D. The sta $C006 only matters on the //e family; save and restore INTCXROM if your code must preserve it.

Text overlay (VT100)

The card can draw a text layer above the normal Apple picture from a buffer your program keeps in ordinary Apple RAM. You lay out 16-bit cells (a character byte, then a VGA-style attribute byte) in one linear, row-major block anywhere from $0200 to $BFFF in main or auxiliary memory; the card watches the writes, keeps a private copy, and composites the text into the HDMI output. It never touches Apple RAM, never stops the processor, and leaves the video soft switches alone, so text, hi-res, double hi-res and Super Hi-Res all keep working underneath.

  • VT100 mode: 7-bit ASCII plus the DEC Special Graphics set at $00–$1F; bit 7 underlines. CP437 mode: all 256 IBM glyphs, for ANSI and BBS screens.
  • 8×14 or 8×16 fonts, integer scaling on each axis, any grid up to 255×127, placed anywhere on the canvas (1120×768 in legacy modes, 1280×800 in SHR).
  • The standard 16-color VGA/ANSI palette, blink or 16 background colors, a hardware cursor (block, underline or bar), and an optional transparent background.
  • An ARM → fill → SHOW handoff: ARM latches the layout and clears the card’s copy, you write every cell, SHOW flips at the next frame edge. Two buffers give flicker-free full redraws.

It coexists with SmartPort. A slot has three separate decodes, and the two cards in slot 7 use different ones, so both are present at the same time and never collide. The SmartPort answers only in the slot-ROM space and the expansion-ROM window; the overlay answers only in the slot’s 16-byte DEVSEL block, which the SmartPort does not decode. A program can draw with the overlay while it boots from, reads and writes SmartPort volumes. In the FPGA the overlay is built into the SmartPort card itself, sharing its bus gating, so it comes and goes with SmartPort: both are present after the boot menu hands off, and both step aside when SuperSprite takes slot 7.

Slot-7 decodeRangeAnswered by
IOSEL, slot ROM$C700–$C7FFSmartPort
IOSTROBE, expansion ROM$C800–$CFEF, plus DATA/CTRL/DPOP at $CFF0–$CFF2SmartPort
DEVSEL$C0F0–$C0FFText overlay

Detection is read-only: +$E reads $4C (‘L’), +$F the BCD version, and +$8–+$D spell LINTXT. Never write to a slot that has not passed all three checks.

OffsetNameR/WFunction
+$0INDEXRWSelects one of 256 indirect registers (buffer base, CONFIG, COLS, ROWS, origin, SCALE, cursor, fill values, canvas size, active layout, CAPS).
+$1DATARWReads or writes the selected register.
+$2DATA_INCRWSame, then increments INDEX.
+$3CMDW$00 OFF, $01 ARM, $02 SHOW, $03 HIDE.
+$4STATUSRBit 7 BUSY, 6 STALE, 5 CONFIG_ERROR, 4 FRAME_PENDING, 1 ARMED, 0 VISIBLE.
+$8–+$FMAGIC, SIG, VERRIdentification, fixed regardless of card state.

The interface is written as a standard any Apple II video card can implement, with the Appletini as the reference. The full specification, with every register, the range rules, frame-edge semantics, the detection routine and a worked 80×24 example, is hosted here: Linear RAM Text Overlay Interface 1.0. A demo that uses it is software/textoverlay.a65.

Virtual TransWarp

The accelerator mirrors the bus contract of the physical Applied Engineering TransWarp: a full shadow of Apple memory, sparse bus traffic, and /DMA held while the soft CPU runs. Because the card’s capture, serving and renderer paths were hardened against that exact master, every test run against the real card doubles as the acceptance suite for the virtual one. The one electrical difference is an improvement: bus cycles are driven with ideal 6502 timing, address and R/W asserted early in PHI1 and held through the cycle.

Things software can observe:

  • Under acceleration the machine is always an Enhanced //e. The 16 KB Enhanced //e ROM is loaded into the shadow on every takeover, and $C011–$C01F status reads are served from the tracked switch state.
  • $C019 RDVBLBAR uses the native VBL boundary at line 192, cycle 0, with the AppleWin convention for bit 7.
  • The TransWarp speed register at $C074 is decoded; vtw.c074.ignore turns that off for software that pokes it by accident.
  • Presets: MAX (33 MHz), 26, 13, 7, 3.6, 2.6 and cycle-exact 1 MHz. Config keys: vtw.enabled, vtw.speed.mode, vtw.pace.divider, vtw.disk2.acceleration.disabled. UART: vtw [status|on|off|speed full|1mhz|div <n>].
  • Ctrl-Reset cannot tear a running session down; the enable is latched for the session.

The core is a synthesizable, cycle-stepped W65C02S in SystemVerilog with the full 256-entry opcode map, verified against 2.54 million SingleStepTests vectors, the Klaus Dormann functional, extended, decimal and interrupt suites, and 266 directed checks. Design notes: README_VIRTUAL_TRANSWARP.md and README_W65C02_CORE.md.

Z80 Appli-Card

Slot 5 can be a PCPI Appli-Card. The Apple-visible latch protocol is in the fabric; the Z80 and its 2 MB of private memory (32 banks of 64 KB) run on the ARM side. There is no slot ROM and no shared memory, exactly as on the original: both processors talk only through the latches, so ARM execution latency never changes Apple-visible timing. The embedded 2 KB v9 boot ROM is CRC-checked at start, and the Z80 interpreter passes ZEXALL, undocumented behavior included.

AddressReadWrite
$C0D0Z80-to-6502 byte; clears its pending flagIgnored
$C0D1Read back the 6502-to-Z80 latchWrite a byte and set its pending flag
$C0D2Bit 7: byte pending for the Z80Ignored
$C0D3Bit 7: byte pending for the 6502Ignored
$C0D4$FFIgnored
$C0D5Reset the Z80; returns $FFReset the Z80
$C0D6$FFIgnored (the CTC socket is unpopulated)
$C0D7Pulse Z80 NMI; returns $FFPulse Z80 NMI
$C0D8–$C0DF$FFIgnored

On the Z80 side, each function is mirrored across a 32-port range: $00–$1F the Z80-to-6502 latch, $20–$3F the 6502-to-Z80 latch, $40–$5F handshake flags, $60–$7F map or unmap the boot ROM, and $C0–$DF the bank and common-area register (bits 1–3 keep GZ/80S behavior, bits 4–5 select the extra banks, bit 6 maps the upper 32 KB common area to bank 0).

Config keys: applicard.slot5.enabled, applicard.resource.max. UART: z80 status|on|off|reset, z80 budget <tstates>, z80 wall <microseconds>, z80 dump <addr> [len]. Demo media and validation programs are under software/applicard/. Full notes: README_APPLICARD.md.

ALF AD8088 Plus

The other slot-5 personality is a 640 KB ALF AD8088 Plus, a compatibility superset of the original Processor Card including the AD128K range. The fabric implements the sixteen I/O ports at $C0D0–$C0DF with the port-0 bit-7 handshake; the ARM side runs an 8088-compatible XTulator core and a clean-room monitor in a dedicated 1 MB DDR arena.

  • $00000–$0FFFF 64 KB local RAM
  • $10000–$1FFFF 64 KB Apple shared-memory window
  • $20000–$2FFFF 64 KB expansion RAM
  • $30000–$3FFFF unpopulated (reads $FF)
  • $40000–$BFFFF 512 KB expansion RAM; $40000–$5FFFF is the AD128K-compatible range
  • $C0000–$FEFFF unpopulated ROM space (reads $FF)

The monitor implements integer commands 29–32, floating-point 33–47, user commands 48–247, SEQUENCE, RANDOM, SET MEMORY, MOVE DATA and far CALL. The original ROM’s graphics and MET commands 1–28 are accepted as no-ops; software that depends on them is outside the current scope. Single-byte accesses use the normal DMA path; fills and copies move four bytes per DMA hold and release the bus between batches, keeping every hold under the 6502’s 10 µs register-retention limit. The 8088 and the virtual TransWarp cannot run together, since both own the bus.

scripts/build_ad8088_test_disks.py generates DOS 3.3 and ProDOS test disks that exercise the mailbox, a monitor command, the AD128K range, and real 8088 code writing straight into the Apple text page. UART 8088 status reports instruction counts and instructions per second. Full notes: README_AD8088.md.

Printer

The virtual Super Serial Card runs the real 1981 Apple SSC ROM with its DIP switches hardwired to the classic printer setup: 9600 baud, printer mode, no delays, no width formatting, LF after CR for the BASIC entry. Ctrl-I commands work as on the real card. The virtual 6551 always reports transmit-ready, and every byte goes into a 2 KB FIFO the ARM drains through the card-control registers.

Slot-1 resourceOwner
$C0n1/$C0n2 DIP readback, $C0n8–$C0nF 6551 ACIASuper Serial Card
$C0n4–$C0n7 W5100 windowUthernet II
$C100–$C1FF slot ROM, $C800–$CFFF expansion ROMSuper Serial Card

The ImageWriter II interpreter renders draft text in all eight pitches, bold, underline, half-height, super- and subscript and double width; ESC G/S/g/V dot graphics at the per-pitch densities; ESC T/A/B line spacing; ESC F/L positioning; ESC H form length; and ESC K 0–6 for black, yellow, magenta, cyan, orange, green and purple on the four-color ribbon, with overlapping strikes mixing their ink. LQ-only commands, MouseText and user-defined characters are consumed and ignored. Each page is a US Letter PNG at 144 dpi. Config key: printing.ssc.enabled. Full notes: README_PRINTER.md.

Ethernet

The Uthernet II-compatible W5100S sits in slot 1 next to the SSC. The user is expected to have configured it first, from the Ethernet tab of the config menu: MAC, IP, subnet and gateway, static or DHCP. The card writes those values to the chip at boot, so software that reads them from the W5100 gets a working setup without carrying its own network configuration. Anything written for the Uthernet II should work unchanged.

software/appletini_webserver/ holds two ProDOS system programs built with cc65 and the IP65 stack: A2WEBSRV.SYSTEM, a small HTTP/1.0 status server, and A2BROWSE.SYSTEM, the text browser on the demo disk. Both follow Contiki’s design (one MACRAW socket, ARP/IPv4/TCP on the 6502), run on an NMOS 6502, and ask the gateway to resolve names since the W5100 has no DNS register.

Demo sources

The software/ directory is the Apple-side toolbox. The ACME sources cover the raster-bar demo, the speed race, the SHR and SHR4 viewers, the SuperSprite demo, the text overlay, the mouse card, aux-memory and bank-map stress tests, and the AD8088 tests. Disk images include the 32 MB demo volume, the AD8088 MS-DOS drive, and the Appli-Card CP/M media. Build helpers in scripts/ regenerate the Appletini-authored programs onto the demo volume. The demo disk itself is a free download.

Boot & flash

The card boots from a 16 MB QSPI flash with a permanent golden updater and one firmware slot.

RegionOffsetSizeContents
Golden0x000000000x00200000BOOT.BIN: FSBL and the golden updater
Firmware0x002000000x00DF0000FIRMWARE.BIN: FSBL, PL bitstream, CPU1 renderer and frontend
Metadata0x00FF00000x00010000Last verified update record

Golden loads first, brings up UART, SD and QSPI, and if the SD root holds a FIRMWARE.BIN it validates and installs it, reading back every programmed byte before committing metadata, then renames the file to FIRMWARE.OK. Otherwise it boots the verified slot through the Zynq multiboot register. The SD path never touches the golden region, and a failed update never marks the slot valid. If no valid firmware exists, golden waits in its serial monitor, where a host can send an image over XMODEM-CRC:

python scripts/serial_firmware_update.py FIRMWARE.BIN --port COM3 --reboot-golden

Direct QSPI programming over JTAG (scripts/program_boot.bat, scripts/program_firmware_slot.bat) covers bring-up and last-resort recovery. Full notes: README_BOOT_UPDATE.md.

Building the firmware

You need Vivado 2025.2 and Vitis 2025.2, plus a JTAG programmer for hardware bring-up and UART access for diagnostics. Generate the Vivado project, build the bitstream and XSA, then build the Vitis applications:

vivado -mode batch -source scripts/create_project.tcl
vivado -mode batch -source scripts/build_and_export_xsa.tcl
vitis -s scripts/create_vitis_workspace.py

Then create the two flash images:

scripts/make_boot_bin.bat
scripts/make_firmware_bin.bat

A C-only frontend change needs a Vitis rebuild and a new FIRMWARE.BIN. HDL, clock, AXI or constraint changes need the full Vivado and Vitis sequence, and the hardware and Vitis platform must match. See README_VIVADO.md and scripts/SCRIPTS_README.md.

Tests

Source-level regressions are scripts/test_*.py, one per subsystem: the Appli-Card protocol and banking, the Uthernet II, config profiles, the SSC and ImageWriter interpreter, the AXI wrapper, PSRAM capture, and the SSI-263 formant pipeline. Most run with plain Python from the repository root. test_vtw.py is the simulator-backed accelerator gate: pin-level Disk II response timing, raw WOZ read and write at native and accelerated speed, and an end-to-end run from the soft core to the Disk II at every preset. Simulator checks need the Xilinx tools on PATH.

Hardware behavior is checked over UART and JTAG and through the MCP server below. The firmware console exposes serve-path counters (write queue drops, deadline misses, lost cycles) that must all read zero on a healthy card.

MCP server

tools/mcp/appletini_mcp.py exposes a live Apple II, through the card’s UART, as MCP tools, so an agent or a script can watch and drive the machine.

ToolWhat it does
apple2_screen_textThe live text screen as plain text, 40 or 80 columns. Reads the DDR write-mirror shadow, so zero bus impact, and it works under graphics modes where error messages hide.
apple2_peekHex dump of main RAM from the shadow (writes since power-on).
apple2_soft_switchesDecoded //e soft-switch state and RamWorks bank.
apple2_machine_statusMachine id, aux memory, RamWorks, physical aux-card probe, firmware status.
apple2_healthServe-path counters; all must be zero.
apple2_bus_traceRecent bus cycles from the trace ring.
apple2_menu_keyDrive the Appletini config menu remotely.
apple2_consoleAny firmware console command; help lists them.
pip install mcp pyserial
claude mcp add appletini -e APPLETINI_PORT=COM5 -- python tools/mcp/appletini_mcp.py

The console is a single shared channel, so close any terminal holding the port first. reboot and reset restart the card’s firmware, not the Apple. The server drives the Appletini menu only; it does not inject Apple keyboard input. Shadows mirror writes since power-on, so ROM and never-written RAM read as zero.

Hardware

Rev. A3, designed by Karl ‘KKR75’ Asseily. The schematic is in the repository as schematics/AppleTini_ONE_vA3.pdf. On board: the Zynq-7020, DDR, QSPI flash, PSRAM for cycle capture, level translators to the Apple bus, mini-HDMI out, RJ45 Ethernet, USB-C host, a 3.5 mm audio jack with an optional optical output, a micro-SD slot, a DC power input, and two holes beside it at the top left which, bridged, disconnect the slot’s +5 V feed so the card can run standalone from the DC input. Pin and timing constraints are in hdl/constraints/.

Found a bug, or built something on the card? Open an issue or a pull request on GitHub, or write to us.