Ethernet PHY Link Detection
Three Days of Register Hunting, No HAL, Down to a Cable Being Plugged In
— Register Hunt Timeline —
How the three days went
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_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.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.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.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:
| Peripheral | Base address | Reference |
|---|---|---|
| ETHERNET MAC | 0x40028000 | RM0433 §58.11 |
| ETH DMA (sub-block) | 0x40028000 + 0x1000 | RM0433 §58.11 — same chapter, offset region |
| SYSCFG | 0x58000400 | RM0433 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.
| Bit | Name | Function |
|---|---|---|
| 15 | ETH1MACEN | MAC core clock |
| 16 | ETH1TXEN | Transmit clock domain |
| 17 | ETH1RXEN | Receive 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.
/* 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 Signal | STM32 Pin | Alternate Function | Solder Bridge |
|---|---|---|---|
| REF_CLK | PA1 | AF11 | SB57 |
| MDIO | PA2 | AF11 | SB72 |
| MDC | PC1 | AF11 | SB64 |
| CRS_DV | PA7 | AF11 | SB31 |
| RXD0 | PC4 | AF11 | SB36 |
| RXD1 | PC5 | AF11 | SB29 |
| TX_EN | PG11 | AF11 | SB27 |
| TXD0 | PG13 | AF11 | SB30 |
| TXD1 | PB13 | AF11 | JP6 (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.
| Register | Offset | Key fields |
|---|---|---|
| ETH_MACMDIOAR | 0x0200 | PA[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_MACMDIODR | 0x0204 | MD[15:0] — the 16-bit value read or to be written |
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.
/* 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.
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.
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
| Component | Status |
|---|---|
| RCC / SYSCFG / GPIO bring-up | Confirmed working, first flash |
| MDIO read/write | Confirmed working — real PHY ID returned |
| Link/speed/duplex detection | Confirmed working on real hardware |
| DMA TX/RX descriptor rings | Separate, 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.