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.

Writing Zorro board plugins

Copperline’s expansion bus models Zorro II/III autoconfig (src/zorro.rs). Boards are data-driven: a board is described by a BoardSpec, and additional boards are added from TOML metadata files without writing any Rust. The built-in [memory] fast and z3 options are themselves just boards built from the same specs, and the [scsi] option can add the A2091 (Zorro II, src/a2091.rs) or A4091 (Zorro III, src/a4091.rs) SCSI controller as a device-backed board (see the device-board notes below and the [scsi] section of Configuration reference; the third [scsi] choice, the A3000’s motherboard SDMAC, is silicon at $DD0000 rather than a Zorro board). Both Zorro controllers have freely redistributable bundled autoboot ROMs: the A2091 image is built from a2091-rom/, the A4091 image is the open A4091 project’s a4091_cdfs.rom release, and they install under share/copperline/a2091/ and share/copperline/a4091/.

There are two board kinds:

Every functional board -- the in-tree boards listed below, the CDTV DMAC, and WASM plugins -- implements the ZorroDevice trait (src/zorro_device.rs): the bus drives every board through that one boundary for register access, ticking, interrupts, and DMA.

Built-in boards

These are the boards Copperline can fit without a metadata file. The table lists them in autoconfig chain order, which decides the base address each one gets:

BoardFitted byBusManufacturer / productWindow
CD32 Full Motion Video modulefmv = true or fmv_rom (CD32 only)II514 / $6A1 MiB
Fast RAM[memory] fastII5192 / 364K-8M
Zorro III RAM[memory] z3III5192 / 464K-1G
RAM boards from [[zorro]] metadata files[[zorro]]II/IIIfrom the filefrom the file
Copperline identification boardidentify (on by default)II5192 / 264K
A2091 SCSI[scsi] controller = "a2091"II514 / 364K
A4091 SCSI[scsi] controller = "a4091"III514 / 8416M
lide RIPPLE / RIDE IDE[lide] board = "ripple" / "ride"II5194 ($144A) / 7, 9128K
lide AT-Bus 2008 IDE[lide] board = "atbus2008"II2092 ($082C) / 664K
copperhf virtual hardfile controller[copperhf]II5192 / 864K
SF2000 SD card controller[sf2000sd]II5194 ($144A) / 1164K
WASM plugin boards from [[zorro]][[zorro]]II/IIIfrom the filefrom the file
HostSocket[hostsocket]II5192 / 664K
zz9k crypto board[zz9k]III or II28014 ($6D6E) / 4 (III), 3 (II)1M-256M (III), 4M (II)
Copperline services board[[filesys]], [clipboard] share, [machine] rom_scsi_device_disableII5192 / 564K
A2065 Ethernet[a2065]II514 / $7064K
Toccata sound board[toccata]II18260 ($4754) / 1264K
MHI MPEG audio decoder[mhi]II5192 / 764K
Z3660 RTG[rtg] card = "z3660"III5195 ($144B) / 1128M
Picasso II / II+[rtg] card = "picasso2" / "picasso2plus"II2167 / 11 + 121-2M + 64K
Graffity [Zorro II][rtg] card = "graffityz2"II2092 / 34 + 331-2M + 128K
Graffity [Zorro III][rtg] card = "graffityz3"III2092 / 3316M

The FMV module is put at the front of the chain because its ROM expects the first Zorro II placement, $200000. On a CDTV the DMAC (Commodore 514, product 3) is not on this chain at all: it autoconfigs ahead of it, as on the real machine (see below). The sections that follow describe the RAM, plugin, network, sound, crypto, and RTG boards; the storage controllers are covered in Peripherals and expansion, copperhf in copperhf.device: register protocol and boot ROM, and the manufacturer 5192 products in The Copperline manufacturer ID.

Describing a board in TOML

Reference a board metadata file from the main configuration:

# copperline.toml
[[zorro]]
metadata = "boards/megaram.toml"

