diff --git a/docs/raspberry-pi.md b/docs/raspberry-pi.md index 064d69e..958fad3 100644 --- a/docs/raspberry-pi.md +++ b/docs/raspberry-pi.md @@ -103,6 +103,7 @@ PL011 on GPIO 14/15: header pins 8 (TX) and 10 (RX), ground on pin 6, 115200 | Bluetooth (CYW43455 on the 4B, CYW43456 on the 400) | `BCM2EA6` (`\_SB.GDV0.BTH0`) on the mini UART (`BCM2836`) | `Drivers/Bluetooth/Broadcom.cpp`, `Drivers/Serial/BtUart.cpp` | | VideoCore firmware mailbox (radio power, clocks, board MAC) | `BCM2849` (`RPIQ`) | `Drivers/Platform/RpiFirmware.cpp` | | GPIO pin functions | `BCM2845` (`GPI0`) | `Drivers/Platform/RpiGpio.cpp` | +| Ethernet (GENET v5 MAC, BCM54213PE PHY) | `BCM6E4E` (`\_SB.ETH0`) | `Drivers/Net/Genet.cpp` | The VL805 sits behind the BCM2711's PCIe, whose config space is not ECAM (the root bridge's `_DSD` says `linux-ecam-quirk-id = "bcm2711"`), so the @@ -157,9 +158,36 @@ The CYW43456 reports a chip-wide default address (`43:45:C5:00:1F:AC`); the driver replaces it with the board's Ethernet MAC from the firmware. `0:/config/bluetooth.toml` still overrides both (Broadcom `0xFC01`). +## Ethernet + +The BCM2711's own Gigabit MAC, GENET v5 (a UniMAC behind a DMA engine), +talks RGMII to a BCM54213PE PHY at MDIO address 1. The firmware describes +it as `BCM6E4E`: a 64 KiB register block, two interrupts (the driver uses +the first, which carries the default queue), `_CCA = 0`, and a `_DSD` with +`phy-mode` (`rgmii-rxid`: the PHY delays the receive clock, the MAC the +transmit clock) and `brcm,max-dma-burst-size`. + +The driver uses only the default queue (ring 16), with all 256 descriptors +of each direction. The descriptors live in the MAC's own SRAM, so only the +2 KiB packet buffers need cache maintenance. Received frames start two +bytes into their buffer (`RBUF_ALIGN_2B`); neither direction uses the +64-byte status blocks, so there is no checksum offload. The UniMAC accepts +broadcast and its own address only (the MDF filter). + +The interrupt only acknowledges and masks; frames are dispatched from the +idle bottom half, which also checks the ring itself, so receive works if +the interrupt never arrives. The PHY is polled once a second (its interrupt +pin is not described); a link change reprograms the RGMII block and the +UniMAC's speed and duplex. A link the firmware already negotiated is kept, +otherwise the PHY is reset and advertises 10/100 and 1000 full duplex. + +The MAC address is the board's, read from OTP through the firmware mailbox +(the one the Bluetooth driver uses too); the address the firmware's own +driver programmed is the fallback. `dhcp.elf` waits up to 10 s for a wired +link, since autonegotiation takes about 3 s after the PHY reset. + ## Not supported yet -- Ethernet (BCM54213 via GENET v5, `BCM6E4E`). - SD high-speed mode: the driver transfers by polled PIO at 25 MHz on a 4-bit bus (multi-block CMD18/CMD25 with Auto CMD12), at most 12.5 MB/s; switching the card to 50 MHz (CMD6) would double that. @@ -168,8 +196,9 @@ the driver replaces it with the board's Ethernet MAC from the firmware. ## Status -Bluetooth has not run yet: QEMU has no model of the radio, so it is -untested on any machine. +Bluetooth works on a Pi 400 (CYW43456), including A2DP music to +headphones. Ethernet has not run yet: QEMU has no GENET model. It is untested on a Pi 4B (CYW43455), and QEMU has no model of +the radio. Everything else above is verified on QEMU only, including the Pi-specific path: the firmware's own XHC0 table, retargeted at QEMU's xHCI and loaded with the diff --git a/kernel/src/Api/BuildNo.hpp b/kernel/src/Api/BuildNo.hpp index 5b504e9..7c6de1d 100644 --- a/kernel/src/Api/BuildNo.hpp +++ b/kernel/src/Api/BuildNo.hpp @@ -12,4 +12,4 @@ #pragma once -#define MONTAUK_BUILD_NUMBER 99 +#define MONTAUK_BUILD_NUMBER 103 diff --git a/kernel/src/Api/Device.hpp b/kernel/src/Api/Device.hpp index 8d51c98..6bdecf4 100644 --- a/kernel/src/Api/Device.hpp +++ b/kernel/src/Api/Device.hpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -122,6 +123,13 @@ namespace montauk::abi { if (Drivers::Net::E1000E::IsInitialized()) { add(5, "Intel E1000E", "Gigabit Ethernet (82574L)"); } + if (Drivers::Net::Genet::IsInitialized()) { + uint32_t speed = Drivers::Net::Genet::GetLinkSpeed(); + add(5, "Broadcom GENET", speed == 1000 ? "Gigabit Ethernet (1000 Mbit/s)" + : speed == 100 ? "Gigabit Ethernet (100 Mbit/s)" + : speed == 10 ? "Gigabit Ethernet (10 Mbit/s)" + : "Gigabit Ethernet (no link)"); + } if (Drivers::Net::Wifi::IsPresent()) { add(5, "Intel Wi-Fi", Drivers::Net::Wifi::IsLinkUp() ? "802.11 wireless (connected)" diff --git a/kernel/src/Api/Net.hpp b/kernel/src/Api/Net.hpp index cbf9818..b28c35e 100644 --- a/kernel/src/Api/Net.hpp +++ b/kernel/src/Api/Net.hpp @@ -18,6 +18,7 @@ #include #include #include +#include #include #include "Syscall.hpp" @@ -157,6 +158,10 @@ namespace montauk::abi { out->rxPackets = Drivers::Net::E1000E::GetRxPacketCount(); out->txPackets = Drivers::Net::E1000E::GetTxPacketCount(); CopyNetStatusDriver(out->driver, "Intel e1000e"); + } else if (Drivers::Net::Genet::IsInitialized()) { + out->rxPackets = Drivers::Net::Genet::GetRxPacketCount(); + out->txPackets = Drivers::Net::Genet::GetTxPacketCount(); + CopyNetStatusDriver(out->driver, "Broadcom GENET"); } } diff --git a/kernel/src/Drivers/Init.cpp b/kernel/src/Drivers/Init.cpp index 020d03d..7d1adb7 100644 --- a/kernel/src/Drivers/Init.cpp +++ b/kernel/src/Drivers/Init.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -356,10 +357,103 @@ namespace Drivers { Storage::Sdhci::ProbePlatform(base, info.Path); } + // A device property (_DSD: { UUID, Package { Package { key, value }, + // ... } }) by key. + static bool FindDsdProperty(uint32_t nodeIndex, const char* key, Hal::AML::Object& value) { + auto& interp = Hal::AML::GetInterpreter(); + Hal::AML::Object dsd, properties; + if (!interp.EvaluateChild(nodeIndex, "_DSD", nullptr, 0, dsd) || + dsd.Type != Hal::AML::ObjectType::Package || + !interp.GetPackageElement(dsd, 1, properties) || + properties.Type != Hal::AML::ObjectType::Package) { + return false; + } + + int count = interp.GetPackageCount(properties); + for (int i = 0; i < count; i++) { + Hal::AML::Object pair, name; + if (!interp.GetPackageElement(properties, i, pair) || pair.Type != Hal::AML::ObjectType::Package || + !interp.GetPackageElement(pair, 0, name) || name.Type != Hal::AML::ObjectType::String) { + continue; + } + int c = 0; + while (key[c] != '\0' && name.String.Data[c] == key[c]) c++; + if (key[c] != '\0' || name.String.Data[c] != '\0') continue; + return interp.GetPackageElement(pair, 1, value); + } + return false; + } + + static bool StringEquals(const char* a, const char* b) { + while (*a != '\0' && *a == *b) { a++; b++; } + return *a == *b; + } + + // The Raspberry Pi 4/400's GENET v5 Ethernet MAC (BCM6E4E). The first + // of its two interrupts carries the default queue. + static void ProbePlatformGenet() { + if (Net::Genet::IsInitialized()) return; + + Hal::AcpiDevices::DeviceInfo info{}; + if (!Hal::AcpiDevices::FindDeviceByHid("BCM6E4E", info)) return; + + Hal::AML::ResourceList resources; + uint64_t base = 0, length = 0; + if (!Hal::AcpiDevices::EvaluateCrs(info.NodeIndex, resources) || + !resources.GetMemory(base, length) || base == 0) { + Kt::KernelLogStream(Kt::WARNING, "Drivers") << "GENET " << info.Path + << ": no register block in _CRS"; + return; + } + + uint32_t gsi = 0; + bool level = true, activeLow = false; + if (auto* irq = resources.FindFirst(Hal::AML::ResourceType::ExtendedIrq)) { + gsi = irq->ExtendedIrq.Interrupt; + level = (irq->ExtendedIrq.Flags & 1) == 0; + activeLow = (irq->ExtendedIrq.Flags & 2) != 0; + } + + bool coherent = false; + Hal::AML::Object cca; + if (Hal::AML::GetInterpreter().EvaluateChild(info.NodeIndex, "_CCA", nullptr, 0, cca) && + cca.Type == Hal::AML::ObjectType::Integer) { + coherent = cca.Integer != 0; + } + + // The Pi 4's PHY delays the receive clock and expects the MAC to + // delay transmit: "rgmii-rxid", also when the table says nothing. + auto phyMode = Net::Genet::PhyMode::RgmiiRxid; + Hal::AML::Object mode; + if (FindDsdProperty(info.NodeIndex, "phy-mode", mode) && mode.Type == Hal::AML::ObjectType::String) { + const char* m = mode.String.Data; + if (StringEquals(m, "rgmii")) phyMode = Net::Genet::PhyMode::Rgmii; + else if (StringEquals(m, "rgmii-id")) phyMode = Net::Genet::PhyMode::RgmiiId; + else if (StringEquals(m, "rgmii-txid")) phyMode = Net::Genet::PhyMode::RgmiiTxid; + else if (!StringEquals(m, "rgmii-rxid")) { + Kt::KernelLogStream(Kt::WARNING, "Drivers") << "GENET " << info.Path + << ": unsupported phy-mode " << m << ", trying rgmii-rxid"; + } + } + + uint32_t burst = 0; + Hal::AML::Object burstObj; + if (FindDsdProperty(info.NodeIndex, "brcm,max-dma-burst-size", burstObj) && + burstObj.Type == Hal::AML::ObjectType::Integer) { + burst = (uint32_t)burstObj.Integer; + } + + Kt::KernelLogStream(Kt::INFO, "Drivers") << "GENET platform device " << info.Path + << " at 0x" << base::hex << base << ", GSI " << base::dec << (uint64_t)gsi; + WarnIfRamBeyondDma(info.Path); + Net::Genet::ProbePlatform(base, length, gsi, level, activeLow, coherent, phyMode, burst); + } + void ProbeNormal() { Pci::ProbeAll(g_driverTable, g_driverTableCount, Pci::ProbePhase::Normal); ProbePlatformXhci(); ProbePlatformSdhci(); + ProbePlatformGenet(); Bluetooth::ProbePlatformAdapter(); } diff --git a/kernel/src/Drivers/Net/Genet.cpp b/kernel/src/Drivers/Net/Genet.cpp new file mode 100644 index 0000000..bb99ffa --- /dev/null +++ b/kernel/src/Drivers/Net/Genet.cpp @@ -0,0 +1,713 @@ +/* + * Genet.cpp + * Broadcom GENET v5 Ethernet driver (Raspberry Pi 4 / 400) + * Copyright (c) 2026 Daniel Hammer +*/ + +#include "Genet.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace Kt; + +namespace Drivers::Net::Genet { + + // ==================================================================== + // State + // ==================================================================== + + static bool g_initialized = false; + static bool g_coherent = false; + static PhyMode g_phyMode = PhyMode::RgmiiRxid; + static volatile uint8_t* g_regs = nullptr; + static uint8_t g_mac[6] = {}; + static uint32_t g_phyAddr = 1; + + // Packet buffers, BUF_LENGTH each, two to a page. Slot n of a ring + // always uses buffer n. + static uint8_t* g_rxBufs[RX_DESCS] = {}; + static uint8_t* g_txBufs[TX_DESCS] = {}; + + // Software copies of the 16-bit ring counters: the RX consumer index we + // last wrote and the TX producer index. Slot = counter % ring size. + static std::atomic g_rxCons{0}; + static uint32_t g_txProd = 0; + static kcp::Spinlock g_txLock; + + static std::atomic g_rxWorkPending{false}; + static std::atomic g_processing{false}; + + // The PHY is polled: an external RGMII PHY raises no MAC interrupt on + // link changes, and its own interrupt pin is not wired to anything + // ACPI describes. + static constexpr uint64_t LINK_POLL_MS = 1000; + static std::atomic g_nextLinkPollMs{0}; + static std::atomic g_linkUp{false}; + static uint32_t g_linkSpeed = 0; + static bool g_linkFullDuplex = false; + + static uint64_t g_rxPacketCount = 0; + static uint64_t g_txPacketCount = 0; + static uint64_t g_rxErrorCount = 0; + + static RxCallback g_rxCallback = nullptr; + + // ==================================================================== + // Register access + // ==================================================================== + + static uint32_t Read(uint32_t reg) { + return *(volatile uint32_t*)(g_regs + reg); + } + + static void Write(uint32_t reg, uint32_t value) { + *(volatile uint32_t*)(g_regs + reg) = value; + } + + static void SetBits(uint32_t reg, uint32_t bits) { Write(reg, Read(reg) | bits); } + static void ClearBits(uint32_t reg, uint32_t bits) { Write(reg, Read(reg) & ~bits); } + + static uint32_t DescReg(uint32_t base, uint32_t slot, uint32_t word) { + return base + slot * DESC_SIZE + word * 4; + } + + // ==================================================================== + // MDIO (the UniMAC's clause 22 master) + // ==================================================================== + + // Standard PHY registers and bits. + static constexpr uint32_t MII_BMCR = 0x00; + static constexpr uint32_t MII_BMSR = 0x01; + static constexpr uint32_t MII_PHYSID1 = 0x02; + static constexpr uint32_t MII_PHYSID2 = 0x03; + static constexpr uint32_t MII_ADVERTISE = 0x04; + static constexpr uint32_t MII_LPA = 0x05; + static constexpr uint32_t MII_CTRL1000 = 0x09; + static constexpr uint32_t MII_STAT1000 = 0x0A; + + static constexpr uint16_t BMCR_SPEED1000 = 1u << 6; + static constexpr uint16_t BMCR_FULLDPLX = 1u << 8; + static constexpr uint16_t BMCR_ANRESTART = 1u << 9; + static constexpr uint16_t BMCR_ANENABLE = 1u << 12; + static constexpr uint16_t BMCR_SPEED100 = 1u << 13; + static constexpr uint16_t BMCR_RESET = 1u << 15; + static constexpr uint16_t BMSR_LSTATUS = 1u << 2; + static constexpr uint16_t BMSR_ANEGCOMPLETE = 1u << 5; + static constexpr uint16_t ADVERTISE_CSMA = 0x0001; + static constexpr uint16_t ADVERTISE_10HALF = 1u << 5; + static constexpr uint16_t ADVERTISE_10FULL = 1u << 6; + static constexpr uint16_t ADVERTISE_100HALF = 1u << 7; + static constexpr uint16_t ADVERTISE_100FULL = 1u << 8; + static constexpr uint16_t ADVERTISE_1000HALF = 1u << 8; // MII_CTRL1000 + static constexpr uint16_t ADVERTISE_1000FULL = 1u << 9; // MII_CTRL1000 + static constexpr uint16_t LPA_1000HALF = 1u << 10; // MII_STAT1000 + static constexpr uint16_t LPA_1000FULL = 1u << 11; // MII_STAT1000 + + static bool MdioRun(uint32_t command) { + Write(MDIO_CMD, command); + Write(MDIO_CMD, command | MDIO_START_BUSY); + uint64_t start = Timekeeping::GetMilliseconds(); + while (Read(MDIO_CMD) & MDIO_START_BUSY) { + if (Timekeeping::GetMilliseconds() - start > 20) return false; + Arch::Pause(); + } + return true; + } + + static bool MdioRead(uint32_t phy, uint32_t reg, uint16_t& value) { + if (!MdioRun(MDIO_RD | (phy << MDIO_PMD_SHIFT) | (reg << MDIO_REG_SHIFT))) return false; + uint32_t result = Read(MDIO_CMD); + if (result & MDIO_READ_FAIL) return false; + value = (uint16_t)result; + return true; + } + + static bool MdioWrite(uint32_t phy, uint32_t reg, uint16_t value) { + return MdioRun(MDIO_WR | (phy << MDIO_PMD_SHIFT) | (reg << MDIO_REG_SHIFT) | value); + } + + static uint16_t PhyRead(uint32_t reg) { + uint16_t value = 0; + if (!MdioRead(g_phyAddr, reg, value)) return 0; + return value; + } + + static bool PhyPresent(uint32_t addr, uint32_t& id) { + uint16_t id1 = 0, id2 = 0; + if (!MdioRead(addr, MII_PHYSID1, id1) || !MdioRead(addr, MII_PHYSID2, id2)) return false; + if ((id1 == 0xFFFF && id2 == 0xFFFF) || (id1 == 0 && id2 == 0)) return false; + id = ((uint32_t)id1 << 16) | id2; + return true; + } + + // The board puts the PHY at address 1; scan the bus if it is not there. + static bool FindPhy(uint32_t& id) { + if (PhyPresent(1, id)) { g_phyAddr = 1; return true; } + for (uint32_t addr = 0; addr < 32; addr++) { + if (addr != 1 && PhyPresent(addr, id)) { g_phyAddr = addr; return true; } + } + return false; + } + + // ==================================================================== + // Link + // ==================================================================== + + // BMSR latches link failures: the first read reports any loss since the + // last one, the second the current state. + static bool PhyLinkUp() { + (void)PhyRead(MII_BMSR); + uint16_t bmsr = PhyRead(MII_BMSR); + if (!(bmsr & BMSR_LSTATUS)) return false; + uint16_t bmcr = PhyRead(MII_BMCR); + return !(bmcr & BMCR_ANENABLE) || (bmsr & BMSR_ANEGCOMPLETE); + } + + // The highest mode both ends advertised (or the forced mode). + static void ResolveLink(uint32_t& speed, bool& fullDuplex) { + uint16_t bmcr = PhyRead(MII_BMCR); + if (!(bmcr & BMCR_ANENABLE)) { + speed = (bmcr & BMCR_SPEED1000) ? 1000 : (bmcr & BMCR_SPEED100) ? 100 : 10; + fullDuplex = (bmcr & BMCR_FULLDPLX) != 0; + return; + } + + uint16_t ctrl1000 = PhyRead(MII_CTRL1000); + uint16_t stat1000 = PhyRead(MII_STAT1000); + if ((ctrl1000 & ADVERTISE_1000FULL) && (stat1000 & LPA_1000FULL)) { + speed = 1000; fullDuplex = true; return; + } + if ((ctrl1000 & ADVERTISE_1000HALF) && (stat1000 & LPA_1000HALF)) { + speed = 1000; fullDuplex = false; return; + } + + uint16_t common = PhyRead(MII_ADVERTISE) & PhyRead(MII_LPA); + if (common & ADVERTISE_100FULL) { speed = 100; fullDuplex = true; } + else if (common & ADVERTISE_100HALF) { speed = 100; fullDuplex = false; } + else if (common & ADVERTISE_10FULL) { speed = 10; fullDuplex = true; } + else { speed = 10; fullDuplex = false; } + } + + // Point the RGMII block and the UniMAC at the negotiated mode and start + // both directions. + static void ConfigureMac(uint32_t speed, bool fullDuplex) { + uint32_t oob = Read(EXT_RGMII_OOB_CTRL); + oob &= ~OOB_DISABLE; + oob |= RGMII_LINK | RGMII_MODE_EN; + if (g_phyMode == PhyMode::Rgmii || g_phyMode == PhyMode::RgmiiId) oob |= ID_MODE_DIS; + else oob &= ~ID_MODE_DIS; + Write(EXT_RGMII_OOB_CTRL, oob); + + uint32_t cmd = (speed == 1000 ? CMD_SPEED_1000 : speed == 100 ? CMD_SPEED_100 : CMD_SPEED_10) + << CMD_SPEED_SHIFT; + if (!fullDuplex) cmd |= CMD_HD_EN; + Write(UMAC_CMD, cmd); + Write(UMAC_CMD, cmd | CMD_TX_EN | CMD_RX_EN); + } + + static void PollLink() { + bool up = PhyLinkUp(); + bool wasUp = g_linkUp.load(std::memory_order_relaxed); + + if (up) { + uint32_t speed = 0; + bool fullDuplex = false; + ResolveLink(speed, fullDuplex); + if (wasUp && speed == g_linkSpeed && fullDuplex == g_linkFullDuplex) return; + + ConfigureMac(speed, fullDuplex); + g_linkSpeed = speed; + g_linkFullDuplex = fullDuplex; + g_linkUp.store(true, std::memory_order_release); + KernelLogStream(OK, "GENET") << "Link up: " << base::dec << (uint64_t)speed + << " Mbit/s " << (fullDuplex ? "full" : "half") << " duplex"; + } else if (wasUp) { + g_linkUp.store(false, std::memory_order_release); + g_linkSpeed = 0; + ClearBits(EXT_RGMII_OOB_CTRL, RGMII_LINK); + KernelLogStream(INFO, "GENET") << "Link down"; + } + } + + // Leave a link the firmware already negotiated alone (renegotiating + // costs seconds); otherwise reset the PHY and advertise 10/100 and + // 1000 full duplex (the MAC has no 1000 half duplex). + static void StartPhy() { + if (PhyLinkUp()) { + KernelLogStream(INFO, "GENET") << "PHY already has a link, keeping it"; + return; + } + + MdioWrite(g_phyAddr, MII_BMCR, BMCR_RESET); + uint64_t start = Timekeeping::GetMilliseconds(); + while (PhyRead(MII_BMCR) & BMCR_RESET) { + if (Timekeeping::GetMilliseconds() - start > 500) { + KernelLogStream(WARNING, "GENET") << "PHY reset timed out"; + break; + } + Timekeeping::BusyWaitUs(100); + } + + MdioWrite(g_phyAddr, MII_ADVERTISE, ADVERTISE_CSMA | ADVERTISE_10HALF | ADVERTISE_10FULL | + ADVERTISE_100HALF | ADVERTISE_100FULL); + uint16_t ctrl1000 = PhyRead(MII_CTRL1000); + ctrl1000 &= ~(ADVERTISE_1000HALF | ADVERTISE_1000FULL); + MdioWrite(g_phyAddr, MII_CTRL1000, ctrl1000 | ADVERTISE_1000FULL); + MdioWrite(g_phyAddr, MII_BMCR, BMCR_ANENABLE | BMCR_ANRESTART); + } + + // ==================================================================== + // MAC bring-up + // ==================================================================== + + static bool WaitDmaDisabled(uint32_t engineRegs) { + uint64_t start = Timekeeping::GetMilliseconds(); + while (!(Read(engineRegs + DMA_STATUS) & DMA_DISABLED)) { + if (Timekeeping::GetMilliseconds() - start > 5) return false; + Arch::Pause(); + } + return true; + } + + // Stop both DMA engines (the firmware's network stack may have left + // them running) and drop whatever the buffers still hold. + static void StopDma() { + Write(TDMA_REGS + DMA_CTRL, 0); + Write(RDMA_REGS + DMA_CTRL, 0); + if (!WaitDmaDisabled(TDMA_REGS)) KernelLogStream(WARNING, "GENET") << "TX DMA did not stop"; + if (!WaitDmaDisabled(RDMA_REGS)) KernelLogStream(WARNING, "GENET") << "RX DMA did not stop"; + + Write(UMAC_TX_FLUSH, 1); + Timekeeping::BusyWaitUs(10); + Write(UMAC_TX_FLUSH, 0); + + SetBits(SYS_RBUF_FLUSH_CTRL, 1u << 0); + Timekeeping::BusyWaitUs(10); + ClearBits(SYS_RBUF_FLUSH_CTRL, 1u << 0); + } + + // Reset the UniMAC with RGMII loopback on, so its receive clock stays + // stable through the reset, and leave it disabled until the link is up. + static void ResetUmac() { + SetBits(SYS_RBUF_FLUSH_CTRL, 1u << 1); + Timekeeping::BusyWaitUs(10); + ClearBits(SYS_RBUF_FLUSH_CTRL, 1u << 1); + Timekeeping::BusyWaitUs(10); + Write(SYS_RBUF_FLUSH_CTRL, 0); + Timekeeping::BusyWaitUs(10); + + Write(UMAC_CMD, 0); + Write(UMAC_CMD, CMD_SW_RESET | CMD_LCL_LOOP_EN); + Timekeeping::BusyWaitUs(2); + Write(UMAC_CMD, 0); + + Write(UMAC_MIB_CTRL, MIB_RESET_RX | MIB_RESET_TX | MIB_RESET_RUNT); + Write(UMAC_MIB_CTRL, 0); + + Write(UMAC_MAX_FRAME_LEN, MAX_FRAME_LEN); + + // No status blocks in front of frames; received frames start two + // bytes into their buffer, so the IP header lands 4-byte aligned. + uint32_t rbuf = Read(RBUF_CTRL); + rbuf &= ~RBUF_64B_EN; + rbuf |= RBUF_ALIGN_2B; + Write(RBUF_CTRL, rbuf); + ClearBits(TBUF_CTRL, TBUF_64B_EN); + Write(RBUF_TBUF_SIZE_CTRL, 1); + } + + static void ReadFirmwareMac(uint8_t mac[6]) { + uint32_t mac0 = Read(UMAC_MAC0); + uint32_t mac1 = Read(UMAC_MAC1); + mac[0] = (uint8_t)(mac0 >> 24); + mac[1] = (uint8_t)(mac0 >> 16); + mac[2] = (uint8_t)(mac0 >> 8); + mac[3] = (uint8_t)mac0; + mac[4] = (uint8_t)(mac1 >> 8); + mac[5] = (uint8_t)mac1; + } + + static bool MacUsable(const uint8_t mac[6]) { + bool allZero = true, allOnes = true; + for (int i = 0; i < 6; i++) { + if (mac[i] != 0x00) allZero = false; + if (mac[i] != 0xFF) allOnes = false; + } + return !allZero && !allOnes && !(mac[0] & 1); + } + + static void SetMdfEntry(uint32_t& index, const uint8_t mac[6]) { + Write(UMAC_MDF_ADDR + index * 4, ((uint32_t)mac[0] << 8) | mac[1]); + Write(UMAC_MDF_ADDR + (index + 1) * 4, ((uint32_t)mac[2] << 24) | ((uint32_t)mac[3] << 16) | + ((uint32_t)mac[4] << 8) | mac[5]); + index += 2; + } + + // Station address, and the destination filter: broadcast and our own + // address. Filter n is enabled by bit (16 - n). + static void ProgramAddress() { + Write(UMAC_MAC0, ((uint32_t)g_mac[0] << 24) | ((uint32_t)g_mac[1] << 16) | + ((uint32_t)g_mac[2] << 8) | g_mac[3]); + Write(UMAC_MAC1, ((uint32_t)g_mac[4] << 8) | g_mac[5]); + + static constexpr uint8_t Broadcast[6] = { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF }; + uint32_t index = 0; + SetMdfEntry(index, Broadcast); + SetMdfEntry(index, g_mac); + uint32_t filters = index / 2; + uint32_t enable = 0; + for (uint32_t i = 0; i < filters; i++) enable |= 1u << (MAX_MDF_FILTER - 1 - i); + Write(UMAC_MDF_CTRL, enable); + } + + static bool AllocateBuffers(uint8_t** bufs, uint32_t count) { + constexpr uint32_t PerPage = 0x1000 / BUF_LENGTH; + for (uint32_t i = 0; i < count; i += PerPage) { + uint8_t* page = (uint8_t*)Memory::g_pfa->AllocateZeroed(); + if (page == nullptr) return false; + // The zeroes sit dirty in the cache; write them back now so no + // eviction lands on top of a frame the MAC wrote later. + Memory::Dma::SyncForDevice(page, 0x1000, g_coherent); + for (uint32_t j = 0; j < PerPage && i + j < count; j++) bufs[i + j] = page + j * BUF_LENGTH; + } + return true; + } + + // Ring 16 spans all 256 descriptors (START_ADDR/END_ADDR count 32-bit + // words). The producer/consumer counters are zeroed, but if the MAC + // keeps an old value the software side follows it instead: the slot is + // always counter % 256. + static void InitRxRing(uint32_t burst) { + Write(RDMA_REGS + DMA_SCB_BURST_SIZE, burst); + + Write(RDMA_RING + RDMA_PROD_INDEX, 0); + Write(RDMA_RING + RDMA_CONS_INDEX, 0); + Write(RDMA_RING + DMA_START_ADDR, 0); + Write(RDMA_RING + RDMA_READ_PTR, 0); + Write(RDMA_RING + RDMA_WRITE_PTR, 0); + Write(RDMA_RING + DMA_END_ADDR, RX_DESCS * DESC_SIZE / 4 - 1); + Write(RDMA_RING + DMA_RING_BUF_SIZE, (RX_DESCS << 16) | BUF_LENGTH); + Write(RDMA_RING + RDMA_XON_XOFF_THRESH, (5u << 16) | (RX_DESCS >> 4)); + Write(RDMA_RING + DMA_MBUF_DONE_THRESH, 1); + + uint32_t cons = Read(RDMA_RING + RDMA_PROD_INDEX) & 0xFFFF; + Write(RDMA_RING + RDMA_CONS_INDEX, cons); + g_rxCons.store(cons, std::memory_order_relaxed); + if (cons != 0) { + KernelLogStream(INFO, "GENET") << "RX ring resumes at index " << base::dec << (uint64_t)cons; + } + + for (uint32_t i = 0; i < RX_DESCS; i++) { + uint64_t phys = Memory::SubHHDM(g_rxBufs[i]); + Write(DescReg(RX_DESC_BASE, i, 1), (uint32_t)phys); + Write(DescReg(RX_DESC_BASE, i, 2), (uint32_t)(phys >> 32)); + Write(DescReg(RX_DESC_BASE, i, 0), (BUF_LENGTH << DMA_BUFLENGTH_SHIFT) | DMA_OWN); + } + + Write(RDMA_REGS + DMA_RING_CFG, 1u << DEFAULT_Q); + } + + static void InitTxRing(uint32_t burst) { + Write(TDMA_REGS + DMA_SCB_BURST_SIZE, burst); + + Write(TDMA_RING + TDMA_PROD_INDEX, 0); + Write(TDMA_RING + TDMA_CONS_INDEX, 0); + Write(TDMA_RING + DMA_START_ADDR, 0); + Write(TDMA_RING + TDMA_READ_PTR, 0); + Write(TDMA_RING + TDMA_WRITE_PTR, 0); + Write(TDMA_RING + DMA_END_ADDR, TX_DESCS * DESC_SIZE / 4 - 1); + Write(TDMA_RING + DMA_RING_BUF_SIZE, (TX_DESCS << 16) | BUF_LENGTH); + Write(TDMA_RING + DMA_MBUF_DONE_THRESH, 1); + Write(TDMA_RING + TDMA_FLOW_PERIOD, 0); + + g_txProd = Read(TDMA_RING + TDMA_CONS_INDEX) & 0xFFFF; + Write(TDMA_RING + TDMA_PROD_INDEX, g_txProd); + if (g_txProd != 0) { + KernelLogStream(INFO, "GENET") << "TX ring resumes at index " << base::dec << (uint64_t)g_txProd; + } + + Write(TDMA_REGS + DMA_RING_CFG, 1u << DEFAULT_Q); + } + + static void StartDma() { + uint32_t ctrl = (1u << (DEFAULT_Q + DMA_RING_BUF_EN_SHIFT)) | DMA_EN; + Write(TDMA_REGS + DMA_CTRL, ctrl); + Write(RDMA_REGS + DMA_CTRL, ctrl); + } + + // ==================================================================== + // Interrupt + // ==================================================================== + + // Acknowledge and mask; the frames are dispatched from the bottom half, + // which takes process-context locks. + static void HandleInterrupt(uint8_t, bool) { + if (!g_initialized) return; + uint32_t status = Read(INTRL2_0 + INTRL2_CPU_STAT) & ~Read(INTRL2_0 + INTRL2_CPU_MASK_STATUS); + if (status == 0) return; + Write(INTRL2_0 + INTRL2_CPU_CLEAR, status); + + if (status & UMAC_IRQ_RXDMA_DONE) { + Write(INTRL2_0 + INTRL2_CPU_MASK_SET, UMAC_IRQ_RXDMA_DONE); + g_rxWorkPending.store(true, std::memory_order_release); + } + } + + // ==================================================================== + // Receive + // ==================================================================== + + static void PollRx() { + uint32_t cons = g_rxCons.load(std::memory_order_relaxed); + uint32_t prod = Read(RDMA_RING + RDMA_PROD_INDEX) & 0xFFFF; + + // At most one revolution per pass; the bottom half comes back for + // the rest. + uint32_t processed = 0; + while (cons != prod && processed < RX_DESCS) { + Arch::DmaReadFence(); + uint32_t slot = cons % RX_DESCS; + uint32_t lenStat = Read(DescReg(RX_DESC_BASE, slot, 0)); + uint32_t length = (lenStat >> DMA_BUFLENGTH_SHIFT) & DMA_BUFLENGTH_MASK; + uint8_t* buffer = g_rxBufs[slot]; + + bool whole = (lenStat & (DMA_SOP | DMA_EOP)) == (DMA_SOP | DMA_EOP); + if (!whole || (lenStat & DMA_RX_ERRORS) || length <= RX_BUF_OFFSET || length > BUF_LENGTH) { + g_rxErrorCount++; + } else { + Memory::Dma::SyncForCpu(buffer, length, g_coherent); + g_rxPacketCount++; + if (g_rxCallback != nullptr) { + g_rxCallback(buffer + RX_BUF_OFFSET, (uint16_t)(length - RX_BUF_OFFSET)); + } + } + + cons = (cons + 1) & 0xFFFF; + g_rxCons.store(cons, std::memory_order_relaxed); + Write(RDMA_RING + RDMA_CONS_INDEX, cons); + processed++; + if (cons == prod) prod = Read(RDMA_RING + RDMA_PROD_INDEX) & 0xFFFF; + } + } + + static bool RxPending() { + uint32_t prod = Read(RDMA_RING + RDMA_PROD_INDEX) & 0xFFFF; + return prod != g_rxCons.load(std::memory_order_relaxed); + } + + // ==================================================================== + // Public API + // ==================================================================== + + bool ProbePlatform(uint64_t mmioPhys, uint64_t mmioSize, uint32_t gsi, bool levelTriggered, + bool activeLow, bool dmaCoherent, PhyMode phyMode, uint32_t burst) { + if (g_initialized) return false; + + if (mmioSize < 0x10000) mmioSize = 0x10000; + for (uint64_t offset = 0; offset < mmioSize; offset += 0x1000) { + Memory::VMM::g_paging->MapMMIO(mmioPhys + offset, Memory::HHDM(mmioPhys + offset)); + } + g_regs = (volatile uint8_t*)Memory::HHDM(mmioPhys); + g_coherent = dmaCoherent; + g_phyMode = phyMode; + if (burst == 0) burst = 8; + + // GENET v5 reports major revision 6 (v4 reports 5). + uint32_t rev = Read(SYS_REV_CTRL); + uint32_t major = (rev >> 24) & 0x0F; + if (major != 6) { + KernelLogStream(WARNING, "GENET") << "Unsupported revision 0x" << base::hex << (uint64_t)rev; + return false; + } + + // Mask everything while the controller is reconfigured. + Write(INTRL2_0 + INTRL2_CPU_MASK_SET, 0xFFFFFFFF); + Write(INTRL2_0 + INTRL2_CPU_CLEAR, 0xFFFFFFFF); + Write(INTRL2_1 + INTRL2_CPU_MASK_SET, 0xFFFFFFFF); + Write(INTRL2_1 + INTRL2_CPU_CLEAR, 0xFFFFFFFF); + + // The board's address comes from OTP through the firmware; else + // whatever the firmware's own driver programmed. + uint8_t firmwareMac[6]; + ReadFirmwareMac(firmwareMac); + const char* macSource = "OTP"; + if (!Platform::RpiFirmware::GetBoardMacAddress(g_mac) || !MacUsable(g_mac)) { + if (MacUsable(firmwareMac)) { + for (int i = 0; i < 6; i++) g_mac[i] = firmwareMac[i]; + macSource = "firmware"; + } else { + // Locally administered, fixed so DHCP leases survive reboots. + static constexpr uint8_t Fallback[6] = { 0x02, 0x4D, 0x54, 0x4B, 0x00, 0x01 }; + for (int i = 0; i < 6; i++) g_mac[i] = Fallback[i]; + macSource = "fallback"; + } + } + + Write(SYS_PORT_CTRL, PORT_MODE_EXT_GPHY); + StopDma(); + ResetUmac(); + ProgramAddress(); + + if (!AllocateBuffers(g_rxBufs, RX_DESCS) || !AllocateBuffers(g_txBufs, TX_DESCS)) { + KernelLogStream(ERROR, "GENET") << "Out of memory for packet buffers"; + return false; + } + InitRxRing(burst); + InitTxRing(burst); + StartDma(); + + uint32_t phyId = 0; + if (!FindPhy(phyId)) { + KernelLogStream(ERROR, "GENET") << "No PHY answers on MDIO"; + StopDma(); + return false; + } + + KernelLogStream(OK, "GENET") << "GENET v5." << base::dec << (uint64_t)((rev >> 16) & 0x0F) + << " at 0x" << base::hex << mmioPhys << ", PHY 0x" << (uint64_t)phyId + << " at MDIO " << base::dec << (uint64_t)g_phyAddr + << (dmaCoherent ? ", coherent DMA" : ", non-coherent DMA"); + KernelLogStream(OK, "GENET") << "MAC " << base::hex + << (uint64_t)g_mac[0] << ":" << (uint64_t)g_mac[1] << ":" << (uint64_t)g_mac[2] << ":" + << (uint64_t)g_mac[3] << ":" << (uint64_t)g_mac[4] << ":" << (uint64_t)g_mac[5] + << " (" << macSource << ")"; + + // RGMII on with the board's delays; the link (RGMII_LINK, speed, + // TX/RX enable) follows from the PHY poll. + uint32_t oob = Read(EXT_RGMII_OOB_CTRL); + oob &= ~(OOB_DISABLE | RGMII_LINK); + oob |= RGMII_MODE_EN; + if (phyMode == PhyMode::Rgmii || phyMode == PhyMode::RgmiiId) oob |= ID_MODE_DIS; + else oob &= ~ID_MODE_DIS; + Write(EXT_RGMII_OOB_CTRL, oob); + + StartPhy(); + + uint8_t line = gsi != 0 ? Hal::GsiRouting::LineForGsi(gsi, levelTriggered, activeLow) : 0; + if (line != 0) { + Arch::RegisterIrqHandler(line, HandleInterrupt); + Arch::EnableLegacyIrq(line); + KernelLogStream(OK, "GENET") << "Interrupt: GSI " << base::dec << (uint64_t)gsi + << " on platform line " << (uint64_t)line; + } else { + KernelLogStream(WARNING, "GENET") << "No interrupt, receive is polled"; + } + + g_initialized = true; + Write(INTRL2_0 + INTRL2_CPU_MASK_CLEAR, UMAC_IRQ_RXDMA_DONE); + + PollLink(); + g_nextLinkPollMs.store(Timekeeping::GetMilliseconds() + LINK_POLL_MS, std::memory_order_relaxed); + return true; + } + + // Frames are copied into the slot's buffer, so the caller's buffer is + // free again on return. Fails while the ring is full or the link down. + bool SendPacket(const uint8_t* data, uint16_t length) { + if (!g_initialized || data == nullptr || length == 0 || length > MAX_FRAME_LEN) return false; + if (!g_linkUp.load(std::memory_order_acquire)) return false; + + g_txLock.Acquire(); + + uint32_t cons = Read(TDMA_RING + TDMA_CONS_INDEX) & 0xFFFF; + if (((g_txProd - cons) & 0xFFFF) >= TX_DESCS - 1) { + g_txLock.Release(); + return false; + } + + uint32_t slot = g_txProd % TX_DESCS; + uint8_t* buffer = g_txBufs[slot]; + memcpy(buffer, data, length); + // The MAC sends short frames as given; pad to the 60-byte minimum. + uint32_t frameLength = length; + if (frameLength < 60) { + memset(buffer + frameLength, 0, 60 - frameLength); + frameLength = 60; + } + Memory::Dma::SyncForDevice(buffer, frameLength, g_coherent); + + uint64_t phys = Memory::SubHHDM(buffer); + Write(DescReg(TX_DESC_BASE, slot, 1), (uint32_t)phys); + Write(DescReg(TX_DESC_BASE, slot, 2), (uint32_t)(phys >> 32)); + Write(DescReg(TX_DESC_BASE, slot, 0), (frameLength << DMA_BUFLENGTH_SHIFT) | + (DMA_TX_QTAG_MASK << DMA_TX_QTAG_SHIFT) | DMA_TX_APPEND_CRC | DMA_SOP | DMA_EOP); + + g_txProd = (g_txProd + 1) & 0xFFFF; + Arch::DmaWriteFence(); + Write(TDMA_RING + TDMA_PROD_INDEX, g_txProd); + g_txPacketCount++; + + g_txLock.Release(); + return true; + } + + const uint8_t* GetMacAddress() { + return g_mac; + } + + bool IsInitialized() { + return g_initialized; + } + + bool IsLinkUp() { + return g_initialized && g_linkUp.load(std::memory_order_acquire); + } + + uint32_t GetLinkSpeed() { + return IsLinkUp() ? g_linkSpeed : 0; + } + + uint64_t GetRxPacketCount() { + return g_rxPacketCount; + } + + uint64_t GetTxPacketCount() { + return g_txPacketCount; + } + + void SetRxCallback(RxCallback callback) { + g_rxCallback = callback; + } + + bool HasDeferredWork() { + if (!g_initialized) return false; + return g_rxWorkPending.load(std::memory_order_acquire) || RxPending() || + Timekeeping::GetMilliseconds() >= g_nextLinkPollMs.load(std::memory_order_relaxed); + } + + void ProcessDeferredWork() { + if (!g_initialized) return; + + bool expected = false; + if (!g_processing.compare_exchange_strong(expected, true, + std::memory_order_acquire, std::memory_order_relaxed)) return; + + uint64_t now = Timekeeping::GetMilliseconds(); + if (now >= g_nextLinkPollMs.load(std::memory_order_relaxed)) { + g_nextLinkPollMs.store(now + LINK_POLL_MS, std::memory_order_relaxed); + PollLink(); + } + + g_rxWorkPending.store(false, std::memory_order_release); + PollRx(); + + // A frame that arrived while masked has latched the status bit, so + // unmasking raises the interrupt again rather than losing it. + Write(INTRL2_0 + INTRL2_CPU_MASK_CLEAR, UMAC_IRQ_RXDMA_DONE); + + g_processing.store(false, std::memory_order_release); + } + +} diff --git a/kernel/src/Drivers/Net/Genet.hpp b/kernel/src/Drivers/Net/Genet.hpp new file mode 100644 index 0000000..ee0aa1f --- /dev/null +++ b/kernel/src/Drivers/Net/Genet.hpp @@ -0,0 +1,197 @@ +/* + * Genet.hpp + * Broadcom GENET v5 Ethernet driver (Raspberry Pi 4 / 400) + * Copyright (c) 2026 Daniel Hammer +*/ + +#pragma once +#include + +// The BCM2711's on-chip Gigabit Ethernet MAC (GENET v5, a UniMAC behind a +// DMA engine), wired over RGMII to a BCM54213PE PHY at MDIO address 1. The +// Pi Firmware Task Force UEFI firmware describes it as BCM6E4E +// (\_SB.ETH0): one register block, two interrupts (the first carries the +// default queue), _CCA = 0 (buffers do not snoop the CPU caches) and a +// _DSD with "phy-mode" and "brcm,max-dma-burst-size". +// +// Only the default queue (ring 16) is used, with all 256 descriptors of +// each direction. The descriptors live in the MAC's own SRAM, so only the +// packet buffers need cache maintenance. + +namespace Drivers::Net::Genet { + + // ---- Register map (offsets from the block base) ---- + + constexpr uint32_t SYS_REV_CTRL = 0x0000; + constexpr uint32_t SYS_PORT_CTRL = 0x0004; + constexpr uint32_t SYS_RBUF_FLUSH_CTRL = 0x0008; + constexpr uint32_t SYS_TBUF_FLUSH_CTRL = 0x000C; + + constexpr uint32_t EXT_RGMII_OOB_CTRL = 0x008C; + + constexpr uint32_t INTRL2_0 = 0x0200; // default queue, MAC events + constexpr uint32_t INTRL2_1 = 0x0240; // priority queues (unused) + constexpr uint32_t INTRL2_CPU_STAT = 0x00; + constexpr uint32_t INTRL2_CPU_CLEAR = 0x08; + constexpr uint32_t INTRL2_CPU_MASK_STATUS = 0x0C; + constexpr uint32_t INTRL2_CPU_MASK_SET = 0x10; + constexpr uint32_t INTRL2_CPU_MASK_CLEAR = 0x14; + + constexpr uint32_t RBUF_CTRL = 0x0300; + constexpr uint32_t RBUF_TBUF_SIZE_CTRL = 0x03B4; + constexpr uint32_t TBUF_CTRL = 0x0600; + + constexpr uint32_t UMAC_CMD = 0x0808; + constexpr uint32_t UMAC_MAC0 = 0x080C; + constexpr uint32_t UMAC_MAC1 = 0x0810; + constexpr uint32_t UMAC_MAX_FRAME_LEN = 0x0814; + constexpr uint32_t UMAC_TX_FLUSH = 0x0B34; + constexpr uint32_t UMAC_MIB_CTRL = 0x0D80; + constexpr uint32_t MDIO_CMD = 0x0E14; + constexpr uint32_t UMAC_MDF_CTRL = 0x0E50; + constexpr uint32_t UMAC_MDF_ADDR = 0x0E54; + + // Descriptor SRAM, then the DMA registers: 17 rings of 0x40, then the + // engine's own registers. + constexpr uint32_t TOTAL_DESCS = 256; + constexpr uint32_t DESC_SIZE = 12; // length/status, address lo, address hi + constexpr uint32_t RX_DESC_BASE = 0x2000; + constexpr uint32_t TX_DESC_BASE = 0x4000; + constexpr uint32_t RDMA_BASE = RX_DESC_BASE + TOTAL_DESCS * DESC_SIZE; + constexpr uint32_t TDMA_BASE = TX_DESC_BASE + TOTAL_DESCS * DESC_SIZE; + constexpr uint32_t DEFAULT_Q = 16; + constexpr uint32_t DMA_RING_SIZE = 0x40; + constexpr uint32_t RDMA_RING = RDMA_BASE + DEFAULT_Q * DMA_RING_SIZE; + constexpr uint32_t TDMA_RING = TDMA_BASE + DEFAULT_Q * DMA_RING_SIZE; + constexpr uint32_t RDMA_REGS = RDMA_BASE + (DEFAULT_Q + 1) * DMA_RING_SIZE; + constexpr uint32_t TDMA_REGS = TDMA_BASE + (DEFAULT_Q + 1) * DMA_RING_SIZE; + + // Per-ring registers. RX: the MAC produces, software consumes; TX the + // other way round. Both indices are 16-bit counters. + constexpr uint32_t RDMA_WRITE_PTR = 0x00; + constexpr uint32_t RDMA_PROD_INDEX = 0x08; + constexpr uint32_t RDMA_CONS_INDEX = 0x0C; + constexpr uint32_t RDMA_XON_XOFF_THRESH = 0x28; + constexpr uint32_t RDMA_READ_PTR = 0x2C; + constexpr uint32_t TDMA_READ_PTR = 0x00; + constexpr uint32_t TDMA_CONS_INDEX = 0x08; + constexpr uint32_t TDMA_PROD_INDEX = 0x0C; + constexpr uint32_t TDMA_FLOW_PERIOD = 0x28; + constexpr uint32_t TDMA_WRITE_PTR = 0x2C; + constexpr uint32_t DMA_RING_BUF_SIZE = 0x10; + constexpr uint32_t DMA_START_ADDR = 0x14; + constexpr uint32_t DMA_END_ADDR = 0x1C; + constexpr uint32_t DMA_MBUF_DONE_THRESH = 0x24; + + // Engine registers. + constexpr uint32_t DMA_RING_CFG = 0x00; + constexpr uint32_t DMA_CTRL = 0x04; + constexpr uint32_t DMA_STATUS = 0x08; + constexpr uint32_t DMA_SCB_BURST_SIZE = 0x0C; + + // ---- Register bits ---- + + constexpr uint32_t PORT_MODE_EXT_GPHY = 3; + + constexpr uint32_t RGMII_LINK = 1u << 4; + constexpr uint32_t OOB_DISABLE = 1u << 5; + constexpr uint32_t RGMII_MODE_EN = 1u << 6; + constexpr uint32_t ID_MODE_DIS = 1u << 16; // no MAC-side TX clock delay + + constexpr uint32_t RBUF_64B_EN = 1u << 0; // receive status block + constexpr uint32_t RBUF_ALIGN_2B = 1u << 1; // 2 pad bytes before each frame + constexpr uint32_t TBUF_64B_EN = 1u << 0; // transmit status block + + constexpr uint32_t CMD_TX_EN = 1u << 0; + constexpr uint32_t CMD_RX_EN = 1u << 1; + constexpr uint32_t CMD_SPEED_SHIFT = 2; + constexpr uint32_t CMD_SPEED_10 = 0; + constexpr uint32_t CMD_SPEED_100 = 1; + constexpr uint32_t CMD_SPEED_1000 = 2; + constexpr uint32_t CMD_PROMISC = 1u << 4; + constexpr uint32_t CMD_HD_EN = 1u << 10; + constexpr uint32_t CMD_SW_RESET = 1u << 13; + constexpr uint32_t CMD_LCL_LOOP_EN = 1u << 15; + + constexpr uint32_t MIB_RESET_RX = 1u << 0; + constexpr uint32_t MIB_RESET_RUNT = 1u << 1; + constexpr uint32_t MIB_RESET_TX = 1u << 2; + + constexpr uint32_t MDIO_START_BUSY = 1u << 29; + constexpr uint32_t MDIO_READ_FAIL = 1u << 28; + constexpr uint32_t MDIO_RD = 2u << 26; + constexpr uint32_t MDIO_WR = 1u << 26; + constexpr uint32_t MDIO_PMD_SHIFT = 21; + constexpr uint32_t MDIO_REG_SHIFT = 16; + + constexpr uint32_t MAX_MDF_FILTER = 17; + + constexpr uint32_t DMA_EN = 1u << 0; + constexpr uint32_t DMA_RING_BUF_EN_SHIFT = 1; + constexpr uint32_t DMA_DISABLED = 1u << 0; // DMA_STATUS + + constexpr uint32_t UMAC_IRQ_RXDMA_DONE = 1u << 13; // ring 16 reached its done threshold + + // Descriptor length/status word: length in 27:16, flags in 15:0. + constexpr uint32_t DMA_BUFLENGTH_SHIFT = 16; + constexpr uint32_t DMA_BUFLENGTH_MASK = 0x0FFF; + constexpr uint32_t DMA_OWN = 0x8000; + constexpr uint32_t DMA_EOP = 0x4000; + constexpr uint32_t DMA_SOP = 0x2000; + constexpr uint32_t DMA_TX_APPEND_CRC = 0x0040; + constexpr uint32_t DMA_TX_QTAG_SHIFT = 7; + constexpr uint32_t DMA_TX_QTAG_MASK = 0x3F; + constexpr uint32_t DMA_RX_LG = 0x0010; + constexpr uint32_t DMA_RX_NO = 0x0008; + constexpr uint32_t DMA_RX_RXER = 0x0004; + constexpr uint32_t DMA_RX_CRC_ERROR = 0x0002; + constexpr uint32_t DMA_RX_OV = 0x0001; + constexpr uint32_t DMA_RX_ERRORS = DMA_RX_LG | DMA_RX_NO | DMA_RX_RXER | + DMA_RX_CRC_ERROR | DMA_RX_OV; + + // ---- Driver parameters ---- + + constexpr uint32_t RX_DESCS = TOTAL_DESCS; + constexpr uint32_t TX_DESCS = TOTAL_DESCS; + constexpr uint32_t BUF_LENGTH = 2048; + constexpr uint32_t RX_BUF_OFFSET = 2; // RBUF_ALIGN_2B padding + // Payload 1500 + header 14 + VLAN 4 + Broadcom tag 6 + FCS 4 + pad 8. + constexpr uint32_t MAX_FRAME_LEN = 1536; + + // How the board wires RGMII clock delays (ACPI _DSD "phy-mode"). + enum class PhyMode : uint8_t { + Rgmii, // no delays from the PHY or the MAC + RgmiiId, // the PHY delays both clocks + RgmiiRxid, // the PHY delays RX, the MAC delays TX (Raspberry Pi 4) + RgmiiTxid, + }; + + // Take over the controller at `mmioPhys`. `gsi` is the default-queue + // interrupt (0: poll only). `burst` is the DMA burst size in 32-bit + // words from "brcm,max-dma-burst-size" (0: 8). + bool ProbePlatform(uint64_t mmioPhys, uint64_t mmioSize, uint32_t gsi, bool levelTriggered, + bool activeLow, bool dmaCoherent, PhyMode phyMode, uint32_t burst); + + bool SendPacket(const uint8_t* data, uint16_t length); + const uint8_t* GetMacAddress(); + bool IsInitialized(); + + // Carrier as of the last PHY poll (once a second from deferred work). + bool IsLinkUp(); + + // Negotiated speed in Mbit/s, 0 while the link is down. + uint32_t GetLinkSpeed(); + + uint64_t GetRxPacketCount(); + uint64_t GetTxPacketCount(); + + using RxCallback = void(*)(const uint8_t* data, uint16_t length); + void SetRxCallback(RxCallback callback); + + // The interrupt only masks and flags; frames are dispatched and the PHY + // polled from the idle bottom half. HasDeferredWork also looks at the + // ring itself, so receive keeps working if the interrupt never fires. + bool HasDeferredWork(); + void ProcessDeferredWork(); + +} diff --git a/kernel/src/Net/Net.cpp b/kernel/src/Net/Net.cpp index 5a51db3..ed30b4f 100644 --- a/kernel/src/Net/Net.cpp +++ b/kernel/src/Net/Net.cpp @@ -16,6 +16,7 @@ #include #include #include +#include #include #include #include @@ -56,6 +57,18 @@ namespace Net { Drivers::Net::E1000E::SetRxCallback(Ethernet::OnFrameReceived); } + // Raspberry Pi 4/400 on-board Ethernet. Genet::IsLinkUp already + // reports no link for a controller that never initialized. + if (Drivers::Net::Genet::IsInitialized()) { + NetIf::Register({ + "eth0", NetIf::Kind::Ethernet, + Drivers::Net::Genet::GetMacAddress, + Drivers::Net::Genet::SendPacket, + Drivers::Net::Genet::IsLinkUp, + }); + Drivers::Net::Genet::SetRxCallback(Ethernet::OnFrameReceived); + } + // Wi-Fi registers whenever the adapter exists; its link only comes up // once a network has been joined and keyed. if (Drivers::Net::Wifi::IsPresent()) { diff --git a/kernel/src/Timekeeping/Timer.cpp b/kernel/src/Timekeeping/Timer.cpp index 7e248aa..b47393d 100644 --- a/kernel/src/Timekeeping/Timer.cpp +++ b/kernel/src/Timekeeping/Timer.cpp @@ -16,6 +16,7 @@ #include #include #include +#include #include #include #include @@ -65,7 +66,8 @@ namespace Timekeeping { Drivers::Serial::BtUart::HasRxData(); bool audioWork = Drivers::Audio::IntelHda::HasDeferredWork(); bool netWork = Drivers::Net::E1000::HasDeferredWork() || - Drivers::Net::E1000E::HasDeferredWork(); + Drivers::Net::E1000E::HasDeferredWork() || + Drivers::Net::Genet::HasDeferredWork(); // A forced trip into the idle bottom-half context is safe only when // this timer interrupted ring 3. If it interrupted a syscall, that // suspended kernel stack may own one of the process-context mutexes @@ -176,6 +178,8 @@ namespace Timekeeping { Drivers::Net::E1000::ProcessDeferredWork(); if (Drivers::Net::E1000E::HasDeferredWork()) Drivers::Net::E1000E::ProcessDeferredWork(); + if (Drivers::Net::Genet::HasDeferredWork()) + Drivers::Net::Genet::ProcessDeferredWork(); // HDA completion IRQs only acknowledge/mask. Resampling and DMA-ring // refill are far too expensive for hard interrupt context. diff --git a/programs/src/dhcp/main.cpp b/programs/src/dhcp/main.cpp index 80d774e..52f1463 100644 --- a/programs/src/dhcp/main.cpp +++ b/programs/src/dhcp/main.cpp @@ -250,16 +250,22 @@ extern "C" void _start() { montauk::service_log("MontaukOS DHCP Client"); - // 0. Check if any device with an up link even exists. + // 0. Check if any device with an up link even exists. A wired PHY + // still autonegotiating at boot (a few seconds after its reset) gets + // a grace period; with no wired interface at all there is nothing to + // wait for. montauk::abi::NetIfInfo ifs[4]; - int n = montauk::net_interfaces(ifs, 4); - bool link = false; - for (int i = 0; i < n; i++) { - if (ifs[i].linkUp == true) { - link = true; + for (int attempt = 0; attempt < 40 && !link; attempt++) { + int n = montauk::net_interfaces(ifs, 4); + bool wired = false; + for (int i = 0; i < n; i++) { + if (ifs[i].linkUp) link = true; + if (ifs[i].kind == montauk::abi::NETIF_KIND_ETHERNET) wired = true; } + if (link || !wired) break; + montauk::sleep_ms(250); } if (link == false) {