Skip to content

UCI networking backend for Ultimate 64 Elite - #16

Merged
JC-000 merged 37 commits into
masterfrom
feat/uci-backend
May 6, 2026
Merged

UCI networking backend for Ultimate 64 Elite#16
JC-000 merged 37 commits into
masterfrom
feat/uci-backend

Conversation

@JC-000

Copy link
Copy Markdown
Owner

Summary

  • Adds a UCI (Ultimate Command Interface) networking backend for the Ultimate 64 Elite, selected via make BACKEND=uci. The existing IP65/RR-Net backend remains the default for original C64 hardware — both coexist indefinitely.
  • Cleans the net ABI leak: callers no longer reference ip65-private symbols (net_set_tcp_dest, ip65_dns_ip_addr). The 12-symbol net_abi.inc contract is now the sole coupling between the networking layer and TLS/HTTP callers.
  • Backend-aware boot banner: ip65 builds show "RR-NET (CS8900A) ETHERNET", UCI builds show "ULTIMATE 64 ELITE (UCI)".
  • HTTP GET verified on real U64E hardware (192.168.1.81):
    • LOCAL test: Python HTTP server on dev LAN, response body "HELLO FROM TEST SERVER" — PASS
    • LIVE test: net_dns_resolve("www.zimmers.net") via firmware DNS, real internet HTTP GET — PASS

Architecture

UCI adapter (src/net/uci/) provides:

  • uci_regs.inc: register equates ($DF1B-$DF1F), status/control bits, network command IDs
  • uci_cmd.s: shared command primitives (wait_idle, begin_cmd, push_wait, check_err, read_resp_bytes, drain, ack) — zero ZP usage, all absolute/SMC
  • net.s: full net_abi implementation — net_init (probe $C9), net_dhcp_acquire (GET_IPADDR), net_dns_resolve (hostname memcpy; firmware resolves DNS inside TCP_CONNECT), net_tcp_connect/send/close, net_poll (SOCKET_READ loop → tcp_recv_buf ring), net_recv_byte (ring drain)

Known issues

  • http_status parsing garbled on large responses (pre-existing http.s poll-timeout issue, not UCI-specific — body arrives correctly)
  • Ring buffer needs explicit zeroing before http_get_plain calls (stale data from auto-init polling)
  • net_tcp_set_recv_cb stubbed (no callers in-tree)
  • Legacy symbol names still exported alongside net_abi.inc names (follow-up cleanup)

Test plan

  • make clean && make — ip65 build still links and produces identical-shape PRG
  • make BACKEND=uci clean && make BACKEND=uci — UCI build links clean
  • tools/uci/boot_check.py — PRG boots on U64E, banner shows UCI text
  • tools/uci/phase2_check.py — net_init + GET_IPADDR, reads 192.168.1.81 via DMA
  • tools/uci/phase3_tcp_echo.py — TCP echo roundtrip through net_* ABI on U64E
  • tools/uci/test_http_local.py — HTTP GET against local test server, body = "HELLO FROM TEST SERVER"
  • tools/uci/test_http_live.py — HTTP GET against www.zimmers.net (real internet)
  • Existing ip65 tests unaffected (test_phase2_http.py still passes on VICE)

🤖 Generated with Claude Code


Originally posted by @JC-000 on 2026-04-16