The metadata path is resolved relative to the working directory. Multiple [[zorro]] entries are allowed and keep their file order on the autoconfig chain: RAM boards follow the built-in fast/Z3 RAM boards, and plugin boards follow the in-tree storage controllers (see Built-in boards).

The metadata file:

# boards/megaram.toml
name = "MegaRAM"        # human-readable, appears in logs
zorro = 3               # 2 or 3
type = "ram"            # "ram" or "wasm"
size = "64M"
manufacturer = 0x07DB   # 16-bit autoconfig manufacturer ID
product = 0x20          # 8-bit product code, unique per manufacturer
serial = 0              # optional, defaults to 0
memlist = true          # optional; defaults true for "ram", false for "wasm"

Field notes:

The file is validated on load (load_board_metadata and BoardSpec::validate, src/zorro.rs): unknown keys, bad sizes, unknown zorro versions, out-of-range IDs, and unknown backing types are reported with the metadata file’s path.

WASM plugin boards

A type = "wasm" board is a functional board whose behaviour comes from an external WebAssembly module (src/wasmboard.rs), so you can ship a working board -- registers, interrupts, DMA -- as a .wasm file plus a TOML manifest, with no changes to Copperline.

# boards/example.toml
name = "Example Board"
zorro = 2
type = "wasm"
size = "64K"            # the board's autoconfig window size
manufacturer = 0x1448
product = 0x10
wasm = "example.wasm"   # module path, relative to this metadata file
dma  = true             # capabilities, all default false:
int2 = true             #   dma  -> the dma_read/dma_write host imports
int6 = false            #   int2 -> may assert INT2 (PORTS)
                        #   int6 -> may assert INT6 (EXTER)
# resource_write = true  # write back into its own file-typed options
                        #   (the resource_write import; see below)
# diag_vec = 0x40        # DiagArea offset in the window, for a plugin that
                        # serves its own autoboot ROM (see below)
# A NIC plugin may also request the shared host networking capability:
# net = "bridge"         # none / loopback / nat / bridge
# net_interface = "en0"  # required for bridge
# resolve = true         # host-OS-resolver DNS lookups (resolve_start/resolve_poll)
# host_sockets = true    # direct host-socket passthrough (the sock_* imports)

WASM is chosen because a module’s entire mutable state lives in its linear memory -- a flat byte array that Copperline’s save states snapshot and restore exactly like Amiga RAM, preserving deterministic replay. The engine runs with NaN canonicalization and without SIMD or threads for determinism; a plugin’s persistent state must live in linear memory (WebAssembly globals are not captured). Every call into the module is metered: its fuel is refilled to 50 million units before each export call, so a runaway loop traps (and takes the board offline, below) instead of hanging the emulator. A save state stores the module’s path and replays its memory image on load, so the .wasm file, and any file a file-typed option names, must remain where the manifest points.

Module ABI

The host calls these exports (all optional except memory):

ExportSignaturePurpose
memory(linear memory)required; the board’s state lives here
init() -> ()called once after instantiation
read(off i32, size i32) -> i32register read at a window offset
write(off i32, size i32, value i32)register write
tick(cck i32)advance by cck colour clocks
int2() -> i32INT2 (PORTS) line state, non-zero = asserted
int6() -> i32INT6 (EXTER) line state

The plugin may import these host functions from module env (gated by the manifest capabilities; importing one that was not granted fails to load):

