Skip to content

Case Study: Z3660 ethernet driver (zen0)

This is the network analogue of the A4091 / 53C710 SCSI driver: a brand-new native driver for a board Amix was never meant to support, written, built, deployed and validated end-to-end on real hardware. Where the A4091 work got Amix to boot with root on a Zorro III SCSI controller, this work gives Amix full bidirectional TCP/IP over the Z3660 accelerator's onboard ethernet, as the interface zen0. It pulls together the STREAMS driver model, the kernel build and the networking stack — applied at once to a real combo board.

The work is first-party and reproduced on real hardware — written, built, deployed and run on a physical Amiga 4000 + Z3660 during 2026-06. Unless noted, every claim here is ✅ Verified (driver source we wrote, firmware source we read, or a result reproduced live on the hardware); inferences and unconfirmed items are tagged 🟡 inline, carried verbatim from the source brief per the repo's confidence-tag policy. The source is the amix-z3660net project — src/z3660eth.{c,h}, driver.conf, userland/S99zen, its docs/ETHERNET-SCOPING.md — and the Z3660 firmware source.

One-line summary. Amix now has full TCP/IP over a native SVR4 STREAMS/DLPI driver (zen0) for the Z3660's onboard ethernet — not NIC emulation. The only real-hardware blocker turned out to be an interrupt storm, fixed driver-side with no firmware change. ✅

What the Z3660 and z3660eth are

The Z3660 is a 68030/68060-class Zorro III accelerator built around a Xilinx Zynq-7000 SoC ✅. The Zynq PS runs firmware (Z3660.bin, loaded by BOOT.bin) that emulates the 68k and drives the card's real peripherals — including the card's Zynq Gigabit Ethernet MAC (GEM) via the Xilinx XEmacPs driver. Ethernet, audio and USB all hang off a piscsi-style register/mailbox interface in the card's RTG register page ✅.

z3660eth is a native Commodore Amiga Unix (Amix, SVR4.0, m68k 68030) STREAMS/DLPI ethernet driver for that onboard GEM ✅. It is the network counterpart of the amix-z3660scsi SCSI driver (z3660.c) for the same combo board.

It is not NIC emulation. The firmware already moves whole ethernet frames between the 68k guest and the Zynq GEM — it does this for AmigaOS through the SANA-II Z3660Net.device. z3660eth speaks that same firmware mailbox (a small MMIO register protocol over the Zorro III window) and presents it to Amix as a DLPI Style-1 connectionless ethernet provider, brought up with the stock slink + ifconfig STREAMS plumbing as interface zen0 ✅. Contrast the hydra-amix driver, which programs a real DP8390/NE2000 chip directly; here the chip-level work lives in the card's firmware and the driver talks to it over a register protocol.

Property Value Tag
Driver tag / interface zen → zen0 ✅
cdevsw major 51 — originally 48 (the free nostr slot on the stock / A4091 kernel), renumbered 2026-07-30; 51 verified in a shipped kernel on real hardware 2026-09-09 ✅
Board AutoConfig id 0x144B0001 ✅
Fixed combo base (AGA fallback) 0x10000000 ✅
Station MAC observed on the wire 00:80:51:01:02:03 ✅
Example IP 192.0.2.39 (documentation address; the build box sits at .38 in the same example LAN) —
DLPI style / limits Style-1; Z3660ETH_MAXDEV = 5 SAP streams, Z3660ETH_MAXBOARDS = 1, MTU 1500 ✅

The firmware mailbox: the protocol contract

All register offsets are board-base-relative, 32-bit big-endian MMIO, living in the RTG register page at board_base + 0x000 (piscsi is at +0x2000) ✅. Every offset was verified against the Z3660 driver headers (z3660_regs.h) and the firmware register enum/dispatcher (rtg/zzregs.h, rtg/rtg.c); the in-repo source of truth is src/z3660eth.h ✅.

Registers

Name Offset Dir Meaning
ZZ_CONFIG 0x104 W 1 = enable INT6 delivery, 0 = disable, 24 (8\|16) = ack/clear ETH
ZZ_ETH_TX 0x190 W / R W byte-length = trigger TX (synchronous, ~1 ms); R = TX result
ZZ_ETH_RX 0x194 W write (any value) = advance / free the current RX slot
ZZ_ETH_MAC_HI 0x198 R/W MAC bytes [0..1] = (b0<<8)\|b1
ZZ_ETH_MAC_LO 0x19C R/W MAC bytes [2..5]; a write triggers the GEM SetMac
ZZ_ETH_RX_ADDR 0x1A4 R board-relative byte offset of the current RX slot
ZZ_INT_STATUS 0x1A8 R pending bits: ETH = 1, AUDIO = 2, USB = 4

On the firmware side (rtg/rtg.c) these dispatch to REG_ZZ_CONFIG (~line 1150), REG_ZZ_ETH_TX → ethernet_send_frame() (~1609), and REG_ZZ_ETH_RX → ethernet_receive_frame() (~1613) ✅ — line numbers here and below are from the firmware fork's amix-main branch (the 1.0 source, release-1.0, is in Sources).