JC-000and others added 30 commits March 23, 2026 15:39
…erged
The harness wait_for_text() now calls transport.resume() between polls
internally, so the inline polling loops are no longer needed. This
replaces 13 copies of the same ~10-line loop with single wait_for_text()
calls, reducing total code by 120 lines.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…r reuse
The parallel runner had two bugs: (1) as_completed() didn't see futures added
mid-iteration, so only the first N suites were collected, and (2) reusing VICE
instances across suites caused state contamination (HKDF 0/12 on reused workers).
Fix: allocate a fresh VICE instance per suite via run_suite_in_own_instance().
Add all 10 suites (was 5). Add --skip-slow and --seed flags. 193/193 pass.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Fix parallel test runner: all 10 suites, instance-per-suite.
… tests
net_dns_resolve and net_set_tcp_dest both passed A/X parameters through
net_save_zp, which uses X as a loop counter and clobbers both registers.
This caused DNS resolution and TCP destination setup to receive garbage
pointers instead of the caller's intended addresses. Fixed by pushing
A/X to the stack before the ZP save and restoring after.
Added test_dns.py (4 tests) exercising net_dns_resolve over TAP with
dnsmasq, and test_http_integration.py (5 tests) for end-to-end plain
HTTP GET (DNS → TCP → request → response). Both use ViceInstanceManager
with ethernet_mode="rrnet" and run unprivileged (only dnsmasq via sudo).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ion-tests
Fix A/X register clobbering in net.asm, add DNS + HTTP integration tests
Replace baseline fe25519/x25519 with optimized versions from the c64-x25519
performance tuning project, achieving ~30% speedup (12,782 jiffies / 3.6 min
per key generation vs 18,005 baseline).
Optimizations imported:
- REU DMA multiplication tables (128KB REU, 4 cyc/product vs mul_8x8)
- mult66 indirect-indexed quarter-square multiply for fe_sqr
- Self-modifying accumulation addresses in fe_mul/fe_sqr inner loops
- 4x unrolled constant-time fe_cswap (38 cyc/byte vs 49)
- Shift-before-accumulate for fe_sqr cross terms
- mul_by_38 lookup tables for fe_reduce_wide
Key integration fixes:
- Optimization tables (mul_dma, sqtab2, mul38) placed early in data.asm to
stay below $A000 and avoid BASIC ROM shadow region
- BASIC ROM banked out at boot and kept off during runtime (data buffers at
$A000+ need direct RAM access)
- VICE launched with -reu -reusize 512 for all test suites
- Zero page lmul0/lmul1 pointers time-shared with ChaCha20 vars
New files:
- tools/test_x25519.py: 71 unit tests (fe25519 field ops + x25519_clamp +
optional --slow RFC 7748 scalarmult vectors)
- tools/bench_x25519.py: key generation benchmark with jiffy clock timing
and Python X25519 verification
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Import optimized X25519 (~30% faster key generation)
Introduces tools/net_test_env.py with NetworkTestEnv context manager
that handles TAP interface, dnsmasq, and optional HTTPS server lifecycle.
Replaces duplicated inline setup/teardown code across network test files
and guarantees cleanup via __exit__, signal handlers, and atexit.
Migrates test_dns.py as proof-of-concept. Includes 14 unit tests.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add consolidated network test environment
Vendor the c64-test-harness bridge networking scripts (setup + cleanup)
and extend them with dnsmasq (DHCP + DNS overrides). Build a reusable
tools/https_e2e/ library with BridgeEnv context manager, single-VICE
launcher, boot menu helpers, and HTTP listener. Two passing e2e tests
drive the real c64-https binary in VICE over RR-Net at normal speed:
Phase 1 verifies DHCP, Phase 2 verifies plain HTTP GET to a local
listener. HTTPS (Phase 3) to follow.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add end-to-end bridge tests for DHCP and HTTP GET
The original CPU JAM at $4DE0 was caused by ip65 writing DHCP packets on
top of sha256_rotr8 code: c64-https crypto code streamed from $3B28 right
through ip65's BSS window at $4000-$5FFF. Fix the layout by inserting
`* = $6000` before word32.asm so all of word32/chacha/poly/aead/sha256/
hmac_drbg/fe25519/x25519/ecdsa + ecdsa_verify/der/tls_cert/tls_ecdh land
above the BSS. Requires three adjacent compromises to fit in the space
below $7C00:
- Relocate sqtab from the hardcoded $7800/$7A00 equates to labels inside
data.asm BSS, freeing the $7800-$7BFF region
- Stub P-384 ECDSA (src/crypto/ecdsa_verify.asm) to return an error
instead of dispatching. P-384 isn't needed for the self-signed P-256
cert used in tests but MUST be restored before real CA chains. Tech
debt tracked in project_p384_stubbed memory
- Remove the `* = $7C00` barrier now that sqtab is gone
With the layout fix, the CPU JAM is gone entirely and the TLS receive
path progresses far enough to expose several follow-on bugs that a
clean walkthrough diagnosed and fixed in sequence:
- tls_record_io.asm: plaintext/encrypted dispatch was comparing against
TLS_STATE_SERVER_HELLO ($02) but tls_state is already $02 when the
ServerHello arrives, causing the code to try to AEAD-decrypt a plaintext
record. Change to TLS_STATE_ENCRYPTED_EXT ($03)
- net.asm: TCP recv callback had an 8-bit X register wrap when ip65
delivered packets >255 bytes, causing re-reads of source bytes.
Clamp cb_remaining to 255 per invocation
- tls_keyschedule.asm: tls_derive_handshake_keys fell off the end of
its final loop without a clc, leaving the carry flag set from the
last hmac_sha256 call. Caller's `bcs @error` then fired on success.
Add clc before rts
- tls13.asm: tls_ecdh_compute_shared was defined in tls_ecdh.asm but
never called anywhere in the codebase, so tls_shared_secret stayed
zeros. HKDF then derived handshake keys from zeros. Add the jsr call
in tls_recv_server_hello right after tls_parse_server_hello succeeds
- tls_record_io.asm: RFC 8446 §5 says TLS 1.3 clients MUST ignore CCS
records during handshake. Add a retry that skips CCS content type
Rename the demo URL from www.apple.com to www.foo.bar throughout boot.asm,
dnsmasq DNS overrides, HTTPS listener cert subject/SAN, and test screen
text expectations, to avoid any risk of real-world impact if the test
environment escapes the bridge.
Bridge setup/cleanup scripts hardened: no blanket pkill, specific PID
tracking, idempotent re-entry, handling of stale vice_eth rc files and
legacy tap-c64 interfaces from older test harness setups.
New test tests/test_phase3_https.py drives the full Phase 3 e2e flow:
BridgeEnv + HTTPS listener + VICE + DHCP + HTTPS GET. Captures granular
post-mortem diagnostics (tls_state, tls_last_state, tls_recv_progress,
tls_recv_sub_progress, tls_rec_header, tls_rec_buf, tls_hs_buf,
tcp_recv_buf, tls_recv_state/count) via dynamic label lookup from
build/labels.txt so diagnostic addresses track the current build.
Instrumentation added to src/data.asm (tls_last_state, tls_recv_progress,
tls_recv_sub_progress, tls_recv_poll_count) and to tls_recv_server_hello,
tls_record_recv_and_decrypt, tls_recv_record for precise failure-site
identification. These are permanent diagnostic helpers.
Current status: handshake reaches tls_state=$03 (ENCRYPTED_EXT) and dies
trying to receive the first encrypted handshake record (EncryptedExtensions).
Two remaining architectural bugs require multi-file refactoring and are
tracked as follow-on work:
1. tcp_recv_buf is 256 bytes with 8-bit indices. TLS 1.3 Certificate
records (~374 bytes) don't fit. Needs 2KB ring with 16-bit indices
and producer overflow check in net_tcp_recv_cb
2. aead_data_len is 1 byte. Caps AEAD decrypt at 255 bytes, so the
~353-byte Certificate ciphertext body is truncated and tag always
fails. Needs widening to 16-bit with updated ChaCha20/Poly1305 loops
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Two coordinated architectural changes to unblock TLS 1.3 handshake
past the ServerHello state:
**tcp_recv_buf at \$C000, 4096 bytes, 16-bit indices**
The previous 256-byte ring with 8-bit head/tail silently overwrote
unread data whenever ip65 delivered a TCP segment larger than the
empty space — the TLS 1.3 post-ServerHello flight (CCS + Encrypted-
Extensions + Certificate + CertVerify + Finished, ~600-800 bytes)
always overflowed.
Move the buffer to the 4KB free-RAM region at \$C000-\$CFFF (always
RAM on a C64, never ROM-shadowed) via an equate in constants.asm —
zero impact on data.asm BSS footprint. Convert head/tail to 16-bit
words. Add a tcp_recv_overflow sticky flag set by the producer when
it would lap the consumer.
Update all consumer and producer paths in net.asm to use 16-bit
indexing with mask-on-access (\$0FFF = 4095). http.asm feed loop
also updated. Add producer overflow check in net_tcp_recv_cb so
ip65's callback cannot silently corrupt the ring.
**aead_data_len widened to 16-bit**
Was a single byte, which capped AEAD encrypt/decrypt at 255 bytes.
TLS 1.3 Certificate records carry ~350+ bytes of ciphertext, so
decrypt was always truncated and the Poly1305 tag always failed.
Change aead_data_len to !word in data.asm. Update the aead_encrypt /
aead_decrypt / aead_compute_tag paths in crypto/aead.asm to 16-bit
counter semantics (compare-with-ora for zero, decrement with borrow,
length block stored in both bytes of the 64-bit field).
Update chacha20_encrypt block loop to a 16-bit counter over
cc20_remain:cc20_remain_hi.
Update tls_record.asm where tls_enc_aead_len is copied into
aead_data_len — both bytes are stored now.
poly1305_update in poly1305.asm is unchanged because AEAD
exclusively drives Poly1305 via aead_process_padded now; the
standalone poly1305_update path is dead code and doesn't need
widening for this fix.
**Verification**
After this change, Phase 3 test reaches tls_state=\$03
(ENCRYPTED_EXT), confirms the full post-ServerHello server flight
is visible and intact in the ring at \$C000 (ServerHello, CCS,
EncryptedExtensions record 50B, Certificate record 369B),
tcp_recv_head/tail 16-bit values working. No regression on DHCP,
DNS, TCP connect, or ServerHello parse.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds on-screen progress markers at each TLS handshake state transition
in tls_connect plus entry/exit counters on net_poll to definitively
localize the Phase 3 HTTPS stall.
**Markers** (src/tls13.asm + src/boot.asm):
CH/SH/HK1/KEYS/ENC1/RX/GOT2/DEC/PROC/EE/CERT/CV/FIN/CFIN printed on
screen as each step of tls_connect completes. The branch-target
trampoline pattern (`bcc @okn / jmp @error / @okn:`) is used because
the inserted print calls push @error out of bcs range.
**Counters** (src/data.asm + src/net.asm):
net_poll_entry_count and net_poll_return_count 16-bit counters
increment at entry and exit of net_poll. Gap between them = number
of active ip65_process calls currently on the stack.
**Test diagnostics** (tests/test_phase3_https.py):
- Dynamic label lookup via _label_addr() against build/labels.txt so
diagnostic reads always hit the current build regardless of BSS
layout shifts
- File-logged post-mortem at /tmp/c64-https-phase3-diag.log with
explicit flush(), survives hard-kill
- Per-heartbeat sampling of tcp_recv_head, tcp_recv_tail,
net_poll_entry/return_count, printed inline
- CPU register read (PC, SP, A, X, Y) via transport.read_registers()
- Top-of-stack return-address chain with label resolution
- Full dumps of tls_rec_buf, tls_hs_buf, tcp_recv_buf in hex+ASCII
**Conclusion from this instrumentation**: Phase 3 handshake progresses
successfully through CH, SH, HK1, KEYS, ENC1, RX markers — i.e., all
of X25519 keygen, X25519 shared secret, HKDF handshake-key derivation,
and transition into tls_recv_encrypted. It then stalls permanently
while net_poll_entry - net_poll_return = 1 (exactly one ip65_process
call on the stack, never returning) and tcp_recv_tail frozen at
0x0198 (408 bytes) across 1800 seconds / 55+ heartbeats.
The stall is inside ip65 — likely the CS8900a driver or IP/TCP
receive path — triggered by the second TCP segment carrying
Certificate record bytes arriving after the multi-minute X25519
silence. None of the c64-https TLS code has a bug at this point;
the TLS layer never gets a CPU quantum because net_poll doesn't
return.
This commit preserves the instrumentation for future sessions to
investigate the ip65 stall with PC sampling and ip65 code inspection.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Phase 3 WIP: layout fix + TLS receive-path fixes (hits architectural limits)
Re-runs Phase 2 bootstrap on the post-PR-13 base (master 6cf0104),
after hard-resetting refactor/ca65-conversion to include the
memory-layout and TLS receive-path fixes from commits 1c75ed9,
ac57d1f, eab7570.
- Add cfg/c64-https-ip65.cfg (ld65 config with NET/CRYPTO/SHADOW/TCP regions)
- Add cfg/c64-https-uci.cfg placeholder for U64E UCI backend
- Add src/macros.inc, src/crypto_abi.inc, src/net_abi.inc facades
- Add src/extern/{c64-x25519,c64-ChaCha20-Poly1305,c64-nist-curves}/README.md
- Add src/net/{ip65,uci}/README.md
- Add Makefile.ca65 (runs alongside ACME Makefile during refactor)
- Convert src/constants.asm -> src/constants.inc (pilot, pure equates,
with post-fix TCP_RECV_BUF_SIZE = 4096)
- Convert src/entropy.asm -> src/entropy.s (pilot, assembles to entropy.o)
ACME originals preserved; Phase 3 agents handle bulk conversion.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Converts 13 crypto leaf files from ACME to ca65 object format on the
post-PR-13 merged base (master 6cf0104). Each file uses explicit
.export/.import discipline, .segment placement per cfg/c64-https-ip65.cfg,
and .include "constants.inc" for zero-page equates.
Preserves all fixes from commits 1c75ed9, ac57d1f, eab7570 that are now
on master after the PR #13 merge:
- aead.s uses 16-bit aead_data_len counter (ora aead_data_len+1 pattern)
- chacha20.s imports cc20_remain_hi for 16-bit counter
- all crypto files inherit the * = \$6000 anchor placement via cfg
Batch A contents:
- crypto/word32.s — 32-bit word primitives
- crypto/sha256.s — SHA-256 hash (K[64] in CRYPTO_RODATA)
- crypto/chacha20.s — ChaCha20 (cc20_set_* macros ported as ca65 .macro)
- crypto/poly1305.s — Poly1305 MAC (scratch to CRYPTO_BSS)
- crypto/fe25519.s — Field arithmetic mod 2^255-19 (uses fe_* prefix)
- crypto/x25519.s — Curve25519 Montgomery ladder
- crypto/hmac_drbg.s — HMAC-DRBG deterministic RNG
- crypto/aead.s — ChaCha20-Poly1305 AEAD envelope (16-bit counters)
- crypto/ecdsa_fp.s — P-256 field prime arithmetic (owns fp_wide)
- crypto/ecdsa_mod.s — P-256 scalar modular arithmetic (owns fp_r0..r3)
- crypto/ecdsa_curve.s — P-256 curve parameters + helpers
- crypto/ecdsa_points.s— Jacobian point arithmetic
- crypto/ecdsa_verify.s— ECDSA signature verification (P-256; P-384 stubbed
to sec/rts, imports preserved for future restore)
All 13 files assembled via:
ca65 -I src -o build/crypto/<name>.o src/crypto/<name>.s
Cross-file dependencies expressed as .import and resolve at link time
once Batch B (TLS primitives) and Batch D (data.asm/net.asm) complete.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Converts 6 TLS primitive files from ACME to ca65 object format on the
post-PR-13 merged base:
- src/hkdf.s — HKDF (RFC 5869) wrapping HMAC-SHA256
- src/tls_transcript.s — TLS 1.3 handshake transcript hash
- src/tls_ecdh.s — TLS 1.3 X25519 key exchange wrapper
- src/tls_record_io.s — TLS record TCP I/O
- src/tls_record.s — TLS 1.3 record layer framing + AEAD
- src/tls_keyschedule.s — TLS 1.3 HKDF key derivation tree
All 6 files assembled clean on first try via ca65 -I src.
All fixes from commits 1c75ed9, ac57d1f, eab7570 are natively present
in the post-PR-13 source and carried through the conversion verbatim:
- tls_record_io.s: TLS_STATE_ENCRYPTED_EXT (\$03) state check +
CCS (ChangeCipherSpec) filter per RFC 8446 \xA75.
- tls_record.s: 16-bit aead_data_len store sequence in both
tls_record_encrypt and tls_record_decrypt.
- tls_keyschedule.s: clc before rts in tls_derive_handshake_keys
to clear stale carry from hmac_sha256.
Cross-file imports (crypto .o files, data.asm BSS) resolve at link
time once Batches C and D complete.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Converts 5 TLS state-machine and HTTP files from ACME to ca65:
- src/tls13.s — TLS 1.3 state machine and record assembly
- src/tls_handshake.s — ClientHello, ServerHello, finished, sig verify
- src/tls_cert.s — X.509 certificate chain validation
- src/der_decode.s — X.509 ASN.1 DER decoder
- src/http.s — HTTP/1.1 client over TLS (www.foo.bar)
All 5 files assemble clean via ca65 -I src. Cross-file imports to
data.asm BSS, net.asm, and src/net/ip65/*.s remain unresolved until
Batch D converts the remaining glue files.
Three of the five agents hit API 500s returning their summary reports
but had already completed the source conversions. A recovery agent
verified all five .s files assemble cleanly before this commit.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Converts the last 4 ACME files to ca65 and introduces the ip65
backend infrastructure under src/net/ip65/:
- src/data.s — program-wide BSS + initialized tables
- src/boot.s — startup, BASIC stub, screen output,
REU multiply support, TLS state markers
- src/main.s — orchestrator shell (was 89 lines of
!source/!binary directives; now ~zero
code since each .s is its own TU)
- src/net/ip65/net.s — ip65/RR-Net networking backend
(relocated from src/net.asm)
- src/net/ip65/ip65_blob.s — .incbin wrapper placing the pre-built
ip65-c64.bin at \$2000 via NET_CODE segment
- src/net/ip65/ip65_symbols.inc — ip65 jump-table + variable-table equates
sourced from ip65-build/ip65-c64.map
All 7 files assemble clean via ca65 -I src -I src/net/ip65.
Load-bearing fixes preserved from PR #13:
- net.s: cb_remaining clamp-to-255 in net_tcp_recv_cb (1c75ed9)
- net.s: ZP \$02-\$1B save/restore around every ip65 call site
- data.s: 4KB tcp_recv_buf, 16-bit aead_data_len (ac57d1f)
- boot.s: all 15 TLS state-transition screen markers (eab7570)
ip65_symbols.inc is guarded with .ifndef ip65_base so it co-exists
with the legacy ip65 equates still in constants.inc. Phase 7 will
consolidate them.
Phase 3 structurally complete. Cross-file imports (data.s BSS
producer, crypto .o files, TLS .o files) all resolve via ca65 object
discipline; final ld65 link is Phase 4.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Resolves all ld65 link errors by restructuring the memory layout
and adding two new segments. Produces a clean 27812-byte .prg.
cfg/c64-https-ip65.cfg:
- Drop 'define = yes' from SEGMENTS entries (was duplicating
__NET_CODE_SIZE__ etc. against the MEMORY-side defines, causing
ld65 abort)
- Route RODATA segment to CRYPTO region (was LOADER; reclaims
~1.9 KB of LOADER headroom)
- Add TLS_CODE segment, mapped to CRYPTO region, so large TLS
object files can opt out of the tight LOADER region
- Add TABLES_BSS segment with align=\$100, mapped to CRYPTO, for
x25519 multiplication tables that must live below \$A000
- Mark ZP_SHARED, ZP_WIDE, LOADADDR, NET_BSS, TCP_RECV_BUF as
optional (silences warnings about empty segments)
src/data.s:
- Move mul_dma_lo, mul_dma_hi, sqtab_lo, sqtab_hi into TABLES_BSS
segment so they land below \$A000 (x25519 optimization requires
it per project_x25519_optimization memory)
- mul38_lo_tab / mul38_hi_tab stay in RODATA (now routed to CRYPTO
via cfg, still below \$A000)
src/tls_keyschedule.s, src/tls_cert.s:
- Change top-level .segment from "CODE" to "TLS_CODE" so these
large TLS state-machine and certificate files load into CRYPTO
region instead of the tight LOADER region
Final region utilization:
LOADER 99% (49 B free)
NET_CODE 84% (1241 B free)
CRYPTO 100% (0 B free — packed with RODATA + CRYPTO_CODE +
CRYPTO_RODATA + TLS_CODE + TABLES_BSS)
SHADOW_BSS 99% (20 B free)
Tables verified below \$A000 via build/labels.txt:
mul_dma_lo = \$9A00
mul_dma_hi = \$9B00
sqtab_lo = \$9C00
sqtab_hi = \$9E00
tls_handshake.s was NOT moved to TLS_CODE because doing so would
overflow CRYPTO by 669 bytes. Memory budget is now tight — future
additions (P-384 restore, sibling crypto vendor) will need more
headroom before they can land.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Resolves three structural problems in the ca65 build output that
would prevent the PRG from loading in VICE or being consumable by
the test harness.
1. Missing PRG load-address header.
Add src/loadaddr.s: a 2-byte .word \$0801 in the LOADADDR segment.
Remove 'optional = yes' from the LOADADDR segment in cfg so ld65
requires it to be populated.
2. No inter-segment padding.
Add 'fill = yes, fillval = \$00' to the LOADER, NET_CODE, and
CRYPTO MEMORY regions in cfg/c64-https-ip65.cfg. Without this,
ld65 packed segments immediately after each other in the file,
so NET_CODE landed at file offset \$17CD instead of \$1801 and
the ip65 blob loaded 50 bytes too early at runtime.
3. Label file format mismatch.
ca65 ld65 -Ln emits 'al XXXXXX .name' (no 'C:' prefix).
The test harness Labels parser requires 'al C:XXXX .name'.
Add a post-link sed pass in Makefile.ca65 'link' target:
sed -i 's/^al 00\\([0-9a-fA-F]\\{4\\}\\) /al C:\\1 /' build/labels.txt
Fixes Labels.from_file() parsing (was 0 entries, now 535).
4. Missing exports for source-level equates.
Three symbols the harness looks up were defined as bare '='
equates in .inc files, so they never appeared in the linker
symbol table: tcp_recv_buf (constants.inc), ip65_init and
ip65_process (ip65_symbols.inc).
Add src/exports.s — a single-TU stub that .include's the
relevant .inc files once and issues explicit .export directives.
Avoids duplicate-symbol errors from putting .export directly in
.inc files that are included from many TUs.
Results:
- build/c64-https.prg: 30721 bytes (up from 27812), valid PRG
header \$01 \$08
- ip65 blob at file offset \$1801 byte-identical to ACME baseline
- build/labels.txt: 535 VICE-format entries, including tcp_recv_buf
(\$C000), ip65_init (\$2000), ip65_process (\$2003)
- Clean ld65 link, no warnings
Delta vs ACME baseline (45900 bytes): ca65 is ~15 KB smaller
because ACME emitted the SHADOW_BSS region into the file as
pre-zeroed data; ca65 correctly treats it as bss. Whether this
matters depends on whether C64 startup zeros its own BSS. Phase 6
VICE smoke test will verify runtime behavior.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Three compounding defects in the ca65 port of the boot path caused
c64-https.prg to return to BASIC READY. without displaying the main
menu. All three are fixed here.
1. BASIC stub SYS target was off by 3.
The stub at $0801 said 'SYS 2064' ($0810), but start: is at
$080D (2061) in both the ACME and ca65 builds. $0810 is
mid-instruction, so SYS 2064 executed garbage on 6502 NMOS and
skipped the bank-switch (LDA $01 / AND #$FE / STA $01) at
$080D-$0812 that maps out BASIC ROM. The ACME build happened to
survive this because none of its BSS sat under ROM, but the
ca65 build placed crypto BSS in SHADOW_BSS ($A000-$BFFF) which
is under BASIC ROM when bank-switch is skipped.
Fix: change the stub text from '2064' to '2061' in src/boot.s.
2. NET_BSS memory gap flattened the file into the wrong addresses.
cfg had:
LOADER $0801-$1FFF file=%O
NET_CODE $2000-$3FFF file=%O
NET_BSS $4000-$5FFF (no file=)
CRYPTO $6000-$9FFF file=%O
ld65 packed the file-backed regions contiguously, skipping the
8 KB NET_BSS hole. But a PRG has a single load address and the
KERNAL LOADs contiguously, so CRYPTO bytes intended for
$6000-$9FFF were loaded physically into $4000-$7FFF. Functions
like drbg_init_entropy, linked at $8243, ended up at $6243 and
calls to them jumped into uninitialized RAM.
Fix: add file=%O, fill=yes, fillval=$00 to the NET_BSS MEMORY
region so ld65 emits 8 KB of zeros in the PRG for the gap. PRG
size grows from 30721 to 38913 bytes.
3. SHADOW_BSS not zeroed on boot.
The C64 KERNAL does not zero BSS on PRG load. net_initialized
at $A000 held powered-on RAM garbage ($55 in test runs), so the
main_loop first-run guard misdispatched. Other crypto BSS
(drbg_seed, hmac_key, sha256_block) also started non-zero.
Fix: add a 20-byte zero loop at the top of start: that clears
$A000-$BFFF before any init runs. Uses self-modifying
sta $A000,y page-walking, preserving registers X/Y minimally.
Verification:
- ca65 build boots cleanly in VICE, banner and main menu appear.
- tools/test_entropy.py passes 7/7 (SID noise, CIA1 timer, non-zero
drbg_seed, DRBG output entropy, reseed changes output).
Known TODO:
- The 'SYS 2064' typo exists verbatim in master's src/boot.asm too.
ACME survives it by luck of BSS placement. Should be fixed on
master before the ACME tree is retired, to avoid confusing any
future reader.
- The NET_BSS file-fill adds 8 KB of zeros to the PRG image. A
cleaner Phase 7 fix would restructure the MEMORY map so all
file-backed regions are physically contiguous (move all BSS to
the end above CRYPTO).
- TCP_RECV_BUF at $C000-$CFFF is not currently zeroed; if the HTTP
GET path assumes an empty ring, add it to the start: zero loop
or make it part of SHADOW_BSS zeroing.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The ACME-era top-level Makefile is gone. Makefile.ca65 has been renamed
to Makefile and extended with the targets from the old file (run, ip65
libs, ip65 blob) so the default `make` invocation drives the ca65/ld65
toolchain. ACME is no longer required to build this project.
New canonical targets:
make build/c64-https.prg + build/labels.txt
make clean remove build/
make run autostart PRG in VICE
make ip65-libs rebuild ip65 object libraries from the submodule
make ip65-blob rebuild ip65-build/ip65-c64.bin
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Test scripts previously ran `make clean && make` unconditionally at
startup. Phase 6 worked around that with an external make shim while
the ca65 build was still being stabilised. Now that the canonical build
is ca65 and safe to reuse, callers that have already built can set
C64_SKIP_BUILD=1 to skip the make invocation entirely.
This is opt-in — with C64_SKIP_BUILD unset, every test still performs
its own clean + rebuild, so the normal path is unchanged.
No shared helper was patched because each test script inlines its own
build block; the 7 sweep-list scripts are edited directly:
test_entropy, test_hkdf, test_chained_hmac, test_keyschedule_steps,
test_tls_handshake, test_http, test_x509.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
src/net/ip65/ip65_symbols.inc is now the single source of truth for
all ip65_* equates (ZP overlap zone, jump-table offsets, variable
table, direct map addresses). The legacy ACME-era copy in
src/constants.inc has been removed and the `.ifndef` guard dropped
from ip65_symbols.inc.
Files that previously got ip65_* symbols transitively through
constants.inc now `.include "ip65_symbols.inc"` directly:
- src/boot.s (boot phase DNS wait)
- src/http.s (DNS response read-out)
- src/exports.s (promotes ip65_init / ip65_process for labels.txt)
Verified by clean rebuild + tools/test_entropy.py (7/7 pass, both
with and without C64_SKIP_BUILD=1).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Covers:
- Build section: make targets, BACKEND variable, C64_SKIP_BUILD
- Crypto ABI section: public symbols, MEMORY requirements for a
drop-in sibling library, mapping to c64-x25519 /
c64-ChaCha20-Poly1305 / c64-nist-curves
- Networking backend ABI section: net_abi.inc, ip65 vs uci backends,
BACKEND=ip65|uci selection
- Memory layout section: cfg region map, tight CRYPTO/SHADOW_BSS
regions, intentional loadaddr / exports stubs
- Smoke test section: the 7 passing scripts and the known ip65
upstream blocker for end-to-end HTTPS
No code changes.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
These were historically committed under the ACME build. The ca65 build
produces different output and build/ is already in .gitignore. Untrack
them so `make clean && make` leaves a clean working tree.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The Phase 6 boot-regression debug (PR #14) corrected the BASIC stub
from 'SYS 2064' to 'SYS 2061' because $0810 is mid-instruction and
the bank-switch at $080D-$0812 was being skipped. The emitted bytes
were updated but the section header comment still read
'; BASIC stub: 10 SYS 2064', which is now misleading.
This commit aligns the comment with the actual stub bytes.
(Historical note: ACME's 'SYS 2064' was never right - it pointed
mid-instruction on 6502 NMOS and the LDA #$FE / AND $01 / STA $01
bank-switch was skipped. The ACME build only survived this by
placing all BSS below $A000 so no code read through the BASIC ROM
shadow. ca65's cleaner layout put SHADOW_BSS at $A000-$BFFF, which
surfaced the bug immediately.)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
JC-000and others added 7 commits April 15, 2026 18:43
Fix stale SYS 2064 comment in boot.s
Callers used to reach into the ip65 adapter directly via
net_set_tcp_dest + ip65_dns_ip_addr, which meant the net_abi.inc
contract was a lie: BACKEND=ip65 was baked into boot.s and http.s
through an .include and three call sites.
Move the resolved-IP -> tcp_dest handoff inside the ip65 adapter
(net_tcp_connect now calls ip65_set_tcp_dest with ip65_dns_ip_addr
while ZP is already saved), drop net_set_tcp_dest entirely, and
remove ip65_symbols.inc includes from the call sites. Split the
ip65_init/ip65_process linker exports into src/net/ip65/exports.s
so they only exist under BACKEND=ip65.
Preparing the link surface for the upcoming UCI backend; ip65 VICE
regression (tests/test_phase2_http.py) still passes.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Every net_abi.inc symbol is an RTS stub. No networking behavior yet
— net_init/net_dhcp_acquire return success, connect/send/dns_resolve
return failure, net_poll is empty. The goal for this commit is just
clean linkage and a bootable PRG on real hardware.
- cfg/c64-https-uci.cfg: mirror the ip65 memory map, carry NET_CODE
as zero-fill for layout parity, reclaim NET_BSS for UCI-owned BSS,
reserve uci_host_buf (256 B) for Phase 4.
- src/net/uci/uci_regs.inc: UCI register, status, control, command
equates (no code references yet).
- src/net/uci/net.s: RTS-stub adapter exporting the full net_abi.inc
contract plus the four legacy names boot.s/http.s/tls_record_io.s
still import (net_dhcp, net_print_ip, net_recv_byte, net_send_len);
finishing the net_abi cleanup is a follow-up.
- Makefile: per-backend source lists; BACKEND=uci no longer depends
on the ip65 blob target.
- tools/uci/boot_check.py: DeviceLock + Ultimate64Client upload +
screen RAM read to verify the PRG boots on 192.168.1.81.
Verified: ip65 build unchanged (byte-identical shape); uci build
links clean; U64E boots to the banner.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Phase 2 puts real behavior behind net_init, net_dhcp_acquire, and
net_local_ip, built on shared UCI command primitives that the next
phases will reuse.
- src/net/uci/uci_cmd.s: shared primitives (abort, wait_idle,
wait_not_busy, begin_cmd, put_byte, push_wait, check_err,
read_resp_bytes, drain_resp, drain_status, ack) + an 8-byte
control block for the read-pointer in UCI_BSS. Zero ZP usage.
- src/net/uci/uci_errors.inc: backend error enum for net_last_error.
- src/net/uci/net.s: net_init probes $DF1D for $C9;
net_dhcp_acquire issues GET_IPADDR, reads 12 bytes (IP +
netmask + gateway), writes the IP into net_local_ip, fails
on all-zero. Local net_print_ip dotted-quad helper (no ip65
dependency). net_banner_str = "ULTIMATE 64 ELITE (UCI)".
- src/net/ip65/net_banner.s: tiny module publishing the ip65
net_banner_str = "RR-NET (CS8900A) ETHERNET".
- src/boot.s: imports net_banner_str and prints it as part of the
boot banner in place of the old hardcoded ethernet line. Also
calls do_net_init automatically during startup so net_local_ip
is populated before the menu paints.
- Makefile: wires the new sources into the per-backend lists.
- tools/uci/phase2_check.py: enable_uci, reset, upload, decode
screen RAM, label-lookup + DMA-read net_local_ip, assert the
banner is backend-aware and the IP is a plausible private-range
address. Disables UCI in finally.
Verified on real U64E at 192.168.1.81: banner prints correctly,
DHCP status reports "ip: 192.168.1.81", net_local_ip reads back
as c0a80151 via DMA.
Firmware note documented inline in uci_cmd.s: pulsing NEXT_DATA
between bytes of a multi-byte response truncates the response on
this firmware revision. The read path uses the tight-poll pattern
from the c64-test-harness SOCKET_READ reference instead.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Implements the complete UCI TCP lifecycle on top of the shared
command primitives from Phase 2. Echo-roundtrip verified on real
U64E at 192.168.1.81 against a Python echo server on the dev LAN.
net_dns_resolve: memcpy hostname into uci_host_buf (256B). No
wire I/O — UCI firmware resolves DNS internally during TCP_CONNECT.
net_tcp_connect: builds TCP_CONNECT command with port (LE) +
uci_host_buf hostname + null terminator, parses socket_id from the
1-byte response. On firmware error "UNRESOLVED HOST" or similar,
surfaces as UCI_ERR_CONNECT_FAIL in net_last_error.
net_tcp_send: chunked SOCKET_WRITE with 800-byte cap per push
(UCI DATA_QUEUE_MAX = 896). 16-bit length loop. Parses the 2-byte
written-count response.
net_poll: if tcp_state == CONNECTED, issues SOCKET_READ with a
512-byte cap. Reads actual_len (2 bytes LE) then actual_len data
bytes directly into tcp_recv_buf ring at $C000 via SMC STA,
mirroring the ip65 adapter's ring-write pattern. Updates
tcp_recv_tail (16-bit, masked with TCP_RECV_MASK). Drain + ack.
net_recv_byte: ring drain (head vs tail compare, masked read,
head advance). Identical logic to ip65 adapter.
net_tcp_close: best-effort SOCKET_CLOSE, clears tcp_state.
net_tcp_set_recv_cb: RTS stub — no callers in-tree.
UCI_CODE segment mapped into NET_CODE memory region ($2000-$3FFF)
to avoid LOADER overflow. Error constants and TCP state enum added
to uci_errors.inc.
tools/uci/phase3_tcp_echo.py: end-to-end test exercising the full
net_* ABI via DMA-injected routine on U64E.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Two HTTP GET tests exercising the full code path (http.s → UCI
adapter → firmware → real network) on U64E at 192.168.1.81:
LOCAL: Python HTTP server on dev LAN IP:8080 responds with
"HELLO FROM TEST SERVER". C64 sends GET / HTTP/1.1, receives
status 200 and the expected body. Ring drains cleanly.
LIVE: net_dns_resolve("www.zimmers.net") → firmware DNS → real
TCP connect to the internet → GET / HTTP/1.1 → receives real
HTML from zimmers.net. Body arrives correctly; http_status
parse is garbled (pre-existing http.s timeout issue — the poll
counter expires before all headers are consumed under UCI's
slower net_poll round-trip, so the parser enters body state
mid-stream; not a UCI adapter bug).
No source changes were needed beyond Phase 3 — the net ABI
holds cleanly through the full HTTP path.
Note: both tests zero tcp_recv_head/tail before calling
http_get_plain to avoid stale ring data from auto-init polling.
A future cleanup should add ring reset to http_get_plain itself.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add a "UCI backend" section covering: register map, command
primitives, DNS (firmware-handled), firmware NEXT_DATA quirk,
memory layout under BACKEND=uci, test scripts in tools/uci/,
and known issues. Update the networking-backend-ABI section to
reflect that UCI is now a working backend, not a placeholder.
Add a pointer to tools/uci/ in the smoke-tests section.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@JC-000