ImportSignatureCapability
log(ptr i32, len i32)always available
config_get(key_ptr i32, key_len i32, out_ptr i32, out_cap i32) -> i32always: copy a setting’s value to out_ptr (truncated to out_cap); returns its full length, or -1 if unset
resource_len(name_ptr i32, name_len i32) -> i32always: byte length of a file resource, or -1 if absent
resource_read(name_ptr i32, name_len i32, off i32, out_ptr i32, len i32) -> i32always: copy up to len bytes from offset off; returns the count, or -1 if absent
resource_write(name_ptr i32, name_len i32, off i32, in_ptr i32, len i32) -> i32resource_write: copy len bytes of plugin memory into the resource at offset off, write-through to its host file; returns the count, or a negative error code
dma_read(addr i32, ptr i32, len i32)dma: Amiga addr -> plugin memory ptr
dma_write(addr i32, ptr i32, len i32)dma: plugin memory ptr -> Amiga addr
net_send(ptr i32, len i32)net: transmit the Ethernet frame at plugin memory ptr
net_recv(ptr i32, cap i32) -> i32net: copy the next inbound frame into ptr (truncated to cap), or 0
resolve_start(name_ptr i32, name_len i32) -> i32resolve: start a host-OS-resolver lookup, returns a request id or -1
resolve_poll(id i32, out_ptr i32) -> i32resolve: poll it -- -2 pending, -1 failed, or 0 with the address at out_ptr
sock_open, sock_connect, sock_send, sock_recv, sock_poll, sock_close, ... (21 in all)see belowhost_sockets: non-blocking passthrough to real host OS sockets

DMA transfers (dma_read/dma_write) are permitted only inside an active host transaction (read, write, tick). A DMA call from init traps and makes instantiation fail; one from an interrupt query (int2, int6) traps and takes the board offline. Within a transaction, dma_write buffers transfers in a host-side journal, bounded to 4,096 transfers and 16 MiB per callback so a plugin cannot exhaust host memory, and the journal is committed to Amiga memory only when the callback returns successfully. If the plugin traps (out of fuel, a panic, or any other trap), the uncommitted writes are discarded and Amiga memory is untouched. dma_read overlays the pending writes, so a plugin reads its own writes, including across the 32-bit address wrap (0xFFFF_FFFF -> 0x0000_0000).

Fault isolation and lifecycle

When a plugin traps in read, write, tick, int2, or int6 (fuel exhaustion, unreachable code, an out-of-bounds access, or a DMA call outside a transaction):

Interrupt lines are level-sensitive and polled, exactly like the in-tree boards: a plugin holds int2/int6 non-zero while the line is asserted, and the bus applies the interrupt-delivery pipeline -- the plugin never pulses INTREQ.

resolve_start/resolve_poll ask Copperline’s own process to resolve a hostname through the host OS resolver (getaddrinfo) on a short-lived background thread, so the plugin need not speak DNS itself over its net traffic. It is the only way a plugin gets the host’s configured name resolution under a backend with no virtual DNS forwarder of its own, such as a direct LAN bridge. resolve_start reads the name from the plugin’s linear memory and returns a request id; resolve_poll polls that id without blocking, and on success (0) writes the IPv4 address (4 bytes, big-endian) into the plugin’s memory at out_ptr. Like net, using it makes a board non-deterministic. The [hostsocket] section of Configuration reference is the concrete example: its resolver key defaults to this capability under net = "nat", "bridge", or "host".

The sock_* family (host_sockets) goes further and hands the plugin real, non-blocking host sockets: BSD-style open/bind/listen/accept/connect, send/receive (including sendto/recvfrom, peek, and out-of-band data), a small set of socket options, dup, and shutdown, with host errno values normalized to the guest’s BSD numbering. A plugin holding it can reach anything the host process can, on the host’s own network identity, so it is never implied by net or resolve. Socket handles are host resources: after a save-state restore every handle the plugin remembers is stale and fails with -EBADF. The module documentation in src/wasmboard.rs details each call’s arguments and return values.

Plugins can be written in any language that targets wasm32 (Rust, C, Zig, ...). The ignored test emit_example_plugin_wasm (src/wasmboard.rs) writes an inert example module to the path in COPPERLINE_EMIT_WASM; pair it with a manifest like the one above.

Plugin settings, files, and the config panel

A plugin can take settings and files. The manifest declares defaults in a [config] table and a schema in [[option]] entries:

