Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
7074d91
Change UCI banner to 'UCI NETWORKING'
JC-000 Apr 16, 2026
6f22c33
Merge pull request #18 from JC-000/fix/uci-banner
JC-000 Apr 17, 2026
5be2a6c
UCI NOP-sled fencing for turbo speed support
JC-000 Apr 16, 2026
a8b63da
Add read-side NOP fencing and turbo timing fixes for 48 MHz support
JC-000 Apr 17, 2026
d952772
Replace NOP-sled fence with delay loop for 48 MHz UCI support
JC-000 Apr 17, 2026
f591f45
Document UCI delay-loop fence and 48 MHz turbo support
JC-000 Apr 17, 2026
3949a89
Merge pull request #20 from JC-000/fix/uci-nop-fencing
JC-000 Apr 17, 2026
1572b3e
Add HTTPS e2e test scaffolding + UCI networking notes
JC-000 Apr 17, 2026
701f547
Merge pull request #21 from JC-000/docs/uci-networking-notes-and-http…
JC-000 Apr 17, 2026
4205e56
Fix UCI net_poll entry gate — wait_not_busy instead of wait_idle
JC-000 Apr 17, 2026
d71972e
Persist UCI debug-stream trace + rotate artifacts
JC-000 Apr 17, 2026
bb89ddc
Dump TLS state + ring contents on _dump_full for stall diagnosis
JC-000 Apr 17, 2026
3fde2a2
Extend TLS state dump with key-schedule inputs + intermediates
JC-000 Apr 18, 2026
e98a3e7
Finalize TLS transcript before deriving handshake + traffic keys
JC-000 Apr 18, 2026
f719f98
Update UCI/HTTPS docs to reflect transcript fix + Certificate stall
JC-000 Apr 18, 2026
bf02de3
Merge pull request #22 from JC-000/fix/uci-tls-handshake-progression
JC-000 Apr 18, 2026
3d4a61e
Eliminate tls_hs_buf staging; parse handshake msgs in tls_rec_buf
JC-000 Apr 18, 2026
4a9f0a5
Document tls_hs_buf removal and new CERT_VERIFY stall
JC-000 Apr 18, 2026
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
86 changes: 79 additions & 7 deletions CLAUDE.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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

Expand All@@ -160,11 +172,32 @@ Scripts under `tools/uci/` require a U64E at 192.168.1.81 and use
- `test_http_local.py` — HTTP GET against a local test server
- `test_http_live.py` — HTTP GET against a real internet host (requires
internet access from the U64E)
- `test_https_local.py` — HTTPS e2e scaffolding against a local TLS 1.3
listener (ECDSA-P256 cert from
`tools/https_e2e/certs/`). DMAs a 6502 stub
that calls `http_get`, flips the U64E to 48
MHz turbo, and captures full diagnostics on
pass or timeout. `DEBUG_CAPTURE=1` enables a
bounded 6510 bus stream for post-mortem.
Each run writes a timestamped artifact dir
under `$UCI_DEBUG_DIR` (default
`/tmp/uci_https_debug/<ISO>/`) containing:
packed raw trace (`trace.bin` + meta sidecar),
derived `summary.txt` / `tail.txt` /
`uci_accesses.txt`, the full 4 KB ring
(`ring.bin` + `ring_meta.json`), a DMA-read
TLS state snapshot (`tls_state_dump.json`),
the listener's `server_result.json`, and
`run_info.txt`. Rotation keeps the last 5
dirs; `UCI_DEBUG_KEEP_ON_PASS=1` preserves
PASS runs. The TLS state snapshot now
includes the full 548 B `tls_rec_buf`
(handshake plaintext is parsed in place
there — see Known issues below for the
current stall site).

### 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
Expand All@@ -174,6 +207,42 @@ 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).
- TLS 1.3 handshake now advances past CERTIFICATE. The 352 B
Certificate record is parsed directly out of `tls_rec_buf`
(the separate 256 B `tls_hs_buf` staging buffer — along with
the three 8-bit copy loops in `src/tls13.s` that populated it,
which had been truncating the Certificate at byte 96 — was
eliminated). Handlers in `src/tls_cert.s`,
`src/tls_handshake.s`, and `src/tls_keyschedule.s` now read
from `tls_rec_buf` / `tls_rec_len` in place; the state machine
enforces that each handler finishes before the next record is
fetched. Current stall is downstream at CERTIFICATE_VERIFY
(`tls_last_state = 0x05`). Two known follow-ups, both
pre-existing, surfaced now that we reach this point:
* ECDSA P-256 `verify` appears not to complete within the
test harness's 120 s deadline at 48 MHz turbo — suspected
wall-clock budget issue, not a correctness bug.
* `tls_transcript_update`'s ingest loop uses an 8-bit
`zp_count`, so handshake messages larger than 256 B (the
352 B Certificate in particular) are only hashed for their
first 96 B. This will break Finished MAC verification once
we clear the CERT_VERIFY stall; needs a 16-bit rewrite.
DHCP and plain HTTP are unaffected at all speeds.

