From 2b070614cb77281739906b9a108cc3af3e3b4cd7 Mon Sep 17 00:00:00 2001 From: James Clark Date: Mon, 5 Oct 2026 17:52:11 +0700 Subject: [PATCH 1/4] net: mdio: add timestamped write operation Add an optional write_sts bus operation returning system timestamp bounds for completion of an MDIO write. This allows PHY drivers to obtain tighter bounds when implementing gettimex64. SPI introduced a similar facility to allow a device driver to obtain timestamp bounds from the bus controller in commit 79591b7db21d ("spi: Add a PTP system timestamp to the transfer structure"). A PHY driver using this operation is responsible for fallback if the bus driver does not implement it. Signed-off-by: James Clark --- drivers/net/phy/mdio_bus.c | 42 ++++++++++++++++++++++++++++++++++++++ include/linux/mdio.h | 16 +++++++++++++++ include/linux/phy.h | 22 ++++++++++++++++++++ 3 files changed, 80 insertions(+) diff --git a/drivers/net/phy/mdio_bus.c b/drivers/net/phy/mdio_bus.c index 4354241137d50d..0d459bad912d28 100644 --- a/drivers/net/phy/mdio_bus.c +++ b/drivers/net/phy/mdio_bus.c @@ -496,6 +496,48 @@ int __mdiobus_write(struct mii_bus *bus, int addr, u32 regnum, u16 val) } EXPORT_SYMBOL(__mdiobus_write); +/** + * __mdiobus_write_sts - Timestamped version of the __mdiobus_write function + * @bus: the mii_bus struct + * @addr: the phy address + * @regnum: register number to write + * @val: value to write to @regnum + * @sts: system timestamps bounding completion, or NULL + * + * Write a MDIO bus register, with system timestamps bounding completion; + * a transfer is considered complete on the rising edge of the MDC + * that clocks the last data bit. Caller must hold the mdio bus lock. + * + * If @sts is NULL, perform an ordinary write. Otherwise, return + * -EOPNOTSUPP if the bus has no write_sts operation. + * + * NOTE: MUST NOT be called from interrupt context. + */ +int __mdiobus_write_sts(struct mii_bus *bus, int addr, u32 regnum, u16 val, + struct ptp_system_timestamp *sts) +{ + int err; + + if (!sts) + return __mdiobus_write(bus, addr, regnum, val); + + lockdep_assert_held_once(&bus->mdio_lock); + + if (addr >= PHY_MAX_ADDR) + return -ENXIO; + + if (bus->write_sts) + err = bus->write_sts(bus, addr, regnum, val, sts); + else + err = -EOPNOTSUPP; + + trace_mdio_access(bus, 0, addr, regnum, val, err); + mdiobus_stats_acct(&bus->stats[addr], false, err); + + return err; +} +EXPORT_SYMBOL_GPL(__mdiobus_write_sts); + /** * __mdiobus_modify_changed - Unlocked version of the mdiobus_modify function * @bus: the mii_bus struct diff --git a/include/linux/mdio.h b/include/linux/mdio.h index c640ba44dd6eed..273d32577a2287 100644 --- a/include/linux/mdio.h +++ b/include/linux/mdio.h @@ -8,10 +8,13 @@ #include #include +#include #include +#include struct gpio_desc; struct mii_bus; +struct ptp_system_timestamp; struct reset_control; /* Multiple levels of nesting are possible. However typically this is @@ -569,8 +572,21 @@ static inline void mii_c73_mod_linkmode(unsigned long *adv, u16 *lpa) /* 5GBASE_KR not implemented */ } +/* Round the lower bound down and the upper bound up. rate must be nonzero. */ +static inline void mdiobus_sts_bounds(unsigned long rate, u32 divisor, + u32 min_cycles, u32 max_cycles, + u64 *pre_ns, u64 *post_ns) +{ + u64 numerator = (u64)divisor * NSEC_PER_SEC; + + *pre_ns = div64_u64(min_cycles * numerator, rate); + *post_ns = div64_u64(max_cycles * numerator + rate - 1, rate); +} + int __mdiobus_read(struct mii_bus *bus, int addr, u32 regnum); int __mdiobus_write(struct mii_bus *bus, int addr, u32 regnum, u16 val); +int __mdiobus_write_sts(struct mii_bus *bus, int addr, u32 regnum, u16 val, + struct ptp_system_timestamp *sts); int __mdiobus_modify(struct mii_bus *bus, int addr, u32 regnum, u16 mask, u16 set); int __mdiobus_modify_changed(struct mii_bus *bus, int addr, u32 regnum, diff --git a/include/linux/phy.h b/include/linux/phy.h index 0bc00a4cceb241..5f2f9f85f2ff1a 100644 --- a/include/linux/phy.h +++ b/include/linux/phy.h @@ -368,6 +368,12 @@ struct mii_bus { int (*read)(struct mii_bus *bus, int addr, int regnum); /** @write: Perform a write transfer on the bus */ int (*write)(struct mii_bus *bus, int addr, int regnum, u16 val); + /** + * @write_sts: Perform a write transfer on the bus, + * with system timestamps bounding its completion + */ + int (*write_sts)(struct mii_bus *bus, int addr, int regnum, u16 val, + struct ptp_system_timestamp *sts); /** @read_c45: Perform a C45 read transfer on the bus */ int (*read_c45)(struct mii_bus *bus, int addr, int devnum, int regnum); /** @write_c45: Perform a C45 write transfer on the bus */ @@ -1451,6 +1457,22 @@ static inline int __phy_write(struct phy_device *phydev, u32 regnum, u16 val) val); } +/** + * __phy_write_sts - Write a PHY register with a frame-end timestamp + * @phydev: the phy_device struct + * @regnum: clause 22 register number + * @val: value to write + * @sts: system timestamp bounds, or NULL + * + * The caller must hold the MDIO bus lock. + */ +static inline int __phy_write_sts(struct phy_device *phydev, u32 regnum, u16 val, + struct ptp_system_timestamp *sts) +{ + return __mdiobus_write_sts(phydev->mdio.bus, phydev->mdio.addr, + regnum, val, sts); +} + /** * __phy_modify_changed() - Convenience function for modifying a PHY register * @phydev: a pointer to a &struct phy_device From 31caf6a89abbabe51490e52d5ff978ddf3f38d94 Mon Sep 17 00:00:00 2001 From: James Clark Date: Mon, 5 Oct 2026 18:33:48 +0700 Subject: [PATCH 2/4] net: phy: broadcom: use timestamped MDIO writes in gettimex64 Use __phy_write_sts() to obtain tighter system timestamp bounds in gettimex64 by timestamping completion of the write that triggers capture of the PHC time. Signed-off-by: James Clark --- drivers/net/phy/bcm-phy-lib.c | 26 ++++++++++++++++++++++++++ drivers/net/phy/bcm-phy-lib.h | 4 ++++ drivers/net/phy/bcm-phy-ptp.c | 30 +++++++++++++++++++++++++----- 3 files changed, 55 insertions(+), 5 deletions(-) diff --git a/drivers/net/phy/bcm-phy-lib.c b/drivers/net/phy/bcm-phy-lib.c index 5198d66dbbc048..908736b4474fbe 100644 --- a/drivers/net/phy/bcm-phy-lib.c +++ b/drivers/net/phy/bcm-phy-lib.c @@ -42,6 +42,32 @@ int bcm_phy_write_exp(struct phy_device *phydev, u16 reg, u16 val) } EXPORT_SYMBOL_GPL(bcm_phy_write_exp); +int __bcm_phy_write_exp_sts(struct phy_device *phydev, u16 reg, u16 val, + struct ptp_system_timestamp *sts) +{ + int rc; + + rc = __phy_write(phydev, MII_BCM54XX_EXP_SEL, reg); + if (rc < 0) + return rc; + + return __phy_write_sts(phydev, MII_BCM54XX_EXP_DATA, val, sts); +} +EXPORT_SYMBOL_GPL(__bcm_phy_write_exp_sts); + +int bcm_phy_write_exp_sts(struct phy_device *phydev, u16 reg, u16 val, + struct ptp_system_timestamp *sts) +{ + int rc; + + phy_lock_mdio_bus(phydev); + rc = __bcm_phy_write_exp_sts(phydev, reg, val, sts); + phy_unlock_mdio_bus(phydev); + + return rc; +} +EXPORT_SYMBOL_GPL(bcm_phy_write_exp_sts); + int __bcm_phy_read_exp(struct phy_device *phydev, u16 reg) { int val; diff --git a/drivers/net/phy/bcm-phy-lib.h b/drivers/net/phy/bcm-phy-lib.h index bceddbc860eb28..a0dfc604b74ef4 100644 --- a/drivers/net/phy/bcm-phy-lib.h +++ b/drivers/net/phy/bcm-phy-lib.h @@ -31,9 +31,13 @@ struct ethtool_wolinfo; int __bcm_phy_write_exp(struct phy_device *phydev, u16 reg, u16 val); +int __bcm_phy_write_exp_sts(struct phy_device *phydev, u16 reg, u16 val, + struct ptp_system_timestamp *sts); int __bcm_phy_read_exp(struct phy_device *phydev, u16 reg); int __bcm_phy_modify_exp(struct phy_device *phydev, u16 reg, u16 mask, u16 set); int bcm_phy_write_exp(struct phy_device *phydev, u16 reg, u16 val); +int bcm_phy_write_exp_sts(struct phy_device *phydev, u16 reg, u16 val, + struct ptp_system_timestamp *sts); int bcm_phy_read_exp(struct phy_device *phydev, u16 reg); int bcm_phy_modify_exp(struct phy_device *phydev, u16 reg, u16 mask, u16 set); diff --git a/drivers/net/phy/bcm-phy-ptp.c b/drivers/net/phy/bcm-phy-ptp.c index bbaf8e4deedb26..4c323b63e7feb8 100644 --- a/drivers/net/phy/bcm-phy-ptp.c +++ b/drivers/net/phy/bcm-phy-ptp.c @@ -214,22 +214,42 @@ static void bcm_ptp_framesync(struct phy_device *phydev, u16 ctrl) bcm_phy_write_exp(phydev, NSE_CTRL, ctrl | NSE_CPU_FRAMESYNC); } +static int bcm_ptp_framesync_sts(struct phy_device *phydev, u16 ctrl, + struct ptp_system_timestamp *sts) +{ + return bcm_phy_write_exp_sts(phydev, NSE_CTRL, + ctrl | NSE_CPU_FRAMESYNC, sts); +} + static int bcm_ptp_framesync_ts(struct phy_device *phydev, struct ptp_system_timestamp *sts, struct timespec64 *ts, u16 orig_ctrl) { + bool mdio_sts = false; u16 ctrl, reg; - int i; + int i, err; ctrl = bcm_ptp_framesync_disable(phydev, orig_ctrl); - ptp_read_system_prets(sts); - /* trigger framesync + capture */ - bcm_ptp_framesync(phydev, ctrl | NSE_CAPTURE_EN); + if (sts && phydev->mdio.bus->write_sts) { + mdio_sts = true; + err = bcm_ptp_framesync_sts(phydev, ctrl | NSE_CAPTURE_EN, sts); + if (err == -EOPNOTSUPP) { + mdio_sts = false; + } else if (err) { + /* sts may be uninitialized on error */ + bcm_ptp_framesync_restore(phydev, orig_ctrl); + return err; + } + } - ptp_read_system_postts(sts); + if (!mdio_sts) { + ptp_read_system_prets(sts); + bcm_ptp_framesync(phydev, ctrl | NSE_CAPTURE_EN); + ptp_read_system_postts(sts); + } /* poll for FSYNC interrupt from TS capture */ for (i = 0; i < 10; i++) { From 70139d4d2e9d1abdf282b23da326b5b04e9e5aff Mon Sep 17 00:00:00 2001 From: James Clark Date: Mon, 5 Oct 2026 19:03:21 +0700 Subject: [PATCH 3/4] net: macb: implement timestamped MDIO writes 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 add bounds on the transfer time calculated from the peripheral clock rate and configured MDC divider. Signed-off-by: James Clark --- drivers/net/ethernet/cadence/macb.h | 2 + drivers/net/ethernet/cadence/macb_main.c | 84 +++++++++++++++++++++--- 2 files changed, 78 insertions(+), 8 deletions(-) diff --git a/drivers/net/ethernet/cadence/macb.h b/drivers/net/ethernet/cadence/macb.h index d046a922a1a4ef..87f15e9ad54d2a 100644 --- a/drivers/net/ethernet/cadence/macb.h +++ b/drivers/net/ethernet/cadence/macb.h @@ -1358,6 +1358,8 @@ struct macb { struct macb_or_gem_ops macbgem_ops; struct mii_bus *mii_bus; + u64 mdio_write_pre_ns; + u64 mdio_write_post_ns; 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 95bc3839173c15..6260b358fdd2f6 100644 --- a/drivers/net/ethernet/cadence/macb_main.c +++ b/drivers/net/ethernet/cadence/macb_main.c @@ -416,11 +416,13 @@ static int macb_mdio_read_c45(struct mii_bus *bus, int mii_id, int devad, return status; } -static int macb_mdio_write_c22(struct mii_bus *bus, int mii_id, int regnum, - u16 value) +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; + unsigned long flags; int status; + u32 cmd; status = pm_runtime_resume_and_get(&bp->pdev->dev); if (status < 0) @@ -430,12 +432,31 @@ static int macb_mdio_write_c22(struct mii_bus *bus, int mii_id, int regnum, if (status < 0) goto mdio_write_exit; - macb_writel(bp, MAN, (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))); + 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); + + if (sts) { + local_irq_save(flags); + ptp_read_system_prets(sts); + /* macb_writel() is relaxed; order it after the timestamp. */ + mb(); + } + macb_writel(bp, MAN, cmd); + if (sts) { + /* 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); + + timespec64_add_ns(&sts->pre_ts, bp->mdio_write_pre_ns); + timespec64_add_ns(&sts->post_ts, bp->mdio_write_post_ns); + } status = macb_mdio_wait_for_idle(bp); if (status < 0) @@ -448,6 +469,12 @@ static int macb_mdio_write_c22(struct mii_bus *bus, int mii_id, int regnum, return status; } +static int macb_mdio_write_c22(struct mii_bus *bus, int mii_id, int regnum, + u16 value) +{ + return macb_mdio_write_c22_sts(bus, mii_id, regnum, value, NULL); +} + static int macb_mdio_write_c45(struct mii_bus *bus, int mii_id, int devad, int regnum, u16 value) @@ -1150,6 +1177,45 @@ static int macb_mdiobus_register(struct macb *bp, struct device_node *mdio_np) return mdiobus_register(bp->mii_bus); } +static bool macb_mdio_init_sts(struct macb *bp) +{ + 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, + }; + unsigned long rate = clk_get_rate(bp->pclk); + u32 config = macb_readl(bp, NCFGR); + u32 divisor; + + if (!rate) + return false; + + if (macb_is_gem(bp)) + divisor = gem_divisors[GEM_BFEXT(CLK, config)]; + else + divisor = macb_divisors[MACB_BFEXT(CLK, config)]; + + /* A full-preamble write completes 63..65 MDC periods after the + * command write, including engine start phase. + */ + mdiobus_sts_bounds(rate, divisor, 63, 65, + &bp->mdio_write_pre_ns, &bp->mdio_write_post_ns); + + return true; +} + static int macb_mii_init(struct macb *bp) { struct device_node *mdio_np, *np = bp->pdev->dev.of_node; @@ -1175,6 +1241,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 (macb_mdio_init_sts(bp)) + 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; bp->mii_bus->reset = &macb_mdio_reset; From d8827283dd742e5cd8bf0b99375393859132c0f5 Mon Sep 17 00:00:00 2001 From: James Clark Date: Mon, 5 Oct 2026 19:47:54 +0700 Subject: [PATCH 4/4] net: mdio: bcm-unimac: implement timestamped MDIO writes 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 start, then add bounds on the transfer time calculated from the reference clock rate and configured MDC divider. Use a 200 MHz reference rate for BCM2711 GENET, whose clock is not described in DT. Signed-off-by: James Clark --- drivers/net/mdio/mdio-bcm-unimac.c | 85 ++++++++++++++++++++++++++++-- 1 file changed, 81 insertions(+), 4 deletions(-) diff --git a/drivers/net/mdio/mdio-bcm-unimac.c b/drivers/net/mdio/mdio-bcm-unimac.c index 37e35f282d9ad8..07efc592e5e601 100644 --- a/drivers/net/mdio/mdio-bcm-unimac.c +++ b/drivers/net/mdio/mdio-bcm-unimac.c @@ -16,6 +16,7 @@ #include #include #include +#include #include #define MDIO_CMD 0x00 @@ -42,6 +43,7 @@ struct unimac_mdio_priv { void *wait_func_data; struct clk *clk; u32 clk_freq; + unsigned long mdio_ref_rate; }; static inline u32 unimac_mdio_readl(struct unimac_mdio_priv *priv, u32 offset) @@ -73,6 +75,27 @@ static inline void unimac_mdio_start(struct unimac_mdio_priv *priv) unimac_mdio_writel(priv, reg, MDIO_CMD); } +static void unimac_mdio_start_sts(struct unimac_mdio_priv *priv, + struct ptp_system_timestamp *sts) +{ + unsigned long flags; + u32 reg; + + reg = unimac_mdio_readl(priv, MDIO_CMD); + reg |= MDIO_START_BUSY; + local_irq_save(flags); + ptp_read_system_prets(sts); + /* Order the timestamp before the relaxed command write. */ + mb(); + unimac_mdio_writel(priv, reg, MDIO_CMD); + /* Flush the posted write before taking the upper bound. */ + unimac_mdio_readl(priv, MDIO_CMD); + /* Order the read-back before the system timestamp. */ + rmb(); + ptp_read_system_postts(sts); + local_irq_restore(flags); +} + static int unimac_mdio_poll(void *wait_func_data) { struct unimac_mdio_priv *priv = wait_func_data; @@ -127,30 +150,69 @@ static int unimac_mdio_read(struct mii_bus *bus, int phy_id, int reg) return ret; } -static int unimac_mdio_write(struct mii_bus *bus, int phy_id, - int reg, u16 val) +static void unimac_mdio_sts_bounds(struct unimac_mdio_priv *priv, u32 config, + u64 *pre_ns, u64 *post_ns) +{ + u32 divisor; + + divisor = 2 * (((config >> MDIO_CLK_DIV_SHIFT) & MDIO_CLK_DIV_MASK) + 1); + /* A full-preamble write completes 63..65 MDC periods after the + * command write, including engine start phase. + */ + mdiobus_sts_bounds(priv->mdio_ref_rate, divisor, 63, 65, pre_ns, post_ns); +} + +static int unimac_mdio_write_sts(struct mii_bus *bus, int phy_id, int reg, + u16 val, struct ptp_system_timestamp *sts) { struct unimac_mdio_priv *priv = bus->priv; - u32 cmd; + u64 pre_ns, post_ns; + u32 cmd, config; int ret; ret = clk_prepare_enable(priv->clk); if (ret) return ret; + if (sts) { + config = unimac_mdio_readl(priv, MDIO_CFG); + if (config & MDIO_SUPP_PREAMBLE) { + ret = -EOPNOTSUPP; + goto out; + } + unimac_mdio_sts_bounds(priv, config, &pre_ns, &post_ns); + } + /* Prepare the write operation */ cmd = MDIO_WR | (phy_id << MDIO_PMD_SHIFT) | (reg << MDIO_REG_SHIFT) | (0xffff & val); unimac_mdio_writel(priv, cmd, MDIO_CMD); - unimac_mdio_start(priv); + if (sts) { + unimac_mdio_start_sts(priv, sts); + timespec64_add_ns(&sts->pre_ts, pre_ns); + timespec64_add_ns(&sts->post_ts, post_ns); + } else { + unimac_mdio_start(priv); + } ret = priv->wait_func(priv->wait_func_data); + /* Some wait callbacks do not propagate a completion timeout. */ + if (!ret && sts && + (unimac_mdio_readl(priv, MDIO_CMD) & MDIO_START_BUSY)) + ret = -ETIMEDOUT; +out: clk_disable_unprepare(priv->clk); return ret; } +static int unimac_mdio_write(struct mii_bus *bus, int phy_id, + int reg, u16 val) +{ + return unimac_mdio_write_sts(bus, phy_id, reg, val, NULL); +} + /* Workaround for integrated BCM7xxx Gigabit PHYs which have a problem with * their internal MDIO management controller making them fail to successfully * be read from or written to for the first transaction. We insert a dummy @@ -234,6 +296,19 @@ static int unimac_mdio_clk_set(struct unimac_mdio_priv *priv) return ret; } +static bool unimac_mdio_init_sts(struct unimac_mdio_priv *priv, + struct device *dev) +{ + priv->mdio_ref_rate = clk_get_rate(priv->clk); + /* BCM2711's 200 MHz GENET reference clock is not described in DT. */ + if (!priv->mdio_ref_rate && dev->parent && + of_device_is_compatible(dev->parent->of_node, + "brcm,bcm2711-genet-v5")) + priv->mdio_ref_rate = 200000000; + + return priv->mdio_ref_rate != 0; +} + static int unimac_mdio_probe(struct platform_device *pdev) { struct unimac_mdio_pdata *pdata = pdev->dev.platform_data; @@ -292,6 +367,8 @@ static int unimac_mdio_probe(struct platform_device *pdev) bus->parent = &pdev->dev; bus->read = unimac_mdio_read; bus->write = unimac_mdio_write; + if (unimac_mdio_init_sts(priv, &pdev->dev)) + bus->write_sts = unimac_mdio_write_sts; bus->reset = unimac_mdio_reset; snprintf(bus->id, MII_BUS_ID_SIZE, "%s-%d", pdev->name, pdev->id);