[config]                 # defaults
mode = "bridged"
mtu = 1500
[[option]]               # schema (drives the launcher's config panel)
key = "mode"
label = "Mode"
type = "enum"            # string | bool | int | file | enum
choices = ["bridged", "nat"]
[[option]]
key = "rom"
label = "Boot ROM"
type = "file"            # the host loads the file and exposes it as a resource

An [[option]] may also carry its own default; a [config] entry for the same key overrides it. label defaults to the key, and an enum option must list its choices. A file-typed default is resolved relative to the metadata file.

At runtime the module reads a setting via the config_get host import, and a file-typed option’s bytes via resource_len / resource_read (keyed by the option’s key). For an autoboot ROM, the plugin copies the rom resource into its linear memory at init and serves those bytes from read(), with diag_vec set in the manifest -- just like the in-tree A2091.

File resources are read-only unless the manifest opts in with resource_write = true, which grants the resource_write import. That is for a board that owns persistent media -- a virtual hard disk image, say -- and needs the guest’s changes to survive the session. A write is addressed by the option’s key, never by a path the plugin supplies, so the writable set is exactly the files the manifest already named; it is write-through to that file at the given offset, and it never extends a resource past the length resource_len reports. It returns the byte count written, or a negative code: -1 no such resource (or one with no host file behind it, such as the bundled HostSocket ROM), -2 offset/length outside the resource, -3 a source pointer outside the plugin’s own linear memory, -4 a host I/O error. None of those faults the board. Like the module itself, file resources are reopened by path on instantiation, on a bus reset, and on a save-state load rather than captured in a snapshot, so they already sit outside the determinism guarantee; writing them makes that visible rather than changing it.

The user overrides settings per board in the main config, layered over the manifest defaults:

[[zorro]]
metadata = "boards/nic.toml"
config = { mode = "nat", rom = "boot.rom" }

The machine-configuration launcher renders the [[option]] schema as an editable field per option (enum/int steppers, a bool toggle, a file picker, and a text box for strings), writing changes back as these per-board overrides.

Networking: the A2065 Ethernet board

Copperline includes an in-tree Commodore A2065 Ethernet board (src/a2065.rs), an Am7990 LANCE NIC the AmigaOS SANA-II a2065.device drives. Fit it from the config:

[a2065]
net = "nat"   # "bridge", "loopback", or "none" for isolation
# interface = "en0"  # required for "bridge"

(--a2065-net BACKEND is the matching per-run flag, and the launcher’s I/O Ports tab’s Networking page has the same picker under its Ethernet: heading. Bridged mode adds a live host-adapter picker. --list-net-interfaces prints the stable names accepted by [a2065] interface and --a2065-interface.)

Unlike the DMAC boards, the LANCE does not master the Amiga bus: its init block, descriptor rings, and packet buffers live in the board’s own 32 KiB RAM (which the CPU reaches through the board window), so the board is self-contained and owns its host network backend directly.

Host network backends live in src/net/ behind the NetBackend trait. Three are built in:

Bridge open failures are fatal to startup and save-state restoration; Copperline never silently substitutes NAT or isolation. If an adapter disappears while running, the worker logs link loss and disconnects the guest.

Networking is non-deterministic. Inbound frames arrive on the host’s schedule, not the emulated clock, so a fitted A2065 (or any net-capable WASM plugin) breaks Copperline’s byte-identical replay and save-state reproducibility while traffic flows -- the emulator logs this when the board is attached. Save states record only the chosen backend and bring up a fresh one on load (in-flight frames are dropped; the guest’s TCP retransmits).

Audio: the Toccata sound board

[toccata] fits an in-tree MacroSystem Toccata (src/toccata.rs), a Zorro II AD1848-based sound board with a mature, open-source AHI driver (toccata.audio), so AHI-aware guest software gets 16-bit sound with no Copperline-specific driver work:

[toccata]
enabled = true

