Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

The zz9k crypto board: ZZ9000 SDK v2 protocol subset

This page is the contract, not an implementation note: it specifies the register- and opcode-level behavior of Copperline’s bundled ZZ9000 SDK crypto board ([zz9k]), precisely enough that the board (crates/zz9k-plugin, hosted by src/wasmboard.rs and bundled by src/zz9k.rs) can be verified against the SDK’s own Amiga-side software. Where this document and the implementation disagree, this document wins.

Unlike the other board contracts in this directory, the protocol is not Copperline’s to define: the board implements a subset of the MNT ZZ9000 “SDK v2” service platform, whose authoritative definition is the zz9000-sdk repository (BlitterStudio/zz9000-sdk, GPL-3.0-or-later, the same license as Copperline). This page pins the exact revision the board was written against and records every choice the board makes where the ABI leaves the firmware latitude. The behavioral reference is the SDK’s own Amiga-side transport, host/src/zz9k_host.c -- the code every SDK tool and the accelerated AmiSSL build link.

Pinned SDK revision: commit 9a7ec6de5069117f08049165e498d1cf6a6f1cab (2026-08-17). File references below (abi.h, zz9k_host.c, ...) are paths in that repository.

What the board is for

The real ZZ9000’s ARM core offers a crypto offload service: the Amiga submits hash/AEAD/key-exchange/signature-verify operations through a ring mailbox in the board window and the ARM computes them at modern speed, which is what makes TLS-era cryptography usable from a 68k. Copperline’s board plays the ARM’s role with host-native code (pure-Rust RustCrypto compiled into the plugin), so the unmodified SDK stack -- the zz9k.library transport, the zz9k-info/zz9k-hash/zz9k-chacha/ zz9k-aead/zz9k-cryptobench/zz9k-irqtest tools, and the SDK’s accelerated AmiSSL integration -- detects and drives it exactly as it does real hardware.

Only the CORE, MEMORY, and CRYPTO services (plus the DIAG_READ counter snapshot) are implemented. Everything else the real board offers -- RTG graphics, surfaces, image/audio/video codecs, USB, Ethernet -- is absent: those opcodes complete with UNSUPPORTED, their QUERY_SERVICE ids report NOT_FOUND, and non-SDK registers read zero. Installing the real board’s P96 zz9000.card RTG driver against this board is therefore unsupported; the board guarantees only that the driver’s register probes read zeroes rather than crash the machine.

Zorro identity

Address map (board-relative)

OffsetContents
0x0000-0x0FFFRegisters (below); everything unlisted reads 0, writes ignored
0x1000-0x1FFFZorro III register aperture: aliases 0x0000-0x0FFF (the transport writes the doorbell through it on Z3)
0xA000-0xFFFFLegacy mapped-IO window = ARM 0x3FE40000-0x3FE46000
0xD000-0xD07FMailbox descriptor (inside the mapped-IO window, = ARM 0x3FE43000)
0xD080-0xD87FRequest ring, 32 x 64-byte entries
0xD880-0xE07FCompletion ring, 32 x 64-byte entries
0x10000-sizeShared-buffer heap = ARM 0x00200000 upward

ARM addresses appear on the wire (the mailbox pointer registers, shared buffer arm_addr replies); the transport maps them back to board offsets through exactly the two windows above (zz9k_arm_range_to_board_offset).

All wire data -- registers, descriptor fields, ring entries, inline payloads -- is big-endian. The guest accesses registers with 16-bit cycles, writes ring entries as 16-bit stores (plus a trailing byte for odd payload lengths), and reads completions byte-wise; the board serves any access size 1/2/4 at any offset, composing bytes big-endian.

Registers

OffsetNameBehavior
0x0004CONFIGRead: interrupt status, bit 0x0008 = SDK completion pending. Write 0x0088 (ACK_MODE|ACK_SDK): acknowledge
0x00E8CONFIG_KEYLatched key query: write a key id, read back the key’s value. Key 5 = int2 (nonzero: completion IRQ on INT2/PORTS instead of INT6/EXTER)
0x00EACONFIG_PRESENTRead: nonzero if the last key id written to CONFIG_KEY is known
0x0100SDK_MAGICReads 0x5A39
0x0102SDK_VERSIONReads 0x0203 (ABI 2.3; the transport checks only major = 2)
0x0104/0x0106SDK_MAILBOX_HI/LORead: ARM address of the mailbox descriptor, 0x3FE43000
0x0108SDK_DOORBELLWrite: request-ring kick. Accepted but redundant: the board scans the request ring from its own clock regardless, because the Zorro II transport never rings the doorbell at all (zz9k_z3_register_window_reg16 returns NULL off Z3)
0x010ASDK_STATUSReads 0
0x010CSDK_IRQ_CTRLWrite 0x0001 ack, 0x0002 enable, 0x0004 disable the completion interrupt
0x0110-0x011EDIAG / APERTURE_INFORead 0 (aperture state LEGACY), writes ignored

Mailbox

The 128-byte descriptor at 0xD000 follows ZZ9KMailboxDescriptor (abi.h): magic 0x5A5A394B (“ZZ9K”), ABI 2.3, ring offsets relative to the descriptor base (0x080 and 0x880), 32 entries each, guest-owned request_tail/completion_head and board-owned request_head/completion_tail indices, and the capability bits.

