diff --git a/CLAUDE.md b/CLAUDE.md index 4e7aa91..bec3140 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -132,11 +132,23 @@ into `uci_host_buf` (256 bytes in UCI_BSS); `net_tcp_connect` passes it to firmware. Dotted-quad IP literals work because firmware passes them through. -### Firmware quirk — per-byte NEXT_DATA ACK +### Firmware quirk — FPGA register timing (delay-loop fence) -Per-byte `NEXT_DATA` ACK truncates multi-byte responses on the current -U64E firmware revision. The read path uses a tight-poll pattern instead -(read `$DF1E` until `DATA_AV` clears). Documented in `uci_cmd.s`. +The U64E's UCI FPGA needs **~38 us** between consecutive register +accesses regardless of CPU clock speed. At stock 1 MHz the bus cycle +time naturally satisfies this. At turbo speeds (4-48 MHz) the CPU +outruns the FPGA, causing double-latched writes and stale reads that +corrupt the UCI command protocol. + +**Fix:** A nested delay-loop macro `uci_fence` (defined in +`src/net/uci/uci_regs.inc`) is inserted after every read/write to UCI +registers `$DF1C-$DF1F`. Parameters: `UCI_FENCE_OUTER = 5`, +`UCI_FENCE_INNER = 100`, yielding ~2525 cycles (~52 us at 48 MHz, +35% safety margin). 14 bytes per fence site, 24 fence sites total +(11 write + 13 read). At 1 MHz the same loop costs ~2.5 ms per +access — negligible for networking. + +48 MHz turbo is fully supported and verified on real U64E hardware. ### Memory layout under UCI @@ -163,8 +175,6 @@ Scripts under `tools/uci/` require a U64E at 192.168.1.81 and use ### Known issues - - Ring buffer needs explicit zeroing before `http_get_plain` calls - (stale data from auto-init polling). - `http_status` parsing is garbled on large responses because the poll-timeout counter in `http.s` expires before all headers are consumed under UCI's slower `net_poll` round-trip. Body arrives @@ -174,6 +184,8 @@ Scripts under `tools/uci/` require a U64E at 192.168.1.81 and use - Boot banner line 03 still says "rr-net" under ip65 build even though Phase 2 made it backend-aware — this is correct/expected behavior. Under UCI it says "ULTIMATE 64 ELITE (UCI)". + - The delay-loop fence adds ~2.5 ms overhead per UCI register access + at 1 MHz (negligible for networking, but visible in tight loops). ## Memory layout diff --git a/src/net/uci/net.s b/src/net/uci/net.s index 360d82a..8c3eb1e 100644 --- a/src/net/uci/net.s +++ b/src/net/uci/net.s @@ -91,6 +91,7 @@ net_init: jsr uci_abort lda UCI_ID + uci_fence ; settle before comparing ID cmp #UCI_ID_VALUE beq @present @@ -175,16 +176,23 @@ net_poll: ; SMC dst. Loop style matches uci_read_resp_bytes — tight-poll ; DATA_AV and read UCI_RESP_DATA; the firmware FIFO auto-advances ; on read (Phase 2 finding), so NO per-byte NEXT_DATA. + uci_fence ; give firmware time to stage response ldy #$00 @hdr_loop: lda UCI_STATUS + uci_fence ; settle before testing DATA_AV and #UCI_STAT_DATA_AV - beq @hdr_done_short + bne @hdr_got ; branch past trampoline + jmp @hdr_done_short ; long branch: fence too wide for BEQ +@hdr_got: lda UCI_RESP_DATA + uci_fence ; settle before storing header byte sta uci_read_hdr,y iny cpy #2 - bcc @hdr_loop + bcs @hdr_got2 ; branch past trampoline (inverted BCC) + jmp @hdr_loop ; long branch back: fence too wide for BCC +@hdr_got2: jmp @hdr_done @hdr_done_short: @@ -234,13 +242,15 @@ net_poll: bne @not_full lda uci_next_hi cmp tcp_recv_head+1 - beq @done_data ; ring full — drop the rest + bne @not_full + jmp @done_data ; ring full — drop the rest @not_full: ; Wait for DATA_AV — the firmware streams data in bursts; if the ; FIFO drained mid-record we bail (shouldn't happen if firmware ; honored actual_len but we defend anyway). lda UCI_STATUS + uci_fence ; settle before testing DATA_AV and #UCI_STAT_DATA_AV bne @have_byte jmp @done_data @@ -256,6 +266,7 @@ net_poll: sta @rb_store+2 lda UCI_RESP_DATA + uci_fence ; settle before storing data byte @rb_store: sta $ffff ; SMC: patched each byte @@ -406,13 +417,18 @@ net_tcp_connect: ldy #$00 @host_loop: lda uci_host_buf,y - beq @host_done + bne @host_push ; branch past trampoline + jmp @host_done ; long branch: fence too wide for BEQ +@host_push: sta UCI_CMD_DATA + uci_fence ; heavy fence: hostname bytes at 48 MHz iny - bne @host_loop ; bounded by 256 B (and by null before that) + beq @host_done ; Y wrapped to 0 — stop (bounded by 256 B) + jmp @host_loop ; long branch back: fence too wide for BNE @host_done: lda #$00 sta UCI_CMD_DATA ; explicit null terminator + uci_fence jsr uci_push_wait @@ -531,6 +547,7 @@ net_tcp_send: @sb_load: lda $ffff,y ; SMC: source base patched above sta UCI_CMD_DATA + uci_fence ; heavy fence: FIFO overruns at 48 MHz with standard fence iny bne @sb_nohi inc @sb_load+2 ; advance base high byte @@ -820,7 +837,7 @@ net_recv_byte: .segment "RODATA" net_banner_str: - .byte "ULTIMATE 64 ELITE (UCI)" + .byte "UCI NETWORKING" .byte $0d, 0 ; ============================================================================= diff --git a/src/net/uci/uci_cmd.s b/src/net/uci/uci_cmd.s index eb337b3..6ff81eb 100644 --- a/src/net/uci/uci_cmd.s +++ b/src/net/uci/uci_cmd.s @@ -53,6 +53,7 @@ uci_abort: lda #UCI_CTRL_ABORT sta UCI_CONTROL + uci_fence ldx #$20 @spin: dex @@ -67,8 +68,11 @@ uci_abort: ; ============================================================================= uci_wait_idle: lda UCI_STATUS + uci_fence ; settle read before testing bits and #(UCI_STAT_STATE | UCI_STAT_CMD_BUSY) ; $31 - bne uci_wait_idle + beq @idle_done + jmp uci_wait_idle ; long branch: fence too wide for BNE +@idle_done: rts ; ============================================================================= @@ -79,8 +83,11 @@ uci_wait_idle: ; ============================================================================= uci_wait_not_busy: lda UCI_STATUS + uci_fence ; settle read before testing bits and #UCI_STAT_CMD_BUSY - bne uci_wait_not_busy + beq @busy_done + jmp uci_wait_not_busy ; long branch: fence too wide for BNE +@busy_done: rts ; ============================================================================= @@ -91,6 +98,7 @@ uci_wait_not_busy: ; ============================================================================= uci_begin_cmd: sta UCI_CMD_DATA + uci_fence rts ; ============================================================================= @@ -100,15 +108,33 @@ uci_begin_cmd: ; ============================================================================= uci_put_byte: sta UCI_CMD_DATA + uci_fence rts ; ============================================================================= ; uci_push_wait — commit pushed bytes as a command, then wait for CMD_BUSY=0 -; Clobbers: A +; +; At turbo speeds the FPGA may not have latched PUSH_CMD by the time the +; CPU starts polling CMD_BUSY. A plain uci_fence after the write gives only +; ≈ 2 µs at 48 MHz — insufficient for the FPGA to assert CMD_BUSY. We add +; a short delay loop ($40 iterations ≈ 6 µs at 48 MHz, ≈ 300 µs at 1 MHz) +; before polling, ensuring CMD_BUSY has been asserted by the time we check. +; +; Clobbers: A, X ; ============================================================================= uci_push_wait: lda #UCI_CTRL_PUSH_CMD sta UCI_CONTROL + uci_fence + ; Fixed settle delay — at turbo speeds the FPGA may not have + ; latched PUSH_CMD and asserted CMD_BUSY by the time the CPU + ; starts polling. $FF iterations × 5 cycles ≈ 27 µs at 48 MHz, + ; ≈ 1.3 ms at 1 MHz — sufficient for the FPGA to latch the + ; command without using inline NOP fences that bloat code size. + ldx #$FF +@pw_settle: + dex + bne @pw_settle jmp uci_wait_not_busy ; ============================================================================= @@ -118,16 +144,18 @@ uci_push_wait: ; ============================================================================= uci_check_err: lda UCI_STATUS + uci_fence ; settle before testing error bit and #UCI_STAT_ERROR - beq @no_err + bne @has_err + clc + rts +@has_err: ; clear the latched error lda #UCI_CTRL_CLR_ERR sta UCI_CONTROL + uci_fence sec rts -@no_err: - clc - rts ; ============================================================================= ; uci_ack — single NEXT_DATA pulse (advance response/status FIFO by one byte) @@ -136,6 +164,7 @@ uci_check_err: uci_ack: lda #UCI_CTRL_NEXT_DATA sta UCI_CONTROL + uci_fence rts ; ============================================================================= @@ -158,12 +187,10 @@ uci_ack: ; ============================================================================= uci_read_resp_bytes: ; Patch the dst pointer into the STA abs,Y instruction below. - ; The inner loop mirrors the SOCKET_READ read pattern in - ; c64-test-harness/scripts/test_uci_tcp_echo.py (lines ~350-362): - ; tight-poll DATA_AV and read $DF1E directly — the UCI response - ; FIFO auto-advances on read, so no per-byte NEXT_DATA is needed - ; inside the loop. NEXT_DATA acknowledgment happens once at the - ; end via uci_drain_resp / uci_ack. + ; At turbo speeds the firmware may not have staged response data + ; by the time the CPU reaches this point (e.g. TCP_CONNECT takes + ; a full network round-trip). Use a 16-bit spin-wait on DATA_AV + ; so we tolerate up to ~150 ms at 48 MHz without bailing early. lda uci_resp_dst sta @rd_store+1 lda uci_resp_dst+1 @@ -171,11 +198,36 @@ uci_read_resp_bytes: ldy #$00 @rd_loop: cpy uci_resp_max - bcs @rd_done + bcc @rd_not_max + jmp @rd_done +@rd_not_max: + ; 16-bit spin-wait for DATA_AV. ~65536 iterations; at 48 MHz + ; each iteration is ~110 cycles → total ≈ 150 ms, enough for + ; TCP handshakes over a LAN. X is preserved across the wait. + stx @rd_save_x + lda #$00 + sta @rd_ctr_hi + ldx #$00 +@rd_wait: lda UCI_STATUS + uci_fence ; settle before testing DATA_AV and #UCI_STAT_DATA_AV - beq @rd_done + bne @rd_have + dex + beq @rd_xzero + jmp @rd_wait ; long branch: fence too wide for BNE +@rd_xzero: + dec @rd_ctr_hi + beq @rd_timeout + jmp @rd_wait ; long branch: fence too wide for BNE +@rd_timeout: + ; Timeout: DATA_AV never appeared — bail with partial read. + ldx @rd_save_x + jmp @rd_done +@rd_have: + ldx @rd_save_x lda UCI_RESP_DATA + uci_fence ; settle before storing/looping @rd_store: sta $FFFF,y ; SMC: dst low/high patched above iny @@ -183,6 +235,8 @@ uci_read_resp_bytes: @rd_done: sty uci_resp_count rts +@rd_save_x: .byte 0 +@rd_ctr_hi: .byte 0 ; ============================================================================= ; uci_drain_resp — ACK remaining response bytes until DATA_AV is clear. @@ -193,14 +247,17 @@ uci_read_resp_bytes: ; ============================================================================= uci_drain_resp: lda UCI_STATUS + uci_fence ; settle before testing DATA_AV and #UCI_STAT_DATA_AV - beq @drn_done + bne @drn_have + rts +@drn_have: lda UCI_RESP_DATA + uci_fence ; settle before NEXT_DATA write lda #UCI_CTRL_NEXT_DATA sta UCI_CONTROL + uci_fence jmp uci_drain_resp -@drn_done: - rts ; ============================================================================= ; uci_drain_status — ACK remaining status string bytes until STAT_AV is clear. @@ -209,14 +266,17 @@ uci_drain_resp: ; ============================================================================= uci_drain_status: lda UCI_STATUS + uci_fence ; settle before testing STAT_AV and #UCI_STAT_STAT_AV - beq @dst_done + bne @dst_have + rts +@dst_have: lda UCI_STATUS_DATA + uci_fence ; settle before NEXT_DATA write lda #UCI_CTRL_NEXT_DATA sta UCI_CONTROL + uci_fence jmp uci_drain_status -@dst_done: - rts ; ============================================================================= ; Control block for uci_read_resp_bytes — lives in UCI_BSS so no ZP is needed diff --git a/src/net/uci/uci_regs.inc b/src/net/uci/uci_regs.inc index f813fda..b328ea7 100644 --- a/src/net/uci/uci_regs.inc +++ b/src/net/uci/uci_regs.inc @@ -46,6 +46,44 @@ UCI_CTRL_CLR_ERR = $08 ; clear error state ; ============================================================================= UCI_TARGET_NETWORK = $03 ; network stack +; ============================================================================= +; Delay-loop fence macro — insert after every STA or LDA that accesses a UCI +; register ($DF1C-$DF1F), giving the FPGA time to latch writes AND settle +; reads before the CPU acts on the value. +; +; A simple NOP sled can't provide enough wall-clock time at high CPU +; speeds without overflowing the code segment (256 NOPs is the max that +; fits, giving only ~10.7 µs at 48 MHz — insufficient for the ~38 µs +; the FPGA needs). Instead we use a nested delay loop: +; total cycles ≈ OUTER * (INNER * 5 + 5) +; +; Tuned empirically via binary search at 48 MHz: +; OUTER=3 INNER=121 (~1830 cycles, ~38 µs at 48 MHz) = FAIL +; OUTER=3 INNER=122 (~1845 cycles, ~38.4 µs at 48 MHz) = PASS (minimum) +; OUTER=5 INNER=100 (~2525 cycles, ~52 µs at 48 MHz) = chosen (35% margin) +; +; At 1 MHz the overhead is ~2.5 ms per access — acceptable for networking. +; The macro preserves A and X via the stack, costing ~14 bytes per call +; site (vs 256 for the NOP sled that still wasn't enough). +; ============================================================================= +UCI_FENCE_OUTER = 5 ; outer loop iterations +UCI_FENCE_INNER = 100 ; inner loop iterations + +.macro uci_fence + pha ; save A + txa + pha ; save X + ldx #UCI_FENCE_OUTER +: lda #UCI_FENCE_INNER +: sbc #1 + bne :- + dex + bne :-- + pla + tax ; restore X + pla ; restore A +.endmacro + ; ============================================================================= ; Command IDs (issued as the first command byte after selecting the target) ; Phase 2+ will use these; Phase 1b keeps them here purely as equates.