No other options exist yet. Toccata’s guest interface is purely register-and-FIFO, not bus-mastering DMA, and unlike the network boards it does not talk to a host device directly: its output joins Copperline’s own mixer as a named source (the toccata stem in --audio-stems). See Toccata and the AD1848 model for the register model and Audio sink service (src/audio/) for the mixer and stem-capture integration.

Audio: the MHI decoder board

[mhi] fits an in-tree virtual MPEG-1/2/2.5 Layer III audio decoder board (src/mhi.rs) that serves the Amiga MHI API through the ported mhi_copperline.library (guest/mhi/), rather than modelling any real physical hardware -- MHI-aware players such as AmigaAMP decode MP3 through it exactly as they would through a real MHI decoder board or software MHI driver:

[mhi]
enabled = true

No other options exist yet. Omit the section (or enabled = false) for no board. Like Toccata, its guest interface is register-and-descriptor-queue, not bus-mastering DMA, and its decoded output joins Copperline’s own mixer as a named source (the mhi stem in --audio-stems) rather than talking to a host device directly -- see The MHI decoder board: mailbox register protocol for the register model and Audio sink service (src/audio/) for the mixer/stem-capture integration.

Networking: the bundled HostSocket board

[hostsocket] fits the bundled HostSocket board: guest-facing bsdsocket.library backed by a host-side smoltcp TCP/IP stack, so socket-using applications run with no guest network stack to boot -- see the configuration guide for the user-facing knobs and caveats. The A2065 is for testing a guest network driver and TCP/IP stack; HostSocket is for running socket applications, and serves as the everyday bsdsocket.library for software running under Copperline. With net = "host" its new TCP and UDP sockets bypass smoltcp and become real host sockets through the plugin host_sockets capability.

Architecturally it is not a native board like the A2065 but a WASM plugin board whose module and guest autoboot ROM ship inside the copperline binary:

The board autoconfigs under the Copperline manufacturer ID with product 6 (see below). Its conformance record against the external bsdsocktest suite is crates/hostsocket-plugin/docs/bsdsocktest-status.md.

Crypto: the bundled zz9k board

[zz9k] fits the bundled ZZ9000 SDK crypto board: a register-compatible subset of the MNT ZZ9000’s “SDK v2” service platform (the CORE + MEMORY + CRYPTO services) whose crypto runs host-side, so the SDK’s unmodified Amiga-side software -- its transport library, the zz9k-* tools, and the accelerated AmiSSL build -- gets modern-speed hashing, AEAD, key exchange, and signature verification from an emulated 68k. See the configuration guide for the user-facing knobs and the protocol contract for the register/opcode surface and the exact zz9000-sdk revision it tracks.

Like HostSocket it is a WASM plugin board whose module ships inside the copperline binary: crates/zz9k-plugin/ is the source, the committed artifact is assets/zz9k/zz9k_plugin.wasm (refresh with make in the crate), and config resolution (src/zz9k.rs) expands [zz9k] into a plugin-board entry with the module-path sentinel <bundled-zz9k>. Unlike HostSocket it is pure compute -- no DMA, no network, no host sockets -- so fitting it keeps the machine fully deterministic and replay-safe, and it carries no autoboot ROM or guest driver of its own: the SDK software finds the board via FindConfigDev and speaks to it directly.

Unlike HostSocket it does not autoconfig under the Copperline manufacturer ID: it presents the ZZ9000’s own identity (manufacturer 0x6D6E, product 4 on Zorro III / 3 on Zorro II), because that identity is what the SDK’s board probe looks for. The RTG/USB/Ethernet faces of the real ZZ9000 are absent -- their registers read zero and their services report unsupported -- so installing the real board’s P96 zz9000.card driver against it is unsupported (harmless, but no display).

Graphics: RTG boards

Copperline fits at most one RTG board through [rtg]: the Z3660, a Picasso II or II+, or a Graffity. All are functional device-backed boards whose guest drivers program real hardware interfaces; there is no software-aware virtual framebuffer.

Z3660

