Skip to content

STM32F769I-DISCO Ethernet: libttak over real hardware, plus sync I/O leak fix - #108

Merged
gg582 merged 2 commits into
mainfrom
feat/stm32-ethernet
Oct 27, 2026
Merged

gg582 merged 2 commits into
mainfrom
feat/stm32-ethernet

Conversation

@gg582

@gg582 gg582 commented Oct 27, 2026

Copy link
Copy Markdown
Member

Summary

This PR runs libttak on the real Ethernet of the STM32F769I-DISCO: ST HAL ETH driver, LAN8742A PHY, DHCP. It verifies the result over the network and documents it. On top of #104's lwIP backend, a network service runs on the board entirely through libttak. Bringing it up exposed a memory leak in libttak itself, which this PR also fixes.

Commit What
fix(io) libttak leak. ttak_io_sync_read()/_write() take a staging buffer from the default detachable context. That context retires every free to the epoch, and nothing reclaims it, so every call leaked its buffer: +42 MB per 20,000 round trips on the host, and the board ran out of heap within a few hundred calls. The I/O buffers now use a context that frees immediately. Also, on bare metal the detachable quarantine cap was 1 MiB, larger than the MCU's whole heap; it is now 8 KiB (TTAK_DETACHABLE_QUARANTINE_BYTE_LIMIT).
docs(examples) examples/stm32f769i-disco-lwip (driver, FreeRTOS/lwIP config, service), the host test script scripts/baremetal/stm32_eth_demo.py, its README, and a CI build job.

What the service does

  • One ttak_net_poller task serves all clients.
  • Each received chunk is upper-cased by a ttak_thread_pool task (submit, then wait on the future) and sent back with ttak_io_sync_write().
  • Port 7001 replies with one line of counters: ttak_mem_usage_bytes(), ttak_mem_buddy_get_stats(), newlib heap and FreeRTOS heap.

Board results (6 of 6 runs, 7/7 checks each)

One STM32F769I-DISCO on a 100 Mbit/s home LAN, with a Linux host on the same switch. Each run reflashed and rebooted the board.

Test Result
boot DHCP lease 13.5–16.1 s after flashing started; 0–2 MAC restarts
ping 0% loss, avg 0.12 ms
echo 500 chunks of 1–1460 B, all upper-cased correctly
throughput 19.4–19.5 Mbit/s each way (4 MiB)
parallel 4 connections × 256 KiB in 0.61–0.68 s
churn 200 connect/send/close cycles in 0.2 s
memory ttak_mem, buddy, newlib heap and FreeRTOS heap byte-for-byte unchanged across 205 connections and 3793 pool tasks

Problems found during bring-up (details in the example README)

  1. On about half the boots, every received frame failed its CRC.
    • A dump showed bits 0 and 4 of every byte set, meaning RXD0 read as 1 on every other REF_CLK cycle.
    • It didn't depend on the clock source (HSE/HSI) or on the init order.
    • It matches the behaviour ST attributes to an RMII clock timing issue (STM32MP25 ES0598 2.22.2).
    • The driver watches the MMC CRC counter and restarts the MAC with its clocks gated; after 3 tries it resets the chip.
  2. ICMP replies went out with checksum 0. HAL_ETH_Init() presets every TX descriptor to full hardware checksum insertion, so the MAC overwrote lwIP's software checksum. The example now uses hardware checksums in both directions.
  3. A MAC restart wedged the stack. HAL_ETH_Init() doesn't reset the handle's TX/RX bookkeeping, so every transmit returned BUSY while the link task held the tcpip core lock. The restart path now clears that state, the TX wait has a timeout, and RX/restart are serialised by a mutex.
  4. Every boot reused the same DHCP xid (unseeded rand()). rand() is now seeded from the hardware RNG.
  5. Parallel clients deadlocked because unread data keeps holding zero-copy RX buffers. There are now enough RX buffers to cover every client's TCP window.

Testing

Target Result
STM32F769I-DISCO Ethernet (stm32_eth_demo.py) 6/6 runs, 7/7 checks
STM32F769I-DISCO FreeRTOS+lwIP / FreeRTOS / bare metal 39/39 / 38/38 / 32/32
QEMU FreeRTOS+lwIP / FreeRTOS / bare metal 39/39 / 38/38 / 32/32
Hosted gcc / clang / tcc 45/45 each

test_net_tcp adds a check that the heap does not grow over 1000 sync round trips. Before the fix it failed with +6.3 MB on the host and an out-of-memory failure on QEMU.

Limits

  • The D-cache is off. Enabling it needs an MPU region or cache maintenance.
  • The service is a single task with blocking writes.
  • ttak_net_poller_wait() calls malloc on every call.

gg582 added 2 commits October 27, 2026 15:27
ttak_io_sync_read()/_write() stage data in a detachable buffer from the
default context, which retires every free to the epoch, and nothing
reclaims it unless the application drives the epoch GC. Every call leaked
its buffer: 42 MB per 20000 hosted round trips, and an STM32F769's heap
within a few hundred, after which the poller's malloc() failed. Staging
buffers and zero-copy regions have a single owner that frees them, so
they now come from an I/O context that frees at once.

The detachable quarantine (frees held back while alloc/free churns) was
capped at 1 MiB, more than an MCU's whole heap. Bare-metal builds now cap
it at 8 KiB; TTAK_DETACHABLE_QUARANTINE_BYTE_LIMIT overrides it.

test_net_tcp runs 1000 sync round trips and checks the heap: it failed
with 6.3 MB of growth on the host and an exhausted heap on QEMU before
this change.
A FreeRTOS + lwIP service on the board's Ethernet (ST HAL ETH driver,
LAN8742A PHY, DHCP) built entirely on libttak: one ttak_net_poller task
serves every client, a ttak_thread_pool upper-cases each chunk, and
ttak_io_sync_write() sends it back. A second port reports libttak,
newlib and FreeRTOS memory counters.

scripts/baremetal/stm32_eth_demo.py flashes the board and checks ping,
echo correctness, throughput, parallel clients, connection churn and
that memory returns to its baseline. On the board 6 of 6 runs passed:
19.4 Mbit/s each way, 0.12 ms ping, no growth over 205 connections.

The README records what bring-up took: the RMII receive phase that came
up wrong on half the boots (detected and recovered by restarting the
MAC), the HAL presetting hardware checksum insertion, HAL handle state
surviving HAL_ETH_Init(), unseeded DHCP xids, and sizing zero-copy RX
buffers. CI builds the example.
@gg582
gg582 merged commit 63f7cc3 into main Oct 27, 2026
21 of 22 checks passed
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.

1 participant