Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,9 @@ elseif(BLUESCSI_TARGET STREQUAL "Pico_2_DaynaPORT")
BLUESCSI_DAYNAPORT
PICO_CYW43_SUPPORTED=1
CYW43_PIO_CLOCK_DIV_DYNAMIC=1
# RP2350 has RAM headroom for a deeper inbound ring (~1.5 KB/slot);
# a deeper ring absorbs wifi bursts between host READ(6) polls.
NETWORK_PACKET_QUEUE_SIZE=48
)
target_link_libraries(BlueSCSI PRIVATE pico_cyw43_arch_poll pico_async_context_poll)
set_target_properties(BlueSCSI PROPERTIES OUTPUT_NAME "BlueSCSI_Pico_2_DaynaPORT")
Expand Down
195 changes: 132 additions & 63 deletions lib/SCSI2SD/src/firmware/network.c
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ void scsiNetworkWifiJoin(uint32_t size)

int scsiNetworkCommand()
{
int parityError, done, idx, total;
int parityError, done, idx;
long len;
uint32_t size = (scsiDev.cdb[3] << 8) + scsiDev.cdb[4];
uint8_t command = scsiDev.cdb[0];
Expand All @@ -223,22 +223,30 @@ int scsiNetworkCommand()
switch (command) {
case 0x08:
{
// read(6)
// CDB[5] bit 6 indicates transfer mode from the host driver:
// 0x80 (bit 6 clear) = polled mode (old SCSI Manager, VM present) -> single packet
// 0xC0 (bit 6 set) = blind mode (new SCSI Manager, no VM) -> multi-packet OK
// When bit 6 is clear, we send only one packet per READ(6) to minimize SCSI bus
// hold time, allowing the VM pager to access the bus between transactions.
// read(6) - a stream of [len_hi len_lo 0 0 0 flags | payload] records.
// Semantics match the Dayna SCSI/Link SL003 device ROM; hex addresses
// cite v2.0. CDB[5] bit 6 selects the mode (ROM 0x0f3b): clear=polled
// (one record, 0x0d77), set=blind (record loop, 0x0d5c).
int multiPacket = (scsiDev.cdb[5] & 0x40) != 0;
int maxRecords = DAYNAPORT_MAX_PACKETS_PER_READ; // blind only; see network.h
int records = 0;

// Per-record payload budget = allocation length minus the 6-byte header
// (applied per record, not cumulatively: ROM 0x0cf2, 0x0e8a).
int allowance = (int)size - 6;

// Polled read offset into the packet from CDB[1..2] (ROM 0x0e05).
uint32_t offset = multiPacket ? 0 : (((uint32_t)scsiDev.cdb[1] << 8) + scsiDev.cdb[2]);

if (unlikely(size == 1))
{
// Not in the device protocol; kept for hosts that probe with len 1.
scsiDev.status = CHECK_CONDITION;
scsiDev.phase = STATUS;
break;
}

// if we have nothing to send, just return early
// Empty queue: one all-zero record (ROM 0x0f4c).
if (scsiNetworkInboundQueue.readIndex == scsiNetworkInboundQueue.writeIndex)
{
scsiDev.data[0] = 0;
Expand All @@ -261,48 +269,27 @@ int scsiNetworkCommand()

scsiEnterPhase(DATA_IN);

for (done = 0, total = 0; !done; )
for (done = 0; !done; )
{
int sendLen, dequeue;
uint8_t nextRead;
int morePending;

idx = scsiNetworkInboundQueue.readIndex;
len = scsiNetworkInboundQueue.sizes[idx];
total += len + 6; // packet data + 6-byte header

if (scsiNetworkInboundQueue.readIndex == NETWORK_PACKET_QUEUE_SIZE - 1)
{
scsiNetworkInboundQueue.readIndex = 0;
}
else
{
scsiNetworkInboundQueue.readIndex++;
}

done = (scsiNetworkInboundQueue.readIndex == scsiNetworkInboundQueue.writeIndex);

// In polled mode (bit 6 clear), only send one packet per READ(6)
// to avoid holding the SCSI bus while the VM pager needs it.
if (!done && !multiPacket)
{
done = 1;
}

// Don't exceed the transfer size requested by the host
if (!done && total + DAYNAPORT_SCSI_PACKET_MAX + 6 > size)
{
done = 1;
}

// Don't tie up the SCSI bus too long even in multi-packet mode
if (!done && total >= (DAYNAPORT_SCSI_PACKET_MAX + 6) * 2)
{
done = 1;
}
nextRead = (idx == NETWORK_PACKET_QUEUE_SIZE - 1) ? 0 : idx + 1;
morePending = (nextRead != scsiNetworkInboundQueue.writeIndex);
records++;

// Header carries the full queued packet length even on a partial
// read (ROM 0x0d85); flag 0x10 = more pending (ROM 0x0ca6, 0x0df2).
scsiDev.data[0] = (len >> 8) & 0xff;
scsiDev.data[1] = len & 0xff;
scsiDev.data[2] = 0;
scsiDev.data[3] = 0;
scsiDev.data[4] = 0;
scsiDev.data[5] = (done ? 0 : 0x10);
scsiDev.data[5] = morePending ? 0x10 : 0;

scsiWrite(scsiDev.data, 6);
while (!scsiIsWriteFinished(NULL))
Expand All @@ -311,22 +298,91 @@ int scsiNetworkCommand()
}
scsiFinishWrite();

// DaynaPort Mac driver needs a short delay after reading size and flags.
// Timing matches real DaynaPORT SCSI/Link-3 behavior observed on a SCSI bus analyzer.
sleep_us(75);
if (multiPacket)
{
// Blind: send up to the budget, always consume (ROM 0x0d46).
sendLen = (allowance > 0) ? ((len < allowance) ? len : allowance) : 0;
dequeue = 1;
}
else if (allowance <= 0)
{
// Polled header-only peek: no payload, stays queued (ROM 0x0ea0).
sendLen = 0;
dequeue = 0;
}
else
{
int remaining = (int)len - (int)offset;
if (remaining < 0)
{
remaining = 0;
}
if (allowance > remaining)
{
// Read reaches packet end: consume it (ROM 0x0ecf).
sendLen = remaining;
dequeue = 1;
}
else
{
// Partial read: consume only if CDB[5] != 0, else the host
// re-reads the rest at an offset (ROM 0x0ed8).
sendLen = allowance;
dequeue = (scsiDev.cdb[5] != 0);
}
}

if (sendLen > 0)
{
// Header/payload gap matching real SCSI/Link-3 bus-analyzer
// timing (e7d9629). No delay exists in the ROM: this reproduces
// emergent ~6 MHz Z180 DMA overhead, not a documented constant.
sleep_us(75);

scsiWrite(scsiNetworkInboundQueue.packets[idx], len);
while (!scsiIsWriteFinished(NULL))
scsiWrite(scsiNetworkInboundQueue.packets[idx] + offset, sendLen);
while (!scsiIsWriteFinished(NULL))
{
platform_poll();
}
scsiFinishWrite();
}

// Dequeue only after the payload has left the bus, as the device
// frees its NIC buffer post-DMA (ROM 0x0d4c, 0x0f17): the
// platform_poll() above can enqueue concurrently, so releasing
// earlier would let a burst overwrite a slot mid-transfer.
if (dequeue)
{
platform_poll();
scsiNetworkInboundQueue.readIndex = nextRead;
}
scsiFinishWrite();

if (!done)
// Batch only plain blind reads (offset 0, whole packet consumed).
// Polled is always one record with no terminator (ROM 0x0d77);
// field-tested, the Mac polled driver stalls on batched records.
// Peeks, offset/partial reads, and a drained queue also end here
// (drained ends a blind batch with no terminator, ROM 0x0cb0).
if (!multiPacket || !dequeue || offset != 0 || sendLen != len || !morePending)
{
done = 1;
}
else if (records >= maxRecords
|| (long)scsiNetworkInboundQueue.sizes[scsiNetworkInboundQueue.readIndex] > (long)allowance)
{
// DaynaPort Mac driver needs a delay between packets.
// Timing matches real DaynaPORT SCSI/Link-3 behavior observed on a SCSI bus analyzer.
sleep_us(300);
// Cap hit, or next packet exceeds the budget (truncating a
// mid-batch record would corrupt the stream): terminate with
// a zero-length record, host re-reads (ROM 0x0c1c).
memset(scsiDev.data, 0, 6);
scsiWrite(scsiDev.data, 6);
while (!scsiIsWriteFinished(NULL))
{
platform_poll();
}
scsiFinishWrite();
done = 1;
}
else
{
sleep_us(300); // inter-record gap, same basis as above
}
}

Expand All @@ -347,12 +403,22 @@ int scsiNetworkCommand()
break;

case 0x0a:
// write(6)
// write(6) - CDB[5] bit 7 selects the format (ROM 0x1294): clear =
// one raw packet of cdb[3..4] bytes; set = a stream of [len_hi len_lo
// pad pad | payload] records ended by a zero-length header (cdb[3..4]
// ignored). The device rejects a nonzero CDB[1..2] (ROM 0x1281).
if (scsiDev.cdb[1] != 0 || scsiDev.cdb[2] != 0)
{
scsiDev.status = CHECK_CONDITION;
scsiDev.phase = STATUS;
break;
}

scsiEnterPhase(DATA_OUT);

for (;;)
{
if (scsiDev.cdb[5] == 0x0)
if (!(scsiDev.cdb[5] & 0x80))
{
// no preamble, single packet
len = size;
Expand Down Expand Up @@ -390,7 +456,7 @@ int scsiNetworkCommand()

platform_network_send(scsiDev.data, len);

if (scsiDev.cdb[5] == 0x0)
if (!(scsiDev.cdb[5] & 0x80))
{
// single-packet mode
break;
Expand Down Expand Up @@ -490,6 +556,17 @@ int scsiNetworkEnqueue(const uint8_t *buf, size_t len)
return 0;
}

// Full ring: drop this packet. Advancing writeIndex onto readIndex
// would make a full queue look empty (readIndex==writeIndex), losing
// everything already queued.
uint8_t nextWriteIndex = (scsiNetworkInboundQueue.writeIndex == NETWORK_PACKET_QUEUE_SIZE - 1)
? 0 : scsiNetworkInboundQueue.writeIndex + 1;
if (nextWriteIndex == scsiNetworkInboundQueue.readIndex)
{
DBGMSG_F("%s: ring full, dropping incoming packet", __func__);
return 0;
}

memcpy(scsiNetworkInboundQueue.packets[scsiNetworkInboundQueue.writeIndex], buf, len);

if (len < 60) {
Expand All @@ -506,15 +583,7 @@ int scsiNetworkEnqueue(const uint8_t *buf, size_t len)

scsiNetworkInboundQueue.sizes[scsiNetworkInboundQueue.writeIndex] = len + 4;

if (scsiNetworkInboundQueue.writeIndex == NETWORK_PACKET_QUEUE_SIZE - 1)
scsiNetworkInboundQueue.writeIndex = 0;
else
scsiNetworkInboundQueue.writeIndex++;

if (scsiNetworkInboundQueue.writeIndex == scsiNetworkInboundQueue.readIndex)
{
DBGMSG_F("%s: dropping packets in ring, write index caught up to read index", __func__);
}
scsiNetworkInboundQueue.writeIndex = nextWriteIndex;

return 1;
}
Expand Down
11 changes: 11 additions & 0 deletions lib/SCSI2SD/src/firmware/network.h
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,17 @@ extern "C" {
// plus 6-byte DaynaPort SCSI packet header
#define DAYNAPORT_SCSI_PACKET_MAX 1524

// Max records returned by one blind-mode multi-record READ(6). Polled mode
// (cdb[5] bit 6 clear) is always exactly one record, like the real device
// (SL003 ROM 0x0d77): field testing shows the Mac polled driver stalls on
// anything else. The real device's own history says blind batches need
// bounding: its v1.3 firmware loops unbounded (the NIC ring refills from
// the wire during the transfer, so sustained traffic can extend a batch
// indefinitely) and v2.0 added a 200-record cap (ROM 0x0d5c). A long batch
// holds the SCSI bus and delays the host's own outbound WRITE(6) traffic,
// TCP ACKs among it. 16 bounds the hold to ~24 KB / ~25 ms at 1 MB/s.
#define DAYNAPORT_MAX_PACKETS_PER_READ 16 // blind mode (cdb[5] bit 6)

struct scsiNetworkPacketQueue {
uint8_t packets[NETWORK_PACKET_QUEUE_SIZE][NETWORK_PACKET_MAX_SIZE];
uint16_t sizes[NETWORK_PACKET_QUEUE_SIZE];
Expand Down