[rtg] card = "z3660" fits an in-tree RTG (retargetable graphics) board (src/z3660.rs) modelled on the Z3660 accelerator’s FPGA graphics core, driven by the open-source Z3660.card Picasso96 driver in the guest (see the configuration guide for setup). Although the physical board sits in the A3000/A4000 CPU slot, its RTG core autoconfigs as an ordinary Zorro III board -- manufacturer 0x144B, product 1, the real board’s identity rather than Copperline’s own manufacturer ID below -- with one 128 MB window. The driver finds it with FindConfigDev and talks to a 32-bit register file in the first 2 KB of the window; the rest is board RAM, with P96 VRAM from window offset +0x200000 and the GFXData blit-parameter mailbox the driver fills before ringing a blit at +0x3200000. Only the first 64 MB is backed -- the driver never touches the window beyond ~52 MB, so the allocation stays honest without carrying all 128 MB.

Like the A2065 it is a device-backed board, not a [[zorro]] metadata board; the scanout and blitter model live in z3660.rs and the presentation path is described in The video pipeline.

Village Tronic Picasso II and II+

[rtg] card = "picasso2" fits the 1993 Zorro II card with its CL-GD5426; card = "picasso2plus" selects the CL-GD5428-based Picasso II+. Both take 1 or 2 MB of VRAM ([rtg] vram). One physical board enumerates as two consecutive autoconfig identities under Village Tronic manufacturer 2167 ($0877). The original reports serial $00020000; the II+ reports $00100000 on both identities:

productsizeautoconfig spacepurpose
111 or 2 MBZorro II memorylinear VRAM aperture
1264 KBZorro II I/OVGA registers and monitor switch

Product 11 has ERTF_CHAINEDCONFIG set and is not added to the system free memory list; product 12 follows it. Both windows route to one Picasso2 device. Copperline tags the VRAM mapping internally so the shared device can distinguish it without changing the common Zorro device interface. Product 13, the physical board’s jumper-selected segmented mode, is intentionally not implemented because Picasso96 does not support it.

The product-12 register window decodes as follows:

offsetfunction
$0000-$0FFFVGA I/O ports at the same numeric port address
$1000-$1FFFthe same ports with address + 1, for odd ISA byte lanes
$2000-$7FFFunused, reads as open bus and drops writes
$8xxx, $Axxxeven-address write selects the RTG output
$9xxx, $Bxxxeven-address write selects native Amiga pass-through

A 68k word write to $3C4 therefore supplies the sequencer index in its high byte and the $3C5 data in its low byte. The linear memory aperture preserves byte order exactly. Both Cirrus revisions interpret 15/16-bit pixels as little-endian words and 24-bit pixels as B, G, R bytes, matching the *PC and BGR formats advertised by the Picasso96 driver. CRTC part ID $27 reads $90 on the CL-GD5426 and $98 on the CL-GD5428.

Only the II+ drives an interrupt line. A write to register-window offset $1001 enables its INT2 output and $1000 disables it. An enabled VGA vertical interrupt latches at the CRTC-programmed retrace edge in emulated time; writing CRTC $11 with bit 4 clear acknowledges it. The original card stores the board-enable bit for register compatibility but never asserts INT2.

Ateo Concepts Graffity [Zorro II] and [Zorro III]

[rtg] card = "graffityz2"/"graffityz3" fit Graffity, a lesser-known board that reuses Picasso II+‘s CL-GD5428 core under Ateo Concepts’ own registered manufacturer ID 2092 ($082C). Both take 1 or 2 MB of VRAM ([rtg] vram); see Graffity [Zorro II]/[Zorro III] model for the chip-level detail. Graffity ships a first-class Picasso96 board driver (Graffity.card in the classic Aminet Picasso96Install package), so no CyberGraphX or custom driver is needed.

The Zorro II variant enumerates the same way Picasso II does -- a chained VRAM aperture (product 34) and a register aperture (product 33), except the register aperture is 128 KB rather than 64 KB, and its VGA ports sit directly at the window offset (no odd-lane +0x1000 mirror):

