Ethernet bring-up: hardware Etherbone is dead on silicon, so Etherbone goes in software over broadcast
Same day as the spec work, the bitstream went on the Versa for the first time.
Timing came first. At the stock 75 MHz the sys and etherbone domain closed at about 66 MHz. SYS_CLK_FREQ is 60e6 in the Makefile now and it closes with margin. The b8008 domain at 25 MHz was never the problem.
With no network I needed a console, so --debug-uart puts the LiteX UART on X3, FPGA transmit on B19 and receive on B12, into a 3.3 V FTDI. The firmware grew bring-up instrumentation to print there at boot. An MDIO scan finds the Marvell 88E1512 at address 0. Its MSCR2 reads 0x1076, meaning the PHY adds the RGMII delays internally in both directions, so the FPGA-side delays have to be 0 and 0. MAC RX and TX counters, and a PHY internal loopback self-test that passed 3 of 3.
The CPU path through the plain ethmac worked. Gigabit link, loopback clean, and a DHCP lease from the LAN. That needed LiteEth vendored at master 276c9e3. The pinned 2026.04 release mangled the preamble on transmit frames in PHY loopback.
The hybrid path is the part where LiteEth’s hardware does Etherbone, ARP and ICMP alongside the CPU’s MAC, and it’s what the spec assumed. On silicon it never answered. I tried LiteEth 2026.04 and master, data width 8 and 32, static IP and the dynamic mux, the stock add_etherbone and a local reconstruction of it. Every permutation dead. The upstream test/test_mac_hybrid.py passes in sim. I couldn’t find a public report of hybrid working at gigabit, and my guess is a rate or clock-crossing defect in the hardware path that doesn’t show at 100 Mbit. The bisect flags, --stock-hybrid and --eb-static-ip, stay in the tree, and --ethmac-only is the production config.
So Etherbone moved into firmware. eb8008.c is a software Etherbone server on the VexRiscv speaking the litex CommUDP dialect, probe plus read records answered as writes with the request’s base return address as the correlation id, 32-bit big-endian. I validated it host-side against litex’s own encoder and decoder before flashing.
Then the network on the bench got in the way. The mesh router, an MR60, drops or NATs unicast from a WiFi client to the wired LAN, and it negative-caches an unanswered ARP and never asks again. Broadcast gets through. So udp.c is now a local fork of libliteeth’s. Upstream has a bug where the destination-equals-my-IP filter in process_frame makes the broadcast callback path unreachable. The fork fixes that and adds /24 subnet-broadcast acceptance, minimum-frame padding on transmit, a gratuitous ARP announce, a forced gateway ARP refresh, and udp_set_peer so replies skip ARP. The serve loop takes a DHCP lease, then answers Etherbone on both the unicast and broadcast receive paths. Replies go unicast straight to the requester’s captured MAC and mirror the request’s ports.
One more firmware trap. The generated regions.ld gets rewritten by the builder with ROM shrunk to whatever the previous firmware measured, so any build that grows fails with “region rom overflowed”. linker.ld has fixed ROM and SRAM ceilings now.
Host side, eb_server.py replaces litex_server --udp. It broadcasts requests to the subnet, accepts unicast replies, and ignores the socket’s own broadcast echo. discovery.py tries one broadcast probe before the cache, DNS and sweep. board.py spawns this bridge instead of the stock server.
Verified on the board: discovery finds it, a RemoteClient reads the SoC identifier, a CSR write reads back, and make login lands at the 8008 monitor prompt over the LAN.
What broke next. The monitor’s D and W commands return a constant 0x44 and writes don’t stick. This is the first time the b8008 console path has run on silicon and I haven’t looked at it yet.