network: rewrite the DaynaPORT SL003 emulation from the device ROM - #401
network: rewrite the DaynaPORT SL003 emulation from the device ROM#401ingpaschke wants to merge 8 commits into
Conversation
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.
|
+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. |
|
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 |
|
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.
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. |
|
I have never pasted LLM responses here as responses to comments. |
|
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? |
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
.ENET01.2.2f0 and Dayna's last 1.2.5f1. With the actualfirmware 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 ROMdisassembly, 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.ckeeps the shared queue/wifi plumbing, and AmigaWIFIis untouched next to it.
What it does that #399's version didn't:
more-pending flag, and the stateless peek/resume through CDB[1..2]
the Mac driver uses under VM.
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.
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-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).
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.
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.