productsizeautoconfig spacepurpose
341 or 2 MBZorro II memorylinear VRAM aperture
33128 KBZorro II I/OVGA registers and monitor switch

The Zorro III variant is a single 16 MB window (product 33, no chained identity) with three fixed sub-apertures instead of one shared register window:

offsetsizepurpose
+$40000064 KBmonitor-switch strobe trap only; never reaches VGA registers
+$80000064 KBVGA registers, same direct port addressing as the Zorro II variant
+$C000001 or 2 MBlinear VRAM

Both variants take the monitor switch as a write strobe, but decode it from different address bits than Picasso II: with address bits 6:5 at 11 (offset +$60) the write selects RTG, and at 10 (+$40) native Amiga pass-through. The Zorro II board takes the strobe anywhere from +$8000 in its register aperture, the Zorro III board anywhere in its switch aperture. Neither has a board-level interrupt-enable latch: INT2 follows the CL-GD5428 core’s own vertical-blank state directly.

How autoconfig works in Copperline

Everything below happens automatically; it is documented so you can debug a board that the guest OS does not pick up, and so the model is clear when adding new backing types.

At reset every board is unconfigured and the first board in the chain appears in the autoconfig window at $E80000-$E8FFFF (AUTOCONFIG_BASE/AUTOCONFIG_SIZE in src/zorro.rs). Kickstart’s expansion library then walks the chain:

  1. Discovery. The board exposes a 16-byte autoconfig ROM, nibble-encoded at even addresses of the window. Byte 0 (er_Type: Zorro generation, memlist flag, size code) is presented as-is; all other bytes are presented inverted, per the hardware convention. The ROM carries the product, manufacturer, and serial from the spec, plus the size code (zorro_ii_size_code / zorro_iii_size_bits).

  2. Base assignment. For a Zorro II board, Kickstart writes the base address high byte to $E80048; for Zorro III it writes a word of the base’s high 16 bits to $E80044. The write configures the board at that base and maps its space.

  3. Chain advance. The configured board disappears from the config window and the next unconfigured board appears. Kickstart can also write $E8004C to “shut up” a board it cannot place, removing it without mapping; a board with ERFF_NOSHUTUP set (the CD32 FMV module) ignores that write.

Successful configuration is logged:

zorro II board "fast RAM" autoconfigured at 0x00200000
zorro II board "Copperline" autoconfigured at 0x00E90000

Once configured, accesses inside a board’s window are routed by ZorroChain::region_at into the board’s backing storage. RAM-backed board space is external-bus memory: it runs at the CPU clock and does not contend on the chip bus (see The timing model). The standalone ZorroChain::power_on_reset API returns every board to the unconfigured state and zeroes its RAM by default. A machine-level cold boot returns it to the same unconfigured state but uses the selected [memory] init fill policy for the RAM.

Device-backed boards (BoardBacking other than Ram) differ in three ways:

On CDTV machines the DMAC occupies the config window first; the Zorro chain follows once it is configured, matching real-machine autoconfig order.

On a CD32 with fmv = true (or fmv_rom), the Commodore Full Motion Video cartridge instead occupies the first Zorro II slot, as its module ROM expects: manufacturer 514, product $6A, serial $0028001E, a 1 MiB memory-space board with DiagArea vector $80 and the no-shut-up flag. Its hardware model and address map are documented in Peripherals and expansion.

The Copperline manufacturer ID

Copperline’s built-in virtual boards autoconfig under manufacturer ID 5192 (0x1448) -- the registered ID of dec0de Consulting, which also makes the real ROMulus flash-ROM board. The product numbers under it are:

ProductBoard
1ROMulus (physical hardware; not emulated)
2Copperline identification board
3Built-in fast RAM ([memory] fast)
4Built-in Zorro III RAM ([memory] z3)
5Copperline services board (host [[filesys]] mounts and the [clipboard] unit; filesys.rs)
6HostSocket bsdsocket.library board ([hostsocket]; hostsocket.rs)
7MHI virtual MPEG audio decoder board ([mhi]; mhi.rs)
8copperhf virtual hardfile controller ([copperhf]; copperhf.rs)

