TamgaOS (yula)
RSS github
August 2026 TamgaOS Ethernet RMII MDIO STM32H7 Register-Level

Ethernet PHY Link Detection
Three Days of Register Hunting, No HAL, Down to a Cable Being Plugged In

This post covers exactly one thing: getting the STM32H753ZI to talk to its onboard Ethernet PHY over MDIO and report real link status — up or down, speed, duplex — the moment a cable is plugged in or pulled out. No DMA, no packet transmission, no TCP/IP. Just proving that the MAC, the clock tree, the GPIO pin mux, and the PHY management interface are all configured correctly, entirely from the register manual, entirely without HAL or CMSIS.
The actual frame transmission (DMA descriptors, TX/RX rings) is a separate, still-unresolved piece of work — this post stops at the point where the board can honestly say "yes, there's a cable, and it's 100Mbps Full-Duplex."
Note on the "three days": that's honestly just how long finding the right registers took. Getting from "registers found" to "this actually works on real hardware" took considerably longer — and is still ongoing for the DMA/frame-transmission side.

— Register Hunt Timeline —

How the three days went

Ethernet MAC+DMA base address, confirmed from the memory map
0x40028000 — RM0433 Section 58.11. EtherneThe actual framet's DMA sub-block sits at a +0x1000 offset from the MAC base, not a separate peripheral entirely — worth knowing before the MAC and DMA register tables start looking like two different chips.
RCC clock enables — already half-confirmed from an earlier ADC session
ETH1MACEN (bit 15), ETH1TXEN (bit 16), ETH1RXEN (bit 17) in RCC_AHB1ENR had actually already turned up during an unrelated ADC bit hunt weeks earlier — a rare case of a previous register-hunting session paying off directly in a later one.
SYSCFG_PMCR — MII vs RMII is a single 3-bit field, easy to miss
EPIS[2:0] at bits 23:21 — 000 for MII, 100 for RMII. Everything else in this chapter is complicated; this one field is not, which made it easy to almost skip past without noticing it needed setting at all.
UM2407's RJ45 pinout table was the wrong table
The first table found in the board manual described the RJ45 connector's own physical pins (TX+/TX-/RX+/RX-, LED cathodes) — not which STM32 GPIO pins the PHY chip is wired to. The correct table (UM2407 Table 13, "Ethernet pin configuration") was a few pages further, cross-referencing each RMII signal against both its MCU pin and its board-specific solder bridge.
All nine RMII pins confirmed AF11, from the datasheet's alternate-function table
Every one of PA1, PA2, PC1, PA7, PC4, PC5, PB13, PG11, PG13 maps to Ethernet on AF11 — a rare case in this project of nine pins all agreeing on one alternate-function number without a single exception to chase down.
MDC clock divider — confirmed via the actual APB1/AHB1 clock tree, not assumed
Rather than guess a safe-looking divider value, traced RCC_D1CFGR.HPRE (already set to /2 by this project's rcc_init_pll_480()) to establish AHB1 is actually 240MHz, then picked the MDIO clock range option that lands the MDC clock inside IEEE 802.3's 2.5MHz ceiling.
First MDIO read returned a real PHY ID, first flash
Register 2 (PHY Identifier 1) read back 0x7 — matching the LAN8742's known OUI-derived ID. Cable in, "LINK UP — 100Mbps Full-Duplex." Cable out, "LINK DOWN." No debugging required on the link-detection path itself — everything upstream of it had already been confirmed correct.

— The Register Hunt —

Base addresses

Two adjacent blocks in the memory map, both crypto/network-adjacent peripherals STMicro grouped together:

PeripheralBase addressReference
ETHERNET MAC0x40028000RM0433 §58.11
ETH DMA (sub-block)0x40028000 + 0x1000RM0433 §58.11 — same chapter, offset region
SYSCFG0x58000400RM0433 memory map

RCC clock enables

RCC_AHB1ENR at offset 0x0D8 carries all three Ethernet clock gates in a contiguous block — worth noting that the reset-side mirror, RCC_AHB1RSTR, has ETH1MACRST at exactly bit 15 too, the same reset/enable bit-position consistency this project has now confirmed across several other peripheral pairs.

BitNameFunction
15ETH1MACENMAC core clock
16ETH1TXENTransmit clock domain
17ETH1RXENReceive clock domain

SYSCFG — MII vs RMII

SYSCFG_PMCR at offset 0x04, bits 23:21, field EPIS[2:0]. This single field decides whether the MAC expects the wider MII interface or the pin-efficient RMII interface — and it has to be set before the Ethernet clocks are enabled, per ST's own application guidance for this peripheral family, or the interface selection doesn't take reliably.

SYSCFG_PMCR.EPIS — confirmed values
/* RM0433 — EPIS[2:0], bits 23:21 */
000: MII   /* wider interface, more pins, not used here */
100: RMII  /* what Nucleo-144's onboard PHY wiring expects */

RMII pins — the table that mattered, and the solder bridges underneath it

UM2407's first Ethernet-adjacent table (RJ45 connector pinout, CN14) describes the physical connector, not the PHY-to-MCU wiring. The table that actually matters is further into the same board manual — Table 13, "Ethernet pin configuration" — which lists every RMII signal against both its STM32 pin and the specific solder bridge that has to be closed for that signal to reach the connector.

RMII SignalSTM32 PinAlternate FunctionSolder Bridge
REF_CLKPA1AF11SB57
MDIOPA2AF11SB72
MDCPC1AF11SB64
CRS_DVPA7AF11SB31
RXD0PC4AF11SB36
RXD1PC5AF11SB29
TX_ENPG11AF11SB27
TXD0PG13AF11SB30
TXD1PB13AF11JP6 (jumper, not solder bridge — shared with I2S_A_CK)

Every RMII signal on this board shares its pin with some other ST Zio/morpho connector function, and every one of those conflicts is resolved by a physical solder bridge rather than a runtime register — meaning a correctly-written driver can still produce nothing if a single one of nine bridges happens to be open. On this particular board, all nine were already closed in the Ethernet-enabled configuration; that's a fact about this specific board, not something the driver can detect or correct for in software.

MDIO — the management interface

Two registers carry the entire MDIO protocol, which itself follows a fixed IEEE 802.3 Clause 22 frame structure (preamble, start bits, opcode, PHY address, register address, turnaround, 16-bit data) that the peripheral generates automatically once the right fields are loaded.

RegisterOffsetKey fields
ETH_MACMDIOAR0x0200PA[4:0] (bits 25:21) PHY address · RDA[4:0] (bits 20:16) register · CR[3:0] (bits 11:8) clock divider · GOC[1:0] (bits 3:2) 01=write/11=read · MB (bit 0) busy
ETH_MACMDIODR0x0204MD[15:0] — the 16-bit value read or to be written
mdio_read() — blocking, polling MB
uint16_t mdio_read(uint8_t phy_addr, uint8_t reg_addr)
{
    ETH_MACMDIOAR = ((uint32_t)phy_addr << 21U)
                   | ((uint32_t)reg_addr << 16U)
                   | (MDIO_CR_VALUE << 8U)
                   | (3UL << 2U)   /* GOC=11, Read */
                   | 1UL;          /* MB */

    while ((ETH_MACMDIOAR & 1UL) != 0U) { }   /* wait for hardware to clear MB */

    return (uint16_t)(ETH_MACMDIODR & 0xFFFFU);
}

The MDC clock math — traced, not guessed

The CR[3:0] field selects the MDC clock divider based on which frequency band the underlying CSR clock falls into — get this wrong and the PHY either never responds or responds unreliably, without the peripheral itself reporting an error. Rather than pick a plausible-looking divider, the actual AHB1 frequency was traced through the clock tree this project's own boot code sets up.

Confirmed, not assumed
/* rcc_init_pll_480() sets: */
RCC_D1CFGR.HPRE = 1000b   /* -> rcc_hclk3 (=AHB1/AHB3) = sys_d1cpre_ck / 2 */
sys_d1cpre_ck = 480MHz    /* PLL1 output, D1CPRE not divided */
                          AHB1 = 480MHz / 2 = 240MHz — confirmed, not assumed

/* CR field table, RM0433 — 240MHz falls in the 150-250MHz band */
CR = 0100b  /* MDC = CSR/102 = 240MHz / 102 ≈ 2.35MHz */
            Inside IEEE 802.3's 2.5MHz ceiling

This is the same clock-tree-tracing discipline this project applied earlier when the UART baud rate turned out to depend on APB1 (240MHz) rather than the core clock (480MHz) — a repeated reminder that "the core runs at 480MHz" says almost nothing about what any individual peripheral's kernel clock actually is.

— Bring-Up —

A deliberately minimal driver

The full Ethernet driver includes DMA descriptor rings for actual frame TX/RX — that part is still being debugged separately and isn't part of this post. For link detection specifically, a stripped-down init function was written that configures exactly the clocks, GPIO pins, and MAC speed/duplex fields needed to talk to the PHY over MDIO, and deliberately does not enable the MAC's TE/RE (transmit/receive) state machines or touch DMA at all — there's no point enabling data paths that have nothing configured to receive them yet.

eth_init_link_only() — MAC/MDIO setup, no DMA
void eth_init_link_only(void)
{
    RCC_AHB4ENR |= GPIOA_EN | GPIOB_EN | GPIOC_EN | GPIOG_EN;
    RCC_APB4ENR |= SYSCFG_EN;

    /* RMII select MUST happen before ETH clocks are enabled */
    SYSCFG_PMCR = (SYSCFG_PMCR & ~EPIS_MASK) | EPIS_RMII;

    RCC_AHB1ENR |= ETH1MACEN | ETH1TXEN | ETH1RXEN;

    /* nine calls, one per RMII pin — all AF11 */
    gpio_set_af_eth(GPIOA_BASE, 1U);   /* PA1  REF_CLK */
    gpio_set_af_eth(GPIOA_BASE, 2U);   /* PA2  MDIO */
    /* ... remaining seven pins, same pattern ... */

    ETH_MACCR = FES_100MBPS | DM_FULL_DUPLEX;
    /* TE/RE deliberately NOT set — no DMA configured yet,
       enabling the MAC's actual data path here would be premature */
}

First cable-in, first result

The link monitor polls the PHY's standard IEEE 802.3 Basic Status Register (register 1, bit 2) for link state, and on the LAN8742 specifically, register 31's speed/duplex bits for the resolved auto-negotiation result once link comes up.

UART output — cable unplugged, then plugged in
TamgaOS STM32H753ZI — Ethernet PHY Link Monitor

[ETH] PHY ID (reg 2) = 0x7 (expect a real value, not 0x0000 or 0xFFFF)

[ETH] LINK DOWN
[ETH] LINK UP — 100Mbps Full-Duplex

Nine RMII pins, three RCC clock domains, one SYSCFG interface-select field, a two-register MDIO interface, and a clock divider traced through the actual clock tree — all correct on the first flash. LD1 blinks fast while searching for link, slows to a steady pulse once link comes up, entirely as a visual side-channel confirmation independent of the UART log.

— Final State —

What works now

✓ RCC clocks — MAC/TX/RX domains enabled
✓ SYSCFG RMII interface selection
✓ All nine RMII GPIO pins, AF11, cross-referenced against UM2407's solder bridge table
✓ MDIO read/write, clock divider traced through the real AHB1 frequency
✓ PHY ID read (0x7, matching the LAN8742)
✓ Real-time link up/down, speed, and duplex detection, confirmed on real hardware with a physical cable

ComponentStatus
RCC / SYSCFG / GPIO bring-upConfirmed working, first flash
MDIO read/writeConfirmed working — real PHY ID returned
Link/speed/duplex detectionConfirmed working on real hardware
DMA TX/RX descriptor ringsSeparate, unresolved — not covered in this post

Lessons worth writing down

A board manual can have the wrong table right next to the right one

UM2407's RJ45 connector pinout table and its actual Ethernet pin configuration table sit a few pages apart and are easy to conflate at a glance — one describes the physical connector, the other describes the MCU-to-PHY wiring, and only the second one is useful for writing a driver.

"240MHz" needs to be earned, not assumed, every single time

The MDC clock divider calculation depended on knowing AHB1's real frequency, not the core clock's. This is the same lesson the UART baud rate taught earlier in this project, reappearing in a completely different peripheral — every clock-dependent register needs its own trace back through the actual RCC prescaler configuration in use, not an assumption carried over from "the chip runs at 480MHz."

Nine pins agreeing is worth noticing when it happens

Every other multi-pin peripheral in this project (CAN, PWM's ALT numbers) has produced at least one pin that didn't match its neighbors. All nine RMII pins landing on AF11 without exception was unusual enough to be worth double-checking rather than trusting on the first read — and it held up.

Prove the simple case before building the complicated one

Link detection needed none of the DMA descriptor complexity that frame transmission requires. Separating "can this peripheral talk to its PHY at all" from "can this peripheral move a full Ethernet frame" turned a three-day register hunt into a contained, verifiable first milestone instead of one long undifferentiated debugging session where a DMA descriptor bug could have been mistaken for a clock or pin-mux problem.