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.
| Directory | What 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.
| Slot | Virtual card |
|---|---|
| 1 | Super 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. |
| 2 | Apple Mouse Card, fed by a USB mouse. |
| 3 | TransWarp accelerator, as its configuration presentation. The 80-column firmware is the //e’s own. |
| 4 | Phasor: dual Mockingboard with dual SSI-263 speech chips. Slot 4 is slowed down under acceleration while the Phasor is active. |
| 5 | PCPI Appli-Card (Z80) or ALF AD8088 Plus (8088), one at a time. Physical slot 5 must be empty. |
| 6 | Disk II controller with two drives, WOZ-accurate. |
| 7 | The 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.
| Address | Value | Meaning |
|---|---|---|
$C701 | $20 | SmartPort/ProDOS signature |
$C703 | $00 | SmartPort/ProDOS signature |
$C705 | $03 | SmartPort/ProDOS signature |
$C707 | $00 | SmartPort device ($3C means boot-menu phase, not ready) |
$C7FF | $0A | ProDOS entry offset, so the ProDOS entry is $C70A |
$C70D | SmartPort 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 decode | Range | Answered by |
|---|---|---|
| IOSEL, slot ROM | $C700–$C7FF | SmartPort |
| IOSTROBE, expansion ROM | $C800–$CFEF, plus DATA/CTRL/DPOP at $CFF0–$CFF2 | SmartPort |
| DEVSEL | $C0F0–$C0FF | Text 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.
| Offset | Name | R/W | Function |
|---|---|---|---|
+$0 | INDEX | RW | Selects one of 256 indirect registers (buffer base, CONFIG, COLS, ROWS, origin, SCALE, cursor, fill values, canvas size, active layout, CAPS). |
+$1 | DATA | RW | Reads or writes the selected register. |
+$2 | DATA_INC | RW | Same, then increments INDEX. |
+$3 | CMD | W | $00 OFF, $01 ARM, $02 SHOW, $03 HIDE. |
+$4 | STATUS | R | Bit 7 BUSY, 6 STALE, 5 CONFIG_ERROR, 4 FRAME_PENDING, 1 ARMED, 0 VISIBLE. |
+$8–+$F | MAGIC, SIG, VER | R | Identification, 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–$C01Fstatus reads are served from the tracked switch state. $C019RDVBLBAR uses the native VBL boundary at line 192, cycle 0, with the AppleWin convention for bit 7.- The TransWarp speed register at
$C074is decoded;vtw.c074.ignoreturns 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.
| Address | Read | Write |
|---|---|---|
$C0D0 | Z80-to-6502 byte; clears its pending flag | Ignored |
$C0D1 | Read back the 6502-to-Z80 latch | Write a byte and set its pending flag |
$C0D2 | Bit 7: byte pending for the Z80 | Ignored |
$C0D3 | Bit 7: byte pending for the 6502 | Ignored |
$C0D4 | $FF | Ignored |
$C0D5 | Reset the Z80; returns $FF | Reset the Z80 |
$C0D6 | $FF | Ignored (the CTC socket is unpopulated) |
$C0D7 | Pulse Z80 NMI; returns $FF | Pulse Z80 NMI |
$C0D8–$C0DF | $FF | Ignored |
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–$0FFFF64 KB local RAM$10000–$1FFFF64 KB Apple shared-memory window$20000–$2FFFF64 KB expansion RAM$30000–$3FFFFunpopulated (reads$FF)$40000–$BFFFF512 KB expansion RAM;$40000–$5FFFFis the AD128K-compatible range$C0000–$FEFFFunpopulated 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 resource | Owner |
|---|---|
$C0n1/$C0n2 DIP readback, $C0n8–$C0nF 6551 ACIA | Super Serial Card |
$C0n4–$C0n7 W5100 window | Uthernet II |
$C100–$C1FF slot ROM, $C800–$CFFF expansion ROM | Super 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.
| Region | Offset | Size | Contents |
|---|---|---|---|
| Golden | 0x00000000 | 0x00200000 | BOOT.BIN: FSBL and the golden updater |
| Firmware | 0x00200000 | 0x00DF0000 | FIRMWARE.BIN: FSBL, PL bitstream, CPU1 renderer and frontend |
| Metadata | 0x00FF0000 | 0x00010000 | Last 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.
| Tool | What it does |
|---|---|
apple2_screen_text | The 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_peek | Hex dump of main RAM from the shadow (writes since power-on). |
apple2_soft_switches | Decoded //e soft-switch state and RamWorks bank. |
apple2_machine_status | Machine id, aux memory, RamWorks, physical aux-card probe, firmware status. |
apple2_health | Serve-path counters; all must be zero. |
apple2_bus_trace | Recent bus cycles from the trace ring. |
apple2_menu_key | Drive the Appletini config menu remotely. |
apple2_console | Any 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.