diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml
index 907d67307a..34728fbbf3 100644
--- a/.github/workflows/linux.yml
+++ b/.github/workflows/linux.yml
@@ -66,6 +66,10 @@ jobs:
if: always()
run: |
(cd test && make run-hosted-linux)
+ - name: Hosted lwIP Unittests
+ if: always()
+ run: |
+ (cd test && make run-hosted-lwip-linux)
- name: Compile STM32 Unittests
if: always()
run: |
@@ -82,6 +86,7 @@ jobs:
(cd test && make compile-nucleo-f767zi)
(cd test && make compile-nucleo-g474re)
(cd test && make compile-nucleo-h723zg)
+ (cd test && make compile-nucleo-h753zi-eth)
(cd test && make compile-nucleo-l432kc)
(cd test && make compile-nucleo-l496zg-p)
(cd test && make compile-nucleo-l552ze-q)
diff --git a/.gitmodules b/.gitmodules
index 8eff409ed0..7abd5799fa 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -64,3 +64,6 @@
[submodule "ext/arm/cmsis-dap"]
path = ext/arm/cmsis-dap
url = https://github.com/modm-ext/cmsis-dap-partial.git
+[submodule "ext/lwip/lwip"]
+ path = ext/lwip/lwip
+ url = https://github.com/modm-ext/lwip-partial.git
diff --git a/README.md b/README.md
index 4d16704806..b521f42dee 100644
--- a/README.md
+++ b/README.md
@@ -311,7 +311,7 @@ Please [discover modm's peripheral drivers for your specific device][discover].
✕ |
✕ |
○ |
-○ |
+✅ |
✕ |
✕ |
✕ |
@@ -999,73 +999,76 @@ your specific needs.
L3GD20 |
LAN8720A |
+| LAN8742A |
+LAN87XX |
LAWICEL |
LIS302DL |
LIS3DSH |
LIS3MDL |
+
| LM75 |
LP503x |
-
| LSM303A |
LSM6DS33 |
LSM6DSO |
LTC2497 |
+
| LTC2499 |
LTC2984 |
-
| MAX14661 |
MAX31855 |
MAX31865 |
MAX6966 |
+
| MAX7219 |
MCP23X08 |
-
| MCP23x17 |
MCP2515 |
MCP3008 |
MCP7941x |
+
| MCP990X |
MMC5603 |
-
| MS5611 |
MS5837 |
NOKIA5110 |
NRF24 |
+
| TFT-DISPLAY |
PAT9125EL |
-
| PCA8574 |
PCA9535 |
PCA9548A |
PCA9685 |
+
| PCAL6524 |
QMC5883L |
-
| SH1106 |
SIEMENS-S65 |
SIEMENS-S75 |
SK6812 |
+
| SK9822 |
SSD1306 |
-
| ST7586S |
ST7789 |
STTS22H |
STUSB4500 |
+
| SX1276 |
SX128X |
-
| TCS3414 |
TCS3472 |
TLC594x |
TMP102 |
+
| TMP12x |
TMP175 |
-
| TOUCH2046 |
VL53L0 |
VL6180 |
WS2812 |
+
diff --git a/examples/README.md b/examples/README.md
index 5d1e559ded..336edb2f6e 100644
--- a/examples/README.md
+++ b/examples/README.md
@@ -76,6 +76,9 @@ supported development boards:
[CMSIS DSP](https://github.com/modm-io/modm/tree/develop/examples/nucleo_f429zi/cmsis_dsp).
- NUCLEO-F446RE:
[Internal Flash Programming](https://github.com/modm-io/modm/blob/develop/examples/nucleo_f446re/flash/main.cpp).
+- NUCLEO-H753ZI:
+[Ethernet MAC/DMA and LAN8742A](https://github.com/modm-io/modm/tree/develop/examples/generic/ethernet),
+[lwIP Ping and UDP/TCP Echo](https://github.com/modm-io/modm/tree/develop/examples/generic/ethernet_lwip_raw).
- STM32F072 Discovery:
[Blinky](https://github.com/modm-io/modm/blob/develop/examples/stm32f072_discovery/blink/main.cpp),
[CAN](https://github.com/modm-io/modm/blob/develop/examples/stm32f072_discovery/can/main.cpp),
diff --git a/examples/generic/ethernet/README.md b/examples/generic/ethernet/README.md
new file mode 100644
index 0000000000..272740566b
--- /dev/null
+++ b/examples/generic/ethernet/README.md
@@ -0,0 +1,57 @@
+# STM32 Ethernet MAC
+
+This example exercises the STM32H5/H7 Ethernet MAC and LAN8742A PHY directly,
+without a network stack. It sends one broadcast Ethernet frame per second and
+alternates between the copied transmit API and the acquire/fill/commit API.
+The MAC configuration uses its default locally administered address derived
+from the STM32 unique identifier. The lwIP example uses the same driver
+helper. The frames use
+`0x88b5`, which the
+[IEEE Registration Authority EtherType registry](https://standards-oui.ieee.org/ethertype/eth.txt)
+assigns as Local Experimental EtherType 1 for public prototype and
+vendor-specific protocol development. They are not IP packets.
+The typed MAC configuration selects `ChecksumMode::Software`, leaving hardware
+checksum offload disabled because these frames contain no IP or transport
+checksum.
+Initialization succeeds with the cable unplugged; periodic link service reports
+`down`, `negotiating`, and `up` transitions, and transmission begins only when
+the negotiated link is up.
+
+Applications that need IP networking should use the
+[lwIP Ethernet example](../ethernet_lwip_raw) instead.
+
+Build from this directory:
+
+```sh
+lbuild build
+scons -Q build=release
+scons -Q build=release program
+```
+
+Connect the board Ethernet port to the capture interface and start the capture
+before running the `program` command. Monitor the ST-LINK virtual COM port at
+115200 baud, 8 data bits, no parity, and 1 stop bit. The serial log reports
+initialization, link transitions, the alternating transmit APIs used, and the
+sequence number.
+
+Use this Wireshark display filter:
+
+```text
+eth.type == 0x88b5
+```
+
+Or capture from a command prompt, replacing `` with the capture
+interface reported by `tshark -D`:
+
+```text
+tshark -i -f "ether proto 0x88b5" -V
+```
+
+Both transmit APIs construct the same frame layout. Each 60-byte frame contains
+the ASCII marker `modm-stm32h7-eth` followed by a little-endian 32-bit sequence
+number. The destination is broadcast so the frames remain visible when the
+board and capture host are connected through a switch.
+
+Validation is manual: confirm that capture lengths are 60 bytes, source MAC
+addresses match the serial log, destination addresses are broadcast, EtherType
+is `0x88b5`, markers are intact, and sequence numbers increase without gaps.
diff --git a/examples/generic/ethernet/main.cpp b/examples/generic/ethernet/main.cpp
new file mode 100644
index 0000000000..80b0a1d443
--- /dev/null
+++ b/examples/generic/ethernet/main.cpp
@@ -0,0 +1,174 @@
+/*
+ * Copyright (c) 2026, Kaelin Laundry
+ *
+ * This file is part of the modm project.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+// ----------------------------------------------------------------------------
+
+// Sends raw IEEE 802 local experimental frames without a network stack.
+
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+
+using namespace Board;
+using namespace std::chrono_literals;
+
+namespace
+{
+
+using Mac = modm::platform::EthernetMac;
+using Phy = modm::Lan8742a;
+
+constexpr std::size_t FrameSize = 60;
+constexpr uint16_t ExperimentalEtherType = 0x88b5;
+constexpr std::array PayloadMarker{{
+ 'm', 'o', 'd', 'm', '-', 's', 't', 'm', '3', '2', 'h', '7', '-', 'e', 't', 'h'
+}};
+
+static void
+logMacAddress(const modm::ethernet::MacAddress& address)
+{
+ MODM_LOG_INFO << "MAC ";
+ for (std::size_t index = 0; index < address.size(); ++index) {
+ MODM_LOG_INFO << modm::hex << address[index] << modm::ascii;
+ if (index + 1 < address.size()) {
+ MODM_LOG_INFO << ':';
+ }
+ }
+ MODM_LOG_INFO << modm::endl;
+}
+
+void
+fillFrame(std::span frame, const std::array& macAddress,
+ uint32_t sequence)
+{
+ std::fill(frame.begin(), frame.end(), 0);
+ std::fill_n(frame.begin(), 6, uint8_t{0xff});
+ std::copy(macAddress.begin(), macAddress.end(), frame.begin() + 6);
+ frame[12] = uint8_t(ExperimentalEtherType >> 8);
+ frame[13] = uint8_t(ExperimentalEtherType);
+ std::copy(PayloadMarker.begin(), PayloadMarker.end(), frame.begin() + 14);
+ frame[30] = uint8_t(sequence);
+ frame[31] = uint8_t(sequence >> 8);
+ frame[32] = uint8_t(sequence >> 16);
+ frame[33] = uint8_t(sequence >> 24);
+}
+
+const char*
+linkStateName(modm::ethernet::LinkState state)
+{
+ switch (state)
+ {
+ case modm::ethernet::LinkState::Down: return "down";
+ case modm::ethernet::LinkState::Negotiating: return "negotiating";
+ case modm::ethernet::LinkState::Up: return "up";
+ }
+ return "unknown";
+}
+
+void
+sendCopiedFrame(const std::array& macAddress, uint32_t sequence)
+{
+ std::array frame{};
+ fillFrame(frame, macAddress, sequence);
+ const auto result = Mac::transmit(frame);
+ modm_assert(result.error != Mac::TransmitError::Busy, "eth.tx.copy.busy",
+ "Copied Ethernet transmit ring is busy");
+ modm_assert(result, "eth.tx.copy", "Copied Ethernet transmit failed",
+ static_cast(result.error));
+ MODM_LOG_INFO << "TX copied sequence=" << sequence << modm::endl;
+}
+
+void
+sendLeaseFrame(const std::array& macAddress, uint32_t sequence)
+{
+ auto lease = Mac::acquireTransmitBuffer(FrameSize);
+ modm_assert(lease.error() != Mac::TransmitError::Busy, "eth.tx.acquire.busy",
+ "Ethernet transmit ring is busy");
+ modm_assert(lease, "eth.tx.acquire", "Failed to acquire Ethernet transmit buffer",
+ static_cast(lease.error()));
+
+ fillFrame(lease.buffer(), macAddress, sequence);
+ const auto result = lease.commit();
+ modm_assert(result, "eth.tx.commit", "Acquired Ethernet transmit commit failed",
+ static_cast(result.error));
+ MODM_LOG_INFO << "TX acquired sequence=" << sequence << modm::endl;
+}
+
+} // namespace
+
+int
+main()
+{
+ Board::initialize();
+ MODM_LOG_INFO << "\n\nReboot: STM32H7 raw Ethernet MAC example" << modm::endl;
+
+ Mac::connect();
+
+ const Mac::Configuration configuration{
+ .checksumMode = Mac::ChecksumMode::Software,
+ };
+ const auto macAddress = configuration.macAddress;
+ const auto initialized =
+ Mac::initialize(configuration);
+ modm_assert(initialized, "eth.initialize", "Ethernet MAC initialization failed",
+ static_cast(initialized.error));
+ const auto phyInitialized = Phy::initialize();
+ modm_assert(phyInitialized, "eth.phy.initialize", "Ethernet PHY initialization failed",
+ static_cast(phyInitialized.error));
+
+ logMacAddress(macAddress);
+ MODM_LOG_INFO << "EtherType 0x88b5" << modm::endl;
+
+ bool haveLinkState = false;
+ modm::ethernet::LinkState previousLinkState = modm::ethernet::LinkState::Down;
+ uint32_t sequence = 0;
+ uint8_t serviceTicks = 0;
+ while (true)
+ {
+ const auto observed = Phy::readLinkStatus();
+ modm_assert(observed, "eth.phy.link", "Ethernet PHY link read failed",
+ static_cast(observed.error));
+ const auto link = Mac::notifyUpdatedLinkStatus(observed.status);
+ modm_assert(link, "eth.link", "Ethernet MAC link update failed",
+ static_cast(link.error));
+ if (not haveLinkState or link.status.state != previousLinkState) {
+ previousLinkState = link.status.state;
+ haveLinkState = true;
+ MODM_LOG_INFO << "Link " << linkStateName(previousLinkState) << modm::endl;
+ }
+
+ if (++serviceTicks == 10) {
+ serviceTicks = 0;
+ if (previousLinkState == modm::ethernet::LinkState::Up) {
+ if ((sequence & 1u) == 0) {
+ sendCopiedFrame(macAddress, sequence);
+ }
+ else {
+ sendLeaseFrame(macAddress, sequence);
+ }
+ sequence++;
+ }
+ }
+
+ modm::delay(100ms);
+ }
+}
diff --git a/examples/generic/ethernet/project.xml b/examples/generic/ethernet/project.xml
new file mode 100644
index 0000000000..a395e13c5b
--- /dev/null
+++ b/examples/generic/ethernet/project.xml
@@ -0,0 +1,14 @@
+
+ modm:nucleo-h753zi
+
+
+
+
+
+
+
+ modm:build:scons
+ modm:platform:eth
+ modm:driver:lan8742a
+
+
diff --git a/examples/generic/ethernet_lwip_raw/README.md b/examples/generic/ethernet_lwip_raw/README.md
new file mode 100644
index 0000000000..35c0e1de9a
--- /dev/null
+++ b/examples/generic/ethernet_lwip_raw/README.md
@@ -0,0 +1,42 @@
+# STM32 lwIP Ethernet
+
+The default configuration uses STM32H7 hardware checksum offload:
+
+```sh
+lbuild build
+scons -Q build=release
+scons -Q build=release program
+```
+
+Build the same example with lwIP software checksums and a separate output
+directory using:
+
+```sh
+lbuild -D modm:lwip:checksum=software \
+ -D modm:build:build.path=../../../build/generic/ethernet_lwip_raw_software \
+ build
+scons -Q build=release
+scons -Q build=release program
+```
+
+After programming, monitor the ST-LINK virtual COM port at 115200 baud, 8 data
+bits, no parity, and 1 stop bit. Run the repository host checks from this
+directory:
+
+```sh
+python ethernet_echo.py 10.66.0.42
+python ethernet_echo.py 10.66.0.42 --stress
+python ethernet_echo.py 10.66.0.42 --throughput
+```
+
+The base checks validate ping plus UDP and TCP echo integrity. Stress mode
+validates delayed reads, concurrent clients, reset recovery, and repeated
+connections. Throughput mode validates the length and SHA-256 digest of a
+32 MiB TCP echo before reporting goodput; no minimum throughput is specified.
+Review the serial diagnostics for adapter, MAC, and DMA errors after each run.
+
+These host checks do not inject malformed packets, force PHY speed or duplex,
+or change the physical link while traffic is active. The STM32H7 Ethernet
+[hardware suite](../../../test/modm/platform/eth/stm32h7/README.md) covers
+malformed loopback frames and forced PHY modes as documented there. Active link
+transitions require a separate external-link-partner procedure.
diff --git a/examples/generic/ethernet_lwip_raw/ethernet_echo.py b/examples/generic/ethernet_lwip_raw/ethernet_echo.py
new file mode 100644
index 0000000000..8f62e4ca6b
--- /dev/null
+++ b/examples/generic/ethernet_lwip_raw/ethernet_echo.py
@@ -0,0 +1,171 @@
+#!/usr/bin/env python3
+#
+# Copyright (c) 2026, Kaelin Laundry
+#
+# This file is part of the modm project.
+#
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+"""Test the ethernet_lwip_raw firmware using ping and UDP/TCP echo.
+
+From the repository root:
+ python examples/generic/ethernet_lwip_raw/ethernet_echo.py
+ python examples/generic/ethernet_lwip_raw/ethernet_echo.py --stress
+ python examples/generic/ethernet_lwip_raw/ethernet_echo.py --throughput
+
+Throughput is TCP echo goodput, i.e. framing and TCP retransmission overhead
+would deduct from the 100 Mbit/s maximum bandwidth.
+"""
+
+import argparse
+import concurrent.futures
+import hashlib
+import os
+import socket
+import struct
+import subprocess
+import time
+
+
+def payload(size, seed=0):
+ pattern = bytes((value + seed) % 251 for value in range(251))
+ return (pattern * ((size + len(pattern) - 1) // len(pattern)))[:size]
+
+
+def ping(address):
+ if os.name == "nt":
+ command = ["ping", "-n", "3", "-w", "2000", address]
+ else:
+ command = ["ping", "-c", "3", "-W", "2", address]
+ return subprocess.run(command, stdout=subprocess.DEVNULL,
+ stderr=subprocess.DEVNULL, timeout=5).returncode == 0
+
+
+def udp_echo(address, port, size):
+ expected = payload(size)
+ with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock:
+ sock.settimeout(2)
+ sock.sendto(expected, (address, port))
+ received, peer = sock.recvfrom(65536)
+ return received == expected and peer[0] == address
+
+
+def tcp_echo(address, port, size, delay=0, read_delay=0, seed=0):
+ expected = payload(size, seed)
+ with socket.create_connection((address, port), timeout=5) as sock:
+ sock.settimeout(30)
+ sock.sendall(expected)
+ sock.shutdown(socket.SHUT_WR)
+ time.sleep(delay)
+
+ received = bytearray()
+ while chunk := sock.recv(65536):
+ received.extend(chunk)
+ if read_delay:
+ time.sleep(read_delay)
+ return received == expected
+
+
+def reset_connection(address, port):
+ sock = socket.create_connection((address, port), timeout=5)
+ linger = "HH" if os.name == "nt" else "ii"
+ sock.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER,
+ struct.pack(linger, 1, 0))
+ sock.sendall(payload(1024 * 1024))
+ sock.close()
+ time.sleep(0.5)
+ return tcp_echo(address, port, 64 * 1024, seed=1)
+
+
+def repeated_connections(address, port, count=50):
+ return all(tcp_echo(address, port, 4 * 1024, seed=index)
+ for index in range(count))
+
+
+def tcp_stream(address, port, size, seed=0):
+ expected = payload(size, seed)
+ expected_digest = hashlib.sha256(expected).digest()
+
+ with socket.create_connection((address, port), timeout=5) as sock:
+ sock.settimeout(60)
+
+ def send():
+ sock.sendall(expected)
+ sock.shutdown(socket.SHUT_WR)
+
+ start = time.perf_counter()
+ with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool:
+ sender = pool.submit(send)
+ received = 0
+ digest = hashlib.sha256()
+ while chunk := sock.recv(65536):
+ digest.update(chunk)
+ received += len(chunk)
+ sender.result()
+ elapsed = time.perf_counter() - start
+
+ if received != size or digest.digest() != expected_digest:
+ raise RuntimeError("TCP throughput data validation failed")
+ return elapsed
+
+
+def throughput(address, port):
+ tcp_stream(address, port, 1024 * 1024, seed=17)
+ size = 32 * 1024 * 1024
+ elapsed = tcp_stream(address, port, size, seed=23)
+ mbit_per_second = size * 8 / elapsed / 1_000_000
+ mib_per_second = size / elapsed / (1024 * 1024)
+ print(f"TCP throughput: {mbit_per_second:.1f} Mbit/s "
+ f"({mib_per_second:.1f} MiB/s)")
+ return True
+
+
+def check(name, function):
+ if not function():
+ raise RuntimeError(f"{name} failed")
+ print(f"{name}: ok")
+
+
+def main():
+ parser = argparse.ArgumentParser(description="Verify the lwIP UDP/TCP echo example")
+ parser.add_argument("address")
+ parser.add_argument("--port", type=int, default=5001)
+ parser.add_argument("--stress", action="store_true")
+ parser.add_argument("--throughput", action="store_true")
+ args = parser.parse_args()
+
+ try:
+ check("Ping", lambda: ping(args.address))
+ except (FileNotFoundError, subprocess.TimeoutExpired) as error:
+ raise RuntimeError("ping command failed") from error
+
+ for size in (1, 64, 512, 1472):
+ check(f"UDP echo ({size} bytes)",
+ lambda size=size: udp_echo(args.address, args.port, size))
+ check("TCP echo (64 KiB)",
+ lambda: tcp_echo(args.address, args.port, 64 * 1024))
+ if args.throughput:
+ check("TCP throughput", lambda: throughput(args.address, args.port))
+
+ if args.stress:
+ check("TCP delayed read (4 MiB)",
+ lambda: tcp_echo(args.address, args.port, 4 * 1024 * 1024, delay=1))
+
+ def concurrent_echo(index):
+ return tcp_echo(args.address, args.port, 512 * 1024,
+ delay=0.25, read_delay=0.0005, seed=index)
+
+ with concurrent.futures.ThreadPoolExecutor(max_workers=4) as pool:
+ check("TCP concurrent clients",
+ lambda: all(pool.map(concurrent_echo, range(4))))
+
+ check("TCP reset recovery",
+ lambda: reset_connection(args.address, args.port))
+ check("TCP repeated reconnects",
+ lambda: repeated_connections(args.address, args.port))
+
+
+if __name__ == "__main__":
+ main()
diff --git a/examples/generic/ethernet_lwip_raw/main.cpp b/examples/generic/ethernet_lwip_raw/main.cpp
new file mode 100644
index 0000000000..7abba45005
--- /dev/null
+++ b/examples/generic/ethernet_lwip_raw/main.cpp
@@ -0,0 +1,461 @@
+/*
+ * Copyright (c) 2026, Kaelin Laundry
+ *
+ * This file is part of the modm project.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+// ----------------------------------------------------------------------------
+
+/*
+ * Provides static IP ping and UDP/TCP echo on port 5001.
+ * The locally administered MAC address is derived from the STM32 unique ID.
+ * IPv4, UDP, and TCP checksums use hardware offload.
+ * Outgoing IPv4 fragmentation is disabled because fragmented transmit traffic
+ * requires software checksum handling. Incoming reassembly remains enabled.
+ * The network fiber polls for received frames, link state, and lwIP timeouts.
+ * Test the echo directly after flashing:
+ * ping 10.66.0.42
+ * echo hello | ncat --udp --idle-timeout 1s 10.66.0.42 5001
+ * echo hello | ncat 10.66.0.42 5001
+ *
+ * Optionally run the complete host checks from this directory:
+ * python ethernet_echo.py 10.66.0.42
+ * Add --stress for large, concurrent, reset, and reconnect tests.
+ * Add --throughput for a validated 32 MiB TCP echo measurement.
+ */
+
+#include
+#include
+#include
+
+#include
+
+#include
+#include
+
+#include
+
+using namespace Board;
+using namespace std::chrono_literals;
+
+// ----------------------------------------------------------------------------
+// Common Ethernet configuration
+
+namespace Ethernet
+{
+static constexpr uint16_t EchoPort = 5001;
+static constexpr uint32_t InputBatchSize = 16;
+
+static void
+logMacAddress(const modm::ethernet::MacAddress& address)
+{
+ MODM_LOG_INFO << "MAC ";
+ for (std::size_t index = 0; index < address.size(); ++index) {
+ MODM_LOG_INFO << modm::hex << address[index] << modm::ascii;
+ if (index + 1 < address.size()) {
+ MODM_LOG_INFO << ':';
+ }
+ }
+ MODM_LOG_INFO << modm::endl;
+}
+
+using LwipEthernet = modm::lwip::LwipEthernet<
+ modm::platform::EthernetMac, modm::Lan8742a>;
+
+struct ServiceDiagnostics
+{
+ uint32_t inputAllocationPressure = 0;
+ uint32_t udpSendAllocationPressure = 0;
+ uint32_t mdioBusy = 0;
+ uint32_t mdioTimeout = 0;
+ modm::Clock::time_point lastReport{};
+ bool haveReported = false;
+};
+
+static ServiceDiagnostics diagnostics;
+static modm::PeriodicTimer linkPollTimer{500ms};
+static modm::PeriodicTimer statisticsReportTimer{10s};
+
+static void
+recordUdpSendError(err_t error)
+{
+ if (error == ERR_MEM) {
+ diagnostics.udpSendAllocationPressure++;
+ return;
+ }
+ modm_assert(error == ERR_OK, "lwip.udp.tx", "UDP echo transmit failed",
+ static_cast(-error));
+}
+
+static void
+reportStatistics()
+{
+ const auto receive = LwipEthernet::getCumulativeReceiveStatistics();
+ const auto errors = LwipEthernet::getErrorCounters();
+ const auto hardware = LwipEthernet::getHardwareErrorStatus();
+
+ MODM_LOG_INFO << "Ethernet adapter RX: acquired=" << receive.acquiredFrames
+ << ", dropped=" << receive.droppedFrames
+ << ", checksum_drops=" << receive.checksumDrops
+ << ", allocation_drops=" << receive.allocationDrops << modm::endl;
+ MODM_LOG_INFO << "Ethernet MAC RX errors: missed=" << errors.rxMissedPackets
+ << ", overflow=" << errors.rxOverflowPackets
+ << ", crc=" << errors.rxCrcErrors
+ << ", alignment=" << errors.rxAlignmentErrors << modm::endl;
+ MODM_LOG_INFO << "Ethernet DMA errors: tx_descriptor=" << errors.txDescriptorErrors
+ << ", fatal_bus=" << errors.fatalBusErrors
+ << ", context_descriptor=" << errors.contextDescriptorErrors
+ << ", rx_watchdog=" << errors.rxWatchdogTimeouts << modm::endl;
+ MODM_LOG_INFO << "Ethernet DMA events: rx_stopped=" << errors.rxProcessStopped
+ << ", rx_unavailable=" << errors.rxBufferUnavailable
+ << ", tx_stopped=" << errors.txProcessStopped
+ << ", tx_unavailable=" << errors.txBufferUnavailable << modm::endl;
+ MODM_LOG_INFO << "Ethernet sticky status: dma=0x" << modm::hex
+ << hardware.dmaStatus << ", tx_descriptor=0x" << hardware.txDescriptorStatus
+ << modm::ascii << modm::endl;
+}
+
+static void
+poll()
+{
+ const err_t inputError = LwipEthernet::pollInput(InputBatchSize);
+ if (inputError == ERR_MEM)
+ diagnostics.inputAllocationPressure++;
+ modm_assert(inputError == ERR_OK or inputError == ERR_MEM,
+ "lwip.input", "Ethernet input failed", static_cast(-inputError));
+
+ if (linkPollTimer.execute()) {
+ const auto link = LwipEthernet::pollLink();
+ switch (link.phyError) {
+ case modm::ethernet::MdioError::None:
+ break;
+ case modm::ethernet::MdioError::Busy:
+ diagnostics.mdioBusy++;
+ break;
+ case modm::ethernet::MdioError::Timeout:
+ diagnostics.mdioTimeout++;
+ break;
+ default:
+ modm_assert(false, "eth.mdio", "Ethernet PHY link read failed",
+ static_cast(link.phyError));
+ }
+ modm_assert(link.macError == LwipEthernet::LinkUpdateError::None,
+ "eth.link", "Ethernet MAC link update failed",
+ static_cast(link.macError));
+ }
+ modm::lwip::processTimeouts();
+ if (statisticsReportTimer.execute())
+ reportStatistics();
+
+ if (diagnostics.inputAllocationPressure == 0 and
+ diagnostics.udpSendAllocationPressure == 0 and
+ diagnostics.mdioBusy == 0 and diagnostics.mdioTimeout == 0)
+ return;
+ const auto now = modm::Clock::now();
+ if (diagnostics.haveReported and now - diagnostics.lastReport < 5s)
+ return;
+
+ if (diagnostics.inputAllocationPressure != 0) {
+ MODM_LOG_WARNING << "Ethernet input allocation pressure: "
+ << diagnostics.inputAllocationPressure << " ERR_MEM events" << modm::endl;
+ }
+ if (diagnostics.udpSendAllocationPressure != 0) {
+ MODM_LOG_WARNING << "UDP echo transmit allocation pressure: "
+ << diagnostics.udpSendAllocationPressure << " ERR_MEM events" << modm::endl;
+ }
+ if (diagnostics.mdioBusy != 0 or diagnostics.mdioTimeout != 0) {
+ MODM_LOG_WARNING << "Ethernet transient MDIO errors: busy="
+ << diagnostics.mdioBusy << ", timeout=" << diagnostics.mdioTimeout
+ << modm::endl;
+ }
+ diagnostics.inputAllocationPressure = 0;
+ diagnostics.udpSendAllocationPressure = 0;
+ diagnostics.mdioBusy = 0;
+ diagnostics.mdioTimeout = 0;
+ diagnostics.lastReport = now;
+ diagnostics.haveReported = true;
+}
+}
+
+// ----------------------------------------------------------------------------
+// UDP echo
+
+static void
+udpReceive(void*, struct udp_pcb* pcb, struct pbuf* p, const ip_addr_t* address, uint16_t port)
+{
+ if (p != nullptr) {
+ Ethernet::recordUdpSendError(udp_sendto(pcb, p, address, port));
+ pbuf_free(p);
+ }
+}
+
+static bool
+setupUdpEcho()
+{
+ modm::lwip::LwIPSingleThreadGuard guard;
+ struct udp_pcb* pcb = udp_new();
+ if (pcb == nullptr)
+ return false;
+
+ if (udp_bind(pcb, IP_ADDR_ANY, Ethernet::EchoPort) == ERR_OK) {
+ udp_recv(pcb, udpReceive, nullptr);
+ return true;
+ }
+ udp_remove(pcb);
+ return false;
+}
+
+// ----------------------------------------------------------------------------
+// TCP echo
+
+static err_t
+tcpReceive(void* argument, struct tcp_pcb* pcb, struct pbuf* p, err_t err);
+
+static err_t
+tcpSent(void* argument, struct tcp_pcb* pcb, uint16_t length);
+
+static err_t
+tcpPoll(void* argument, struct tcp_pcb* pcb);
+
+static void
+tcpError(void* argument, err_t err);
+
+struct TcpEchoState
+{
+ struct pbuf* pending;
+ bool remoteClosed;
+};
+
+static constexpr uint8_t TcpPollInterval = 2;
+
+static void
+freeTcpState(TcpEchoState* state)
+{
+ if (state == nullptr)
+ return;
+
+ if (state->pending != nullptr)
+ pbuf_free(state->pending);
+ mem_free(state);
+}
+
+static void
+registerTcpCallbacks(struct tcp_pcb* pcb, TcpEchoState* state)
+{
+ tcp_arg(pcb, state);
+ tcp_recv(pcb, tcpReceive);
+ tcp_sent(pcb, tcpSent);
+ tcp_poll(pcb, tcpPoll, TcpPollInterval);
+ tcp_err(pcb, tcpError);
+}
+
+static err_t
+abortTcpConnection(struct tcp_pcb* pcb)
+{
+ tcp_abort(pcb);
+ return ERR_ABRT;
+}
+
+static err_t
+closeTcpConnection(struct tcp_pcb* pcb, TcpEchoState* state)
+{
+ tcp_arg(pcb, nullptr);
+ tcp_recv(pcb, nullptr);
+ tcp_sent(pcb, nullptr);
+ tcp_poll(pcb, nullptr, 0);
+ tcp_err(pcb, nullptr);
+
+ const err_t error = tcp_close(pcb);
+ if (error == ERR_OK) {
+ freeTcpState(state);
+ return ERR_OK;
+ }
+
+ registerTcpCallbacks(pcb, state);
+ if (error == ERR_MEM)
+ return ERR_OK;
+ return abortTcpConnection(pcb);
+}
+
+static err_t
+sendTcpEcho(struct tcp_pcb* pcb, TcpEchoState* state)
+{
+ if (state == nullptr)
+ return abortTcpConnection(pcb);
+
+ bool queued = false;
+ while (state->pending != nullptr and state->pending->len <= tcp_sndbuf(pcb)) {
+ struct pbuf* current = state->pending;
+ const err_t error = tcp_write(
+ pcb, current->payload, current->len, TCP_WRITE_FLAG_COPY);
+ if (error == ERR_MEM)
+ break;
+ if (error != ERR_OK)
+ return abortTcpConnection(pcb);
+
+ const uint16_t length = current->len;
+ state->pending = current->next;
+ if (state->pending != nullptr) {
+ // Preserve the tail while pbuf_free() releases the consumed head.
+ pbuf_ref(state->pending);
+ }
+ pbuf_free(current);
+ tcp_recved(pcb, length);
+ queued = queued or length != 0;
+ }
+
+ if (queued)
+ (void) tcp_output(pcb);
+ if (state->pending == nullptr and state->remoteClosed)
+ return closeTcpConnection(pcb, state);
+ return ERR_OK;
+}
+
+static err_t
+tcpReceive(void* argument, struct tcp_pcb* pcb, struct pbuf* p, err_t err)
+{
+ if (err != ERR_OK)
+ return err;
+
+ auto* state = static_cast(argument);
+ if (state == nullptr)
+ return abortTcpConnection(pcb);
+
+ if (p == nullptr) {
+ state->remoteClosed = true;
+ return sendTcpEcho(pcb, state);
+ }
+
+ if (state->pending != nullptr) {
+ // Let lwIP retain one refused packet until the pending chain advances.
+ return ERR_MEM;
+ }
+ state->pending = p;
+ return sendTcpEcho(pcb, state);
+}
+
+static void
+tcpError(void* argument, err_t)
+{
+ freeTcpState(static_cast(argument));
+}
+
+static err_t
+tcpSent(void* argument, struct tcp_pcb* pcb, uint16_t)
+{
+ return sendTcpEcho(pcb, static_cast(argument));
+}
+
+static err_t
+tcpPoll(void* argument, struct tcp_pcb* pcb)
+{
+ return sendTcpEcho(pcb, static_cast(argument));
+}
+
+static err_t
+tcpAccept(void*, struct tcp_pcb* newPcb, err_t err)
+{
+ if (err != ERR_OK)
+ return err;
+ if (newPcb == nullptr)
+ return ERR_VAL;
+
+ auto* state = static_cast(mem_malloc(sizeof(TcpEchoState)));
+ if (state == nullptr)
+ return ERR_MEM;
+ state->pending = nullptr;
+ state->remoteClosed = false;
+ registerTcpCallbacks(newPcb, state);
+ return ERR_OK;
+}
+
+static bool
+setupTcpEcho()
+{
+ modm::lwip::LwIPSingleThreadGuard guard;
+ struct tcp_pcb* pcb = tcp_new();
+ if (pcb == nullptr)
+ return false;
+
+ if (tcp_bind(pcb, IP_ADDR_ANY, Ethernet::EchoPort) != ERR_OK) {
+ if (tcp_close(pcb) != ERR_OK)
+ tcp_abort(pcb);
+ return false;
+ }
+
+ struct tcp_pcb* listenPcb = tcp_listen_with_backlog(pcb, 4);
+ if (listenPcb != nullptr) {
+ tcp_accept(listenPcb, tcpAccept);
+ return true;
+ }
+ if (tcp_close(pcb) != ERR_OK)
+ tcp_abort(pcb);
+ return false;
+}
+
+// ----------------------------------------------------------------------------
+// Application
+
+modm::Fiber networkFiber([]{
+ while (true) {
+ Ethernet::poll();
+ (void) modm::this_fiber::poll_for(1ms, []{
+ return Ethernet::LwipEthernet::hasPendingWakeupEvents();
+ });
+ (void) Ethernet::LwipEthernet::consumeWakeupEvents();
+ modm::this_fiber::yield();
+ }
+});
+
+int
+main()
+{
+ Board::initialize();
+ MODM_LOG_INFO << "\n\nReboot: lwIP raw Ethernet Example" << modm::endl;
+ MODM_LOG_INFO << "IPv4 10.66.0.42, UDP/TCP echo port "
+ << Ethernet::EchoPort << modm::endl;
+
+ Ethernet::LwipEthernet::connect();
+
+ const modm::lwip::StaticIPv4Configuration config {
+ .macAddress = modm::platform::EthernetMac::getDefaultMacAddress(),
+ .ipAddress = {{ 10, 66, 0, 42 }},
+ .netmask = {{ 255, 255, 255, 0 }},
+ .gateway = {{ 10, 66, 0, 1 }},
+ };
+ Ethernet::logMacAddress(config.macAddress);
+
+ const auto initialization = Ethernet::LwipEthernet::initialize(config);
+ if (not initialization) {
+ MODM_LOG_ERROR << "Ethernet/lwIP initialization failed (MAC error "
+ << static_cast(initialization.macError) << ", PHY error "
+ << static_cast(initialization.phyError) << ", PHY MDIO error "
+ << static_cast(initialization.phyMdioError) << ", link MDIO error "
+ << static_cast(initialization.linkMdioError) << ", link update error "
+ << static_cast(initialization.linkUpdateError) << ", adapter error "
+ << static_cast(initialization.adapterError) << ")" << modm::endl;
+ }
+ const uintptr_t initializationErrorContext =
+ (static_cast(initialization.macError) << 8) |
+ static_cast(initialization.adapterError);
+ modm_assert(initialization, "lwip.initialize", "Ethernet/lwIP initialization failed",
+ initializationErrorContext);
+ modm_assert(Ethernet::LwipEthernet::setDefault(), "lwip.default",
+ "Failed to select the default network interface");
+ modm_assert(setupUdpEcho(), "lwip.udp", "Failed to set up UDP echo");
+ modm_assert(setupTcpEcho(), "lwip.tcp", "Failed to set up TCP echo");
+
+ modm::fiber::Scheduler::run();
+ return 0;
+}
diff --git a/examples/generic/ethernet_lwip_raw/project.xml b/examples/generic/ethernet_lwip_raw/project.xml
new file mode 100644
index 0000000000..1057480ea7
--- /dev/null
+++ b/examples/generic/ethernet_lwip_raw/project.xml
@@ -0,0 +1,19 @@
+
+ modm:nucleo-h753zi
+
+
+
+
+
+
+
+
+
+ modm:build:scons
+ modm:platform:eth
+ modm:driver:lan8742a
+ modm:lwip
+ modm:processing:fiber
+ modm:processing:timer
+
+
diff --git a/examples/nucleo_f429zi/ethernet/main.cpp b/examples/nucleo_f429zi/ethernet/main.cpp
index 864d5f42a8..79c585214e 100755
--- a/examples/nucleo_f429zi/ethernet/main.cpp
+++ b/examples/nucleo_f429zi/ethernet/main.cpp
@@ -28,7 +28,7 @@ namespace Ethernet
using RMII_Mdc = GpioOutputC1;
using RMII_Rx_D0 = GpioInputC4;
using RMII_Rx_D1 = GpioInputC5;
- using Port = Eth;
+ using Port = Eth>;
}
UBaseType_t ulNextRand;
diff --git a/examples/nucleo_f767zi/ethernet/main.cpp b/examples/nucleo_f767zi/ethernet/main.cpp
index 864d5f42a8..79c585214e 100755
--- a/examples/nucleo_f767zi/ethernet/main.cpp
+++ b/examples/nucleo_f767zi/ethernet/main.cpp
@@ -28,7 +28,7 @@ namespace Ethernet
using RMII_Mdc = GpioOutputC1;
using RMII_Rx_D0 = GpioInputC4;
using RMII_Rx_D1 = GpioInputC5;
- using Port = Eth;
+ using Port = Eth>;
}
UBaseType_t ulNextRand;
diff --git a/ext/aws/modm_lan8720a.cpp b/ext/aws/modm_lan8720a.cpp
index bbea38ccff..ea2fa2ec86 100644
--- a/ext/aws/modm_lan8720a.cpp
+++ b/ext/aws/modm_lan8720a.cpp
@@ -24,12 +24,12 @@
#include
-using EMAC = modm::platform::Eth;
+using EMAC = modm::platform::Eth>;
namespace modm
{
-struct ethernet
+struct EthernetDriver
{
static constexpr BaseType_t MAX_PACKET_SIZE { 1536 };
static constexpr BaseType_t RX_BUFFER_SIZE { 1536 };
@@ -464,34 +464,34 @@ struct ethernet
}
};
-ethernet::InitStatus ethernet::initStatus = ethernet::InitStatus::Init;
-SemaphoreHandle_t ethernet::txDescriptorSemaphore { nullptr };
-TaskHandle_t ethernet::emacTaskHandle { nullptr };
+EthernetDriver::InitStatus EthernetDriver::initStatus = EthernetDriver::InitStatus::Init;
+SemaphoreHandle_t EthernetDriver::txDescriptorSemaphore { nullptr };
+TaskHandle_t EthernetDriver::emacTaskHandle { nullptr };
-modm::platform::eth::Event_t ethernet::isrEvent { modm::platform::eth::Event::None };
+modm::platform::eth::Event_t EthernetDriver::isrEvent { modm::platform::eth::Event::None };
-TimeOut_t ethernet::phyLinkStatusTimer;
-modm::platform::eth::LinkStatus ethernet::lastPhyLinkStatus { modm::platform::eth::LinkStatus::Down };
-TickType_t ethernet::phyLinkStatusRemaining { 0 };
+TimeOut_t EthernetDriver::phyLinkStatusTimer;
+modm::platform::eth::LinkStatus EthernetDriver::lastPhyLinkStatus { modm::platform::eth::LinkStatus::Down };
+TickType_t EthernetDriver::phyLinkStatusRemaining { 0 };
-ethernet::DmaDescriptor_t ethernet::DmaRxDescriptorTable[RX_BUFFER_NUMBER];
-ethernet::DmaDescriptor_t ethernet::DmaTxDescriptorTable[TX_BUFFER_NUMBER];
-ethernet::DmaDescriptor_t *ethernet::RxDescriptor { nullptr }; /*!< Rx descriptor to Get */
-ethernet::DmaDescriptor_t *ethernet::TxDescriptor { nullptr }; /*!< Tx descriptor to Set */
-ethernet::DmaDescriptor_t *ethernet::DmaTxDescriptorToClear { nullptr };
+EthernetDriver::DmaDescriptor_t EthernetDriver::DmaRxDescriptorTable[RX_BUFFER_NUMBER];
+EthernetDriver::DmaDescriptor_t EthernetDriver::DmaTxDescriptorTable[TX_BUFFER_NUMBER];
+EthernetDriver::DmaDescriptor_t *EthernetDriver::RxDescriptor { nullptr }; /*!< Rx descriptor to Get */
+EthernetDriver::DmaDescriptor_t *EthernetDriver::TxDescriptor { nullptr }; /*!< Tx descriptor to Set */
+EthernetDriver::DmaDescriptor_t *EthernetDriver::DmaTxDescriptorToClear { nullptr };
} // namespace modm
extern "C" BaseType_t
xNetworkInterfaceInitialise()
{
- using modm::ethernet;
+ using modm::EthernetDriver;
- if (ethernet::initStatus == ethernet::InitStatus::Init) {
- ethernet::txDescriptorSemaphore = xSemaphoreCreateCounting(UBaseType_t(ethernet::TX_BUFFER_NUMBER),
- UBaseType_t(ethernet::TX_BUFFER_NUMBER ));
- if (ethernet::txDescriptorSemaphore == NULL) {
- ethernet::initStatus = ethernet::InitStatus::Failed;
+ if (EthernetDriver::initStatus == EthernetDriver::InitStatus::Init) {
+ EthernetDriver::txDescriptorSemaphore = xSemaphoreCreateCounting(UBaseType_t(EthernetDriver::TX_BUFFER_NUMBER),
+ UBaseType_t(EthernetDriver::TX_BUFFER_NUMBER ));
+ if (EthernetDriver::txDescriptorSemaphore == NULL) {
+ EthernetDriver::initStatus = EthernetDriver::InitStatus::Failed;
return pdFAIL;
}
@@ -504,29 +504,29 @@ xNetworkInterfaceInitialise()
(void) EMAC::initialize();
- ethernet::TxDescriptor = ethernet::DmaTxDescriptorTable;
- ethernet::RxDescriptor = ethernet::DmaRxDescriptorTable;
+ EthernetDriver::TxDescriptor = EthernetDriver::DmaTxDescriptorTable;
+ EthernetDriver::RxDescriptor = EthernetDriver::DmaRxDescriptorTable;
- std::memset(ðernet::DmaTxDescriptorTable, 0, sizeof(ethernet::DmaTxDescriptorTable));
- std::memset(ðernet::DmaRxDescriptorTable, 0, sizeof(ethernet::DmaRxDescriptorTable));
+ std::memset(&EthernetDriver::DmaTxDescriptorTable, 0, sizeof(EthernetDriver::DmaTxDescriptorTable));
+ std::memset(&EthernetDriver::DmaRxDescriptorTable, 0, sizeof(EthernetDriver::DmaRxDescriptorTable));
- ethernet::DmaTxDescriptorToClear = ethernet::DmaTxDescriptorTable;
+ EthernetDriver::DmaTxDescriptorToClear = EthernetDriver::DmaTxDescriptorTable;
- ethernet::DMATxDescListInit();
- ethernet::DMARxDescListInit();
+ EthernetDriver::DMATxDescListInit();
+ EthernetDriver::DMARxDescListInit();
- ethernet::updateConfig(true);
+ EthernetDriver::updateConfig(true);
- if (not xTaskCreate(ethernet::emacHandlerTask, "EMAC", ethernet::emacTaskStackDepth, NULL,
- ethernet::emacTaskPriority, ðernet::emacTaskHandle)) {
- ethernet::initStatus = ethernet::InitStatus::Failed;
+ if (not xTaskCreate(EthernetDriver::emacHandlerTask, "EMAC", EthernetDriver::emacTaskStackDepth, NULL,
+ EthernetDriver::emacTaskPriority, &EthernetDriver::emacTaskHandle)) {
+ EthernetDriver::initStatus = EthernetDriver::InitStatus::Failed;
return pdFAIL;
}
- ethernet::initStatus = ethernet::InitStatus::Pass;
+ EthernetDriver::initStatus = EthernetDriver::InitStatus::Pass;
}
- if (ethernet::initStatus != ethernet::InitStatus::Pass)
+ if (EthernetDriver::initStatus != EthernetDriver::InitStatus::Pass)
return pdFAIL;
if (EMAC::getLinkStatus() == modm::platform::eth::LinkStatus::Up) {
@@ -557,20 +557,20 @@ xNetworkInterfaceInitialise()
extern "C" BaseType_t
xNetworkInterfaceOutput(NetworkBufferDescriptor_t * const descriptor, BaseType_t releaseAfterSend)
{
- using modm::ethernet;
+ using modm::EthernetDriver;
static constexpr TickType_t blockTimeTicks { pdMS_TO_TICKS(50) };
- static constexpr ethernet::TDes0_t transmitStatus {
- ethernet::CrcControl_t(ethernet::CrcControl::HardwareCalculated) |
- ethernet::TDes0_t(ethernet::TDes0::InterruptOnCompletion |
- ethernet::TDes0::LastSegment |
- ethernet::TDes0::FirstSegment
+ static constexpr EthernetDriver::TDes0_t transmitStatus {
+ EthernetDriver::CrcControl_t(EthernetDriver::CrcControl::HardwareCalculated) |
+ EthernetDriver::TDes0_t(EthernetDriver::TDes0::InterruptOnCompletion |
+ EthernetDriver::TDes0::LastSegment |
+ EthernetDriver::TDes0::FirstSegment
)
};
BaseType_t result { pdFAIL };
uint32_t transmitSize { 0 };
- __IO ethernet::DmaDescriptor_t *dmaTxDescriptor { nullptr };
+ __IO EthernetDriver::DmaDescriptor_t *dmaTxDescriptor { nullptr };
do {
ProtocolPacket_t *packet = reinterpret_cast(descriptor->pucEthernetBuffer);
@@ -581,15 +581,15 @@ xNetworkInterfaceOutput(NetworkBufferDescriptor_t * const descriptor, BaseType_t
// no link, drop packet
break;
- if (xSemaphoreTake(ethernet::txDescriptorSemaphore, blockTimeTicks) != pdPASS)
+ if (xSemaphoreTake(EthernetDriver::txDescriptorSemaphore, blockTimeTicks) != pdPASS)
break;
- dmaTxDescriptor = ethernet::TxDescriptor;
- configASSERT((dmaTxDescriptor->Status & uint32_t(ethernet::TDes0::DmaOwned)) == 0);
+ dmaTxDescriptor = EthernetDriver::TxDescriptor;
+ configASSERT((dmaTxDescriptor->Status & uint32_t(EthernetDriver::TDes0::DmaOwned)) == 0);
transmitSize = descriptor->xDataLength;
- if (transmitSize > ethernet::TX_BUFFER_SIZE)
- transmitSize = ethernet::TX_BUFFER_SIZE;
+ if (transmitSize > EthernetDriver::TX_BUFFER_SIZE)
+ transmitSize = EthernetDriver::TX_BUFFER_SIZE;
configASSERT(releaseAfterSend != 0);
@@ -598,10 +598,10 @@ xNetworkInterfaceOutput(NetworkBufferDescriptor_t * const descriptor, BaseType_t
dmaTxDescriptor->Status |= transmitStatus.value;
- dmaTxDescriptor->ControlBufferSize = transmitSize & modm::ethernet::Buffer1SizeMask;
+ dmaTxDescriptor->ControlBufferSize = transmitSize & modm::EthernetDriver::Buffer1SizeMask;
- dmaTxDescriptor->Status |= uint32_t(ethernet::TDes0::DmaOwned);
- ethernet::TxDescriptor = reinterpret_cast(ethernet::TxDescriptor->Buffer2NextDescAddr);
+ dmaTxDescriptor->Status |= uint32_t(EthernetDriver::TDes0::DmaOwned);
+ EthernetDriver::TxDescriptor = reinterpret_cast(EthernetDriver::TxDescriptor->Buffer2NextDescAddr);
__DSB();
ETH->DMATPDR = 0;
iptraceNETWORK_INTERFACE_TRANSMIT();
@@ -623,23 +623,23 @@ BaseType_t xGetPhyLinkStatus()
MODM_ISR(ETH)
{
using modm::platform::eth;
- using modm::ethernet;
+ using modm::EthernetDriver;
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
EMAC::InterruptFlags_t irq = EMAC::getInterruptFlags();
EMAC::acknowledgeInterrupt(irq);
if (irq & (eth::InterruptFlags::Receive | eth::InterruptFlags::ReceiveBufferUnavailable)) {
- ethernet::isrEvent |= eth::Event::Receive;
- if (ethernet::emacTaskHandle) {
- vTaskNotifyGiveFromISR(ethernet::emacTaskHandle, &xHigherPriorityTaskWoken);
+ EthernetDriver::isrEvent |= eth::Event::Receive;
+ if (EthernetDriver::emacTaskHandle) {
+ vTaskNotifyGiveFromISR(EthernetDriver::emacTaskHandle, &xHigherPriorityTaskWoken);
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
}
if (irq & (eth::InterruptFlags::Transmit)) {
- ethernet::isrEvent |= eth::Event::Transmit;
- if (ethernet::emacTaskHandle) {
- vTaskNotifyGiveFromISR(ethernet::emacTaskHandle, &xHigherPriorityTaskWoken);
+ EthernetDriver::isrEvent |= eth::Event::Transmit;
+ if (EthernetDriver::emacTaskHandle) {
+ vTaskNotifyGiveFromISR(EthernetDriver::emacTaskHandle, &xHigherPriorityTaskWoken);
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
}
diff --git a/ext/gcc/module_c.lb b/ext/gcc/module_c.lb
index bddf10bcdc..a44e20cddb 100644
--- a/ext/gcc/module_c.lb
+++ b/ext/gcc/module_c.lb
@@ -4,6 +4,7 @@
# Copyright (c) 2016-2017, Niklas Hauser
# Copyright (c) 2017-2018, Fabian Greif
# Copyright (c) 2018, Christopher Durand
+# Copyright (c) 2026, Kaelin Laundry
#
# This file is part of the modm project.
#
@@ -25,7 +26,8 @@ Refines the C language to make it easier to use on embedded targets.
Additional compiler options:
-- `--specs=nano.specs`: use Newlib Nano (when not using exceptions).
+- `--specs=nano.specs`: use Newlib Nano according to the `newlib`
+ option.
- `--specs=nosys.specs`: No additional C library features are implemented.
"""
@@ -35,8 +37,24 @@ def prepare(module, options):
if core.startswith("avr") or core.startswith("cortex-m"):
module.depends(":architecture:assert")
+ if core.startswith("cortex-m"):
+ module.add_option(
+ EnumerationOption(
+ name="newlib",
+ enumeration=["auto", "full", "nano"],
+ default="auto",
+ description=descr_newlib))
+
return True
+def validate(env):
+ core = env[":target"].get_driver("core")["type"]
+ if not core.startswith("cortex-m"):
+ return
+
+ if env.get("newlib", "auto") == "nano" and env.get(":stdc++:exceptions", False):
+ raise ValidateException("Newlib Nano does not support C++ exceptions!")
+
def build(env):
core = env[":target"].get_driver("core")["type"]
@@ -46,6 +64,20 @@ def build(env):
# Compiler options for targets
env.collect(":build:linkflags", "--specs=nosys.specs")
- if not env.get(":stdc++:exceptions", False):
- # Newlib Nano does not support C++ exceptions at all
+ newlib = env.get("newlib", "auto")
+ if newlib == "nano" or (
+ newlib == "auto" and not env.get(":stdc++:exceptions", False)):
env.collect(":build:linkflags", "--specs=nano.specs")
+
+
+descr_newlib = """# Newlib
+
+Select the C standard library variant on Cortex-M targets:
+
+- `auto`: use Newlib Nano unless C++ exceptions are enabled.
+- `nano`: force Newlib Nano. This is invalid with C++ exceptions.
+- `full`: force full Newlib.
+
+Newlib Nano optimizes for code size at the expense of performance. Use full
+Newlib when standard library performance (e.g. memcpy) matters.
+"""
diff --git a/ext/lwip/ethernet/ethernet.hpp b/ext/lwip/ethernet/ethernet.hpp
new file mode 100644
index 0000000000..5f1a4fd777
--- /dev/null
+++ b/ext/lwip/ethernet/ethernet.hpp
@@ -0,0 +1,657 @@
+/*
+ * Copyright (c) 2026, Kaelin Laundry
+ *
+ * This file is part of the modm project.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+#ifndef MODM_LWIP_ETHERNET_HPP
+#define MODM_LWIP_ETHERNET_HPP
+
+#include
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#ifndef MODM_LWIP_CHECKSUM_HARDWARE
+#define MODM_LWIP_CHECKSUM_HARDWARE 0
+#endif
+#if MODM_LWIP_CHECKSUM_HARDWARE && IP_FRAG
+#error "Hardware checksum mode does not support outgoing IPv4 fragmentation"
+#endif
+#if MODM_LWIP_CHECKSUM_HARDWARE && !LWIP_CHECKSUM_CTRL_PER_NETIF
+#error "Hardware checksum mode requires LWIP_CHECKSUM_CTRL_PER_NETIF"
+#endif
+#if !LWIP_IPV4 || !LWIP_ETHERNET || !LWIP_ARP
+#error "modm::lwip::LwipEthernet requires LWIP_IPV4, LWIP_ETHERNET, and LWIP_ARP"
+#endif
+#if LWIP_IPV6 || LWIP_IGMP || LWIP_IPV6_MLD
+#error "modm::lwip::LwipEthernet supports IPv4 unicast and broadcast only"
+#endif
+
+namespace modm::lwip
+{
+
+struct IPv4Address
+{
+ std::array bytes;
+};
+
+struct StaticIPv4Configuration
+{
+ ethernet::MacAddress macAddress;
+ IPv4Address ipAddress;
+ IPv4Address netmask;
+ IPv4Address gateway;
+};
+
+namespace detail
+{
+
+inline constexpr uint16_t EthernetMtu = 1500;
+
+template
+concept EthernetMac = ethernet::Clause22Mdio and
+ requires(std::size_t size, ethernet::LinkStatus linkStatus) {
+ typename Mac::MediaInterface;
+ typename Mac::Configuration;
+ typename Mac::ChecksumMode;
+ typename Mac::InitializationError;
+ typename Mac::TransmitError;
+ typename Mac::TransmitBufferLease;
+ typename Mac::ReceiveError;
+ typename Mac::ReceiveChecksumStatus;
+ typename Mac::ReceiveBufferLease;
+ typename Mac::LinkUpdateError;
+ typename Mac::LinkUpdateResult;
+ { Mac::MaxFrameSize } -> std::convertible_to;
+ { Mac::acquireTransmitBuffer(size) } -> std::same_as;
+ { Mac::tryAcquireReceiveBuffer() } -> std::same_as;
+ { Mac::getLinkStatus() } -> std::same_as;
+ { Mac::notifyUpdatedLinkStatus(linkStatus) } -> std::same_as;
+};
+
+template
+concept EthernetPhy = EthernetMac and requires {
+ typename Phy::InitializationError;
+ typename Phy::InitializationResult;
+ typename Phy::LinkStatusResult;
+ { Phy::template initialize() } -> std::same_as;
+ { Phy::template readLinkStatus() } -> std::same_as;
+};
+
+template
+struct EthernetInterfaceState
+{
+ struct ReceiveStatistics
+ {
+ uint64_t acquiredFrames{};
+ uint64_t droppedFrames{};
+ uint64_t checksumDrops{};
+ uint64_t allocationDrops{};
+ };
+
+ void const* adapterOwner{};
+ bool netifRegistered{};
+ bool adapterReady{};
+ struct netif netif{};
+ ethernet::MacAddress macAddress{};
+ ReceiveStatistics receiveStatistics{};
+};
+
+template
+inline EthernetInterfaceState ethernetInterfaceState{};
+
+inline ip4_addr_t
+makeIp4(IPv4Address address)
+{
+ ip4_addr_t ip;
+ IP4_ADDR(&ip, address.bytes[0], address.bytes[1], address.bytes[2], address.bytes[3]);
+ return ip;
+}
+
+} // namespace detail
+
+template
+ requires detail::EthernetPhy
+class LwipEthernet
+{
+ using State = detail::EthernetInterfaceState;
+
+public:
+ /// Cumulative receive counters for the physical MAC.
+ using ReceiveStatistics = typename State::ReceiveStatistics;
+ using MediaInterface = ethernet::MediaInterface;
+ using LinkState = ethernet::LinkState;
+ using LinkUpdateError = typename Mac::LinkUpdateError;
+
+ template
+ static void
+ connect()
+ {
+ Mac::template connect();
+ }
+
+ enum class AdapterInitializationError
+ {
+ None,
+ MacAlreadyBound,
+ NetifRegistrationFailed,
+ };
+
+ struct [[nodiscard]] InitializationResult
+ {
+ typename Mac::InitializationError macError = Mac::InitializationError::None;
+ typename Phy::InitializationError phyError = Phy::InitializationError::None;
+ ethernet::MdioError phyMdioError = ethernet::MdioError::None;
+ ethernet::MdioError linkMdioError = ethernet::MdioError::None;
+ typename Mac::LinkUpdateError linkUpdateError = Mac::LinkUpdateError::None;
+ AdapterInitializationError adapterError = AdapterInitializationError::None;
+
+ explicit operator bool() const
+ {
+ return macError == Mac::InitializationError::None and
+ phyError == Phy::InitializationError::None and
+ phyMdioError == ethernet::MdioError::None and
+ linkMdioError == ethernet::MdioError::None and
+ linkUpdateError == Mac::LinkUpdateError::None and
+ adapterError == AdapterInitializationError::None;
+ }
+ };
+
+ struct [[nodiscard]] LinkPollResult
+ {
+ ethernet::LinkStatus status;
+ ethernet::MdioError phyError = ethernet::MdioError::None;
+ typename Mac::LinkUpdateError macError = Mac::LinkUpdateError::None;
+
+ constexpr explicit operator bool() const noexcept
+ {
+ return phyError == ethernet::MdioError::None and
+ macError == Mac::LinkUpdateError::None;
+ }
+ };
+
+ /**
+ * Initialize the MAC and register or refresh its lwIP network interface.
+ *
+ * The call initializes lwIP, then the MAC and PHY, registers the netif, and
+ * applies the first observed link state. A failed retry leaves an existing
+ * netif down and may be retried.
+ */
+ template
+ [[nodiscard]] static InitializationResult
+ initialize(StaticIPv4Configuration const& config, uint8_t priority = 5)
+ {
+ modm::lwip::initialize();
+
+ auto& state = getState();
+ if (not claimAdapterOwnership())
+ return {.adapterError = AdapterInitializationError::MacAlreadyBound};
+
+ // Initialize the MAC with the checksum mode selected by the lwIP module
+ const typename Mac::Configuration macConfiguration{
+ .macAddress = config.macAddress,
+ .checksumMode = MODM_LWIP_CHECKSUM_HARDWARE ?
+ Mac::ChecksumMode::Hardware : Mac::ChecksumMode::Software,
+ };
+ const auto macResult =
+ Mac::template initialize(macConfiguration, priority);
+ if (not macResult) {
+ state.adapterReady = false;
+ markNetifUnavailable();
+ if (not state.netifRegistered)
+ state.adapterOwner = nullptr;
+ return {.macError = macResult.error};
+ }
+
+ // Reset and configure the PHY
+ const auto phyResult = Phy::template initialize();
+ if (not phyResult) {
+ state.adapterReady = false;
+ markNetifUnavailable();
+ return {.phyError = phyResult.error, .phyMdioError = phyResult.mdioError};
+ }
+
+ // Register the netif or refresh its addressing
+ if (const auto error = configureNetif(config);
+ error != AdapterInitializationError::None)
+ return {.adapterError = error};
+
+ // Apply the first observed physical link state to the MAC and lwIP
+ state.adapterReady = true;
+ const auto link = updateLinkStatus();
+ if (link.phyError != ethernet::MdioError::None)
+ return {.linkMdioError = link.phyError};
+ if (link.macError != Mac::LinkUpdateError::None)
+ return {.linkUpdateError = link.macError};
+ return {};
+ }
+
+ /// Access the underlying netif for direct lwIP API use.
+ static struct netif*
+ netif()
+ {
+ return &getState().netif;
+ }
+
+ /// Select this interface as lwIP's default route. Interface must already be initialized.
+ [[nodiscard]] static bool
+ setDefault()
+ {
+ LwIPSingleThreadGuard guard;
+ auto& state = getState();
+ if (not ownsState() or not state.adapterReady or not state.netifRegistered)
+ return false;
+ netif_set_default(&state.netif);
+ return true;
+ }
+
+ /**
+ * Deliver at most @p frameLimit acquired frames to lwIP.
+ *
+ * The call stops when the receive ring is empty, the limit is reached, or an
+ * error occurs. Call after a receive wakeup or often enough to avoid filling
+ * the MAC receive ring.
+ */
+ [[nodiscard]] static err_t
+ pollInput(std::size_t frameLimit = Mac::RxDescriptorCount)
+ {
+ LwIPSingleThreadGuard guard;
+ auto& state = getState();
+ if (not ownsState() or not state.adapterReady or not state.netifRegistered)
+ return ERR_IF;
+ return drainInput(frameLimit);
+ }
+
+ /**
+ * Service the physical link and notify lwIP of its current state.
+ *
+ * Scheduling and rate limiting are owned by the application.
+ */
+ [[nodiscard]] static LinkPollResult
+ pollLink()
+ {
+ auto& state = getState();
+ if (not ownsState() or not state.adapterReady or not state.netifRegistered)
+ return {Mac::getLinkStatus(), {}, Mac::LinkUpdateError::NotInitialized};
+
+ return updateLinkStatus();
+ }
+
+ /// Return current cumulative receive statistics.
+ [[nodiscard]] static ReceiveStatistics
+ getCumulativeReceiveStatistics()
+ {
+ return getState().receiveStatistics;
+ }
+
+ // Pass-throughs to the configured MAC.
+ static ethernet::LinkStatus
+ getLinkStatus()
+ {
+ return Mac::getLinkStatus();
+ }
+
+ static auto
+ consumeWakeupEvents()
+ requires requires { Mac::consumeWakeupEvents(); }
+ {
+ return Mac::consumeWakeupEvents();
+ }
+
+ static bool
+ hasPendingWakeupEvents()
+ requires requires { Mac::hasPendingWakeupEvents(); }
+ {
+ return Mac::hasPendingWakeupEvents();
+ }
+
+ static auto
+ getErrorCounters()
+ requires requires { Mac::getErrorCounters(); }
+ {
+ return Mac::getErrorCounters();
+ }
+
+ static auto
+ getHardwareErrorStatus()
+ requires requires { Mac::getHardwareErrorStatus(); }
+ {
+ return Mac::getHardwareErrorStatus();
+ }
+
+ static void
+ resetErrorCounters()
+ requires requires { Mac::resetErrorCounters(); }
+ {
+ Mac::resetErrorCounters();
+ }
+
+ static void
+ resetHardwareErrorStatus()
+ requires requires { Mac::resetHardwareErrorStatus(); }
+ {
+ Mac::resetHardwareErrorStatus();
+ }
+
+private:
+ // Dummy static memory location to give a per-(MAC x PHY) address for ownership
+ // tracking. Per-MAC EthernetInterfaceState remembers which LwipEthernet
+ // configured it to ensure we don't confuse different PHYs with the same MAC.
+ // Today we don't support multiple PHYs at once.
+ inline static constexpr char OwnerMarker{};
+
+ static bool
+ claimAdapterOwnership()
+ {
+ auto& state = getState();
+ auto const* const owner = &OwnerMarker;
+ if (state.adapterOwner != nullptr and state.adapterOwner != owner)
+ return false;
+ state.adapterOwner = owner;
+ return true;
+ }
+
+ static State&
+ getState()
+ {
+ return detail::ethernetInterfaceState;
+ }
+
+ static bool
+ ownsState()
+ {
+ return getState().adapterOwner == &OwnerMarker;
+ }
+
+ static AdapterInitializationError
+ configureNetif(StaticIPv4Configuration const& config)
+ {
+ auto& state = getState();
+ const ip4_addr_t ipAddress = detail::makeIp4(config.ipAddress);
+ const ip4_addr_t netmask = detail::makeIp4(config.netmask);
+ const ip4_addr_t gateway = detail::makeIp4(config.gateway);
+
+ LwIPSingleThreadGuard guard;
+ state.macAddress = config.macAddress;
+ if (state.netifRegistered) {
+ std::memcpy(state.netif.hwaddr, state.macAddress.data(), state.macAddress.size());
+ netif_set_addr(&state.netif, &ipAddress, &netmask, &gateway);
+#if LWIP_CHECKSUM_CTRL_PER_NETIF
+ NETIF_SET_CHECKSUM_CTRL(&state.netif, buildConfiguredChecksumFlags());
+#endif
+ }
+ else {
+ std::memset(&state.netif, 0, sizeof(state.netif));
+ if (netif_add(&state.netif, &ipAddress, &netmask, &gateway, nullptr,
+ &LwipEthernet::netifInitialize, ethernet_input) == nullptr) {
+ state.adapterOwner = nullptr;
+ return AdapterInitializationError::NetifRegistrationFailed;
+ }
+ state.netifRegistered = true;
+ }
+ netif_set_up(&state.netif);
+ return AdapterInitializationError::None;
+ }
+
+ static LinkPollResult
+ updateLinkStatus()
+ {
+ const auto observed = Phy::template readLinkStatus();
+ if (not observed)
+ return {Mac::getLinkStatus(), observed.error, {}};
+ const auto updated = Mac::notifyUpdatedLinkStatus(observed.status);
+ {
+ LwIPSingleThreadGuard guard;
+ setNetifLinkStatus(updated.status);
+ }
+ return {updated.status, {}, updated.error};
+ }
+
+ static void
+ markNetifUnavailable()
+ {
+ auto& state = getState();
+ if (not state.netifRegistered)
+ return;
+ LwIPSingleThreadGuard guard;
+ netif_set_down(&state.netif);
+ netif_set_link_down(&state.netif);
+ }
+
+ static err_t
+ netifInitialize(struct netif* netif)
+ {
+ auto& state = getState();
+ netif->name[0] = 'e';
+ netif->name[1] = 'n';
+ netif->output = etharp_output;
+ netif->linkoutput = &LwipEthernet::lowLevelOutput;
+ netif->mtu = detail::EthernetMtu;
+ netif->flags = NETIF_FLAG_BROADCAST | NETIF_FLAG_ETHARP | NETIF_FLAG_ETHERNET;
+ // NETIF_FLAG_UP and NETIF_FLAG_LINK_UP are managed by the netif APIs.
+ netif->hwaddr_len = ETH_HWADDR_LEN;
+ std::memcpy(netif->hwaddr, state.macAddress.data(), state.macAddress.size());
+#if LWIP_CHECKSUM_CTRL_PER_NETIF
+ NETIF_SET_CHECKSUM_CTRL(netif, buildConfiguredChecksumFlags());
+#endif
+ return ERR_OK;
+ }
+
+ static err_t
+ lowLevelOutput(struct netif*, struct pbuf* p)
+ {
+ auto& state = getState();
+ if (not ownsState() or not state.adapterReady or not state.netifRegistered)
+ return ERR_IF;
+
+ const std::size_t length = p->tot_len;
+ if (length > Mac::MaxFrameSize)
+ return ERR_BUF;
+
+ auto lease = Mac::acquireTransmitBuffer(length);
+ if (not lease) {
+ incrementTransmitErrorStatistics(lease.error());
+ return mapTransmitErrorToLwipError(lease.error());
+ }
+ auto buffer = lease.buffer();
+ modm_assert(buffer.size() >= length, "lwip.tx.buffer",
+ "Ethernet MAC returned an undersized transmit buffer", buffer.size());
+
+ std::size_t offset = 0;
+ for (struct pbuf* q = p; q != nullptr; q = q->next) {
+ if (offset + q->len > length)
+ return ERR_BUF;
+ std::memcpy(buffer.data() + offset, q->payload, q->len);
+ offset += q->len;
+ }
+
+ if (offset != length)
+ return ERR_BUF;
+ const auto result = lease.commit();
+ if (not result) {
+ incrementTransmitErrorStatistics(result.error);
+ return mapTransmitErrorToLwipError(result.error);
+ }
+ LINK_STATS_INC(link.xmit);
+ return ERR_OK;
+ }
+
+ static err_t
+ drainInput(std::size_t frameLimit)
+ {
+ auto& state = getState();
+ for (std::size_t processed = 0; processed < frameLimit; ++processed) {
+ auto lease = Mac::tryAcquireReceiveBuffer();
+ if (not lease) {
+ if (lease.error() == Mac::ReceiveError::NoFrameAvailable)
+ return ERR_OK;
+ LINK_STATS_INC(link.err);
+ return ERR_IF;
+ }
+
+ incrementSaturating(state.receiveStatistics.acquiredFrames);
+ const auto checksumStatus = lease.checksumStatus();
+ if (checksumStatus == Mac::ReceiveChecksumStatus::Invalid) {
+ incrementSaturating(state.receiveStatistics.droppedFrames);
+ incrementSaturating(state.receiveStatistics.checksumDrops);
+ LINK_STATS_INC(link.chkerr);
+ LINK_STATS_INC(link.drop);
+ continue;
+ }
+
+ const auto frame = lease.buffer();
+ struct pbuf* p = pbuf_alloc(PBUF_RAW, frame.size(), PBUF_POOL);
+ if (p == nullptr) {
+ incrementSaturating(state.receiveStatistics.droppedFrames);
+ incrementSaturating(state.receiveStatistics.allocationDrops);
+ LINK_STATS_INC(link.memerr);
+ LINK_STATS_INC(link.drop);
+ return ERR_MEM;
+ }
+
+ const err_t copyError = pbuf_take(p, frame.data(), frame.size());
+ lease.release();
+ modm_assert(copyError == ERR_OK, "lwip.pbuf",
+ "receive frame copy failed", copyError);
+
+ LINK_STATS_INC(link.recv);
+#if MODM_LWIP_CHECKSUM_HARDWARE
+ ChecksumCheckScope checksumScope{
+ state.netif, checksumStatus == Mac::ReceiveChecksumStatus::NotChecked};
+#endif
+ const err_t inputError = state.netif.input(p, &state.netif);
+ if (inputError != ERR_OK) {
+ pbuf_free(p);
+ incrementSaturating(state.receiveStatistics.droppedFrames);
+ LINK_STATS_INC(link.err);
+ LINK_STATS_INC(link.drop);
+ return inputError;
+ }
+ }
+ return ERR_OK;
+ }
+
+ static void
+ setNetifLinkStatus(ethernet::LinkStatus status)
+ {
+ if (status.state == ethernet::LinkState::Up)
+ netif_set_link_up(&getState().netif);
+ else
+ netif_set_link_down(&getState().netif);
+ }
+
+#if LWIP_CHECKSUM_CTRL_PER_NETIF
+ static constexpr uint16_t
+ buildConfiguredChecksumFlags()
+ {
+ // Disable lwIP software checksums when hardware checksums are enabled.
+ // ChecksumCheckScope temporarily re-enables RX software checksums for
+ // incoming fragments that the MAC leaves unchecked.
+ return MODM_LWIP_CHECKSUM_HARDWARE ?
+ uint16_t(0) :
+ uint16_t(NETIF_CHECKSUM_ENABLE_ALL);
+ }
+#endif
+
+#if MODM_LWIP_CHECKSUM_HARDWARE
+ class ChecksumCheckScope
+ {
+ public:
+ ChecksumCheckScope(struct netif& netif, bool enable) :
+ netif(netif), previous(netif.chksum_flags), changed(enable and MODM_LWIP_CHECKSUM_HARDWARE)
+ {
+ if (changed) {
+ constexpr uint16_t ReceiveChecksums = NETIF_CHECKSUM_CHECK_IP |
+ NETIF_CHECKSUM_CHECK_UDP | NETIF_CHECKSUM_CHECK_TCP |
+ NETIF_CHECKSUM_CHECK_ICMP;
+ NETIF_SET_CHECKSUM_CTRL(&netif, uint16_t(previous | ReceiveChecksums));
+ }
+ }
+
+ ~ChecksumCheckScope()
+ {
+ if (changed)
+ NETIF_SET_CHECKSUM_CTRL(&netif, previous);
+ }
+
+ private:
+ struct netif& netif;
+ uint16_t previous;
+ bool changed;
+ };
+#endif
+
+ static err_t
+ mapTransmitErrorToLwipError(typename Mac::TransmitError error)
+ {
+ switch (error) {
+ case Mac::TransmitError::None:
+ return ERR_OK;
+ case Mac::TransmitError::Busy:
+ return ERR_MEM;
+ case Mac::TransmitError::InvalidLength:
+ return ERR_BUF;
+ case Mac::TransmitError::UnsupportedFragmentation:
+ return ERR_VAL;
+ case Mac::TransmitError::LinkDown:
+ case Mac::TransmitError::NotInitialized:
+ case Mac::TransmitError::Faulted:
+ return ERR_IF;
+ }
+ return ERR_IF;
+ }
+
+ static void
+ incrementTransmitErrorStatistics(typename Mac::TransmitError error)
+ {
+ switch (error) {
+ case Mac::TransmitError::None:
+ return;
+ case Mac::TransmitError::Busy:
+ LINK_STATS_INC(link.memerr);
+ return;
+ case Mac::TransmitError::InvalidLength:
+ LINK_STATS_INC(link.lenerr);
+ return;
+ case Mac::TransmitError::UnsupportedFragmentation:
+ case Mac::TransmitError::LinkDown:
+ case Mac::TransmitError::NotInitialized:
+ case Mac::TransmitError::Faulted:
+ LINK_STATS_INC(link.err);
+ return;
+ }
+ }
+
+ static void
+ incrementSaturating(uint64_t& counter)
+ {
+ if (counter != std::numeric_limits::max())
+ ++counter;
+ }
+};
+
+} // namespace modm::lwip
+
+#endif // MODM_LWIP_ETHERNET_HPP
diff --git a/ext/lwip/lwip b/ext/lwip/lwip
new file mode 160000
index 0000000000..9de573715d
--- /dev/null
+++ b/ext/lwip/lwip
@@ -0,0 +1 @@
+Subproject commit 9de573715d1db316adb7b9368abdcf69e0076651
diff --git a/ext/lwip/modm_lwip.hpp b/ext/lwip/modm_lwip.hpp
new file mode 100644
index 0000000000..bee374df9a
--- /dev/null
+++ b/ext/lwip/modm_lwip.hpp
@@ -0,0 +1,60 @@
+/*
+ * Copyright (c) 2026, Kaelin Laundry
+ *
+ * This file is part of the modm project.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+#ifndef MODM_LWIP_HPP
+#define MODM_LWIP_HPP
+
+#include
+#include
+
+namespace modm::lwip
+{
+
+/** RAII guard to ensure lwIP and callbacks it invokes never yield or are called
+ * in an ISR. lwIP is not thread-safe. Backs lwIP's LWIP_ASSERT_CORE_LOCKED check. */
+class LwIPSingleThreadGuard
+{
+public:
+ LwIPSingleThreadGuard() { modm_lwip_single_thread_acquire(); }
+ ~LwIPSingleThreadGuard() { modm_lwip_single_thread_release(); }
+
+ LwIPSingleThreadGuard(LwIPSingleThreadGuard const&) = delete;
+ LwIPSingleThreadGuard& operator=(LwIPSingleThreadGuard const&) = delete;
+ LwIPSingleThreadGuard(LwIPSingleThreadGuard&&) = delete;
+ LwIPSingleThreadGuard& operator=(LwIPSingleThreadGuard&&) = delete;
+};
+
+namespace detail
+{
+inline bool initialized{false};
+}
+
+inline void
+initialize()
+{
+ LwIPSingleThreadGuard guard;
+ if (not detail::initialized) {
+ lwip_init();
+ detail::initialized = true;
+ }
+}
+
+/// Process all lwIP timeouts that are currently due.
+inline void
+processTimeouts()
+{
+ LwIPSingleThreadGuard guard;
+ if (detail::initialized)
+ sys_check_timeouts();
+}
+
+} // namespace modm::lwip
+
+#endif // MODM_LWIP_HPP
diff --git a/ext/lwip/module.lb b/ext/lwip/module.lb
new file mode 100644
index 0000000000..cd3ef1ece9
--- /dev/null
+++ b/ext/lwip/module.lb
@@ -0,0 +1,114 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2026, Kaelin Laundry
+#
+# This file is part of the modm project.
+#
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+# -----------------------------------------------------------------------------
+
+def init(module):
+ module.name = "lwip"
+ module.description = """
+# lwIP Callback API
+
+lwIP 2.2.1 configured for `NO_SYS=1`. IPv4, Ethernet, ARP, ICMP, UDP, and TCP
+are enabled by default. Optional callback API protocols and resource sizes can
+be selected in an application tuning header. Sequential netconn and socket APIs
+are not included.
+
+Applications must hold `modm::lwip::LwIPSingleThreadGuard` while calling lwIP APIs.
+
+When `modm:architecture:ethernet` is selected, ``
+provides an adapter that connects a Clause 22 PHY and Ethernet MAC to an lwIP
+network interface.
+
+Hardware checksum offload cannot be combined with transmit-side IPv4
+fragmentation. lwIP invokes a netif's `linkoutput` callback once per fragment,
+where the MAC no longer sees the complete transport datagram. Receive
+reassembly remains available and unchecked frames use software verification.
+"""
+
+def prepare(module, options):
+ module.add_option(
+ EnumerationOption(
+ name="debug",
+ description="lwIP debug logging: off, network diagnostics, or all compiled debug categories.",
+ enumeration=["off", "network", "all"],
+ default="off"))
+ module.add_option(
+ BooleanOption(
+ name="stats",
+ description="Enable lwIP runtime statistics for diagnostics.",
+ default=False))
+ module.add_option(
+ EnumerationOption(
+ name="checksum",
+ description="Checksum mode for lwIP Ethernet traffic.",
+ enumeration=["hardware", "software"],
+ default="software"))
+ module.add_option(
+ BooleanOption(
+ name="ip.fragmentation",
+ description="Enable outgoing IPv4 fragmentation.",
+ default=True))
+ module.add_option(
+ StringOption(
+ name="tuning.header",
+ description="Optional resource and protocol tuning header on the application include path.",
+ default=""))
+ module.depends(":architecture:clock", ":architecture:assert",
+ ":architecture:interrupt", ":debug", ":stdc++")
+ return True
+
+def validate(env):
+ if env["checksum"] == "hardware" and env["ip.fragmentation"]:
+ raise ValidateException(
+ "Hardware checksum offload cannot be combined with outgoing IPv4 fragmentation.")
+
+def build(env):
+ target = env[":target"].identifier
+ env.substitutions = {
+ "lwip_debug": env["debug"],
+ "lwip_stats": env["stats"],
+ "lwip_checksum": env["checksum"],
+ "lwip_ip_fragmentation": env["ip.fragmentation"],
+ "lwip_tuning_header": env["tuning.header"],
+ # Required allocation alignment for the target ABI.
+ "lwip_mem_alignment": 8 if target.platform == "hosted" else 4,
+ }
+ env.outbasepath = "modm/ext/lwip"
+ env.collect(":build:path.include", "modm/ext/lwip")
+ env.collect(":build:path.include", "modm/ext/lwip/port/include")
+ env.collect(":build:path.include", "modm/ext/lwip/include")
+
+ env.copy("lwip/COPYING", "COPYING")
+ env.copy("lwip/README.md", "README.md")
+ env.copy("lwip/src/include", "include")
+ env.template("port/include/arch/cc.h.in", "port/include/arch/cc.h")
+ env.template("port/include/lwipopts.h.in", "port/include/lwipopts.h")
+ env.template("port/include/modm_lwip_tuning.h.in", "port/include/modm_lwip_tuning.h")
+ env.copy("port/sys_now.cpp", "port/sys_now.cpp")
+ env.copy("port/platform.cpp", "port/platform.cpp")
+ env.copy("port/single_thread_guard.cpp", "port/single_thread_guard.cpp")
+ env.copy("modm_lwip.hpp")
+ if env.has_module(":architecture:ethernet"):
+ env.copy("ethernet/ethernet.hpp", "modm_lwip/ethernet.hpp")
+
+ for source in [
+ "api/err.c", "core/def.c", "core/dns.c", "core/init.c",
+ "core/inet_chksum.c", "core/ip.c", "core/mem.c", "core/memp.c",
+ "core/netif.c", "core/pbuf.c", "core/raw.c", "core/stats.c",
+ "core/tcp.c", "core/tcp_in.c", "core/tcp_out.c", "core/timeouts.c",
+ "core/udp.c", "core/ipv4/acd.c", "core/ipv4/autoip.c",
+ "core/ipv4/dhcp.c", "core/ipv4/etharp.c", "core/ipv4/igmp.c",
+ "core/ipv4/icmp.c", "core/ipv4/ip4.c", "core/ipv4/ip4_addr.c",
+ "core/ipv4/ip4_frag.c", "core/ipv6/dhcp6.c", "core/ipv6/ethip6.c",
+ "core/ipv6/icmp6.c", "core/ipv6/inet6.c", "core/ipv6/ip6.c",
+ "core/ipv6/ip6_addr.c", "core/ipv6/ip6_frag.c", "core/ipv6/mld6.c",
+ "core/ipv6/nd6.c", "netif/ethernet.c",
+ ]:
+ env.copy("lwip/src/" + source, source)
diff --git a/ext/lwip/port/include/arch/cc.h.in b/ext/lwip/port/include/arch/cc.h.in
new file mode 100644
index 0000000000..b2800a294f
--- /dev/null
+++ b/ext/lwip/port/include/arch/cc.h.in
@@ -0,0 +1,34 @@
+/*
+ * Copyright (c) 2026, Kaelin Laundry
+ *
+ * This file is part of the modm project.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+#ifndef MODM_LWIP_ARCH_CC_H
+#define MODM_LWIP_ARCH_CC_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+// modm implementations of lwIP platform hooks.
+void modm_lwip_diag(const char *format, ...);
+void modm_lwip_assert_fail(const char *message, const char *file, int line);
+void modm_lwip_single_thread_acquire(void);
+void modm_lwip_single_thread_release(void);
+void modm_lwip_assert_core_locked(void);
+
+#ifdef __cplusplus
+}
+#endif
+
+// lwIP passes diagnostics as a parenthesized printf-style argument tuple.
+#define LWIP_PLATFORM_DIAG(message) do { modm_lwip_diag message; } while (0)
+#define LWIP_PLATFORM_ASSERT(message) do { modm_lwip_assert_fail((message), __FILE__, __LINE__); } while (0)
+#define LWIP_ASSERT_CORE_LOCKED() modm_lwip_assert_core_locked()
+
+#endif // MODM_LWIP_ARCH_CC_H
diff --git a/ext/lwip/port/include/lwipopts.h.in b/ext/lwip/port/include/lwipopts.h.in
new file mode 100644
index 0000000000..fa6cd06858
--- /dev/null
+++ b/ext/lwip/port/include/lwipopts.h.in
@@ -0,0 +1,295 @@
+/*
+ * Copyright (c) 2026, Kaelin Laundry
+ *
+ * This file is part of the modm project.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+#ifndef MODM_LWIPOPTS_H
+#define MODM_LWIPOPTS_H
+
+#define NO_SYS 1
+#define SYS_LIGHTWEIGHT_PROT 0
+#define LWIP_IPV4 1
+#define LWIP_ARP 1
+#define LWIP_ETHERNET 1
+#define LWIP_CALLBACK_API 1
+#define LWIP_NETCONN 0
+#define LWIP_SOCKET 0
+#define LWIP_NETIF_API 0
+
+// Application overrides are included before optional protocol defaults.
+#include
+
+#ifndef LWIP_IPV6
+#define LWIP_IPV6 0
+#endif
+#ifndef LWIP_ICMP
+#define LWIP_ICMP 1
+#endif
+#ifndef LWIP_RAW
+#define LWIP_RAW 0
+#endif
+#ifndef LWIP_UDP
+#define LWIP_UDP 1
+#endif
+#ifndef LWIP_TCP
+#define LWIP_TCP 1
+#endif
+#ifndef LWIP_DHCP
+#define LWIP_DHCP 0
+#endif
+#ifndef LWIP_AUTOIP
+#define LWIP_AUTOIP 0
+#endif
+#ifndef LWIP_DNS
+#define LWIP_DNS 0
+#endif
+#ifndef LWIP_IGMP
+#define LWIP_IGMP 0
+#endif
+
+#ifndef LWIP_NETIF_LINK_CALLBACK
+#define LWIP_NETIF_LINK_CALLBACK 0
+#endif
+#ifndef LWIP_NETIF_STATUS_CALLBACK
+#define LWIP_NETIF_STATUS_CALLBACK 0
+#endif
+
+#ifndef IP_FRAG
+#define IP_FRAG {{ 1 if lwip_ip_fragmentation else 0 }}
+#endif
+#ifndef IP_REASSEMBLY
+#define IP_REASSEMBLY 1
+#endif
+
+#ifndef MODM_LWIP_CHECKSUM_HARDWARE
+%% if lwip_checksum == "hardware"
+#define MODM_LWIP_CHECKSUM_HARDWARE 1
+%% else
+#define MODM_LWIP_CHECKSUM_HARDWARE 0
+%% endif
+#endif
+#ifndef LWIP_CHECKSUM_CTRL_PER_NETIF
+#define LWIP_CHECKSUM_CTRL_PER_NETIF 1
+#endif
+// Keep software checksum implementations compiled. Per-netif flags select
+// hardware offload and temporarily restore RX checks for unchecked frames.
+#ifndef CHECKSUM_GEN_IP
+#define CHECKSUM_GEN_IP 1
+#endif
+#ifndef CHECKSUM_GEN_UDP
+#define CHECKSUM_GEN_UDP 1
+#endif
+#ifndef CHECKSUM_GEN_TCP
+#define CHECKSUM_GEN_TCP 1
+#endif
+#ifndef CHECKSUM_GEN_ICMP
+#define CHECKSUM_GEN_ICMP 1
+#endif
+#ifndef CHECKSUM_CHECK_IP
+#define CHECKSUM_CHECK_IP 1
+#endif
+#ifndef CHECKSUM_CHECK_UDP
+#define CHECKSUM_CHECK_UDP 1
+#endif
+#ifndef CHECKSUM_CHECK_TCP
+#define CHECKSUM_CHECK_TCP 1
+#endif
+#ifndef CHECKSUM_CHECK_ICMP
+#define CHECKSUM_CHECK_ICMP 1
+#endif
+#ifndef CHECKSUM_GEN_ICMP6
+#define CHECKSUM_GEN_ICMP6 1
+#endif
+#ifndef CHECKSUM_CHECK_ICMP6
+#define CHECKSUM_CHECK_ICMP6 1
+#endif
+#ifndef LWIP_CHECKSUM_ON_COPY
+#define LWIP_CHECKSUM_ON_COPY 0
+#endif
+
+#if MODM_LWIP_CHECKSUM_HARDWARE && IP_FRAG
+#error "Hardware checksum mode does not support outgoing IPv4 fragmentation"
+#endif
+
+%% if lwip_stats
+#ifndef LWIP_STATS
+#define LWIP_STATS 1
+#endif
+#ifndef MEM_STATS
+#define MEM_STATS 1
+#endif
+#ifndef MEMP_STATS
+#define MEMP_STATS 1
+#endif
+#ifndef LINK_STATS
+#define LINK_STATS 1
+#endif
+#ifndef IP_STATS
+#define IP_STATS 1
+#endif
+#ifndef ICMP_STATS
+#define ICMP_STATS 1
+#endif
+#ifndef UDP_STATS
+#define UDP_STATS 1
+#endif
+#ifndef TCP_STATS
+#define TCP_STATS 1
+#endif
+#ifndef ETHARP_STATS
+#define ETHARP_STATS 1
+#endif
+%% else
+#ifndef LWIP_STATS
+#define LWIP_STATS 0
+#endif
+%% endif
+
+%% if lwip_debug != "off"
+#ifndef LWIP_DEBUG
+#define LWIP_DEBUG 1
+#endif
+#ifndef LWIP_DBG_MIN_LEVEL
+%% if lwip_debug == "network"
+#define LWIP_DBG_MIN_LEVEL LWIP_DBG_LEVEL_WARNING
+%% else
+#define LWIP_DBG_MIN_LEVEL LWIP_DBG_LEVEL_ALL
+%% endif
+#endif
+#ifndef LWIP_DBG_TYPES_ON
+%% if lwip_debug == "all"
+#define LWIP_DBG_TYPES_ON \
+ (LWIP_DBG_ON | LWIP_DBG_TRACE | LWIP_DBG_STATE | LWIP_DBG_FRESH)
+%% else
+#define LWIP_DBG_TYPES_ON LWIP_DBG_ON
+%% endif
+#endif
+
+#ifndef NETIF_DEBUG
+#define NETIF_DEBUG LWIP_DBG_ON
+#endif
+#ifndef ETHARP_DEBUG
+#define ETHARP_DEBUG LWIP_DBG_ON
+#endif
+#ifndef IP_DEBUG
+#define IP_DEBUG LWIP_DBG_ON
+#endif
+#ifndef ICMP_DEBUG
+#define ICMP_DEBUG LWIP_DBG_ON
+#endif
+#ifndef UDP_DEBUG
+#define UDP_DEBUG LWIP_DBG_ON
+#endif
+#ifndef TCP_DEBUG
+#define TCP_DEBUG LWIP_DBG_ON
+#endif
+#ifndef TCP_INPUT_DEBUG
+#define TCP_INPUT_DEBUG LWIP_DBG_ON
+#endif
+%% if lwip_debug == "all"
+#ifndef TCP_OUTPUT_DEBUG
+#define TCP_OUTPUT_DEBUG LWIP_DBG_ON
+#endif
+#ifndef TCP_RTO_DEBUG
+#define TCP_RTO_DEBUG LWIP_DBG_ON
+#endif
+#ifndef TCP_CWND_DEBUG
+#define TCP_CWND_DEBUG LWIP_DBG_ON
+#endif
+#ifndef TCP_WND_DEBUG
+#define TCP_WND_DEBUG LWIP_DBG_ON
+#endif
+#ifndef TCP_FR_DEBUG
+#define TCP_FR_DEBUG LWIP_DBG_ON
+#endif
+#ifndef TCP_RST_DEBUG
+#define TCP_RST_DEBUG LWIP_DBG_ON
+#endif
+#ifndef TCP_QLEN_DEBUG
+#define TCP_QLEN_DEBUG LWIP_DBG_ON
+#endif
+#ifndef PBUF_DEBUG
+#define PBUF_DEBUG LWIP_DBG_ON
+#endif
+#ifndef INET_DEBUG
+#define INET_DEBUG LWIP_DBG_ON
+#endif
+#ifndef IP_REASS_DEBUG
+#define IP_REASS_DEBUG LWIP_DBG_ON
+#endif
+#ifndef RAW_DEBUG
+#define RAW_DEBUG LWIP_DBG_ON
+#endif
+#ifndef MEM_DEBUG
+#define MEM_DEBUG LWIP_DBG_ON
+#endif
+#ifndef MEMP_DEBUG
+#define MEMP_DEBUG LWIP_DBG_ON
+#endif
+#ifndef SYS_DEBUG
+#define SYS_DEBUG LWIP_DBG_ON
+#endif
+#ifndef TIMERS_DEBUG
+#define TIMERS_DEBUG LWIP_DBG_ON
+#endif
+#ifndef IGMP_DEBUG
+#define IGMP_DEBUG LWIP_DBG_ON
+#endif
+#ifndef DHCP_DEBUG
+#define DHCP_DEBUG LWIP_DBG_ON
+#endif
+#ifndef AUTOIP_DEBUG
+#define AUTOIP_DEBUG LWIP_DBG_ON
+#endif
+#ifndef ACD_DEBUG
+#define ACD_DEBUG LWIP_DBG_ON
+#endif
+#ifndef DNS_DEBUG
+#define DNS_DEBUG LWIP_DBG_ON
+#endif
+#ifndef IP6_DEBUG
+#define IP6_DEBUG LWIP_DBG_ON
+#endif
+#ifndef DHCP6_DEBUG
+#define DHCP6_DEBUG LWIP_DBG_ON
+#endif
+#ifndef ICMP6_DEBUG
+#define ICMP6_DEBUG LWIP_DBG_ON
+#endif
+#ifndef MLD6_DEBUG
+#define MLD6_DEBUG LWIP_DBG_ON
+#endif
+#ifndef ND6_DEBUG
+#define ND6_DEBUG LWIP_DBG_ON
+#endif
+%% else
+#ifndef TCP_OUTPUT_DEBUG
+#define TCP_OUTPUT_DEBUG LWIP_DBG_OFF
+#endif
+#ifndef TCP_RTO_DEBUG
+#define TCP_RTO_DEBUG LWIP_DBG_OFF
+#endif
+#ifndef TCP_CWND_DEBUG
+#define TCP_CWND_DEBUG LWIP_DBG_OFF
+#endif
+#ifndef TCP_WND_DEBUG
+#define TCP_WND_DEBUG LWIP_DBG_OFF
+#endif
+#ifndef TCP_FR_DEBUG
+#define TCP_FR_DEBUG LWIP_DBG_OFF
+#endif
+#ifndef TCP_RST_DEBUG
+#define TCP_RST_DEBUG LWIP_DBG_OFF
+#endif
+#ifndef TCP_QLEN_DEBUG
+#define TCP_QLEN_DEBUG LWIP_DBG_OFF
+#endif
+%% endif
+%% endif
+
+#endif // MODM_LWIPOPTS_H
diff --git a/ext/lwip/port/include/modm_lwip_tuning.h.in b/ext/lwip/port/include/modm_lwip_tuning.h.in
new file mode 100644
index 0000000000..8ec1282324
--- /dev/null
+++ b/ext/lwip/port/include/modm_lwip_tuning.h.in
@@ -0,0 +1,34 @@
+/*
+ * Copyright (c) 2026, Kaelin Laundry
+ *
+ * This file is part of the modm project.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+#ifndef MODM_LWIP_TUNING_H
+#define MODM_LWIP_TUNING_H
+
+%% if lwip_tuning_header
+#include <{{lwip_tuning_header}}>
+%% endif
+
+#ifndef MEM_ALIGNMENT
+#define MEM_ALIGNMENT {{lwip_mem_alignment}}
+#endif
+#ifndef MEM_SIZE
+#define MEM_SIZE (32 * 1024)
+#endif
+#ifndef TCP_MSS
+#define TCP_MSS 1460
+#endif
+#ifndef TCP_SND_BUF
+#define TCP_SND_BUF (4 * TCP_MSS)
+#endif
+#ifndef TCP_LISTEN_BACKLOG
+#define TCP_LISTEN_BACKLOG 1
+#endif
+
+#endif // MODM_LWIP_TUNING_H
diff --git a/ext/lwip/port/platform.cpp b/ext/lwip/port/platform.cpp
new file mode 100644
index 0000000000..65935506dc
--- /dev/null
+++ b/ext/lwip/port/platform.cpp
@@ -0,0 +1,34 @@
+/*
+ * Copyright (c) 2026, Kaelin Laundry
+ *
+ * This file is part of the modm project.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+#include
+#include
+
+#include
+#include
+
+extern "C" void
+modm_lwip_diag(const char* format, ...)
+{
+ MODM_LOG_DEBUG << "[lwIP] ";
+ va_list args;
+ va_start(args, format);
+ MODM_LOG_DEBUG.vprintf(format, args);
+ va_end(args);
+ MODM_LOG_DEBUG << modm::endl;
+}
+
+extern "C" void
+modm_lwip_assert_fail(const char* message, const char* file, int line)
+{
+ char description[192];
+ std::snprintf(description, sizeof(description), "%s at %s:%d", message, file, line);
+ modm_assert(false, "lwip.assert", description, line);
+}
diff --git a/ext/lwip/port/single_thread_guard.cpp b/ext/lwip/port/single_thread_guard.cpp
new file mode 100644
index 0000000000..1b893bd1e6
--- /dev/null
+++ b/ext/lwip/port/single_thread_guard.cpp
@@ -0,0 +1,47 @@
+/*
+ * Copyright (c) 2026, Kaelin Laundry
+ *
+ * This file is part of the modm project.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+#include
+#include
+
+#include
+
+namespace
+{
+
+// lwIP calls are synchronous and must not yield. Entry tracking catches
+// accidental re-entry added to these call paths later.
+std::atomic_bool entered{false};
+
+} // namespace
+
+extern "C" void
+modm_lwip_single_thread_acquire()
+{
+ modm_assert(not modm::isInterruptContext(), "lwip.context",
+ "lwIP cannot be entered from an interrupt");
+ modm_assert(not entered.exchange(true), "lwip.owner", "lwIP core already locked");
+}
+
+extern "C" void
+modm_lwip_single_thread_release()
+{
+ modm_assert(not modm::isInterruptContext(), "lwip.context",
+ "lwIP cannot be released from an interrupt");
+ modm_assert(entered.exchange(false), "lwip.owner", "lwIP core not locked");
+}
+
+extern "C" void
+modm_lwip_assert_core_locked()
+{
+ modm_assert(not modm::isInterruptContext(), "lwip.context",
+ "lwIP cannot be entered from an interrupt");
+ modm_assert(entered.load(), "lwip.owner", "lwIP core not locked");
+}
diff --git a/ext/lwip/port/sys_now.cpp b/ext/lwip/port/sys_now.cpp
new file mode 100644
index 0000000000..220de79db5
--- /dev/null
+++ b/ext/lwip/port/sys_now.cpp
@@ -0,0 +1,19 @@
+/*
+ * Copyright (c) 2026, Kaelin Laundry
+ *
+ * This file is part of the modm project.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+#include
+
+#include
+
+extern "C" u32_t
+sys_now(void)
+{
+ return static_cast(modm::Clock::now().time_since_epoch().count());
+}
diff --git a/src/modm/architecture/interface/assert.h.in b/src/modm/architecture/interface/assert.h.in
index e2053a8ce0..3b789aea9b 100644
--- a/src/modm/architecture/interface/assert.h.in
+++ b/src/modm/architecture/interface/assert.h.in
@@ -30,7 +30,7 @@
#endif
// C-type information struct
-typedef struct modm_packed
+typedef struct
{
const char *name;
#if MODM_ASSERTION_INFO_HAS_DESCRIPTION
diff --git a/src/modm/architecture/interface/assert.hpp.in b/src/modm/architecture/interface/assert.hpp.in
index 6893c910de..159e0d6400 100644
--- a/src/modm/architecture/interface/assert.hpp.in
+++ b/src/modm/architecture/interface/assert.hpp.in
@@ -35,7 +35,7 @@ Abandonment : uint8_t
using AbandonmentBehavior = Abandonment;
/// Contains information about the failed assertion.
-struct modm_packed
+struct
AssertionInfo
{
const char *name; ///< Can be used to recognize the assertion in code
diff --git a/src/modm/architecture/interface/ethernet.hpp b/src/modm/architecture/interface/ethernet.hpp
new file mode 100644
index 0000000000..8e63a8595a
--- /dev/null
+++ b/src/modm/architecture/interface/ethernet.hpp
@@ -0,0 +1,87 @@
+/*
+ * Copyright (c) 2026, Kaelin Laundry
+ *
+ * This file is part of the modm project.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+#ifndef MODM_ARCHITECTURE_ETHERNET_HPP
+#define MODM_ARCHITECTURE_ETHERNET_HPP
+
+#include
+#include
+#include
+#include
+
+namespace modm::ethernet
+{
+
+using MacAddress = std::array;
+
+enum class MediaInterface : uint8_t
+{
+ MII,
+ RMII,
+};
+
+enum class Speed : uint8_t
+{
+ Speed10M,
+ Speed100M,
+};
+
+enum class DuplexMode : uint8_t
+{
+ Half,
+ Full,
+};
+
+struct LinkMode
+{
+ Speed speed = Speed::Speed100M;
+ DuplexMode duplex = DuplexMode::Full;
+
+ friend bool operator==(LinkMode const&, LinkMode const&) = default;
+};
+
+enum class LinkState : uint8_t
+{
+ Down,
+ Negotiating,
+ Up,
+};
+
+struct LinkStatus
+{
+ LinkState state = LinkState::Down;
+ std::optional mode;
+};
+
+enum class ChecksumMode : uint8_t
+{
+ Software,
+ Hardware,
+};
+
+enum class MdioError : uint8_t
+{
+ None,
+ Busy,
+ Timeout,
+ InvalidRegister,
+ InvalidPhyAddress,
+};
+
+template
+concept Clause22Mdio = requires(uint8_t phyAddress, uint8_t reg,
+ uint16_t value, uint16_t& result) {
+ { Controller::writePhyRegister(phyAddress, reg, value) } -> std::same_as;
+ { Controller::readPhyRegister(phyAddress, reg, result) } -> std::same_as;
+};
+
+} // namespace modm::ethernet
+
+#endif // MODM_ARCHITECTURE_ETHERNET_HPP
diff --git a/src/modm/architecture/interface/interrupt.hpp b/src/modm/architecture/interface/interrupt.hpp
index 77e13d0925..a020118217 100644
--- a/src/modm/architecture/interface/interrupt.hpp
+++ b/src/modm/architecture/interface/interrupt.hpp
@@ -151,4 +151,33 @@
#endif // __DOXYGEN__
+#ifdef __cplusplus
+
+#ifdef MODM_CPU_CORTEX_M
+# include
+#endif
+
+namespace modm
+{
+
+/**
+ * Check whether execution is currently inside an interrupt context.
+ *
+ * This is implemented for Cortex-M and returns `false` on other
+ * architectures.
+ */
+inline bool
+isInterruptContext() noexcept
+{
+#ifdef MODM_CPU_CORTEX_M
+ return __get_IPSR() != 0;
+#else
+ return false;
+#endif
+}
+
+}
+
+#endif // __cplusplus
+
#endif // MODM_INTERRUPT_HPP
diff --git a/src/modm/architecture/module.lb b/src/modm/architecture/module.lb
index 0ff8e46708..d94d752bfa 100644
--- a/src/modm/architecture/module.lb
+++ b/src/modm/architecture/module.lb
@@ -153,6 +153,20 @@ class Clock(Module):
env.copy("interface/clock.hpp")
# -----------------------------------------------------------------------------
+class Ethernet(Module):
+ def init(self, module):
+ module.name = "ethernet"
+ module.description = "Common Ethernet MAC, PHY, link, and MDIO types"
+
+ def prepare(self, module, options):
+ module.depends(":stdc++")
+ return True
+
+ def build(self, env):
+ env.outbasepath = "modm/src/modm/architecture"
+ env.copy("interface/ethernet.hpp")
+# -----------------------------------------------------------------------------
+
class Delay(Module):
def init(self, module):
module.name = "delay"
@@ -395,6 +409,7 @@ def prepare(module, options):
module.add_submodule(BuildId())
module.add_submodule(Can())
module.add_submodule(Clock())
+ module.add_submodule(Ethernet())
module.add_submodule(Delay())
module.add_submodule(Fiber())
module.add_submodule(Gpio())
diff --git a/src/modm/board/devebox_stm32h750vb/board.hpp b/src/modm/board/devebox_stm32h750vb/board.hpp
index ccddf52d20..303bace24b 100644
--- a/src/modm/board/devebox_stm32h750vb/board.hpp
+++ b/src/modm/board/devebox_stm32h750vb/board.hpp
@@ -74,6 +74,7 @@ struct SystemClock
static constexpr uint32_t Can1 = Apb1;
static constexpr uint32_t Can2 = Apb1;
+ static constexpr uint32_t Eth = Ahb1;
static constexpr uint32_t I2c1 = Apb1;
static constexpr uint32_t I2c2 = Apb1;
diff --git a/src/modm/board/nucleo_h563zi/board.hpp b/src/modm/board/nucleo_h563zi/board.hpp
index 3c19167303..dd4877d551 100644
--- a/src/modm/board/nucleo_h563zi/board.hpp
+++ b/src/modm/board/nucleo_h563zi/board.hpp
@@ -194,6 +194,7 @@ namespace eth
{
/// @ingroup modm_board_nucleo_h563zi
/// @{
+static constexpr uint32_t PhyAddress = 0;
using RefClk = GpioA1;
using Mdio = GpioA2;
using Mdc = GpioC1;
diff --git a/src/modm/board/nucleo_h723zg/board.hpp b/src/modm/board/nucleo_h723zg/board.hpp
index a092477e72..adf4d83a99 100644
--- a/src/modm/board/nucleo_h723zg/board.hpp
+++ b/src/modm/board/nucleo_h723zg/board.hpp
@@ -80,6 +80,7 @@ struct SystemClock
static constexpr uint32_t Fdcan1 = Pll2Q;
static constexpr uint32_t Fdcan2 = Pll2Q;
static constexpr uint32_t Fdcan3 = Pll2Q;
+ static constexpr uint32_t Eth = Ahb1;
static constexpr uint32_t I2c1 = Apb1;
static constexpr uint32_t I2c2 = Apb1;
@@ -201,6 +202,24 @@ using Device = UsbHs;
/// @}
}
+namespace eth
+{
+/// @ingroup modm_board_nucleo_h723zg
+/// @{
+/// On-board LAN8742A address selected by the PHYAD0 strap.
+static constexpr uint32_t PhyAddress = 0;
+using RefClk = GpioA1;
+using Mdio = GpioA2;
+using Mdc = GpioC1;
+using CrsDv = GpioA7;
+using Rxd0 = GpioC4;
+using Rxd1 = GpioC5;
+using TxEn = GpioG11;
+using Txd0 = GpioG13;
+using Txd1 = GpioB13;
+/// @}
+}
+
namespace stlink
{
/// @ingroup modm_board_nucleo_h723zg
diff --git a/src/modm/board/nucleo_h743zi/board.hpp.in b/src/modm/board/nucleo_h743zi/board.hpp.in
index d0b2c5f56a..5d652c4946 100644
--- a/src/modm/board/nucleo_h743zi/board.hpp.in
+++ b/src/modm/board/nucleo_h743zi/board.hpp.in
@@ -1,5 +1,6 @@
/*
* Copyright (c) 2021, Niklas Hauser
+ * Copyright (c) 2026, Kaelin Laundry
*
* This file is part of the modm project.
*
@@ -78,6 +79,7 @@ struct SystemClock
static constexpr uint32_t Fdcan1 = Pll2Q;
static constexpr uint32_t Fdcan2 = Pll2Q;
+ static constexpr uint32_t Eth = Ahb1;
static constexpr uint32_t I2c1 = Apb1;
static constexpr uint32_t I2c2 = Apb1;
@@ -213,6 +215,26 @@ using Uart = BufferedUart>;
/// @}
}
+%% if board in ["nucleo-h753zi", "stm32h743zi"]
+namespace eth
+{
+/// @ingroup modm_board_nucleo_h743zi
+/// @{
+/// On-board LAN8742A address selected by the PHYAD0 strap.
+static constexpr uint32_t PhyAddress = 0;
+using RefClk = GpioA1;
+using Mdio = GpioA2;
+using Mdc = GpioC1;
+using CrsDv = GpioA7;
+using Rxd0 = GpioC4;
+using Rxd1 = GpioC5;
+using TxEn = GpioG11;
+using Txd0 = GpioG13;
+using Txd1 = GpioB13;
+/// @}
+}
+%% endif
+
/// @ingroup modm_board_nucleo_h743zi
/// @{
using LoggerDevice = modm::IODeviceWrapper< stlink::Uart, modm::IOBuffer::BlockIfFull >;
diff --git a/src/modm/driver/ethernet/lan8720a.hpp b/src/modm/driver/ethernet/lan8720a.hpp
index 09c657fbce..eaa63d3535 100644
--- a/src/modm/driver/ethernet/lan8720a.hpp
+++ b/src/modm/driver/ethernet/lan8720a.hpp
@@ -1,5 +1,6 @@
/*
* Copyright (c) 2020, Mike Wolfram
+ * Copyright (c) 2026, Kaelin Laundry
*
* This file is part of the modm project.
*
@@ -11,47 +12,34 @@
#ifndef MODM_LAN8720A_HPP
#define MODM_LAN8720A_HPP
+#include "lan87xx.hpp"
+
namespace modm
{
-/// @ingroup modm_driver_lan8720a
-struct Lan8720a
+template
+struct Lan8720a : Lan87xx
{
- static constexpr uint32_t Address = 0x00;
+ static_assert(PhyAddress < 32, "Clause 22 PHY addresses are five bits wide");
+ static constexpr uint8_t Address = PhyAddress;
+ static constexpr uint16_t PhyIdentifier1 = 0x0007;
+ static constexpr uint16_t PhyIdentifier2 = 0xc0f0;
+ static constexpr uint16_t PhyIdentifier2Mask = 0xfff0;
+
+ template
+ static InitializationResult initialize()
+ {
+ return Lan87xx::initialize(
+ PhyIdentifier1, PhyIdentifier2, PhyIdentifier2Mask);
+ }
- struct Register
+ template
+ static LinkStatusResult readLinkStatus()
{
- static constexpr uint16_t BCR = 0x0000;
- static constexpr uint16_t BSR = 0x0001;
- static constexpr uint16_t AN = 0x0004;
- static constexpr uint16_t SR = 0x001f;
- static constexpr uint16_t ISFR = 0x001d;
- static constexpr uint16_t ISFR_INT4 = 0x000B;
- };
-
- static constexpr uint32_t ResetDelay = 0x000000FF;
- static constexpr uint32_t ConfigDelay = 0x00000FFF;
- static constexpr int ReadTimeout = 0xffff;
- static constexpr int WriteTimeout = 0xffff;
-
- // TODO: use modm::Register for these bits
- static constexpr uint16_t Reset = 0x8000;
- static constexpr uint16_t LoopBack = 0x4000;
- static constexpr uint16_t FullDuplex100M = 0x2100;
- static constexpr uint16_t HalfDuplex100M = 0x2000;
- static constexpr uint16_t FullDuplex10M = 0x0100;
- static constexpr uint16_t HalfDuplex10M = 0x0000;
- static constexpr uint16_t AutoNegotiation = 0x1000;
- static constexpr uint16_t RestartAutoNegotiation = 0x0200;
-
- static constexpr uint16_t LinkedStatus = 0x0004;
- static constexpr uint16_t DuplexStatus = 0x0010;
- static constexpr uint16_t SpeedStatus = 0x0004;
- static constexpr uint16_t AutoNegotiationComplete = 0x0020;
- static constexpr uint16_t JabberDetection = 0x0002;
+ return Lan87xx::readLinkStatus();
+ }
};
-}
+} // namespace modm
#endif // MODM_LAN8720A_HPP
-
diff --git a/src/modm/driver/ethernet/lan8720a.lb b/src/modm/driver/ethernet/lan8720a.lb
index 5b5242f9c9..893a827418 100644
--- a/src/modm/driver/ethernet/lan8720a.lb
+++ b/src/modm/driver/ethernet/lan8720a.lb
@@ -2,6 +2,7 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2020, Mike Wolfram
+# Copyright (c) 2026, Kaelin Laundry
#
# This file is part of the modm project.
#
@@ -10,17 +11,12 @@
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
# -----------------------------------------------------------------------------
-
def init(module):
module.name = ":driver:lan8720a"
- module.description = """\
-# LAN8720A Ethernet Transceiver
-
-Microchip's LAN8720A/LAN8720Ai are high-performance, small-footprint,
-low-power 10BASE-T/100BASE-TX transceiver connected via an RMII interface.
-"""
+ module.description = "Microchip LAN8720A Ethernet PHY"
def prepare(module, options):
+ module.depends(":driver:lan87xx")
return True
def build(env):
diff --git a/src/modm/driver/ethernet/lan8742a.hpp b/src/modm/driver/ethernet/lan8742a.hpp
new file mode 100644
index 0000000000..bc658ceeab
--- /dev/null
+++ b/src/modm/driver/ethernet/lan8742a.hpp
@@ -0,0 +1,44 @@
+/*
+ * Copyright (c) 2026, Kaelin Laundry
+ *
+ * This file is part of the modm project.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+#ifndef MODM_LAN8742A_HPP
+#define MODM_LAN8742A_HPP
+
+#include "lan87xx.hpp"
+
+namespace modm
+{
+
+template
+struct Lan8742a : Lan87xx
+{
+ static_assert(PhyAddress < 32, "Clause 22 PHY addresses are five bits wide");
+ static constexpr uint8_t Address = PhyAddress;
+ static constexpr uint16_t PhyIdentifier1 = 0x0007;
+ static constexpr uint16_t PhyIdentifier2 = 0xc130;
+ static constexpr uint16_t PhyIdentifier2Mask = 0xfff0;
+
+ template
+ static InitializationResult initialize()
+ {
+ return Lan87xx::initialize(
+ PhyIdentifier1, PhyIdentifier2, PhyIdentifier2Mask);
+ }
+
+ template
+ static LinkStatusResult readLinkStatus()
+ {
+ return Lan87xx::readLinkStatus();
+ }
+};
+
+} // namespace modm
+
+#endif // MODM_LAN8742A_HPP
diff --git a/src/modm/driver/ethernet/lan8742a.lb b/src/modm/driver/ethernet/lan8742a.lb
new file mode 100644
index 0000000000..21676615fc
--- /dev/null
+++ b/src/modm/driver/ethernet/lan8742a.lb
@@ -0,0 +1,23 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2026, Kaelin Laundry
+#
+# This file is part of the modm project.
+#
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+# -----------------------------------------------------------------------------
+
+def init(module):
+ module.name = ":driver:lan8742a"
+ module.description = "Microchip LAN8742A Ethernet PHY"
+
+def prepare(module, options):
+ module.depends(":driver:lan87xx")
+ return True
+
+def build(env):
+ env.outbasepath = "modm/src/modm/driver/ethernet"
+ env.copy("lan8742a.hpp")
diff --git a/src/modm/driver/ethernet/lan87xx.hpp b/src/modm/driver/ethernet/lan87xx.hpp
new file mode 100644
index 0000000000..9c925029df
--- /dev/null
+++ b/src/modm/driver/ethernet/lan87xx.hpp
@@ -0,0 +1,262 @@
+/*
+ * Copyright (c) 2020, Mike Wolfram
+ * Copyright (c) 2026, Kaelin Laundry
+ *
+ * This file is part of the modm project.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+#ifndef MODM_LAN87XX_HPP
+#define MODM_LAN87XX_HPP
+
+#include
+#include
+#include
+
+#include
+#include
+#include
+
+namespace modm
+{
+
+/// Common Clause 22 register definitions and operations for LAN87xx PHYs.
+struct Lan87xx
+{
+ struct Register
+ {
+ static constexpr uint8_t BasicControl = 0;
+ static constexpr uint8_t BasicStatus = 1;
+ static constexpr uint8_t PhyIdentifier1 = 2;
+ static constexpr uint8_t PhyIdentifier2 = 3;
+ static constexpr uint8_t AutoNegotiationAdvertisement = 4;
+ static constexpr uint8_t AutoNegotiationLinkPartnerAbility = 5;
+ static constexpr uint8_t InterruptSourceFlag = 29;
+ static constexpr uint8_t PhySpecialControlStatus = 31;
+ };
+
+ enum class BasicControl : uint16_t
+ {
+ SoftReset = Bit15,
+ Loopback = Bit14,
+ SpeedSelect = Bit13,
+ AutoNegotiationEnable = Bit12,
+ PowerDown = Bit11,
+ Isolate = Bit10,
+ RestartAutoNegotiation = Bit9,
+ DuplexMode = Bit8,
+ };
+ MODM_FLAGS16(BasicControl);
+
+ enum class BasicStatus : uint16_t
+ {
+ AutoNegotiationComplete = Bit5,
+ RemoteFault = Bit4,
+ AutoNegotiationAbility = Bit3,
+ LinkStatus = Bit2,
+ JabberDetect = Bit1,
+ };
+ MODM_FLAGS16(BasicStatus);
+
+ enum class AutoNegotiationAdvertisement : uint16_t
+ {
+ SelectorIeee8023 = Bit0,
+ Base10THalfDuplex = Bit5,
+ Base10TFullDuplex = Bit6,
+ Base100TxHalfDuplex = Bit7,
+ Base100TxFullDuplex = Bit8,
+ // Pause capabilities require corresponding MAC flow control support
+ SymmetricPause = Bit10,
+ AsymmetricPause = Bit11,
+ // Remote fault is status signalling, not a speed/duplex mode.
+ RemoteFault = Bit13,
+ AllSpeedDuplexModes = Base100TxFullDuplex | Base100TxHalfDuplex |
+ Base10TFullDuplex | Base10THalfDuplex | SelectorIeee8023,
+ };
+ MODM_FLAGS16(AutoNegotiationAdvertisement);
+
+ enum class PhySpecialControlStatus : uint16_t
+ {
+ AutoNegotiationDone = Bit12,
+ };
+ MODM_FLAGS16(PhySpecialControlStatus);
+
+ enum class SpeedIndication : uint16_t
+ {
+ Base10THalfDuplex = 0b001,
+ Base10TFullDuplex = 0b101,
+ Base100TxHalfDuplex = 0b010,
+ Base100TxFullDuplex = 0b110,
+ };
+ using SpeedIndication_t =
+ modm::Configuration;
+
+ static constexpr auto SoftwareResetTimeout = std::chrono::milliseconds{550};
+
+ enum class InitializationError : uint8_t
+ {
+ None,
+ NotFound,
+ ResetTimeout,
+ };
+
+ struct [[nodiscard]] InitializationResult
+ {
+ InitializationError error = InitializationError::None;
+ ethernet::MdioError mdioError = ethernet::MdioError::None;
+
+ constexpr explicit operator bool() const noexcept
+ {
+ return error == InitializationError::None and
+ mdioError == ethernet::MdioError::None;
+ }
+ };
+
+ struct [[nodiscard]] LinkStatusResult
+ {
+ ethernet::LinkStatus status;
+ ethernet::MdioError error = ethernet::MdioError::None;
+
+ constexpr explicit operator bool() const noexcept
+ {
+ return error == ethernet::MdioError::None;
+ }
+ };
+
+ static constexpr std::optional
+ decodeSpeedIndication(uint16_t specialStatus)
+ {
+ switch (SpeedIndication_t::get(PhySpecialControlStatus_t(specialStatus))) {
+ case SpeedIndication::Base10THalfDuplex:
+ return ethernet::LinkMode{
+ ethernet::Speed::Speed10M, ethernet::DuplexMode::Half};
+ case SpeedIndication::Base10TFullDuplex:
+ return ethernet::LinkMode{
+ ethernet::Speed::Speed10M, ethernet::DuplexMode::Full};
+ case SpeedIndication::Base100TxHalfDuplex:
+ return ethernet::LinkMode{
+ ethernet::Speed::Speed100M, ethernet::DuplexMode::Half};
+ case SpeedIndication::Base100TxFullDuplex:
+ return ethernet::LinkMode{
+ ethernet::Speed::Speed100M, ethernet::DuplexMode::Full};
+ }
+ return std::nullopt;
+ }
+
+protected:
+ template
+ static InitializationResult
+ initialize(uint16_t expectedIdentifier1, uint16_t expectedIdentifier2,
+ uint16_t identifier2Mask)
+ {
+ static_assert(Address < 32, "Clause 22 PHY addresses are five bits wide");
+
+ // Check the PHY is online and matches the expected device IDs
+ uint16_t identifier1 = 0;
+ uint16_t identifier2 = 0;
+ if (const auto error = read(Register::PhyIdentifier1, identifier1);
+ error != ethernet::MdioError::None)
+ return {.mdioError = error};
+ if (const auto error = read(Register::PhyIdentifier2, identifier2);
+ error != ethernet::MdioError::None)
+ return {.mdioError = error};
+ if (identifier1 != expectedIdentifier1 or
+ (identifier2 & identifier2Mask) != (expectedIdentifier2 & identifier2Mask))
+ return {.error = InitializationError::NotFound};
+
+ // Trigger a software reset and poll until it completes
+ constexpr uint16_t SoftwareReset = uint16_t(BasicControl::SoftReset);
+ if (const auto error = write(Register::BasicControl, SoftwareReset);
+ error != ethernet::MdioError::None)
+ return {.mdioError = error};
+ ethernet::MdioError pollError = ethernet::MdioError::None;
+ const bool resetDone = modm::this_fiber::poll_for(SoftwareResetTimeout, [&] {
+ uint16_t control = 0;
+ pollError = read(Register::BasicControl, control);
+ return pollError != ethernet::MdioError::None or
+ (control & SoftwareReset) == 0;
+ });
+ if (pollError != ethernet::MdioError::None)
+ return {.mdioError = pollError};
+ if (not resetDone)
+ return {.error = InitializationError::ResetTimeout};
+
+ // Advertise supported modes and restart auto negotiation
+ if (const auto error = write(Register::AutoNegotiationAdvertisement,
+ uint16_t(AutoNegotiationAdvertisement::AllSpeedDuplexModes));
+ error != ethernet::MdioError::None)
+ return {.mdioError = error};
+ if (const auto error = write(Register::BasicControl,
+ uint16_t(BasicControl::AutoNegotiationEnable) |
+ uint16_t(BasicControl::RestartAutoNegotiation));
+ error != ethernet::MdioError::None)
+ return {.mdioError = error};
+ return {};
+ }
+
+ template
+ static LinkStatusResult
+ readLinkStatus()
+ {
+ uint16_t basicStatus = 0;
+ // BMSR link status is latched low, so use the second read as current state.
+ if (const auto error = read(Register::BasicStatus, basicStatus);
+ error != ethernet::MdioError::None)
+ return {{}, error};
+ if (const auto error = read(Register::BasicStatus, basicStatus);
+ error != ethernet::MdioError::None)
+ return {{}, error};
+ const BasicStatus_t status{basicStatus};
+ if (status.none(BasicStatus::LinkStatus))
+ return {{ethernet::LinkState::Down, std::nullopt}, {}};
+
+ uint16_t basicControl = 0;
+ if (const auto error = read(Register::BasicControl, basicControl);
+ error != ethernet::MdioError::None)
+ return {{}, error};
+ const BasicControl_t control{basicControl};
+ if (control.none(BasicControl::AutoNegotiationEnable)) {
+ return {{ethernet::LinkState::Up, ethernet::LinkMode{
+ control.any(BasicControl::SpeedSelect) ?
+ ethernet::Speed::Speed100M : ethernet::Speed::Speed10M,
+ control.any(BasicControl::DuplexMode) ?
+ ethernet::DuplexMode::Full : ethernet::DuplexMode::Half}}, {}};
+ }
+ if (status.none(BasicStatus::AutoNegotiationComplete))
+ return {{ethernet::LinkState::Negotiating, std::nullopt}, {}};
+
+ uint16_t specialStatus = 0;
+ if (const auto error = read(Register::PhySpecialControlStatus,
+ specialStatus); error != ethernet::MdioError::None)
+ return {{}, error};
+ if (PhySpecialControlStatus_t(specialStatus).none(
+ PhySpecialControlStatus::AutoNegotiationDone))
+ return {{ethernet::LinkState::Negotiating, std::nullopt}, {}};
+
+ const auto mode = decodeSpeedIndication(specialStatus);
+ if (not mode)
+ return {{ethernet::LinkState::Negotiating, std::nullopt}, {}};
+ return {{ethernet::LinkState::Up, *mode}, {}};
+ }
+
+ template
+ static ethernet::MdioError
+ read(uint8_t reg, uint16_t& value)
+ {
+ return Mdio::readPhyRegister(Address, reg, value);
+ }
+
+ template
+ static ethernet::MdioError
+ write(uint8_t reg, uint16_t value)
+ {
+ return Mdio::writePhyRegister(Address, reg, value);
+ }
+};
+
+} // namespace modm
+
+#endif // MODM_LAN87XX_HPP
diff --git a/src/modm/driver/ethernet/lan87xx.lb b/src/modm/driver/ethernet/lan87xx.lb
new file mode 100644
index 0000000000..24f9a0ea2c
--- /dev/null
+++ b/src/modm/driver/ethernet/lan87xx.lb
@@ -0,0 +1,23 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2026, Kaelin Laundry
+#
+# This file is part of the modm project.
+#
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+# -----------------------------------------------------------------------------
+
+def init(module):
+ module.name = ":driver:lan87xx"
+ module.description = "Common LAN87xx Ethernet PHY definitions and operations"
+
+def prepare(module, options):
+ module.depends(":architecture:ethernet", ":architecture:register", ":processing:fiber")
+ return True
+
+def build(env):
+ env.outbasepath = "modm/src/modm/driver/ethernet"
+ env.copy("lan87xx.hpp")
diff --git a/src/modm/platform/clock/stm32/module.lb b/src/modm/platform/clock/stm32/module.lb
index 695b2727e6..22964e3383 100644
--- a/src/modm/platform/clock/stm32/module.lb
+++ b/src/modm/platform/clock/stm32/module.lb
@@ -4,6 +4,7 @@
# Copyright (c) 2016-2019, Niklas Hauser
# Copyright (c) 2017, Fabian Greif
# Copyright (c) 2021, Christopher Durand
+# Copyright (c) 2026, Kaelin Laundry
#
# This file is part of the modm project.
#
@@ -24,6 +25,20 @@ def prepare(module, options):
module.depends(":cmsis:device", ":utils", ":platform:clock", ":architecture:delay")
return True
+ethernet_companion_clocks = {
+ "ETH": ["ETHRX", "ETHTX"],
+ "ETHMAC": ["ETHMACRX", "ETHMACTX"],
+ "ETH1MAC": ["ETH1RX", "ETH1TX"],
+}
+
+def rcc_ethernet_companion_clocks(rcc_map, mac_clock):
+ companions = []
+ for clock in ethernet_companion_clocks.get(mac_clock, []):
+ mode = rcc_map.get(clock)
+ if mode and "EN" in mode:
+ companions.append((clock, mode["EN"]))
+ return companions
+
def build(env):
device = env[":target"]
@@ -163,6 +178,7 @@ def build(env):
all_peripherals = env.query(":cmsis:device:peripherals")
rcc_map = env.query(":cmsis:device:rcc-map")
rcc_enable = {}
+ rcc_extra_enable = {}
rcc_reset = {}
for per, mode in rcc_map.items():
@@ -204,7 +220,7 @@ def build(env):
if "Dsihost" in all_peripherals and per == "DSI":
per = "Dsihost"
nper = "DSI"
- if "Eth" in all_peripherals and per == "ETHMAC":
+ if "Eth" in all_peripherals and per in ethernet_companion_clocks:
per = "Eth"
if "Rtc" in all_peripherals and per == "RTCAPB":
per = "RTC"
@@ -227,11 +243,14 @@ def build(env):
kw = per.capitalize()
if kw not in rcc_enable:
rcc_enable[kw] = (nper, mode["EN"])
+ if kw == "Eth":
+ rcc_extra_enable[kw] = rcc_ethernet_companion_clocks(rcc_map, nper)
if "RST" in mode:
rcc_reset[per.capitalize()] = (nper, mode["RST"])
env.substitutions.update({
"rcc_enable": rcc_enable,
+ "rcc_extra_enable": rcc_extra_enable,
"rcc_reset": rcc_reset,
})
env.template("rcc_impl.hpp.in")
diff --git a/src/modm/platform/clock/stm32/rcc_impl.hpp.in b/src/modm/platform/clock/stm32/rcc_impl.hpp.in
index cf5cfef79e..968f580294 100644
--- a/src/modm/platform/clock/stm32/rcc_impl.hpp.in
+++ b/src/modm/platform/clock/stm32/rcc_impl.hpp.in
@@ -1,5 +1,6 @@
/*
* Copyright (c) 2019, 2021, 2024, Niklas Hauser
+ * Copyright (c) 2026, Kaelin Laundry
*
* This file is part of the modm project.
*
@@ -124,11 +125,17 @@ Rcc::enable()
%% for peripheral, (st_per, bus) in rcc_enable.items() | sort
if constexpr (peripheral == Peripheral::{{ peripheral }})
{% if peripheral in rcc_reset %}if (not Rcc::isEnabled()) {% endif %}{
- RCC->{{bus}} |= RCC_{{bus}}_{{st_per}}EN;{% if peripheral in rcc_reset %} __DSB();
- RCC->{{rcc_reset[peripheral][1]}} |= RCC_{{rcc_reset[peripheral][1]}}_{{rcc_reset[peripheral][0]}}RST; __DSB();
- RCC->{{rcc_reset[peripheral][1]}} &= ~RCC_{{rcc_reset[peripheral][1]}}_{{rcc_reset[peripheral][0]}}RST;{% endif %}{% if peripheral == "Eth" %} __DSB();
- RCC->{{bus}} |= RCC_{{bus}}_{{st_per}}RXEN; __DSB();
- RCC->{{bus}} |= RCC_{{bus}}_{{st_per}}TXEN;{% endif %}
+ RCC->{{bus}} |= RCC_{{bus}}_{{st_per}}EN;
+ %% if peripheral in rcc_reset
+ __DSB();
+ RCC->{{rcc_reset[peripheral][1]}} |= RCC_{{rcc_reset[peripheral][1]}}_{{rcc_reset[peripheral][0]}}RST;
+ __DSB();
+ RCC->{{rcc_reset[peripheral][1]}} &= ~RCC_{{rcc_reset[peripheral][1]}}_{{rcc_reset[peripheral][0]}}RST;
+ %% endif
+ %% for st_extra_per, extra_bus in rcc_extra_enable.get(peripheral, [])
+ __DSB();
+ RCC->{{extra_bus}} |= RCC_{{extra_bus}}_{{st_extra_per}}EN;
+ %% endfor
}
%% endfor
__DSB();
@@ -144,9 +151,11 @@ Rcc::disable()
__DSB();
%% for peripheral, (st_per, bus) in rcc_enable.items() | sort
if constexpr (peripheral == Peripheral::{{ peripheral }}) {
- RCC->{{bus}} &= ~RCC_{{bus}}_{{st_per}}EN;{% if peripheral == "Eth" %} __DSB();
- RCC->{{bus}} &= ~RCC_{{bus}}_{{st_per}}RXEN; __DSB();
- RCC->{{bus}} &= ~RCC_{{bus}}_{{st_per}}TXEN;{% endif %}
+ RCC->{{bus}} &= ~RCC_{{bus}}_{{st_per}}EN;
+ %% for st_extra_per, extra_bus in rcc_extra_enable.get(peripheral, [])
+ __DSB();
+ RCC->{{extra_bus}} &= ~RCC_{{extra_bus}}_{{st_extra_per}}EN;
+ %% endfor
}
%% endfor
__DSB();
@@ -161,7 +170,11 @@ Rcc::isEnabled()
%% for peripheral, (st_per, bus) in rcc_enable.items() | sort
if constexpr (peripheral == Peripheral::{{ peripheral }})
- return RCC->{{bus}} & RCC_{{bus}}_{{st_per}}EN;
+ return (RCC->{{bus}} & RCC_{{bus}}_{{st_per}}EN)
+ %% for st_extra_per, extra_bus in rcc_extra_enable.get(peripheral, [])
+ and (RCC->{{extra_bus}} & RCC_{{extra_bus}}_{{st_extra_per}}EN)
+ %% endfor
+ ;
%% endfor
}
diff --git a/src/modm/platform/core/stm32/startup_platform.c.in b/src/modm/platform/core/stm32/startup_platform.c.in
index 04b76545bc..8ea83ca3b2 100644
--- a/src/modm/platform/core/stm32/startup_platform.c.in
+++ b/src/modm/platform/core/stm32/startup_platform.c.in
@@ -4,6 +4,7 @@
* Copyright (c) 2016-2017, 2019, 2024, Niklas Hauser
* Copyright (c) 2021, Raphael Lehmann
* Copyright (c) 2021, Christopher Durand
+ * Copyright (c) 2026, Kaelin Laundry
*
* This file is part of the modm project.
*
@@ -83,6 +84,9 @@ __modm_initialize_platform(void)
// Enable all SRAMs
%% if target.name[0].isnumeric()
RCC->AHB2ENR |= RCC_AHB2ENR_SRAM1EN | RCC_AHB2ENR_SRAM2EN;
+#ifdef RCC_AHB2ENR_SRAM3EN
+ RCC->AHB2ENR |= RCC_AHB2ENR_SRAM3EN;
+#endif
%% else
RCC->AHB2ENR |= RCC_AHB2ENR_AHBSRAM1EN | RCC_AHB2ENR_AHBSRAM2EN;
%% endif
diff --git a/src/modm/platform/eth/stm32/eth.hpp b/src/modm/platform/eth/stm32/eth.hpp
index c27779af86..01f16b844e 100644
--- a/src/modm/platform/eth/stm32/eth.hpp
+++ b/src/modm/platform/eth/stm32/eth.hpp
@@ -1,5 +1,6 @@
/*
* Copyright (c) 2020, Mike Wolfram
+ * Copyright (c) 2026, Kaelin Laundry
*
* This file is part of the modm project.
*
@@ -428,21 +429,20 @@ class Eth : public eth
uint32_t phy_register { 0 };
/* Reset */
- (void) readPhyRegister(PHY::Register::BCR, phy_register);
- phy_register |= PHY::Reset;
- if (not writePhyRegister(PHY::Register::BCR, phy_register)) {
+ if (not writePhyRegister(PHY::Register::BasicControl,
+ uint16_t(PHY::BasicControl::SoftReset))) {
configureMac(true);
configureDma();
return false;
}
// wait for reset done
- modm::delay_us(PHY::ResetDelay);
+ modm::delay_us(PhyResetDelayMicroseconds);
timeout = 1'000;
do {
- (void) readPhyRegister(PHY::Register::BCR, phy_register);
- if ((phy_register & PHY::Reset) == 0)
+ (void) readPhyRegister(PHY::Register::BasicControl, phy_register);
+ if ((phy_register & uint16_t(PHY::BasicControl::SoftReset)) == 0)
break;
modm::delay_ms(1);
} while (timeout-- > 0);
@@ -459,7 +459,7 @@ class Eth : public eth
| 0x20 // 10
| 0x01 // 802.3
;
- (void) writePhyRegister(PHY::Register::AN, phy_register);
+ (void) writePhyRegister(PHY::Register::AutoNegotiationAdvertisement, phy_register);
configureMac(true);
configureDma();
@@ -582,16 +582,16 @@ class Eth : public eth
uint32_t phy_register { 0 };
// enable auto-negotiation
- (void) readPhyRegister(PHY::Register::BCR, phy_register);
- phy_register |= PHY::RestartAutoNegotiation;
- if (not writePhyRegister(PHY::Register::BCR, phy_register))
+ (void) readPhyRegister(PHY::Register::BasicControl, phy_register);
+ phy_register |= uint16_t(PHY::BasicControl::RestartAutoNegotiation);
+ if (not writePhyRegister(PHY::Register::BasicControl, phy_register))
return false;
// wait for auto-negotiation complete (5s)
int timeout = 5'000;
do {
- (void) readPhyRegister(PHY::Register::BSR, phy_register);
- if ((phy_register & PHY::AutoNegotiationComplete) == PHY::AutoNegotiationComplete)
+ (void) readPhyRegister(PHY::Register::BasicStatus, phy_register);
+ if ((phy_register & uint16_t(PHY::BasicStatus::AutoNegotiationComplete)) != 0)
break;
modm::delay_ms(1);
} while (timeout-- > 0);
@@ -599,15 +599,15 @@ class Eth : public eth
return false;
// read auto-negotiation result
- if (not readPhyRegister(PHY::Register::SR, phy_register))
+ if (not readPhyRegister(PHY::Register::PhySpecialControlStatus, phy_register))
return false;
- if ((phy_register & PHY::DuplexStatus) == PHY::DuplexStatus)
+ if ((phy_register & PhyDuplexStatus) != 0)
duplexMode = DuplexMode::Full;
else
duplexMode = DuplexMode::Half;
- if ((phy_register & PHY::SpeedStatus) == PHY::SpeedStatus)
+ if ((phy_register & PhySpeedStatus) != 0)
speed = Speed::Speed10M;
else
speed = Speed::Speed100M;
@@ -620,8 +620,8 @@ class Eth : public eth
{
uint32_t phy_register { 0 };
- (void) readPhyRegister(PHY::Register::BSR, phy_register);
- if ((phy_register & PHY::LinkedStatus) == PHY::LinkedStatus)
+ (void) readPhyRegister(PHY::Register::BasicStatus, phy_register);
+ if ((phy_register & uint16_t(PHY::BasicStatus::LinkStatus)) != 0)
linkStatus = LinkStatus::Up;
else
linkStatus = LinkStatus::Down;
@@ -634,6 +634,12 @@ class Eth : public eth
}
private:
+ static constexpr uint32_t PhyResetDelayMicroseconds = 0xff;
+ static constexpr uint16_t PhyDuplexStatus = modm::Bit4;
+ static constexpr uint16_t PhySpeedStatus = modm::Bit2;
+ static constexpr int PhyReadTimeout = 0xffff;
+ static constexpr int PhyWriteTimeout = 0xffff;
+
static void
writeMACCR(uint32_t value) {
ETH->MACCR = value;
diff --git a/src/modm/platform/eth/stm32/eth_impl.hpp b/src/modm/platform/eth/stm32/eth_impl.hpp
index 9ce0b060d8..c4e4f38975 100644
--- a/src/modm/platform/eth/stm32/eth_impl.hpp
+++ b/src/modm/platform/eth/stm32/eth_impl.hpp
@@ -100,7 +100,7 @@ Eth::readPhyRegister(uint16_t reg, uint32_t &value)
ETH->MACMIIAR = tmp;
- int timeout = PHY::ReadTimeout;
+ int timeout = PhyReadTimeout;
while (timeout-- > 0) {
if ((ETH->MACMIIAR & ETH_MACMIIAR_MB) == 0) {
// busy flag cleared, read data
@@ -126,7 +126,7 @@ Eth::writePhyRegister(uint16_t reg, uint32_t value)
ETH->MACMIIDR = value;
ETH->MACMIIAR = tmp;
- int timeout = PHY::WriteTimeout;
+ int timeout = PhyWriteTimeout;
while (timeout-- > 0) {
if ((ETH->MACMIIAR & ETH_MACMIIAR_MB) == 0)
return true;
diff --git a/src/modm/platform/eth/stm32/module.lb b/src/modm/platform/eth/stm32/module.lb
index b74e261d2f..b20ff05c74 100644
--- a/src/modm/platform/eth/stm32/module.lb
+++ b/src/modm/platform/eth/stm32/module.lb
@@ -13,14 +13,13 @@
def init(module):
module.name = ":platform:eth"
- module.description = "Ethernet"
+ module.description = "STM32F4/F7 Ethernet MAC driver"
def prepare(module, options):
device = options[":target"]
if not device.has_driver("eth:stm32*"):
return False
- # FIXME the driver is for F7 only right now
if device.identifier["family"] not in ["f7", "f4"]:
return False
@@ -40,4 +39,3 @@ def build(env):
env.copy("eth.hpp")
env.copy("eth_impl.hpp")
-
diff --git a/src/modm/platform/eth/stm32h7/eth.cpp.in b/src/modm/platform/eth/stm32h7/eth.cpp.in
new file mode 100644
index 0000000000..e42cfa58c5
--- /dev/null
+++ b/src/modm/platform/eth/stm32h7/eth.cpp.in
@@ -0,0 +1,107 @@
+/*
+ * Copyright (c) 2026, Kaelin Laundry
+ *
+ * This file is part of the modm project.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+// ----------------------------------------------------------------------------
+
+#include "eth.hpp"
+
+namespace modm::platform::detail
+{
+
+// Cache invalidations apply at line granularity so DMA buffers should be aligned
+// to line width.
+modm_section("{{ eth_dma_section }}") modm_aligned(EthH7DescriptorAlignment)
+EthH7DmaStorage ethH7DmaStorage;
+
+EthH7MacState ethH7MacState{};
+
+void
+ethH7RecordDmaStatus(uint32_t status, bool running)
+{
+ ethH7MacState.hardwareDmaErrorStatus |= status & EthH7DmaErrorStatus;
+ if ((status & ETH_DMACSR_FBE) and
+ ethH7MacState.driverFailure != EthH7MacState::DriverFailure::FatalBusError)
+ ++ethH7MacState.fatalBusErrors;
+ if (status & ETH_DMACSR_CDE) ++ethH7MacState.contextDescriptorErrors;
+ if (status & ETH_DMACSR_RWT) ++ethH7MacState.rxWatchdogTimeouts;
+ if (running and (status & ETH_DMACSR_RPS)) ++ethH7MacState.rxProcessStopped;
+ if (status & ETH_DMACSR_RBU) ++ethH7MacState.rxBufferUnavailable;
+ if (running and (status & ETH_DMACSR_TPS)) ++ethH7MacState.txProcessStopped;
+ if (status & ETH_DMACSR_TBU) ++ethH7MacState.txBufferUnavailable;
+}
+
+uint32_t
+ethH7BuildWakeupEvents(uint32_t status, bool running)
+{
+ uint32_t events = 0;
+ if (status & (ETH_DMACSR_RI | ETH_DMACSR_RBU | ETH_DMACSR_RPS | ETH_DMACSR_RWT))
+ events |= uint32_t(EthernetMac::WakeupEvent::Receive);
+ if (status & (ETH_DMACSR_TI | ETH_DMACSR_TPS | ETH_DMACSR_TBU))
+ events |= uint32_t(EthernetMac::WakeupEvent::Transmit);
+ if ((status & (ETH_DMACSR_CDE | ETH_DMACSR_FBE | ETH_DMACSR_RWT)) or
+ (running and (status & (ETH_DMACSR_RPS | ETH_DMACSR_TPS))))
+ events |= uint32_t(EthernetMac::WakeupEvent::Error);
+ return events;
+}
+
+namespace
+{
+
+void
+restartStoppedDma(uint32_t status)
+{
+ // Resume DMA channels stopped by recoverable ring starvation while the driver is
+ // still running. Fatal bus errors are handled separately.
+ if (status & ETH_DMACSR_RPS) {
+ ETH->DMACRCR |= ETH_DMACRCR_SR;
+ __DMB();
+ ETH->DMACRDTPR = uint32_t(reinterpret_cast(
+ ðH7DmaStorage.rxDescriptors[ethH7MacState.rxTailIndex]));
+ }
+ if (status & ETH_DMACSR_TPS) {
+ ETH->DMACTCR |= ETH_DMACTCR_ST;
+ __DMB();
+ ETH->DMACTDTPR = uint32_t(reinterpret_cast(
+ ðH7DmaStorage.txDescriptors[ethH7MacState.txIndex]));
+ }
+}
+
+} // namespace
+
+void
+ethH7HandleInterrupt()
+{
+ const uint32_t status = ETH->DMACSR;
+ const bool running =
+ ethH7MacState.driverState == EthH7MacState::DriverState::Running;
+ const bool fatalBusError = (status & ETH_DMACSR_FBE) != 0;
+
+ ethH7RecordDmaStatus(status, running);
+ ethH7MacState.interruptEvents |= ethH7BuildWakeupEvents(status, running);
+
+ if (fatalBusError) {
+ ethH7MacState.driverFailure = EthH7MacState::DriverFailure::FatalBusError;
+ ethH7MacState.driverState = EthH7MacState::DriverState::Faulted;
+ ETH->DMACIER = 0;
+ }
+
+ const uint32_t acknowledge = status & EthH7DmaHandledStatus;
+ if (acknowledge != 0)
+ ETH->DMACSR = acknowledge;
+
+ if (running and not fatalBusError)
+ restartStoppedDma(status);
+}
+
+}
+
+MODM_ISR(ETH)
+{
+ modm::platform::detail::ethH7HandleInterrupt();
+}
diff --git a/src/modm/platform/eth/stm32h7/eth.hpp.in b/src/modm/platform/eth/stm32h7/eth.hpp.in
new file mode 100644
index 0000000000..30ae55e0f1
--- /dev/null
+++ b/src/modm/platform/eth/stm32h7/eth.hpp.in
@@ -0,0 +1,741 @@
+/*
+ * Copyright (c) 2026, Kaelin Laundry
+ *
+ * This file is part of the modm project.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+// ----------------------------------------------------------------------------
+
+#ifndef MODM_ETH_HPP
+#define MODM_ETH_HPP
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include "../device.hpp"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+namespace modm
+{
+namespace platform
+{
+
+namespace detail
+{
+
+static constexpr std::size_t EthH7DmaBufferSize = {{ eth_dma_buffer_size }};
+static constexpr std::size_t EthH7MaxFrameSize = {{ eth_max_frame_size }};
+static constexpr std::size_t EthH7RxDescriptorCount = {{ eth_rx_descriptor_count }};
+static constexpr std::size_t EthH7TxDescriptorCount = {{ eth_tx_descriptor_count }};
+static constexpr uint32_t EthH7DescriptorAlignment = {{ eth_descriptor_alignment }};
+
+enum class EthH7TxDescriptor2 : uint32_t
+{
+ InterruptOnCompletion = modm::Bit31,
+};
+using EthH7TxDescriptor2_t = modm::Flags32;
+MODM_TYPE_FLAGS(EthH7TxDescriptor2_t);
+using EthH7TxBufferLength = modm::Value;
+
+enum class EthH7TxDescriptor3 : uint32_t
+{
+ OwnedByDma = modm::Bit31,
+ FirstDescriptor = modm::Bit29,
+ LastDescriptor = modm::Bit28,
+};
+using EthH7TxDescriptor3_t = modm::Flags32;
+MODM_TYPE_FLAGS(EthH7TxDescriptor3_t);
+using EthH7TxPacketLength = modm::Value;
+
+enum class EthH7TxChecksumInsertion : uint32_t
+{
+ Disabled = 0,
+ Full = 3,
+};
+using EthH7TxChecksumInsertion_t = modm::Configuration<
+ EthH7TxDescriptor3_t, EthH7TxChecksumInsertion, 0b11, 16>;
+
+struct alignas(EthH7DescriptorAlignment) EthH7TxDmaDescriptor
+{
+ __IO uint32_t word0;
+ __IO uint32_t word1;
+ __IO uint32_t word2;
+ __IO uint32_t word3;
+ uint32_t reserved[4];
+
+ void clear()
+ {
+ word0 = word1 = word2 = word3 = 0;
+ }
+
+ bool isOwnedByDma() const
+ {
+ return EthH7TxDescriptor3_t(word3).any(EthH7TxDescriptor3::OwnedByDma);
+ }
+
+ uint32_t writebackErrors() const
+ {
+ return word3 & 0x0000ff0c;
+ }
+
+ void prepare(uint32_t bufferAddress, std::size_t length, bool insertChecksum)
+ {
+ word0 = bufferAddress;
+ word1 = 0;
+ word2 = (EthH7TxDescriptor2::InterruptOnCompletion |
+ EthH7TxBufferLength(uint32_t(length))).value;
+ auto control = EthH7TxDescriptor3::FirstDescriptor |
+ EthH7TxDescriptor3::LastDescriptor |
+ EthH7TxPacketLength(uint32_t(length));
+ if (insertChecksum)
+ control |= EthH7TxChecksumInsertion_t(EthH7TxChecksumInsertion::Full);
+ word3 = control.value;
+ }
+
+ void releaseToDma()
+ {
+ word3 |= uint32_t(EthH7TxDescriptor3::OwnedByDma);
+ }
+};
+
+enum class EthH7RxDescriptor3 : uint32_t
+{
+ OwnedByDma = modm::Bit31,
+ ContextDescriptor = modm::Bit30,
+ InterruptOnCompletion = modm::Bit30,
+ FirstDescriptor = modm::Bit29,
+ LastDescriptor = modm::Bit28,
+ ReceiveStatus1Valid = modm::Bit26,
+ Buffer1Valid = modm::Bit24,
+ ErrorSummary = modm::Bit15,
+};
+using EthH7RxDescriptor3_t = modm::Flags32;
+MODM_TYPE_FLAGS(EthH7RxDescriptor3_t);
+using EthH7RxPacketLength = modm::Value;
+
+struct alignas(EthH7DescriptorAlignment) EthH7RxDmaDescriptor
+{
+ __IO uint32_t word0;
+ __IO uint32_t word1;
+ __IO uint32_t word2;
+ __IO uint32_t word3;
+ uint32_t reserved[4];
+
+ void prepare(uint32_t bufferAddress)
+ {
+ word0 = bufferAddress;
+ word1 = 0;
+ word2 = 0;
+ word3 = (EthH7RxDescriptor3::Buffer1Valid |
+ EthH7RxDescriptor3::InterruptOnCompletion).value;
+ }
+
+ void releaseToDma()
+ {
+ word3 |= uint32_t(EthH7RxDescriptor3::OwnedByDma);
+ }
+
+ EthH7RxDescriptor3_t status() const
+ {
+ return EthH7RxDescriptor3_t(word3);
+ }
+
+ std::size_t packetLength() const
+ {
+ return EthH7RxPacketLength::get(status());
+ }
+};
+
+// Cache invalidations apply at line granularity so DMA buffers should be
+// aligned to line width.
+static_assert(sizeof(EthH7TxDmaDescriptor) == 32);
+static_assert(sizeof(EthH7RxDmaDescriptor) == 32);
+static_assert(alignof(EthH7TxDmaDescriptor) == EthH7DescriptorAlignment);
+static_assert(alignof(EthH7RxDmaDescriptor) == EthH7DescriptorAlignment);
+static_assert(std::is_standard_layout_v);
+static_assert(std::is_standard_layout_v);
+static_assert(std::is_trivially_copyable_v);
+static_assert(std::is_trivially_copyable_v);
+static_assert(offsetof(EthH7TxDmaDescriptor, word3) == 12);
+static_assert(offsetof(EthH7RxDmaDescriptor, word3) == 12);
+
+struct alignas(EthH7DescriptorAlignment) EthH7DmaStorage
+{
+ EthH7RxDmaDescriptor rxDescriptors[EthH7RxDescriptorCount];
+ EthH7TxDmaDescriptor txDescriptors[EthH7TxDescriptorCount];
+ uint8_t rxBuffers[EthH7RxDescriptorCount][EthH7DmaBufferSize];
+ uint8_t txBuffers[EthH7TxDescriptorCount][EthH7DmaBufferSize];
+};
+
+static_assert(alignof(EthH7DmaStorage) >= EthH7DescriptorAlignment);
+static_assert(EthH7DmaBufferSize % EthH7DescriptorAlignment == 0);
+static_assert(offsetof(EthH7DmaStorage, rxDescriptors) % EthH7DescriptorAlignment == 0);
+static_assert(offsetof(EthH7DmaStorage, txDescriptors) % EthH7DescriptorAlignment == 0);
+static_assert(offsetof(EthH7DmaStorage, rxBuffers) % EthH7DescriptorAlignment == 0);
+static_assert(offsetof(EthH7DmaStorage, txBuffers) % EthH7DescriptorAlignment == 0);
+
+extern EthH7DmaStorage ethH7DmaStorage;
+
+} // namespace detail
+
+namespace detail
+{
+
+constexpr bool
+needsRmii10MWorkaround(ethernet::MediaInterface interface, ethernet::Speed speed)
+{
+ return interface == ethernet::MediaInterface::RMII and
+ speed == ethernet::Speed::Speed10M;
+}
+
+constexpr bool
+isExactRmii10MDribbleCrcError(uint32_t status)
+{
+ constexpr uint32_t ErrorSummary = modm::Bit15;
+ constexpr uint32_t DribbleError = modm::Bit19;
+ constexpr uint32_t ReceiveError = modm::Bit20;
+ constexpr uint32_t OverflowError = modm::Bit21;
+ constexpr uint32_t ReceiveWatchdog = modm::Bit22;
+ constexpr uint32_t GiantPacket = modm::Bit23;
+ constexpr uint32_t CrcError = modm::Bit24;
+ constexpr uint32_t ErrorMask = DribbleError | ReceiveError | OverflowError |
+ ReceiveWatchdog | GiantPacket | CrcError;
+ return (status & (ErrorSummary | ErrorMask)) ==
+ (ErrorSummary | DribbleError | CrcError);
+}
+
+struct EthH7MacState
+{
+ enum class DriverState : uint8_t
+ {
+ Uninitialized,
+ Ready,
+ Running,
+ Stopping,
+ Faulted,
+ };
+
+ enum class DriverFailure : uint8_t
+ {
+ None,
+ MacStopTimeout,
+ FatalBusError,
+ };
+
+ volatile DriverState driverState = DriverState::Uninitialized;
+ DriverFailure driverFailure = DriverFailure::None;
+ ethernet::DuplexMode duplexMode = ethernet::DuplexMode::Full;
+ ethernet::Speed speed = ethernet::Speed::Speed100M;
+ ethernet::MediaInterface mediaInterface = ethernet::MediaInterface::MII;
+ uint8_t linkState = 0;
+ bool checksumOffloadEnabled = false;
+ ethernet::MacAddress macAddress{};
+
+ volatile std::size_t rxIndex = 0;
+ volatile std::size_t txIndex = 0;
+ volatile std::size_t rxTailIndex = 0;
+ bool txLeaseOutstanding = false;
+ bool rxLeaseOutstanding = false;
+ volatile uint32_t interruptEvents = 0;
+
+ uint32_t accumulatedRxMissedPackets = 0;
+ uint32_t accumulatedRxOverflowPackets = 0;
+ uint32_t accumulatedRxCrcErrors = 0;
+ uint32_t accumulatedRxAlignmentErrors = 0;
+ uint32_t txDescriptorErrors = 0;
+ uint32_t fatalBusErrors = 0;
+ uint32_t contextDescriptorErrors = 0;
+ uint32_t rxWatchdogTimeouts = 0;
+ uint32_t rxProcessStopped = 0;
+ uint32_t rxBufferUnavailable = 0;
+ uint32_t txProcessStopped = 0;
+ uint32_t txBufferUnavailable = 0;
+ uint32_t hardwareDmaErrorStatus = 0;
+ uint32_t hardwareTxDescriptorErrorStatus = 0;
+};
+
+constexpr bool
+canCommitLinkUpState(EthH7MacState::DriverState state)
+{
+ return state != EthH7MacState::DriverState::Stopping and
+ state != EthH7MacState::DriverState::Faulted;
+}
+
+constexpr bool
+needsFullToHalfDuplexFlush(
+ ethernet::DuplexMode oldMode, ethernet::DuplexMode newMode)
+{
+ return oldMode == ethernet::DuplexMode::Full and
+ newMode == ethernet::DuplexMode::Half;
+}
+
+// DMACSR interrupt bits the driver handles and acknowledges.
+inline constexpr uint32_t EthH7DmaHandledStatus =
+ ETH_DMACSR_NIS | ETH_DMACSR_AIS | ETH_DMACSR_CDE |
+ ETH_DMACSR_FBE | ETH_DMACSR_RWT | ETH_DMACSR_RPS |
+ ETH_DMACSR_RBU | ETH_DMACSR_RI | ETH_DMACSR_TBU |
+ ETH_DMACSR_TPS | ETH_DMACSR_TI;
+inline constexpr uint32_t EthH7DmaErrorStatus =
+ ETH_DMACSR_CDE | ETH_DMACSR_FBE | ETH_DMACSR_RWT |
+ ETH_DMACSR_RPS | ETH_DMACSR_RBU | ETH_DMACSR_TBU |
+ ETH_DMACSR_TPS;
+
+extern EthH7MacState ethH7MacState;
+void ethH7RecordDmaStatus(uint32_t status, bool running);
+uint32_t ethH7BuildWakeupEvents(uint32_t status, bool running);
+void ethH7HandleInterrupt();
+
+} // namespace detail
+
+/// @ingroup modm_platform_eth
+class EthernetMac
+{
+ using TxDmaDescriptor = detail::EthH7TxDmaDescriptor;
+ using RxDmaDescriptor = detail::EthH7RxDmaDescriptor;
+
+ static constexpr std::size_t DmaBufferSize = detail::EthH7DmaBufferSize;
+ static constexpr uint32_t DescriptorAlignment = detail::EthH7DescriptorAlignment;
+ static constexpr uint32_t Rdes1IpPayloadError = modm::Bit7;
+ static constexpr uint32_t Rdes1IpChecksumBypassed = modm::Bit6;
+ static constexpr uint32_t Rdes1Ipv6Header = modm::Bit5;
+ static constexpr uint32_t Rdes1Ipv4Header = modm::Bit4;
+ static constexpr uint32_t Rdes1IpHeaderError = modm::Bit3;
+ static constexpr uint32_t Rdes1PayloadTypeMask = 0x00000003;
+ // RM0433 Rev 8, Tables 709 and 713: normal descriptor status fields used by
+ // software. Context and timestamp descriptors are not supported by this API.
+ static constexpr uint32_t MacDebugStateMask = ETH_MACDR_TFCSTS |
+ ETH_MACDR_TPESTS | ETH_MACDR_RFCFCSTS | ETH_MACDR_RPESTS;
+ static constexpr uint32_t OperationalDmaInterrupts =
+ ETH_DMACIER_NIE | ETH_DMACIER_AIE | ETH_DMACIER_CDEE |
+ ETH_DMACIER_FBEE | ETH_DMACIER_RWTE | ETH_DMACIER_RSE |
+ ETH_DMACIER_RBUE | ETH_DMACIER_RIE | ETH_DMACIER_TXSE |
+ ETH_DMACIER_TIE;
+ static constexpr uint32_t MediaClockUnavailableDmaInterrupts =
+ ETH_DMACIER_AIE | ETH_DMACIER_FBEE;
+ static constexpr int MacResetTimeoutUs = 500'000; // Bound a reset with missing PHY clocks.
+ static constexpr int MacStopTimeoutUs = 100'000;
+ static constexpr int MdioTimeoutUs = 1'000; // Longer than one Clause 22 frame at 1 MHz.
+ static constexpr uint32_t MdioClockMinimum = 20'000'000;
+ static constexpr uint32_t MdioClockMaximum = 300'000'000;
+ static constexpr uint32_t MdioDiv16ClockLimit = 35'000'000;
+ static constexpr uint32_t MdioDiv26ClockLimit = 60'000'000;
+ static constexpr uint32_t MdioDiv42ClockLimit = 100'000'000;
+ static constexpr uint32_t MdioDiv62ClockLimit = 150'000'000;
+ static constexpr uint32_t MdioDiv102ClockLimit = 250'000'000;
+ static constexpr uint32_t MdioPhyAddressMaximum = 31;
+
+public:
+ using MediaInterface = ethernet::MediaInterface;
+ using Speed = ethernet::Speed;
+ using DuplexMode = ethernet::DuplexMode;
+ using LinkMode = ethernet::LinkMode;
+ using LinkState = ethernet::LinkState;
+ using LinkStatus = ethernet::LinkStatus;
+ using ChecksumMode = ethernet::ChecksumMode;
+ using MdioError = ethernet::MdioError;
+ using MacAddress = ethernet::MacAddress;
+
+ enum class WakeupEvent : uint32_t
+ {
+ None = 0x00,
+ Receive = 0x01,
+ Transmit = 0x02,
+ Error = 0x04,
+ };
+ MODM_FLAGS32(WakeupEvent);
+
+ /// Maximum untagged Ethernet frame size without the FCS.
+ static constexpr std::size_t MaxFrameSize = detail::EthH7MaxFrameSize;
+ static constexpr std::size_t RxDescriptorCount = detail::EthH7RxDescriptorCount;
+ static constexpr std::size_t TxDescriptorCount = detail::EthH7TxDescriptorCount;
+ static_assert(RxDescriptorCount >= 4 and RxDescriptorCount <= 1024);
+ static_assert(TxDescriptorCount >= 4 and TxDescriptorCount <= 1024);
+
+ /**
+ * Generate a stable, locally administered unicast MAC address from the
+ * STM32 unique device identifier.
+ */
+ static MacAddress getDefaultMacAddress();
+
+ struct Configuration
+ {
+ MacAddress macAddress = getDefaultMacAddress();
+ /// Select software checksums or MAC checksum insertion and validation.
+ ChecksumMode checksumMode = ChecksumMode::Software;
+ };
+
+ enum class InitializationError : uint8_t
+ {
+ None,
+ OutstandingLease,
+ UnsupportedMdioClock,
+ UnsupportedChecksumOffload,
+ MacResetTimeout,
+ MacStopTimeout, ///< Bounded OSP or duplex transition stop/flush failed.
+ FatalBusError, ///< DMA fault occurred while starting an already up link.
+ };
+
+ /// Result of MAC, DMA, and MDIO controller setup.
+ struct [[nodiscard]] InitializationResult
+ {
+ InitializationError error = InitializationError::None;
+ constexpr explicit operator bool() const noexcept { return error == InitializationError::None; }
+ };
+
+ /// Initialize or reinitialize the MAC, DMA, and MDIO controller.
+ template
+ static InitializationResult
+ initialize(Configuration const& configuration, uint8_t priority = 5)
+ {
+ return initialize(Interface, SystemClock::Eth, configuration, priority);
+ }
+
+ enum class LinkUpdateError : uint8_t
+ {
+ None,
+ NotInitialized,
+ MacStopTimeout, ///< Bounded OSP or duplex transition stop/flush failed.
+ OutstandingLease, ///< Release the frame lease before retrying the transition.
+ FatalBusError,
+ };
+
+ /// MAC update result. Errors return the last effective status.
+ struct [[nodiscard]] LinkUpdateResult
+ {
+ LinkStatus status;
+ LinkUpdateError error = LinkUpdateError::None;
+ constexpr explicit operator bool() const noexcept { return error == LinkUpdateError::None; }
+ };
+
+ /// Notify MAC of latest link state from the PHY. Handles link-down, speed/mode changes, etc.
+ static LinkUpdateResult notifyUpdatedLinkStatus(LinkStatus status);
+
+ enum class TransmitError : uint8_t
+ {
+ None,
+ Busy, ///< Producer lease or descriptor is unavailable.
+ InvalidLength, ///< Frame is empty or exceeds MaxFrameSize.
+ LinkDown, ///< MAC/DMA is not running with an up link.
+ NotInitialized, ///< initialize() has not completed successfully.
+ Faulted, ///< Fatal hardware/lifecycle failure; recover with initialize().
+ UnsupportedFragmentation,///< Hardware checksum mode cannot transmit IPv4 fragments.
+ };
+
+ struct [[nodiscard]] TransmitResult
+ {
+ TransmitError error = TransmitError::None;
+ constexpr explicit operator bool() const noexcept { return error == TransmitError::None; }
+ };
+
+ enum class ReceiveError : uint8_t
+ {
+ None,
+ NoFrameAvailable, ///< No completed descriptor, or another RX lease owns it.
+ NotInitialized, ///< initialize() has not completed successfully.
+ Faulted, ///< Fatal hardware/lifecycle failure; recover with initialize().
+ };
+
+ enum class ReceiveChecksumStatus : uint8_t
+ {
+ /// Software mode, unavailable or bypassed status, fragmented or non-IP
+ /// frame, or a transport protocol other than UDP, TCP, or ICMP.
+ NotChecked,
+ Valid,
+ Invalid,
+ };
+
+ /// Owned handle for a transmit buffer in the DMA ring. Caller must reserve
+ /// space, use the lease to write their data, and commit.
+ class TransmitBufferLease
+ {
+ public:
+ TransmitBufferLease() = default;
+ TransmitBufferLease(TransmitBufferLease const &) = delete;
+ TransmitBufferLease &operator=(TransmitBufferLease const &) = delete;
+ TransmitBufferLease(TransmitBufferLease &&other) noexcept;
+ TransmitBufferLease &operator=(TransmitBufferLease &&other) noexcept;
+ ~TransmitBufferLease() noexcept;
+
+ constexpr explicit operator bool() const noexcept { return error_ == TransmitError::None and active_; }
+ constexpr TransmitError error() const noexcept { return error_; }
+ std::span buffer() noexcept;
+ TransmitResult commit() noexcept;
+ void cancel() noexcept;
+
+ private:
+ friend class EthernetMac;
+ TransmitBufferLease(TransmitError error) : error_(error) {}
+ TransmitBufferLease(std::size_t index, std::size_t length) :
+ index_(index), length_(length), error_(TransmitError::None), active_(true) {}
+
+ std::size_t index_ = 0;
+ std::size_t length_ = 0;
+ TransmitError error_ = TransmitError::Busy;
+ bool active_ = false;
+ };
+
+ /// Owned handle for a received frame in the DMA ring. Caller reads the frame
+ /// through the lease and releases it when finished.
+ class ReceiveBufferLease
+ {
+ public:
+ ReceiveBufferLease() = default;
+ ReceiveBufferLease(ReceiveBufferLease const &) = delete;
+ ReceiveBufferLease &operator=(ReceiveBufferLease const &) = delete;
+ ReceiveBufferLease(ReceiveBufferLease &&other) noexcept;
+ ReceiveBufferLease &operator=(ReceiveBufferLease &&other) noexcept;
+ ~ReceiveBufferLease() noexcept;
+
+ constexpr explicit operator bool() const noexcept { return error_ == ReceiveError::None and active_; }
+ constexpr ReceiveError error() const noexcept { return error_; }
+ std::span buffer() const noexcept;
+ ReceiveChecksumStatus checksumStatus() const noexcept;
+ void release() noexcept;
+
+ private:
+ friend class EthernetMac;
+ ReceiveBufferLease(ReceiveError error) : error_(error) {}
+ ReceiveBufferLease(std::size_t index, std::size_t length,
+ ReceiveChecksumStatus checksumStatus) :
+ index_(index), length_(length), error_(ReceiveError::None),
+ checksumStatus_(checksumStatus), active_(true) {}
+
+ std::size_t index_ = 0;
+ std::size_t length_ = 0;
+ ReceiveError error_ = ReceiveError::NoFrameAvailable;
+ ReceiveChecksumStatus checksumStatus_ = ReceiveChecksumStatus::NotChecked;
+ bool active_ = false;
+ };
+
+ /// Snapshot of driver-owned software diagnostics and MAC/MTL counters.
+ struct ErrorCounters
+ {
+ uint32_t rxMissedPackets = 0;
+ uint32_t rxOverflowPackets = 0;
+ uint32_t rxCrcErrors = 0;
+ uint32_t rxAlignmentErrors = 0;
+ uint32_t txDescriptorErrors = 0;
+ uint32_t fatalBusErrors = 0;
+ uint32_t contextDescriptorErrors = 0;
+ uint32_t rxWatchdogTimeouts = 0;
+ uint32_t rxProcessStopped = 0;
+ uint32_t rxBufferUnavailable = 0;
+ uint32_t txProcessStopped = 0;
+ uint32_t txBufferUnavailable = 0;
+ };
+
+ /// Sticky raw hardware errors kept across initialization and separate from
+ /// cumulative event counters. Only resetHardwareErrorStatus() clears them.
+ struct HardwareErrorStatus
+ {
+ uint32_t dmaStatus = 0;
+ uint32_t txDescriptorStatus = 0;
+ };
+
+ template< class... Signals >
+ static void
+ connect()
+ {
+ (GpioStatic::configure(Gpio::OutputType::PushPull, Gpio::OutputSpeed::VeryHigh), ...);
+ GpioConnector::connect();
+ }
+
+ /// Return current value of cumulative error counters.
+ static ErrorCounters
+ getErrorCounters()
+ {
+ modm::atomic::Lock lock;
+ // Register is cleared by read, so accumulate its value each time we read it
+ const uint32_t mtlRxMissedOverflow = ETH->MTLRQMPOCR;
+ state().accumulatedRxMissedPackets +=
+ (mtlRxMissedOverflow & ETH_MTLRQMPOCR_MISPKTCNT) >> ETH_MTLRQMPOCR_MISPKTCNT_Pos;
+ state().accumulatedRxOverflowPackets +=
+ (mtlRxMissedOverflow & ETH_MTLRQMPOCR_OVFPKTCNT) >> ETH_MTLRQMPOCR_OVFPKTCNT_Pos;
+ return {
+ .rxMissedPackets = state().accumulatedRxMissedPackets,
+ .rxOverflowPackets = state().accumulatedRxOverflowPackets,
+ .rxCrcErrors = state().accumulatedRxCrcErrors + ETH->MMCRCRCEPR,
+ .rxAlignmentErrors =
+ state().accumulatedRxAlignmentErrors + ETH->MMCRAEPR,
+ .txDescriptorErrors = state().txDescriptorErrors,
+ .fatalBusErrors = state().fatalBusErrors,
+ .contextDescriptorErrors = state().contextDescriptorErrors,
+ .rxWatchdogTimeouts = state().rxWatchdogTimeouts,
+ .rxProcessStopped = state().rxProcessStopped,
+ .rxBufferUnavailable = state().rxBufferUnavailable,
+ .txProcessStopped = state().txProcessStopped,
+ .txBufferUnavailable = state().txBufferUnavailable,
+ };
+ }
+
+ /// Reset all software and hardware counters.
+ /// @warning Does not support being called from an ISR.
+ static void
+ resetErrorCounters()
+ {
+ modm::atomic::Lock lock;
+ state().accumulatedRxMissedPackets = 0;
+ state().accumulatedRxOverflowPackets = 0;
+ state().accumulatedRxCrcErrors = 0;
+ state().accumulatedRxAlignmentErrors = 0;
+ state().txDescriptorErrors = 0;
+ state().fatalBusErrors = 0;
+ state().contextDescriptorErrors = 0;
+ state().rxWatchdogTimeouts = 0;
+ state().rxProcessStopped = 0;
+ state().rxBufferUnavailable = 0;
+ state().txProcessStopped = 0;
+ state().txBufferUnavailable = 0;
+ (void) ETH->MTLRQMPOCR;
+ ETH->MMCCR |= ETH_MMCCR_CNTRST;
+ }
+
+ static HardwareErrorStatus
+ getHardwareErrorStatus()
+ {
+ modm::atomic::Lock lock;
+ return {
+ .dmaStatus = state().hardwareDmaErrorStatus,
+ .txDescriptorStatus =
+ state().hardwareTxDescriptorErrorStatus,
+ };
+ }
+
+ static void
+ resetHardwareErrorStatus()
+ {
+ modm::atomic::Lock lock;
+ state().hardwareDmaErrorStatus = 0;
+ state().hardwareTxDescriptorErrorStatus = 0;
+ }
+
+ /// Transmits the given data as an Ethernet frame. Supports at most one MTU size.
+ static TransmitResult transmit(std::span frame);
+ static TransmitBufferLease acquireTransmitBuffer(std::size_t length);
+ static ReceiveBufferLease tryAcquireReceiveBuffer();
+
+ static MdioError writePhyRegister(uint8_t phyAddress, uint8_t reg, uint16_t value);
+ static MdioError readPhyRegister(uint8_t phyAddress, uint8_t reg, uint16_t &value);
+
+ static LinkStatus
+ getLinkStatus() {
+ return currentLinkStatus();
+ }
+
+ /// Return and clear pending wakeup events from MAC interrupts.
+ static WakeupEvent_t
+ consumeWakeupEvents()
+ {
+ modm::atomic::Lock lock;
+ const auto events = state().interruptEvents;
+ state().interruptEvents = 0;
+ return WakeupEvent_t(events);
+ }
+
+ /// Test whether an interrupt has requested foreground servicing.
+ static bool
+ hasPendingWakeupEvents(WakeupEvent_t events = WakeupEvent::Receive |
+ WakeupEvent::Transmit | WakeupEvent::Error)
+ {
+ return (state().interruptEvents & events.value) != 0;
+ }
+
+private:
+ using DriverState = detail::EthH7MacState::DriverState;
+ using DriverFailure = detail::EthH7MacState::DriverFailure;
+
+ static detail::EthH7MacState& state() { return detail::ethH7MacState; }
+ static detail::EthH7DmaStorage& dmaStorage() { return detail::ethH7DmaStorage; }
+
+ static InitializationResult initialize(MediaInterface interface, uint32_t ethernetClock,
+ Configuration const& configuration, uint8_t priority);
+ static bool configureMdioClock(uint32_t ethernetClock);
+ static void selectMediaInterface(MediaInterface interface);
+ static void configureMac();
+ static void configureMacAddress();
+ static void configureDma();
+ static void configureReceiveErrorForwarding();
+ static void initializeDescriptors();
+ static void enableInterruptVector() { NVIC_EnableIRQ(ETH_IRQn); }
+
+ static LinkUpdateError updateLinkMode(bool requireReceiveQueueIdle = true);
+ static LinkUpdateError start();
+ static LinkUpdateError stop();
+ static LinkUpdateError stopWithoutMediaClock();
+ static LinkUpdateError resumeWithRestoredMediaClock(LinkMode mode);
+ static LinkUpdateError resetMacDma(LinkMode mode);
+
+ static void accumulateHardwareCounters();
+ static uint32_t captureLiveDmaStatus(bool running);
+ static bool latchPendingFatalBusError();
+ static LinkUpdateError faultError();
+ static LinkUpdateError stopTimeoutError();
+ static void setDriverFailure(DriverFailure failure);
+
+ static bool waitForTransmitDmaSuspend();
+ static bool waitForTransmitDmaInactive();
+ static bool waitForReceivePathIdle();
+ static bool isTransmitPathIdle();
+ static bool waitForTransmitPathIdle();
+ static bool waitForMacIdle();
+ static bool waitForTransmitQueueFlush();
+
+ static TransmitResult commitTransmitLease(std::size_t index, std::size_t length);
+ static void cancelTransmitLease(std::size_t index);
+ static bool hasUnsupportedHardwareChecksumFragment(std::span frame);
+ static bool releaseRxDescriptor(std::size_t index, bool resumeDma=true);
+ static bool releaseBadRxDescriptor();
+ static void resumeReceiveDma();
+ static void releaseReceiveLease(std::size_t index);
+ static bool isValidReceiveDescriptor(RxDmaDescriptor const &descriptor);
+ static ReceiveChecksumStatus classifyReceiveChecksum(RxDmaDescriptor const &descriptor);
+
+ static LinkStatus currentLinkStatus();
+ static DriverState driverState() { return state().driverState; }
+ static void setDriverState(DriverState value) { state().driverState = value; }
+
+ static uint32_t
+ dmaAddress(void const *ptr) {
+ static_assert(sizeof(std::uintptr_t) == sizeof(uint32_t));
+ return static_cast(reinterpret_cast(ptr));
+ }
+
+ static std::uintptr_t
+ cacheLineStart(std::uintptr_t address) {
+ return address & ~(std::uintptr_t(DescriptorAlignment) - 1);
+ }
+
+ static std::uintptr_t
+ cacheLineEnd(std::uintptr_t address) {
+ return (address + DescriptorAlignment - 1) & ~(std::uintptr_t(DescriptorAlignment) - 1);
+ }
+
+ static void cleanDCache(void const *address, std::size_t length);
+ static void invalidateDCache(void const *address, std::size_t length);
+
+};
+
+}
+}
+
+#include "eth_impl.hpp"
+
+#endif // MODM_ETH_HPP
diff --git a/src/modm/platform/eth/stm32h7/eth_impl.hpp.in b/src/modm/platform/eth/stm32h7/eth_impl.hpp.in
new file mode 100644
index 0000000000..588d5c38a9
--- /dev/null
+++ b/src/modm/platform/eth/stm32h7/eth_impl.hpp.in
@@ -0,0 +1,1143 @@
+/*
+ * Copyright (c) 2026, Kaelin Laundry
+ *
+ * This file is part of the modm project.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+// ----------------------------------------------------------------------------
+
+#ifndef MODM_ETH_HPP
+# error "Don't include this file directly, use 'eth.hpp' instead!"
+#endif
+
+namespace modm::platform {
+
+
+inline EthernetMac::MacAddress
+EthernetMac::getDefaultMacAddress()
+{
+ // FNV-1a folds the 96-bit device ID into 64 bits with inexpensive mixing;
+ // the low 48 bits provide the address before local and unicast bits are applied
+ constexpr uint64_t FnvOffsetBasis = 14'695'981'039'346'656'037ull;
+ constexpr uint64_t FnvPrime = 1'099'511'628'211ull;
+ uint64_t hash = FnvOffsetBasis;
+
+ for (uint8_t wordIndex = 0; wordIndex < 3; ++wordIndex) {
+ uint32_t word = getUniqueId(wordIndex);
+ for (uint8_t byteIndex = 0; byteIndex < 4; ++byteIndex) {
+ hash ^= uint8_t(word);
+ hash *= FnvPrime;
+ word >>= 8;
+ }
+ }
+
+ MacAddress address{};
+ for (uint8_t index = 0; index < address.size(); ++index)
+ address[index] = uint8_t(hash >> (index * 8));
+ // Unicast, locally administered, administratively assigned identifier.
+ address[0] = uint8_t((address[0] & 0xf0) | 0x02);
+ return address;
+}
+
+
+inline EthernetMac::InitializationResult
+EthernetMac::initialize(MediaInterface interface, uint32_t ethernetClock,
+ Configuration const& configuration, uint8_t priority)
+{
+ if (state().txLeaseOutstanding or state().rxLeaseOutstanding)
+ return {InitializationError::OutstandingLease};
+
+ Rcc::enable();
+ const bool wasRunning = driverState() == DriverState::Running;
+ const bool preserveHardwareCounters = driverState() != DriverState::Uninitialized;
+ NVIC_DisableIRQ(ETH_IRQn);
+ ETH->DMACIER = 0;
+ const uint32_t activeDmaStatus = captureLiveDmaStatus(wasRunning);
+ ETH->DMACSR = activeDmaStatus & detail::EthH7DmaHandledStatus;
+ if ((activeDmaStatus & ETH_DMACSR_FBE) != 0)
+ setDriverFailure(DriverFailure::FatalBusError);
+ ETH->DMACTCR &= ~ETH_DMACTCR_ST;
+ ETH->DMACRCR &= ~ETH_DMACRCR_SR;
+ // Stop MAC TX/RX
+ ETH->MACCR &= ~(ETH_MACCR_TE | ETH_MACCR_RE);
+ // Preserve hardware diagnostics across reinitialization
+ if (preserveHardwareCounters)
+ accumulateHardwareCounters();
+ setDriverState(DriverState::Uninitialized);
+ state().driverFailure = DriverFailure::None;
+ state().linkState = uint8_t(LinkState::Down);
+ state().interruptEvents = 0;
+
+ // Route Ethernet signals through the selected MII or RMII interface
+ NVIC_SetPriority(ETH_IRQn, priority);
+ selectMediaInterface(interface);
+ state().mediaInterface = interface;
+ const uint32_t pendingDmaStatus = captureLiveDmaStatus(false);
+ ETH->DMACSR = pendingDmaStatus & detail::EthH7DmaHandledStatus;
+ if ((pendingDmaStatus & ETH_DMACSR_FBE) != 0)
+ setDriverFailure(DriverFailure::FatalBusError);
+ ETH->DMAMR |= ETH_DMAMR_SWR;
+ if (not modm::this_fiber::poll_for(std::chrono::microseconds{MacResetTimeoutUs},
+ []{ return (ETH->DMAMR & ETH_DMAMR_SWR) == 0; }))
+ return {InitializationError::MacResetTimeout};
+
+ // Validate the requested checksum offload against the MAC capabilities
+ state().checksumOffloadEnabled = configuration.checksumMode == ChecksumMode::Hardware;
+ if (state().checksumOffloadEnabled) {
+ constexpr uint32_t capabilities = ETH_MACHWF0R_RXCOESEL | ETH_MACHWF0R_TXCOESEL;
+ if ((ETH->MACHWF0R & capabilities) != capabilities) {
+ state().checksumOffloadEnabled = false;
+ return {InitializationError::UnsupportedChecksumOffload};
+ }
+ }
+
+ // Select an MDIO divider for the Ethernet peripheral clock
+ if (not configureMdioClock(ethernetClock))
+ return {InitializationError::UnsupportedMdioClock};
+
+ // Configure a stopped interface with fresh descriptor rings
+ state().macAddress = configuration.macAddress;
+ state().duplexMode = DuplexMode::Full;
+ state().speed = Speed::Speed100M;
+ configureMac();
+ configureDma();
+ initializeDescriptors();
+ ETH->DMACSR = detail::EthH7DmaHandledStatus;
+ state().driverFailure = DriverFailure::None;
+ setDriverState(DriverState::Ready);
+ state().linkState = uint8_t(LinkState::Down);
+ NVIC_ClearPendingIRQ(ETH_IRQn);
+ enableInterruptVector();
+ return {};
+}
+
+inline void
+EthernetMac::selectMediaInterface(MediaInterface interface)
+{
+%% if target.family == "h5"
+ const uint32_t selection = interface == MediaInterface::RMII ?
+ SBS_PMCR_ETH_SEL_PHY_2 : 0;
+ SBS->PMCR = (SBS->PMCR & ~SBS_PMCR_ETH_SEL_PHY) | selection;
+ (void) SBS->PMCR;
+%% else
+ const uint32_t selection = interface == MediaInterface::RMII ?
+ SYSCFG_PMCR_EPIS_SEL_2 : 0;
+ SYSCFG->PMCR = (SYSCFG->PMCR & ~SYSCFG_PMCR_EPIS_SEL) | selection;
+ (void) SYSCFG->PMCR;
+%% endif
+}
+
+inline bool
+EthernetMac::configureMdioClock(uint32_t ethernetClock)
+{
+ if (ethernetClock < MdioClockMinimum or ethernetClock > MdioClockMaximum)
+ return false;
+ uint32_t divider;
+ if (ethernetClock < MdioDiv16ClockLimit) divider = ETH_MACMDIOAR_CR_DIV16;
+ else if (ethernetClock < MdioDiv26ClockLimit) divider = ETH_MACMDIOAR_CR_DIV26;
+ else if (ethernetClock < MdioDiv42ClockLimit) divider = ETH_MACMDIOAR_CR_DIV42;
+ else if (ethernetClock < MdioDiv62ClockLimit) divider = ETH_MACMDIOAR_CR_DIV62;
+ else if (ethernetClock < MdioDiv102ClockLimit) divider = ETH_MACMDIOAR_CR_DIV102;
+ else divider = ETH_MACMDIOAR_CR_DIV124;
+ ETH->MACMDIOAR = (ETH->MACMDIOAR & ~ETH_MACMDIOAR_CR) | divider;
+ return true;
+}
+
+inline void
+EthernetMac::configureMac()
+{
+ uint32_t maccr = ETH_MACCR_IPG_96BIT | ETH_MACCR_CST | ETH_MACCR_ACS;
+ if (state().speed == Speed::Speed100M) maccr |= ETH_MACCR_FES;
+ if (state().duplexMode == DuplexMode::Full) maccr |= ETH_MACCR_DM;
+ if (state().checksumOffloadEnabled) maccr |= ETH_MACCR_IPC;
+ ETH->MACCR = maccr;
+ ETH->MACPFR = ETH_MACPFR_PCF_BLOCKALL;
+ ETH->MACHT0R = 0;
+ ETH->MACHT1R = 0;
+ ETH->MACVTR = 0;
+ ETH->MACTFCR = 0;
+ ETH->MACRFCR = 0;
+ configureMacAddress();
+}
+
+inline void
+EthernetMac::configureDma()
+{
+ // Enable store and forward queues. They buffer each complete frame before
+ // DMA transmission or reception.
+ ETH->MTLTQOMR |= ETH_MTLTQOMR_TSF;
+ // Retain checksum error frames only when software must validate them
+ uint32_t receiveQueue = ETH->MTLRQOMR | ETH_MTLRQOMR_RSF;
+ if (state().checksumOffloadEnabled) receiveQueue |= ETH_MTLRQOMR_DISTCPEF;
+ else receiveQueue &= ~ETH_MTLRQOMR_DISTCPEF;
+ receiveQueue &= ~ETH_MTLRQOMR_FEP;
+ ETH->MTLRQOMR = receiveQueue;
+
+ // Give TX and RX equal DMA priority
+ ETH->DMAMR = ETH_DMAMR_PR_1_1;
+ // Use aligned fixed bursts on the system bus
+ ETH->DMASBMR = ETH_DMASBMR_AAL | ETH_DMASBMR_FB;
+
+ // Descriptors occupy one cache line each
+ ETH->DMACCR = (ETH->DMACCR & ~(ETH_DMACCR_DSL | ETH_DMACCR_MSS | ETH_DMACCR_8PBL)) |
+ ETH_DMACCR_DSL_128BIT;
+ // Transfer packet buffers in 32 beat bursts
+ ETH->DMACTCR = ETH_DMACTCR_TPBL_32PBL | ETH_DMACTCR_OSP;
+ ETH->DMACRCR = ETH_DMACRCR_RPBL_32PBL |
+ ((DmaBufferSize << ETH_DMACRCR_RBSZ_Pos) & ETH_DMACRCR_RBSZ);
+
+ // Disable DMA channel interrupts until start() enables the link
+ ETH->DMACIER = 0;
+ // Mask MAC interrupt sources because this driver only uses DMA channel interrupts
+ ETH->MACIER = 0;
+ // Mask unused MMC counter interrupts
+ ETH->MMCTIMR = ETH_MMCTIMR_TXLPITRCIM | ETH_MMCTIMR_TXLPIUSCIM |
+ ETH_MMCTIMR_TXGPKTIM | ETH_MMCTIMR_TXMCOLGPIM |
+ ETH_MMCTIMR_TXSCOLGPIM;
+ ETH->MMCRIMR = ETH_MMCRIMR_RXLPITRCIM | ETH_MMCRIMR_RXLPIUSCIM |
+ ETH_MMCRIMR_RXUCGPIM | ETH_MMCRIMR_RXALGNERPIM |
+ ETH_MMCRIMR_RXCRCERPIM;
+}
+
+inline EthernetMac::LinkUpdateError
+EthernetMac::updateLinkMode(bool requireReceiveQueueIdle)
+{
+ const bool fullToHalf = detail::needsFullToHalfDuplexFlush(
+ (ETH->MACCR & ETH_MACCR_DM) ? DuplexMode::Full : DuplexMode::Half,
+ state().duplexMode);
+ uint32_t macConfiguration = ETH->MACCR & ~(ETH_MACCR_FES | ETH_MACCR_DM);
+ if (state().speed == Speed::Speed100M) macConfiguration |= ETH_MACCR_FES;
+ if (state().duplexMode == DuplexMode::Full) macConfiguration |= ETH_MACCR_DM;
+
+ if (fullToHalf) {
+ // ES0392 Rev 15, section 2.26.10: validate the stopped receive and transmit
+ // paths, then make FTQ the immediately following MMIO write after MACCR.DM
+ // is cleared. RM0433 Rev 8, section 58.9.8 defines link-mode changes.
+ if (requireReceiveQueueIdle and not waitForReceivePathIdle()) {
+ return stopTimeoutError();
+ }
+ if (not waitForTransmitPathIdle()) {
+ return stopTimeoutError();
+ }
+ const uint32_t flushRequest = ETH->MTLTQOMR | ETH_MTLTQOMR_FTQ;
+ {
+ // ES0392 Rev 15, section 2.26.10 requires these two precomputed
+ // peripheral writes to be adjacent in both execution and MMIO order
+ modm::atomic::Lock lock;
+ ETH->MACCR = macConfiguration;
+ ETH->MTLTQOMR = flushRequest;
+ }
+ if (not waitForTransmitQueueFlush()) {
+ return stopTimeoutError();
+ }
+ }
+ else {
+ ETH->MACCR = macConfiguration;
+ }
+
+ configureReceiveErrorForwarding();
+ return LinkUpdateError::None;
+}
+
+inline void
+EthernetMac::configureReceiveErrorForwarding()
+{
+ uint32_t receiveQueue = ETH->MTLRQOMR;
+ // ES0392 Rev 15, section 2.26.20: forward errors only in RMII
+ // 10 Mbit/s so software can accept the exact false dribble+CRC signature
+ if (detail::needsRmii10MWorkaround(
+ state().mediaInterface, state().speed)) {
+ receiveQueue |= ETH_MTLRQOMR_FEP;
+ }
+ else {
+ receiveQueue &= ~ETH_MTLRQOMR_FEP;
+ }
+ ETH->MTLRQOMR = receiveQueue;
+}
+
+inline void
+EthernetMac::initializeDescriptors()
+{
+ state().rxIndex = 0;
+ state().txIndex = 0;
+ state().rxTailIndex = RxDescriptorCount - 1;
+ state().txLeaseOutstanding = false;
+ state().rxLeaseOutstanding = false;
+ for (std::size_t index = 0; index < TxDescriptorCount; ++index) {
+ dmaStorage().txDescriptors[index].clear();
+ cleanDCache(&dmaStorage().txDescriptors[index], sizeof(TxDmaDescriptor));
+ }
+ for (std::size_t index = 0; index < RxDescriptorCount; ++index)
+ releaseRxDescriptor(index, false);
+ __DSB();
+ ETH->DMACTDRLR = TxDescriptorCount - 1;
+ ETH->DMACTDLAR = dmaAddress(dmaStorage().txDescriptors);
+ ETH->DMACTDTPR = dmaAddress(dmaStorage().txDescriptors);
+ ETH->DMACRDRLR = RxDescriptorCount - 1;
+ ETH->DMACRDLAR = dmaAddress(dmaStorage().rxDescriptors);
+ ETH->DMACRDTPR = dmaAddress(&dmaStorage().rxDescriptors[state().rxTailIndex]);
+}
+
+inline void
+EthernetMac::configureMacAddress()
+{
+ ETH->MACA0HR = (uint32_t(state().macAddress[5]) << 8) | uint32_t(state().macAddress[4]);
+ ETH->MACA0LR = (uint32_t(state().macAddress[3]) << 24) | (uint32_t(state().macAddress[2]) << 16) |
+ (uint32_t(state().macAddress[1]) << 8) | uint32_t(state().macAddress[0]);
+ ETH->MACA1HR = ETH->MACA1LR = 0;
+ ETH->MACA2HR = ETH->MACA2LR = 0;
+ ETH->MACA3HR = ETH->MACA3LR = 0;
+}
+
+inline void
+EthernetMac::accumulateHardwareCounters()
+{
+ const uint32_t missedOverflow = ETH->MTLRQMPOCR;
+ state().accumulatedRxMissedPackets +=
+ (missedOverflow & ETH_MTLRQMPOCR_MISPKTCNT) >> ETH_MTLRQMPOCR_MISPKTCNT_Pos;
+ state().accumulatedRxOverflowPackets +=
+ (missedOverflow & ETH_MTLRQMPOCR_OVFPKTCNT) >> ETH_MTLRQMPOCR_OVFPKTCNT_Pos;
+ state().accumulatedRxCrcErrors += ETH->MMCRCRCEPR;
+ state().accumulatedRxAlignmentErrors += ETH->MMCRAEPR;
+}
+
+inline uint32_t
+EthernetMac::captureLiveDmaStatus(bool running)
+{
+ const uint32_t status = ETH->DMACSR;
+ detail::ethH7RecordDmaStatus(status, running);
+ return status;
+}
+
+inline EthernetMac::LinkUpdateError
+EthernetMac::start()
+{
+ if (driverState() != DriverState::Ready)
+ return driverState() == DriverState::Faulted ?
+ faultError() : LinkUpdateError::NotInitialized;
+ if (const auto error = updateLinkMode(); error != LinkUpdateError::None)
+ return error;
+
+ modm::atomic::Lock lock;
+ if (latchPendingFatalBusError() or driverState() != DriverState::Ready)
+ return faultError();
+
+ // Mark the TX ring empty at the next descriptor software will fill
+ ETH->DMACTDTPR = dmaAddress(&dmaStorage().txDescriptors[state().txIndex]);
+ // Expose all released RX descriptors to DMA
+ ETH->DMACRDTPR = dmaAddress(&dmaStorage().rxDescriptors[state().rxTailIndex]);
+ ETH->DMACSR = ETH_DMACSR_TPS | ETH_DMACSR_RPS;
+ ETH->DMACIER = 0;
+ NVIC_ClearPendingIRQ(ETH_IRQn);
+ // Start both DMA channels
+ ETH->DMACTCR |= ETH_DMACTCR_ST;
+ ETH->DMACRCR |= ETH_DMACRCR_SR;
+ __DSB();
+ // Enable MAC transmission and reception
+ ETH->MACCR |= ETH_MACCR_TE | ETH_MACCR_RE;
+ // Publish the running state before enabling operational DMA interrupts
+ setDriverState(DriverState::Running);
+ ETH->DMACIER = OperationalDmaInterrupts;
+ NVIC_EnableIRQ(ETH_IRQn);
+ return LinkUpdateError::None;
+}
+
+inline EthernetMac::LinkUpdateError
+EthernetMac::stop()
+{
+ // RM0433 Rev 8, sections 58.11.2-58.11.4: ETH_MTLRQDR and ETH_MACDR expose
+ // the receive-drain state. Leave DMACRCR.SR enabled while MACCR.RE is clear
+ // so frames already accepted by the MAC can drain into prepared descriptors.
+ {
+ modm::atomic::Lock lock;
+ if (latchPendingFatalBusError())
+ return LinkUpdateError::FatalBusError;
+ if (driverState() == DriverState::Faulted)
+ return faultError();
+ if (state().txLeaseOutstanding or state().rxLeaseOutstanding)
+ return LinkUpdateError::OutstandingLease;
+
+ ETH->DMACIER = 0;
+ NVIC_DisableIRQ(ETH_IRQn);
+ setDriverState(DriverState::Stopping);
+ ETH->MACCR &= ~ETH_MACCR_RE;
+ }
+
+ // ES0392 Rev 15, sections 2.26.1, 2.26.3, and 2.26.5: with OSP enabled,
+ // stop producer-tail updates, wait for Suspend, clear ST, then wait until
+ // the DMA, MTL, and MAC transmit paths are fully idle
+ if (not waitForTransmitDmaSuspend()) {
+ return stopTimeoutError();
+ }
+
+ ETH->DMACTCR &= ~ETH_DMACTCR_ST;
+ if (not waitForTransmitDmaInactive()) {
+ return stopTimeoutError();
+ }
+
+ if (not waitForReceivePathIdle()) {
+ return stopTimeoutError();
+ }
+
+ if (not waitForTransmitPathIdle()) {
+ return stopTimeoutError();
+ }
+
+ ETH->MACCR &= ~ETH_MACCR_TE;
+ ETH->DMACSR = ETH_DMACSR_TPS | ETH_DMACSR_RPS;
+ {
+ modm::atomic::Lock lock;
+ if (latchPendingFatalBusError())
+ return LinkUpdateError::FatalBusError;
+ // Ready blocks public packet operations; internally SR may remain enabled
+ // while MACCR.RE is clear because no new frames can enter the receive path
+ NVIC_ClearPendingIRQ(ETH_IRQn);
+ setDriverState(DriverState::Ready);
+ ETH->DMACIER = MediaClockUnavailableDmaInterrupts;
+ NVIC_EnableIRQ(ETH_IRQn);
+ }
+ return LinkUpdateError::None;
+}
+
+inline EthernetMac::LinkUpdateError
+EthernetMac::stopWithoutMediaClock()
+{
+ modm::atomic::Lock lock;
+ if (latchPendingFatalBusError())
+ return LinkUpdateError::FatalBusError;
+ if (driverState() == DriverState::Faulted)
+ return faultError();
+ if (state().txLeaseOutstanding or state().rxLeaseOutstanding)
+ return LinkUpdateError::OutstandingLease;
+
+ // Work around STM32H7 errata ES0392 sections 2.26.1, 2.26.3, and 2.26.5:
+ // link loss may remove the MII media clocks, so clear TE/RE but leave ST/SR
+ // set until the clocks return and the required OSP stop sequence can run
+ setDriverState(DriverState::Stopping);
+ ETH->MACCR &= ~(ETH_MACCR_TE | ETH_MACCR_RE);
+ ETH->DMACTCR |= ETH_DMACTCR_ST;
+ ETH->DMACRCR |= ETH_DMACRCR_SR;
+ ETH->DMACIER = MediaClockUnavailableDmaInterrupts;
+ NVIC_EnableIRQ(ETH_IRQn);
+ return LinkUpdateError::None;
+}
+
+inline EthernetMac::LinkUpdateError
+EthernetMac::resumeWithRestoredMediaClock(LinkMode mode)
+{
+ // Complete the STM32H7 errata stop workaround after media clocks return.
+ // Every defined MACDR state must be idle before changing mode or enabling TE/RE.
+ if (not waitForMacIdle())
+ return stopTimeoutError();
+ {
+ modm::atomic::Lock lock;
+ if (latchPendingFatalBusError())
+ return LinkUpdateError::FatalBusError;
+ if (driverState() != DriverState::Stopping)
+ return driverState() == DriverState::Faulted ?
+ faultError() : LinkUpdateError::NotInitialized;
+
+ ETH->DMACIER = 0;
+ NVIC_DisableIRQ(ETH_IRQn);
+ }
+
+ const bool fullToHalf = detail::needsFullToHalfDuplexFlush(
+ (ETH->MACCR & ETH_MACCR_DM) ? DuplexMode::Full : DuplexMode::Half,
+ mode.duplex);
+ if (fullToHalf and not isTransmitPathIdle())
+ return resetMacDma(mode);
+
+ state().speed = mode.speed;
+ state().duplexMode = mode.duplex;
+ if (const auto error = updateLinkMode(false); error != LinkUpdateError::None)
+ return error;
+
+ modm::atomic::Lock lock;
+ if (latchPendingFatalBusError())
+ return LinkUpdateError::FatalBusError;
+ if (driverState() != DriverState::Stopping)
+ return driverState() == DriverState::Faulted ?
+ faultError() : LinkUpdateError::NotInitialized;
+ // Speed-only and half-to-full transitions retain the DMA rings and queued
+ // transmit descriptors. Only the stopped MAC is re-enabled.
+ ETH->MACCR |= ETH_MACCR_TE | ETH_MACCR_RE;
+ setDriverState(DriverState::Running);
+ ETH->DMACIER = OperationalDmaInterrupts;
+ NVIC_EnableIRQ(ETH_IRQn);
+ return LinkUpdateError::None;
+}
+
+inline EthernetMac::LinkUpdateError
+EthernetMac::resetMacDma(LinkMode mode)
+{
+ uint32_t mdioClock;
+ {
+ modm::atomic::Lock lock;
+ if (latchPendingFatalBusError())
+ return LinkUpdateError::FatalBusError;
+ if (driverState() != DriverState::Stopping)
+ return driverState() == DriverState::Faulted ?
+ faultError() : LinkUpdateError::NotInitialized;
+ ETH->DMACIER = 0;
+ NVIC_DisableIRQ(ETH_IRQn);
+ accumulateHardwareCounters();
+ const uint32_t pendingDmaStatus = captureLiveDmaStatus(false);
+ if ((pendingDmaStatus & ETH_DMACSR_FBE) != 0) {
+ setDriverFailure(DriverFailure::FatalBusError);
+ ETH->DMACSR = pendingDmaStatus & detail::EthH7DmaHandledStatus;
+ return LinkUpdateError::FatalBusError;
+ }
+ ETH->DMACSR = pendingDmaStatus & detail::EthH7DmaHandledStatus;
+ mdioClock = ETH->MACMDIOAR & ETH_MACMDIOAR_CR;
+ ETH->DMAMR |= ETH_DMAMR_SWR;
+ }
+
+ // ES0392 Rev 15, section 2.26.10: if a full-to-half transition cannot
+ // atomically clear DM and request FTQ from an idle transmitter, discard
+ // uncertain queued frames with an internal MAC/DMA reset
+ if (not modm::this_fiber::poll_for(std::chrono::microseconds{MacResetTimeoutUs},
+ []{ return (ETH->DMAMR & ETH_DMAMR_SWR) == 0; }))
+ return stopTimeoutError();
+
+ ETH->MACMDIOAR = mdioClock;
+ state().speed = mode.speed;
+ state().duplexMode = mode.duplex;
+ configureMac();
+ configureDma();
+ initializeDescriptors();
+ ETH->DMACSR = detail::EthH7DmaHandledStatus;
+ setDriverState(DriverState::Ready);
+ return start();
+}
+
+inline bool
+EthernetMac::waitForTransmitDmaSuspend()
+{
+ return modm::this_fiber::poll_for(std::chrono::microseconds{MacStopTimeoutUs}, [] {
+ const uint32_t state = ETH->DMADSR & ETH_DMADSR_TPS;
+ return state == ETH_DMADSR_TPS_SUSPENDED or state == ETH_DMADSR_TPS_STOPPED;
+ });
+}
+
+inline bool
+EthernetMac::waitForTransmitDmaInactive()
+{
+ return modm::this_fiber::poll_for(std::chrono::microseconds{MacStopTimeoutUs}, [] {
+ const uint32_t state = ETH->DMADSR & ETH_DMADSR_TPS;
+ return state == ETH_DMADSR_TPS_STOPPED;
+ });
+}
+
+inline bool
+EthernetMac::waitForReceivePathIdle()
+{
+ return modm::this_fiber::poll_for(std::chrono::microseconds{MacStopTimeoutUs}, [] {
+ constexpr uint32_t MacReceiveState = ETH_MACDR_RFCFCSTS | ETH_MACDR_RPESTS;
+ return ETH->MTLRQDR == 0 and (ETH->MACDR & MacReceiveState) == 0;
+ });
+}
+
+inline bool
+EthernetMac::waitForMacIdle()
+{
+ return modm::this_fiber::poll_for(std::chrono::microseconds{MacStopTimeoutUs}, [] {
+ return (ETH->MACDR & MacDebugStateMask) == 0;
+ });
+}
+
+inline bool
+EthernetMac::isTransmitPathIdle()
+{
+ const uint32_t dmaState = ETH->DMADSR & ETH_DMADSR_TPS;
+ const bool dmaInactive = dmaState == ETH_DMADSR_TPS_SUSPENDED or
+ dmaState == ETH_DMADSR_TPS_STOPPED;
+ return dmaInactive and ETH->MTLTQDR == 0 and
+ (ETH->MACDR & MacDebugStateMask) == 0;
+}
+
+inline bool
+EthernetMac::waitForTransmitPathIdle()
+{
+ return modm::this_fiber::poll_for(std::chrono::microseconds{MacStopTimeoutUs}, [] {
+ return isTransmitPathIdle();
+ });
+}
+
+inline bool
+EthernetMac::waitForTransmitQueueFlush()
+{
+ return modm::this_fiber::poll_for(std::chrono::microseconds{MacStopTimeoutUs}, [] {
+ return (ETH->MTLTQOMR & ETH_MTLTQOMR_FTQ) == 0 and
+ ETH->MTLTQDR == 0 and (ETH->MACDR & MacDebugStateMask) == 0;
+ });
+}
+
+inline EthernetMac::LinkUpdateError
+EthernetMac::stopTimeoutError()
+{
+ modm::atomic::Lock lock;
+ if (latchPendingFatalBusError())
+ return LinkUpdateError::FatalBusError;
+ if (driverState() == DriverState::Faulted)
+ return faultError();
+ setDriverFailure(DriverFailure::MacStopTimeout);
+ return LinkUpdateError::MacStopTimeout;
+}
+
+inline bool
+EthernetMac::latchPendingFatalBusError()
+{
+ // Preserve a live fatal DMA fault in software so all later operations fail
+ // consistently until initialize() resets the driver
+ const uint32_t status = ETH->DMACSR;
+ if ((status & ETH_DMACSR_FBE) == 0)
+ return false;
+ const uint32_t capturedStatus =
+ captureLiveDmaStatus(driverState() == DriverState::Running);
+ setDriverFailure(DriverFailure::FatalBusError);
+ ETH->DMACSR = capturedStatus & detail::EthH7DmaHandledStatus;
+ return true;
+}
+
+inline EthernetMac::LinkUpdateError
+EthernetMac::faultError()
+{
+ return state().driverFailure == DriverFailure::MacStopTimeout ?
+ LinkUpdateError::MacStopTimeout : LinkUpdateError::FatalBusError;
+}
+
+inline void
+EthernetMac::setDriverFailure(DriverFailure failure)
+{
+ state().driverFailure = failure;
+ setDriverState(DriverState::Faulted);
+ ETH->DMACIER = 0;
+}
+
+inline EthernetMac::LinkStatus
+EthernetMac::currentLinkStatus()
+{
+ LinkStatus status{LinkState(state().linkState), std::nullopt};
+ if (status.state == LinkState::Up)
+ status.mode = LinkMode{state().speed, state().duplexMode};
+ return status;
+}
+
+inline EthernetMac::LinkUpdateResult
+EthernetMac::notifyUpdatedLinkStatus(LinkStatus observed)
+{
+ if (driverState() == DriverState::Uninitialized)
+ return {currentLinkStatus(), LinkUpdateError::NotInitialized};
+ if (driverState() == DriverState::Faulted) {
+ modm::atomic::Lock lock;
+ if (latchPendingFatalBusError())
+ return {currentLinkStatus(), LinkUpdateError::FatalBusError};
+ return {currentLinkStatus(), faultError()};
+ }
+
+ {
+ modm::atomic::Lock lock;
+ if (latchPendingFatalBusError())
+ return {currentLinkStatus(), LinkUpdateError::FatalBusError};
+ if (driverState() == DriverState::Faulted)
+ return {currentLinkStatus(), faultError()};
+ }
+
+ if (observed.state != LinkState::Up) {
+ if (driverState() == DriverState::Running) {
+ // Work around STM32H7 errata ES0392: RMII keeps its shared 50 MHz
+ // REF_CLK, while MII Tx/Rx clocks may disappear on link loss
+ const bool clocksContinue =
+ state().mediaInterface == MediaInterface::RMII;
+ const auto error = observed.state == LinkState::Down and not clocksContinue ?
+ stopWithoutMediaClock() : stop();
+ if (error != LinkUpdateError::None)
+ return {currentLinkStatus(), error};
+ }
+ modm::atomic::Lock lock;
+ if (latchPendingFatalBusError())
+ return {currentLinkStatus(), LinkUpdateError::FatalBusError};
+ if (driverState() == DriverState::Faulted)
+ return {currentLinkStatus(), faultError()};
+ state().linkState = uint8_t(observed.state);
+ return {observed, LinkUpdateError::None};
+ }
+
+ modm_assert(observed.mode.has_value(), "eth.link.mode",
+ "An up Ethernet link requires a speed and duplex mode");
+ const LinkMode mode = *observed.mode;
+ const bool modeChanged = mode.speed != state().speed or mode.duplex != state().duplexMode;
+ if (driverState() == DriverState::Stopping) {
+ const auto error = resumeWithRestoredMediaClock(mode);
+ if (error != LinkUpdateError::None)
+ return {currentLinkStatus(), error};
+ }
+ else {
+ if (driverState() == DriverState::Running and modeChanged) {
+ const auto error = stop();
+ if (error != LinkUpdateError::None)
+ return {currentLinkStatus(), error};
+ }
+ if (driverState() == DriverState::Faulted)
+ return {currentLinkStatus(), faultError()};
+ if (driverState() == DriverState::Ready or modeChanged) {
+ state().speed = mode.speed;
+ state().duplexMode = mode.duplex;
+ const auto error = start();
+ if (error != LinkUpdateError::None)
+ return {currentLinkStatus(), error};
+ }
+ }
+ {
+ modm::atomic::Lock lock;
+ if (latchPendingFatalBusError())
+ return {currentLinkStatus(), LinkUpdateError::FatalBusError};
+ if (not detail::canCommitLinkUpState(driverState()))
+ return {currentLinkStatus(), faultError()};
+ state().linkState = uint8_t(LinkState::Up);
+ }
+ return {currentLinkStatus(), LinkUpdateError::None};
+}
+
+inline EthernetMac::MdioError
+EthernetMac::readPhyRegister(uint8_t phyAddress, uint8_t reg, uint16_t &value)
+{
+ if (reg > 31) return MdioError::InvalidRegister;
+ if (phyAddress > MdioPhyAddressMaximum) return MdioError::InvalidPhyAddress;
+ if ((ETH->MACMDIOAR & ETH_MACMDIOAR_MB) != 0) return MdioError::Busy;
+ uint32_t command = ETH->MACMDIOAR & ETH_MACMDIOAR_CR;
+ command |= (uint32_t(phyAddress) << ETH_MACMDIOAR_PA_Pos) & ETH_MACMDIOAR_PA;
+ command |= (uint32_t(reg) << ETH_MACMDIOAR_RDA_Pos) & ETH_MACMDIOAR_RDA;
+ ETH->MACMDIOAR = command | ETH_MACMDIOAR_MOC_RD | ETH_MACMDIOAR_MB;
+ if (not modm::this_fiber::poll_for(std::chrono::microseconds{MdioTimeoutUs},
+ []{ return (ETH->MACMDIOAR & ETH_MACMDIOAR_MB) == 0; }))
+ return MdioError::Timeout;
+ value = uint16_t(ETH->MACMDIODR & ETH_MACMDIODR_MD);
+ return MdioError::None;
+}
+
+inline EthernetMac::MdioError
+EthernetMac::writePhyRegister(uint8_t phyAddress, uint8_t reg, uint16_t value)
+{
+ if (reg > 31) return MdioError::InvalidRegister;
+ if (phyAddress > MdioPhyAddressMaximum) return MdioError::InvalidPhyAddress;
+ if ((ETH->MACMDIOAR & ETH_MACMDIOAR_MB) != 0) return MdioError::Busy;
+ uint32_t command = ETH->MACMDIOAR & ETH_MACMDIOAR_CR;
+ command |= (uint32_t(phyAddress) << ETH_MACMDIOAR_PA_Pos) & ETH_MACMDIOAR_PA;
+ command |= (uint32_t(reg) << ETH_MACMDIOAR_RDA_Pos) & ETH_MACMDIOAR_RDA;
+ ETH->MACMDIODR = value;
+ ETH->MACMDIOAR = command | ETH_MACMDIOAR_MOC_WR | ETH_MACMDIOAR_MB;
+ return modm::this_fiber::poll_for(std::chrono::microseconds{MdioTimeoutUs},
+ []{ return (ETH->MACMDIOAR & ETH_MACMDIOAR_MB) == 0; }) ?
+ MdioError::None : MdioError::Timeout;
+}
+
+inline EthernetMac::TransmitBufferLease::TransmitBufferLease(TransmitBufferLease &&other) noexcept :
+ index_(other.index_), length_(other.length_), error_(other.error_), active_(other.active_)
+{
+ other.active_ = false;
+ other.error_ = TransmitError::Busy;
+}
+
+inline EthernetMac::TransmitBufferLease &
+EthernetMac::TransmitBufferLease::operator=(TransmitBufferLease &&other) noexcept
+{
+ if (this != &other) {
+ if (active_) EthernetMac::cancelTransmitLease(index_);
+ index_ = other.index_;
+ length_ = other.length_;
+ error_ = other.error_;
+ active_ = other.active_;
+ other.active_ = false;
+ other.error_ = TransmitError::Busy;
+ }
+ return *this;
+}
+
+inline EthernetMac::TransmitBufferLease::~TransmitBufferLease() noexcept
+{
+ if (active_) EthernetMac::cancelTransmitLease(index_);
+}
+
+inline std::span
+EthernetMac::TransmitBufferLease::buffer() noexcept
+{
+ modm_assert(active_, "eth.tx.lease", "Inactive transmit lease accessed");
+ return {EthernetMac::dmaStorage().txBuffers[index_], length_};
+}
+
+inline EthernetMac::TransmitResult
+EthernetMac::TransmitBufferLease::commit() noexcept
+{
+ modm_assert(active_, "eth.tx.lease", "Inactive transmit lease committed");
+ const auto result = EthernetMac::commitTransmitLease(index_, length_);
+ active_ = false;
+ error_ = result.error;
+ return result;
+}
+
+inline void
+EthernetMac::TransmitBufferLease::cancel() noexcept
+{
+ if (not active_) return;
+ EthernetMac::cancelTransmitLease(index_);
+ active_ = false;
+}
+
+inline EthernetMac::ReceiveBufferLease::ReceiveBufferLease(ReceiveBufferLease &&other) noexcept :
+ index_(other.index_), length_(other.length_), error_(other.error_),
+ checksumStatus_(other.checksumStatus_), active_(other.active_)
+{
+ other.active_ = false;
+ other.error_ = ReceiveError::NoFrameAvailable;
+}
+
+inline EthernetMac::ReceiveBufferLease &
+EthernetMac::ReceiveBufferLease::operator=(ReceiveBufferLease &&other) noexcept
+{
+ if (this != &other) {
+ if (active_) EthernetMac::releaseReceiveLease(index_);
+ index_ = other.index_;
+ length_ = other.length_;
+ error_ = other.error_;
+ checksumStatus_ = other.checksumStatus_;
+ active_ = other.active_;
+ other.active_ = false;
+ other.error_ = ReceiveError::NoFrameAvailable;
+ }
+ return *this;
+}
+
+inline EthernetMac::ReceiveBufferLease::~ReceiveBufferLease() noexcept
+{
+ if (active_) EthernetMac::releaseReceiveLease(index_);
+}
+
+inline std::span
+EthernetMac::ReceiveBufferLease::buffer() const noexcept
+{
+ modm_assert(active_, "eth.rx.lease", "Inactive receive lease accessed");
+ return {EthernetMac::dmaStorage().rxBuffers[index_], length_};
+}
+
+inline EthernetMac::ReceiveChecksumStatus
+EthernetMac::ReceiveBufferLease::checksumStatus() const noexcept
+{
+ modm_assert(active_, "eth.rx.lease", "Inactive receive lease checksum accessed");
+ return checksumStatus_;
+}
+
+inline void
+EthernetMac::ReceiveBufferLease::release() noexcept
+{
+ if (not active_) return;
+ EthernetMac::releaseReceiveLease(index_);
+ active_ = false;
+}
+
+inline EthernetMac::TransmitBufferLease
+EthernetMac::acquireTransmitBuffer(std::size_t length)
+{
+ if (driverState() == DriverState::Uninitialized)
+ return TransmitBufferLease(TransmitError::NotInitialized);
+ if (driverState() == DriverState::Faulted)
+ return TransmitBufferLease(TransmitError::Faulted);
+ if (length == 0 or length > MaxFrameSize)
+ return TransmitBufferLease(TransmitError::InvalidLength);
+ if (driverState() != DriverState::Running or LinkState(state().linkState) != LinkState::Up)
+ return TransmitBufferLease(TransmitError::LinkDown);
+ if (state().txLeaseOutstanding)
+ return TransmitBufferLease(TransmitError::Busy);
+
+ TxDmaDescriptor &descriptor = dmaStorage().txDescriptors[state().txIndex];
+ invalidateDCache(&descriptor, sizeof(descriptor));
+ if (descriptor.isOwnedByDma()) {
+ return TransmitBufferLease(TransmitError::Busy);
+ }
+ const uint32_t writebackErrors = descriptor.writebackErrors();
+ if (writebackErrors != 0) {
+ ++state().txDescriptorErrors;
+ state().hardwareTxDescriptorErrorStatus |= writebackErrors;
+ }
+ descriptor.clear();
+ {
+ modm::atomic::Lock lock;
+ if (latchPendingFatalBusError())
+ return TransmitBufferLease(TransmitError::Faulted);
+ if (driverState() == DriverState::Faulted)
+ return TransmitBufferLease(TransmitError::Faulted);
+ if (driverState() != DriverState::Running or LinkState(state().linkState) != LinkState::Up)
+ return TransmitBufferLease(TransmitError::LinkDown);
+ state().txLeaseOutstanding = true;
+ }
+ return TransmitBufferLease(state().txIndex, length);
+}
+
+inline EthernetMac::TransmitResult
+EthernetMac::commitTransmitLease(std::size_t index, std::size_t length)
+{
+ // Validate the lease and current link state
+ modm_assert(state().txLeaseOutstanding and index == state().txIndex,
+ "eth.tx.lease", "Transmit lease does not own the producer descriptor");
+ TransmitError stateError = TransmitError::None;
+ if (driverState() == DriverState::Uninitialized) stateError = TransmitError::NotInitialized;
+ else if (driverState() == DriverState::Faulted) stateError = TransmitError::Faulted;
+ else if (driverState() != DriverState::Running or LinkState(state().linkState) != LinkState::Up)
+ stateError = TransmitError::LinkDown;
+ if (stateError != TransmitError::None) {
+ cancelTransmitLease(index);
+ return {stateError};
+ }
+ // Reject fragmented IPv4 frames that hardware cannot checksum
+ if (state().checksumOffloadEnabled and hasUnsupportedHardwareChecksumFragment(
+ {dmaStorage().txBuffers[index], length})) {
+ cancelTransmitLease(index);
+ return {TransmitError::UnsupportedFragmentation};
+ }
+
+ // Prepare the frame and descriptor in DMA-visible memory
+ TxDmaDescriptor &descriptor = dmaStorage().txDescriptors[index];
+ invalidateDCache(&descriptor, sizeof(descriptor));
+ if (descriptor.isOwnedByDma()) {
+ cancelTransmitLease(index);
+ return {TransmitError::Busy};
+ }
+ cleanDCache(dmaStorage().txBuffers[index], length);
+ descriptor.prepare(dmaAddress(dmaStorage().txBuffers[index]), length,
+ state().checksumOffloadEnabled);
+ cleanDCache(&descriptor, sizeof(descriptor));
+ // Recheck the driver state before releasing the descriptor to DMA
+ {
+ modm::atomic::Lock lock;
+ if (latchPendingFatalBusError()) {
+ state().txLeaseOutstanding = false;
+ return {TransmitError::Faulted};
+ }
+ if (driverState() == DriverState::Faulted) {
+ state().txLeaseOutstanding = false;
+ return {TransmitError::Faulted};
+ }
+ if (driverState() != DriverState::Running or LinkState(state().linkState) != LinkState::Up) {
+ state().txLeaseOutstanding = false;
+ return {TransmitError::LinkDown};
+ }
+ descriptor.releaseToDma();
+ cleanDCache(&descriptor, sizeof(descriptor));
+ __DSB();
+ state().txLeaseOutstanding = false;
+ state().txIndex = (state().txIndex + 1) % TxDescriptorCount;
+ ETH->DMACTDTPR = dmaAddress(&dmaStorage().txDescriptors[state().txIndex]);
+ }
+ return {};
+}
+
+inline bool
+EthernetMac::hasUnsupportedHardwareChecksumFragment(std::span frame)
+{
+ if (frame.size() < 22)
+ return false;
+ const bool ipv4 = frame[12] == 0x08 and frame[13] == 0x00 and
+ (frame[14] >> 4) == 4;
+ const uint16_t fragment = (uint16_t(frame[20]) << 8) | frame[21];
+ return ipv4 and (fragment & 0x3fff) != 0;
+}
+
+inline void
+EthernetMac::cancelTransmitLease(std::size_t index)
+{
+ modm_assert(state().txLeaseOutstanding and index == state().txIndex,
+ "eth.tx.lease", "Transmit lease does not own the producer descriptor");
+ state().txLeaseOutstanding = false;
+}
+
+inline EthernetMac::TransmitResult
+EthernetMac::transmit(std::span frame)
+{
+ auto lease = acquireTransmitBuffer(frame.size());
+ if (not lease) return {lease.error()};
+ std::memcpy(lease.buffer().data(), frame.data(), frame.size());
+ return lease.commit();
+}
+
+inline EthernetMac::ReceiveChecksumStatus
+EthernetMac::classifyReceiveChecksum(RxDmaDescriptor const &descriptor)
+{
+ if (not state().checksumOffloadEnabled or
+ not descriptor.status().any(detail::EthH7RxDescriptor3::ReceiveStatus1Valid))
+ return ReceiveChecksumStatus::NotChecked;
+ const uint32_t status = descriptor.word1;
+ if ((status & (Rdes1IpHeaderError | Rdes1IpPayloadError)) != 0)
+ return ReceiveChecksumStatus::Invalid;
+ const uint32_t payloadType = status & Rdes1PayloadTypeMask;
+ const bool supportedPayload = payloadType == 1 or payloadType == 2 or payloadType == 3;
+ if ((status & Rdes1IpChecksumBypassed) != 0 or
+ (status & (Rdes1Ipv4Header | Rdes1Ipv6Header)) == 0 or not supportedPayload)
+ return ReceiveChecksumStatus::NotChecked;
+ return ReceiveChecksumStatus::Valid;
+}
+
+inline bool
+EthernetMac::isValidReceiveDescriptor(RxDmaDescriptor const &descriptor)
+{
+ const auto status = descriptor.status();
+ // ES0392 Rev 15, section 2.26.20: RMII 10 Mbit/s can falsely
+ // report the exact dribble+CRC signature. No other error is accepted.
+ const bool toleratedRmii10MError = detail::needsRmii10MWorkaround(
+ state().mediaInterface, state().speed) and
+ detail::isExactRmii10MDribbleCrcError(status.value);
+ return status.none(detail::EthH7RxDescriptor3::ContextDescriptor) and
+ (status.none(detail::EthH7RxDescriptor3::ErrorSummary) or toleratedRmii10MError) and
+ status.all(detail::EthH7RxDescriptor3::FirstDescriptor |
+ detail::EthH7RxDescriptor3::LastDescriptor);
+}
+
+inline EthernetMac::ReceiveBufferLease
+EthernetMac::tryAcquireReceiveBuffer()
+{
+ // Validate the driver and lease state
+ if (driverState() == DriverState::Uninitialized)
+ return ReceiveBufferLease(ReceiveError::NotInitialized);
+ if (driverState() == DriverState::Faulted)
+ return ReceiveBufferLease(ReceiveError::Faulted);
+ if (state().rxLeaseOutstanding) {
+ return ReceiveBufferLease(ReceiveError::NoFrameAvailable);
+ }
+
+ // Scan completed descriptors, recycling invalid frames as they are found
+ for (std::size_t count = 0; count < RxDescriptorCount; ++count) {
+ if (driverState() == DriverState::Faulted)
+ return ReceiveBufferLease(ReceiveError::Faulted);
+ RxDmaDescriptor &descriptor = dmaStorage().rxDescriptors[state().rxIndex];
+ invalidateDCache(&descriptor, sizeof(descriptor));
+ const auto status = descriptor.status();
+ if (status.any(detail::EthH7RxDescriptor3::OwnedByDma))
+ return ReceiveBufferLease(ReceiveError::NoFrameAvailable);
+ if (not isValidReceiveDescriptor(descriptor)) {
+ if (not releaseBadRxDescriptor())
+ return ReceiveBufferLease(driverState() == DriverState::Faulted ?
+ ReceiveError::Faulted : ReceiveError::NoFrameAvailable);
+ continue;
+ }
+
+ // Validate the frame metadata and make its buffer visible to the CPU
+ const std::size_t length = descriptor.packetLength();
+ if (length == 0 or length > MaxFrameSize) {
+ if (not releaseBadRxDescriptor())
+ return ReceiveBufferLease(driverState() == DriverState::Faulted ?
+ ReceiveError::Faulted : ReceiveError::NoFrameAvailable);
+ continue;
+ }
+ const auto checksum = classifyReceiveChecksum(descriptor);
+ invalidateDCache(dmaStorage().rxBuffers[state().rxIndex], length);
+
+ // Reserve the receive lease only while the MAC remains operational
+ {
+ modm::atomic::Lock lock;
+ if (latchPendingFatalBusError())
+ return ReceiveBufferLease(ReceiveError::Faulted);
+ if (driverState() == DriverState::Faulted)
+ return ReceiveBufferLease(ReceiveError::Faulted);
+ if (driverState() != DriverState::Running)
+ return ReceiveBufferLease(ReceiveError::NoFrameAvailable);
+ state().rxLeaseOutstanding = true;
+ }
+ return ReceiveBufferLease(state().rxIndex, length, checksum);
+ }
+ return ReceiveBufferLease(ReceiveError::NoFrameAvailable);
+}
+
+inline void
+EthernetMac::releaseReceiveLease(std::size_t index)
+{
+ modm_assert(state().rxLeaseOutstanding and index == state().rxIndex,
+ "eth.rx.lease", "Receive lease does not own the consumer descriptor");
+ state().rxLeaseOutstanding = false;
+ if (releaseRxDescriptor(index))
+ state().rxIndex = (state().rxIndex + 1) % RxDescriptorCount;
+}
+
+inline bool
+EthernetMac::releaseBadRxDescriptor()
+{
+ if (not releaseRxDescriptor(state().rxIndex))
+ return false;
+ state().rxIndex = (state().rxIndex + 1) % RxDescriptorCount;
+ return true;
+}
+
+inline bool
+EthernetMac::releaseRxDescriptor(std::size_t index, bool resumeDma)
+{
+ invalidateDCache(dmaStorage().rxBuffers[index], DmaBufferSize);
+ auto &descriptor = dmaStorage().rxDescriptors[index];
+ descriptor.prepare(dmaAddress(dmaStorage().rxBuffers[index]));
+ cleanDCache(&descriptor, sizeof(descriptor));
+ if (not resumeDma) {
+ descriptor.releaseToDma();
+ cleanDCache(&descriptor, sizeof(descriptor));
+ return true;
+ }
+ {
+ modm::atomic::Lock lock;
+ if (latchPendingFatalBusError() or driverState() != DriverState::Running)
+ return false;
+ descriptor.releaseToDma();
+ cleanDCache(&descriptor, sizeof(descriptor));
+ __DMB();
+ state().rxTailIndex = index;
+ ETH->DMACRDTPR = dmaAddress(&dmaStorage().rxDescriptors[index]);
+ }
+ resumeReceiveDma();
+ return true;
+}
+
+inline void
+EthernetMac::resumeReceiveDma()
+{
+ if (driverState() != DriverState::Running)
+ return;
+ modm::atomic::Lock lock;
+ if (latchPendingFatalBusError() or driverState() != DriverState::Running)
+ return;
+ ETH->DMACSR = ETH_DMACSR_RBU | ETH_DMACSR_RPS;
+ ETH->DMACRCR |= ETH_DMACRCR_SR;
+ __DMB();
+ ETH->DMACRDTPR = dmaAddress(&dmaStorage().rxDescriptors[state().rxTailIndex]);
+}
+
+inline void
+EthernetMac::cleanDCache(void const *address, std::size_t length)
+{
+#if defined(__DCACHE_PRESENT) && (__DCACHE_PRESENT == 1U)
+ if (address == nullptr or length == 0 or (SCB->CCR & SCB_CCR_DC_Msk) == 0)
+ return;
+
+ const auto start = cacheLineStart(reinterpret_cast(address));
+ const auto end = cacheLineEnd(reinterpret_cast(address) + length);
+ SCB_CleanDCache_by_Addr(reinterpret_cast(start), static_cast(end - start));
+#else
+ (void) address;
+ (void) length;
+#endif
+}
+
+inline void
+EthernetMac::invalidateDCache(void const *address, std::size_t length)
+{
+#if defined(__DCACHE_PRESENT) && (__DCACHE_PRESENT == 1U)
+ if (address == nullptr or length == 0 or (SCB->CCR & SCB_CCR_DC_Msk) == 0)
+ return;
+
+ const auto start = cacheLineStart(reinterpret_cast(address));
+ const auto end = cacheLineEnd(reinterpret_cast(address) + length);
+ SCB_InvalidateDCache_by_Addr(reinterpret_cast(start), static_cast(end - start));
+#else
+ (void) address;
+ (void) length;
+#endif
+}
+
+}
diff --git a/src/modm/platform/eth/stm32h7/module.lb b/src/modm/platform/eth/stm32h7/module.lb
new file mode 100644
index 0000000000..5de88b0f75
--- /dev/null
+++ b/src/modm/platform/eth/stm32h7/module.lb
@@ -0,0 +1,210 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2020, Mike Wolfram
+# Copyright (c) 2021, Niklas Hauser
+# Copyright (c) 2026, Kaelin Laundry
+#
+# This file is part of the modm project.
+#
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+# -----------------------------------------------------------------------------
+
+ETH_H7_CACHE_LINE_SIZE = 32
+# One untagged Ethernet header and the standard 1500-byte payload MTU; FCS is stripped.
+ETH_H7_MAX_FRAME_SIZE = 14 + 1500
+# A normal descriptor is four words. Isolate each descriptor on a Cortex-M7 cache line.
+ETH_H7_DESCRIPTOR_HARDWARE_SIZE = 16
+ETH_H7_DESCRIPTOR_COUNT_MIN = 4
+# DMACTXRLR/DMACRXRLR contain ten-bit ring lengths and encode count minus one.
+ETH_H7_DESCRIPTOR_COUNT_MAX = 1 << 10
+ETH_H7_RX_DESCRIPTOR_COUNT_DEFAULT = 8
+ETH_H7_TX_DESCRIPTOR_COUNT_DEFAULT = 4
+
+
+def align_up(value, alignment):
+ return (value + alignment - 1) // alignment * alignment
+
+
+ETH_H7_DESCRIPTOR_STORAGE_SIZE = align_up(
+ ETH_H7_DESCRIPTOR_HARDWARE_SIZE, ETH_H7_CACHE_LINE_SIZE)
+ETH_H7_FRAME_BUFFER_SIZE = align_up(
+ ETH_H7_MAX_FRAME_SIZE, ETH_H7_CACHE_LINE_SIZE)
+
+
+def memory_size(memory):
+ size = memory["size"]
+ return int(size, 0) if isinstance(size, str) else int(size)
+
+
+def eth_h7_dma_storage_size(rx_descriptors, tx_descriptors):
+ return (rx_descriptors + tx_descriptors) * (
+ ETH_H7_DESCRIPTOR_STORAGE_SIZE + ETH_H7_FRAME_BUFFER_SIZE)
+
+
+def eth_h7_dma_memories(device):
+ minimum_storage = eth_h7_dma_storage_size(
+ ETH_H7_DESCRIPTOR_COUNT_MIN, ETH_H7_DESCRIPTOR_COUNT_MIN)
+ # TODO: Use modm-devices DMA-domain accessibility metadata when available.
+ memories = [
+ memory
+ for memory in listify(device.get_driver("core")["memory"])
+ if "rw" in memory.get("access", "")
+ and memory_size(memory) >= minimum_storage
+ ]
+ return sorted(memories, key=memory_size, reverse=True)
+
+
+def init(module):
+ module.name = ":platform:eth"
+ module.description = """\
+# Ethernet
+
+STM32H7 Ethernet v3.0 MAC/DMA backend.
+
+This driver implements Ethernet MAC initialization, link status notifications,
+TX/RX DMA, and MDIO communication with an external PHY. It does not implement a
+specific PHY or a network stack. Use a separate PHY driver and, for higher level
+protocols, a stack such as lwIP or FreeRTOS+TCP.
+
+Packets can be sent and received through copying or zero copy APIs. The zero
+copy API exposes DMA buffers for the caller to populate directly.
+
+The normal copying API is:
+
+```cpp
+const auto result = EthernetMac::transmit(frame);
+modm_assert(result, "eth.tx", "Frame was not accepted");
+```
+
+For in-place construction, the lease owns the reservation until it is
+committed, cancelled, or destroyed:
+
+```cpp
+auto tx = EthernetMac::acquireTransmitBuffer(length);
+modm_assert(tx, "eth.tx.acquire", "Transmit ring is busy");
+fillFrame(tx.buffer());
+modm_assert(tx.commit(), "eth.tx.commit", "Frame was not accepted");
+```
+
+`examples/generic/ethernet` exercises both paths.
+
+Descriptors and frame buffers must be placed in DMA accessible SRAM.
+Auto negotiation is assumed; forced link modes are not implemented.
+
+The application calls `notifyUpdatedLinkStatus()` with each observed PHY link
+update. Fatal bus errors and other failures reject traffic until the next
+successful `initialize()` call. Reinitialization and link transitions that stop
+the MAC require all frame leases to be released.
+
+Only untagged Ethernet is supported. VLAN filtering, multicast hashing,
+IEEE 802.3x flow control, and Energy Efficient Ethernet are not implemented.
+Each frame occupies one descriptor and is limited to the standard 1500-byte MTU.
+Jumbo frames are not supported.
+"""
+
+def prepare(module, options):
+ device = options[":target"]
+
+ if not device.has_driver("eth:stm32*"):
+ return False
+
+ family = device.identifier["family"]
+ driver_type = device.get_driver("eth")["type"]
+ if family not in ["h5", "h7"] or driver_type != "stm32-v3.0":
+ return False
+
+ module.add_option(
+ NumericOption(
+ name="dma.rx_descriptors",
+ description=(
+ "Number of STM32H7 Ethernet receive descriptors and frame buffers. "
+ "The driver requires at least four; the 10-bit "
+ "ring length field supports at most 1024."),
+ minimum=ETH_H7_DESCRIPTOR_COUNT_MIN,
+ maximum=ETH_H7_DESCRIPTOR_COUNT_MAX,
+ default=ETH_H7_RX_DESCRIPTOR_COUNT_DEFAULT))
+ module.add_option(
+ NumericOption(
+ name="dma.tx_descriptors",
+ description=(
+ "Number of STM32H7 Ethernet transmit descriptors and frame buffers. "
+ "The driver requires at least four; the 10-bit "
+ "ring length field supports at most 1024."),
+ minimum=ETH_H7_DESCRIPTOR_COUNT_MIN,
+ maximum=ETH_H7_DESCRIPTOR_COUNT_MAX,
+ default=ETH_H7_TX_DESCRIPTOR_COUNT_DEFAULT))
+ memories = eth_h7_dma_memories(device)
+ if not memories:
+ raise ValidateException(
+ "STM32H7 Ethernet needs writable memory large enough for its DMA storage.")
+ sections = [".noinit_" + memory["name"] for memory in memories]
+ default_storage = eth_h7_dma_storage_size(
+ ETH_H7_RX_DESCRIPTOR_COUNT_DEFAULT, ETH_H7_TX_DESCRIPTOR_COUNT_DEFAULT)
+ default_section = next((
+ ".noinit_" + memory["name"] for memory in memories
+ if memory_size(memory) >= default_storage), None)
+ if default_section is None:
+ raise ValidateException(
+ "STM32H7 Ethernet needs internal SRAM large enough for its default DMA rings.")
+ module.add_option(
+ EnumerationOption(
+ name="dma.section",
+ description=(
+ "No-init linker section for STM32H7 Ethernet DMA descriptors and "
+ "frame buffers. Select memory accessible to the Ethernet DMA."),
+ enumeration=sections,
+ default=default_section))
+
+ module.depends(":architecture:atomic",
+ ":architecture:clock",
+ ":architecture:ethernet",
+ ":architecture:interrupt",
+ ":architecture:register",
+ ":architecture:assert",
+ ":cmsis:device",
+ ":platform:gpio",
+ ":platform:id",
+ ":platform:rcc",
+ ":processing:fiber",
+ ":math:utils")
+
+ return True
+
+
+def validate(env):
+ device = env[":target"]
+ family = device.identifier["family"]
+ driver_type = device.get_driver("eth")["type"] if device.has_driver("eth:stm32*") else None
+ if family not in ["h5", "h7"] or driver_type != "stm32-v3.0":
+ return
+
+ required = eth_h7_dma_storage_size(env["dma.rx_descriptors"], env["dma.tx_descriptors"])
+ section = env["dma.section"]
+ memory_name = section.removeprefix(".noinit_")
+ memories = eth_h7_dma_memories(device)
+ memory = next((memory for memory in memories
+ if memory["name"] == memory_name), None)
+ if memory is None or memory_size(memory) < required:
+ raise ValidateException(
+ "STM32H7 Ethernet DMA storage needs {} bytes of writable memory, "
+ "but {} cannot provide it."
+ .format(required, section))
+
+def build(env):
+ env.substitutions = {"target": env[":target"].identifier}
+ env.outbasepath = "modm/src/modm/platform/eth"
+
+ env.substitutions.update({
+ "eth_dma_section": env["dma.section"],
+ "eth_dma_buffer_size": ETH_H7_FRAME_BUFFER_SIZE,
+ "eth_descriptor_alignment": ETH_H7_CACHE_LINE_SIZE,
+ "eth_max_frame_size": ETH_H7_MAX_FRAME_SIZE,
+ "eth_rx_descriptor_count": env["dma.rx_descriptors"],
+ "eth_tx_descriptor_count": env["dma.tx_descriptors"],
+ })
+ env.template("eth.hpp.in")
+ env.template("eth_impl.hpp.in")
+ env.template("eth.cpp.in")
diff --git a/src/modm/processing/fiber/module.lb b/src/modm/processing/fiber/module.lb
index db2c5a1902..9d5dc0de91 100644
--- a/src/modm/processing/fiber/module.lb
+++ b/src/modm/processing/fiber/module.lb
@@ -18,7 +18,8 @@ def init(module):
def prepare(module, options):
module.depends(":architecture:clock", ":architecture:atomic",
- ":architecture:assert", ":architecture:fiber", ":stdc++")
+ ":architecture:assert", ":architecture:fiber",
+ ":architecture:interrupt", ":stdc++")
core = options[":target"].get_driver("core")["type"]
if core.startswith("cortex-m"): module.depends(":cmsis:device")
diff --git a/src/modm/processing/fiber/scheduler.hpp.in b/src/modm/processing/fiber/scheduler.hpp.in
index e208722c99..86851b04ad 100644
--- a/src/modm/processing/fiber/scheduler.hpp.in
+++ b/src/modm/processing/fiber/scheduler.hpp.in
@@ -67,16 +67,6 @@ protected:
return reinterpret_cast(current);
}
- static bool inline
- isInsideInterrupt()
- {
-%% if core.startswith("cortex-m")
- return __get_IPSR();
-%% else
- return false;
-%% endif
- }
-
void inline
runNext(Task* task)
{
diff --git a/src/modm/processing/fiber/task_impl.hpp b/src/modm/processing/fiber/task_impl.hpp
index 1d2c6155e3..212ab0f117 100644
--- a/src/modm/processing/fiber/task_impl.hpp
+++ b/src/modm/processing/fiber/task_impl.hpp
@@ -12,6 +12,7 @@
#pragma once
#include "scheduler.hpp"
+#include
#include
/// @cond
@@ -82,7 +83,7 @@ bool inline
Task::joinable() const
{
if (not isRunning()) return false;
- if (Scheduler::isInsideInterrupt()) return false;
+ if (modm::isInterruptContext()) return false;
return get_id() != Scheduler::instance().get_id();
}
diff --git a/test/Makefile b/test/Makefile
index 6fd062453a..c3b5689174 100644
--- a/test/Makefile
+++ b/test/Makefile
@@ -2,6 +2,7 @@
#
# Copyright (c) 2017, Fabian Greif
# Copyright (c) 2018, Niklas Hauser
+# Copyright (c) 2026, Kaelin Laundry
#
# This file is part of the modm project.
#
@@ -37,6 +38,11 @@ run-hosted-darwin-arm64:
run-hosted-windows:
$(call compile-test,hosted,run,-D":target=hosted-windows")
+run-hosted-lwip-linux:
+ $(call compile-test,hosted-lwip,run,-D":target=hosted-linux")
+run-hosted-lwip-windows:
+ $(call compile-test,hosted-lwip,run,-D":target=hosted-windows")
+
compile-nucleo-f091rc_A:
$(call compile-test,nucleo-f091rc_A,size)
@@ -115,6 +121,10 @@ compile-nucleo-h723zg:
run-nucleo-h723zg:
$(call run-test,nucleo-h723zg,size)
+compile-nucleo-h753zi-eth:
+ $(call compile-test,nucleo-h753zi-eth,size)
+run-nucleo-h753zi-eth:
+ $(call run-test,nucleo-h753zi-eth,size)
compile-nucleo-l432kc:
$(call compile-test,nucleo-l432kc,size)
diff --git a/test/README.md b/test/README.md
index 07fbfb9a3e..d4871d1031 100644
--- a/test/README.md
+++ b/test/README.md
@@ -65,10 +65,13 @@ lbuild as well. They are all submodules of `modm:test` and are all platform
independent. We've written a small Makefile for the most commonly used commands.
```sh
-cd tests
+cd test
# generates, compiles and executes the unit tests for hosted targets
make run-hosted-linux
make run-hosted-darwin
+# executes only the lwIP hosted tests on Linux or Windows
+make run-hosted-lwip-linux
+make run-hosted-lwip-windows
# generates and compiles the unit tests for embedded targets
make compile-nucleo-f411re
make compile-nucleo-f103rb_A
@@ -81,8 +84,14 @@ make run-nucleo-f103rb_B
make run-arduino-nano_A # to _H
```
+See [the STM32H7 Ethernet test documentation](modm/platform/eth/stm32h7/README.md)
+for compile targets, hardware setup, and coverage.
+
The embedded test targets all use the `modm::Board` interface to initialize the
-targets and output unit tests results via the default serial connection.
+targets and output unit test results via the default serial connection. The
+`run-*` Make targets program the board but do not capture or interpret that
+serial output; the serial test summary determines whether a hardware suite
+passed.
The unit test library we use is located in `modm/src/unittest` which corresponds
to the `modm:unittest` modules. See the existing unit tests for examples on how
diff --git a/test/config/hosted-lwip.xml b/test/config/hosted-lwip.xml
new file mode 100644
index 0000000000..af0d24bc98
--- /dev/null
+++ b/test/config/hosted-lwip.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+ modm:platform:core
+ modm-test:test:lwip
+
+
diff --git a/test/config/nucleo-h753zi-eth.xml b/test/config/nucleo-h753zi-eth.xml
new file mode 100644
index 0000000000..6ab817363e
--- /dev/null
+++ b/test/config/nucleo-h753zi-eth.xml
@@ -0,0 +1,16 @@
+
+
+ modm:nucleo-h753zi
+
+
+
+
+
+
+
+
+
+ modm:platform:heap
+ modm-test:test:platform:eth.stm32h7
+
+
diff --git a/test/modm/lwip/lwip_ethernet_test.cpp b/test/modm/lwip/lwip_ethernet_test.cpp
new file mode 100644
index 0000000000..c7533c0b30
--- /dev/null
+++ b/test/modm/lwip/lwip_ethernet_test.cpp
@@ -0,0 +1,1347 @@
+/*
+ * Copyright (c) 2026, Kaelin Laundry
+ *
+ * This file is part of the modm project.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+// ----------------------------------------------------------------------------
+
+#include "lwip_ethernet_test.hpp"
+
+#include
+#include
+#include
+
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#if defined(__unix__)
+#include
+#include
+#include
+#endif
+
+namespace
+{
+
+struct TestClock {};
+
+template
+struct FakeMac
+{
+ using MediaInterface = modm::ethernet::MediaInterface;
+ using ChecksumMode = modm::ethernet::ChecksumMode;
+ using Speed = modm::ethernet::Speed;
+ using DuplexMode = modm::ethernet::DuplexMode;
+ using LinkMode = modm::ethernet::LinkMode;
+ using LinkState = modm::ethernet::LinkState;
+ using LinkStatus = modm::ethernet::LinkStatus;
+ enum class InitializationError { None, Failed };
+ enum class TransmitError {
+ None, Busy, InvalidLength, LinkDown, NotInitialized, Faulted,
+ UnsupportedFragmentation,
+ };
+ enum class ReceiveError { None, NoFrameAvailable, NotInitialized, Faulted };
+ enum class ReceiveChecksumStatus { NotChecked, Valid, Invalid };
+ enum class LinkUpdateError {
+ None, NotInitialized, MacStopTimeout, OutstandingLease, FatalBusError,
+ };
+
+ struct Configuration
+ {
+ std::array macAddress{};
+ ChecksumMode checksumMode{ChecksumMode::Software};
+ };
+
+ struct InitializationResult
+ {
+ InitializationError error{InitializationError::None};
+ explicit operator bool() const { return error == InitializationError::None; }
+ };
+
+ struct TransmitResult
+ {
+ TransmitError error{TransmitError::None};
+ explicit operator bool() const { return error == TransmitError::None; }
+ };
+
+ struct LinkUpdateResult
+ {
+ LinkStatus status{};
+ LinkUpdateError error{LinkUpdateError::None};
+ explicit operator bool() const { return error == LinkUpdateError::None; }
+ };
+
+ static constexpr std::size_t MaxFrameSize = 1514;
+ static constexpr std::size_t RxDescriptorCount = 8;
+
+ class TransmitBufferLease
+ {
+ public:
+ TransmitBufferLease() = default;
+ TransmitBufferLease(const TransmitBufferLease&) = delete;
+ TransmitBufferLease& operator=(const TransmitBufferLease&) = delete;
+
+ TransmitBufferLease(TransmitBufferLease&& other) noexcept
+ : error_(other.error_), length_(other.length_), active_(other.active_)
+ {
+ other.active_ = false;
+ }
+
+ TransmitBufferLease&
+ operator=(TransmitBufferLease&& other) noexcept
+ {
+ if (this != &other) {
+ cancel();
+ error_ = other.error_;
+ length_ = other.length_;
+ active_ = other.active_;
+ other.active_ = false;
+ }
+ return *this;
+ }
+
+ ~TransmitBufferLease() { cancel(); }
+
+ explicit operator bool() const
+ {
+ return active_ and error_ == TransmitError::None;
+ }
+
+ TransmitError error() const { return error_; }
+
+ std::span
+ buffer()
+ {
+ return *this ? std::span{FakeMac::tx.data(), length_} : std::span{};
+ }
+
+ TransmitResult
+ commit()
+ {
+ if (not *this)
+ return {error_};
+
+ active_ = false;
+ FakeMac::txLeaseActive = false;
+ FakeMac::committedLength = length_;
+ if (FakeMac::committedFrameCount < FakeMac::committedFrames.size()) {
+ const auto index = FakeMac::committedFrameCount++;
+ FakeMac::committedLengths[index] = length_;
+ std::copy_n(FakeMac::tx.begin(), length_, FakeMac::committedFrames[index].begin());
+ }
+ FakeMac::commitCalls++;
+ return {FakeMac::commitError};
+ }
+
+ void
+ cancel()
+ {
+ if (active_) {
+ active_ = false;
+ FakeMac::txLeaseActive = false;
+ FakeMac::cancelCalls++;
+ }
+ }
+
+ private:
+ friend FakeMac;
+
+ explicit TransmitBufferLease(TransmitError error)
+ : error_(error)
+ {}
+
+ TransmitBufferLease(std::size_t length, bool active)
+ : length_(length), active_(active)
+ {}
+
+ TransmitError error_{TransmitError::None};
+ std::size_t length_{0};
+ bool active_{false};
+ };
+
+ class ReceiveBufferLease
+ {
+ public:
+ ReceiveBufferLease() = default;
+ ReceiveBufferLease(const ReceiveBufferLease&) = delete;
+ ReceiveBufferLease& operator=(const ReceiveBufferLease&) = delete;
+
+ ReceiveBufferLease(ReceiveBufferLease&& other) noexcept
+ : error_(other.error_), length_(other.length_), checksum_(other.checksum_), active_(other.active_)
+ {
+ other.active_ = false;
+ }
+
+ ReceiveBufferLease&
+ operator=(ReceiveBufferLease&& other) noexcept
+ {
+ if (this != &other) {
+ release();
+ error_ = other.error_;
+ length_ = other.length_;
+ checksum_ = other.checksum_;
+ active_ = other.active_;
+ other.active_ = false;
+ }
+ return *this;
+ }
+
+ ~ReceiveBufferLease() { release(); }
+
+ explicit operator bool() const
+ {
+ return active_ and error_ == ReceiveError::None;
+ }
+
+ ReceiveError error() const { return error_; }
+
+ std::span