[PATCH net-next v3 5/6] net: macb: implement timestamped MDIO writes

From: James Clark

Date: Thu Nov 05 2026 - 00:49:39 EST


Implement write_sts to provide system timestamp bounds for completion
of an MDIO write. This enables tighter system timestamp bounds in PHY
implementations of gettimex64.

Take system timestamps around the command register write, then shift
them by the time from the command write until the MDC edge that
clocks the last data bit, calculated from the MDC rate.

A clause 22 frame is 64 MDC periods long, and the transfer ends on the
rising edge of MDC in the last period, half way through it. So if the
frame starts with the command write, that edge comes 63.5 periods after
the command write. If MDC runs freely, the frame can start up to one
period after the command write, so by default bound the edge by 63.5 and
64.5 periods. Add the MACB_CAPS_MDC_WRITE_SYNC flag for a MAC that
starts the frame with the command write, and use exactly 63.5 periods
there.

Set the flag for RP1. Measurements performed on a Raspberry Pi CM5 with
its BCM54210PE PHY show the transfer consistently takes 63.5 periods.
The PHY's PHC was read with PTP_SYS_OFFSET_EXTENDED while the MDC
divisor was switched between 48 and 128 through /dev/mem. With 63.5
periods, the PHC offset jumped by under 2 ns at each switch, after
compensating for drift. A frame starting on average half a period after
the command write would have made it jump by 200 ns. The spread of the
PHC offsets also did not change with the divisor.

write_sts runs under the MDIO bus lock, so it can't call clk_get_rate(),
which takes the clock framework's prepare_lock: a clock provider that
does MDIO accesses, such as air_en8811h's, takes the two locks in the
other order. So record the MDC rate when macb_mdc_clk_div() chooses the
divisor, which it does at probe and when the MAC is opened or resumed.
If the pclk rate changes after that, neither the driver nor write_sts
adjusts to the new rate; handling this is outside the scope of this
patch.

Signed-off-by: James Clark <jjc@xxxxxxxxxx>
---
Changes in v3:
- Pass &bp->info to macb_is_gem(), which takes struct macb_info in
net-next

Changes in v2:
- Bound the edge by 63.5 and 64.5 MDC periods by default, and use
exactly 63.5 with MACB_CAPS_MDC_WRITE_SYNC, which RP1 sets
- Explain how 63.5 follows from the frame timing
- Record the MDC rate when the divisor is chosen, in place of reading
NCFGR (Théo)
- Explain why clk_get_rate() can't be called under the MDIO bus lock
(Théo)
- Say that neither the driver nor write_sts adjusts to a change of the
pclk rate, and that handling one is outside the scope of this patch
- Make macb_mdio_write_c22_sts() separate from macb_mdio_write_c22(),
as Andrew asked for UniMAC
- Express the delay with MDIO_C22_FRAME_BITS (Andrew)

drivers/net/ethernet/cadence/macb.h | 2 +
drivers/net/ethernet/cadence/macb_main.c | 115 ++++++++++++++++++++++-
2 files changed, 116 insertions(+), 1 deletion(-)

diff --git a/drivers/net/ethernet/cadence/macb.h b/drivers/net/ethernet/cadence/macb.h
index b7b4b6a38298..fa32e4100a45 100644
--- a/drivers/net/ethernet/cadence/macb.h
+++ b/drivers/net/ethernet/cadence/macb.h
@@ -785,6 +785,7 @@
#define MACB_CAPS_USRIO_HAS_MII BIT(26)
#define MACB_CAPS_USRIO_HAS_REFCLK_SOURCE BIT(27)
#define MACB_CAPS_USRIO_HAS_TSUCLK_SOURCE BIT(28)
+#define MACB_CAPS_MDC_WRITE_SYNC BIT(29)

/* LSO settings */
#define MACB_LSO_UFO_ENABLE 0x01
@@ -1373,6 +1374,7 @@ struct macb {
} hw_stats;

struct mii_bus *mii_bus;
+ unsigned long mdc_rate;
struct phylink *phylink;
struct phylink_config phylink_config;
struct phylink_pcs phylink_usx_pcs;
diff --git a/drivers/net/ethernet/cadence/macb_main.c b/drivers/net/ethernet/cadence/macb_main.c
index f087f278f9e5..c8ed34fc6d28 100644
--- a/drivers/net/ethernet/cadence/macb_main.c
+++ b/drivers/net/ethernet/cadence/macb_main.c
@@ -444,6 +444,112 @@ static int macb_mdio_write_c22(struct mii_bus *bus, int mii_id, int regnum,
return status;
}

