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 + buffer() const + { + return *this ? std::span{FakeMac::rx.data(), length_} : + std::span{}; + } + + ReceiveChecksumStatus checksumStatus() const { return checksum_; } + + void + release() + { + if (active_) { + active_ = false; + FakeMac::rxLeaseActive = false; + if (FakeMac::rxFrames != 0) + FakeMac::rxFrames--; + FakeMac::rxReady = FakeMac::rxFrames != 0; + FakeMac::releaseCalls++; + } + } + + private: + friend FakeMac; + + explicit ReceiveBufferLease(ReceiveError error) + : error_(error) + {} + + ReceiveBufferLease(std::size_t length, ReceiveChecksumStatus checksum) + : length_(length), checksum_(checksum), active_(true) + {} + + ReceiveError error_{ReceiveError::None}; + std::size_t length_{0}; + ReceiveChecksumStatus checksum_{ReceiveChecksumStatus::NotChecked}; + bool active_{false}; + }; + + static_assert(not std::is_copy_constructible_v); + static_assert(std::is_move_constructible_v); + static_assert(not std::is_copy_constructible_v); + static_assert(std::is_move_constructible_v); + + static void + reset() + { + initializeError = InitializationError::None; + acquireError = TransmitError::None; + commitError = TransmitError::None; + receiveError = ReceiveError::None; + rxChecksum = ReceiveChecksumStatus::NotChecked; + initializeCalls = 0; + linkUpdateCalls = 0; + releaseCalls = 0; + commitCalls = 0; + cancelCalls = 0; + committedLength = 0; + committedFrameCount = 0; + committedLengths.fill(0); + rxLength = 0; + rxReady = false; + rxFrames = 0; + txLeaseActive = false; + rxLeaseActive = false; + configuration = {}; + linkStatus = {}; + linkUpdateError = LinkUpdateError::None; + mdioError = modm::ethernet::MdioError::None; + tx.fill(0); + rx.fill(0); + } + + template + static InitializationResult + initialize(Configuration const& config, uint8_t) + { + initializeCalls++; + configuration = config; + return {initializeError}; + } + + static TransmitBufferLease + acquireTransmitBuffer(std::size_t size) + { + if (acquireError != TransmitError::None) + return TransmitBufferLease{acquireError}; + if (size == 0 or size > tx.size()) + return TransmitBufferLease{TransmitError::InvalidLength}; + if (txLeaseActive) + return TransmitBufferLease{TransmitError::Busy}; + txLeaseActive = true; + return TransmitBufferLease{size, true}; + } + + static TransmitResult + transmit(std::span frame) + { + auto lease = acquireTransmitBuffer(frame.size()); + if (not lease) + return {lease.error()}; + std::copy(frame.begin(), frame.end(), lease.buffer().begin()); + return lease.commit(); + } + + static ReceiveBufferLease + tryAcquireReceiveBuffer() + { + if (receiveError != ReceiveError::None) + return ReceiveBufferLease{receiveError}; + if (not rxReady) + return ReceiveBufferLease{ReceiveError::NoFrameAvailable}; + if (rxLeaseActive) + return ReceiveBufferLease{ReceiveError::NoFrameAvailable}; + rxLeaseActive = true; + return ReceiveBufferLease{rxLength, rxChecksum}; + } + + static LinkUpdateResult + notifyUpdatedLinkStatus(LinkStatus status) + { + linkUpdateCalls++; + if (linkUpdateError == LinkUpdateError::None) + linkStatus = status; + return {linkStatus, linkUpdateError}; + } + + static LinkStatus + getLinkStatus() + { + return linkStatus; + } + + static modm::ethernet::MdioError + readPhyRegister(uint8_t, uint8_t, uint16_t& value) + { + value = 0; + return mdioError; + } + + static modm::ethernet::MdioError + writePhyRegister(uint8_t, uint8_t, uint16_t) + { + return mdioError; + } + + static inline InitializationError initializeError{InitializationError::None}; + static inline TransmitError acquireError{TransmitError::None}; + static inline TransmitError commitError{TransmitError::None}; + static inline ReceiveError receiveError{ReceiveError::None}; + static inline ReceiveChecksumStatus rxChecksum{ReceiveChecksumStatus::NotChecked}; + static inline unsigned initializeCalls{0}; + static inline unsigned linkUpdateCalls{0}; + static inline unsigned releaseCalls{0}; + static inline unsigned commitCalls{0}; + static inline unsigned cancelCalls{0}; + static inline std::size_t committedLength{0}; + static inline std::size_t committedFrameCount{0}; + static inline std::array committedLengths{}; + static inline std::array, 3> committedFrames{}; + static inline std::size_t rxLength{0}; + static inline bool rxReady{false}; + static inline std::size_t rxFrames{0}; + static inline bool txLeaseActive{false}; + static inline bool rxLeaseActive{false}; + static inline Configuration configuration{}; + static inline LinkStatus linkStatus{}; + static inline LinkUpdateError linkUpdateError{LinkUpdateError::None}; + static inline modm::ethernet::MdioError mdioError{modm::ethernet::MdioError::None}; + static inline std::array tx{}; + static inline std::array rx{}; +}; + +template +struct FakePhy +{ + enum class InitializationError { None, Failed }; + + struct InitializationResult + { + InitializationError error{InitializationError::None}; + modm::ethernet::MdioError mdioError{modm::ethernet::MdioError::None}; + explicit operator bool() const + { + return error == InitializationError::None and + mdioError == modm::ethernet::MdioError::None; + } + }; + + struct LinkStatusResult + { + modm::ethernet::LinkStatus status{}; + modm::ethernet::MdioError error{modm::ethernet::MdioError::None}; + explicit operator bool() const { return error == modm::ethernet::MdioError::None; } + }; + + static void reset() + { + initializationError = InitializationError::None; + initializationMdioError = modm::ethernet::MdioError::None; + linkMdioError = modm::ethernet::MdioError::None; + initializeCalls = 0; + linkReadCalls = 0; + } + + template + static InitializationResult initialize() + { + initializeCalls++; + return {initializationError, initializationMdioError}; + } + + template + static LinkStatusResult readLinkStatus() + { + linkReadCalls++; + return {FakeMac::linkStatus, linkMdioError}; + } + + static inline InitializationError initializationError{InitializationError::None}; + static inline modm::ethernet::MdioError initializationMdioError{ + modm::ethernet::MdioError::None}; + static inline modm::ethernet::MdioError linkMdioError{ + modm::ethernet::MdioError::None}; + static inline unsigned initializeCalls{0}; + static inline unsigned linkReadCalls{0}; +}; + +template +void +resetFakeEthernet() +{ + FakeMac::reset(); + FakePhy::reset(); +} + +constexpr modm::lwip::StaticIPv4Configuration ConfigA{ + .macAddress = {0x02, 0, 0, 0, 0, 1}, + .ipAddress = {{10, 0, 0, 2}}, + .netmask = {{255, 255, 255, 0}}, + .gateway = {{10, 0, 0, 1}}, +}; + +constexpr modm::lwip::StaticIPv4Configuration ConfigB{ + .macAddress = {0x02, 0, 0, 0, 0, 2}, + .ipAddress = {{10, 0, 1, 2}}, + .netmask = {{255, 255, 255, 0}}, + .gateway = {{10, 0, 1, 1}}, +}; + +std::array capturedInput{}; +std::size_t capturedInputLength{0}; +err_t inputResult{ERR_OK}; +bool timerExpired{false}; +bool checksumCheckingObserved{false}; +bool icmpChecksumCheckingObserved{false}; +unsigned udpDeliveryCount{0}; +std::array udpPayload{}; +std::size_t udpPayloadLength{0}; + +void +write16(uint8_t* data, uint16_t value) +{ + data[0] = uint8_t(value >> 8); + data[1] = uint8_t(value); +} + +ip4_addr_t +makeIp4(modm::lwip::IPv4Address address) +{ + ip4_addr_t ip; + IP4_ADDR(&ip, address.bytes[0], address.bytes[1], address.bytes[2], address.bytes[3]); + return ip; +} + +uint16_t +udpChecksum(std::span datagram, ip4_addr_t const& source, + ip4_addr_t const& destination) +{ + modm::lwip::LwIPSingleThreadGuard guard; + auto* p = pbuf_alloc(PBUF_RAW, datagram.size(), PBUF_RAM); + if (p == nullptr) + return 0; + pbuf_take(p, datagram.data(), datagram.size()); + const uint16_t result = inet_chksum_pseudo(p, IP_PROTO_UDP, datagram.size(), + &source, &destination); + pbuf_free(p); + return result; +} + +std::size_t +makeUdpFrame(std::array::MaxFrameSize>& frame, + std::span payload, uint16_t identification, bool zeroChecksum=false) +{ + constexpr std::array SourceMac{{0x02, 0, 0, 0, 0, 0x55}}; + std::copy(ConfigA.macAddress.begin(), ConfigA.macAddress.end(), frame.begin()); + std::copy(SourceMac.begin(), SourceMac.end(), frame.begin() + 6); + write16(frame.data() + 12, 0x0800); + auto* ip = frame.data() + 14; + const uint16_t udpLength = uint16_t(8 + payload.size()); + const uint16_t ipLength = uint16_t(20 + udpLength); + std::fill(ip, ip + ipLength, 0); + ip[0] = 0x45; + write16(ip + 2, ipLength); + write16(ip + 4, identification); + ip[8] = 64; + ip[9] = IP_PROTO_UDP; + std::copy(ConfigA.gateway.bytes.begin(), ConfigA.gateway.bytes.end(), ip + 12); + std::copy(ConfigA.ipAddress.bytes.begin(), ConfigA.ipAddress.bytes.end(), ip + 16); + auto* udp = ip + 20; + write16(udp, 1234); + write16(udp + 2, 4321); + write16(udp + 4, udpLength); + std::copy(payload.begin(), payload.end(), udp + 8); + const auto source = makeIp4(ConfigA.gateway); + const auto destination = makeIp4(ConfigA.ipAddress); + if (not zeroChecksum) { + const uint16_t checksum = udpChecksum({udp, udpLength}, source, destination); + std::memcpy(udp + 6, &checksum, sizeof(checksum)); + } + const uint16_t ipChecksum = inet_chksum(ip, 20); + std::memcpy(ip + 10, &ipChecksum, sizeof(ipChecksum)); + return 14 + ipLength; +} + +void +receiveUdp(void*, struct udp_pcb*, struct pbuf* p, const ip_addr_t*, uint16_t) +{ + LWIP_ASSERT_CORE_LOCKED(); + udpDeliveryCount++; + udpPayloadLength = std::min(p->tot_len, udpPayload.size()); + pbuf_copy_partial(p, udpPayload.data(), udpPayloadLength, 0); + pbuf_free(p); +} + +bool +checksumEnabled(struct netif const* netif, uint16_t flag) +{ +#if LWIP_CHECKSUM_CTRL_PER_NETIF + return (netif->chksum_flags & flag) != 0; +#else + (void) netif; + (void) flag; + return CHECKSUM_CHECK_IP and CHECKSUM_CHECK_UDP and + CHECKSUM_CHECK_TCP and CHECKSUM_CHECK_ICMP; +#endif +} + +err_t +captureInput(struct pbuf* p, struct netif*) +{ + LWIP_ASSERT_CORE_LOCKED(); + capturedInputLength = p->tot_len; + pbuf_copy_partial(p, capturedInput.data(), p->tot_len, 0); + if (inputResult == ERR_OK) + pbuf_free(p); + return inputResult; +} + +err_t +captureChecksumState(struct pbuf* p, struct netif* netif) +{ + LWIP_ASSERT_CORE_LOCKED(); +#if LWIP_CHECKSUM_CTRL_PER_NETIF + checksumCheckingObserved = checksumEnabled(netif, NETIF_CHECKSUM_CHECK_IP) and + checksumEnabled(netif, NETIF_CHECKSUM_CHECK_UDP) and + checksumEnabled(netif, NETIF_CHECKSUM_CHECK_TCP); + icmpChecksumCheckingObserved = checksumEnabled(netif, NETIF_CHECKSUM_CHECK_ICMP); +#else + (void) netif; + checksumCheckingObserved = CHECKSUM_CHECK_IP and CHECKSUM_CHECK_UDP and CHECKSUM_CHECK_TCP; + icmpChecksumCheckingObserved = CHECKSUM_CHECK_ICMP; +#endif + pbuf_free(p); + return ERR_OK; +} + +void +expireTimer(void*) +{ + LWIP_ASSERT_CORE_LOCKED(); + timerExpired = true; +} + +std::size_t +netifCount() +{ + std::size_t count = 0; + for (auto* item = netif_list; item != nullptr; item = item->next) + count++; + return count; +} + +bool +containsNetif(struct netif* expected) +{ + for (auto* item = netif_list; item != nullptr; item = item->next) { + if (item == expected) + return true; + } + return false; +} + +template +using Interface = modm::lwip::LwipEthernet, FakePhy>; + +} // namespace + +void +LwipEthernetTest::testInitializationRequired() +{ + using TestPort = FakeMac<13>; + using TestInterface = Interface<13>; + resetFakeEthernet<13>(); + + const auto result = TestInterface::initialize(ConfigA); + TEST_ASSERT_TRUE(bool(result)); + TEST_ASSERT_EQUALS(TestPort::initializeCalls, 1u); + TEST_ASSERT_EQUALS(FakePhy<13>::initializeCalls, 1u); + TEST_ASSERT_EQUALS(FakePhy<13>::linkReadCalls, 1u); +} + +void +LwipEthernetTest::testInitializationAndRouting() +{ + modm::lwip::initialize(); + resetFakeEthernet<0>(); + resetFakeEthernet<1>(); + const auto initialCount = netifCount(); + + TEST_ASSERT_TRUE(bool(Interface<0>::initialize(ConfigA))); + TEST_ASSERT_TRUE(bool(Interface<1>::initialize(ConfigB))); + TEST_ASSERT_EQUALS(netifCount(), initialCount + 2); + TEST_ASSERT_TRUE(containsNetif(Interface<0>::netif())); + TEST_ASSERT_TRUE(containsNetif(Interface<1>::netif())); + TEST_ASSERT_EQUALS(FakeMac<0>::initializeCalls, 1u); + TEST_ASSERT_EQUALS(FakeMac<1>::initializeCalls, 1u); + TEST_ASSERT_TRUE(FakeMac<0>::configuration.macAddress == ConfigA.macAddress); + const auto expectedChecksumMode = MODM_LWIP_CHECKSUM_HARDWARE ? + FakeMac<0>::ChecksumMode::Hardware : FakeMac<0>::ChecksumMode::Software; + TEST_ASSERT_TRUE(FakeMac<0>::configuration.checksumMode == expectedChecksumMode); + TEST_ASSERT_EQUALS(Interface<0>::netif()->mtu, 1500u); + TEST_ASSERT_EQUALS(Interface<0>::netif()->name[0], 'e'); + TEST_ASSERT_EQUALS(Interface<0>::netif()->name[1], 'n'); + TEST_ASSERT_EQUALS(Interface<1>::netif()->name[0], 'e'); + TEST_ASSERT_EQUALS(Interface<1>::netif()->name[1], 'n'); + TEST_ASSERT_DIFFERS(Interface<0>::netif()->num, Interface<1>::netif()->num); + TEST_ASSERT_TRUE(Interface<0>::netif()->output != nullptr); + TEST_ASSERT_TRUE(Interface<0>::netif()->linkoutput != nullptr); + TEST_ASSERT_TRUE((Interface<0>::netif()->flags & NETIF_FLAG_ETHARP) != 0); + TEST_ASSERT_TRUE((Interface<0>::netif()->flags & NETIF_FLAG_ETHERNET) != 0); +#if MODM_LWIP_CHECKSUM_HARDWARE + TEST_ASSERT_FALSE(checksumEnabled(Interface<0>::netif(), NETIF_CHECKSUM_GEN_IP)); + TEST_ASSERT_FALSE(checksumEnabled(Interface<0>::netif(), NETIF_CHECKSUM_GEN_ICMP6)); + TEST_ASSERT_FALSE(checksumEnabled(Interface<0>::netif(), NETIF_CHECKSUM_CHECK_ICMP6)); +#endif + + TEST_ASSERT_TRUE(Interface<0>::setDefault()); + TEST_ASSERT_TRUE(netif_default == Interface<0>::netif()); + TEST_ASSERT_TRUE(Interface<1>::setDefault()); + TEST_ASSERT_TRUE(netif_default == Interface<1>::netif()); + + TEST_ASSERT_TRUE(bool(Interface<0>::initialize(ConfigA))); + TEST_ASSERT_EQUALS(FakeMac<0>::initializeCalls, 2u); + TEST_ASSERT_EQUALS(netifCount(), initialCount + 2); +} + +void +LwipEthernetTest::testPhysicalMacBinding() +{ + using Mac = FakeMac<11>; + using FirstPhy = FakePhy<11, 0>; + using SecondPhy = FakePhy<11, 1>; + using FirstInterface = modm::lwip::LwipEthernet; + using SecondInterface = modm::lwip::LwipEthernet; + Mac::reset(); + FirstPhy::reset(); + SecondPhy::reset(); + + TEST_ASSERT_TRUE(bool(FirstInterface::initialize(ConfigA))); + TEST_ASSERT_TRUE(bool(FirstInterface::initialize(ConfigB))); + const auto result = SecondInterface::initialize(ConfigA); + TEST_ASSERT_FALSE(bool(result)); + TEST_ASSERT_TRUE(result.adapterError == + SecondInterface::AdapterInitializationError::MacAlreadyBound); + TEST_ASSERT_EQUALS(Mac::initializeCalls, 2u); + TEST_ASSERT_TRUE(FirstInterface::netif() == SecondInterface::netif()); +} + +void +LwipEthernetTest::testFailedBindingCanBeReplaced() +{ + using Mac = FakeMac<14>; + using FailedPhy = FakePhy<14, 0>; + using ReplacementPhy = FakePhy<14, 1>; + using FailedInterface = modm::lwip::LwipEthernet; + using ReplacementInterface = modm::lwip::LwipEthernet; + Mac::reset(); + FailedPhy::reset(); + ReplacementPhy::reset(); + Mac::initializeError = Mac::InitializationError::Failed; + const auto initialCount = netifCount(); + + const auto failed = FailedInterface::initialize(ConfigA); + TEST_ASSERT_FALSE(bool(failed)); + TEST_ASSERT_TRUE(failed.macError == Mac::InitializationError::Failed); + TEST_ASSERT_EQUALS(Mac::initializeCalls, 1u); + + Mac::initializeError = Mac::InitializationError::None; + const auto replacement = ReplacementInterface::initialize(ConfigB); + TEST_ASSERT_TRUE(bool(replacement)); + TEST_ASSERT_EQUALS(Mac::initializeCalls, 2u); + TEST_ASSERT_EQUALS(netifCount(), initialCount + 1); + TEST_ASSERT_TRUE(containsNetif(ReplacementInterface::netif())); +} + +void +LwipEthernetTest::testReinitializationAndRecovery() +{ + using Mac = FakeMac<8>; + using TestInterface = Interface<8>; + resetFakeEthernet<8>(); + Mac::linkStatus = { + Mac::LinkState::Up, + Mac::LinkMode{Mac::Speed::Speed100M, Mac::DuplexMode::Full}, + }; + const auto initialCount = netifCount(); + TEST_ASSERT_TRUE(bool(TestInterface::initialize(ConfigA))); + auto* const registeredInterface = TestInterface::netif(); + TEST_ASSERT_EQUALS(netifCount(), initialCount + 1); + TEST_ASSERT_TRUE(netif_is_link_up(registeredInterface)); + + Mac::initializeError = Mac::InitializationError::Failed; + const auto failure = TestInterface::initialize(ConfigB); + TEST_ASSERT_FALSE(bool(failure)); + TEST_ASSERT_TRUE(failure.macError == Mac::InitializationError::Failed); + TEST_ASSERT_TRUE(TestInterface::netif() == registeredInterface); + TEST_ASSERT_EQUALS(netifCount(), initialCount + 1); + TEST_ASSERT_FALSE(netif_is_up(registeredInterface)); + TEST_ASSERT_FALSE(netif_is_link_up(registeredInterface)); + + Mac::initializeError = Mac::InitializationError::None; + TEST_ASSERT_TRUE(bool(TestInterface::initialize(ConfigB))); + TEST_ASSERT_EQUALS(Mac::initializeCalls, 3u); + TEST_ASSERT_TRUE(TestInterface::netif() == registeredInterface); + TEST_ASSERT_EQUALS(netifCount(), initialCount + 1); + TEST_ASSERT_TRUE(netif_is_link_up(registeredInterface)); + TEST_ASSERT_TRUE(std::equal(ConfigB.macAddress.begin(), ConfigB.macAddress.end(), + registeredInterface->hwaddr)); + const auto expectedAddress = makeIp4(ConfigB.ipAddress); + const auto expectedNetmask = makeIp4(ConfigB.netmask); + const auto expectedGateway = makeIp4(ConfigB.gateway); + TEST_ASSERT_TRUE(ip4_addr_cmp(netif_ip4_addr(registeredInterface), &expectedAddress)); + TEST_ASSERT_TRUE(ip4_addr_cmp(netif_ip4_netmask(registeredInterface), &expectedNetmask)); + TEST_ASSERT_TRUE(ip4_addr_cmp(netif_ip4_gw(registeredInterface), &expectedGateway)); + const auto expectedChecksumMode = MODM_LWIP_CHECKSUM_HARDWARE ? + Mac::ChecksumMode::Hardware : Mac::ChecksumMode::Software; + TEST_ASSERT_TRUE(Mac::configuration.checksumMode == expectedChecksumMode); +} + +void +LwipEthernetTest::testConfigurationAndLeaseContract() +{ + using Mac = FakeMac<6>; + resetFakeEthernet<6>(); + + { + auto lease = Mac::acquireTransmitBuffer(32); + TEST_ASSERT_TRUE(bool(lease)); + TEST_ASSERT_EQUALS(lease.buffer().size(), 32u); + TEST_ASSERT_TRUE(Mac::acquireTransmitBuffer(32).error() == Mac::TransmitError::Busy); + + auto moved = std::move(lease); + TEST_ASSERT_FALSE(bool(lease)); + TEST_ASSERT_TRUE(bool(moved)); + std::fill(moved.buffer().begin(), moved.buffer().end(), 0xa5); + TEST_ASSERT_TRUE(bool(moved.commit())); + TEST_ASSERT_EQUALS(Mac::commitCalls, 1u); + TEST_ASSERT_EQUALS(Mac::committedLength, 32u); + } + TEST_ASSERT_EQUALS(Mac::cancelCalls, 0u); + + { + auto lease = Mac::acquireTransmitBuffer(16); + TEST_ASSERT_TRUE(bool(lease)); + } + TEST_ASSERT_EQUALS(Mac::cancelCalls, 1u); + { + auto lease = Mac::acquireTransmitBuffer(16); + TEST_ASSERT_TRUE(bool(lease)); + } + TEST_ASSERT_EQUALS(Mac::cancelCalls, 2u); + TEST_ASSERT_TRUE(Mac::acquireTransmitBuffer(0).error() == Mac::TransmitError::InvalidLength); + TEST_ASSERT_TRUE(Mac::acquireTransmitBuffer(Mac::MaxFrameSize + 1).error() == + Mac::TransmitError::InvalidLength); + + Mac::rxReady = true; + Mac::rxFrames = 1; + Mac::rxLength = 24; + Mac::rxChecksum = Mac::ReceiveChecksumStatus::Valid; + { + auto lease = Mac::tryAcquireReceiveBuffer(); + TEST_ASSERT_TRUE(bool(lease)); + TEST_ASSERT_EQUALS(lease.buffer().size(), 24u); + TEST_ASSERT_TRUE(lease.checksumStatus() == Mac::ReceiveChecksumStatus::Valid); + TEST_ASSERT_TRUE(Mac::tryAcquireReceiveBuffer().error() == + Mac::ReceiveError::NoFrameAvailable); + auto moved = std::move(lease); + TEST_ASSERT_FALSE(bool(lease)); + TEST_ASSERT_TRUE(bool(moved)); + } + TEST_ASSERT_EQUALS(Mac::releaseCalls, 1u); + TEST_ASSERT_TRUE(Mac::tryAcquireReceiveBuffer().error() == + Mac::ReceiveError::NoFrameAvailable); +} + +void +LwipEthernetTest::testTransmitPath() +{ + resetFakeEthernet<2>(); + TEST_ASSERT_TRUE(bool(Interface<2>::initialize(ConfigA))); + auto* netif = Interface<2>::netif(); + modm::lwip::LwIPSingleThreadGuard guard; + + const std::array first{{1, 2, 3}}; + const std::array second{{4, 5, 6, 7}}; + auto* head = pbuf_alloc(PBUF_RAW, first.size(), PBUF_RAM); + auto* tail = pbuf_alloc(PBUF_RAW, second.size(), PBUF_RAM); + TEST_ASSERT_TRUE(head != nullptr); + TEST_ASSERT_TRUE(tail != nullptr); + TEST_ASSERT_EQUALS(pbuf_take(head, first.data(), first.size()), ERR_OK); + TEST_ASSERT_EQUALS(pbuf_take(tail, second.data(), second.size()), ERR_OK); + pbuf_cat(head, tail); + TEST_ASSERT_EQUALS(netif->linkoutput(netif, head), ERR_OK); + TEST_ASSERT_EQUALS(FakeMac<2>::committedLength, 7u); + const std::array expected{{1, 2, 3, 4, 5, 6, 7}}; + TEST_ASSERT_TRUE(std::equal(expected.begin(), expected.end(), FakeMac<2>::tx.begin())); + pbuf_free(head); + + auto* packet = pbuf_alloc(PBUF_RAW, 64, PBUF_RAM); + TEST_ASSERT_TRUE(packet != nullptr); + constexpr std::array transmitErrorMappings{ + std::pair{FakeMac<2>::TransmitError::Busy, ERR_MEM}, + std::pair{FakeMac<2>::TransmitError::InvalidLength, ERR_BUF}, + std::pair{FakeMac<2>::TransmitError::LinkDown, ERR_IF}, + std::pair{FakeMac<2>::TransmitError::NotInitialized, ERR_IF}, + std::pair{FakeMac<2>::TransmitError::Faulted, ERR_IF}, + std::pair{FakeMac<2>::TransmitError::UnsupportedFragmentation, ERR_VAL}, + }; + for (const auto& [error, expected] : transmitErrorMappings) { + FakeMac<2>::acquireError = error; + TEST_ASSERT_EQUALS(netif->linkoutput(netif, packet), expected); + } + FakeMac<2>::acquireError = FakeMac<2>::TransmitError::None; + FakeMac<2>::commitError = FakeMac<2>::TransmitError::Faulted; + TEST_ASSERT_EQUALS(netif->linkoutput(netif, packet), ERR_IF); + FakeMac<2>::commitError = FakeMac<2>::TransmitError::None; + pbuf_free(packet); + + auto* oversized = pbuf_alloc(PBUF_RAW, FakeMac<2>::MaxFrameSize + 1, PBUF_RAM); + TEST_ASSERT_TRUE(oversized != nullptr); + TEST_ASSERT_EQUALS(netif->linkoutput(netif, oversized), ERR_BUF); + pbuf_free(oversized); +} + +void +LwipEthernetTest::testReceivePath() +{ + resetFakeEthernet<3>(); + TEST_ASSERT_TRUE(bool(Interface<3>::initialize(ConfigA))); + Interface<3>::netif()->input = captureInput; + for (std::size_t index = 0; index < 60; ++index) + FakeMac<3>::rx[index] = uint8_t(index); + FakeMac<3>::rxLength = 60; + FakeMac<3>::rxReady = true; + capturedInputLength = 0; + inputResult = ERR_OK; + + TEST_ASSERT_EQUALS(Interface<3>::pollInput(FakeMac<3>::RxDescriptorCount), ERR_OK); + TEST_ASSERT_EQUALS(FakeMac<3>::releaseCalls, 1u); + TEST_ASSERT_EQUALS(capturedInputLength, 60u); + TEST_ASSERT_TRUE(std::equal(capturedInput.begin(), capturedInput.begin() + 60, + FakeMac<3>::rx.begin())); + + FakeMac<3>::rxReady = true; + FakeMac<3>::rxFrames = 1; + inputResult = ERR_IF; + TEST_ASSERT_EQUALS(Interface<3>::pollInput(FakeMac<3>::RxDescriptorCount), ERR_IF); + TEST_ASSERT_EQUALS(FakeMac<3>::releaseCalls, 2u); + TEST_ASSERT_FALSE(FakeMac<3>::rxReady); + + inputResult = ERR_OK; + FakeMac<3>::rxReady = true; + FakeMac<3>::rxFrames = 20; + TEST_ASSERT_EQUALS(Interface<3>::pollInput(FakeMac<3>::RxDescriptorCount), ERR_OK); + TEST_ASSERT_EQUALS(FakeMac<3>::releaseCalls, 10u); + TEST_ASSERT_EQUALS(FakeMac<3>::rxFrames, 12u); + TEST_ASSERT_TRUE(FakeMac<3>::rxReady); + TEST_ASSERT_EQUALS(Interface<3>::pollInput(16), ERR_OK); + TEST_ASSERT_EQUALS(FakeMac<3>::releaseCalls, 22u); + TEST_ASSERT_FALSE(FakeMac<3>::rxReady); + + const auto cumulative = Interface<3>::getCumulativeReceiveStatistics(); + TEST_ASSERT_EQUALS(cumulative.acquiredFrames, 22u); + TEST_ASSERT_EQUALS(cumulative.droppedFrames, 1u); + TEST_ASSERT_EQUALS(cumulative.checksumDrops, 0u); + TEST_ASSERT_EQUALS(cumulative.allocationDrops, 0u); + TEST_ASSERT_TRUE(bool(Interface<3>::initialize(ConfigB))); + const auto afterReinitialization = Interface<3>::getCumulativeReceiveStatistics(); + TEST_ASSERT_EQUALS(afterReinitialization.acquiredFrames, cumulative.acquiredFrames); + TEST_ASSERT_EQUALS(afterReinitialization.droppedFrames, cumulative.droppedFrames); + TEST_ASSERT_EQUALS(afterReinitialization.checksumDrops, cumulative.checksumDrops); + TEST_ASSERT_EQUALS(afterReinitialization.allocationDrops, cumulative.allocationDrops); +} + +void +LwipEthernetTest::testReceiveStatisticsAndChecksums() +{ + using Mac = FakeMac<7>; + using TestInterface = Interface<7>; + resetFakeEthernet<7>(); + TEST_ASSERT_TRUE(bool(TestInterface::initialize(ConfigA))); + TestInterface::netif()->input = captureChecksumState; + Mac::rxLength = 60; + +#if LINK_STATS + const auto initialLinkReceive = lwip_stats.link.recv; + const auto initialLinkDrop = lwip_stats.link.drop; + const auto initialLinkMemoryError = lwip_stats.link.memerr; + const auto initialLinkChecksumError = lwip_stats.link.chkerr; + const auto initialLinkError = lwip_stats.link.err; +#endif + + const auto beforeZeroLimit = TestInterface::getCumulativeReceiveStatistics(); + TEST_ASSERT_EQUALS(TestInterface::pollInput(0), ERR_OK); + TEST_ASSERT_EQUALS(TestInterface::getCumulativeReceiveStatistics().acquiredFrames, + beforeZeroLimit.acquiredFrames); + + Mac::rxReady = true; + Mac::rxFrames = 1; + Mac::rxChecksum = Mac::ReceiveChecksumStatus::NotChecked; + checksumCheckingObserved = false; + icmpChecksumCheckingObserved = false; + TEST_ASSERT_EQUALS(TestInterface::pollInput(Mac::RxDescriptorCount), ERR_OK); + TEST_ASSERT_TRUE(checksumCheckingObserved); + TEST_ASSERT_TRUE(icmpChecksumCheckingObserved); + auto statistics = TestInterface::getCumulativeReceiveStatistics(); + TEST_ASSERT_EQUALS(statistics.acquiredFrames, beforeZeroLimit.acquiredFrames + 1); + TEST_ASSERT_EQUALS(statistics.droppedFrames, beforeZeroLimit.droppedFrames); + TEST_ASSERT_EQUALS(statistics.checksumDrops, beforeZeroLimit.checksumDrops); + TEST_ASSERT_EQUALS(statistics.allocationDrops, beforeZeroLimit.allocationDrops); +#if LINK_STATS + TEST_ASSERT_EQUALS(lwip_stats.link.recv, initialLinkReceive + 1); +#endif + + const auto beforeSecondFrame = TestInterface::getCumulativeReceiveStatistics(); + Mac::rxReady = true; + Mac::rxFrames = 1; + Mac::rxChecksum = Mac::ReceiveChecksumStatus::NotChecked; + TEST_ASSERT_EQUALS(TestInterface::pollInput(Mac::RxDescriptorCount), ERR_OK); + const auto afterSecondFrame = TestInterface::getCumulativeReceiveStatistics(); + TEST_ASSERT_EQUALS(afterSecondFrame.acquiredFrames, beforeSecondFrame.acquiredFrames + 1); + + const auto beforeReceiveError = TestInterface::getCumulativeReceiveStatistics(); + Mac::receiveError = Mac::ReceiveError::Faulted; + TEST_ASSERT_EQUALS(TestInterface::pollInput(Mac::RxDescriptorCount), ERR_IF); + const auto afterReceiveError = TestInterface::getCumulativeReceiveStatistics(); + TEST_ASSERT_EQUALS(afterReceiveError.acquiredFrames, beforeReceiveError.acquiredFrames); + TEST_ASSERT_EQUALS(afterReceiveError.droppedFrames, beforeReceiveError.droppedFrames); +#if LINK_STATS + TEST_ASSERT_EQUALS(lwip_stats.link.err, initialLinkError + 1); +#endif + Mac::receiveError = Mac::ReceiveError::None; + +#if MODM_LWIP_CHECKSUM_HARDWARE + TEST_ASSERT_FALSE(checksumEnabled(TestInterface::netif(), NETIF_CHECKSUM_CHECK_IP)); + Mac::rxReady = true; + Mac::rxFrames = 1; + Mac::rxChecksum = Mac::ReceiveChecksumStatus::Valid; + checksumCheckingObserved = true; + TEST_ASSERT_EQUALS(TestInterface::pollInput(Mac::RxDescriptorCount), ERR_OK); + TEST_ASSERT_FALSE(checksumCheckingObserved); + + const auto beforeInvalidChecksum = TestInterface::getCumulativeReceiveStatistics(); + Mac::rxReady = true; + Mac::rxFrames = 1; + Mac::rxChecksum = Mac::ReceiveChecksumStatus::Invalid; + TEST_ASSERT_EQUALS(TestInterface::pollInput(Mac::RxDescriptorCount), ERR_OK); + statistics = TestInterface::getCumulativeReceiveStatistics(); + TEST_ASSERT_EQUALS(statistics.acquiredFrames, beforeInvalidChecksum.acquiredFrames + 1); + TEST_ASSERT_EQUALS(statistics.droppedFrames, beforeInvalidChecksum.droppedFrames + 1); + TEST_ASSERT_EQUALS(statistics.checksumDrops, beforeInvalidChecksum.checksumDrops + 1); +#if LINK_STATS + TEST_ASSERT_EQUALS(lwip_stats.link.drop, initialLinkDrop + 1); + TEST_ASSERT_EQUALS(lwip_stats.link.chkerr, initialLinkChecksumError + 1); +#endif +#else +#if LWIP_CHECKSUM_CTRL_PER_NETIF + TEST_ASSERT_TRUE(checksumEnabled(TestInterface::netif(), NETIF_CHECKSUM_CHECK_IP)); +#else + TEST_ASSERT_EQUALS(CHECKSUM_CHECK_IP, 1); +#endif + const auto beforeInvalidChecksum = TestInterface::getCumulativeReceiveStatistics(); + Mac::rxReady = true; + Mac::rxFrames = 1; + Mac::rxChecksum = Mac::ReceiveChecksumStatus::Invalid; + TEST_ASSERT_EQUALS(TestInterface::pollInput(Mac::RxDescriptorCount), ERR_OK); + statistics = TestInterface::getCumulativeReceiveStatistics(); + TEST_ASSERT_EQUALS(statistics.acquiredFrames, beforeInvalidChecksum.acquiredFrames + 1); + TEST_ASSERT_EQUALS(statistics.droppedFrames, beforeInvalidChecksum.droppedFrames + 1); + TEST_ASSERT_EQUALS(statistics.checksumDrops, beforeInvalidChecksum.checksumDrops + 1); +#if LINK_STATS + TEST_ASSERT_EQUALS(lwip_stats.link.chkerr, initialLinkChecksumError + 1); +#endif +#endif + + const auto beforeInputError = TestInterface::getCumulativeReceiveStatistics(); + TestInterface::netif()->input = captureInput; + inputResult = ERR_IF; + Mac::rxReady = true; + Mac::rxFrames = 1; + Mac::rxChecksum = Mac::ReceiveChecksumStatus::NotChecked; + TEST_ASSERT_EQUALS(TestInterface::pollInput(Mac::RxDescriptorCount), ERR_IF); + statistics = TestInterface::getCumulativeReceiveStatistics(); + TEST_ASSERT_EQUALS(statistics.acquiredFrames, beforeInputError.acquiredFrames + 1); + TEST_ASSERT_EQUALS(statistics.droppedFrames, beforeInputError.droppedFrames + 1); +#if LINK_STATS + TEST_ASSERT_EQUALS(lwip_stats.link.err, initialLinkError + 2); +#endif + inputResult = ERR_OK; + TestInterface::netif()->input = captureChecksumState; + + std::array heldBuffers{}; + std::size_t heldCount = 0; + { + modm::lwip::LwIPSingleThreadGuard guard; + while (heldCount < heldBuffers.size()) { + auto* held = pbuf_alloc(PBUF_RAW, 1, PBUF_POOL); + if (held == nullptr) + break; + heldBuffers[heldCount++] = held; + } + TEST_ASSERT_TRUE(heldCount != 0); + auto* exhaustionCheck = pbuf_alloc(PBUF_RAW, 1, PBUF_POOL); + TEST_ASSERT_TRUE(exhaustionCheck == nullptr); + if (exhaustionCheck != nullptr) + pbuf_free(exhaustionCheck); + } + Mac::rxReady = true; + Mac::rxFrames = 1; + Mac::rxChecksum = Mac::ReceiveChecksumStatus::NotChecked; + const auto beforeAllocationDrop = TestInterface::getCumulativeReceiveStatistics(); + TEST_ASSERT_EQUALS(TestInterface::pollInput(Mac::RxDescriptorCount), ERR_MEM); + statistics = TestInterface::getCumulativeReceiveStatistics(); + TEST_ASSERT_EQUALS(statistics.acquiredFrames, beforeAllocationDrop.acquiredFrames + 1); + TEST_ASSERT_EQUALS(statistics.droppedFrames, beforeAllocationDrop.droppedFrames + 1); + TEST_ASSERT_EQUALS(statistics.allocationDrops, beforeAllocationDrop.allocationDrops + 1); +#if LINK_STATS + TEST_ASSERT_EQUALS(lwip_stats.link.drop, initialLinkDrop + 3); + TEST_ASSERT_EQUALS(lwip_stats.link.memerr, initialLinkMemoryError + 1); +#endif + { + modm::lwip::LwIPSingleThreadGuard guard; + for (std::size_t index = 0; index < heldCount; ++index) + pbuf_free(heldBuffers[index]); + } + + const auto beforeLimit = TestInterface::getCumulativeReceiveStatistics(); + TEST_ASSERT_TRUE(beforeLimit.droppedFrames >= + beforeLimit.checksumDrops + beforeLimit.allocationDrops); + Mac::rxReady = true; + Mac::rxFrames = Mac::RxDescriptorCount + 3; + Mac::rxChecksum = Mac::ReceiveChecksumStatus::NotChecked; + TEST_ASSERT_EQUALS(TestInterface::pollInput(Mac::RxDescriptorCount), ERR_OK); + TEST_ASSERT_EQUALS(Mac::rxFrames, 3u); + const auto afterLimit = TestInterface::getCumulativeReceiveStatistics(); + TEST_ASSERT_EQUALS(afterLimit.acquiredFrames, + beforeLimit.acquiredFrames + Mac::RxDescriptorCount); +} + +void +LwipEthernetTest::testRealUdpChecksumAndReassembly() +{ + using Mac = FakeMac<9>; + using TestInterface = Interface<9>; + resetFakeEthernet<9>(); + Mac::linkStatus = { + Mac::LinkState::Up, + Mac::LinkMode{Mac::Speed::Speed100M, Mac::DuplexMode::Full}, + }; + TEST_ASSERT_TRUE(bool(TestInterface::initialize(ConfigA))); + struct udp_pcb* pcb = nullptr; + { + modm::lwip::LwIPSingleThreadGuard guard; + pcb = udp_new(); + TEST_ASSERT_TRUE(pcb != nullptr); + TEST_ASSERT_EQUALS(udp_bind(pcb, IP_ADDR_ANY, 4321), ERR_OK); + udp_recv(pcb, receiveUdp, nullptr); + } + + auto deliver = [](std::span frame) { + std::copy(frame.begin(), frame.end(), Mac::rx.begin()); + Mac::rxLength = frame.size(); + Mac::rxFrames = 1; + Mac::rxReady = true; + Mac::rxChecksum = Mac::ReceiveChecksumStatus::NotChecked; + return TestInterface::pollInput(Mac::RxDescriptorCount); + }; + + std::array frame{}; + constexpr std::array OddPayload{{1, 2, 3, 4, 5}}; + udpDeliveryCount = 0; + auto length = makeUdpFrame(frame, OddPayload, 0x100); + TEST_ASSERT_EQUALS(deliver({frame.data(), length}), ERR_OK); + TEST_ASSERT_EQUALS(udpDeliveryCount, 1u); + TEST_ASSERT_EQUALS(udpPayloadLength, OddPayload.size()); + TEST_ASSERT_TRUE(std::equal(OddPayload.begin(), OddPayload.end(), udpPayload.begin())); + + length = makeUdpFrame(frame, OddPayload, 0x101); + frame[42] ^= 0x80; + TEST_ASSERT_EQUALS(deliver({frame.data(), length}), ERR_OK); + TEST_ASSERT_EQUALS(udpDeliveryCount, 1u); + + length = makeUdpFrame(frame, OddPayload, 0x102, true); + TEST_ASSERT_EQUALS(deliver({frame.data(), length}), ERR_OK); + TEST_ASSERT_EQUALS(udpDeliveryCount, 2u); + + std::array fragmentedPayload{}; + for (std::size_t index = 0; index < fragmentedPayload.size(); ++index) + fragmentedPayload[index] = uint8_t(0x30 + index); + std::array complete{}; + makeUdpFrame(complete, fragmentedPayload, 0x103); + auto makeFragment = [&](std::size_t offset, std::size_t fragmentLength, uint16_t fragmentField) { + std::array fragment{}; + std::copy_n(complete.begin(), 14, fragment.begin()); + auto* ip = fragment.data() + 14; + std::copy_n(complete.data() + 14, 20, ip); + write16(ip + 2, uint16_t(20 + fragmentLength)); + write16(ip + 6, fragmentField); + write16(ip + 10, 0); + std::copy_n(complete.data() + 34 + offset, fragmentLength, ip + 20); + const uint16_t checksum = inet_chksum(ip, 20); + std::memcpy(ip + 10, &checksum, sizeof(checksum)); + return std::pair{fragment, 14 + 20 + fragmentLength}; + }; + auto first = makeFragment(0, 16, 0x2000); + auto second = makeFragment(16, 9, 2); + TEST_ASSERT_EQUALS(deliver({first.first.data(), first.second}), ERR_OK); + TEST_ASSERT_EQUALS(udpDeliveryCount, 2u); + TEST_ASSERT_EQUALS(deliver({second.first.data(), second.second}), ERR_OK); + TEST_ASSERT_EQUALS(udpDeliveryCount, 3u); + TEST_ASSERT_EQUALS(udpPayloadLength, fragmentedPayload.size()); + TEST_ASSERT_TRUE(std::equal(fragmentedPayload.begin(), fragmentedPayload.end(), udpPayload.begin())); + + { + modm::lwip::LwIPSingleThreadGuard guard; + udp_remove(pcb); + } +} + +void +LwipEthernetTest::testOutgoingSoftwareFragmentation() +{ +#if IP_FRAG + using Mac = FakeMac<10>; + using TestInterface = Interface<10>; + resetFakeEthernet<10>(); + Mac::linkStatus = { + Mac::LinkState::Up, + Mac::LinkMode{Mac::Speed::Speed100M, Mac::DuplexMode::Full}, + }; + TEST_ASSERT_TRUE(bool(TestInterface::initialize(ConfigA))); + Mac::committedFrameCount = 0; + Mac::committedLengths.fill(0); + std::array payload{}; + for (std::size_t index = 0; index < payload.size(); ++index) + payload[index] = uint8_t((index * 17 + 3) % 251); + + { + modm::lwip::LwIPSingleThreadGuard guard; + auto* pcb = udp_new(); + TEST_ASSERT_TRUE(pcb != nullptr); + ip_set_option(pcb, SOF_BROADCAST); + auto* p = pbuf_alloc(PBUF_TRANSPORT, payload.size(), PBUF_RAM); + TEST_ASSERT_TRUE(p != nullptr); + TEST_ASSERT_EQUALS(pbuf_take(p, payload.data(), payload.size()), ERR_OK); + ip_addr_t destination; + IP_ADDR4(&destination, 10, 0, 0, 255); + TEST_ASSERT_EQUALS(udp_sendto_if(pcb, p, &destination, 5555, + TestInterface::netif()), ERR_OK); + pbuf_free(p); + udp_remove(pcb); + } + + TEST_ASSERT_EQUALS(Mac::committedFrameCount, 2u); + std::array datagram{}; + auto read16 = [](const uint8_t* data) { + return uint16_t((uint16_t(data[0]) << 8) | data[1]); + }; + std::size_t reconstructedLength = 0; + uint16_t identification = 0; + bool sawFirst = false; + bool sawLast = false; + for (std::size_t index = 0; index < Mac::committedFrameCount; ++index) { + const auto& captured = Mac::committedFrames[index]; + const auto frameLength = Mac::committedLengths[index]; + TEST_ASSERT_TRUE(frameLength <= Mac::MaxFrameSize); + if (frameLength < 34 or frameLength > Mac::MaxFrameSize) { + TEST_ASSERT_TRUE(false); + continue; + } + TEST_ASSERT_EQUALS(read16(captured.data() + 12), 0x0800u); + const auto* ip = captured.data() + 14; + const std::size_t headerLength = std::size_t(ip[0] & 0x0f) * 4; + TEST_ASSERT_EQUALS(headerLength, 20u); + TEST_ASSERT_EQUALS(inet_chksum(ip, headerLength), 0u); + const uint16_t totalLength = read16(ip + 2); + TEST_ASSERT_EQUALS(frameLength, std::size_t(14 + totalLength)); + if (headerLength > totalLength or totalLength > frameLength - 14) { + TEST_ASSERT_TRUE(false); + continue; + } + const uint16_t fragment = read16(ip + 6); + const std::size_t offset = std::size_t(fragment & 0x1fff) * 8; + const std::size_t fragmentLength = totalLength - headerLength; + TEST_ASSERT_TRUE(offset + fragmentLength <= datagram.size()); + if (offset > datagram.size() or fragmentLength > datagram.size() - offset) { + TEST_ASSERT_TRUE(false); + continue; + } + std::copy_n(ip + headerLength, fragmentLength, datagram.begin() + offset); + reconstructedLength += fragmentLength; + if (offset == 0) { + sawFirst = true; + identification = read16(ip + 4); + TEST_ASSERT_TRUE((fragment & 0x2000) != 0); + } + else { + sawLast = true; + TEST_ASSERT_EQUALS(read16(ip + 4), identification); + TEST_ASSERT_TRUE((fragment & 0x2000) == 0); + } + } + TEST_ASSERT_TRUE(sawFirst); + TEST_ASSERT_TRUE(sawLast); + TEST_ASSERT_EQUALS(reconstructedLength, datagram.size()); + TEST_ASSERT_EQUALS(read16(datagram.data() + 2), 5555u); + TEST_ASSERT_EQUALS(read16(datagram.data() + 4), datagram.size()); + TEST_ASSERT_TRUE(read16(datagram.data() + 6) != 0); + TEST_ASSERT_TRUE(std::equal(payload.begin(), payload.end(), datagram.begin() + 8)); + const auto source = makeIp4(ConfigA.ipAddress); + const modm::lwip::IPv4Address broadcast{{10, 0, 0, 255}}; + const auto destination = makeIp4(broadcast); + TEST_ASSERT_EQUALS(udpChecksum(datagram, source, destination), 0u); +#else + TEST_ASSERT_EQUALS(IP_FRAG, 0); +#endif +} + +void +LwipEthernetTest::testLinkPollingAndTimers() +{ + resetFakeEthernet<4>(); + TEST_ASSERT_TRUE(bool(Interface<4>::initialize(ConfigA))); + TEST_ASSERT_EQUALS(FakePhy<4>::linkReadCalls, 1u); + TEST_ASSERT_EQUALS(FakeMac<4>::linkUpdateCalls, 1u); + TEST_ASSERT_FALSE(netif_is_link_up(Interface<4>::netif())); + + FakeMac<4>::linkStatus = { + FakeMac<4>::LinkState::Up, + FakeMac<4>::LinkMode{FakeMac<4>::Speed::Speed100M, FakeMac<4>::DuplexMode::Full}, + }; + TEST_ASSERT_TRUE(bool(Interface<4>::pollLink())); + TEST_ASSERT_EQUALS(FakePhy<4>::linkReadCalls, 2u); + TEST_ASSERT_EQUALS(FakeMac<4>::linkUpdateCalls, 2u); + TEST_ASSERT_TRUE(netif_is_link_up(Interface<4>::netif())); + FakeMac<4>::linkStatus = {}; + FakePhy<4>::linkMdioError = modm::ethernet::MdioError::Timeout; + const auto errorResult = Interface<4>::pollLink(); + TEST_ASSERT_EQUALS(FakePhy<4>::linkReadCalls, 3u); + TEST_ASSERT_EQUALS(FakeMac<4>::linkUpdateCalls, 2u); + TEST_ASSERT_TRUE(errorResult.phyError == modm::ethernet::MdioError::Timeout); + TEST_ASSERT_TRUE(netif_is_link_up(Interface<4>::netif())); + FakePhy<4>::linkMdioError = modm::ethernet::MdioError::None; + TEST_ASSERT_TRUE(bool(Interface<4>::pollLink())); + TEST_ASSERT_EQUALS(FakePhy<4>::linkReadCalls, 4u); + TEST_ASSERT_EQUALS(FakeMac<4>::linkUpdateCalls, 3u); + TEST_ASSERT_FALSE(netif_is_link_up(Interface<4>::netif())); + + timerExpired = false; + { + modm::lwip::LwIPSingleThreadGuard guard; + sys_timeout(0, expireTimer, nullptr); + } + modm::lwip::processTimeouts(); + TEST_ASSERT_TRUE(timerExpired); +} + +void +LwipEthernetTest::testInitializationFailure() +{ + resetFakeEthernet<5>(); + FakeMac<5>::initializeError = FakeMac<5>::InitializationError::Failed; + const auto initialCount = netifCount(); + const auto result = Interface<5>::initialize(ConfigA); + TEST_ASSERT_FALSE(bool(result)); + TEST_ASSERT_TRUE(result.macError == FakeMac<5>::InitializationError::Failed); + TEST_ASSERT_TRUE(result.adapterError == Interface<5>::AdapterInitializationError::None); + TEST_ASSERT_EQUALS(FakeMac<5>::initializeCalls, 1u); + TEST_ASSERT_EQUALS(netifCount(), initialCount); + TEST_ASSERT_FALSE(containsNetif(Interface<5>::netif())); +} + +void +LwipEthernetTest::testSingleThreadGuard() +{ + static_assert(not std::is_copy_constructible_v); + static_assert(not std::is_move_constructible_v); + modm::lwip::LwIPSingleThreadGuard guard; + LWIP_ASSERT_CORE_LOCKED(); +#if defined(__unix__) + const auto child = fork(); + if (child == 0) { + modm::lwip::LwIPSingleThreadGuard nested; + _exit(0); + } + int status = 0; + TEST_ASSERT_TRUE(child > 0); + TEST_ASSERT_EQUALS(waitpid(child, &status, 0), child); + TEST_ASSERT_TRUE(WIFSIGNALED(status)); + TEST_ASSERT_EQUALS(WTERMSIG(status), SIGABRT); +#endif + TEST_ASSERT_TRUE(true); +} diff --git a/test/modm/lwip/lwip_ethernet_test.hpp b/test/modm/lwip/lwip_ethernet_test.hpp new file mode 100644 index 0000000000..64caa12e80 --- /dev/null +++ b/test/modm/lwip/lwip_ethernet_test.hpp @@ -0,0 +1,31 @@ +/* + * 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 + +class LwipEthernetTest : public unittest::TestSuite +{ +public: + void testInitializationRequired(); + void testInitializationAndRouting(); + void testPhysicalMacBinding(); + void testFailedBindingCanBeReplaced(); + void testReinitializationAndRecovery(); + void testConfigurationAndLeaseContract(); + void testTransmitPath(); + void testReceivePath(); + void testReceiveStatisticsAndChecksums(); + void testRealUdpChecksumAndReassembly(); + void testOutgoingSoftwareFragmentation(); + void testLinkPollingAndTimers(); + void testInitializationFailure(); + void testSingleThreadGuard(); +}; diff --git a/test/modm/lwip/module.lb b/test/modm/lwip/module.lb new file mode 100644 index 0000000000..a5a45ed479 --- /dev/null +++ b/test/modm/lwip/module.lb @@ -0,0 +1,29 @@ +#!/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 = ":test:lwip" + module.description = "Tests for the lwIP Ethernet adapter" + + +def prepare(module, options): + if options[":target"].identifier.platform != "hosted": + return False + + module.depends(":architecture:ethernet", ":lwip", ":mock:clock") + return True + + +def build(env): + env.outbasepath = "modm-test/src/modm-test/lwip" + env.copy("lwip_ethernet_test.hpp") + env.copy("lwip_ethernet_test.cpp") diff --git a/test/modm/platform/eth/stm32h7/README.md b/test/modm/platform/eth/stm32h7/README.md new file mode 100644 index 0000000000..b42199a351 --- /dev/null +++ b/test/modm/platform/eth/stm32h7/README.md @@ -0,0 +1,26 @@ +# STM32H7 Ethernet Tests + +These tests exercise the MAC/DMA, PHY, frame APIs, descriptor rings, checksum +handling, malformed traffic, and recovery. Compile checks cover the MII and RMII +APIs. Hardware tests run on a NUCLEO-H753ZI using RMII. + +Hardware tests use LAN8742A near end loopback. This exercises the PHY and RMII +data path rather than the MAC internal loopback. + +Disconnect the RJ45 cable before running the hardware suite. Teardown disables +loopback and restores auto negotiation. + +Traffic tests cover 10/full and 100/full. Half duplex modes only test +configuration because LAN8742A RMII loopback is not reliable in those modes. +Active link transitions require an external link partner. + +From the repository root: + +```sh +cd test +make compile-nucleo-h753zi-eth +make run-nucleo-h753zi-eth +``` + +The run target programs the board and reports results through its default serial +connection. diff --git a/test/modm/platform/eth/stm32h7/ethernet_compile_test.cpp b/test/modm/platform/eth/stm32h7/ethernet_compile_test.cpp new file mode 100644 index 0000000000..adb2e8246b --- /dev/null +++ b/test/modm/platform/eth/stm32h7/ethernet_compile_test.cpp @@ -0,0 +1,122 @@ +/* + * 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 + +namespace +{ + +using Mac = modm::platform::EthernetMac; +using Phy = modm::Lan8742a<7>; +using DriverState = modm::platform::detail::EthH7MacState::DriverState; + +static_assert(modm::ethernet::Clause22Mdio); +static_assert(requires { + { Phy::initialize() } -> std::same_as; + { Phy::readLinkStatus() } -> std::same_as; +}); +static_assert(Mac::LinkUpdateError::OutstandingLease != Mac::LinkUpdateError::None); +static_assert(std::same_as); + +constexpr bool +isPhyMode(uint16_t hcd, Mac::Speed speed, Mac::DuplexMode duplex) +{ + const auto mode = Phy::decodeSpeedIndication(hcd); + return mode and mode->speed == speed and mode->duplex == duplex; +} + +static_assert(isPhyMode(Phy::SpeedIndication_t(Phy::SpeedIndication::Base10THalfDuplex).value, + Mac::Speed::Speed10M, Mac::DuplexMode::Half)); +static_assert(isPhyMode(Phy::SpeedIndication_t(Phy::SpeedIndication::Base10TFullDuplex).value, + Mac::Speed::Speed10M, Mac::DuplexMode::Full)); +static_assert(isPhyMode(Phy::SpeedIndication_t(Phy::SpeedIndication::Base100TxHalfDuplex).value, + Mac::Speed::Speed100M, Mac::DuplexMode::Half)); +static_assert(isPhyMode(Phy::SpeedIndication_t(Phy::SpeedIndication::Base100TxFullDuplex).value, + Mac::Speed::Speed100M, Mac::DuplexMode::Full)); +static_assert(not Phy::decodeSpeedIndication(0x00)); +static_assert(not Phy::decodeSpeedIndication(0x0c)); +static_assert(not Phy::decodeSpeedIndication(0x10)); +static_assert(not Phy::decodeSpeedIndication(0x1c)); +static_assert(uint16_t(Phy::BasicControl::SoftReset) == modm::Bit15); +static_assert(uint16_t(Phy::BasicControl::RestartAutoNegotiation) == modm::Bit9); +static_assert(uint16_t(Phy::BasicStatus::AutoNegotiationComplete) == modm::Bit5); +static_assert(uint16_t(Phy::AutoNegotiationAdvertisement::Base10THalfDuplex) == + modm::Bit5); +static_assert(uint16_t(Phy::AutoNegotiationAdvertisement::Base100TxHalfDuplex) == + modm::Bit7); +static_assert(uint16_t(Phy::AutoNegotiationAdvertisement::SymmetricPause) == modm::Bit10); +static_assert(uint16_t(Phy::AutoNegotiationAdvertisement::AsymmetricPause) == modm::Bit11); + +static_assert(modm::platform::detail::needsRmii10MWorkaround( + Mac::MediaInterface::RMII, Mac::Speed::Speed10M)); +static_assert(not modm::platform::detail::needsRmii10MWorkaround( + Mac::MediaInterface::RMII, Mac::Speed::Speed100M)); +static_assert(not modm::platform::detail::needsRmii10MWorkaround( + Mac::MediaInterface::MII, Mac::Speed::Speed10M)); +static_assert(not modm::platform::detail::needsRmii10MWorkaround( + Mac::MediaInterface::MII, Mac::Speed::Speed100M)); + +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 FirstAndLastDescriptor = modm::Bit29 | modm::Bit28; +static_assert(modm::platform::detail::isExactRmii10MDribbleCrcError( + ErrorSummary | DribbleError | CrcError)); +static_assert(modm::platform::detail::isExactRmii10MDribbleCrcError( + ErrorSummary | DribbleError | CrcError | FirstAndLastDescriptor)); +static_assert(not modm::platform::detail::isExactRmii10MDribbleCrcError( + DribbleError | CrcError)); +static_assert(not modm::platform::detail::isExactRmii10MDribbleCrcError( + ErrorSummary | CrcError)); +static_assert(not modm::platform::detail::isExactRmii10MDribbleCrcError( + ErrorSummary | DribbleError)); +static_assert(not modm::platform::detail::isExactRmii10MDribbleCrcError( + ErrorSummary | DribbleError | CrcError | ReceiveError)); +static_assert(not modm::platform::detail::isExactRmii10MDribbleCrcError( + ErrorSummary | DribbleError | CrcError | OverflowError)); +static_assert(not modm::platform::detail::isExactRmii10MDribbleCrcError( + ErrorSummary | DribbleError | CrcError | ReceiveWatchdog)); +static_assert(not modm::platform::detail::isExactRmii10MDribbleCrcError( + ErrorSummary | DribbleError | CrcError | GiantPacket)); + +static_assert(modm::platform::detail::canCommitLinkUpState(DriverState::Uninitialized)); +static_assert(modm::platform::detail::canCommitLinkUpState(DriverState::Ready)); +static_assert(modm::platform::detail::canCommitLinkUpState(DriverState::Running)); +static_assert(not modm::platform::detail::canCommitLinkUpState(DriverState::Stopping)); +static_assert(not modm::platform::detail::canCommitLinkUpState(DriverState::Faulted)); + +static_assert(modm::platform::detail::needsFullToHalfDuplexFlush( + Mac::DuplexMode::Full, Mac::DuplexMode::Half)); +static_assert(not modm::platform::detail::needsFullToHalfDuplexFlush( + Mac::DuplexMode::Full, Mac::DuplexMode::Full)); +static_assert(not modm::platform::detail::needsFullToHalfDuplexFlush( + Mac::DuplexMode::Half, Mac::DuplexMode::Full)); +static_assert(not modm::platform::detail::needsFullToHalfDuplexFlush( + Mac::DuplexMode::Half, Mac::DuplexMode::Half)); + +[[maybe_unused]] Mac::InitializationResult +initializeMii() +{ + const Mac::Configuration configuration{ + .checksumMode = Mac::ChecksumMode::Software, + }; + return Mac::initialize(configuration); +} + +} // namespace diff --git a/test/modm/platform/eth/stm32h7/ethernet_hardware_test.cpp.in b/test/modm/platform/eth/stm32h7/ethernet_hardware_test.cpp.in new file mode 100644 index 0000000000..c14af3e4a2 --- /dev/null +++ b/test/modm/platform/eth/stm32h7/ethernet_hardware_test.cpp.in @@ -0,0 +1,1254 @@ +/* + * 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 "ethernet_hardware_test.hpp" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +using namespace modm::platform; + +namespace +{ + +constexpr uint8_t MissingPhyAddress = Board::eth::PhyAddress == 31 ? 30 : 31; +using Eth = EthernetMac; +using Port = EthernetMac; +using Phy = modm::Lan8742a; +using MissingPhy = modm::Lan8742a; + +constexpr std::array MacAddress{ {0x02, 0, 0, 0, 0, 0} }; +constexpr std::array PeerAddress{ {0x02, 0, 0, 0, 0, 1} }; +constexpr uint16_t TestEtherType = 0x88b5; +constexpr uint16_t EtherTypeIpv4 = 0x0800; +constexpr uint16_t EtherTypeArp = 0x0806; +constexpr uint16_t PhyIdentifier1 = Phy::PhyIdentifier1; +constexpr uint16_t PhyIdentifier2 = Phy::PhyIdentifier2; +constexpr uint16_t PhyIdentifier2Mask = Phy::PhyIdentifier2Mask; +constexpr uint16_t PhyLoopback = uint16_t(Phy::BasicControl::Loopback); +constexpr uint16_t PhySpeed100 = uint16_t(Phy::BasicControl::SpeedSelect); +constexpr uint16_t PhyFullDuplex = uint16_t(Phy::BasicControl::DuplexMode); +constexpr auto FrameTimeout = std::chrono::milliseconds{250}; +constexpr auto PhyModeTimeout = std::chrono::seconds{1}; +constexpr auto PhyResetTimeout = std::chrono::milliseconds{550}; + +using Frame = std::array; + +template +Eth::MdioError +readPhyRegister(Register reg, uint16_t& value) +{ + return Eth::readPhyRegister(Board::eth::PhyAddress, uint8_t(reg), value); +} + +template +Eth::MdioError +writePhyRegister(Register reg, uint16_t value) +{ + return Eth::writePhyRegister(Board::eth::PhyAddress, uint8_t(reg), value); +} + +struct LinkSynchronizationResult +{ + Port::LinkStatus status{}; + Eth::MdioError mdioError{Eth::MdioError::None}; + Port::LinkUpdateError macError{Port::LinkUpdateError::None}; + + explicit operator bool() const + { + return mdioError == Eth::MdioError::None and + macError == Port::LinkUpdateError::None; + } +}; + +LinkSynchronizationResult +synchronizeLink() +{ + const auto observed = Phy::readLinkStatus(); + if (not observed) + return {Port::getLinkStatus(), observed.error, {}}; + const auto updated = Port::notifyUpdatedLinkStatus(observed.status); + return {updated.status, {}, updated.error}; +} + +void +write16(uint8_t* data, uint16_t value) +{ + data[0] = uint8_t(value >> 8); + data[1] = uint8_t(value); +} + +void +write32(uint8_t* data, uint32_t value) +{ + data[0] = uint8_t(value >> 24); + data[1] = uint8_t(value >> 16); + data[2] = uint8_t(value >> 8); + data[3] = uint8_t(value); +} + +uint32_t +checksumSum(std::span data, uint32_t sum = 0) +{ + std::size_t index = 0; + for (; index + 1 < data.size(); index += 2) + sum += (uint32_t(data[index]) << 8) | data[index + 1]; + if (index < data.size()) + sum += uint32_t(data[index]) << 8; + while (sum >> 16) + sum = (sum & 0xffff) + (sum >> 16); + return sum; +} + +uint16_t +checksum(std::span data, uint32_t sum = 0) +{ + return uint16_t(~checksumSum(data, sum)); +} + +uint32_t +pseudoHeaderSum(const uint8_t* ip, uint8_t protocol, uint16_t length) +{ + uint32_t sum = checksumSum({ip + 12, 8}); + sum += protocol; + sum += length; + return sum; +} + +void +fillEthernetHeader(Frame& frame, uint16_t etherType) +{ + std::copy(MacAddress.begin(), MacAddress.end(), frame.begin()); + std::copy(PeerAddress.begin(), PeerAddress.end(), frame.begin() + 6); + write16(frame.data() + 12, etherType); +} + +void +fillTestFrame(Frame& frame, std::size_t length, uint32_t sequence) +{ + frame.fill(0); + fillEthernetHeader(frame, TestEtherType); + write32(frame.data() + 14, sequence); + write16(frame.data() + 18, uint16_t(length)); + for (std::size_t index = 20; index < length; ++index) + frame[index] = uint8_t((index + sequence * 17) % 251); +} + +void +fillIpv4Header(Frame& frame, uint8_t protocol, uint16_t payloadLength, bool softwareChecksum) +{ + auto* ip = frame.data() + 14; + ip[0] = 0x45; + ip[1] = 0; + write16(ip + 2, uint16_t(20 + payloadLength)); + write16(ip + 4, 0x1234); + write16(ip + 6, 0x4000); + ip[8] = 64; + ip[9] = protocol; + write16(ip + 10, 0); + ip[12] = 10; ip[13] = 0; ip[14] = 0; ip[15] = 1; + ip[16] = 10; ip[17] = 0; ip[18] = 0; ip[19] = 2; + if (softwareChecksum) + write16(ip + 10, checksum({ip, 20})); +} + +std::size_t +makeArp(Frame& frame) +{ + constexpr std::size_t length = 60; + frame.fill(0); + fillEthernetHeader(frame, EtherTypeArp); + auto* arp = frame.data() + 14; + write16(arp, 1); + write16(arp + 2, EtherTypeIpv4); + arp[4] = 6; + arp[5] = 4; + write16(arp + 6, 1); + std::copy(PeerAddress.begin(), PeerAddress.end(), arp + 8); + arp[14] = 10; arp[15] = 0; arp[16] = 0; arp[17] = 1; + std::copy(MacAddress.begin(), MacAddress.end(), arp + 18); + arp[24] = 10; arp[25] = 0; arp[26] = 0; arp[27] = 2; + return length; +} + +std::size_t +makeIcmp(Frame& frame) +{ + constexpr uint16_t icmpLength = 26; + constexpr std::size_t frameLength = 60; + frame.fill(0); + fillEthernetHeader(frame, EtherTypeIpv4); + fillIpv4Header(frame, 1, icmpLength, true); + auto* icmp = frame.data() + 34; + icmp[0] = 8; + icmp[1] = 0; + write16(icmp + 2, 0); + write16(icmp + 4, 0x1234); + write16(icmp + 6, 1); + for (std::size_t index = 8; index < icmpLength; ++index) + icmp[index] = uint8_t(index); + write16(icmp + 2, checksum({icmp, icmpLength})); + return frameLength; +} + +std::size_t +makeUdp(Frame& frame, bool softwareChecksum, bool malformed = false) +{ + constexpr uint16_t udpLength = 26; + constexpr std::size_t frameLength = 60; + frame.fill(0); + fillEthernetHeader(frame, EtherTypeIpv4); + fillIpv4Header(frame, 17, udpLength, softwareChecksum or malformed); + auto* ip = frame.data() + 14; + auto* udp = frame.data() + 34; + write16(udp, 5000); + write16(udp + 2, 5001); + write16(udp + 4, udpLength); + write16(udp + 6, 0); + for (std::size_t index = 8; index < udpLength; ++index) + udp[index] = uint8_t(index + 31); + if (softwareChecksum) + write16(udp + 6, checksum({udp, udpLength}, pseudoHeaderSum(ip, 17, udpLength))); + else if (malformed) + write16(udp + 6, 0x1234); + return frameLength; +} + +void +makeUdpFragments(Frame& first, Frame& second) +{ + constexpr uint16_t udpLength = 26; + constexpr uint16_t firstPayloadLength = 16; + constexpr uint16_t secondPayloadLength = udpLength - firstPayloadLength; + Frame datagram{}; + (void) makeUdp(datagram, true); + + first.fill(0); + fillEthernetHeader(first, EtherTypeIpv4); + fillIpv4Header(first, 17, firstPayloadLength, false); + auto* firstIp = first.data() + 14; + write16(firstIp + 6, 0x2000); + std::copy_n(datagram.begin() + 34, firstPayloadLength, first.begin() + 34); + write16(firstIp + 10, checksum({firstIp, 20})); + + second.fill(0); + fillEthernetHeader(second, EtherTypeIpv4); + fillIpv4Header(second, 17, secondPayloadLength, false); + auto* secondIp = second.data() + 14; + write16(secondIp + 6, firstPayloadLength / 8); + std::copy_n(datagram.begin() + 34 + firstPayloadLength, + secondPayloadLength, second.begin() + 34); + write16(secondIp + 10, checksum({secondIp, 20})); +} + +bool +validFragmentedUdpChecksum(const Frame& first, const Frame& second) +{ + constexpr uint16_t udpLength = 26; + constexpr uint16_t firstPayloadLength = 16; + std::array udp{}; + std::copy_n(first.begin() + 34, firstPayloadLength, udp.begin()); + std::copy_n(second.begin() + 34, udpLength - firstPayloadLength, + udp.begin() + firstPayloadLength); + return checksum(udp, pseudoHeaderSum(first.data() + 14, 17, udpLength)) == 0; +} + +std::size_t +makeTcp(Frame& frame, bool softwareChecksum, bool malformed = false) +{ + constexpr uint16_t tcpLength = 26; + constexpr std::size_t frameLength = 60; + frame.fill(0); + fillEthernetHeader(frame, EtherTypeIpv4); + fillIpv4Header(frame, 6, tcpLength, softwareChecksum or malformed); + auto* ip = frame.data() + 14; + auto* tcp = frame.data() + 34; + write16(tcp, 5000); + write16(tcp + 2, 5001); + write32(tcp + 4, 0x12345678); + write32(tcp + 8, 0x01020304); + tcp[12] = 0x50; + tcp[13] = 0x18; + write16(tcp + 14, 4096); + write16(tcp + 16, 0); + write16(tcp + 18, 0); + for (std::size_t index = 20; index < tcpLength; ++index) + tcp[index] = uint8_t(index + 47); + if (softwareChecksum) + write16(tcp + 16, checksum({tcp, tcpLength}, pseudoHeaderSum(ip, 6, tcpLength))); + else if (malformed) + write16(tcp + 16, 0x1234); + return frameLength; +} + +bool +validIpv4Checksum(const Frame& frame) +{ + return checksum({frame.data() + 14, 20}) == 0; +} + +bool +validTransportChecksum(const Frame& frame, uint8_t protocol, uint16_t length) +{ + const auto* ip = frame.data() + 14; + return checksum({frame.data() + 34, length}, pseudoHeaderSum(ip, protocol, length)) == 0; +} + +void +connectPins() +{ + Port::connect(); +} + +bool +resetPhyAndWait() +{ + // LAN8742A DS00001989A section 3.8.6.2 specifies up to 500 ms for a + // software reset; the fixture adds 50 ms for polling and scheduling. + constexpr uint16_t Reset = uint16_t(Phy::BasicControl::SoftReset); + if (writePhyRegister(Phy::Register::BasicControl, Reset) != Eth::MdioError::None) + return false; + + const auto start = modm::Clock::now(); + while (modm::Clock::now() - start < PhyResetTimeout) { + uint16_t control = 0; + if (readPhyRegister(Phy::Register::BasicControl, control) != Eth::MdioError::None) + return false; + if ((control & Reset) == 0) + return true; + modm::delay_ms(1); + } + return false; +} + +bool +writeLoopbackMode(Port::Speed speed, Port::DuplexMode duplex) +{ + // LAN8742A DS00001989A sections 3.8.10.1 and 4.2.1: BMCR Loopback + // selects near-end loopback; forced speed and duplex select its test mode. + uint16_t control = PhyLoopback; + if (speed == Port::Speed::Speed100M) + control |= PhySpeed100; + if (duplex == Port::DuplexMode::Full) + control |= PhyFullDuplex; + return writePhyRegister(Phy::Register::BasicControl, control) == + Eth::MdioError::None; +} + +bool +waitForLoopbackMode(Port::Speed speed, Port::DuplexMode duplex) +{ + const Port::LinkMode expected{speed, duplex}; + const auto start = modm::Clock::now(); + LinkSynchronizationResult last{}; + while (modm::Clock::now() - start < PhyModeTimeout) { + last = synchronizeLink(); + if (not last) { + MODM_LOG_ERROR << "Ethernet link service error=" + << ((uint32_t(last.mdioError) << 8) | uint32_t(last.macError)) + << " DMACSR=" << modm::hex + << uint32_t(ETH->DMACSR) << " DMADSR=" << uint32_t(ETH->DMADSR) + << " MTLTQDR=" << uint32_t(ETH->MTLTQDR) + << " MTLRQDR=" << uint32_t(ETH->MTLRQDR) + << " MACDR=" << uint32_t(ETH->MACDR) << modm::endl; + return false; + } + if (last.status.state == Port::LinkState::Up and last.status.mode == expected) + return true; + modm::delay_ms(1); + } + MODM_LOG_ERROR << "Ethernet PHY mode timeout state=" + << uint32_t(last.status.state) << " DMACSR=" << modm::hex + << uint32_t(ETH->DMACSR) << " DMADSR=" << uint32_t(ETH->DMADSR) + << " MTLTQDR=" << uint32_t(ETH->MTLTQDR) + << " MTLRQDR=" << uint32_t(ETH->MTLRQDR) + << " MACDR=" << uint32_t(ETH->MACDR) << modm::endl; + return false; +} + +bool +setLoopbackMode(Port::Speed speed, Port::DuplexMode duplex) +{ + const Port::LinkMode expected{speed, duplex}; + const auto current = Port::getLinkStatus(); + if (current.state == Port::LinkState::Up and current.mode == expected) + return true; + return writeLoopbackMode(speed, duplex) and + waitForLoopbackMode(speed, duplex); +} + +bool +disableLoopbackAndWaitForLinkDown() +{ + // LAN8742A DS00001989A section 4.2.1: clear Loopback and return BMCR to + // normal auto-negotiation. The RJ45 must be disconnected so BSR Link Status + // clears and the next link update performs the normal MAC/DMA stop transition. + constexpr uint16_t normalOperation = + uint16_t(Phy::BasicControl::AutoNegotiationEnable) | + uint16_t(Phy::BasicControl::RestartAutoNegotiation); + if (writePhyRegister(Phy::Register::BasicControl, normalOperation) != + Eth::MdioError::None) + return false; + + const auto start = modm::Clock::now(); + while (modm::Clock::now() - start < PhyModeTimeout) { + const auto link = synchronizeLink(); + if (not link) + return false; + if (link.status.state == Port::LinkState::Down) + return true; + modm::delay_ms(1); + } + return false; +} + +bool +initializeLoopback(bool checksumOffload) +{ + connectPins(); + // LAN8742A DS00001989A section 3.4.1.2 and RM0433 Rev 8 section 58.6.3 + // define the 50 MHz RMII interface used by this board and fixture. + const Port::Configuration configuration{ + .macAddress = MacAddress, + .checksumMode = checksumOffload ? + Port::ChecksumMode::Hardware : Port::ChecksumMode::Software, + }; + if (not Port::initialize(configuration)) + return false; + if (not Phy::initialize()) + return false; + if (not setLoopbackMode(Port::Speed::Speed100M, Port::DuplexMode::Full)) + return false; + Port::resetErrorCounters(); + (void) Port::consumeWakeupEvents(); + return true; +} + +Port::ReceiveBufferLease +waitForReceiveBuffer() +{ + const auto start = modm::Clock::now(); + while (modm::Clock::now() - start < FrameTimeout) { + auto frame = Port::tryAcquireReceiveBuffer(); + if (frame) + return frame; + if (frame.error() != Port::ReceiveError::NoFrameAvailable) + return frame; + modm::delay_us(50); + } + return {}; +} + +std::size_t +receiveCopy(Frame& frame, Port::ReceiveChecksumStatus* checksumStatus = nullptr) +{ + auto packet = waitForReceiveBuffer(); + if (not packet or packet.buffer().size() > frame.size()) + return 0; + if (checksumStatus != nullptr) + *checksumStatus = packet.checksumStatus(); + std::copy(packet.buffer().begin(), packet.buffer().end(), frame.begin()); + const auto length = packet.buffer().size(); + packet.release(); + return length; +} + +Port::TransmitResult +transmitEventually(std::span frame) +{ + const auto start = modm::Clock::now(); + while (modm::Clock::now() - start < FrameTimeout) { + const auto result = Port::transmit(frame); + if (result or result.error != Port::TransmitError::Busy) + return result; + modm::delay_us(50); + } + return {Port::TransmitError::Busy}; +} + +bool +drainReceive() +{ + auto quietSince = modm::Clock::now(); + const auto start = quietSince; + while (modm::Clock::now() - start < FrameTimeout) { + auto packet = Port::tryAcquireReceiveBuffer(); + if (packet) { + packet.release(); + quietSince = modm::Clock::now(); + } + else if (packet.error() != Port::ReceiveError::NoFrameAvailable) { + return false; + } + else if (modm::Clock::now() - quietSince >= std::chrono::milliseconds{10}) { + return true; + } + modm::delay_us(50); + } + return false; +} + +bool +roundTrip(std::span expected, Frame& received) +{ + if (transmitEventually(expected).error != Port::TransmitError::None) + return false; + const auto length = receiveCopy(received); + return length == expected.size() and + std::equal(expected.begin(), expected.end(), received.begin()); +} + +} // namespace + +// ---------------------------------------------------------------------------- +// Initialization and PHY + +void +EthernetHardwareTest::setUp() +{ + ready = initializeLoopback(false); + if (not ready) + TEST_FAIL("MAC, DMA, MDIO, or LAN8742A near-end loopback setup failed"); +} + +void +EthernetHardwareTest::tearDown() +{ + if (not ready) + return; + if (not drainReceive()) { + TEST_FAIL("LAN8742A loopback receive drain failed"); + } + const bool linkDown = disableLoopbackAndWaitForLinkDown(); + if (not linkDown) { + TEST_FAIL("LAN8742A loopback-off LinkDown teardown failed"); + } + else if (not resetPhyAndWait()) { + TEST_FAIL("LAN8742A software reset teardown failed"); + } + ready = false; +} + +void +EthernetHardwareTest::testInitializationAndPhyIdentity() +{ + if (not ready) { + TEST_FAIL("MAC, DMA, MDIO, or PHY initialization failed"); + return; + } + uint16_t identifier1 = 0; + uint16_t identifier2 = 0; + TEST_ASSERT_TRUE(readPhyRegister(Phy::Register::PhyIdentifier1, identifier1) == + Eth::MdioError::None); + TEST_ASSERT_TRUE(readPhyRegister(Phy::Register::PhyIdentifier2, identifier2) == + Eth::MdioError::None); + TEST_ASSERT_EQUALS(identifier1, PhyIdentifier1); + TEST_ASSERT_EQUALS(identifier2 & PhyIdentifier2Mask, PhyIdentifier2); + uint16_t invalidRegisterValue = 0; + TEST_ASSERT_TRUE(readPhyRegister(32, invalidRegisterValue) == + Eth::MdioError::InvalidRegister); + TEST_ASSERT_TRUE(writePhyRegister(32, 0) == Eth::MdioError::InvalidRegister); + TEST_ASSERT_TRUE(Eth::readPhyRegister(32, uint8_t(Phy::Register::PhyIdentifier1), + invalidRegisterValue) == Eth::MdioError::InvalidPhyAddress); + TEST_ASSERT_TRUE(Eth::writePhyRegister(32, uint8_t(Phy::Register::BasicControl), 0) == + Eth::MdioError::InvalidPhyAddress); + TEST_ASSERT_TRUE(readPhyRegister(Phy::Register::PhyIdentifier1, identifier1) == + Eth::MdioError::None); + const auto link = synchronizeLink(); + if (not link) { + TEST_FAIL("link service failed after initialization"); + return; + } + TEST_ASSERT_TRUE(link.status.state == Port::LinkState::Up); + if (not link.status.mode) { + TEST_FAIL("initialized link has no resolved mode"); + return; + } + TEST_ASSERT_TRUE(link.status.mode->speed == Port::Speed::Speed100M); + TEST_ASSERT_TRUE(link.status.mode->duplex == Port::DuplexMode::Full); + TEST_ASSERT_TRUE((ETH->MACHWF0R & (ETH_MACHWF0R_RXCOESEL | ETH_MACHWF0R_TXCOESEL)) == + (ETH_MACHWF0R_RXCOESEL | ETH_MACHWF0R_TXCOESEL)); +} + +void +EthernetHardwareTest::testInvalidPhyAddressRecovery() +{ + if (not ready) { + TEST_FAIL("loopback setup failed"); + return; + } + if (not disableLoopbackAndWaitForLinkDown()) { + TEST_FAIL("LinkDown before invalid PHY address test failed"); + return; + } + const auto missing = MissingPhy::initialize(); + TEST_ASSERT_FALSE(missing); + TEST_ASSERT_TRUE(missing.error == MissingPhy::InitializationError::NotFound); + if (not initializeLoopback(false)) { + TEST_FAIL("loopback recovery after invalid PHY address failed"); + return; + } + Frame sent{}; + Frame received{}; + fillTestFrame(sent, 64, 1); + TEST_ASSERT_TRUE(roundTrip({sent.data(), 64}, received)); +} + +void +EthernetHardwareTest::testPhyResetRecovery() +{ + if (not ready) { + TEST_FAIL("loopback setup failed"); + return; + } + Frame sent{}; + Frame received{}; + fillTestFrame(sent, 64, 6); + TEST_ASSERT_TRUE(roundTrip({sent.data(), 64}, received)); + if (not disableLoopbackAndWaitForLinkDown()) { + TEST_FAIL("LinkDown before PHY reset failed"); + return; + } + if (not resetPhyAndWait()) { + TEST_FAIL("PHY reset failed"); + return; + } + if (not initializeLoopback(false)) { + TEST_FAIL("loopback recovery after PHY reset failed"); + return; + } + fillTestFrame(sent, 64, 7); + TEST_ASSERT_TRUE(roundTrip({sent.data(), 64}, received)); +} + +void +EthernetHardwareTest::testFullDuplexPhyModes() +{ + if (not ready) { + TEST_FAIL("loopback setup failed"); + return; + } + constexpr std::array modes{ { + {Port::Speed::Speed10M, Port::DuplexMode::Full}, + {Port::Speed::Speed100M, Port::DuplexMode::Full}, + } }; + Frame sent{}; + Frame received{}; + uint32_t sequence = 0x20; + + fillTestFrame(sent, 128, sequence++); + if (not roundTrip({sent.data(), 128}, received)) { + TEST_FAIL("initial 100/full PHY mode round-trip failed"); + return; + } + + for (const auto mode : modes) { + if (not setLoopbackMode(mode.speed, mode.duplex)) { + TEST_FAIL("full-duplex PHY mode transition failed"); + return; + } + const auto status = Port::getLinkStatus(); + TEST_ASSERT_TRUE(status.state == Port::LinkState::Up); + TEST_ASSERT_TRUE(status.mode == mode); + TEST_ASSERT_TRUE(((ETH->MACCR & ETH_MACCR_FES) != 0) == + (mode.speed == Port::Speed::Speed100M)); + TEST_ASSERT_TRUE(((ETH->MACCR & ETH_MACCR_DM) != 0) == + (mode.duplex == Port::DuplexMode::Full)); + TEST_ASSERT_TRUE(((ETH->MTLRQOMR & ETH_MTLRQOMR_FEP) != 0) == + (mode.speed == Port::Speed::Speed10M)); + TEST_ASSERT_TRUE((ETH->MTLTQOMR & ETH_MTLTQOMR_FTQ) == 0); + + fillTestFrame(sent, 128, sequence++); + if (not roundTrip({sent.data(), 128}, received)) { + TEST_FAIL("full-duplex PHY mode round-trip failed"); + return; + } + } +} + +void +EthernetHardwareTest::testHalfDuplexPhyModeConfiguration() +{ + if (not ready) { + TEST_FAIL("loopback setup failed"); + return; + } + // LAN8742A DS00001989A Table 2-1 excludes RMII receive loopback in 10BASE-T + // half duplex, while sections 3.2.4 and 3.8.7 make CRS depend on both + // transmit and receive activity in half duplex. Near-end loopback therefore + // cannot be a reliable half-duplex traffic oracle. + constexpr std::array speeds{ { + Port::Speed::Speed100M, + Port::Speed::Speed10M, + } }; + for (const auto speed : speeds) { + if (not setLoopbackMode(speed, Port::DuplexMode::Full)) { + TEST_FAIL("matching full-duplex PHY mode setup failed"); + return; + } + if (not setLoopbackMode(speed, Port::DuplexMode::Half)) { + TEST_FAIL("half-duplex PHY mode configuration failed"); + return; + } + const Port::LinkMode mode{speed, Port::DuplexMode::Half}; + const auto status = Port::getLinkStatus(); + TEST_ASSERT_TRUE(status.state == Port::LinkState::Up); + TEST_ASSERT_TRUE(status.mode == mode); + TEST_ASSERT_TRUE(((ETH->MACCR & ETH_MACCR_FES) != 0) == + (speed == Port::Speed::Speed100M)); + TEST_ASSERT_TRUE((ETH->MACCR & ETH_MACCR_DM) == 0); + TEST_ASSERT_TRUE(((ETH->MTLRQOMR & ETH_MTLRQOMR_FEP) != 0) == + (speed == Port::Speed::Speed10M)); + TEST_ASSERT_TRUE((ETH->MTLTQOMR & ETH_MTLTQOMR_FTQ) == 0); + if (not setLoopbackMode(speed, Port::DuplexMode::Full)) { + TEST_FAIL("matching full-duplex PHY mode restore failed"); + return; + } + } +} + +void +EthernetHardwareTest::testLinkTransitionWithOutstandingReceiveLease() +{ + if (not ready) { + TEST_FAIL("loopback setup failed"); + return; + } + Frame sent{}; + Frame received{}; + fillTestFrame(sent, 128, 0x40); + TEST_ASSERT_TRUE(transmitEventually({sent.data(), 128}).error == + Port::TransmitError::None); + auto retained = waitForReceiveBuffer(); + if (not retained) { + TEST_FAIL("receive lease acquisition failed"); + return; + } + TEST_ASSERT_EQUALS(retained.buffer().size(), 128u); + + const auto oldStatus = Port::getLinkStatus(); + const uint32_t oldMacControl = ETH->MACCR; + const uint32_t oldTxDmaControl = ETH->DMACTCR; + const uint32_t oldRxDmaControl = ETH->DMACRCR; + TEST_ASSERT_TRUE(writePhyRegister(Phy::Register::BasicControl, + PhyLoopback | PhyFullDuplex) == Eth::MdioError::None); + const auto blocked = synchronizeLink(); + TEST_ASSERT_FALSE(blocked); + TEST_ASSERT_TRUE(blocked.macError == Port::LinkUpdateError::OutstandingLease); + TEST_ASSERT_TRUE(blocked.status.state == oldStatus.state); + TEST_ASSERT_TRUE(blocked.status.mode == oldStatus.mode); + TEST_ASSERT_EQUALS(uint32_t(ETH->MACCR), oldMacControl); + TEST_ASSERT_EQUALS(uint32_t(ETH->DMACTCR), oldTxDmaControl); + TEST_ASSERT_EQUALS(uint32_t(ETH->DMACRCR), oldRxDmaControl); + + retained.release(); + if (not waitForLoopbackMode(Port::Speed::Speed10M, Port::DuplexMode::Full)) { + TEST_FAIL("10/full transition after receive lease release failed"); + return; + } + fillTestFrame(sent, 129, 0x41); + TEST_ASSERT_TRUE(roundTrip({sent.data(), 129}, received)); +} + +// ---------------------------------------------------------------------------- +// Copied transmit and buffer leases + +void +EthernetHardwareTest::testCopyAndLeaseApis() +{ + if (not ready) { + TEST_FAIL("loopback setup failed"); + return; + } + Frame sent{}; + Frame received{}; + fillTestFrame(sent, 64, 2); + auto emptyRx = Port::tryAcquireReceiveBuffer(); + TEST_ASSERT_FALSE(emptyRx); + TEST_ASSERT_TRUE(emptyRx.error() == Port::ReceiveError::NoFrameAvailable); + const auto invalidTransmit = Port::transmit({}); + TEST_ASSERT_FALSE(invalidTransmit); + TEST_ASSERT_TRUE(invalidTransmit.error == Port::TransmitError::InvalidLength); + auto invalidTx = Port::acquireTransmitBuffer(0); + TEST_ASSERT_FALSE(invalidTx); + TEST_ASSERT_TRUE(invalidTx.error() == Port::TransmitError::InvalidLength); + TEST_ASSERT_TRUE(roundTrip({sent.data(), 64}, received)); + + fillTestFrame(sent, 65, 3); + auto tx = Port::acquireTransmitBuffer(65); + if (not tx) { + TEST_FAIL("transmit lease acquisition failed"); + return; + } + TEST_ASSERT_EQUALS(tx.buffer().size(), 65u); + std::copy_n(sent.begin(), 65, tx.buffer().begin()); + if (not tx.commit()) { + TEST_FAIL("transmit lease commit failed"); + return; + } + auto rx = waitForReceiveBuffer(); + if (not rx) { + TEST_FAIL("receive lease acquisition failed"); + return; + } + TEST_ASSERT_EQUALS(rx.buffer().size(), 65u); + TEST_ASSERT_TRUE(std::equal(rx.buffer().begin(), rx.buffer().end(), sent.begin())); + rx.release(); + TEST_ASSERT_FALSE(Port::tryAcquireReceiveBuffer()); + + fillTestFrame(sent, 64, 4); + TEST_ASSERT_TRUE(transmitEventually({sent.data(), 64}).error == Port::TransmitError::None); + rx = waitForReceiveBuffer(); + if (not rx) { + TEST_FAIL("receive lease acquisition failed"); + return; + } + std::array tooSmall{}; + TEST_ASSERT_TRUE(rx.buffer().size() > tooSmall.size()); + rx.release(); + fillTestFrame(sent, 64, 5); + TEST_ASSERT_TRUE(roundTrip({sent.data(), 64}, received)); + + const auto events = Port::consumeWakeupEvents(); + TEST_ASSERT_TRUE((events & Port::WakeupEvent::Receive) == Port::WakeupEvent::Receive); + TEST_ASSERT_TRUE((events & Port::WakeupEvent::Transmit) == Port::WakeupEvent::Transmit); + const auto errors = Port::getErrorCounters(); + TEST_ASSERT_EQUALS(errors.fatalBusErrors, 0u); + TEST_ASSERT_EQUALS(errors.contextDescriptorErrors, 0u); + TEST_ASSERT_EQUALS(errors.txDescriptorErrors, 0u); +} + +void +EthernetHardwareTest::testTransmitLeaseStateTransitions() +{ + if (not ready) { + TEST_FAIL("loopback setup failed"); + return; + } + Frame sent{}; + Frame received{}; + fillTestFrame(sent, 64, 8); + + auto first = Port::acquireTransmitBuffer(64); + if (not first) { + TEST_FAIL("initial transmit lease acquisition failed"); + return; + } + auto busy = Port::acquireTransmitBuffer(64); + TEST_ASSERT_FALSE(busy); + TEST_ASSERT_TRUE(busy.error() == Port::TransmitError::Busy); + first.cancel(); + + auto committed = Port::acquireTransmitBuffer(64); + if (not committed) { + TEST_FAIL("committed transmit lease acquisition failed"); + return; + } + std::copy_n(sent.begin(), 64, committed.buffer().begin()); + if (not committed.commit()) { + TEST_FAIL("transmit lease commit failed"); + return; + } + TEST_ASSERT_EQUALS(receiveCopy(received), 64u); + TEST_ASSERT_TRUE(std::equal(sent.begin(), sent.begin() + 64, received.begin())); + + { + auto abandoned = Port::acquireTransmitBuffer(64); + TEST_ASSERT_TRUE(static_cast(abandoned)); + } + auto afterDestruction = Port::acquireTransmitBuffer(64); + if (not afterDestruction) { + TEST_FAIL("transmit lease recovery after destruction failed"); + return; + } + auto moved = std::move(afterDestruction); + TEST_ASSERT_FALSE(afterDestruction); + if (not moved) { + TEST_FAIL("transmit lease move failed"); + return; + } + moved.cancel(); + + fillTestFrame(sent, 64, 9); + TEST_ASSERT_TRUE(roundTrip({sent.data(), 64}, received)); +} + +void +EthernetHardwareTest::testFrameSizeAndCacheBoundaries() +{ + if (not ready) { + TEST_FAIL("loopback setup failed"); + return; + } + constexpr std::array sizes{ { + 60, 61, 63, 64, 65, 127, 128, 129, 511, 512, 513, 1514 + } }; + Frame sent{}; + Frame received{}; + uint32_t sequence = 10; + for (const auto size : sizes) { + fillTestFrame(sent, size, sequence++); + if (not roundTrip({sent.data(), size}, received)) + TEST_FAIL("frame boundary round-trip failed"); + } + auto oversizedLease = Port::acquireTransmitBuffer(1515); + TEST_ASSERT_FALSE(oversizedLease); + TEST_ASSERT_TRUE(oversizedLease.error() == Port::TransmitError::InvalidLength); + std::array oversizedFrame{}; + const auto oversizedTransmit = Port::transmit(oversizedFrame); + TEST_ASSERT_FALSE(oversizedTransmit); + TEST_ASSERT_TRUE(oversizedTransmit.error == Port::TransmitError::InvalidLength); +} + +// ---------------------------------------------------------------------------- +// Descriptor rings and recovery + +void +EthernetHardwareTest::testDescriptorRingWraparound() +{ + if (not ready) { + TEST_FAIL("loopback setup failed"); + return; + } + Frame sent{}; + Frame received{}; + constexpr std::size_t rounds = 4; + const auto count = rounds * (Port::RxDescriptorCount + Port::TxDescriptorCount); + for (std::size_t sequence = 0; sequence < count; ++sequence) { + const auto size = 60 + sequence % 257; + fillTestFrame(sent, size, uint32_t(sequence)); + if ((sequence & 1) == 0) { + if (transmitEventually({sent.data(), size}).error != Port::TransmitError::None) + TEST_FAIL("descriptor ring copied transmit failed"); + } + else { + auto tx = Port::acquireTransmitBuffer(size); + if (not tx) { + TEST_FAIL("descriptor ring transmit lease failed"); + return; + } + std::copy_n(sent.begin(), size, tx.buffer().begin()); + if (not tx.commit()) { + TEST_FAIL("descriptor ring transmit lease commit failed"); + return; + } + } + + auto rx = waitForReceiveBuffer(); + if (not rx) { + TEST_FAIL("descriptor ring receive lease failed"); + return; + } + auto moved = std::move(rx); + if (rx or moved.buffer().size() != size or + not std::equal(moved.buffer().begin(), moved.buffer().end(), sent.begin())) + TEST_FAIL("descriptor ring lease sequence mismatch"); + moved.release(); + } + + const auto burst = std::min(Port::RxDescriptorCount, Port::TxDescriptorCount); + for (std::size_t sequence = 0; sequence < burst; ++sequence) { + fillTestFrame(sent, 128, uint32_t(0x100 + sequence)); + if (transmitEventually({sent.data(), 128}).error != Port::TransmitError::None) + TEST_FAIL("ordered burst transmit failed"); + } + for (std::size_t sequence = 0; sequence < burst; ++sequence) { + fillTestFrame(sent, 128, uint32_t(0x100 + sequence)); + if (receiveCopy(received) != 128 or + not std::equal(sent.begin(), sent.begin() + 128, received.begin())) + TEST_FAIL("ordered burst receive mismatch"); + } +} + +void +EthernetHardwareTest::testTransmitDescriptorExhaustionRecovery() +{ + if (not ready) { + TEST_FAIL("loopback setup failed"); + return; + } + Port::resetErrorCounters(); + ETH->DMACTCR &= ~ETH_DMACTCR_ST; + Frame sent{}; + for (std::size_t sequence = 0; sequence < Port::TxDescriptorCount; ++sequence) { + fillTestFrame(sent, 64, uint32_t(0x200 + sequence)); + TEST_ASSERT_TRUE(static_cast(Port::transmit({sent.data(), 64}))); + } + auto unavailable = Port::acquireTransmitBuffer(64); + TEST_ASSERT_FALSE(unavailable); + TEST_ASSERT_TRUE(unavailable.error() == Port::TransmitError::Busy); + + if (not initializeLoopback(false)) { + TEST_FAIL("loopback recovery after descriptor exhaustion failed"); + return; + } + Frame received{}; + fillTestFrame(sent, 64, 0x210); + TEST_ASSERT_TRUE(roundTrip({sent.data(), 64}, received)); +} + +void +EthernetHardwareTest::testReceiveOwnershipPressureRecovery() +{ + if (not ready) { + TEST_FAIL("loopback setup failed"); + return; + } + Frame sent{}; + Frame received{}; + fillTestFrame(sent, 1514, 0x300); + TEST_ASSERT_TRUE(transmitEventually(sent).error == Port::TransmitError::None); + auto retained = waitForReceiveBuffer(); + if (not retained) { + TEST_FAIL("receive pressure lease acquisition failed"); + return; + } + TEST_ASSERT_EQUALS(retained.buffer().size(), 1514u); + + for (std::size_t sequence = 0; sequence < Port::RxDescriptorCount + 16; ++sequence) { + fillTestFrame(sent, 1514, uint32_t(0x301 + sequence)); + if (transmitEventually(sent).error != Port::TransmitError::None) + TEST_FAIL("traffic generation failed during RX pressure"); + } + modm::delay_ms(20); + TEST_ASSERT_EQUALS(retained.buffer().size(), 1514u); + const auto pressureErrors = Port::getErrorCounters(); + TEST_ASSERT_TRUE(pressureErrors.rxMissedPackets + pressureErrors.rxOverflowPackets > 0); + TEST_ASSERT_TRUE(pressureErrors.rxBufferUnavailable > 0); + const auto pressureEvents = Port::consumeWakeupEvents(); + TEST_ASSERT_TRUE((pressureEvents & Port::WakeupEvent::Receive) == Port::WakeupEvent::Receive); + retained.release(); + TEST_ASSERT_TRUE(drainReceive()); + + fillTestFrame(sent, 64, 0x400); + TEST_ASSERT_TRUE(roundTrip({sent.data(), 64}, received)); +} + +// ---------------------------------------------------------------------------- +// Protocols and checksums + +void +EthernetHardwareTest::testProtocolAndChecksumModes() +{ + if (not ready) { + TEST_FAIL("loopback setup failed"); + return; + } + Frame sent{}; + Frame received{}; + auto length = makeArp(sent); + TEST_ASSERT_TRUE(roundTrip({sent.data(), length}, received)); + length = makeIcmp(sent); + TEST_ASSERT_TRUE(roundTrip({sent.data(), length}, received)); + TEST_ASSERT_TRUE(validIpv4Checksum(received)); + TEST_ASSERT_TRUE(checksum({received.data() + 34, 26}) == 0); + + if (not initializeLoopback(true)) { + TEST_FAIL("hardware-checksum loopback initialization failed"); + return; + } + ETH->MACCR &= ~ETH_MACCR_IPC; + length = makeUdp(sent, false); + TEST_ASSERT_TRUE(transmitEventually({sent.data(), length}).error == Port::TransmitError::None); + TEST_ASSERT_EQUALS(receiveCopy(received), length); + TEST_ASSERT_TRUE(validIpv4Checksum(received)); + TEST_ASSERT_TRUE(validTransportChecksum(received, 17, 26)); + length = makeTcp(sent, false); + TEST_ASSERT_TRUE(transmitEventually({sent.data(), length}).error == Port::TransmitError::None); + TEST_ASSERT_EQUALS(receiveCopy(received), length); + TEST_ASSERT_TRUE(validIpv4Checksum(received)); + TEST_ASSERT_TRUE(validTransportChecksum(received, 6, 26)); + + if (not initializeLoopback(false)) { + TEST_FAIL("software-checksum loopback initialization failed"); + return; + } + length = makeUdp(sent, true); + TEST_ASSERT_TRUE(roundTrip({sent.data(), length}, received)); + TEST_ASSERT_TRUE(validTransportChecksum(received, 17, 26)); + length = makeTcp(sent, true); + TEST_ASSERT_TRUE(roundTrip({sent.data(), length}, received)); + TEST_ASSERT_TRUE(validTransportChecksum(received, 6, 26)); +} + +void +EthernetHardwareTest::testFragmentedUdpChecksumModes() +{ + if (not ready) { + TEST_FAIL("loopback setup failed"); + return; + } + if (not initializeLoopback(true)) { + TEST_FAIL("fragmented hardware-checksum loopback initialization failed"); + return; + } + + Frame first{}; + Frame second{}; + Frame receivedFirst{}; + Frame receivedSecond{}; + makeUdpFragments(first, second); + const auto firstHardware = Port::transmit({first.data(), 60}); + TEST_ASSERT_FALSE(firstHardware); + TEST_ASSERT_TRUE(firstHardware.error == Port::TransmitError::UnsupportedFragmentation); + const auto secondHardware = Port::transmit({second.data(), 60}); + TEST_ASSERT_FALSE(secondHardware); + TEST_ASSERT_TRUE(secondHardware.error == Port::TransmitError::UnsupportedFragmentation); + + if (not initializeLoopback(false)) { + TEST_FAIL("fragmented software-checksum loopback initialization failed"); + return; + } + Port::ReceiveChecksumStatus firstStatus{}; + Port::ReceiveChecksumStatus secondStatus{}; + TEST_ASSERT_TRUE(transmitEventually({first.data(), 60}).error == Port::TransmitError::None); + TEST_ASSERT_EQUALS(receiveCopy(receivedFirst, &firstStatus), 60u); + TEST_ASSERT_TRUE(transmitEventually({second.data(), 60}).error == Port::TransmitError::None); + TEST_ASSERT_EQUALS(receiveCopy(receivedSecond, &secondStatus), 60u); + TEST_ASSERT_TRUE(firstStatus == Port::ReceiveChecksumStatus::NotChecked); + TEST_ASSERT_TRUE(secondStatus == Port::ReceiveChecksumStatus::NotChecked); + TEST_ASSERT_TRUE(validIpv4Checksum(receivedFirst)); + TEST_ASSERT_TRUE(validIpv4Checksum(receivedSecond)); + TEST_ASSERT_TRUE(validFragmentedUdpChecksum(receivedFirst, receivedSecond)); +} + +void +EthernetHardwareTest::testMalformedChecksumRecovery() +{ + if (not ready) { + TEST_FAIL("loopback setup failed"); + return; + } + if (not initializeLoopback(true)) { + TEST_FAIL("malformed-checksum loopback initialization failed"); + return; + } + // Submit malformed checksums without TX insertion while MAC RX checksum + // classification remains enabled. + modm::platform::detail::ethH7MacState.checksumOffloadEnabled = false; + Frame sent{}; + auto length = makeUdp(sent, false, true); + TEST_ASSERT_TRUE(transmitEventually({sent.data(), length}).error == Port::TransmitError::None); + modm::platform::detail::ethH7MacState.checksumOffloadEnabled = true; + auto received = waitForReceiveBuffer(); + if (not received) { + TEST_FAIL("malformed UDP receive lease acquisition failed"); + return; + } + TEST_ASSERT_TRUE(received.checksumStatus() == Port::ReceiveChecksumStatus::Invalid); + received.release(); + length = makeTcp(sent, false, true); + modm::platform::detail::ethH7MacState.checksumOffloadEnabled = false; + TEST_ASSERT_TRUE(transmitEventually({sent.data(), length}).error == Port::TransmitError::None); + modm::platform::detail::ethH7MacState.checksumOffloadEnabled = true; + received = waitForReceiveBuffer(); + if (not received) { + TEST_FAIL("malformed TCP receive lease acquisition failed"); + return; + } + TEST_ASSERT_TRUE(received.checksumStatus() == Port::ReceiveChecksumStatus::Invalid); + received.release(); + + length = makeUdp(sent, true); + TEST_ASSERT_TRUE(transmitEventually({sent.data(), length}).error == Port::TransmitError::None); + received = waitForReceiveBuffer(); + if (not received) { + TEST_FAIL("valid UDP receive lease acquisition failed"); + return; + } + TEST_ASSERT_TRUE(received.checksumStatus() == Port::ReceiveChecksumStatus::Valid); + TEST_ASSERT_TRUE(checksum(received.buffer().subspan(34, 26), + pseudoHeaderSum(received.buffer().data() + 14, 17, 26)) == 0); + received.release(); +} + +// ---------------------------------------------------------------------------- +// MAC, DMA, and memory configuration + +void +EthernetHardwareTest::testReinitializationRecovery() +{ + if (not ready) { + TEST_FAIL("loopback setup failed"); + return; + } + Frame sent{}; + Frame received{}; + fillTestFrame(sent, 128, 0x500); + TEST_ASSERT_TRUE(roundTrip({sent.data(), 128}, received)); + if (not initializeLoopback(false)) { + TEST_FAIL("loopback reinitialization failed"); + return; + } + fillTestFrame(sent, 129, 0x501); + TEST_ASSERT_TRUE(roundTrip({sent.data(), 129}, received)); + TEST_ASSERT_FALSE(Port::tryAcquireReceiveBuffer()); +} + +void +EthernetHardwareTest::testHardwareErrorStatusReset() +{ + if (not ready) { + TEST_FAIL("loopback setup failed"); + return; + } + Port::resetHardwareErrorStatus(); + auto status = Port::getHardwareErrorStatus(); + TEST_ASSERT_EQUALS(status.dmaStatus, 0u); + TEST_ASSERT_EQUALS(status.txDescriptorStatus, 0u); + + detail::ethH7MacState.hardwareDmaErrorStatus |= ETH_DMACSR_RBU; + detail::ethH7MacState.hardwareTxDescriptorErrorStatus |= modm::Bit2; + status = Port::getHardwareErrorStatus(); + TEST_ASSERT_EQUALS(status.dmaStatus, uint32_t(ETH_DMACSR_RBU)); + TEST_ASSERT_EQUALS(status.txDescriptorStatus, uint32_t(modm::Bit2)); + + detail::ethH7MacState.hardwareDmaErrorStatus |= ETH_DMACSR_TBU; + status = Port::getHardwareErrorStatus(); + TEST_ASSERT_EQUALS(status.dmaStatus, uint32_t(ETH_DMACSR_RBU | ETH_DMACSR_TBU)); + TEST_ASSERT_EQUALS(status.txDescriptorStatus, uint32_t(modm::Bit2)); + + Port::resetHardwareErrorStatus(); + status = Port::getHardwareErrorStatus(); + TEST_ASSERT_EQUALS(status.dmaStatus, 0u); + TEST_ASSERT_EQUALS(status.txDescriptorStatus, 0u); +} + +void +EthernetHardwareTest::testDmaStoragePlacement() +{ + if (not ready) { + TEST_FAIL("loopback setup failed"); + return; + } + const auto address = reinterpret_cast(&modm::platform::detail::ethH7DmaStorage); + TEST_ASSERT_EQUALS(address % 32, 0u); + TEST_ASSERT_EQUALS(Port::MaxFrameSize, 1514u); + TEST_ASSERT_EQUALS(Port::RxDescriptorCount, 4u); + TEST_ASSERT_EQUALS(Port::TxDescriptorCount, 4u); +} diff --git a/test/modm/platform/eth/stm32h7/ethernet_hardware_test.hpp b/test/modm/platform/eth/stm32h7/ethernet_hardware_test.hpp new file mode 100644 index 0000000000..84277dae37 --- /dev/null +++ b/test/modm/platform/eth/stm32h7/ethernet_hardware_test.hpp @@ -0,0 +1,41 @@ +/* + * 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 + +class EthernetHardwareTest : public unittest::TestSuite +{ +public: + void setUp() override; + void tearDown() override; + + void testInitializationAndPhyIdentity(); + void testInvalidPhyAddressRecovery(); + void testPhyResetRecovery(); + void testFullDuplexPhyModes(); + void testHalfDuplexPhyModeConfiguration(); + void testLinkTransitionWithOutstandingReceiveLease(); + void testCopyAndLeaseApis(); + void testTransmitLeaseStateTransitions(); + void testFrameSizeAndCacheBoundaries(); + void testDescriptorRingWraparound(); + void testTransmitDescriptorExhaustionRecovery(); + void testReceiveOwnershipPressureRecovery(); + void testProtocolAndChecksumModes(); + void testFragmentedUdpChecksumModes(); + void testMalformedChecksumRecovery(); + void testReinitializationRecovery(); + void testHardwareErrorStatusReset(); + void testDmaStoragePlacement(); + +private: + bool ready{false}; +}; diff --git a/test/modm/platform/eth/stm32h7/module.lb b/test/modm/platform/eth/stm32h7/module.lb new file mode 100644 index 0000000000..1ed3f1d387 --- /dev/null +++ b/test/modm/platform/eth/stm32h7/module.lb @@ -0,0 +1,38 @@ +#!/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 = ":test:platform:eth.stm32h7" + module.description = "STM32H7 Ethernet compile coverage and LAN8742A hardware tests" + + +def prepare(module, options): + target = options[":target"] + if (target.identifier.platform != "stm32" or + target.identifier.family != "h7" or + not target.has_driver("eth:stm32*") or + target.get_driver("eth")["type"] != "stm32-v3.0"): + return False + + module.depends(":architecture:delay", ":driver:lan8742a", ":platform:eth", + ":mock:clock") + return True + + +def build(env): + env.outbasepath = "modm-test/src/modm-test/platform/eth/stm32h7" + env.copy("ethernet_compile_test.cpp") + hardware_board = (env[":target"].partname.startswith("stm32h753") and + env.has_module(":board:nucleo-h753zi")) + if hardware_board: + env.copy("ethernet_hardware_test.hpp") + env.template("ethernet_hardware_test.cpp.in", "ethernet_hardware_test.cpp")