Frame windows

The frame buffers are real DDR reached through the Zorro III window, at fixed board-relative offsets (from the firmware memorymap.h) ✅. Read these registers as longs ✅ (the firmware's register read handler): it masks the address with 0x1FFFFC, ignoring the two low address bits and the access size, so every register comes back as a right-aligned 32-bit value, and an undecoded register reads 0. On the real 68060 a word read at the register's own offset gets the high half, usually 0; the firmware's emulated CPU answers a word read with the low half at both +0 and +2. So read a long, or a word at +2, in every mode. Measured: on a real 68LC060 a word read of ZZ_ETH_MAC_HI gave 0x0000 at boot, before the driver had loaded ✅, while a long read gives 0x00000080, the card's MAC starting 00:80 ✅. The MAC is set from the card's configuration before either CPU runs and changes only when written ✅. 0x0000 is also what Amiberry returns for an empty window, so a probe that reads a word at +0 cannot tell the board from open bus. A better identity is FW_VERSION (0x1A0, read as a long): the firmware returns its version there, major in bits 15–8 and minor in bits 7–0, so 0x00000103 on the 1.03 line, never 0 ✅. The board's 144B:01 AutoConfig entry exists only with the extended ROM (ext_kickstart 2) ✅. Note the board base is 0x10000000, so these windows land inside the identity-mapped low 1 GB (< 0x40000000) and are directly reachable — this is not the A4091's above-the-identity-map case; the driver first used sptalloc simply as a clean way to map the full 128 KB region, which on a large-memory machine was the wrong trade, and now uses the windows directly — see the >80 MB sptmap exhaustion below:

  • RX backlog ring — 0x07ED0000, 32 slots × 2048 bytes = 64 KB.
  • TX staging frame — 0x07EE0000, one frame.
  • GEM RX frame — 0x07EF0000.
  • Per-slot stride — 2048 bytes; per-slot header — 4 bytes: [0..1] = length, [2..3] = serial (a monotonic frame serial used as the drain sentinel). The ethernet payload follows the 4-byte header.

🟡 Latent firmware bug (documented, mitigated, not the cause of any observed failure). The firmware constant FRAME_MAX_BACKLOG is 64 while only 32 real slots exist before the TX window — a 64-deep backlog would overrun the ring. The driver mitigates by eager-draining every poll tick and by mapping the full 64-slot / 128 KB range, so any ZZ_ETH_RX_ADDR value the firmware returns is always mapped memory. No overrun was observed in practice.

✅ Confirmed, and fixed upstream-bound (2026-10-20/21). The overrun is real: with a firmware receive ring and an AmigaOS driver that falls behind, the backlog does grow past 32, and slots 32–63 overwrite the TX frame and the receive DMA buffers. Under Amix the eager drain keeps it far below that. shanshe/Z3660 PR #33 caps the backlog at 32 slots. See Z3660 ethernet.

TX / RX mechanics and the FCS boundary

  • TX ✅ — copy the frame (min-padded to 60 bytes) into the TX window, then write the byte length to ZZ_ETH_TX. The trigger is synchronous (~1 ms) with no completion IRQ; reading ZZ_ETH_TX back returns the result code (res = 0 = OK, observed on the wire). The single shared TX window is serialized by info->tx_busy; the ~1 ms trigger is not held at raised spl. Measured on a real 68060 (2026-10-19), the trigger itself takes about 188 µs per full frame, most of it the firmware's wait for transmit-done. The whole driver transmit path is about 730 µs per 1514-byte frame (build, copy, trigger in roughly equal thirds). The firmware key eth_tx_poll cuts the held bus cycle to about 22 µs; see Z3660 ethernet.
  • RX ✅ — drained by re-reading ZZ_ETH_RX_ADDR, comparing each slot's serial header against info->last_serial to find fresh frames, delivering each up the stream, and strobing ZZ_ETH_RX to free the slot. The drain loop is bounded (≤ 256 iterations per call) so a runaway firmware can't wedge the kernel.
  • FCS / minimum-length boundary ✅ — the firmware delivers the raw XEmacPs_BdGetLength and does not strip the FCS (there is no FCS_STRIP option). So the driver pads TX up to the 60-byte minimum and does not subtract ETH_CRC_LEN from the RX length — it must not inherit the stock aen driver's -4. (Same 60-vs-64 CRC boundary as the hydra RX min-frame gotcha, handled the other way round: hydra's DP8390 reports a length that includes the CRC, so it accounts for the 4 bytes; the Z3660 firmware hands back the raw length, so the driver must not subtract ETH_CRC_LEN. Both honour the 60-byte post-CRC minimum.)

Driver architecture

src/z3660eth.c is a STREAMS/DLPI skeleton of about 1,650 lines adapted from the stock Amix aen (LANCE) and hydra drivers, with the chip layer replaced by the Z3660 mailbox ✅.

