netboot: actually TRANSMIT the ACK
"timeout mid-transfer (22 seen, 0 bad, 0 lapped, desc@+0=5 +1=17)". Reception now works -- 0 bad, 0 lapped, and the descriptor locator handles both offsets -- but 22 frames seen is almost exactly the sender's 20 retries of block 0. It never saw an ACK and gave up. send_ack() called bba_tx_bytes() and nothing else. That helper only streams bytes into the TXDATA FIFO: it does not write TWD (the frame length) and does not fire the NCRA ST trigger. So every ACK was written into the FIFO and never put on the wire. Entirely my bug -- t_tx(), which is proven on hardware, has done the full sequence all along and I did not follow it. send_ack() now matches t_tx(): clear IR, write TWD_LO/TWD_HI, stream the bytes, then write NCRA SR|ST0. TWD must come BEFORE the stream -- that is what queues the length so TXFrameDrain consumes concurrently with the EXI writes; without it the drain waits for a length that only arrives at the ST trigger and the whole frame must fit the 16-deep FIFO, which is the truncate-at-16-bytes bug. It then waits briefly for TI and counts failures as txto=N in the status line. TI is not proof the frame reached the wire, but its absence is a clear failure. Blocking here costs nothing: stop-and-wait means the host is idle until the ACK lands anyway. Also recorded: the descriptor offset is genuinely MIXED on hardware, +0 for 5 frames and +1 for 17 in one run. A race in the assembler, still unexplained, and not reproducible in simulation -- T2/T5 keep the descriptor at +0 with the write port free-running and stalled 1-in-3. The locator makes the transfer work regardless while the counts stay visible. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -33,6 +33,8 @@ static u8 s_last_desc[4];
|
|||||||
* supposed to put it at +0 always; hardware shows it sometimes landing at +1,
|
* supposed to put it at +0 always; hardware shows it sometimes landing at +1,
|
||||||
* varying frame to frame, so count both rather than assume either. */
|
* varying frame to frame, so count both rather than assume either. */
|
||||||
static u32 s_shift_hits[2];
|
static u32 s_shift_hits[2];
|
||||||
|
/* ACKs where TI never set -- the frame did not reach the W5100. */
|
||||||
|
static u32 s_tx_timeouts;
|
||||||
static u8 s_last_dump[32];
|
static u8 s_last_dump[32];
|
||||||
static u32 s_last_page;
|
static u32 s_last_page;
|
||||||
|
|
||||||
@@ -139,6 +141,8 @@ static int ring_next(u8 *buf, u32 cap)
|
|||||||
static int send_ack(u16 seq)
|
static int send_ack(u16 seq)
|
||||||
{
|
{
|
||||||
u8 f[NB_MIN];
|
u8 f[NB_MIN];
|
||||||
|
int i, ir = 0;
|
||||||
|
|
||||||
memset(f, 0, sizeof(f));
|
memset(f, 0, sizeof(f));
|
||||||
memcpy(f + 0, s_peer_mac, 6);
|
memcpy(f + 0, s_peer_mac, 6);
|
||||||
memcpy(f + 6, s_our_mac, 6);
|
memcpy(f + 6, s_our_mac, 6);
|
||||||
@@ -148,7 +152,37 @@ static int send_ack(u16 seq)
|
|||||||
wr16(f + 20, seq);
|
wr16(f + 20, seq);
|
||||||
wr16(f + 22, 0);
|
wr16(f + 22, 0);
|
||||||
wr16(f + 24, 0);
|
wr16(f + 24, 0);
|
||||||
return bba_tx_bytes(f, sizeof(f));
|
|
||||||
|
/* THE FULL SEQUENCE, matching t_tx() which is proven on hardware.
|
||||||
|
* bba_tx_bytes() only streams bytes into the TXDATA FIFO -- it does NOT
|
||||||
|
* set the length or fire the transmit. An earlier version of this
|
||||||
|
* function called it alone, so every ACK was written into the FIFO and
|
||||||
|
* never put on the wire: the host retried block 0 twenty times, saw no
|
||||||
|
* ACK and gave up, while the GC reported frames arriving normally
|
||||||
|
* ("22 seen, 0 bad" against the sender's 20 retries).
|
||||||
|
*
|
||||||
|
* TWD must be written BEFORE streaming: that is what queues the length
|
||||||
|
* so TXFrameDrain can consume concurrently with the EXI writes. Without
|
||||||
|
* it the drain waits for a length that only arrives at the ST trigger and
|
||||||
|
* the frame has to fit the 16-deep FIFO whole -- the truncate-at-16-bytes
|
||||||
|
* bug. The NCRA ST write afterwards is the fallback trigger. */
|
||||||
|
bba_w8(REG_IR, 0xFF); /* clear TI/RI */
|
||||||
|
bba_w8(REG_TWD_LO, (u8)(sizeof(f) & 0xFF));
|
||||||
|
bba_w8(REG_TWD_HI, (u8)((sizeof(f) >> 8) & 0xFF));
|
||||||
|
if (bba_tx_bytes(f, sizeof(f)) < 0) return -1;
|
||||||
|
bba_w8(REG_NCRA, NCRA_SR | NCRA_ST0);
|
||||||
|
|
||||||
|
/* Wait briefly for TI. Not proof it reached the wire, but its absence
|
||||||
|
* is a clear failure, and blocking here is fine: stop-and-wait means the
|
||||||
|
* host is idle until this ACK lands anyway. */
|
||||||
|
for (i = 0; i < 20; i++) {
|
||||||
|
ir = bba_r8(REG_IR);
|
||||||
|
if (ir >= 0 && (ir & IR_TI))
|
||||||
|
return 0;
|
||||||
|
usleep(500);
|
||||||
|
}
|
||||||
|
s_tx_timeouts++;
|
||||||
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Destination path carried in the DONE frame, so the host decides where the
|
/* Destination path carried in the DONE frame, so the host decides where the
|
||||||
@@ -331,11 +365,12 @@ int netboot_receive(void *dst, u32 maxlen, void (*status)(const char *msg))
|
|||||||
if (idle > (got ? 5000u : 30000u)) {
|
if (idle > (got ? 5000u : 30000u)) {
|
||||||
if (status) {
|
if (status) {
|
||||||
snprintf(line, sizeof(line),
|
snprintf(line, sizeof(line),
|
||||||
"%s (%u seen, %u bad, %u lapped, desc@+0=%u +1=%u)",
|
"%s (%u seen, %u bad, desc+0=%u +1=%u, txto=%u)",
|
||||||
got ? "timeout mid-transfer" : "timeout: no match",
|
got ? "timeout mid-transfer" : "timeout: no match",
|
||||||
(unsigned)seen, (unsigned)bad, (unsigned)lapped,
|
(unsigned)seen, (unsigned)bad,
|
||||||
(unsigned)s_shift_hits[0],
|
(unsigned)s_shift_hits[0],
|
||||||
(unsigned)s_shift_hits[1]);
|
(unsigned)s_shift_hits[1],
|
||||||
|
(unsigned)s_tx_timeouts);
|
||||||
status(line);
|
status(line);
|
||||||
}
|
}
|
||||||
return -3;
|
return -3;
|
||||||
|
|||||||
Reference in New Issue
Block a user