From 2cbd7c4a2bba909cd8dc01701b894b8bfd9cac3b Mon Sep 17 00:00:00 2001 From: Jim Ramsay Date: Thu, 22 Oct 2026 20:14:23 +0000 Subject: [PATCH 1/2] Add configurable mock PHC PPS delay --- README.md | 13 ++++++ include/linux/ptp_mock.h | 3 ++ ptp/ptp_mock.c | 85 ++++++++++++++++++++++++++++++++++------ scripts/test-phc.sh | 71 ++++++++++++++++++++++++--------- 4 files changed, 141 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index b0fc416..ac54597 100644 --- a/README.md +++ b/README.md @@ -120,6 +120,19 @@ echo "3:${IF0_IDX} 3:${IF1_IDX}" > /sys/bus/netdevsim/link_device exec 3<&- ``` +The mock PHC exposes a per-device `extts_delay_ms` sysfs attribute under +`/sys/class/nsim_ptp/nsim_ptpN/`. It delays EXTTS/PPS event delivery after each +`CLOCK_TAI` second boundary while keeping the event timestamp aligned to the +configured delayed edge. The default is `0`; values from `0` to `900` ms are +accepted. For example, a 400 ms delay exceeds linuxptp ts2phc's 333 ms poll +window: + +```bash +echo 400 | sudo tee /sys/class/nsim_ptp/nsim_ptp0/extts_delay_ms +# Restore normal PPS timing +echo 0 | sudo tee /sys/class/nsim_ptp/nsim_ptp0/extts_delay_ms +``` + ## Container / Kubernetes Notes When using netdevsim inside containers or Kubernetes pods: diff --git a/include/linux/ptp_mock.h b/include/linux/ptp_mock.h index d7ae58a..63f4f46 100644 --- a/include/linux/ptp_mock.h +++ b/include/linux/ptp_mock.h @@ -11,6 +11,7 @@ #include #include #include +#include struct device; struct mock_phc { @@ -34,6 +35,8 @@ struct mock_phc { bool extts_enabled; int extts_channel; u64 last_extts_sec; /* dedup: last CLOCK_TAI second we reported */ + unsigned int extts_delay_ms; /* delivery delay after each TAI second boundary */ + struct kobj_attribute extts_delay_attr; }; #if IS_ENABLED(CONFIG_PTP_1588_CLOCK_MOCK) diff --git a/ptp/ptp_mock.c b/ptp/ptp_mock.c index b05f4f9..0d52f07 100644 --- a/ptp/ptp_mock.c +++ b/ptp/ptp_mock.c @@ -118,8 +118,8 @@ static int mock_phc_gettime64(struct ptp_clock_info *info, } /* - * Simulate a GNSS 1PPS input: one EXTTS edge per CLOCK_TAI second, with - * the event timestamp equal to the PHC reading at that edge. + * Simulate a GNSS 1PPS input: one EXTTS event per CLOCK_TAI second, optionally + * delayed by extts_delay_ms, with a timestamp captured at the delayed edge. * * Why TAI (not PHC) second boundaries? * ts2phc pairs EXTTS with NMEA, which is UTC converted to TAI via the @@ -129,15 +129,17 @@ static int mock_phc_gettime64(struct ptp_clock_info *info, * (offset 0 ↔ -1e9), which flapped T-GM clock class 6↔248 and broke * downstream OC lock. * - * Why report the PHC reading corrected back to the TAI edge? - * A polled hrtimer can run up to ~1 ms after the boundary. Stamping - * the live PHC then injects that latency into every sample (typically + * Why correct the timestamp to the configured edge? + * A polled hrtimer can run up to ~1 ms after the edge. Stamping the + * live PHC then injects that latency into every sample (typically * several µs–ms of jitter), which exceeds ts2phc's lock threshold - * (1500 ns). Subtracting the observed TAI lateness approximates the - * PHC capture at the true edge (valid while |freq_ppb| is modest). + * (1500 ns). Subtract timer lateness beyond the configured delay so + * the timestamp approximates the PHC capture at the delayed edge + * (valid while |freq_ppb| is modest). */ #define EXTTS_IDLE_POLL_NS (NSEC_PER_SEC / 10) /* 100 ms while disabled */ #define EXTTS_EDGE_SLACK_NS (NSEC_PER_MSEC) /* wake 1 ms after edge */ +#define EXTTS_MAX_DELAY_MS 900 /* keep events within the TAI second */ static enum hrtimer_restart mock_phc_extts_timer(struct hrtimer *timer) { @@ -145,6 +147,7 @@ static enum hrtimer_restart mock_phc_extts_timer(struct hrtimer *timer) struct ptp_clock_event event; unsigned long flags; u64 tai_ns, tai_sec, phc_ns, late_ns, next_delta_ns; + u64 delay_ns = (u64)READ_ONCE(phc->extts_delay_ms) * NSEC_PER_MSEC; if (!READ_ONCE(phc->extts_enabled)) { hrtimer_forward_now(timer, ns_to_ktime(EXTTS_IDLE_POLL_NS)); @@ -156,20 +159,29 @@ static enum hrtimer_restart mock_phc_extts_timer(struct hrtimer *timer) late_ns = tai_ns - tai_sec * NSEC_PER_SEC; if (tai_sec != phc->last_extts_sec) { + /* Wait until the configured phase delay has elapsed after the edge. */ + if (late_ns < delay_ns) { + hrtimer_forward_now(timer, + ns_to_ktime(delay_ns - late_ns)); + return HRTIMER_RESTART; + } + phc->last_extts_sec = tai_sec; spin_lock_irqsave(&phc->lock, flags); phc_ns = mock_phc_read_locked(phc); spin_unlock_irqrestore(&phc->lock, flags); - event.timestamp = phc_ns - late_ns; + /* Timestamp the delayed edge, removing only timer scheduling jitter. */ + event.timestamp = phc_ns - (late_ns - delay_ns); event.type = PTP_CLOCK_EXTTS; event.index = phc->extts_channel; ptp_clock_event(phc->clock, &event); } - /* Aim for ~1 ms after the next TAI second (MONOTONIC ≈ TAI rate). */ - next_delta_ns = (NSEC_PER_SEC - late_ns) + EXTTS_EDGE_SLACK_NS; + /* Aim for the configured delay after the next TAI second. */ + next_delta_ns = (NSEC_PER_SEC - late_ns) + delay_ns + + EXTTS_EDGE_SLACK_NS; if (next_delta_ns < EXTTS_EDGE_SLACK_NS) next_delta_ns = EXTTS_EDGE_SLACK_NS; hrtimer_forward_now(timer, ns_to_ktime(next_delta_ns)); @@ -224,6 +236,40 @@ static int mock_phc_enable(struct ptp_clock_info *info, } } +static ssize_t extts_delay_ms_show(struct kobject *kobj, + struct kobj_attribute *attr, char *buf) +{ + struct mock_phc *phc = container_of(attr, struct mock_phc, + extts_delay_attr); + + return sysfs_emit(buf, "%u\n", READ_ONCE(phc->extts_delay_ms)); +} + +static ssize_t extts_delay_ms_store(struct kobject *kobj, + struct kobj_attribute *attr, + const char *buf, size_t count) +{ + struct mock_phc *phc = container_of(attr, struct mock_phc, + extts_delay_attr); + unsigned int delay_ms; + int err; + + err = kstrtouint(buf, 0, &delay_ms); + if (err) + return err; + if (delay_ms > EXTTS_MAX_DELAY_MS) + return -ERANGE; + + WRITE_ONCE(phc->extts_delay_ms, delay_ms); + /* Re-evaluate promptly, especially when lowering an active delay. */ + if (READ_ONCE(phc->extts_enabled)) + hrtimer_start(&phc->extts_timer, + ns_to_ktime(EXTTS_EDGE_SLACK_NS), + HRTIMER_MODE_REL); + + return count; +} + int mock_phc_index(struct mock_phc *phc) { return ptp_clock_index(phc->clock); @@ -306,14 +352,25 @@ struct mock_phc *mock_phc_create(struct device *dev, int logical_clk_id) phc->freq_ppb = 0; phc->clock = ptp_clock_register(&phc->info, dev); - if (!IS_ERR(phc->clock)) - hrtimer_start(&phc->extts_timer, ns_to_ktime(EXTTS_IDLE_POLL_NS), - HRTIMER_MODE_REL); if (IS_ERR(phc->clock)) { err = PTR_ERR(phc->clock); goto out_free_phc; } + sysfs_attr_init(&phc->extts_delay_attr.attr); + phc->extts_delay_attr.attr.name = "extts_delay_ms"; + phc->extts_delay_attr.attr.mode = 0644; + phc->extts_delay_attr.show = extts_delay_ms_show; + phc->extts_delay_attr.store = extts_delay_ms_store; + err = sysfs_create_file(mock_phc_dev_kobj(phc), + &phc->extts_delay_attr.attr); + if (err) { + ptp_clock_unregister(phc->clock); + goto out_free_phc; + } + + hrtimer_start(&phc->extts_timer, ns_to_ktime(EXTTS_IDLE_POLL_NS), + HRTIMER_MODE_REL); return phc; out_free_phc: @@ -328,6 +385,8 @@ static void mock_phc_destroy(struct kref *ref) struct mock_phc *phc = container_of(ref, struct mock_phc, ref); hrtimer_cancel(&phc->extts_timer); + sysfs_remove_file(mock_phc_dev_kobj(phc), + &phc->extts_delay_attr.attr); ptp_clock_unregister(phc->clock); kfree(phc); } diff --git a/scripts/test-phc.sh b/scripts/test-phc.sh index f10ccc3..a98cdc0 100755 --- a/scripts/test-phc.sh +++ b/scripts/test-phc.sh @@ -9,6 +9,7 @@ # - adjtime (time step) # - adjfine (frequency adjustment via scaled_ppm) # - EXTTS enable, event delivery, second-boundary alignment +# - Configurable per-PHC EXTTS delivery delay # - EXTTS self-correction after PHC time step # - PHC sharing across ports (same logical_clk_id) # - Pin configuration (2 pins: NONE, GNSS1PPS) @@ -721,9 +722,43 @@ fi echo # ------------------------------------------------------------------- -# 10. EXTTS self-correction after PHC time step +# 10. Configurable EXTTS delivery delay # ------------------------------------------------------------------- -log "10. EXTTS self-correction after time step" +log "10. Configurable EXTTS delivery delay" + +EXTTS_DELAY_ATTR="/sys/class/nsim_ptp/${PTP_CLASS_DEV}/extts_delay_ms" +if [[ -w "$EXTTS_DELAY_ATTR" ]]; then + # A 400 ms delay exceeds linuxptp ts2phc's 333 ms polling window. + echo 400 > "$EXTTS_DELAY_ATTR" + assert_eq "EXTTS delay sysfs readback" "400" "$(cat "$EXTTS_DELAY_ATTR")" + + phc_cmd settime "$PTP_DEV" 1700000000 0 >/dev/null + phc_cmd adjfine "$PTP_DEV" 0 >/dev/null + phc_cmd enable_extts "$PTP_DEV" 0 >/dev/null + EVENTS=$(phc_cmd read_extts "$PTP_DEV" 5.0) + phc_cmd disable_extts "$PTP_DEV" 0 >/dev/null + echo 0 > "$EXTTS_DELAY_ATTR" + + DELAY_OK=$(echo "$EVENTS" | python3 -c " +import sys, json +values = [e['nsec'] for e in json.load(sys.stdin)] +print('ok' if len(values) >= 2 and all(390000000 <= n <= 410000000 for n in values) else 'bad:' + repr(values)) +") + if [[ "$DELAY_OK" == "ok" ]]; then + pass "EXTTS event timestamps reflect configured 400 ms delay" + else + fail "EXTTS delay was not applied as expected ($DELAY_OK)" + fi +else + skip "EXTTS delay sysfs attribute unavailable: $EXTTS_DELAY_ATTR" +fi + +echo + +# ------------------------------------------------------------------- +# 11. EXTTS self-correction after PHC time step +# ------------------------------------------------------------------- +log "11. EXTTS self-correction after time step" phc_cmd settime "$PTP_DEV" 1700000000 0 >/dev/null phc_cmd adjfine "$PTP_DEV" 0 >/dev/null @@ -773,9 +808,9 @@ phc_cmd disable_extts "$PTP_DEV" 0 >/dev/null echo # ------------------------------------------------------------------- -# 11. EXTTS with frequency adjustment +# 12. EXTTS with frequency adjustment # ------------------------------------------------------------------- -log "11. EXTTS with frequency adjustment" +log "12. EXTTS with frequency adjustment" phc_cmd settime "$PTP_DEV" 1700000000 0 >/dev/null phc_cmd adjfine "$PTP_DEV" 6553600 >/dev/null # +100 ppm @@ -813,9 +848,9 @@ fi echo # ------------------------------------------------------------------- -# 12. PHC sharing (same logical_clk_id) +# 13. PHC sharing (same logical_clk_id) # ------------------------------------------------------------------- -log "12. PHC sharing across ports (same logical_clk_id)" +log "13. PHC sharing across ports (same logical_clk_id)" cleanup_all_devices create_device 1 1 2 1 1 # 2 ports, clock_id=1 @@ -846,9 +881,9 @@ fi echo # ------------------------------------------------------------------- -# 13. Multiple settime64 operations +# 14. Multiple settime64 operations # ------------------------------------------------------------------- -log "13. Multiple settime64 operations (no corruption)" +log "14. Multiple settime64 operations (no corruption)" cleanup_all_devices create_device 1 1 2 1 1 @@ -879,9 +914,9 @@ fi echo # ------------------------------------------------------------------- -# 14. REGRESSION: EXTTS with realistic TAI time (large offset_ns) +# 15. REGRESSION: EXTTS with realistic TAI time (large offset_ns) # ------------------------------------------------------------------- -log "14. REGRESSION: EXTTS with realistic TAI time (offset_ns ≈ 56 years)" +log "15. REGRESSION: EXTTS with realistic TAI time (offset_ns ≈ 56 years)" # Bug: with CLOCK_MONOTONIC_RAW base, setting PHC to TAI time created # offset_ns ≈ 1.78e18. ts2phc's servo drove freq_ppb to -500M, causing @@ -962,9 +997,9 @@ fi echo # ------------------------------------------------------------------- -# 15. REGRESSION: EXTTS must not rapid-fire with extreme freq_ppb +# 16. REGRESSION: EXTTS must not rapid-fire with extreme freq_ppb # ------------------------------------------------------------------- -log "15. REGRESSION: EXTTS with extreme freq_ppb = -500M" +log "16. REGRESSION: EXTTS with extreme freq_ppb = -500M" # Bug: when freq_ppb = -500000000 (max negative), PHC advances at 50% # speed. With CLOCK_MONOTONIC_RAW the EXTTS delay was doubled, but the @@ -1023,9 +1058,9 @@ fi echo # ------------------------------------------------------------------- -# 16. REGRESSION: freq_ppb stays small after realistic settime +# 17. REGRESSION: freq_ppb stays small after realistic settime # ------------------------------------------------------------------- -log "16. REGRESSION: freq_ppb stays near zero with MONOTONIC base" +log "17. REGRESSION: freq_ppb stays near zero with MONOTONIC base" # Bug: with CLOCK_MONOTONIC_RAW, after settime to TAI, the PHC drifted # and ts2phc pushed freq_ppb to -500M. With CLOCK_MONOTONIC base, the @@ -1076,9 +1111,9 @@ fi echo # ------------------------------------------------------------------- -# 17. dmesg sanity +# 18. dmesg sanity # ------------------------------------------------------------------- -log "17. dmesg sanity check" +log "18. dmesg sanity check" DMESG_PHC=$(dmesg | grep -i "ptp_mock\|mock_phc\|nsim_ptp" | tail -20 || true) if echo "$DMESG_PHC" | grep -qiE "bug|oops|panic|call.trace|rcu.*stall"; then @@ -1091,9 +1126,9 @@ fi echo # ------------------------------------------------------------------- -# 18. Cleanup +# 19. Cleanup # ------------------------------------------------------------------- -log "18. Cleanup" +log "19. Cleanup" cleanup_all_devices pass "all devices cleaned up" From 323e4cde56f69cbae2af11021da3d539b51a4a7e Mon Sep 17 00:00:00 2001 From: Jim Ramsay Date: Thu, 22 Oct 2026 23:49:51 +0000 Subject: [PATCH 2/2] Fix mock PHC sysfs teardown ordering --- ptp/ptp_mock.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ptp/ptp_mock.c b/ptp/ptp_mock.c index 0d52f07..9fd849e 100644 --- a/ptp/ptp_mock.c +++ b/ptp/ptp_mock.c @@ -384,9 +384,9 @@ static void mock_phc_destroy(struct kref *ref) { struct mock_phc *phc = container_of(ref, struct mock_phc, ref); - hrtimer_cancel(&phc->extts_timer); sysfs_remove_file(mock_phc_dev_kobj(phc), &phc->extts_delay_attr.attr); + hrtimer_cancel(&phc->extts_timer); ptp_clock_unregister(phc->clock); kfree(phc); }