Skip to content

network: rewrite the DaynaPORT SL003 emulation from the device ROM - #401

Closed
ingpaschke wants to merge 8 commits into
BlueSCSI:mainfrom
ingpaschke:pr/daynaport-sl003
Closed

network: rewrite the DaynaPORT SL003 emulation from the device ROM#401
ingpaschke wants to merge 8 commits into
BlueSCSI:mainfrom
ingpaschke:pr/daynaport-sl003

Conversation

@ingpaschke

Copy link
Copy Markdown

Follow-up to #399, and intended to supersede it. #399 patched the
existing handler toward what the real SCSI/Link does, based on
disassembling Dayna's Mac driver. Since then I've disassembled the
device itself: the SL003 v2.0 Z180 ROM (and 1.3 for comparison), plus
Apple's .ENET0 1.2.2f0 and Dayna's last 1.2.5f1. With the actual
firmware on the table, patching the old handler further made less sense
than rewriting the emulation against the ROM, so that's what this is.

The protocol logic now lives in a standalone core
(lib/SCSI2SD/src/firmware/DaynaPort/) transcribed from the ROM
disassembly, with ROM addresses cited in comments. The core has no I/O
of its own; a small personality file connects it to the bus and the
radio. network.c keeps the shared queue/wifi plumbing, and AmigaWIFI
is untouched next to it.