Function map (all in src/z3660eth.c)

Function Role
z3660eth_map() direct-map the register page and the 64-slot frame region below VSECT1 (the shipped base 0x10000000, through the identity map; sptalloc only for a board at 0x40000000+); fills board_base/regs/frame/txwin/paddress. Returns ENXIO when no Z3660 GEM is present
z3660eth_autoconfig() discovery: autocon(0x144B0001), else (AGA only) the fixed combo base 0x10000000, verified via the firmware MAC register
z3660eth_initialize() writes ZZ_CONFIG, primes last_serial, starts the RX poll callout
z3660ethopen / z3660ethclose lazy-open: autoconfig on first open (like aen)
z3660ethwput / z3660ethwsrv STREAMS write put / service
z3660ethxmit() TX path: pad, copy to txwin, optional cache push, write ZZ_ETH_TX
z3660eth_drain() the bounded RX drain (serial sentinel + ZZ_ETH_RX strobe)
z3660eth_poll() the timeout() callout that calls z3660eth_drain each tick
z3660eth_tx_drain() TX-side completion bookkeeping
z3660ethintr() future interrupt entry point (not wired into int2_tbl yet)
z3660ethproto() DLPI primitive dispatch (bind / unbind / attach / unitdata / …)
toss_packet_up_stream() deliver a received frame up the matching SAP stream
z3660ethioctl() the zen.c presence / status ioctls
z3660ethinit() driver init

