Skip to content

firmware: stop static-IP Ethernet init hanging when the link is down - #94

Open
wesbiggs wants to merge 3 commits into
SensoriumEmbedded:mainfrom
wesbiggs:firmware/eth-static-link-timeout
Open

wesbiggs wants to merge 3 commits into
SensoriumEmbedded:mainfrom
wesbiggs:firmware/eth-static-link-timeout

Conversation

@wesbiggs

@wesbiggs wesbiggs commented Nov 10, 2026 •

Copy link
Copy Markdown
Contributor

With a static IP configured and TCP listen enabled at power-up, a board with no Ethernet link (cable unplugged, dead switch port) hung at boot. ATC in SwiftLink hung the same way.

The cause is in Teensyduino's NativeEthernet: its static-IP Ethernet.begin(mac, ip, dns, gw, mask) ends in while(!link_status){}, with no timeout. DHCP mode was already bounded by AT+DHCPTIME.

EthBeginStatic(), added to both the main image (IOH_Swiftlink.c) and MinimalBoot/VMBoot (Min_ServiceTCP.ino), now:

  1. Brings the stack up through the DHCP begin() with a zero timeout. That starts the link poll and returns at once.
  2. Sets the static address, mask, gateway and DNS through the same fnet calls the static begin() makes. With an address already on the interface, the library's link-up callback doesn't start DHCP, so a cable plugged in after a timeout still comes up on the static IP rather than a lease.
  3. Waits up to 5 s for the link, then makes the static begin(), which returns at once.

With no link, it reports "Ethernet cable is not connected." and returns failure. The callers already handle failure by leaving TCP listen off.

The link wait depends on the stack's poll running, as the DHCP begin() already does. While Kernal Replace holds the poll in loop() (#85), the only callers that can run are SwiftLink AT commands, and the SwiftLink dispatch unmasks the PIT around them.

The branch also queues a related problem in Known-Issues.md. Switching from static to DHCP without a reboot leaves the DHCP init waiting out its timeout, because the library only starts its DHCP client on a link-up while the address is 0.0.0.0. This already happens on main after a successful static init; this change extends it to one that timed out.

Host build only, not hardware tested.

Memory impact

From CI: base is main at ac24701, head is this PR at 990d770. Figures are in bytes. RAM1 variables are unchanged on every image, and the TR+ extension images are unchanged on every figure.

TR+ (Fab 0.4)

Image Metric Base Head Change
MinimalBoot RAM1 code 127560 127560 0
MinimalBoot RAM1 padding 3512 3512 0
MinimalBoot Free for local variables 27068 27068 0
MinimalBoot RAM2 variables 17600 17600 0
MinimalBoot Flash code 136308 136508 +200
Main RAM1 code 203576 203576 0
Main RAM1 padding 25800 25800 0
Main Free for local variables 79868 79868 0
Main RAM2 variables 17600 17600 0
Main Flash code 285884 286060 +176

TR (Fab 0.2/0.3)

Image Metric Base Head Change
MinimalBoot RAM1 code 126136 126136 0
MinimalBoot RAM1 padding 4936 4936 0
MinimalBoot Free for local variables 29116 29116 0
MinimalBoot RAM2 variables 17600 17600 0
MinimalBoot Flash code 132556 132764 +208
Main RAM1 code 190472 190536 +64
Main RAM1 padding 6136 6072 −64
Main Free for local variables 122748 122748 0
Main RAM2 variables 17600 17600 0
Main Flash code 252940 253244 +304

The new functions are FLASHMEM. On the plain TR's main image, the 64 bytes of RAM1 code come from an inlining shift elsewhere in the same translation unit. A symbol diff of the two ELFs shows LoadFile() (DriveDirLoad.ino, never FLASHMEM) inlining one more File::close() call, so it grows 32 bytes, and an out-of-line copy of MouseController::mouseDataClear() (20 bytes) dropping out. Padding absorbs the growth, and free for local variables is unchanged. LoadFile() is already on the FLASHMEM audit list in Known-Issues.md.

🤖 Generated with Claude Code

wesbiggs and others added 3 commits November 9, 2026 23:52
NativeEthernet's static-IP begin() finishes with an unbounded spin on the
link status. With TCP listen enabled at power-up, a static IP and no
cable (or a dead port), boot hung in MinimalBoot, the extension host and
the main image, and ATC hung the Swiftlink session the same way. The DHCP
path was already bounded by its own timeout.

Bring the stack up first through the DHCP begin() with a zero timeout,
which starts the link poll and returns at once, wait up to 5 s for the
link, and only then make the static begin(). No link reports the cable
as not connected and returns failure, which the callers already handle
by leaving TCP listen off.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…k timeout

After the bounded link wait gives up, the network stack is already running
with no address. NativeEthernet's link-up callback starts a DHCP client on
an interface still at 0.0.0.0, so a cable plugged in later left the board
on a DHCP lease (or no address), and EthernetInit then reported it
connected without applying the configured static IP.

Set the static address, mask, gateway and DNS before the wait, through the
same fnet calls the static begin() makes, so a later link-up has no reason
to start DHCP. Calling fnet directly rather than through the Ethernet
setters avoids linking those library wrappers into ITCM.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Switching from a static IP to DHCP without a reboot leaves the DHCP init
waiting out its timeout with the cable connected, because NativeEthernet
only starts its DHCP client from a link-up on an unaddressed interface.
This predates the static link-wait change, which extends it to a static
init that timed out. Fixing it needs a decision on restarting the fnet
DHCP client and a board to check it on, so it is queued rather than
fixed here.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@SensoriumEmbedded SensoriumEmbedded left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the fix, Wes! I've known about this hang for a while and have a fix planned as part of vendoring NativeEthernet into the tree, as we did with USBHost_t36, so every Ethernet init goes through one copy of the library.

With the library in the tree I'll fix it there: bound the static begin()'s link wait with the existing DHCP timeout and have it return a status. That replaces EthBeginStatic() in both images, and the DHCP-after-static case you queued in Known-Issues can then be handled in link_callback too.

So I'm going to hold this one rather than merge it, and close it once the vendored fix lands. Your write-up of the link_callback behavior feeds straight into that.

Thank you!

@wesbiggs

Copy link
Copy Markdown
Contributor Author

Sounds good -- it was just bothering me and looked like a quick fix, but if a better fix is on the slate, I can wait. Feel free to close this whenever.

@SensoriumEmbedded

Copy link
Copy Markdown
Owner

Great, thank you.
I also meant to mention, I appreciate the memory impact as part of the submission. Very helpful! :)

Sign up for free to 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.

2 participants