What it does that #399's version didn't:

  • Polled READ(6) exactly as the ROM: one record per command, live
    more-pending flag, and the stateless peek/resume through CDB[1..2]
    the Mac driver uses under VM.
  • Blind READ(6) as a record stream whose receive ring refills
    mid-transaction: the radio runs from a low-priority IRQ
    (threadsafe-background cyw43 arch, scoped to Pico_2_DaynaPORT; other
    targets stay on the poll arch). A batch is no longer capped at
    whatever was queued when it started.
  • A capability contract for fixed-length initiators: INQUIRY byte 36
    bit 0 declares that CDB[5] bit 0x20 on a blind READ bounds the batch
    to the allocation length. Atari SCSIDRV programs a fixed transfer
    length with no residual reporting, so an unbounded stream is fatal
    there. Both dumped ROMs build byte 36 from the enabled/mode bits
    only, so real hardware never sets bit 0, and without the request bit
    every read streams exactly like the ROM. The paired FreeMiNT driver
    (0.90, on my freemint fork) probes the bit and falls back to polled
    single-record i/o on real hardware, PiSCSI, or older firmware.
  • READ STATS returns the real device's 22 bytes with a working
    read-and-clear missed counter; ADD MULTICAST programs the radio
    filter (EtherTalk/mDNS work); SET MAC returns CHECK CONDITION instead
    of silently doing nothing (the radio can't change its receive
    address).
  • INQUIRY reports revision 2.4f: the Mac driver masks interrupts around
    every SCSI poll unless byte 34 is '4' or higher, and it only reads
    the minor digit, so genuine "2.0f" hardware ran masked on every
    driver Dayna ever shipped. 2.4f takes the unmasked path Dayna built
    for firmware 1.4a.
  • Inter-record pacing (75/300 us, matching the real device's timing)
    configurable via bluescsi.ini, plus a WiFiPowerSave option, default
    off.

The ring-full fix and the 48-slot ring from #399 are carried over; the
multi-record read semantics are now the ROM's rather than an
approximation.

Tested in both directions, wire-measured server-side: Atari Falcon
(FreeMiNT), Mac LC475 and SE/30 (7.5.5/Open Transport), Mac Plus
(7.0.1/MacTCP). The protocol core is additionally covered by ~320
host-side unit checks including a loopback test against the FreeMiNT
driver's parser; the test suite lives on a separate branch to keep this
PR to firmware, happy to submit it as a follow-up if wanted.

Rebuilt against a disassembly of the Dayna SL003 v2.0 firmware; ROM
addresses are cited in the source. The protocol core lives in DaynaPort/
next to AmigaWIFI/ and performs no I/O of its own; network.c keeps the
shared receive ring, enqueue, WiFi subcommands and crc32.

- Polled READ(6): one record, live more-pending flag, offset/peek/resume
  per CDB[1..2].
- Blind READ(6): record stream, 200-record cap, terminator only when the
  cap cuts a batch short. An allocation of two or more maximum records
  bounds the whole batch; ALLOC 1524 streams unbounded as before.
- WRITE(6): raw or record stream by CDB[5] bit 7; nonzero CDB[1..2] is
  rejected with INVALID FIELD IN CDB.
- READ STATS: 22 bytes, read-and-clear missed counter.
- INQUIRY byte 36 carries the enabled/mode bits.
- ADD MULTICAST walks the whole list and programs the radio filter.
- SET MAC returns CHECK CONDITION; the radio cannot change its receive
  address.
- Inter-record pacing configurable in bluescsi.ini
  (DaynaPortGapHeaderUs/RecordUs); SE/30 and Plus blind mode needs the
  defaults.

-DBLUESCSI_NETWORK_DEBUG=ON logs batch depth and radio send timing.
Radio power-save (PM2) buffers unsolicited inbound frames at the AP until
the station wakes on a beacon, adding up to a beacon interval of latency
to pings and incoming connections. Default 0 disables it after each
association; set WiFiPowerSave=1 in bluescsi.ini to keep the chip's
default mode for lower power draw. Applied from poll context, since the
ioctl cannot be issued from cyw43's own link-up callback and a fresh
association resets the mode.
On the poll arch nothing services the radio while a SCSI transaction runs
(scsiWrite blocks), so no frame can reach the receive ring mid-transfer
and a blind batch is capped at whatever was queued when it started. With
the threadsafe-background arch the radio runs from a low-priority IRQ and
the ring fills during transactions. Only the Pico_2_DaynaPORT target is
switched; the arch define moves from a global to a per-target setting,
since defining both arch macros compiles both SDK implementations and
they collide at link.

Concurrency consequences:
- The three cyw43 calls that do not take the driver lock themselves
  (wifi_link_status, wifi_scan_active, wifi_get_mac) are bracketed with
  the async-context lock for thread-context callers; the bracket macros
  compile away under the poll arch.
- scsiNetworkEnqueue publishes the packet and size before the write index
  that announces them.
- The missed-packet counter is read and cleared with interrupts off, so a
  drop from the receive IRQ cannot land between the two.
…upts on

Apple's .ENET0 driver masks interrupts at IPL 6 for the whole of every
SCSI poll unless INQUIRY byte 34 -- the third character of the revision
string -- is '4' or higher, meaning firmware 1.4 or later. The comparison
only reads the minor digit, so the real device's "2.0f" is classified as
pre-1.4 and every poll runs masked: TickCount loses ticks during
transfers (rates read up to 2x high on a Plus) and the machine cannot
service anything else. Reporting 2.4f takes the unmasked path Dayna
shipped for firmware 1.4a. The identity string is the one deliberate
departure from the real device's INQUIRY.
INQUIRY byte 36 bit 0 declares that blind READ batches can be bounded.
Both dumped SL003 ROMs build that byte from the enabled and mode bits
only, so real hardware never sets it, and firmware without byte 36
reads as 0 from a zeroed probe buffer.

CDB[5] bit 0x20 on a blind READ requests the bound: the allocation
length becomes a hard ceiling on the batch, checked before each record,
with the more-pending flag never promising a record the batch will not
send. Without the bit every allocation streams unbounded as the ROM
does; the earlier allocation-size heuristic is gone. Hosts must see the
INQUIRY bit before relying on the CDB bit, since the ROM ignores
unknown control-byte bits.

Lets a host driver choose between bounded batches and the polled
single-record reads that fixed-length initiators otherwise need.
Verified on a Falcon: driver 0.90 selects batched i/o on this firmware
and polled i/o on firmware without the bit.
The 1.5 KB landing area made the core's static instance overflow RAM on
the Pico_Audio_SPDIF target (the linker's reserved top-of-RAM region).
The caller supplies the buffer instead; the firmware passes
scsiDev.data, which the pre-rewrite handler already used for DATA OUT
and which is idle whenever the core fetches. Every fetch length is
clamped to SL003_PKT_MAX, well inside that buffer.
The target links network code into RAM (XIP-cache avoidance) and the
SL003 core is larger than the handler it replaced; together with its
audio buffers the target overflowed RAM at link. The receive ring gives
back the difference: 12 slots to 10, ~1.5 KB each. Links with ~3.8 KB
of headroom.
READ CDB[5] bit 0x10 stages a blind batch whole and sends it as one
transfer, never split: a batch that would outgrow the staging area
ends early instead, as blind mode permits, and the rest lead the next
command. Without the bit records go out header-then-payload with the
ROM's 75/300 us gaps, which software-timed blind loops (Plus, SE/30)
depend on.

A blind SCSIExecIO promises the SIM a stream without stalls, and the
Quadra-era SCSI Manager 4.3 SIM fabricates two bytes at any stall the
host did not declare. The pause between two records is such a stall,
so one transfer per record does not suffice; the whole batch must go
in one. Short records make those boundaries frequent, so ACK-heavy
traffic shows it and bulk transfers do not.

Staging uses wbuf, the caller's data-phase scratch, idle during READ.
sl003_init takes its length; the firmware passes the whole SCSI
buffer, 64 KB against a 16 KB bounded budget.

Bounded batches end as blind batches do: a drained queue clears the
last more-flag and sends nothing further; only a batch cut short by
budget or record cap appends a zero-length terminator, as the ROM does
at its cap. Its byte 2 reports queue depth in records, clamped to 255.
Records keep truthful more-flags.

The allocation clips one record, as on the ROM; the batch bound keeps
the total inside it and admits a record only if the terminator still
fits behind it, so a cut batch always closes with one. Sole exception:
a first record that fills the whole allocation, where the batch ends
on that record's more-flag and only the depth hint is lost. Hosts that
want the terminator in that case too allocate one record plus two
headers (1530 or more).

INQUIRY byte 36 bit 0x02 advertises seamless emission, separately from
the bounded bit: a device could honor the bound and ignore the
seamless request, and a 4.3 host that inferred one from the other
would corrupt. Hosts gate the CDB bit on it.

The seamless bit no longer counts as a drop request in polled mode's
CDB[5] test, so peek/resume composes with it; the ROM-defined bits
keep their meaning.
@jcs

jcs commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

+1,331, -278

This seems like a whole lot of churn just to replace something that needed a minor fix with a ton of AI-generated code, but maybe that's where this project has been headed anyway.

@ingpaschke

Copy link
Copy Markdown
Author

Yes, it's a rewrite and a huge change. It would have been easier to just fix a few things and speed up the existing implementation by a comparable amount.

But when I dug further and analyzed the original firmware, it became clear that there are other subtle differences. The one that actually bit people is the more-flag: the existing code answers it from a two-record heuristic instead of from what is really queued, so the host stops with frames waiting or keeps reading past the end. There are more like it, and once I had the ROM decompiled it was feasible to port the exact code over and make the emulation and the original device behave identically instead of approximately.

I also wanted to separate the ported core from the interface, so I moved the SCSI/Link protocol out of network.c into a vendor-neutral core plus a small per-platform sl003_io (emit, fetch, gap, tx). network.c still handles all the WiFi/network plumbing, and there is precedent for that shape: AmigaWIFI already uses network.c the same way.

I use AI tooling for reverse engineering, for review, plumbing and mechanical work. But that doesn't make this AI generated code: the protocol logic is not invented, it is a faithful implementation of Dayna's, this was a lot of work and I did a huge amount of testing against various initiators and platforms.

It is well documented here:

SCSI/Link SL003 ROM RE: https://github.com/ingpaschke/daynaport-scsilink
Protocol description: https://github.com/ingpaschke/scsilink-guide

@erichelgeson

erichelgeson commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Hi there. Sorry for the delay in replying but I'd wanted to get a release out and dig into this massive PR.

Second thanks for trying to help improve BlueSCSI.

In this form I'm not able to merge it and I'd personally like to consider/see the original direction of #399 continue for a few reasons.

  1. We are not a z80 machine so we don't need to try to simulate a z80 rom in c in BlueSCSI - we have the ram/cpu/etc to do it better. It would be much less code to simulate the interface.
  2. The original LLM based RE of the ROM has issues. From singed/unsigned being wrong many places, turning >= into >, etc.
  3. Claim on the one byte in revision being 4 in the version string changes the driver behavior seems odd and would need some proof.
  4. More minor items but going to stop here as they probably wont affect the actual SCSI impl.

After reviewing and thinking about this for a few days there's good details in here from the ROM but needs to be re-evaluated as there's some gaps.

@ingpaschke

Copy link
Copy Markdown
Author

I have never pasted LLM responses here as responses to comments.

@ingpaschke ingpaschke closed this Aug 20, 2026
@erichelgeson

Copy link
Copy Markdown
Contributor

I do apologize, it was just some strong signs - no offense meant. I see you closed this PR but would appreciate any thoughts on my findings above (I could be wrong!) and/or if you'd like to/thoughts on moving forward with the 399 original approach?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants