Repository navigation
Description
Filing so the UCI adapters can align. c64-wireguard found and fixed three handshake defects that all trace to one misunderstanding of the Command Interface. c64-https's adapter shares the ancestry, so it likely shares some of these.
Verified against Gideon's own 6502 client, the Register API doc, and the FPGA source; the fixes are hardware-verified on a U64 Elite (fw 3.15).
The misunderstanding
We treated control bit 1 ($02, which we call UCI_CTRL_NEXT_DATA) as "advance to the next byte". It is not. It is DATA_ACC — the data-accept handshake that ends a transfer. Register API v1.1 §2.4.1, verbatim:
"DATA_ACC: Writing a '1' to this register bit tells the communication layer that all data from the Ultimate was accepted. … Writing to this bit also causes the transfer of the data/status queues to be aborted and reset. Thus, the data response and status response queues will be empty after writing this bit."
Reads advance the pointers by themselves — command_protocol.vhd, on the C64 read strobe:
when c_cif_slot_response => response_pointer <= response_pointer + 1;
when c_cif_slot_status => status_pointer <= status_pointer + 1;The canonical sequence (Gideon's software/6502/unsorted/uci_wedge.s, and every wrapper in the reference C lib) is:
push command -> read response until DATA_AV clears -> read status until STAT_AV clears -> ONE DATA_ACC
Nothing is written to $DF1C between the two reads. Both queues are live simultaneously in the data state, and the single accept releases both.
The three defects
1. Per-byte accept flushes the queue. Our drain loops read a byte then pulsed $02. Because that resets both queues, we captured exactly one byte and discarded the rest. It presented as "the firmware only sends one status character".
2. The accept is mandatory on every transaction — including writes. It returns the state machine to idle. Without it the next PUSH_CMD hits else error_busy <= '1' and is silently dropped. Worse, command-byte writes are not state-gated: a rejected command still advances command_pointer, and only the Ultimate resets it — so one missing accept corrupts every subsequent command. In our tree SOCKET_WRITE returns no response payload, so its path skipped the response drain and its only accept was hiding inside the status drain; removing the per-byte pulse silently removed it. Worth checking your write path specifically.
3. Status read after the accept reads nothing. On most of our paths the response drain ran first and its pulse destroyed the status line before anything read it. Status must be read before the accept.
What we changed
- Removed per-byte
$02writes from both drain loops; reads auto-advance. - Exactly one
uci_ackper transaction, on every exit path (12 paths, 11 sites). - Status drained (and captured) before the accept.
- Added the missing accept to the
SOCKET_WRITEpath.
Hardware-verified: repeated UDP echo round-trips pass, 893-byte reads arrive whole, and the status line now reads in full — 04,DATAGRAM TRUNCATED: 1420 where before we saw a single 0.
Two things worth knowing
The comment is the trap. Ours said UCI_CTRL_NEXT_DATA = $02 ; advance response data FIFO — wrong on both counts, and it propagated. Check uci_regs.inc for the same wording.
A too-long command is silently corrupted. The command buffer has the identical end-of-buffer guard as the response buffer: writing more than 896 command bytes overwrites byte 895 repeatedly and command_length saturates, with no rejection. Relevant to any chunked SOCKET_WRITE.
Also: multi-block reads just landed upstream
GideonZ/1541ultimate#806 (merged 2026-08-27) returns UDP datagrams up to 1472 bytes by splitting the reply across Data More blocks. The gateware always supported this; only the software side was missing. If c64-https ever grows a UDP path, the read cap is no longer 893.
Cross-referenced from GideonZ/1541ultimate#802 and c64-wireguard#46.
Activity
uci: capture the firmware's STATUS line instead of discarding it (#147)
Merge pull request #157 from JC-000/fix/144-drain-resp-data-acc
JC-000 commentedon Sep 5, 2026
Re-verified against
masteratdc06095. All three defects are addressed inthis tree, so this closes with a zero diff. Evidence per item, read out of the
current sources rather than from the PR that claimed them.
Item 1 — per-byte accept flushes the queue
Fixed in
be698e7(PR #157, merged 2026-08-31).uci_drain_respno longerwrites
$02per byte;src/net/uci/uci_cmd.s:452-456carries the reason at thesite. The only
UCI_CTRL_DATA_ACCwrite anywhere in the tree is insideuci_ack:The other three
sta UCI_CONTROLsites inuci_cmd.sareABORT(:71),PUSH_CMD(:291) andCLR_ERR(:330) — none is an accept.Pinned behaviourally by
tools/test_uci_data_acc.py, which executes theshipped
uci_drain_resp/uci_drain_status/uci_ackbytes out of the PRGon a 6502 interpreter against a model of the Command Interface built from the
Register API §2.4.1 quote and
command_protocol.vhd. It was recorded redagainst the pre-fix PRG (
1bb5fad9…, on the "wrote DATA_ACC 1 time(s)"assertion). It runs today:
pytest -q tools/test_uci_data_acc.py→ 3 passedagainst a
BACKEND=uci USE_NISTCURVES_ONCHIP=1build(
5f9e9fb9e70edd2a15ab2249120f161d1826262140fa6ce6f3e01410a876b474). Since#165/
1ace6bca missing UCI build there is a loud failure, not a silent pass,so that green is not vacuous.
Item 2 — the accept is mandatory on every transaction, including writes
net.shas 11jsr uci_acksites, and every one is preceded, in order, byuci_drain_respthenuci_drain_status:The two the issue singles out are the
SOCKET_WRITEpair insidenet_tcp_send::827-831(theUCI_ERR_SEND_FAILpath) and:846-850(the normal path, afteruci_read_resp_bytescollects the 2-byte written count). So the write path hasits own accept and never depended on one hiding inside the status drain — the
specific way removing the per-byte pulse bites, which is why #157 could remove
it safely.
The exits that reach
rtswithout an accept are the wall-clock timeout paths(
@sb_err_drain_to,@sb_ok_drain_to, anduci_push_wait's bail) — C=1 withUCI_ERR_WAIT_TIMEOUTalready set. Those are a wedged FPGA, not a completedtransaction.
Item 3 — status must be read before the accept
Same grep: the order is
drain_resp → drain_status → ackat all 11 sites,never
ackbeforedrain_status.uci_drain_statuscaptures rather thandiscards (#147,
c2e3bf1) intouci_status_buf/uci_status_seen, anduci_cmd.s:28-29states the invariant at the top of the file:"The comment is the trap"
Also closed.
UCI_CTRL_NEXT_DATA ; advance response data FIFO— the exactwording flagged here — is gone;
src/net/uci/uci_regs.inc:51-52now readsUCI_CTRL_DATA_ACC = $02 ; accept + end transfer (resets both queues)with anote naming the old name and this issue.
"A too-long command is silently corrupted"
Not reachable in this tree:
net_tcp_sendchunks atUCI_DATA_QUEUE_MAX = 800(uci_errors.inc:73, applied atnet.s:737-752),below the 896-byte command buffer, so the saturating end-of-buffer guard is
never approached. The read-side cap lore (893, and why never exactly 894) is
recorded at
uci_errors.inc:80-95.One residual, deliberately not held against this issue
#157 halved the inter-access gap in the drain loop (it used to carry two
uci_fences between the response read and the next status read; it now carriesone, matching the hardware-verified
uci_drain_status). That is a registertiming question, not a queue-semantics one — it is none of this issue's three
items, and the FPGA model in
test_uci_data_acc.pycannot speak to it. It isdocumented in #157's own body along with the pass/fail oracle for a hardware
run; if it needs tracking it belongs in a fresh issue against the fence floor,
not here.
Closing.