Key design decisions

  • Lazy-open + polled RX → no int2_tbl / init_tbl edit. ✅ The driver autoconfigs on the first open and services RX from a clock-level timeout() poll callout (period Z3660ETH_POLL_TICKS = 1). This is why a GEM-less build box boots cleanly and open() simply returns ENXIO — there is no boot-time probe that can panic. It is a deliberately different choice from hydra's "probe at boot (init_tbl/hydrainit), full-init on open" split — see Writing a STREAMS driver.
  • spl6() / splx() are real, from <sys/inline.h>. ✅ They are the stock primitives, static inline in sys/inline.h; on this kernel spl5/spl6/spl7 all map to _spl4 (SR 0x2400, IPL 4), which blocks the level-2 CIA-A clock and with it every callout, including the RX poll. The first kernel link left them undefined because the driver did not include sys/inline.h — the nm -u clean-gate (below) rejects that. An earlier header then no-opped them on the wrong premise that Amix has none; z3660eth.h now defines splz3660eth() as spl6() and masks three short sections (the tx_busy claim in xmit, open's slot publication, the status snapshot), never across the ~1 ms TX trigger.
  • DLPI Style-1, up to Z3660ETH_MAXDEV = 5 SAP streams per board, Z3660ETH_MAXBOARDS = 1, MTU 1500 ✅.

Kernel integration

A STREAMS network driver is not registered in sd.c's scsicard[] table (that is the SCSI-stack path the A4091 driver uses) — it is wired directly into master.d/kernel.c's cdevsw[] and built in its own amiga/driver/<dir>/ subtree. Three edits plus the subtree ✅:

  1. /usr/sys/amiga/driver/z3660eth/ — z3660eth.c, z3660eth.h, z3660ethuser.h, Makefile.
  2. master.d/kernel.c: add extern struct streamtab z3660ethinfo; after the stock aeninfo, then claim cdevsw slot 51 (verify it is still the empty /*51*/ … notty,nostr,… row) by replacing nostr with &z3660ethinfo.
  3. amiga/driver/Makefile: add z3660eth/exp to OBJ and a z3660eth/exp: build rule.

The machine-readable form is driver.conf, consumed by the amix-kerntools amix-build tool, which parses the net <subdir> <src> <streamtab> <major> <iface> stanza and stages the driver; the on-box half is tools/onbox-build.sh and the gate tools/build-clean-net-kernel.sh ✅:

net  z3660eth  z3660eth.c  z3660ethinfo  51  zen  "Z3660 Ethernet"

Create the node and bring the interface up. As on hydra, Amix is SVR4.0 so there is no ifconfig plumb — you link the stream in with slink, then ifconfig ✅:

mknod /dev/zen0 c 51 0                                      # major 51 since 2026-07-30 (was 48)
slink                                                       # ensure the base inet streams are linked
slink addaen /dev/zen0 zen0                                 # STREAMS multiplexor link
ifconfig zen0 192.0.2.39 netmask 255.255.255.0 up -trailers
route add default 192.0.2.1 1                             # trailing 1 = the (required) metric

★ The INT6 interrupt storm — the real-hardware blocker

The single most important lesson of the project ✅. On the WinUAE build box (no Z3660 GEM) the whole integration and build path worked — the kernel boots, z3660eth registers at its cdevsw slot (48 at the time; 51 today), and open(/dev/zen0) returns ENXIO with no panic. But on the real A4000 + Z3660:

Symptom. The kernel booted with the driver and reached login:, but the moment the interface was brought up the box hard-locked (caps-lock LED dead = the 68k is hung hard, not merely busy). ✅

Root cause (diagnosed from the firmware serial [PC] heartbeat + nm of relocunix): the firmware raises Amiga INT6 (EXTER) on every received frame — rtg/rtg.c sets interrupt_enabled_ethernet from ZZ_CONFIG bit 0 (~line 1166), and the RX path calls amiga_interrupt_set(AMIGA_INTERRUPT_ETH) (~line 336). Amix has no level-6 ethernet interrupt handler — its p6int / aciabintr chain runs but never acks the eth source, so the LAN's normal ARP broadcast traffic storms INT6 and instantly hard-locks the machine ✅. (Exactly the INT6 risk the scoping doc had flagged in Phase 0.)

Fix (commit 028c8d2): the driver writes ZZ_CONFIG_DISABLE (0) instead of ZZ_CONFIG_ENABLE (1), so the firmware never raises INT6. RX is unaffected — the firmware still fills the backlog ring regardless of the interrupt-enable flag (ethernet.c only gates a debug print on it), so the bounded poll/drain callout keeps working. No firmware change was needed ✅.

The lesson generalizes: the firmware's interrupt model assumed an AmigaOS handler that Amix does not provide; the safe path is polled RX with the firmware interrupt explicitly disabled. This is recorded as a gotcha on the quirks checklist.

Other bugs that mattered

  • spl6/splx + rico.h-isms (Phase-1 build-breakers, commit 9d12f3b). ✅ Besides the undefined spl6/splx (the missing sys/inline.h include, above), a STREAMS driver must spell out unsigned char/long/short — not uchar/ulong/ushort, which are rico.h-only (included by the SCSI driver, not this one). Both showed up only at kernel-link time, as undefined symbols.
  • Cache maintenance — not needed on the 030 line, a run-time gate on 040/060. ✅ On the 030 line the eth DDR windows are cacheable (they sit inside the identity map), so a stale-cache TX/RX coherency hazard was the scoping's stated top risk. On real hardware it did not bite — TX and RX are byte-correct with no cache maintenance; that is the 030-line observation only. The original compile-time flush (Z3660ETH_CACHE_FLUSH) was never built into any kernel and has been removed. Maintenance is now always compiled and gated at run time by z3660eth_cache: 0 = off (the shipping default), 40 / 60 = 68040 / 68060 data-cache line push and invalidate (cpushl / cinvl as raw .word encodings), poked through /dev/kmem. On the 040/060 kernel DTT0 cache-inhibits the whole low 1 GB, so the direct-mapped windows need none, and an attach-time check reports that verdict as z3660eth_ci_ok.
  • Auto-bring-up race (the boot script, not the driver). 🟡 On a fast clean boot the bring-up slink addaen can race ahead of the inet base STREAMS plumbing and fail to open zen0 (observed once: the box reached login: but zen0 never came up; cause inferred from the working manual bring-up, which always ran a bare slink first — not independently traced). The boot script now runs a bare slink first, then retries slink addaen in a loop (below).

The ">80 MB board not found" — sptmap exhaustion, not a map clash ✅

On a kernel that can describe more than 80 MB of RAM — one whose kernel arena had been moved; a stock 68030 kernel describes only ≈64 MB (the RAM ceiling) — the driver stopped finding its board, and the obvious suspect was the firmware memory map: a clash between the board's Zorro III banks and the enlarged guest memory. That was falsified directly — the board's banks read byte-identical at every window, and the host DDR behind them was empty ✅.

The cause is on the kernel side, and it is the same arena the RAM ceiling is about. page[], sptalloc() and the u-areas are all handed out of one fixed 4 MiB kernel arena through the sptmap resource map. With ~80 MB of RAM described, page[] has taken that arena, and this driver's 65 pages of sptalloc() mappings have nothing left to allocate from — the mapping fails and the board simply is not there ✅.

The fix was to stop mapping. The board's fixed base 0x10000000 sits below VSECT1 — already inside the identity-mapped low 1 GB — so the register window and the frame windows are directly usable. Direct-mapping whenever base < VSECT1 takes the driver from 65 sptmap pages to 0 ✅: the >80 MB failure disappears, and the driver stops competing with the kernel for the frame array's arena.

Verified live on metal. On a running real A4000 + Z3660 the driver's z3660eth_direct_map flag and its SCSI sibling's z3660_direct_map both read set in the live kernel, so the 98 arena pages the pair would otherwise have consumed are a measured reclaim, not a build-time intention ✅. Reading those flags has its own trap — they are one-byte objects and adb's int format prints a set byte as 16777216, not 1 (quirk 46).

The law worth carrying to any Amix driver: a driver's mapping budget and the machine's RAM ceiling are the same budget ✅. sptalloc() is not free space — page-map only what you cannot reach directly.

Build & the nm -u clean-gate

The kernel is built on a networked Amix box (the WinUAE/Amiberry build box at 192.0.2.38). On-box ld corrupts a high fraction of kernel links (the "D245" boot-breaker), so the link must be repeated until it produces a clean kernel ✅ — the same hazard the A4091 build and the kernel-build page describe.

★ The correct clean-kernel bar is nm -u, not sum -r recurrence or checkunix. ✅ In detail:

  • relocunix is literally ln unix relocunix, gated by the kernel Makefile's own test: nm -h -u unix | egrep -v '(etext|edata|end)' must be empty (no undefined symbols).
  • A unix can pass checkunix (which only validates the .symtab shape) while still having undefined symbols — so checkunix alone is insufficient. The working gate relinks until the kernel is both nm -u-clean and checkunix-clean, then lns it to relocunix. (A recurring sum -r among clean builds is gold confirmation but does not converge on this larger kernel and is not required.)
  • Implemented in the amix-kerntools tools/build-clean-net-kernel.sh.

⚠️ Never delete the 22 generic SVR4 exp blobs (os, io, netinet, rpc, vm, …): they ship pre-linked with no source on the box and are not regenerable. Only the amiga/ tree + master.d + fs/exp + local/exp are re-rolled. ✅

Other on-box build realities (✅, validated on the build box): Makefiles have no header-dependency tracking — after editing a .h, force-recompile the dependent .o (or touch the .c) and verify the md5; FTP is asymmetric on the a2065 build box — push (PUT) is reliable but large pull (GET) is flaky, so patch kernel.c on the box with ed and use NFS (nfsvers=2) for large pulls; and an emulated build box needs its Zorro III/DDR memory configured, or a fresh /usr/sys rebuild faults at $40000000.

Deploy & bring-up to the real box

The real A4000 + Z3660 has no network until zen0 is up (chicken-and-egg), so deployment is out-of-band ✅. Two paths:

  1. Kernel-only shuttle — copy the clean relocunix to the box via a small scratch Amiga hardfile on a spare SCSI target, stage it, cd /stand; make, reboot. Never re-image the root.
  2. Full-image / firmware deploy (no-console) — the Z3660 ARM firmware console has a TFTP server. Power-cycle into the ARM console (spam C at power-on), P to start TFTP at 192.0.2.29, PUT the artifact, R to reboot: path 0: (FAT32) holds Z3660.bin (the firmware, ~12.3 MB, ~21 s TFTP; built with cd <your Z3660 checkout> && ./docker/run.sh make bootbin → …/Alfa/sd_card/BOOT.BIN, incremental control-core builds in seconds); path 1: (exFAT) holds hdf/Amix.hdf (the full root image, ~629 MB, ~20 min TFTP).

⚠️ Operational gotchas (✅). A power cut on a still-running box costs you an fsck on the next boot, so confirm the machine is down before powering back on. Detect state from the firmware serial [PC] heartbeat (pinned PC = hung; varying = alive) and, once zen0 is up, over telnet/ftp. Keep the serial stream logged to a file — a bring-up is reconstructed from the log, not from the screen.

★ Why the stock aen0 driver cannot substitute for zen0 — and why that depends on the firmware build

Amix ships a stock ethernet driver, aen, and on a Z3660 box it is tempting to reach for it instead of installing zen0. Under an Amix-interop Z3660 firmware build it can never work, and the reason is a board-ID mismatch that no configuration key can bridge ✅:

  • The Amix kernel's aenautoconfig probes the Zorro chain for manufacturer 514 (Commodore), product 112 — the A2065 — pushing the packed id 0x02020070 to autocon() ✅ (read from the linked kernel's disassembly).
  • The Amix-interop firmware presents every emulated autoconfig board under MANUF_ID 0x144B (Double H Tech), and has A2065 emulation commented out of its embedded UAE core ✅ (Z3660_emu/src/cpu_emulator.cpp:263; uae/include/sysconfig.h:79, /* #define A2065 */). ⚠ Note the tree: there are two cpu_emulator files in the firmware — Z3660/src/cpu_emulator.c and Z3660_emu/src/cpu_emulator.cpp — and the autoconfig code lives only in the .cpp. A grep in the wrong tree returns nothing and makes the claim look false.

