diff --git a/patches/linux/6.18.46/0052-net-dsa-mv88e6xxx-Shorten-PTP-timecounter-refresh-pe.patch b/patches/linux/6.18.46/0052-net-dsa-mv88e6xxx-Shorten-PTP-timecounter-refresh-pe.patch new file mode 100644 index 000000000..94acc4b4e --- /dev/null +++ b/patches/linux/6.18.46/0052-net-dsa-mv88e6xxx-Shorten-PTP-timecounter-refresh-pe.patch @@ -0,0 +1,45 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Mattias Walstrom +Date: Mon, 24 Aug 2026 14:35:00 +0200 +Subject: [PATCH 52/52] net: dsa: mv88e6xxx: Shorten PTP timecounter refresh + period + +Hardware timestamps are reconstructed with timecounter_cyc2time(), +which is only correct for timestamps within half a counter wrap of +cycle_last. On 250MHz devices (4ns/tick, e.g. 88E6190) the 32-bit +counter wraps every 2^34 ns (~17.2s), so half a wrap is ~8.6s. The +overflow work refreshes cycle_last every 8s, leaving only ~0.6s of +margin for delayed_work scheduling latency. + +On a loaded system the work can slip past that margin, and timestamps +then convert exactly one wrap (2^34 ns) off. Fed to ptp4l, such a +sample either steps the clock ~17.2s away (recovery at max_adj takes +hours) or repeatedly slams the PI servo into its frequency clamp, so +the clock never converges. + +Refresh every 2s instead, giving ~6.6s of margin on the fastest +(4ns/tick) devices. The 8ns and 10ns devices wrap even slower, so the +margin there is larger still. The cost is one TAI read per 2s. + +Observed on a CN9130 board with three 88E6190 switches: offsets +reported as -2^34 ns + slew, with sporadic outliers at exactly +true_offset + 2^34 ns. +--- + drivers/net/dsa/mv88e6xxx/ptp.c | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/drivers/net/dsa/mv88e6xxx/ptp.c b/drivers/net/dsa/mv88e6xxx/ptp.c +index 1111111111111..2222222222222 100644 +--- a/drivers/net/dsa/mv88e6xxx/ptp.c ++++ b/drivers/net/dsa/mv88e6xxx/ptp.c +@@ -480,7 +480,7 @@ static u64 mv88e6xxx_ptp_clock_read(struct cyclecounter *cc) + /* With a 250MHz input clock, the 32-bit timestamp counter overflows in ~17.2 + * seconds; this task forces periodic reads so that we don't miss any. + */ +-#define MV88E6XXX_TAI_OVERFLOW_PERIOD (HZ * 8) ++#define MV88E6XXX_TAI_OVERFLOW_PERIOD (HZ * 2) + static void mv88e6xxx_ptp_overflow_check(struct work_struct *work) + { + struct delayed_work *dw = to_delayed_work(work); +-- +2.43.0 diff --git a/patches/linux/6.18.46/0053-net-dsa-mv88e6xxx-DEBUG-log-hardware-timestamps-at-r.patch b/patches/linux/6.18.46/0053-net-dsa-mv88e6xxx-DEBUG-log-hardware-timestamps-at-r.patch new file mode 100644 index 000000000..e689f437c --- /dev/null +++ b/patches/linux/6.18.46/0053-net-dsa-mv88e6xxx-DEBUG-log-hardware-timestamps-at-r.patch @@ -0,0 +1,74 @@ +From 0000000000000000000000000000000000000001 Mon Sep 17 00:00:00 2001 +From: Mattias Walstrom +Date: Tue, 25 Aug 2026 09:00:00 +0200 +Subject: [PATCH 53/53] net: dsa: mv88e6xxx: DEBUG: log hardware timestamps at + risk of wrap error + +NOT FOR UPSTREAM - debug instrumentation. + +Hardware timestamps are reconstructed against the timecounter's +cycle_last; a timestamp more than half a counter wrap away converts +exactly one wrap (2^34 ns on 4ns/tick devices) off. With the refresh +period at 2s, a legitimate conversion is never more than ~2.5s +(~0x25000000 ticks) from cycle_last. + +Warn on every conversion in the 4.3s-12.9s band (raw delta between +0x40000000 and 0xc0000000 ticks): these are the candidates for a +2^34 ns wrap error, and the log tells us which port, direction, and +how far off - i.e. whether the refresh work stalled or the timestamp +sat latched too long. +--- + drivers/net/dsa/mv88e6xxx/hwtstamp.c | 24 ++++++++++++++++++++++-- + 1 file changed, 22 insertions(+), 2 deletions(-) + +diff --git a/drivers/net/dsa/mv88e6xxx/hwtstamp.c b/drivers/net/dsa/mv88e6xxx/hwtstamp.c +index 3333333333333..4444444444444 100644 +--- a/drivers/net/dsa/mv88e6xxx/hwtstamp.c ++++ b/drivers/net/dsa/mv88e6xxx/hwtstamp.c +@@ -20,6 +20,26 @@ + + #define SKB_PTP_TYPE(__skb) (*(unsigned int *)((__skb)->cb)) + ++/* DEBUG: must be called with the reg lock held. Warn on conversions in ++ * the danger band around half a counter wrap from cycle_last; on 4ns/tick ++ * devices these risk coming out exactly 2^34 ns off. ++ */ ++static u64 mv88e6xxx_hwts_cyc2time(struct mv88e6xxx_chip *chip, int port, ++ const char *dir, u32 raw) ++{ ++ u32 last = (u32)chip->tstamp_tc.cycle_last; ++ u32 delta = raw - last; ++ u64 ns; ++ ++ ns = timecounter_cyc2time(&chip->tstamp_tc, raw); ++ if (delta > 0x40000000 && delta < 0xc0000000) ++ dev_warn(chip->dev, ++ "p%d: %s hwtstamp %08x is %u ticks from cycle_last %08x, converted %llu - possible 2^34 ns wrap\n", ++ port, dir, raw, delta, last, ns); ++ ++ return ns; ++} ++ + static int mv88e6xxx_port_ptp_read(struct mv88e6xxx_chip *chip, int port, + int addr, u16 *data, int len) + { +@@ -291,7 +311,7 @@ static void mv88e6xxx_get_rxts(struct mv88e6xxx_chip *chip, + ns = timehi << 16 | timelo; + + mv88e6xxx_reg_lock(chip); +- ns = timecounter_cyc2time(&chip->tstamp_tc, ns); ++ ns = mv88e6xxx_hwts_cyc2time(chip, ps->port_id, "rx", ns); + mv88e6xxx_reg_unlock(chip); + shwt = skb_hwtstamps(skb); + memset(shwt, 0, sizeof(*shwt)); +@@ -411,7 +431,7 @@ static int mv88e6xxx_txtstamp_work(struct mv88e6xxx_chip *chip, + memset(&shhwtstamps, 0, sizeof(shhwtstamps)); + time_raw = ((u32)departure_block[2] << 16) | departure_block[1]; + mv88e6xxx_reg_lock(chip); +- ns = timecounter_cyc2time(&chip->tstamp_tc, time_raw); ++ ns = mv88e6xxx_hwts_cyc2time(chip, ps->port_id, "tx", time_raw); + mv88e6xxx_reg_unlock(chip); + shhwtstamps.hwtstamp = ns_to_ktime(ns); + +-- +2.43.0 diff --git a/test/case/interfaces/lag_failure/test.py b/test/case/interfaces/lag_failure/test.py index 124ffebfe..afb61e96b 100755 --- a/test/case/interfaces/lag_failure/test.py +++ b/test/case/interfaces/lag_failure/test.py @@ -11,6 +11,7 @@ each failure mode step using the `mon` interface. """ +import json from time import time import infamy import infamy.lag @@ -37,7 +38,7 @@ def forward(self, lb1, lb2): getattr(self.lb1, lb1)() getattr(self.lb2, lb2)() - def fail_check(self, peer): + def fail_check(self, peer, duts=()): """Verify connectivity with a given peer during failure.""" sequence = [ ("forward", "forward"), @@ -55,7 +56,7 @@ def fail_check(self, peer): try: print(f"{lb1:<8} | {lb2:<8} | {'...':<8}", end="\r# ") self.forward(lb1, lb2) - self.net.must_reach(peer, timeout=30) + self.net.must_reach(peer, timeout=60) print(f"{lb1:<8} | {lb2:<8} | {'OK':<8} in " f"{time() - state_start:.2f}s") except Exception as e: @@ -63,6 +64,14 @@ def fail_check(self, peer): f"{time() - state_start:.2f}s") print(f"\nError encountered: {e}") print(f"Link breakers were in state: LB1='{lb1}', LB2='{lb2}'") + for dut in duts: + try: + state = dut.get_data("/ietf-interfaces:interfaces" + "/interface[name='lag0']") + print(f"{dut} lag0 state: " + f"{json.dumps(state, indent=2)}") + except Exception as err: + print(f"{dut}: failed to get lag0 state: {err}") raise print(f"Total time: {time() - total_start:.2f}s") @@ -139,6 +148,6 @@ def dut_init(dut, addr, peer): ns.must_reach(IP2, timeout=30) with test.step("Verify failure modes"): - lb.fail_check(IP2) + lb.fail_check(IP2, (dut1, dut2)) test.succeed()