Repository navigation
Conversation
NativeEthernet's socketSend() busy-waits until the send buffer has room, so a large upload from the C64 stalled loop() inside client.write() until the stack freed space (for good if the stack is polled from loop()). Collect Tx bytes in a 256-byte buffer, send it after a 2 ms pause, after 20 ms or when full, and never write more than availableForWrite(). While the buffer is full Tx-empty stays clear, so the C64 waits as it would on a 6551. One segment per burst instead of one per byte. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
NativeEthernet's client.write() busy-waits for send-buffer space. With the stack polled from loop() (0.9.0.14), a full buffer after a lost segment hung loop() for good: no retransmits, and the upload stalled. firmware/swiftlink-tx-fix.patch (upstream PR SensoriumEmbedded/TeensyROM#80, applied by make_tr_patch.sh after the Kernal Replace fixes) collects Tx bytes in a 256-byte buffer, sends it after a 2 ms pause / 20 ms / when full, and never writes more than availableForWrite(); while it is full Tx-empty stays clear. Works on hardware. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SensoriumEmbedded
left a comment
There was a problem hiding this comment.
Thank you for tracking this down, the tcpdump walk-through made the stall easy to follow, and getting large uploads through the SwiftLink reliably is a real improvement!
I reviewed this merged onto current main (136baa8) and built both targets. As always, feel free to push back on any that are inappropriate or would take significant memory/code to resolve.
Memory: TR+ main grows by 192 B of ITCM (RAM1 padding 27,112 → 26,920) and 256 B of RAM2 for the static SwiftTxOut; DTCM and free-for-locals are unchanged. Plain TR main grows by 256 B of ITCM.
There are 2 (small) things I'd like you to look at before merging, plus 1 optional tweak. Suggestions are in-line:
- Clear
SwiftTxOutwhen the connection changes (IOH_Swiftlink.c:556) - Allocate
SwiftTxOutwith the other SwiftLink buffers instead of as a staticDMAMEMarray (IOH_Swiftlink.c:188) - Optional: send right away when nothing is in flight (
IOH_Swiftlink.c:606)
Hardware testing: There's a lot of tight timing in the SwiftLink implementation, so we'll need to do a little more hardware testing before release:
Your 8 KB uploads over WiFi cover the lossy-path case.
- CCGMS session on a BBS: some typing, plus an XMODEM upload and download, checksums compared
- Your upload program again, on a build of this branch once the changes are in: it's the only test that fills the 256 B buffer, since CCGMS's XMODEM blocks are under 256 bytes
Thank you!
… a connection change, send at once when nothing is in flight Review follow-ups: - SwiftTxOut is malloc'd in InitHndlr_SwiftLink() and freed in FreeSwiftlinkBuffs(), so other modes keep the 256 B of RAM2 - emptied in the connection-change block, so Tx bytes for an old connection cannot reach a new one - sent right away when nothing is unacknowledged (availableForWrite() >= FNET_SOCKET_DEFAULT_SIZE, Nagle's rule) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Thanks for the review! All three are in d42086f. Both targets build. Hardware, on a stock
|
Summary
PollingHndlr_SwiftLink()sends each byte the C64 writes with its ownclient.write(SwiftTxBuf). In NativeEthernet,EthernetClient::write()doesn't return early when the socket's send buffer is full.EthernetClass::socketSend()busy-waits until there is room:
So when a large upload fills the send buffer,
loop()stops insidewrite()until the stack has freed space. Thatmeans waiting for ACKs and, after a lost segment, for the retransmission. While it waits, no polling handler runs:
received data isn't queued, Rx NMIs aren't raised, and so on.
On stock firmware the stack runs from NativeEthernet's 1 ms PIT interrupt, so the wait does end, but the main loop can
stall for a long time. In a build that polls the stack from
loop()instead (the change inteensyrom-vdrive's patches that masks
IRQ_PITwhile KernalReplace/SwiftLink handlers run and calls
fnet_poll()fromloop()), nothing frees the buffer whileloop()waits, soit never returns. That build is how I found it, but the blocking write is upstream code.
A second, smaller problem: because every byte is its own
write(), every byte goes out as its own TCP segment (40bytes of headers per data byte). A few KB become thousands of packets, which fill the send buffer quickly and make it
likely that one of them is lost somewhere on the path.
How it was found
A C64 program sent an 8 KB HTTP POST through the SwiftLink at 38400 baud. It polls Tx-empty before every write, as a
6551 requires. The path was TeensyROM+ Ethernet → Raspberry Pi (NAT) → WiFi → server. Requests failed now and then,
and large ones almost always did. A logging proxy in front of the server received the headers and the first 675 body
bytes, then nothing more for 97 s, with the connection still open.
tcpdumpon the Pi's Ethernet side (the TeensyROM side) of a failing request showed:win 2048, mss 1460.4096 throughout, so the limit was the TeensyROM's own send buffer.
followed (165 in the capture) arrived after it had stopped sending, and none was acted on: no fast retransmit and
no retransmission on timeout, although FNET does both (
FNET_TCP_NUMBER_FOR_FAST_RET) and had retransmitted asegment earlier in the same connection.
So the stack wasn't running. The main loop was stuck in
socketSend()waiting for space that only the stack couldfree, and in this build the stack only ran from that same loop. The C64 side matches: Tx-empty stays clear (the byte
is still pending), so the program waits for it forever.
Fix
In
IOH_Swiftlink.c:write()with more than the stack can take. Bytes from the C64 go into a small buffer(
SwiftTxOut, 256 bytes).SendSwiftTxOut()checksclient.availableForWrite()and writes at most that much, sosocketSend()never waits. Whatever doesn't fit stays in the buffer for a later poll, andloop()keeps running,with the stack and the rest of the handler.
SwiftTxOutis full, the byte inSwiftTxBufisn't taken andSwiftStatusTxEmptystays clear, as on a real 6551 whose line is busy. Programs that check Tx-empty before writingwait and lose nothing. The Tx IRQ also isn't raised until there is room.
20 ms, or when it is full. A burst then goes out in segments of up to 256 bytes instead of one segment per byte,
while a single keystroke is delayed by about 2 ms.
The buffer is emptied when the connection drops and in
InitHndlr_SwiftLink(). The+++guard-time logic isunchanged and still counts bytes as the C64 writes them. Browser mode and the offline/AT command path are unchanged.
Why this fixes the stall: once
write()can't block, a full send buffer only means the C64 waits on Tx-empty. The mainloop keeps going, the stack keeps processing the duplicate ACKs, retransmits the lost segment, frees the buffer, and
the upload continues. With far fewer packets per KB, a loss is also much less likely to happen in the first place.
Testing
loop(), the same 8 KB uploads over the same WiFi path that stalled beforenow complete.
main(npm run build:tr-plus) with only this change. Not run on hardware with stock firmware,where the stack runs from the PIT interrupt; the change doesn't depend on how the stack is driven.
Reproducing
Send a few KB in one burst through the SwiftLink to any TCP server, over a path with some packet loss (WiFi, or
tc qdisc add dev <if> root netem loss 1%on a gateway). Compare the bytes the server receives with the bytes sent,and capture on the TeensyROM side to see the 1-byte segments and the unanswered duplicate ACKs.
🤖 Generated with Claude Code