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_BACKLOGis 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 anyZZ_ETH_RX_ADDRvalue 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; readingZZ_ETH_TXback returns the result code (res = 0= OK, observed on the wire). The single shared TX window is serialized byinfo->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 keyeth_tx_pollcuts 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 againstinfo->last_serialto find fresh frames, delivering each up the stream, and strobingZZ_ETH_RXto 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_BdGetLengthand does not strip the FCS (there is noFCS_STRIPoption). So the driver pads TX up to the 60-byte minimum and does not subtractETH_CRC_LENfrom the RX length — it must not inherit the stockaendriver'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 subtractETH_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_tbledit. ✅ The driver autoconfigs on the firstopenand services RX from a clock-leveltimeout()poll callout (periodZ3660ETH_POLL_TICKS = 1). This is why a GEM-less build box boots cleanly andopen()simply returnsENXIO— 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 inlineinsys/inline.h; on this kernelspl5/spl6/spl7all map to_spl4(SR0x2400, 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 includesys/inline.h— thenm -uclean-gate (below) rejects that. An earlier header then no-opped them on the wrong premise that Amix has none;z3660eth.hnow definessplz3660eth()asspl6()and masks three short sections (thetx_busyclaim in xmit, open's slot publication, the status snapshot), never across the ~1 ms TX trigger.- DLPI Style-1, up to
Z3660ETH_MAXDEV = 5SAP 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 ✅:
/usr/sys/amiga/driver/z3660eth/—z3660eth.c,z3660eth.h,z3660ethuser.h,Makefile.master.d/kernel.c: addextern struct streamtab z3660ethinfo;after the stockaeninfo, then claim cdevsw slot 51 (verify it is still the empty/*51*/ … notty,nostr,…row) by replacingnostrwith&z3660ethinfo.amiga/driver/Makefile: addz3660eth/exptoOBJand az3660eth/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 ✅:
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, commit9d12f3b). ✅ Besides the undefinedspl6/splx(the missingsys/inline.hinclude, above), a STREAMS driver must spell outunsigned char/long/short— notuchar/ulong/ushort, which arerico.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 byz3660eth_cache:0= off (the shipping default),40/60= 68040 / 68060 data-cache line push and invalidate (cpushl/cinvlas raw.wordencodings), poked through/dev/kmem. On the 040/060 kernelDTT0cache-inhibits the whole low 1 GB, so the direct-mapped windows need none, and an attach-time check reports that verdict asz3660eth_ci_ok. - Auto-bring-up race (the boot script, not the driver). 🟡 On a fast clean boot the bring-up
slink addaencan race ahead of the inet base STREAMS plumbing and fail to openzen0(observed once: the box reachedlogin:butzen0never came up; cause inferred from the working manual bring-up, which always ran a bareslinkfirst — not independently traced). The boot script now runs a bareslinkfirst, then retriesslink addaenin 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:
relocunixis literallyln 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
unixcan passcheckunix(which only validates the.symtabshape) while still having undefined symbols — socheckunixalone is insufficient. The working gate relinks until the kernel is bothnm -u-clean andcheckunix-clean, thenlns it torelocunix. (A recurringsum -ramong 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
expblobs (os, io, netinet, rpc, vm, …): they ship pre-linked with no source on the box and are not regenerable. Only theamiga/tree +master.d+fs/exp+local/expare 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:
- Kernel-only shuttle — copy the clean
relocunixto the box via a small scratch Amiga hardfile on a spare SCSI target, stage it,cd /stand; make, reboot. Never re-image the root. - Full-image / firmware deploy (no-console) — the Z3660 ARM firmware console has a TFTP
server. Power-cycle into the ARM console (spam
Cat power-on),Pto start TFTP at192.0.2.29,PUTthe artifact,Rto reboot: path0:(FAT32) holdsZ3660.bin(the firmware, ~12.3 MB, ~21 s TFTP; built withcd <your Z3660 checkout> && ./docker/run.sh make bootbin→…/Alfa/sd_card/BOOT.BIN, incremental control-core builds in seconds); path1:(exFAT) holdshdf/Amix.hdf(the full root image, ~629 MB, ~20 min TFTP).
⚠️ Operational gotchas (✅). A power cut on a still-running box costs you an
fsckon 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, oncezen0is 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
aenautoconfigprobes the Zorro chain for manufacturer 514 (Commodore), product 112 — the A2065 — pushing the packed id0x02020070toautocon()✅ (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 twocpu_emulatorfiles in the firmware —Z3660/src/cpu_emulator.candZ3660_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 onaen0" 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 &
⚠️
zen0only comes up after the full boot completes (including anfsck+ 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-z3660netmaster): the frame is freed and counted incouldnt_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 queueQWANTW. When IP drains, STREAMS back-enables the driver's new read service procedurez3660ethrsrv, 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 incouldnt_putnow.
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
fsckis 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
MODE0x100, 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 oneasmblock 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;
zen0is 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
zen0joins; static IP, DNS off by default, theroutemetric, and the 4 KB TCP queue that a polled driver hits hardest. - Z3660 ethernet — the firmware half of the path: descriptors,
backlog, the
z3660cfg.txtethernet 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 →
relocunixcycle and the D245 clean-gate this driver's build depends on. - Device & card list — the
cdevswmajor 51 /zenassignment (renumbered from 48 on 2026-07-30). - Quirks — the firmware-INT6/polled-RX gotcha in the checklist.
Sources¶
- The
ZZ_ETH_MAC_HIread before load (first-party record, 2026-11-02) ✅/🟡 — the amixkernel/dlmbprobetest modules' word read of0x10000198on an A4000D with a Z3660 (real 68LC060), root and boot passes; the test module now reading a long (3bdf89d, onmaster). The register read handler, the MAC set-up,FW_VERSIONand theext_kickstartcondition from the Z3660 fork's firmware source (jusii/Z3660-amix;rtg/rtg.c;aab97b5, oncore1-runtime) ✅ (amigaux/amixkernel/dlm/tests/bench/bprobe/, (51dd57a, onmaster)). - 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 andz3660eth_direct_map),src/z3660eth.h(register map, frame-window layout,splz3660eth()=spl6()viasys/inline.h),src/z3660ethuser.h,src/Makefile,driver.conf(thenet z3660eth … 51 zenstanza; 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-timez3660eth_cachegate replacingZ3660ETH_CACHE_FLUSH; 2026-09-27: interrupt masking made real); commitsf4f64f7(initial STREAMS/DLPI driver),a164bb9(on-box integration scripts),9d12f3b(Phase-1 gate: therico.h-isms, andspl6/splxno-opped — corrected 2026-09-27),028c8d2(do not enable firmware INT6 — the storm fix),88425eb(validated on real HW; hardenedS99zen+ 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_RXdispatch, the INT6 raise),ethernet.c,memorymap.h,rtg/zzregs.h. The body's line numbers are on theamix-mainbranch; onrelease-1.0(the 1.0 source)rtg.chasREG_ZZ_CONFIGat 1111, the INT6 enable at 1127, the ETH raise at 329 and theREG_ZZ_ETH_TX/REG_ZZ_ETH_RXdispatch at 1570 / 1574. - Design / scoping (primary, ✅): amix-z3660net
docs/ETHERNET-SCOPING.mdanddocs/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↔boxping/ftp, and firmware serial[ETHCFG]/[ETHTX]debug breadcrumbs (a temporaryrtg.cbuild, since reverted). - Kernel build tooling (✅): the amix-kerntools project
(https://github.com/jusii/amix-kerntools) —
amix-build(parses thedriver.confnetstanza and assembles the bundle),tools/onbox-build.sh(the on-box half),tools/build-clean-net-kernel.sh(thenm -uclean-gate). - The RAM-ceiling investigation (first-party record, 2026-08-12/14): the ">80 MB board not found" root cause —
sptmapexhaustion against the fixed 4 MiB kernel arena, with the firmware-map-clash hypothesis falsified (banks byte-identical at every window, host DDR empty) — and thebase < VSECT1direct-map fix taking the driver from 65sptmappages to 0 ✅. Kernel-side context on the RAM ceiling. - The
aen0-vs-zen0determination (first-party record, 2026-09-09) ✅ — the board-ID mismatch read from both ends of the interface: the Amix kernel'saenautoconfigpushing the packed A2065 id0x02020070toautocon()(disassembled from the linked kernel), against the Amix-interop firmware'sMANUF_ID 0x144Bfor 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 -Sreturning 1 underautoconfig_rtgboth NO and YES, and the five-commandzen0bring-up succeeding first try on a real 68LC060 — while a different firmware build networked onaen0on 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 -kagainst the on-box native-linked kernel; and the byte-vs-intadbformat trap that makes a set flag print as16777216. - 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
z3660ethrsrvresume 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 base0x10000000; ifacezen0@192.0.2.39; build box192.0.2.38; TFTP192.0.2.29; station MAC00:80:51:01:02:03. - The INT6 receive rows and the address-capture arming (first-party record, 2026-10-26) ✅:
amix-z3660netCHANGELOG 1.0.0-5 (every board register access armed); the A4000D module rows (polled and INT6 pings, the 4 MiB FTP throughput transfers).