So the probe hunts an id the firmware never offers. A manufacturer id is not settable by any autoconfig_* option, which is why the probe fails identically with autoconfig_rtg NO and YES — measured on real hardware, both values, 2026-09-09 ✅. zen0/z3660eth instead binds the Z3660's own product id 0x144B0001 — the same id the sibling z3660scsi driver uses to find its board, so if root mounted, the board is demonstrably in the chain ✅.

🟡 The corollary that costs the most time: this is a property of the firmware build, not of the hardware. Other Z3660 firmware builds do present an A2065 façade, and a box running one of them networks perfectly on aen0 — observed on the same machine and card under a different firmware build, 2026-09-09. An observation that "that box networks on aen0" therefore never transfers across a firmware boundary. Establish which build is running before concluding anything about which interface can work; the runtime proof is the firmware's own [SD Init] Done (…) line, not a checksum of the files on the card.

Diagnosing it takes one command. /etc/inet/network-config brings the stock path up as a single && chain — aen -S && slink addaen /dev/aen0 aen0 && ifconfig aen0 … — so a failed probe short-circuits at step one and prints nothing at all ✅. A box whose network is dead this way produces no console output during boot, which reads convincingly like "the startup script never ran". Run /usr/amiga/bin/aen -S; echo $? by hand: a return of 1 is the board probe failing, and netstat -in will show aen0 with address and network none.

