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
$02 writes from both drain loops; reads auto-advance.
- Exactly one
uci_ack per transaction, on every exit path (12 paths, 11 sites).
- Status drained (and captured) before the accept.
- Added the missing accept to the
SOCKET_WRITE path.
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.
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 callUCI_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:Reads advance the pointers by themselves —
command_protocol.vhd, on the C64 read strobe:The canonical sequence (Gideon's
software/6502/unsorted/uci_wedge.s, and every wrapper in the reference C lib) is:Nothing is written to
$DF1Cbetween 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_CMDhitselse error_busy <= '1'and is silently dropped. Worse, command-byte writes are not state-gated: a rejected command still advancescommand_pointer, and only the Ultimate resets it — so one missing accept corrupts every subsequent command. In our treeSOCKET_WRITEreturns 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
$02writes from both drain loops; reads auto-advance.uci_ackper transaction, on every exit path (12 paths, 11 sites).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: 1420where before we saw a single0.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. Checkuci_regs.incfor 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_lengthsaturates, with no rejection. Relevant to any chunkedSOCKET_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 Moreblocks. 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.