### Design note — bounded timeouts must use wall-clock time

Any future robustness work on the UCI adapter's spin-wait helpers
(`uci_wait_idle`, `uci_push_wait`, etc.) MUST use a wall-clock time
source — CIA timer on stock C64, TOD clock on U64E — rather than a
cycle-counted iteration budget. The fences around every UCI register
access make per-iteration cost scale with CPU speed: a budget that is
ample at 1 MHz collapses to far too short at 48 MHz because turbo
scales CPU cycles but not the FPGA's wire-level operation durations.
A prior attempt on branch `feat/net-drain-abi` split waits into
fast/long tiers with cycle-count budgets and broke DHCP at turbo for
exactly this reason; the branch was abandoned.

## Memory layout

Expand All@@ -192,7 +261,10 @@ TCP ring at $C000.
Tight regions (after Phase 6 fit-up):
- **CRYPTO** is **100%** full. Any new crypto byte requires relocation
or reclamation somewhere.
- **SHADOW_BSS** is **99.8%** full — roughly 20 bytes of slack.
- **SHADOW_BSS** was **99.8%** full after Phase 6; ≈258 B was reclaimed
in the `tls_hs_buf` removal (256 B buffer + 2 B length word), so
there is a bit more slack now. Still the tightest region after
CRYPTO — check the linker map before adding anything sizeable.

There is a known TODO to restructure the MEMORY map so that all
file-backed regions are physically contiguous in a single ROM-like
Expand Down
16 changes: 11 additions & 5 deletions src/data.s
Original file line numberDiff line numberDiff line change
Expand Up@@ -222,11 +222,17 @@ tls_rec_buf: .res 548
.export tls_nonce
tls_nonce: .res 12

; Handshake message buffer
.export tls_hs_buf
.export tls_hs_len
tls_hs_buf: .res 256
tls_hs_len: .res 2
; Handshake messages are parsed and assembled directly in tls_rec_buf /
; tls_rec_len. The previous tls_hs_buf / tls_hs_len copy staging buffer
; was removed: an 8-bit copy loop truncated records >=256 B (e.g. the
; 352 B Certificate), and tls_hs_buf was too small to hold the
; decrypted plaintext anyway. tls_rec_buf is 548 B so every fragment
; that fits in our negotiated max_fragment_length of 512 fits there.
; Invariant: handshake handlers (tls_handle_certificate,
; tls_handle_cert_verify, tls_parse_server_hello, ...) must finish
; reading tls_rec_buf before the next record is fetched. The state
; machine already enforces this (each handler runs synchronously and
; the next-record fetch sits after the handler returns).

; -----------------------------------------------------------------------------
; HKDF buffers
Expand Down
32 changes: 25 additions & 7 deletions src/net/uci/net.s
Original file line numberDiff line numberDiff line change
Expand Up@@ -54,6 +54,7 @@
; --- primitives from uci_cmd.s ---
.import uci_abort
.import uci_wait_idle
.import uci_wait_not_busy
.import uci_begin_cmd
.import uci_put_byte
.import uci_push_wait
Expand DownExpand Up@@ -91,6 +92,7 @@ net_init:
jsr uci_abort

lda UCI_ID
uci_fence ; settle before comparing ID
cmp #UCI_ID_VALUE
beq @present

Expand DownExpand Up@@ -136,7 +138,7 @@ net_poll:
beq @do_poll
rts
@do_poll:
jsr uci_wait_idle
jsr uci_wait_not_busy

lda #UCI_TARGET_NETWORK
jsr uci_begin_cmd
Expand DownExpand Up@@ -175,16 +177,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:
Expand DownExpand Up@@ -234,13 +243,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
Expand All@@ -256,6 +267,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

Expand DownExpand Up@@ -406,13 +418,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

Expand DownExpand Up@@ -531,6 +548,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
Expand DownExpand Up@@ -820,7 +838,7 @@ net_recv_byte:
.segment "RODATA"

net_banner_str:
.byte "ULTIMATE 64 ELITE (UCI)"
.byte "UCI NETWORKING"
.byte $0d, 0

; =============================================================================
Expand Down
Loading