Boot auto-bring-up — /etc/rc2.d/S99zen

The amix-z3660net repo ships userland/S99zen (installed as /etc/rc2.d/S99zen). It is backgrounded so a datapath problem can never hang the boot (the box still reaches login:), and hardened for the bring-up race above ✅:

#!/bin/sh
# S99zen -- bring up zen0 at boot, backgrounded; cdevsw major 51 (was 48 before 2026-07-30).
[ -c /dev/zen0 ] || mknod /dev/zen0 c 51 0
(
  sleep 5
  /usr/sbin/slink                       # ensure the base inet streams are linked
  i=0
  while [ $i -lt 20 ]; do
    /usr/sbin/slink addaen /dev/zen0 zen0 && break
    sleep 3
    i=`expr $i + 1`
  done
  /usr/sbin/ifconfig zen0 192.0.2.39 netmask 255.255.255.0 up -trailers
  /usr/sbin/route add default 192.0.2.1 1
  /usr/sbin/ifconfig zen0
) > /tmp/zen-bringup.log 2>&1 &

⚠️ zen0 only comes up after the full boot completes (including an fsck + reboot if the SD image is dirty). Do not conclude "broken" from an early serial check — give it the whole boot. ✅

Receive service model and throughput ✅

The model. Receive is polled at the clock tick: a timeout() callout every tick (Z3660ETH_POLL_TICKS 1, HZ 60, so 16.7 ms) runs z3660eth_drain, at IPL 4. The drain walks up to 256 slots. For each fresh frame it asks canput() of the queue above it, IP's lower read queue (iplrput → putq, high-water mark 8192), and passes the frame on with putnext. One drain therefore hands IP up to about six full frames at once. A frame waits up to one tick for its poll. Under a push the measured residence in the firmware was 10–16 ms, and the box's acknowledgements leave from inside the drain, so the sender's next burst also waits for a tick.

What happens when IP refuses differs by version:

  • Published driver (amix-z3660net master): the frame is freed and counted in couldnt_put. Its firmware slot is already released, so the frame is gone.
  • Back-pressure version (first-party, 2026-10-20; not yet published): the drain stops and leaves that frame, and everything behind it, in the firmware backlog. The failed canput() marks IP's queue QWANTW. When IP drains, STREAMS back-enables the driver's new read service procedure z3660ethrsrv, which resumes the drain, usually within the same tick. Two bounds keep a stuck stream from wedging the card: at slot 24 of a backlog round the drain catches up instead of holding, and a frame held 30 ticks (HZ/2) is released. Only those forced releases count in couldnt_put now.

The batch meets the stack's 4 KB limit. IP's service pass passes the whole batch upward before TCP's service procedure runs, and the stock TCP lower read queue holds only 4096 bytes. So the fourth full segment of every batch is freed inside IP, uncounted. That, not the card, was why pushes into a zen0 box ran at 25–30 KB/s (networking: the 4 KB TCP queue). A polled driver is the worst case for that limit, because it batches by construction.

Measured (4 MiB FTP, real 68060, 100 Mbit, 2026-10-21):

