diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 38446ef..d9e0ca8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -139,10 +139,16 @@ jobs: IFACE=$(ls /sys/bus/pci/devices/${DOMAIN}:${BUS}:02.0/net/ 2>/dev/null | head -1) [ -n "$IFACE" ] && ethtool -T "$IFACE" || echo "WARN: No interface found" + - name: DPLL unit tests + if: steps.load.outputs.load_ok == 'true' + run: sudo ./scripts/test-dpll.sh --no-load + - name: Cleanup if: always() run: | - echo "1" | sudo tee /sys/bus/netdevsim/del_device 2>/dev/null || true + for id in $(ls /sys/bus/netdevsim/devices/ 2>/dev/null | sed 's/netdevsim//'); do + echo "$id" | sudo tee /sys/bus/netdevsim/del_device 2>/dev/null || true + done sudo rmmod netdevsim nsim_dpll nsim_ptp_mock nsim_ptp 2>/dev/null || true sudo dkms remove ${DKMS_PKG}/${DKMS_VER} --all 2>/dev/null || true diff --git a/.gitignore b/.gitignore index 0b31028..41cb87b 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ *.o *.ko *.ko.cmd +*.ko.zst *.mod *.mod.c *.mod.cmd @@ -9,11 +10,16 @@ *.symvers *.unsigned *.o.d +*.o.cmd .tmp_versions/ .cache.mk modules.order Module.symvers +# kbuild per-object command files +.*.cmd +.module-common.o + # RPM build rpmbuild/ *.tar.gz diff --git a/99-nsim-ptp.rules b/99-nsim-ptp.rules index cb5ad73..ca6f937 100644 --- a/99-nsim-ptp.rules +++ b/99-nsim-ptp.rules @@ -14,7 +14,15 @@ # is not in the default cgroup device allowlist; containers and # systemd-sandboxed services (DevicePolicy=closed) would get # "Operation not permitted" without world-readable permissions. +# +# The bind-mount of /sys/class/nsim_ptp over /sys/class/ptp makes +# PTP sysfs entries (pins, clock_name, etc.) visible at the standard +# path expected by monitoring tools and the PTP dashboard. SUBSYSTEM=="nsim_ptp", KERNEL=="nsim_ptp[0-9]*", MODE="0666", \ - RUN+="/bin/sh -c 'rm -f /dev/ptp%n; mknod /dev/ptp%n c %M %m; chmod 666 /dev/ptp%n'" + RUN+="/bin/sh -c 'rm -f /dev/ptp%n; mknod /dev/ptp%n c %M %m; chmod 666 /dev/ptp%n; if [ -d /sys/class/ptp ] && ! mountpoint -q /sys/class/ptp; then mount --bind /sys/class/nsim_ptp /sys/class/ptp 2>/dev/null || true; fi'" SUBSYSTEM=="nsim_ptp", KERNEL=="nsim_ptp[0-9]*", ACTION=="remove", \ RUN+="/bin/rm -f /dev/ptp%n" + +# gpsd drops privileges to nobody; without world access it fails with +# EACCES, frees the device, and ts2phc starves for NMEA. +SUBSYSTEM=="gnss", KERNEL=="gnss[0-9]*", MODE="0666" diff --git a/README.md b/README.md index 94e874a..b0fc416 100644 --- a/README.md +++ b/README.md @@ -271,7 +271,40 @@ ssh my-dev-vm If an SSH config entry for the VM name already exists, the script prompts you to remove it or pick a different `--vm-name`. -### Testing +### DPLL Unit Tests + +`scripts/test-dpll.sh` exercises the DPLL emulation in `netdevsim/dpll.c`. +It requires root privileges and loaded modules: + +```bash +# Run after DKMS install (modules will be loaded automatically) +sudo ./scripts/test-dpll.sh + +# If modules are already loaded, skip load/unload +sudo ./scripts/test-dpll.sh --no-load + +# Verbose mode (set -x) +sudo ./scripts/test-dpll.sh --verbose +``` + +Or via `make`: + +```bash +make test-dpll +``` + +The test suite covers: +- Module loading (`nsim_dpll`, `netdevsim`) +- Device creation with `wpc=1` (DPLL activation) and `wpc=0` (no DPLL) +- Sysfs `lock_status` read/write/transitions/invalid input/rapid cycling +- Generic netlink DPLL device and pin dump (PPS+EEC devices, GNSS/EXT/SyncE pins) +- Sysfs-to-netlink lock status consistency +- GNSS device presence and NMEA echo +- PTP clock and network interface verification +- Device teardown and re-creation +- dmesg sanity (no kernel warnings/errors) + +### Testing with UTM `scripts/test-utm-ubuntu.sh` does the same VM setup plus smoke tests and (optionally) the full ptp-operator test suite. diff --git a/docs/netdevsim-ptp-operator-ci.drawio b/docs/netdevsim-ptp-operator-ci.drawio new file mode 100644 index 0000000..fcb372d --- /dev/null +++ b/docs/netdevsim-ptp-operator-ci.drawio @@ -0,0 +1,371 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dpll/dpll_core.c b/dpll/dpll_core.c index 8892e06..3318987 100644 --- a/dpll/dpll_core.c +++ b/dpll/dpll_core.c @@ -853,14 +853,67 @@ const struct dpll_pin_ops *dpll_pin_ops(struct dpll_pin_ref *ref) return reg->ops; } +/* + * On kernels with CONFIG_DPLL=y the built-in DPLL subsystem already + * registers a genl family named "dpll". We need to replace it with + * our own so that netdevsim DPLL devices are served by the DKMS code + * (which matches the netdevsim driver API exactly). + * + * Strategy: + * 1. Look up the kernel's dpll_nl_family via kprobes + * 2. genl_unregister_family() it + * 3. genl_register_family() our own (also named "dpll") + * 4. On exit, reverse the process + */ + +#include + +static bool kernel_dpll_hijacked; + +typedef unsigned long (*kallsyms_lookup_name_t)(const char *name); + +static unsigned long nsim_lookup_name(const char *name) +{ + static kallsyms_lookup_name_t fn; + + if (!fn) { + struct kprobe kp = { .symbol_name = "kallsyms_lookup_name" }; + int ret = register_kprobe(&kp); + + if (ret < 0) + return 0; + fn = (kallsyms_lookup_name_t)kp.addr; + unregister_kprobe(&kp); + } + return fn(name); +} + static int __init dpll_init(void) { + unsigned long addr; int ret; + addr = nsim_lookup_name("dpll_nl_family"); + if (!addr) { + pr_warn("nsim_dpll: cannot find kernel dpll_nl_family\n"); + } else { + const struct genl_family *kfam = + (const struct genl_family *)addr; + + ret = genl_unregister_family(kfam); + if (ret) { + pr_warn("nsim_dpll: failed to unregister kernel dpll family: %d\n", ret); + } else { + kernel_dpll_hijacked = true; + pr_info("nsim_dpll: unregistered kernel built-in dpll genl family\n"); + } + } + ret = genl_register_family(&dpll_nl_family); if (ret) goto error; + pr_info("nsim_dpll: registered dpll genl family (replacing kernel built-in)\n"); return 0; error: @@ -871,10 +924,24 @@ static int __init dpll_init(void) static void __exit dpll_exit(void) { genl_unregister_family(&dpll_nl_family); + + /* + * We intentionally do NOT re-register the kernel's built-in + * dpll genl family here. genl_register_family() writes to the + * struct (setting family->id etc.) and the kernel's + * dpll_nl_family is in __ro_after_init memory — writing to it + * causes a fatal page fault on aarch64. + * + * This means the dpll genl family is absent after module + * unload, which is acceptable for CI/test environments. + */ + if (kernel_dpll_hijacked) + pr_info("nsim_dpll: kernel dpll family was hijacked; not restoring (ro_after_init)\n"); + mutex_destroy(&dpll_lock); } -subsys_initcall(dpll_init); +module_init(dpll_init); module_exit(dpll_exit); MODULE_LICENSE("GPL"); diff --git a/dpll/dpll_nl.c b/dpll/dpll_nl.c index 5026b5f..b2cf2c3 100644 --- a/dpll/dpll_nl.c +++ b/dpll/dpll_nl.c @@ -149,7 +149,7 @@ static const struct genl_multicast_group dpll_nl_mcgrps[] = { }; struct genl_family dpll_nl_family __ro_after_init = { - .name = "nsim_dpll", + .name = "dpll", .version = DPLL_FAMILY_VERSION, .netnsok = true, .parallel_ops = true, diff --git a/include/linux/ptp_mock.h b/include/linux/ptp_mock.h index be00dd9..d7ae58a 100644 --- a/include/linux/ptp_mock.h +++ b/include/linux/ptp_mock.h @@ -16,28 +16,33 @@ struct device; struct mock_phc { struct ptp_clock_info info; struct ptp_clock *clock; - struct timecounter tc; - struct cyclecounter cc; int logical_clk_id; struct kref ref; spinlock_t lock; + /* MONOTONIC-based timekeeping (immune to phc2sys stepping REALTIME/TAI) */ + s64 offset_ns; /* PHC = CLOCK_MONOTONIC + offset_ns */ + s64 freq_ppb; /* rate correction from adjfine */ + u64 last_mono_ns; /* CLOCK_MONOTONIC snapshot at last update */ /* - * Two pins: index 0 unused, index 1 "GNSS1PPS" — matches common NIC - * layouts and ts2phc defaults (e.g. ts2phc.pin_index 1 on ens1f0). + * E810-style pin layout: GNSS-1PPS input + 4 external connectors. + * Pin index 0 = GNSS-1PPS (ts2phc default), followed by SMA1, SMA2, + * U.FL1, U.FL2 so the dashboard SMA probe discovers them. */ - struct ptp_pin_desc pins[2]; + struct ptp_pin_desc pins[5]; /* EXTTS (1PPS) simulation */ struct hrtimer extts_timer; bool extts_enabled; int extts_channel; + u64 last_extts_sec; /* dedup: last CLOCK_TAI second we reported */ }; #if IS_ENABLED(CONFIG_PTP_1588_CLOCK_MOCK) struct mock_phc *mock_phc_create(struct device *dev, int logical_clk_id); int mock_phc_index(struct mock_phc *phc); -int mock_phc_logical_clk_id(struct mock_phc *phc); +int mock_phc_logical_clk_id(struct mock_phc *phc); void mock_phc_release(struct mock_phc *phc); +struct kobject *mock_phc_dev_kobj(struct mock_phc *phc); struct ptp_clock_info *mock_phc_get_ptp_info(struct mock_phc *phc); #else diff --git a/include/nsim_rename.h b/include/nsim_rename.h index 3b0f26a..3479bd0 100644 --- a/include/nsim_rename.h +++ b/include/nsim_rename.h @@ -51,6 +51,7 @@ #define mock_phc_index nsim_mock_phc_index #define mock_phc_logical_clk_id nsim_mock_phc_logical_clk_id #define mock_phc_get_ptp_info nsim_mock_phc_get_ptp_info +#define mock_phc_dev_kobj nsim_mock_phc_dev_kobj /* ---- DPLL core (dpll_core.c) ------------------------------------------- */ #define dpll_device_get nsim_dpll_device_get diff --git a/netdevsim/dpll.c b/netdevsim/dpll.c index cc2427d..64b7c2c 100644 --- a/netdevsim/dpll.c +++ b/netdevsim/dpll.c @@ -2,14 +2,17 @@ /* * DPLL emulation for netdevsim * - * Registers a pair of DPLL devices (PPS + EEC) that always report - * LOCKED_HO_ACQ status, and a GNSS input pin with zero phase offset. + * Registers a pair of DPLL devices (PPS + EEC) with a user-space + * controllable lock_status via sysfs, and a GNSS input pin with + * zero phase offset. * Each PF netdev gets a SyncE-type output pin associated via * dpll_netdev_pin_set so the linuxptp-daemon can discover the DPLL * through netlink. */ #include + +#include #include #include #include @@ -17,6 +20,7 @@ #include #include #include +#include #include #include #include "netdevsim.h" @@ -42,7 +46,9 @@ static int nsim_dpll_lock_status_get(DPLL_DEVICE_CONST struct dpll_device *dpll, enum dpll_lock_status *status, struct netlink_ext_ack *extack) { - *status = DPLL_LOCK_STATUS_LOCKED_HO_ACQ; + struct nsim_dpll *ndpll = priv; + + *status = ndpll->lock_status; return 0; } #else @@ -52,7 +58,9 @@ static int nsim_dpll_lock_status_get(DPLL_DEVICE_CONST struct dpll_device *dpll, enum dpll_lock_status_error *status_error, struct netlink_ext_ack *extack) { - *status = DPLL_LOCK_STATUS_LOCKED_HO_ACQ; + struct nsim_dpll *ndpll = priv; + + *status = ndpll->lock_status; *status_error = DPLL_LOCK_STATUS_ERROR_NONE; return 0; } @@ -125,6 +133,117 @@ static const struct dpll_pin_ops nsim_dpll_rclk_pin_ops = { .phase_offset_get = nsim_dpll_pin_phase_offset_get, }; +/* Match gnss-sim default --holdover-timeout (Kind cannot write sysfs). + * Keep long enough for TGMBC cascading-holdover tests to observe CC7 on the BC + * before FREERUN/CC248 (announce + CLOCK_CLASS_CHANGE propagation). + */ +#define NSIM_DPLL_HOLDOVER_TIMEOUT_SEC 30 + +static void nsim_dpll_set_lock_status(struct nsim_dpll *ndpll, + enum dpll_lock_status status) +{ + if (ndpll->lock_status == status) + return; + ndpll->lock_status = status; + schedule_work(&ndpll->ntf_work); +} + +static enum hrtimer_restart nsim_dpll_holdover_timer_cb(struct hrtimer *timer) +{ + struct nsim_dpll *ndpll = container_of(timer, struct nsim_dpll, + holdover_timer); + + /* + * Coasting window expired: drop to FREERUN while GNSS remains lost. + * Restore (GGA fix / UBX clear) cancels this timer before fire. + */ + if (ndpll->lock_status == DPLL_LOCK_STATUS_HOLDOVER && + (ndpll->gnss_gps_fix == 0 || ndpll->signal_blocked)) { + nsim_dpll_set_lock_status(ndpll, DPLL_LOCK_STATUS_UNLOCKED); + pr_info("netdevsim: DPLL HOLDOVER → FREERUN (holdover timeout)\n"); + } + return HRTIMER_NORESTART; +} + +static void nsim_dpll_enter_holdover(struct nsim_dpll *ndpll) +{ + if (ndpll->lock_status != DPLL_LOCK_STATUS_HOLDOVER) { + nsim_dpll_set_lock_status(ndpll, DPLL_LOCK_STATUS_HOLDOVER); + pr_info("netdevsim: DPLL → HOLDOVER (GNSS signal lost)\n"); + } + hrtimer_start(&ndpll->holdover_timer, + ns_to_ktime(NSIM_DPLL_HOLDOVER_TIMEOUT_SEC * (u64)NSEC_PER_SEC), + HRTIMER_MODE_REL); +} + +static void nsim_dpll_enter_locked(struct nsim_dpll *ndpll) +{ + hrtimer_cancel(&ndpll->holdover_timer); + if (ndpll->lock_status != DPLL_LOCK_STATUS_LOCKED_HO_ACQ) { + nsim_dpll_set_lock_status(ndpll, + DPLL_LOCK_STATUS_LOCKED_HO_ACQ); + pr_info("netdevsim: DPLL → LOCKED (GNSS signal restored)\n"); + } +} + +/* ---- sysfs: writable lock_status for user-space DPLL control ----------- */ + +static ssize_t nsim_dpll_lock_status_show(struct kobject *kobj, + struct kobj_attribute *attr, + char *buf) +{ + struct nsim_dpll *ndpll = container_of(attr, struct nsim_dpll, + lock_status_attr); + + switch (ndpll->lock_status) { + case DPLL_LOCK_STATUS_LOCKED_HO_ACQ: + return sysfs_emit(buf, "locked\n"); + case DPLL_LOCK_STATUS_HOLDOVER: + return sysfs_emit(buf, "holdover\n"); + case DPLL_LOCK_STATUS_UNLOCKED: + return sysfs_emit(buf, "freerun\n"); + default: + return sysfs_emit(buf, "unknown\n"); + } +} + +static ssize_t nsim_dpll_lock_status_store(struct kobject *kobj, + struct kobj_attribute *attr, + const char *buf, size_t count) +{ + struct nsim_dpll *ndpll = container_of(attr, struct nsim_dpll, + lock_status_attr); + enum dpll_lock_status new_status; + + if (sysfs_streq(buf, "locked")) + new_status = DPLL_LOCK_STATUS_LOCKED_HO_ACQ; + else if (sysfs_streq(buf, "holdover")) + new_status = DPLL_LOCK_STATUS_HOLDOVER; + else if (sysfs_streq(buf, "freerun")) + new_status = DPLL_LOCK_STATUS_UNLOCKED; + else + return -EINVAL; + + /* + * When signal is blocked via UBX CFG-VALSET or NMEA GGA NoFix, the + * kernel owns lock_status (incl. holdover→freerun timer). Ignore + * sysfs writes so Kind RO /sys does not matter for that path. + */ + if (ndpll->signal_blocked || ndpll->gnss_gps_fix == 0) + return count; + + if (new_status == DPLL_LOCK_STATUS_HOLDOVER) + nsim_dpll_enter_holdover(ndpll); + else if (new_status == DPLL_LOCK_STATUS_LOCKED_HO_ACQ) + nsim_dpll_enter_locked(ndpll); + else { + hrtimer_cancel(&ndpll->holdover_timer); + nsim_dpll_set_lock_status(ndpll, new_status); + } + + return count; +} + /* ---- UBX protocol simulation ------------------------------------------- */ #define UBX_SYNC1 0xB5 @@ -142,8 +261,12 @@ static const struct dpll_pin_ops nsim_dpll_rclk_pin_ops = { #define UBX_NAV_CLOCK 0x22 #define UBX_ACK_ACK 0x01 #define UBX_CFG_MSG 0x01 +#define UBX_CFG_VALSET 0x8A #define UBX_MON_VER 0x04 +#define UBX_CFG_VALSET_HDR_LEN 4 +#define UBX_CFGKEY_INFIL_NCNOTHRS 0x201100aa + #define UBX_NAV_STATUS_LEN 16 #define UBX_NAV_CLOCK_LEN 20 #define UBX_ACK_ACK_LEN 2 @@ -274,10 +397,13 @@ static int ubx_build_nav_clock(u8 *buf, size_t bufsz, u8 gps_fix) static void nsim_dpll_ntf_work(struct work_struct *work) { struct nsim_dpll *ndpll = container_of(work, struct nsim_dpll, ntf_work); + int i; dpll_device_change_ntf(ndpll->pps_dpll); dpll_device_change_ntf(ndpll->eec_dpll); dpll_pin_change_ntf(ndpll->gnss_pin); + for (i = 0; i < ndpll->num_ext_pins; i++) + dpll_pin_change_ntf(ndpll->ext_pins[i]); } static enum hrtimer_restart nsim_dpll_ntf_timer_cb(struct hrtimer *timer) @@ -354,6 +480,11 @@ static u8 nsim_gga_quality_to_gps_fix(u8 gga_quality) * the fix quality from field 6. The buffer may contain multiple * concatenated NMEA sentences from a single write() call, so we * search for '$' start markers rather than only checking buf[0]. + * + * Drive DPLL lock_status from fix quality so gnss-sim API signal + * loss/restore works without writing Kind's read-only /sys: + * GGA quality 0 (NoFix) → HOLDOVER → FREERUN after timeout + * GGA quality >0 → LOCKED */ static void nsim_parse_gga_fix(struct nsim_dpll *ndpll, const unsigned char *buf, size_t count) @@ -377,13 +508,29 @@ static void nsim_parse_gga_fix(struct nsim_dpll *ndpll, if (buf[i] == ',') { commas++; if (commas == 5) { - if (i + 1 < count && - buf[i + 1] >= '0' && - buf[i + 1] <= '9') { - ndpll->gnss_gps_fix = - nsim_gga_quality_to_gps_fix( - buf[i + 1] - '0'); - } + u8 old_fix, new_fix; + + if (i + 1 >= count || + buf[i + 1] < '0' || + buf[i + 1] > '9') + return; + + old_fix = ndpll->gnss_gps_fix; + new_fix = nsim_gga_quality_to_gps_fix( + buf[i + 1] - '0'); + ndpll->gnss_gps_fix = new_fix; + + /* UBX CFG-VALSET path owns DPLL state */ + if (ndpll->signal_blocked) + return; + + if (new_fix == 0 && old_fix != 0) + nsim_dpll_enter_holdover(ndpll); + else if (new_fix != 0 && + (old_fix == 0 || + ndpll->lock_status != + DPLL_LOCK_STATUS_LOCKED_HO_ACQ)) + nsim_dpll_enter_locked(ndpll); return; } } @@ -396,6 +543,20 @@ static void nsim_parse_gga_fix(struct nsim_dpll *ndpll, static int nsim_gnss_open(struct gnss_device *gdev) { + struct nsim_dpll *ndpll = gnss_get_drvdata(gdev); + + /* + * linuxptp-daemon's ubxtool monitor runs as + * `ubxtool -t -P … -w ` and expects unsolicited NAV-STATUS + * / NAV-CLOCK. Real receivers emit those after CFG-MSGOUT enable; + * our CFG-VALSET path does not decode those keys, so start the + * periodic UBX emitter as soon as the device is opened. + */ + if (ndpll && !ndpll->ubx_nav_enabled) { + ndpll->ubx_nav_enabled = true; + hrtimer_start(&ndpll->ubx_timer, ns_to_ktime(NSEC_PER_SEC), + HRTIMER_MODE_REL); + } return 0; } @@ -420,13 +581,23 @@ static int nsim_gnss_write_raw(struct gnss_device *gdev, if (!ndpll || count == 0) return count; - /* NMEA sentence: parse GGA fix quality, then echo to read side */ + /* NMEA sentence: parse GGA fix quality, then echo to read side. + * When signal is blocked (via UBX CFG-VALSET INFIL_NCNOTHRS), + * still forward NMEA so ts2phc keeps its time reference and the + * PHC stays disciplined, but skip GGA parsing so gnss_gps_fix + * stays at 0 (NoFix) — UBX NAV-STATUS reports the loss. */ if (buf[0] == '$') { - nsim_parse_gga_fix(ndpll, buf, count); + if (!ndpll->signal_blocked) + nsim_parse_gga_fix(ndpll, buf, count); nsim_ubx_insert_locked(ndpll, buf, count); return count; } + // #region agent log + pr_info("netdevsim: d753ad gnss_write_raw: count=%zu first=0x%02x signal_blocked=%d\n", + count, buf[0], ndpll->signal_blocked); + // #endregion + /* UBX frame: sync(2) + class(1) + id(1) + len(2) minimum */ if (count < UBX_HDR_LEN || buf[0] != UBX_SYNC1 || buf[1] != UBX_SYNC2) return count; @@ -434,6 +605,11 @@ static int nsim_gnss_write_raw(struct gnss_device *gdev, cls = buf[2]; id = buf[3]; + // #region agent log + pr_info("netdevsim: d753ad UBX frame: cls=0x%02x id=0x%02x count=%zu\n", + cls, id, count); + // #endregion + switch (cls) { case UBX_CLASS_MON: if (id == UBX_MON_VER) { @@ -458,6 +634,79 @@ static int nsim_gnss_write_raw(struct gnss_device *gdev, } } + len = ubx_build_ack(resp, sizeof(resp), cls, id); + if (len > 0) + nsim_ubx_insert_locked(ndpll, resp, len); + } else if (id == UBX_CFG_VALSET && + count >= UBX_HDR_LEN + UBX_CFG_VALSET_HDR_LEN + 5) { + const u8 *kv = buf + UBX_HDR_LEN + UBX_CFG_VALSET_HDR_LEN; + size_t kv_end = count - UBX_CK_LEN; + size_t pos = UBX_HDR_LEN + UBX_CFG_VALSET_HDR_LEN; + + // #region agent log + pr_info("netdevsim: d753ad CFG-VALSET: payload_start=%zu kv_end=%zu\n", + pos, kv_end); + // #endregion + while (pos + 4 < kv_end) { + u32 key = kv[0] | (kv[1] << 8) | + (kv[2] << 16) | (kv[3] << 24); + kv += 4; + pos += 4; + + // #region agent log + pr_info("netdevsim: d753ad CFG-VALSET key=0x%08x pos=%zu expect=0x%08x\n", + key, pos, UBX_CFGKEY_INFIL_NCNOTHRS); + // #endregion + if (key == UBX_CFGKEY_INFIL_NCNOTHRS && pos < kv_end) { + u8 val = *kv; + + if (val > 0) { + ndpll->signal_blocked = true; + ndpll->gnss_gps_fix = 0x00; + nsim_dpll_enter_holdover(ndpll); + pr_info("netdevsim: UBX CFG-VALSET INFIL_NCNOTHRS=%u → signal blocked, holdover\n", val); + } else { + ndpll->signal_blocked = false; + ndpll->gnss_gps_fix = 0x03; + nsim_dpll_enter_locked(ndpll); + pr_info("netdevsim: UBX CFG-VALSET INFIL_NCNOTHRS=0 → signal restored, locked\n"); + } + kv++; + pos++; + break; + } + kv++; + pos++; + } + + len = ubx_build_ack(resp, sizeof(resp), cls, id); + if (len > 0) + nsim_ubx_insert_locked(ndpll, resp, len); + } + break; + case UBX_CLASS_NAV: + /* + * Real u-blox receivers answer NAV polls with the message + * payload (not just ACK). linuxptp-daemon's ubxtool path + * requires UBX-NAV-STATUS to advance gnss State beyond s0. + */ + if (id == UBX_NAV_STATUS || id == UBX_NAV_CLOCK) { + if (!ndpll->ubx_nav_enabled) { + ndpll->ubx_nav_enabled = true; + hrtimer_start(&ndpll->ubx_timer, + ns_to_ktime(NSEC_PER_SEC), + HRTIMER_MODE_REL); + } + + if (id == UBX_NAV_STATUS) + len = ubx_build_nav_status(resp, sizeof(resp), + ndpll->gnss_gps_fix); + else + len = ubx_build_nav_clock(resp, sizeof(resp), + ndpll->gnss_gps_fix); + if (len > 0) + nsim_ubx_insert_locked(ndpll, resp, len); + } else { len = ubx_build_ack(resp, sizeof(resp), cls, id); if (len > 0) nsim_ubx_insert_locked(ndpll, resp, len); @@ -493,6 +742,23 @@ static struct dpll_pin_properties nsim_dpll_gnss_pin_props = { .freq_supported_num = ARRAY_SIZE(nsim_dpll_gnss_freq), }; +static struct dpll_pin_frequency nsim_dpll_ext_freq[] = { + DPLL_PIN_FREQUENCY_1PPS, + DPLL_PIN_FREQUENCY(DPLL_PIN_FREQUENCY_10_MHZ), +}; + +#define NSIM_DPLL_NUM_EXT_PINS 4 +#define NSIM_DPLL_EXT_PIN_BASE (NSIM_DPLL_GNSS_PIN_IDX + 1) + +static const char *nsim_dpll_ext_pin_labels[NSIM_DPLL_NUM_EXT_PINS] = { + "SMA1", "SMA2", "U.FL1", "U.FL2", +}; + +static const enum dpll_pin_direction nsim_dpll_ext_pin_dirs[NSIM_DPLL_NUM_EXT_PINS] = { + DPLL_PIN_DIRECTION_INPUT, DPLL_PIN_DIRECTION_OUTPUT, + DPLL_PIN_DIRECTION_INPUT, DPLL_PIN_DIRECTION_OUTPUT, +}; + /* ---- helpers ----------------------------------------------------------- */ static void nsim_dpll_cleanup_port_pins(struct nsim_dpll *ndpll); @@ -509,6 +775,7 @@ int nsim_dpll_init(struct nsim_dev *nsim_dev) return -ENOMEM; ndpll->clock_id = NSIM_DPLL_CLOCK_ID; + ndpll->lock_status = DPLL_LOCK_STATUS_LOCKED_HO_ACQ; INIT_LIST_HEAD(&ndpll->port_pins); /* PPS DPLL */ @@ -555,6 +822,47 @@ int nsim_dpll_init(struct nsim_dev *nsim_dev) if (err) goto err_gnss_put; + /* E810-compatible external pins (SMA1, SMA2, U.FL1, U.FL2) */ + { + int i; + + for (i = 0; i < NSIM_DPLL_NUM_EXT_PINS; i++) { + struct dpll_pin_properties eprops = {}; + struct dpll_pin *epin; + + eprops.board_label = nsim_dpll_ext_pin_labels[i]; + eprops.type = DPLL_PIN_TYPE_EXT; + eprops.capabilities = 0; + eprops.freq_supported = nsim_dpll_ext_freq; + eprops.freq_supported_num = + ARRAY_SIZE(nsim_dpll_ext_freq); + + epin = dpll_pin_get(ndpll->clock_id, + NSIM_DPLL_EXT_PIN_BASE + i, + THIS_MODULE, &eprops); + if (IS_ERR(epin)) { + err = PTR_ERR(epin); + goto err_ext_cleanup; + } + + ndpll->ext_pin_privs[i].direction = + nsim_dpll_ext_pin_dirs[i]; + ndpll->ext_pin_privs[i].frequency = + DPLL_PIN_FREQUENCY_1_HZ; + + err = dpll_pin_register(ndpll->pps_dpll, epin, + &nsim_dpll_gnss_pin_ops, + &ndpll->ext_pin_privs[i]); + if (err) { + dpll_pin_put(epin); + goto err_ext_cleanup; + } + + ndpll->ext_pins[i] = epin; + ndpll->num_ext_pins = i + 1; + } + } + /* * Start a 1 Hz timer that re-emits DPLL device/pin change * notifications so that late-joining userspace consumers @@ -566,13 +874,15 @@ int nsim_dpll_init(struct nsim_dev *nsim_dev) ndpll->ntf_timer.function = nsim_dpll_ntf_timer_cb; hrtimer_start(&ndpll->ntf_timer, ns_to_ktime(NSEC_PER_SEC), HRTIMER_MODE_REL); + hrtimer_init(&ndpll->holdover_timer, CLOCK_MONOTONIC, HRTIMER_MODE_REL); + ndpll->holdover_timer.function = nsim_dpll_holdover_timer_cb; nsim_dev->dpll = ndpll; /* Associate per-port output pins with netdevs */ { struct nsim_dev_port *nsim_dev_port; - int pin_idx = NSIM_DPLL_GNSS_PIN_IDX + 1; + int pin_idx = NSIM_DPLL_EXT_PIN_BASE + NSIM_DPLL_NUM_EXT_PINS; list_for_each_entry(nsim_dev_port, &nsim_dev->port_list, list) { struct netdevsim *ns = nsim_dev_port->ns; @@ -657,11 +967,45 @@ int nsim_dpll_init(struct nsim_dev *nsim_dev) HRTIMER_MODE_REL); ndpll->ubx_timer.function = nsim_ubx_timer_cb; ndpll->ubx_nav_enabled = false; + ndpll->signal_blocked = false; ndpll->gnss_gps_fix = 0x03; pr_info("netdevsim: GNSS device registered (gnss%d)\n", gdev->id); } + /* + * Writable sysfs: /dpll/lock_status + * Uses a per-instance kobject (no global class) so multiple WPC + * devices can coexist without sysfs name collisions. + */ + { + struct device *parent_dev; + struct kobject *parent_kobj; + + if (nsim_dev->fake_pci_dev) + parent_dev = &nsim_dev->fake_pci_dev->dev; + else + parent_dev = &nsim_dev->nsim_bus_dev->dev; + + parent_kobj = &parent_dev->kobj; + ndpll->sysfs_kobj = kobject_create_and_add("dpll", + parent_kobj); + if (!ndpll->sysfs_kobj) { + err = -ENOMEM; + goto err_gnss_cleanup; + } + + ndpll->lock_status_attr.attr.name = "lock_status"; + ndpll->lock_status_attr.attr.mode = 0644; + ndpll->lock_status_attr.show = nsim_dpll_lock_status_show; + ndpll->lock_status_attr.store = nsim_dpll_lock_status_store; + + err = sysfs_create_file(ndpll->sysfs_kobj, + &ndpll->lock_status_attr.attr); + if (err) + goto err_sysfs_kobj; + } + /* * Emit change notifications so that userspace consumers (e.g. * linuxptp-daemon) that subscribe to DPLL multicast before we @@ -671,6 +1015,12 @@ int nsim_dpll_init(struct nsim_dev *nsim_dev) dpll_device_change_ntf(ndpll->pps_dpll); dpll_device_change_ntf(ndpll->eec_dpll); dpll_pin_change_ntf(ndpll->gnss_pin); + { + int i; + + for (i = 0; i < ndpll->num_ext_pins; i++) + dpll_pin_change_ntf(ndpll->ext_pins[i]); + } { struct nsim_dpll_pin *npin; @@ -682,10 +1032,34 @@ int nsim_dpll_init(struct nsim_dev *nsim_dev) ndpll->clock_id); return 0; +err_sysfs_kobj: + kobject_put(ndpll->sysfs_kobj); + ndpll->sysfs_kobj = NULL; +err_gnss_cleanup: + if (ndpll->gnss_dev) { + ndpll->ubx_nav_enabled = false; + hrtimer_cancel(&ndpll->ubx_timer); + gnss_deregister_device(ndpll->gnss_dev); + gnss_put_device(ndpll->gnss_dev); + ndpll->gnss_dev = NULL; + } err_ports_cleanup: hrtimer_cancel(&ndpll->ntf_timer); cancel_work_sync(&ndpll->ntf_work); nsim_dpll_cleanup_port_pins(ndpll); +err_ext_cleanup: + { + int i; + + for (i = ndpll->num_ext_pins - 1; i >= 0; i--) { + dpll_pin_unregister(ndpll->pps_dpll, + ndpll->ext_pins[i], + &nsim_dpll_gnss_pin_ops, + &ndpll->ext_pin_privs[i]); + dpll_pin_put(ndpll->ext_pins[i]); + } + ndpll->num_ext_pins = 0; + } dpll_pin_unregister(ndpll->pps_dpll, ndpll->gnss_pin, &nsim_dpll_gnss_pin_ops, &ndpll->gnss_pin_priv); err_gnss_put: @@ -724,7 +1098,15 @@ void nsim_dpll_exit(struct nsim_dev *nsim_dev) if (!ndpll) return; + if (ndpll->sysfs_kobj) { + sysfs_remove_file(ndpll->sysfs_kobj, + &ndpll->lock_status_attr.attr); + kobject_put(ndpll->sysfs_kobj); + ndpll->sysfs_kobj = NULL; + } + hrtimer_cancel(&ndpll->ntf_timer); + hrtimer_cancel(&ndpll->holdover_timer); cancel_work_sync(&ndpll->ntf_work); if (ndpll->gnss_dev) { @@ -736,6 +1118,19 @@ void nsim_dpll_exit(struct nsim_dev *nsim_dev) nsim_dpll_cleanup_port_pins(ndpll); + { + int i; + + for (i = ndpll->num_ext_pins - 1; i >= 0; i--) { + dpll_pin_unregister(ndpll->pps_dpll, + ndpll->ext_pins[i], + &nsim_dpll_gnss_pin_ops, + &ndpll->ext_pin_privs[i]); + dpll_pin_put(ndpll->ext_pins[i]); + } + ndpll->num_ext_pins = 0; + } + dpll_pin_unregister(ndpll->pps_dpll, ndpll->gnss_pin, &nsim_dpll_gnss_pin_ops, &ndpll->gnss_pin_priv); dpll_pin_put(ndpll->gnss_pin); diff --git a/netdevsim/netdev.c b/netdevsim/netdev.c index af5bfc0..13572e0 100644 --- a/netdevsim/netdev.c +++ b/netdevsim/netdev.c @@ -27,6 +27,7 @@ #include #include #include +#include #include "netdevsim.h" #include @@ -66,7 +67,25 @@ static netdev_tx_t nsim_start_xmit(struct sk_buff *skb, struct net_device *dev) if (ptp_info) ptp_info->gettime64(ptp_info, &rx_ts); skb_hwtstamps(skb)->hwtstamp = timespec64_to_ktime(rx_ts); - if (unlikely(dev_forward_skb(peer_ns->netdev, skb) == NET_RX_DROP)) + + /* + * Inject into peer's RX path without dev_forward_skb which scrubs + * skb_shared_info hwtstamps during cross-netns forwarding (kernel 6.11+). + * We replicate the necessary parts of __dev_forward_skb manually. + */ + if (skb_orphan_frags(skb, GFP_ATOMIC) || + unlikely(!is_skb_forwardable(peer_ns->netdev, skb))) { + kfree_skb(skb); + goto out_drop_cnt; + } + skb_orphan(skb); + skb->pkt_type = PACKET_HOST; + skb->protocol = eth_type_trans(skb, peer_ns->netdev); + skb->skb_iif = 0; + skb_dst_drop(skb); + skb->mark = 0; + nf_reset_ct(skb); + if (unlikely(netif_rx(skb) == NET_RX_DROP)) goto out_drop_cnt; /* only timestamp the outbound packet if the user has requested it */ if (gen_tx_tstamp) { @@ -92,6 +111,18 @@ static netdev_tx_t nsim_start_xmit(struct sk_buff *skb, struct net_device *dev) return NETDEV_TX_OK; } +static int nsim_open(struct net_device *dev) +{ + netif_carrier_on(dev); + return 0; +} + +static int nsim_stop(struct net_device *dev) +{ + netif_carrier_off(dev); + return 0; +} + static void nsim_set_rx_mode(struct net_device *dev) { @@ -401,6 +432,8 @@ static int nsim_set_ts_config(struct net_device *netdev, return 0; } static const struct net_device_ops nsim_netdev_ops = { + .ndo_open = nsim_open, + .ndo_stop = nsim_stop, .ndo_start_xmit = nsim_start_xmit, .ndo_set_rx_mode = nsim_set_rx_mode, .ndo_set_mac_address = eth_mac_addr, @@ -424,6 +457,8 @@ static const struct net_device_ops nsim_netdev_ops = { }; static const struct net_device_ops nsim_vf_netdev_ops = { + .ndo_open = nsim_open, + .ndo_stop = nsim_stop, .ndo_start_xmit = nsim_start_xmit, .ndo_set_rx_mode = nsim_set_rx_mode, .ndo_set_mac_address = eth_mac_addr, @@ -440,7 +475,6 @@ static void nsim_setup(struct net_device *dev) eth_hw_addr_random(dev); dev->tx_queue_len = 0; - dev->flags &= ~IFF_MULTICAST; dev->priv_flags |= IFF_LIVE_ADDR_CHANGE | IFF_NO_QUEUE; dev->features |= NETIF_F_HIGHDMA | NETIF_F_SG | NETIF_F_FRAGLIST | NETIF_F_HW_CSUM | NETIF_F_TSO; @@ -540,7 +574,27 @@ static int nsim_init_netdevsim(struct netdevsim *ns) err = register_netdevice(ns->netdev); if (err) goto err_ipsec_teardown; + netif_carrier_on(ns->netdev); rtnl_unlock(); + + /* + * Create /sys/devices/.../pci_dev/ptp/ptpN symlink so tools that + * walk /sys/class/net//device/ptp/ find our PTP clock. + */ + if (ns->nsim_dev->fake_pci_dev) { + struct kobject *pci_kobj = &ns->nsim_dev->fake_pci_dev->dev.kobj; + struct kobject *ptp_dir = kobject_create_and_add("ptp", pci_kobj); + if (ptp_dir) { + char link_name[16]; + snprintf(link_name, sizeof(link_name), "ptp%d", + mock_phc_index(phc)); + (void)sysfs_create_link(ptp_dir, + mock_phc_dev_kobj(phc), + link_name); + ns->ptp_compat_kobj = ptp_dir; + } + } + return 0; err_ipsec_teardown: @@ -563,12 +617,18 @@ static int nsim_init_netdevsim_vf(struct netdevsim *ns) ns->netdev->netdev_ops = &nsim_vf_netdev_ops; rtnl_lock(); err = register_netdevice(ns->netdev); + if (!err) + netif_carrier_on(ns->netdev); rtnl_unlock(); return err; } static void nsim_exit_netdevsim(struct netdevsim *ns) { + if (ns->ptp_compat_kobj) { + kobject_put(ns->ptp_compat_kobj); + ns->ptp_compat_kobj = NULL; + } nsim_udp_tunnels_info_destroy(ns->netdev); mock_phc_release(ns->phc); } diff --git a/netdevsim/netdevsim.h b/netdevsim/netdevsim.h index 526834b..5701f58 100644 --- a/netdevsim/netdevsim.h +++ b/netdevsim/netdevsim.h @@ -90,15 +90,23 @@ struct nsim_dpll { struct dpll_device *eec_dpll; struct dpll_pin *gnss_pin; struct nsim_dpll_pin gnss_pin_priv; + struct dpll_pin *ext_pins[4]; + struct nsim_dpll_pin ext_pin_privs[4]; + int num_ext_pins; struct list_head port_pins; u64 clock_id; struct gnss_device *gnss_dev; struct hrtimer ubx_timer; struct hrtimer ntf_timer; + struct hrtimer holdover_timer; struct work_struct ntf_work; spinlock_t gnss_lock; bool ubx_nav_enabled; + bool signal_blocked; u8 gnss_gps_fix; + enum dpll_lock_status lock_status; + struct kobject *sysfs_kobj; + struct kobj_attribute lock_status_attr; }; struct nsim_ethtool_pauseparam { @@ -123,6 +131,7 @@ struct netdevsim { struct nsim_dev *nsim_dev; struct nsim_dev_port *nsim_dev_port; struct mock_phc *phc; + struct kobject *ptp_compat_kobj; /* /sys/.../pci_dev/ptp/ for compat */ int logical_clk_id; /* Logical clock ID for PHC sharing (-1 = unique) */ u64 tx_packets; diff --git a/ptp/ptp_mock.c b/ptp/ptp_mock.c index 13cb404..b05f4f9 100644 --- a/ptp/ptp_mock.c +++ b/ptp/ptp_mock.c @@ -4,8 +4,15 @@ * * Mock-up PTP Hardware Clock driver for virtual network devices * - * Create a PTP clock which offers PTP time manipulation operations - * using a timecounter/cyclecounter on top of CLOCK_MONOTONIC_RAW. + * The PHC time is derived from CLOCK_MONOTONIC plus an accumulated offset. + * CLOCK_MONOTONIC never steps (immune to phc2sys stepping CLOCK_REALTIME/TAI) + * and its rate is NTP-adjusted so freq_ppb stays near zero. adjfine() + * applies a frequency correction on top of this base so the PTP servo + * feedback loop operates normally. + * + * Basing the PHC on CLOCK_TAI looks attractive for GNSS TOD alignment, but on + * T-GM/T-BC profiles phc2sys steers REALTIME from the PHC; TAI moves with + * REALTIME, so a TAI-based PHC forms a positive feedback loop with ts2phc. * * Two PTP pins are exposed so PTP_PIN_SETFUNC2 with pin index 1 (typical for * 1PPS input on Intel-style devices) succeeds; both may map EXTTS to channel 0. @@ -15,27 +22,12 @@ #include #include #include -#include #include +#include -/* Clamp scaled_ppm between -32,768,000,000 and 32,768,000,000, - * and thus "adj" between -1,073,741,824 and 1,073,741,824 - */ -#define MOCK_PHC_MAX_ADJ_PPB 500000000 -/* Timestamps from ktime_get_raw() have 1 ns resolution, so the scale factor - * (MULT >> SHIFT) needs to be 1. Pick SHIFT as 31 bits, which translates - * MULT(freq 0) into 0x80000000. - */ -#define MOCK_PHC_CC_SHIFT 31 -#define MOCK_PHC_CC_MULT (1 << MOCK_PHC_CC_SHIFT) -#define MOCK_PHC_FADJ_SHIFT 9 -#define MOCK_PHC_FADJ_DENOMINATOR 15625ULL - -/* The largest cycle_delta that timecounter_read_delta() can handle without a - * 64-bit overflow during the multiplication with cc->mult, given the max "adj" - * we permit, is ~5.7 seconds. Make sure readouts are more frequent than that. - */ -#define MOCK_PHC_REFRESH_INTERVAL (HZ * 3) +#include "ptp_private.h" + +#define MOCK_PHC_MAX_ADJ_PPB 500000000 #define info_to_phc(d) container_of((d), struct mock_phc, info) @@ -45,23 +37,35 @@ struct ptp_clock_info *mock_phc_get_ptp_info(struct mock_phc *phc) } EXPORT_SYMBOL_GPL(mock_phc_get_ptp_info); -static u64 mock_phc_cc_read(CYCLECOUNTER_READ_CONST struct cyclecounter *cc) +/* + * Advance the internal bookkeeping: accumulate the frequency-induced + * drift since the last update and snapshot the monotonic time. + * Returns the PHC time = CLOCK_MONOTONIC + accumulated offset. + * Must be called with phc->lock held. + */ +static u64 mock_phc_read_locked(struct mock_phc *phc) { - return ktime_get_raw_ns(); + u64 raw = ktime_get_ns(); + s64 elapsed = (s64)(raw - phc->last_mono_ns); + s64 drift; + + if (elapsed > 0 && phc->freq_ppb != 0) { + drift = div_s64(elapsed * phc->freq_ppb, 1000000000LL); + phc->offset_ns += drift; + } + phc->last_mono_ns = raw; + + return (u64)((s64)raw + phc->offset_ns); } static int mock_phc_adjfine(struct ptp_clock_info *info, long scaled_ppm) { struct mock_phc *phc = info_to_phc(info); unsigned long flags; - s64 adj; - - adj = (s64)scaled_ppm << MOCK_PHC_FADJ_SHIFT; - adj = div_s64(adj, MOCK_PHC_FADJ_DENOMINATOR); spin_lock_irqsave(&phc->lock, flags); - timecounter_read(&phc->tc); - phc->cc.mult = MOCK_PHC_CC_MULT + adj; + mock_phc_read_locked(phc); + phc->freq_ppb = div_s64((s64)scaled_ppm * 1000LL, 65536LL); spin_unlock_irqrestore(&phc->lock, flags); return 0; @@ -73,7 +77,8 @@ static int mock_phc_adjtime(struct ptp_clock_info *info, s64 delta) unsigned long flags; spin_lock_irqsave(&phc->lock, flags); - timecounter_adjtime(&phc->tc, delta); + mock_phc_read_locked(phc); + phc->offset_ns += delta; spin_unlock_irqrestore(&phc->lock, flags); return 0; @@ -83,24 +88,28 @@ static int mock_phc_settime64(struct ptp_clock_info *info, const struct timespec64 *ts) { struct mock_phc *phc = info_to_phc(info); - u64 ns = timespec64_to_ns(ts); unsigned long flags; + u64 raw; spin_lock_irqsave(&phc->lock, flags); - timecounter_init(&phc->tc, &phc->cc, ns); + raw = ktime_get_ns(); + phc->last_mono_ns = raw; + phc->offset_ns = (s64)(timespec64_to_ns(ts)) - (s64)raw; + phc->freq_ppb = 0; spin_unlock_irqrestore(&phc->lock, flags); return 0; } -static int mock_phc_gettime64(struct ptp_clock_info *info, struct timespec64 *ts) +static int mock_phc_gettime64(struct ptp_clock_info *info, + struct timespec64 *ts) { struct mock_phc *phc = info_to_phc(info); unsigned long flags; u64 ns; spin_lock_irqsave(&phc->lock, flags); - ns = timecounter_read(&phc->tc); + ns = mock_phc_read_locked(phc); spin_unlock_irqrestore(&phc->lock, flags); *ts = ns_to_timespec64(ns); @@ -108,32 +117,62 @@ static int mock_phc_gettime64(struct ptp_clock_info *info, struct timespec64 *ts return 0; } -static long mock_phc_refresh(struct ptp_clock_info *info) -{ - struct timespec64 ts; - - mock_phc_gettime64(info, &ts); - - return MOCK_PHC_REFRESH_INTERVAL; -} +/* + * Simulate a GNSS 1PPS input: one EXTTS edge per CLOCK_TAI second, with + * the event timestamp equal to the PHC reading at that edge. + * + * Why TAI (not PHC) second boundaries? + * ts2phc pairs EXTTS with NMEA, which is UTC converted to TAI via the + * leapfile. Real GNSS 1PPS is aligned to that absolute time, not to + * the PHC's own second counter. Firing on PHC seconds made EXTTS + * wander with adjfine() and produced a persistent ±1 s pairing flip + * (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 + * 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). + */ +#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 */ static enum hrtimer_restart mock_phc_extts_timer(struct hrtimer *timer) { struct mock_phc *phc = container_of(timer, struct mock_phc, extts_timer); struct ptp_clock_event event; + unsigned long flags; + u64 tai_ns, tai_sec, phc_ns, late_ns, next_delta_ns; - /* - * Report the TAI second boundary as the EXTTS timestamp. ts2phc - * converts NMEA (UTC) to TAI via its leap-second file, so the - * EXTTS timestamp must also be in TAI for offset ≈ 0. - */ - event.type = PTP_CLOCK_EXTTS; - event.index = phc->extts_channel; - event.timestamp = div64_u64(ktime_get_clocktai_ns(), - NSEC_PER_SEC) * NSEC_PER_SEC; - ptp_clock_event(phc->clock, &event); - - hrtimer_forward_now(timer, ns_to_ktime(NSEC_PER_SEC)); + if (!READ_ONCE(phc->extts_enabled)) { + hrtimer_forward_now(timer, ns_to_ktime(EXTTS_IDLE_POLL_NS)); + return HRTIMER_RESTART; + } + + tai_ns = ktime_get_clocktai_ns(); + tai_sec = div_u64(tai_ns, NSEC_PER_SEC); + late_ns = tai_ns - tai_sec * NSEC_PER_SEC; + + if (tai_sec != phc->last_extts_sec) { + 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; + 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; + 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)); return HRTIMER_RESTART; } @@ -150,22 +189,33 @@ static int mock_phc_enable(struct ptp_clock_info *info, switch (rq->type) { case PTP_CLK_REQ_EXTTS: { - u64 now_ns, ns_to_next; - if (rq->extts.index >= info->n_ext_ts) return -EINVAL; if (on) { + unsigned long flags; + u64 mono_ns, tai_ns; + phc->extts_channel = rq->extts.index; - phc->extts_enabled = true; - now_ns = ktime_get_real_ns(); - ns_to_next = NSEC_PER_SEC - - do_div(now_ns, NSEC_PER_SEC); - hrtimer_start(&phc->extts_timer, - ns_to_ktime(ns_to_next), - HRTIMER_MODE_REL); + /* + * One-shot: park PHC near current TAI so the first + * ts2phc samples are small. After this the PHC runs + * from MONOTONIC and is immune to phc2sys. + */ + spin_lock_irqsave(&phc->lock, flags); + mono_ns = ktime_get_ns(); + tai_ns = ktime_get_clocktai_ns(); + phc->last_mono_ns = mono_ns; + phc->offset_ns = (s64)tai_ns - (s64)mono_ns; + phc->freq_ppb = 0; + spin_unlock_irqrestore(&phc->lock, flags); + + /* Avoid a spurious edge for the current TAI second */ + phc->last_extts_sec = div_u64(tai_ns, NSEC_PER_SEC); + /* Barrier ensures channel/sec are visible before flag */ + smp_wmb(); + WRITE_ONCE(phc->extts_enabled, true); } else { - phc->extts_enabled = false; - hrtimer_cancel(&phc->extts_timer); + WRITE_ONCE(phc->extts_enabled, false); } return 0; } @@ -186,6 +236,12 @@ int mock_phc_logical_clk_id(struct mock_phc *phc) } EXPORT_SYMBOL_GPL(mock_phc_logical_clk_id); +struct kobject *mock_phc_dev_kobj(struct mock_phc *phc) +{ + return &phc->clock->dev.kobj; +} +EXPORT_SYMBOL_GPL(mock_phc_dev_kobj); + struct mock_phc *mock_phc_create(struct device *dev, int logical_clk_id) { struct mock_phc *phc; @@ -197,16 +253,31 @@ struct mock_phc *mock_phc_create(struct device *dev, int logical_clk_id) goto out; } - strscpy(phc->pins[0].name, "NONE", sizeof(phc->pins[0].name)); + strscpy(phc->pins[0].name, "GNSS-1PPS", sizeof(phc->pins[0].name)); phc->pins[0].index = 0; - phc->pins[0].func = PTP_PF_NONE; + phc->pins[0].func = PTP_PF_EXTTS; phc->pins[0].chan = 0; - strscpy(phc->pins[1].name, "GNSS1PPS", sizeof(phc->pins[1].name)); + strscpy(phc->pins[1].name, "SMA1", sizeof(phc->pins[1].name)); phc->pins[1].index = 1; phc->pins[1].func = PTP_PF_NONE; phc->pins[1].chan = 0; + strscpy(phc->pins[2].name, "SMA2", sizeof(phc->pins[2].name)); + phc->pins[2].index = 2; + phc->pins[2].func = PTP_PF_NONE; + phc->pins[2].chan = 0; + + strscpy(phc->pins[3].name, "U.FL1", sizeof(phc->pins[3].name)); + phc->pins[3].index = 3; + phc->pins[3].func = PTP_PF_NONE; + phc->pins[3].chan = 0; + + strscpy(phc->pins[4].name, "U.FL2", sizeof(phc->pins[4].name)); + phc->pins[4].index = 4; + phc->pins[4].func = PTP_PF_NONE; + phc->pins[4].chan = 0; + phc->info = (struct ptp_clock_info) { .owner = THIS_MODULE, .name = "Mock-up PTP clock", @@ -218,36 +289,31 @@ struct mock_phc *mock_phc_create(struct device *dev, int logical_clk_id) .adjtime = mock_phc_adjtime, .gettime64 = mock_phc_gettime64, .settime64 = mock_phc_settime64, - .do_aux_work = mock_phc_refresh, .enable = mock_phc_enable, .verify = mock_phc_verify, }; phc->logical_clk_id = logical_clk_id; - phc->cc = (struct cyclecounter) { - .read = mock_phc_cc_read, - .mask = CYCLECOUNTER_MASK(64), - .mult = MOCK_PHC_CC_MULT, - .shift = MOCK_PHC_CC_SHIFT, - }; - spin_lock_init(&phc->lock); kref_init(&phc->ref); hrtimer_init(&phc->extts_timer, CLOCK_MONOTONIC, HRTIMER_MODE_REL); phc->extts_timer.function = mock_phc_extts_timer; - timecounter_init(&phc->tc, &phc->cc, ktime_get_real_ns()); + phc->last_mono_ns = ktime_get_ns(); + phc->offset_ns = 0; + 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; } - ptp_schedule_worker(phc->clock, MOCK_PHC_REFRESH_INTERVAL); - return phc; out_free_phc: @@ -268,12 +334,12 @@ static void mock_phc_destroy(struct kref *ref) void mock_phc_release(struct mock_phc *phc) { - pr_info("Releasing mock phc. Current ref count before kref_put: %u\n", kref_read(&phc->ref)); + pr_info("Releasing mock phc. Current ref count before kref_put: %u\n", + kref_read(&phc->ref)); kref_put(&phc->ref, mock_phc_destroy); } EXPORT_SYMBOL_GPL(mock_phc_release); - MODULE_DESCRIPTION("Mock-up PTP Hardware Clock driver"); MODULE_LICENSE("GPL");