+static u32 macb_mdc_divisor(struct macb *bp, u32 config)
+{
+ static const u16 gem_divisors[] = {
+ [GEM_CLK_DIV8] = 8,
+ [GEM_CLK_DIV16] = 16,
+ [GEM_CLK_DIV32] = 32,
+ [GEM_CLK_DIV48] = 48,
+ [GEM_CLK_DIV64] = 64,
+ [GEM_CLK_DIV96] = 96,
+ [GEM_CLK_DIV128] = 128,
+ [GEM_CLK_DIV224] = 224,
+ };
+ static const u16 macb_divisors[] = {
+ [MACB_CLK_DIV8] = 8,
+ [MACB_CLK_DIV16] = 16,
+ [MACB_CLK_DIV32] = 32,
+ [MACB_CLK_DIV64] = 64,
+ };
+
+ if (macb_is_gem(&bp->info))
+ return gem_divisors[GEM_BFEXT(CLK, config)];
+ return macb_divisors[MACB_BFEXT(CLK, config)];
+}
+
+/* write_sts runs under the MDIO bus lock, so it can't call
+ * clk_get_rate(): that takes the clock framework's prepare_lock, and a
+ * clock provider that does MDIO accesses, such as air_en8811h's, takes
+ * the two locks in the other order. So record the MDC rate when the
+ * divisor is chosen.
+ */
+static void macb_record_mdc_rate(struct macb *bp, unsigned long pclk_hz,
+ u32 config)
+{
+ unsigned long rate = pclk_hz / macb_mdc_divisor(bp, config);
+
+ if (rate)
+ WRITE_ONCE(bp->mdc_rate, rate);
+}
+
+static void macb_mdio_sts_delays(struct macb *bp, u64 *pre_ns, u64 *post_ns)
+{
+ u64 rate2 = 2ULL * READ_ONCE(bp->mdc_rate);
+ u64 half_periods = 2ULL * MDIO_C22_FRAME_BITS - 1;
+
+ /* The MDC edge that clocks the last bit comes half a period
+ * before the end of the frame. A MAC with MACB_CAPS_MDC_WRITE_SYNC
+ * starts the frame with the command write. Otherwise MDC may run
+ * freely, so the frame starts up to one period after the command
+ * write.
+ */
+ *pre_ns = div64_u64(half_periods * NSEC_PER_SEC, rate2);
+ if (!(bp->caps & MACB_CAPS_MDC_WRITE_SYNC))
+ half_periods += 2;
+ *post_ns = div64_u64(half_periods * NSEC_PER_SEC + rate2 - 1, rate2);
+}
+
+static int macb_mdio_write_c22_sts(struct mii_bus *bus, int mii_id, int regnum,
+ u16 value, struct ptp_system_timestamp *sts)
+{
+ struct macb *bp = bus->priv;
+ u64 pre_ns, post_ns;
+ unsigned long flags;
+ int status;
+ u32 cmd;
+
+ status = pm_runtime_resume_and_get(&bp->pdev->dev);
+ if (status < 0)
+ goto mdio_pm_exit;
+
+ status = macb_mdio_wait_for_idle(bp);
+ if (status < 0)
+ goto mdio_write_exit;
+
+ cmd = MACB_BF(SOF, MACB_MAN_C22_SOF)
+ | MACB_BF(RW, MACB_MAN_C22_WRITE)
+ | MACB_BF(PHYA, mii_id)
+ | MACB_BF(REGA, regnum)
+ | MACB_BF(CODE, MACB_MAN_C22_CODE)
+ | MACB_BF(DATA, value);
+ macb_mdio_sts_delays(bp, &pre_ns, &post_ns);
+
+ local_irq_save(flags);
+ ptp_read_system_prets(sts);
+ /* macb_writel() is relaxed; order it after the timestamp. */
+ mb();
+ macb_writel(bp, MAN, cmd);
+ /* Flush the posted write before taking the upper bound. */
+ macb_readl(bp, NSR);
+ /* Order the read-back before the system timestamp. */
+ rmb();
+ ptp_read_system_postts(sts);
+ local_irq_restore(flags);
+
+ ptp_adjust_system_prets(sts, pre_ns);
+ ptp_adjust_system_postts(sts, post_ns);
+
+ status = macb_mdio_wait_for_idle(bp);
+ if (status < 0)
+ goto mdio_write_exit;
+
+mdio_write_exit:
+ pm_runtime_put_autosuspend(&bp->pdev->dev);
+mdio_pm_exit:
+ return status;
+}
+
static int macb_mdio_write_c45(struct mii_bus *bus, int mii_id,
int devad, int regnum,
u16 value)
@@ -1231,6 +1337,8 @@ static int macb_mii_init(struct macb *bp)
bp->mii_bus->name = "MACB_mii_bus";
bp->mii_bus->read = &macb_mdio_read_c22;
bp->mii_bus->write = &macb_mdio_write_c22;
+ if (bp->mdc_rate)
+ bp->mii_bus->write_sts = &macb_mdio_write_c22_sts;
bp->mii_bus->read_c45 = &macb_mdio_read_c45;
bp->mii_bus->write_c45 = &macb_mdio_write_c45;
snprintf(bp->mii_bus->id, MII_BUS_ID_SIZE, "%s-%x",
@@ -3196,6 +3304,8 @@ static u32 gem_mdc_clk_div(struct macb *bp)
else
config = GEM_BF(CLK, GEM_CLK_DIV224);

+ macb_record_mdc_rate(bp, pclk_hz, config);
+
return config;
}

@@ -3217,6 +3327,8 @@ static u32 macb_mdc_clk_div(struct macb *bp)
else
config = MACB_BF(CLK, MACB_CLK_DIV64);

+ macb_record_mdc_rate(bp, pclk_hz, config);
+
return config;
}

@@ -6182,7 +6294,8 @@ static const struct macb_config raspberrypi_rp1_config = {
MACB_CAPS_JUMBO |
MACB_CAPS_GEM_HAS_PTP |
MACB_CAPS_EEE |
- MACB_CAPS_USRIO_HAS_MII,
+ MACB_CAPS_USRIO_HAS_MII |
+ MACB_CAPS_MDC_WRITE_SYNC,
.dma_burst_length = 16,
.usrio = &at91_default_usrio,
.jumbo_max_len = 10240,
--
2.56.0