Fixes in place Push Retransmissions Pull
none (stock kernel, published driver, 1.0 firmware) 22–31 KB/s 319–392 ~600 KB/s
kernel's TCP queue raised to 16 KB 120–126 KB/s 109–119 ~575 KB/s
+ driver back-pressure 227–334 KB/s 10–46 570–640 KB/s
+ firmware receive ring (PR #33 build) 314–372 KB/s 0–8 560–650 KB/s

The full ladder, the firmware's keys and the AmigaOS comparison are on Z3660 ethernet. The next ceiling is the 60 Hz poll itself: the push settles at about half the pull 🟡, and interrupt-driven receive is the lever (see Known issues).

Status — ✅ works on real hardware (2026-06-21)

Validated on a real A4000 + Z3660, all reproduced live ✅:

Test Result
ifconfig zen0 flags=23<UP,BROADCAST,NOTRAILERS>, inet 192.0.2.39, mask ffffff00
Inbound flood ping (laptop → .39, 40 pkts @ 50 ms) 40/40 received, 0% loss (~3–19 ms RTT)
Outbound: box → laptop (ping 192.0.2.66) 192.0.2.66 is alive
Outbound: box → gateway (ping 192.0.2.1) 192.0.2.1 is alive
netstat -in for zen0 Ierrs = 0 Oerrs = 0 Collis = 0 (e.g. 895 Ipkts / 376 Opkts)
Remote login (telnet / ftp over zen0) works (drove the box entirely over the network)
Sustained FTP throughput ~185 KiB/s, stable across 3 consecutive ~3.4 MB transfers

🟡 Throughput note. ~185 KiB/s (≈1.5 Mbit/s) was the measured FTP rate in June 2026, a working baseline, not a tuned result. The interface itself reports zero errors/collisions even under flood load. The October 2026 measurements and the fixes behind 340–370 KB/s pushes are under Receive service model and throughput.

Known issues & open questions

  • 🟡 Boot reliability is gated by unrelated boot panics, not the driver. The Amix root is a non-journaled UFS; a cold power-cut can corrupt it and boot-time fsck is effectively broken (a separate known issue), so a dirty root can panic before reaching the bring-up. When the boot is clean, networking comes up. Always prefer clean shutdowns (shutdown -i0; verify the network drops, i.e. the FS unmounted) before power-cycling.
  • 🟡 Auto-bring-up race — mitigated by the hardened S99zen, not yet stress-tested across many cold boots.
  • ✅ Interrupt-driven RX exists in the 1.1 module form. The static driver polls. The loadable module (1.1, in development) can attach an INT6 receive path through the module core's interrupt service: the level-6 handler only acknowledges and flags, and the drain runs at level 2. On real hardware (A4000D, 68LC060) it held 3 × 2000 pings at 5 ms with 0 % loss and a 1.2–1.4 ms round trip, against 11.6 ms polled, and pushed 418–489 KiB/s ✅ (2026-10-26). Polled stays the default.
  • ✅ Arm every register access against the address-capture race. The Z3660 can deliver the second of two back-to-back accesses at (previous AND this address) 🟡 — see the SCSI driver page. In this driver the pairs lost the INT6 acknowledge (a write that lands on MODE 0x100, logged by the firmware as [RTG] Error mode: 255), lost the receive strobe (one way into a receive-ring wedge), lost the transmit trigger, and read the wrong half of the station MAC at attach ✅ (from the register map and the firmware log). Every register access is now one asm block at IPL 7 with a covering read before it ✅.
  • ✅ Cache maintenance off by default (z3660eth_cache = 0; the 040/060 line can turn it on at run time); revisit only if a future board, firmware or kernel changes the cacheability of the eth windows.

See also

  • Z3660 piscsi SCSI driver — the SCSI sibling on the same Z3660 combo board: the same firmware-mailbox style of register protocol, applied to disk I/O — it boots Amix multiuser with root on real hardware.
  • A4091 / 53C710 SCSI driver — a separate Zorro III SCSI card (a real 53C710); the D245 clean-gate and Zorro III bring-up context this builds on.
  • Writing a STREAMS driver — the third driver kind; zen0 is a second worked example (polled RX, firmware mailbox) alongside hydra (INT2, DP8390).
  • Hydra DLPI case study — the other modern Amix net driver; the mirror-image 60-vs-64 CRC boundary lives there.
  • Networking — the SVR4 STREAMS TCP/IP stack zen0 joins; static IP, DNS off by default, the route metric, and the 4 KB TCP queue that a polled driver hits hardest.
  • Z3660 ethernet — the firmware half of the path: descriptors, backlog, the z3660cfg.txt ethernet keys, and measured Amix and AmigaOS throughput.
  • Emulation Fidelity — the 68030 bus-error-frame and SCSI INT2-latency requirements of the same Z3660 UAE-derived emulator this driver runs on.
  • Building and installing a kernel — the relink → relocunix cycle and the D245 clean-gate this driver's build depends on.
  • Device & card list — the cdevsw major 51 / zen assignment (renumbered from 48 on 2026-07-30).
  • Quirks — the firmware-INT6/polled-RX gotcha in the checklist.

Sources

  • The ZZ_ETH_MAC_HI read before load (first-party record, 2026-11-02) ✅/🟡 — the amix kernel/dlm bprobe test modules' word read of 0x10000198 on an A4000D with a Z3660 (real 68LC060), root and boot passes; the test module now reading a long (3bdf89d, on master). The register read handler, the MAC set-up, FW_VERSION and the ext_kickstart condition from the Z3660 fork's firmware source (jusii/Z3660-amix; rtg/rtg.c; aab97b5, on core1-runtime) ✅ (amigaux/amix kernel/dlm/tests/bench/bprobe/, (51dd57a, on master)).
  • Driver source we wrote (primary, ✅): the amix-z3660net project (https://github.com/jusii/amix-z3660net, branch master) — src/z3660eth.c (about 1,650 lines; the MAPPING STRATEGY note and z3660eth_direct_map), src/z3660eth.h (register map, frame-window layout, splz3660eth() = spl6() via sys/inline.h), src/z3660ethuser.h, src/Makefile, driver.conf (the net z3660eth … 51 zen stanza; 48 until the 2026-07-30 renumber), userland/S99zen (boot auto-bring-up), userland/{bringup.sh,network-config.zen,zen.c}, README.md, BUILD.md, CHANGELOG.md (2026-08-24: the run-time z3660eth_cache gate replacing Z3660ETH_CACHE_FLUSH; 2026-09-27: interrupt masking made real); commits f4f64f7 (initial STREAMS/DLPI driver), a164bb9 (on-box integration scripts), 9d12f3b (Phase-1 gate: the rico.h-isms, and spl6/splx no-opped — corrected 2026-09-27), 028c8d2 (do not enable firmware INT6 — the storm fix), 88425eb (validated on real HW; hardened S99zen + README real-HW status).
  • Firmware source we read (primary, ✅): the Z3660 firmware fork (https://github.com/jusii/Z3660-amix) z3660-firmware/Z-TURN/vitis_ide/Z3660/src/ — rtg/rtg.c (REG_ZZ_CONFIG / REG_ZZ_ETH_TX / REG_ZZ_ETH_RX dispatch, the INT6 raise), ethernet.c, memorymap.h, rtg/zzregs.h. The body's line numbers are on the amix-main branch; on release-1.0 (the 1.0 source) rtg.c has REG_ZZ_CONFIG at 1111, the INT6 enable at 1127, the ETH raise at 329 and the REG_ZZ_ETH_TX / REG_ZZ_ETH_RX dispatch at 1570 / 1574.
  • Design / scoping (primary, ✅): amix-z3660net docs/ETHERNET-SCOPING.md and docs/ETHERNET-SCOPING-evidence.md (the protocol contract, cache-coherency analysis, the INT6 risk, the phased plan).
  • Real-hardware results reproduced live (2026-06-21, A4000 + Z3660, ✅): ifconfig zen0, netstat -in, laptop↔box ping / ftp, and firmware serial [ETHCFG] / [ETHTX] debug breadcrumbs (a temporary rtg.c build, since reverted).
  • Kernel build tooling (✅): the amix-kerntools project (https://github.com/jusii/amix-kerntools) — amix-build (parses the driver.conf net stanza and assembles the bundle), tools/onbox-build.sh (the on-box half), tools/build-clean-net-kernel.sh (the nm -u clean-gate).
  • The RAM-ceiling investigation (first-party record, 2026-08-12/14): the ">80 MB board not found" root cause — sptmap exhaustion against the fixed 4 MiB kernel arena, with the firmware-map-clash hypothesis falsified (banks byte-identical at every window, host DDR empty) — and the base < VSECT1 direct-map fix taking the driver from 65 sptmap pages to 0 ✅. Kernel-side context on the RAM ceiling.
  • The aen0-vs-zen0 determination (first-party record, 2026-09-09) ✅ — the board-ID mismatch read from both ends of the interface: the Amix kernel's aenautoconfig pushing the packed A2065 id 0x02020070 to autocon() (disassembled from the linked kernel), against the Amix-interop firmware's MANUF_ID 0x144B for every emulated board with A2065 emulation commented out of the embedded UAE core (Z3660_emu/src/cpu_emulator.cpp:263; uae/include/sysconfig.h:79) — line references re-verified against the firmware source 2026-09-09. Confirmed on real hardware the same day: aen -S returning 1 under autoconfig_rtg both NO and YES, and the five-command zen0 bring-up succeeding first try on a real 68LC060 — while a different firmware build networked on aen0 on the same machine and card, which is the evidence for the firmware-build dependency (🟡 for that build's façade, which we did not read).
  • A live-kernel check (first-party record, 2026-08-14/15) ✅ — both direct-map flags read set in the live kernel on a real A4000 + Z3660 (98 arena pages reclaimed across the ethernet and SCSI drivers), read with adb -k against the on-box native-linked kernel; and the byte-vs-int adb format trap that makes a set flag print as 16777216.
  • The October 2026 receive-path work (first-party record, 2026-10-19 to 2026-10-21) ✅: the stamped transmit/receive profile on a real 68060, the back-pressure version of the drain (not yet in the public repository; its design notes describe the hold, the z3660ethrsrv resume and the 24-slot / 30-tick bounds), and the push/pull rows on two A4000-class machines. The firmware side is cited on Z3660 ethernet.
  • Magic numbers (✅): cdevsw major 51 (48 until 2026-07-30); board autocon 0x144B0001; fixed combo base 0x10000000; iface zen0 @ 192.0.2.39; build box 192.0.2.38; TFTP 192.0.2.29; station MAC 00:80:51:01:02:03.
  • The INT6 receive rows and the address-capture arming (first-party record, 2026-10-26) ✅: amix-z3660net CHANGELOG 1.0.0-5 (every board register access armed); the A4000D module rows (polled and INT6 pings, the 4 MiB FTP throughput transfers).