Open hardware, open firmware.
The FPGA logic, ARM firmware and schematic are on GitHub; Apple II demos and diagnostics have their own software repository. 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.
Apple-side demos, diagnostics and their builders now live in
github.com/hasseily/appletini-software.
This reference covers firmware source through
7b972bf
(14 October 2026, F1.3.0). Check a release’s notes for the
features included in its downloadable image. The second firmware,
Multitini, has its own repository.
| 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/ | Firmware ROM inputs, shared test media, renderer fixtures and the ca65 memory API example. Standalone apps moved to appletini-software. |
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, and the Super Serial Card
and SmartPort ROM sources come from the a2retronet project. Software
projects and bundled media in appletini-software carry
their own license and provenance notes, including IP65 for the network
demos.
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 that conflicts with an enabled virtual slot must be removed. Install Appletini in slot 7, or leave physical slot 7 empty when installing it elsewhere on a legacy host. IIgs use requires physical slot 7 and has a restricted slot policy.
| 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 | Off, Apple Mouse Card, 4Play (four players), or SNES MAX (two players), fed by USB input. One personality at a time. |
| 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, synthesized natively in the FPGA. 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.
Fresh settings leave TransWarp and the optional slot cards off, including the SSC, Ethernet, slot 2, Phasor, slot 5 and Disk II. SmartPort remains available. Turning Disk II off also changes a Disk II boot target to SmartPort, including in ONE//e.
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
examples/detect_appletini/detect_appletini.a65
in appletini-software:
; 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
demos/appletini_demos/textoverlay.a65
in appletini-software. Output resolution and picture size
scale the overlay with the Apple picture; its logical canvas dimensions
and register interface stay the same.
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. - Fixed presets: 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, andvtw.slowdown.cyclesfor the per-slot slowdown window (menu presets 32 through 65535 cycles, default 512; any decimal value up to 65535 is accepted). UART:vtw [status|on|off]orvtw speed full|turbo|1mhz|div <n>. Speed hotkeys showTW: OFFand change nothing without an active accelerator session. - Ctrl-Reset cannot tear a running session down; the enable is latched for the session.
- On a physical //e, a saved vTW session starts while the initial Apple RESET is held, before the boot prompt: CPU0 patches the ARM-loaded C8 boot helper so only the //e identification return waits for vTW. The prompt and menu run at 1 MHz to keep the ROM’s VBL timing, and the saved speed applies at storage handoff without another reset. II/II+, IIgs and unidentified hosts keep the post-handoff start.
- When Appletini supplies auxiliary RAM, the C8 helper waits for the
RAM policy and selects the internal slot-3 ROM before booting, so
ProDOS’s slot scan and BASIC.SYSTEM’s
PR#3find the //e 80-column firmware on the first boot.
TURBO execution
TURBO is a separate execution mode, enabled by the saved opt-in
vtw.turbo.enabled and selected with
vtw.speed.mode=3 or vtw speed turbo. It has
no fixed MHz rating. The fixed presets keep the original cycle-exact
path; TURBO caches shadow memory and skips safe internal CPU cycles.
I/O retains its bus timing, soft-switch ordering and slowdown rules.
Every accepted video or overlay write still reaches Appletini’s renderer. Writes mirrored to motherboard video may combine, and writes to inactive pages may wait until a display change, card access, bank change or memory hold needs them. Use a fixed preset when software depends on intermediate physical display writes. Cache coherence covers self-modifying code, bank changes and ARM memory holds. See README_TURBO.md for the execution, cache and video rules.
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.
Copy/fill API 1.0
The ARM memory service copies and fills explicit physical memory ranges while holding the virtual TransWarp CPU. Use F1.1.4 or later with its matching FPGA image: earlier versions could report false DMA timeouts. Active vTW is required at any speed, including TURBO; extended AUX also requires RamWorks. The native Apple CPU is unsupported.
- Identify the Appletini SmartPort controller before using its private selector.
- Call SmartPort STATUS (
$00), unit 0, selector$80, into a 32-byte buffer. Require success, signatureAMEM, major version 1, descriptor size 16, the feature bits you need, and availability bit 0. - Build a length-prefixed list of 1–16 descriptors. Call
CONTROL (
$04), unit 0, selector$80. A successful return confirms execution and CPU-hold release. - STATUS can then report the last result, completed descriptors, confirmed byte count and ARM elapsed microseconds.
Both calls use a nine-byte parameter block: count 3, unit 0, a
two-byte CPU buffer pointer, selector $80, then four zero
padding bytes. All multibyte fields are little endian. Success returns
A=$00, C=0; failure returns A=error, C=1.
Treat X and Y as clobbered.
The CONTROL list starts with a two-byte payload length
(8 + 16*N), followed by AMEM, version 1,
descriptor count, zero flags and a zero reserved byte. Each descriptor
is 16 bytes:
| Offset | Field |
|---|---|
| 0–1 | Operation: 1 COPY, 2 FILL; flags: bit 0 PRIVATE |
| 2–5 | Source space, bank, 16-bit address; all zero for FILL |
| 6–9 | Destination space, bank, 16-bit address |
| 10–11 | Nonzero byte count |
| 12–15 | Fill byte (zero for COPY), then three reserved zero bytes |
Space 0 is MAIN (bank 0 only); space 1 is AUX (banks 0–126).
Endpoints must stay within $0200–$BFFF and cannot
wrap. COPY rejects overlap within the same physical bank. The service
validates the full list before writing, then executes it in order.
Choose a CPU fallback only when the initial probe says the API is
unsupported or unavailable. A failed CONTROL may have changed memory;
do not retry with a CPU copy or treat the progress count as a safe
resume offset. Keep the caller, stack and return path outside all
destination ranges. Normal SmartPort entry also writes workspace byte
$07F8, which matters for whole-MAIN snapshots.
Since F1.2.2, a matching FPGA image runs each descriptor in its own copy engine, directly between shadow memory and RamWorks PSRAM; the ARM path remains the fallback when that engine is absent. The request format and results are unchanged.
The full API reference covers capability fields, errors, hold recovery and raw FIFO transport without the ROM workspace write. The firmware repository supplies ca65 constants, command builders and an example.
Character glyph API 1.0
From F1.3.0, Apple II programs can replace the character ROM
that Appletini renders with, through private SmartPort selector
$81. The service accepts a complete 4096-byte //e video
ROM in chunks, checks its CRC32 and swaps it in at a frame boundary.
It works from the motherboard CPU or vTW and needs no CPU hold or DMA.
Only Appletini’s rendered video changes: the motherboard’s
character ROM, composite output, soft switches and the saved
video.rom menu choice are untouched. The $80
copy/fill selector keeps its format.
- Identify the Appletini SmartPort controller. Never send private selectors to another card’s SmartPort.
- Call STATUS (
$00), unit 0, selector$81, into a 32-byte buffer. CheckAGLY, major version 1, header size 16, ROM size 4096, a maximum chunk of at least your chunk size, and the AVAILABLE flag. Older firmware returns$21(BADCTL). - Wait until PENDING clears, and note the applied generation.
- Call CONTROL (
$04), unit 0, selector$81: BEGIN with a nonzero transaction ID, WRITE all 4096 bytes (eight 512-byte chunks, in any order), then COMMIT with the CRC32 of the whole ROM. Partial uploads never change the active ROM. - Poll STATUS until PENDING clears, the generation changes and CUSTOM_ACTIVE is set.
- On exit, ABORT any unfinished upload, then RESTORE with ID zero to return to the menu-selected ROM.
The parameter list matches the copy/fill API: count 3, unit 0, a
two-byte buffer pointer, selector $81 and four zero bytes.
Each CONTROL buffer starts with a two-byte payload length, then a
16-byte AGLY header (version, operation, transaction ID,
WRITE offset and count) and the WRITE data or COMMIT CRC. Operations
are BEGIN 1, WRITE 2, COMMIT 3, ABORT 4 and
RESTORE 5; errors run from $60 UNAVAILABLE to
$67 CHECKSUM, and BUSY ($66) means an earlier
change still awaits the renderer.
$C00E/$C00F still choose primary or
alternate (MouseText) characters from the uploaded ROM.Apple reset discards an unfinished upload but keeps the active ROM; a
menu ROM change ends any upload and wins. The full
README_GLYPH_API.md
has every field, flag and error, and
software/glyph_api/glyph_api.inc
supplies the constants. A complete client is
demos/glyph_lab
in appletini-software.
USB paddles
On a physical Apple, USB paddle input requires active vTW. ONE//e uses its private input path with the same saved mappings. Up to eight shared USB input slots supply four independently mapped paddles; the lowest connected joystick supplies PB0–PB2. Device numbers can change with connection order. The user guide describes supported controllers and mapping controls.
The vTW bridge serves paddles at $C064–$C067
(mirrored at $C06C–$C06F) and buttons at
$C061–$C063 (mirrored at $C069–$C06B).
Each $C070–$C07F access snapshots the axes and
restarts all four timers; an axis value lasts
4 + 11 * value native Apple cycles. Only $C070
completes privately; the other aliases preserve their physical
RamWorks and speed-register effects.
Actual USB paddle measurement runs at native speed even in TURBO or with Slow Paddles/joystick disabled. Timers continue while the CPU is paused. On a //e, USB buttons combine with the physical Apple keys; disconnecting all controllers restores the physical connector.
Settings use usb.joystick.paddle.0 through
usb.joystick.paddle.3, including each paddle’s
.device. The ARM bridge stages PDL0–3 at
$400002AC and commits axes, presence and buttons at
$400002B0; check its $4A signature in bits
31:24 and physical-vTW enable bit 8 before publishing. Full register,
mapping and USB diagnostics:
README_USB_JOYSTICK.md.
UART usb1 status reports connected inputs and driver state.
4Play & SNES MAX
Slot 2 selects Off, Mouse, 4Play or SNES MAX. The gamepad cards work through the physical Apple bus and ONE//e without TransWarp. They require the updated frontend and FPGA. Neither provides slot ROM, expansion ROM, IRQ or no-slot-clock access. On a physical IIgs, slot 2 remains disabled.
4Play
Read $C0A0–$C0A3 for players 1–4. The ports
repeat across $C0A0–$C0AF; writes do nothing.
Controls are active high:
| Bit | Meaning |
|---|---|
| 0, 1, 2, 3 | Up, Down, Left, Right |
| 4 | Trigger 3 |
| 5 | Always high |
| 6, 7 | Trigger 2, Trigger 1 |
An idle or disconnected player reads $20.
SNES MAX
Write $C0A0 to latch both players, then read D7/D6 for
players 1/2. Write $C0A1 to advance both streams; reads
do not advance them. Controls are active low in this order:
B, Y, Select, Start, Up, Down, Left, Right, A, X, L, R, followed by
four high bits. Position 16 reads low for a connected pad and high
for an empty port; further clocks stay at that presence value.
A latch snapshots buttons and presence together. All even write offsets in the slot’s I/O block latch, all odd offsets clock, and all sixteen offsets can be read. Games fixed to slot 4 need a slot-2 setting, autodetection or a patch.
Settings & ARM interface
slot2.card saves OFF, MOUSE,
FOUR_PLAY or SNES_MAX.
slot2.player1.device through
slot2.player4.device save AUTO,
1–8 or OFF. Auto retains
each player assignment while connected. Old
mouse.slot2.enabled settings still load; an explicit
slot2.card wins. New settings default to Off and Auto.
Stage player pairs at $400002B8 and
$400002BC; a byte-0 write to CONTROL at
$400002B4 commits the staged buttons, card choice and
presence together. Check CONTROL’s $5332 signature
in bits 31:16 first. An older FPGA supports Mouse only. Full protocol,
button mapping, register fields and checks:
README_SLOT2_GAMEPADS.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 now in
appletini-software/diagnostics/applicard/.
The embedded software/applicard/APPLICARD.ROM remains in
the firmware repository. 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.
In appletini-software,
diagnostics/ad8088/tools/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 (off in
fresh settings). 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.
In appletini-software,
demos/appletini_demos/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 appletini-software
repository is the Apple-side toolbox. Its
demos/appletini_demos/ project contains the 32 MB
showcase disk, launcher, raster and speed demos, SHR/SHR4 viewers,
SuperSprite and text-overlay examples, network apps and the MS-DOS
demo. AUX-memory, AD8088 and Appli-Card diagnostics are under
diagnostics/; the detection example is under
examples/detect_appletini/.
Build and check the demo disk from that repository’s root:
python demos/appletini_demos/tools/build_appletini_demo_disk.py
python demos/appletini_demos/tools/test_demo_disk.py
python demos/appletini_demos/tools/test_demo_disk.py --disk demos/appletini_demos/build/Appletini_Demos.po
The builder needs Python 3.10+, Java, AppleCommander, ACME and cc65.
Generated output goes under demos/appletini_demos/build/.
See the project README
for tool paths and network-app tests. The demo disk remains a free
download.
libtini C SDK
libtini is a C game SDK for an enhanced Apple //e with the Appletini, for cc65. It draws Super Hi-Res screens, keeps large data in RamWorks banks, plays Mockingboard or Phasor music and SSI-263 speech, reads the keyboard and joystick, and loads and saves ProDOS files. It uses only interfaces that the firmware has. libtini has the MIT License.
The libtini reference has first steps, guides with
sample code and the full API. The example game, STARFALL, also
plays in your browser. The source is in
libtini/
in appletini-software.
Display output
The frontend supports six HDMI outputs: 1024×768,
1200×800, 1280×1024, 1360×768, 1680×1050 and
1920×1080. Settings and profiles save
video.resolution as that size string and
video.size_multiplier as max, 1
or 2. Missing or invalid values use 1920×1080 and
Max. The user guide lists picture sizes and
display requirements.
Resolution switching requires matching PL support; an older FPGA stays at 1080p. The PL drains framebuffer reads, changes the pixel clock and commits dimensions with the first frame address. A failed clock change tries to restore 1080p; composition remains paused if scanout stays held or unlocked. The three RGB565 output buffers retain their 4 MiB spacing with a packed stride of width × 2 bytes.
Size changes affect composition without changing the output clock. The text-overlay canvas and Apple-only screenshot format stay fixed; full-output captures use the current resolution. Bezels draw at native size and must match the output width without exceeding its height. No EDID mode selection is implemented. Full timing tables and tests: README_DISPLAY_RESOLUTIONS.md.
Legacy monochrome shaping saves video.dot.bleed as
OFF, LIGHT, MEDIUM or
STRONG, with Light as the default. See
README_VIDEO_MONO.md
for filter and rendering details.
The pixel mask is applied by the FPGA during scanout and saved as
video.pixel_mask: OFF (default),
APERTURE, SHADOW or LCD. Masked
channels drop to 75%. Aperture keeps one of R, G or B per output
column in a three-column cycle, Shadow shifts that cycle one column on
alternate rows, and LCD dims every third row and column.
Screenshots go to
0:/screenshots/<timestamp>-<a2|1080p|WxH>-<serial>.png.
a2 captures keep raw renderer colors at the fixed Apple
size (1120×768 legacy, 1280×800 SHR) with scanlines;
full-output captures copy the completed output slot’s pixels,
effects and mask before yielding, then write PNG rows in batches.
IIgs host policy
IIgs support remains experimental. Use physical slot 7 configured as Your Card. The boot ROM reports host identity before optional bus ownership is allowed. A confirmed IIgs host permits the boot menu and SmartPort in external slot 7, with SuperSprite using polling; passive capture and video output remain available.
All Appletini-emulated cards in logical slots 1–6 stay blocked
for the entire physical IIgs boot. This is a slot-ownership restriction,
not a per-card compatibility list: the PCB has live DEVSEL only for
physical slot 7, and a cached $C02D report cannot establish
ownership of other slots after GS/OS changes their mapping. Both
boot_menu_service_slot_allowed()
and the
FPGA machine policy
enforce this limit. It does not disable separate cards installed in
the IIgs's other physical slots.
Memory replacement, RamWorks, vTW, AD8088,
PS host-memory operations and fake-SHR $C029 writes stay
blocked. Physical IRQ, INH, DMA and address/RW drive stay disabled.
Slot-7 I/O uses live DEVSEL and active-low M2SEL qualification. A
separate LINTXT path can serve overlay I/O when another slot boots,
without granting SmartPort or bus ownership.
Pre-identification ROM responses still have a documented potential for data contention; these controls do not establish general IIgs hardware compatibility. See README_IIGS_SAFETY.md for host identification, electrical limits and hardware checks. ONE//e uses a separate private-bus policy; its physical slot drivers remain isolated throughout standalone operation.
Boot, flash & multiboot
The card boots from a 16 MB QSPI flash. Golden Boot B2 and firmware F1.3.0 use the multiboot layout, with one slot each for Appletini and Multitini:
| Region | Offset | Size | Contents |
|---|---|---|---|
| Primary Golden | 0x000000 | 1 MiB | BOOT.BIN: FSBL and Golden updater |
| Trial Golden | 0x100000 | 1 MiB | Trial and backup BOOT.BIN |
| Appletini | 0x200000 | 6 MiB | Appletini FIRMWARE.BIN: FSBL, PL bitstream, CPU1 renderer and frontend |
| Multitini | 0x800000 | 0x7E0000 | FIRMWARE-MULTITINI.BIN: supervisor bitstream and ARM firmware |
| Metadata A | 0xFE0000 | 64 KiB | CRC-protected multiboot record |
| Metadata B | 0xFF0000 | 64 KiB | Second copy of the record |
Firmware files carry a version 2 manifest with their product
identity, exact length and CRC, and each executable links its own
product marker, so renaming a file cannot move an image into the other
slot. Golden B2 scans the SD root for FIRMWARE.BIN,
FIRMWARE-APPLETINI.BIN and
FIRMWARE-MULTITINI.BIN, in that order, and installs each
valid file into the slot its manifest names. An update first revokes
only its target slot, programs and reads back every byte, then commits
the verified image and a new metadata record, and renames the file to
.OK. A failed file stays .BIN and the scan
continues; if several files target one product, the last success
wins. The other slot is never touched.
Before every boot, Golden checks the full image in flash. It tries a
one-shot request first, then the saved startup choice
(default_product in the metadata record, outside the SD
settings), then the other verified firmware. A missing, damaged,
wrong-product or stale image never boots. A verified legacy
single-slot image can still boot as a fallback; otherwise Golden stays
in its serial monitor. Its one-second window at power-on listens on
both UARTs at 921600 baud. ? opens it;
boot appletini and boot multitini (B2.0.6)
start one verified firmware without changing the saved choice, and
a host can send an image over XMODEM-CRC:
python scripts/serial_firmware_update.py FIRMWARE.BIN --port COM3 --reboot-golden
A firmware switch (Switch to Multitini or Multitini’s Switch to Appletini) flushes writable media, drains video, closes SD owners, stops USB, holds CPU1 in reset and restarts through Golden with a one-shot request. Appletini also requires a safe Apple-bus hold: it holds RESET on a //e or II+ and pauses ONE//e, and refuses while SD sharing, a screenshot or a print job is active. A failed preparation cancels the switch.
BOOT.BIN never installs from the SD scan. Golden
updates start from the running firmware: :selfupdate on
the serial console (F1.0.1 and later), or from F1.3.0 the About
tab’s Update Golden Boot.... The menu action is offered only
while 0:/BOOT.BIN exists and SD sharing is off, and is
enabled in ONE//e or in the initial boot menu of a II+/IIe, before
storage handoff. Both paths check the image and the recovery firmware,
write and verify the trial Golden, boot it, then repair and verify the
primary. Golden B2.0.4 and later rename BOOT.BIN to
BOOT.OK once it matches the verified primary. Both menus
read the installed Golden version from its executable in flash.
Follow the field procedure.
Earlier B1 Golden Boots copy FIRMWARE.BIN verbatim to
0x200000 and ignore the named files, so a tagged
Appletini image installs on them with its multiboot controls disabled.
Never give a B1 Golden a Multitini image as FIRMWARE.BIN.
Golden B2.0.6 can also restore an identified, recovery-capable
Appletini F1.1.0 or later image.
Direct QSPI programming over JTAG (scripts/program_boot.bat,
scripts/program_firmware_slot.bat) covers bring-up and
last-resort recovery, without the SD updater’s power-loss
protection. Full notes:
README_MULTIBOOT.md;
README_BOOT_UPDATE.md
describes the original single-slot layout and the serial
procedure.
Multitini
Multitini is the second firmware: a stand-alone FPGA computer that
loads machine cores (Commodore 64, Game Boy / Game Boy Color,
Genesis / Mega Drive, NES, Vectrex, Apple IIgs) from /multitini/cores/
on the SD card, behind a shared supervisor that handles the menu, USB
input, audio, storage and core loading. It does not use the Apple bus:
its connector outputs stay in safe idle states, so the Apple keyboard
and slot cards are never read. The source is at
github.com/hasseily/multitini-one,
GPL-3.0-only for original work, with imported cores and files keeping
their own licenses. The Multitini guide
covers use.
The two repositories share the multiboot image validator, metadata,
updater and Golden self-update code;
scripts/check_multiboot_sync.py in
multitini-one checks that the shared files match.
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. Save work before running the Vitis script: it recreates
vitis_workspace and terminates existing Vitis IDE/server
and Java processes.
Normal hardware builds use current synthesis with no incremental
reference. Recreate the project after source-list or block-design
changes. For an incremental comparison, explicitly set
APPLETINI_INCREMENTAL_REF_DCP to a tested checkpoint.
The build applies temporary setup margins, runs one post-route
AggressiveExplore pass, then restores nominal constraints
before checking export. The current gate requires at least
+0.050 ns setup slack, nonnegative hold and pulse-width
slack, clean routing and bus skew, and valid bound constraints with no
failing or unconstrained internal endpoints. Never package a bitstream
from a failed gate. Build records go under .timing_runs/;
promoting a timing reference requires two passing clean full builds
of the same commit. See
README_VIVADO.md
and
scripts/SCRIPTS_README.md.
Multiboot release images rebuild the ARM code against a pinned,
audited FSBL and FPGA image. Run
vitis -s scripts/build_multiboot_arm.py with the exported
platform in APP_MULTIBOOT_PLATFORM and the audited
firmware directory in APP_MULTIBOOT_BASELINE. CPU1 is
kept from the baseline unless APP_MULTIBOOT_REBUILD_CORE1=1
rebuilds it from source. The package audit checks the product
manifest, CRC, partitions and linked memory limits; the Appletini image
must fit its 6 MiB slot.
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.
Keep appletini-software beside the firmware checkout,
or set APPLETINI_SOFTWARE_ROOT: border/overlay and AD8088
source checks read migrated examples from there. Demo-disk and
network-app tests run in the software repository. Focused firmware
checks for the newer interfaces include:
python scripts/test_memory_api.py
python scripts/test_memory_api_hw.py
python scripts/test_ps_dma_command.py
python scripts/test_vtw_usb_joystick.py
python scripts/test_usb_gamepad_input.py
python scripts/test_usb_ps4.py
python scripts/test_usb_xbox_one.py
python scripts/test_slot2_card.py
python scripts/test_slot2_gamepad_service.py
python scripts/test_slot2_config_menu.py
python scripts/test_display_scaling.py
python scripts/test_display_modes_rtl.py
python scripts/test_iigs_bus_safety.py
python scripts/test_video_pixel_mask.py
python scripts/test_phasor_ssi_ui.py
python scripts/test_vtw_copy_engine.py
F1.3.0 adds checks for the glyph API, multiboot and the native boot path:
python scripts/test_glyph_api.py
python scripts/test_glyph_transport.py
python scripts/test_glyph_reset.py
python scripts/test_video_rom_service.py
python scripts/test_multiboot_frontend.py
python scripts/test_golden_about_menu.py
python scripts/test_golden_boot_policy.py
python scripts/test_native_slot3_boot.py
python scripts/test_vtw_boot_start.py
test_native_slot3_boot.py runs the actual Enhanced //e
reset ROM, ProDOS 2.4.3 slot scan and BASIC.SYSTEM
PR#3 against the old and fixed C8 helpers.
test_glyph_transport.py drives the shipped SmartPort ROM
against the production parser and needs py65 and a C compiler.
Some checks compile native C harnesses; RTL checks require the simulator. Each subsystem README lists its full suite and dependencies. Build and simulation results do not replace a board test.
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, supply the card’s 5 V from the DC input
so it can run standalone or
Multitini out of the Apple. In an Apple the
slot’s +5 V powers the card. 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.
For development questions and to share your projects, join the Appletini FPGA Discord.