(The bundled zz9k crypto board is the one exception: it autoconfigs under MNT’s manufacturer ID 0x6D6E with the ZZ9000’s own product numbers, because the ZZ9000 SDK detects the board by that identity.)

The identification board (BoardSpec::copperline_id) is always added to the chain (unless disabled, below) so guest software can detect that it is running under Copperline rather than on real hardware or another emulator -- for example identify.library calling FindConfigDev(5192, 2). It is the smallest legal Zorro II board (64K), is kept out of the Exec free-memory list, and never autoboots, so it sits inertly on the chain without changing the machine’s usable memory map. Its autoconfig serial number carries the running Copperline version packed as major << 16 | minor << 8 | patch, so a tool can report the exact version and not just the emulator name.

The board follows the RAM boards (built-in and [[zorro]]) on the chain, so those keep the base addresses they would get without it; the functional boards come after it (see Built-in boards). Set identify = false in the configuration to drop it entirely (for a chain with no emulator-identifying board); see the identify option in Configuration reference.

Adding a board in Rust

Most functional boards should be WASM plugins (above). Add an in-tree board in Rust only when it needs host integration or performance that a plugin cannot give (the A2091 SCSI controller, src/a2091.rs, is the worked example). In-tree functional boards implement the ZorroDevice trait (src/zorro_device.rs) and are stored as a BoardDevice enum variant in Bus::devices; the chain maps each board’s window to a BoardBacking::Device(slot) index into that vector.

  1. Implement ZorroDevice for the board (register read/write, tick, int2_line/int6_line, reset); DMA goes through the DeviceHost passed to each call. Add a BoardDevice variant wrapping it to the declaration in src/zorro_device/state.rs, assigning a new, unused wire ID. Existing IDs are permanent, including those belonging to disabled features. Extend all of BoardDevice’s forwarding match arms (read, write, peek_word, tick, int2_line, int6_line, take_activity, reset, kind) for the new variant. Bus ticks every board at each timed-device boundary, then samples its IRQ lines. A board may return early internally when it has no work; the bus does not query board idle/deadline hooks.

  2. Provide a BoardSpec constructor mirroring the existing ones, with the full field set. A functional board uses backing: BoardBacking::Device(slot) where this RAM example has BoardBacking::Ram:

    pub fn fast_ram(size_bytes: usize) -> Self {
        Self {
            name: "fast RAM".into(),
            version: ZorroVersion::II,
            manufacturer: COPPERLINE_MANUFACTURER_ID,
            product: PRODUCT_FAST_RAM,
            serial: 0,
            size_bytes,
            backing: BoardBacking::Ram,
            memlist: true,
            memory_space: true,
            chained: false,
            no_shutup: false,
            window: 0,
            diag_vec: None,
        }
    }
  3. Instantiate the device in build_machine (src/emulator.rs, not src/main.rs): assign it a slot, add its BoardSpec to the chain, and push the BoardDevice onto Bus::devices (the A2091 block, and the lide-compatible IDE board’s block right after it, are worked templates). A new board needs no save-state version change: boards travel in the ZORR chunk, whose payload names its fields, so older states simply lack the board. Bump that chunk’s version in savestate/chunk.rs (with a migration) only if an existing board’s serialized meaning changes in a way a #[serde(default)] cannot express (see Versioning).

  4. Add unit tests next to the existing ones in src/zorro.rs, which cover ROM nibble encoding, Zorro II/III base assignment, chain advance, shut-up, and power-on reset -- they are the best worked examples of the protocol.

Keep the hardware-first rule in mind: boards model autoconfig hardware behaviour, and anything guest-visible (IDs, sizes, ROM bytes) should match what a real board of that class would expose.