Wire entries are 64-byte ZZ9KMailboxWireEntrys: request_id u32, opcode u16, status u16, flags u16, payload_len u16, user_cookie u32, 48-byte inline payload. The board echoes request_id, opcode, and user_cookie into the completion (the transport matches on all three) and sets status to the result. Error completions carry an empty payload.

Capability bits

The descriptor and QUERY_CAPS advertise 0x7D07: MAILBOX (1<<0), IRQ_COMPLETION (1<<1), SHARED_ALLOC (1<<2), CRYPTO (1<<8), MEMORY_OPS (1<<10), DIAGNOSTICS (1<<11), DOORBELL (1<<12), POLLING_COMPLETION (1<<13), SERVICE_DISCOVERY (1<<14). Deliberately clear: HOST_WINDOW_HEAP (1<<20) and APERTURE_LAYOUT (1<<24) -- their absence selects the transport’s simple “historical fixed 4 MB” Zorro II path and skips the aperture-layout acknowledgement handshake entirely.

Request pickup and completion timing

The board consumes at most one request per emulated tick (one CPU instruction boundary), computes it immediately -- inputs are read at dispatch, so mutating a buffer after submission does not affect an in-flight op, matching real firmware -- and publishes the completion after a deterministic latency in colour clocks (CCK = 3,546,895 per emulated second), modelling a serial coprocessor:

OperationLatency
CORE / MEMORY / DIAG ops50 us
HASH / STREAM / AEAD200 us + length / 50 MB/s
KX X255191 ms
KX P-256 (derive or keygen)2 ms
VERIFY ECDSA-P2562 ms
VERIFY RSA1 ms

Completions publish in submission order; a full completion ring holds the queue until the guest consumes, and once a ring’s worth of completions is waiting the board stops consuming requests -- the request ring then fills and the guest transport reports BUSY at submit, like stalled hardware -- so an unconsumed completion ring can never grow board state without bound. When the completion interrupt is enabled (0x010C = 2), publishing a completion raises the selected line (INT6 by default, INT2 when the int2 config key says so) level-sensitively until either acknowledge form (0x010C = 1 or CONFIG = 0x0088) clears it.

Services

CORE (0x0000)

The CRYPTO service flags are 0x003F0001: FIRMWARE plus X25519 (1<<16), P256 (1<<17), ECDSA_P256 (1<<18), RSA_2048 (1<<19), AES_GCM (1<<20), P256_KEYGEN (1<<21) -- the exact gates the provider and zz9k-cryptobench check before offloading each primitive.

MEMORY (0x0100)

ALLOC_SHARED allocates from the heap at 0x10000..size (16-byte granules, minimum alignment 16, requested alignment honoured), replying {handle, arm_addr, rounded length, flags echo}. Handles are generation-tagged, never 0 (the SDK tools use handle != 0 as their cleanup guard) and never 0xFFFFFFFF (ZZ9K_INVALID_HANDLE); a freed handle goes stale (BAD_HANDLE forever after). The HOST_WINDOW and CARD_ONLY flags are accepted as no-op placement hints: the whole heap is Amiga-visible. Exhaustion (space or the 64-slot table) is NO_MEMORY. FREE_SHARED coalesces. MEM_FILL/MEM_COPY operate on handle+offset+length triples (copies may overlap), subject to the per-operation length cap below.

CRYPTO (0x0800)

Descriptors reference (handle, offset, length) triples into shared buffers; every triple is bounds-checked (BAD_HANDLE on any violation). Every variable-length input -- source, key, AAD, and the MEM_FILL/MEM_COPY lengths -- is additionally capped at 256 KiB (BAD_REQUEST beyond): each operation computes synchronously inside one fuel-metered host call, and the cap keeps the costliest allowed request at least 2x below the measured budget ceiling while leaving 16x headroom over the largest real consumer (a 16 KiB TLS record). Undefined descriptor flag bits are rejected with UNSUPPORTED rather than ignored, on every op (the SDK builders never set any). Results are the 48-byte big-endian {bytes_written, algorithm, flags} payload; the SDK reply decoder treats bytes_written == 0 on an OK status as an internal error, so successful results are always nonzero.

DIAG (0x0900)

DIAG_READ only: the 48-byte counter payload (requests completed/failed, last status, pending count, buffers used, heap total/free/largest, mailbox ARM address, ring entries, 0, 0). The other DIAG ops are UNSUPPORTED and QUERY_SERVICE(DIAG) is NOT_FOUND; zz9k-info prints exactly the counters and skips the rest.

Determinism, randomness, and save states

The board is pure compute: its only host imports are log and config_get, so fitting it keeps a machine fully deterministic and replay-safe (the same guarantee as any DMA-and-interrupts-only WASM board -- see Writing Zorro board plugins). No operation draws randomness; the KEYGEN primitive is a deterministic scalar multiplication and TLS ephemeral scalars are generated guest-side by the SDK/AmiSSL themselves. A ChaCha20 DRBG seeded from the [zz9k] seed config key exists reserved and dormant for any future entropy-consuming op; its default seed is a fixed constant, so runs stay byte-reproducible either way.

All board state -- the window (registers, mailbox, heap contents), the allocator, pending completions and their remaining latencies -- lives in the plugin’s linear memory, so save states snapshot and resume the board exactly, including mid-operation (a completion whose latency had not elapsed at save time arrives on schedule after load).

Verification