From 3494890562e1583e9db2cd235d52c4c5250b45c0 Mon Sep 17 00:00:00 2001 From: David Elie-Dit-Cosaque Date: Fri, 29 May 2026 23:08:07 +0000 Subject: [PATCH 01/16] Enhance DPLL subsystem integration and extend external pin support - Updated the DPLL initialization process to unregister the kernel's built-in DPLL netlink family and register a custom one for netdevsim devices, using a boolean flag instead of a static kernel pointer. - Prevent restoration of the kernel's built-in DPLL family during module exit, addressing potential memory issues on aarch64. - Added support for external pins in the nsim_dpll structure, including properties and capabilities for four new external pins. - Improved notification handling for external pins during DPLL device changes. - Adjusted cleanup routines to ensure proper unregistration of external pins on exit. - Improved logging to clarify the status of the kernel DPLL family during initialization and cleanup. - Updated the .gitignore file to include additional build artifacts. --- .gitignore | 6 +++ dpll/dpll_core.c | 69 ++++++++++++++++++++++++++++++- dpll/dpll_nl.c | 2 +- netdevsim/dpll.c | 96 ++++++++++++++++++++++++++++++++++++++++++- netdevsim/netdevsim.h | 3 ++ 5 files changed, 173 insertions(+), 3 deletions(-) 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/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/netdevsim/dpll.c b/netdevsim/dpll.c index cc2427d..aae4374 100644 --- a/netdevsim/dpll.c +++ b/netdevsim/dpll.c @@ -10,6 +10,7 @@ */ #include + #include #include #include @@ -274,10 +275,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) @@ -493,6 +497,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); @@ -555,6 +576,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 @@ -572,7 +634,7 @@ int nsim_dpll_init(struct nsim_dev *nsim_dev) /* 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; @@ -671,6 +733,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; @@ -686,6 +754,19 @@ int nsim_dpll_init(struct nsim_dev *nsim_dev) 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: @@ -736,6 +817,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/netdevsim.h b/netdevsim/netdevsim.h index 526834b..c28c640 100644 --- a/netdevsim/netdevsim.h +++ b/netdevsim/netdevsim.h @@ -90,6 +90,9 @@ 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; From ab108c4d872ce8abb1ebd893ce001e357538b2f2 Mon Sep 17 00:00:00 2001 From: David Elie-Dit-Cosaque Date: Fri, 29 May 2026 23:45:21 +0000 Subject: [PATCH 02/16] Update timecounter initialization to use ktime_get_clocktai_ns() - Changed the timecounter initialization in mock_phc_create to utilize ktime_get_clocktai_ns() instead of ktime_get_real_ns(), ensuring accurate timekeeping in the mock PHC implementation. --- 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 13cb404..98bc797 100644 --- a/ptp/ptp_mock.c +++ b/ptp/ptp_mock.c @@ -238,7 +238,7 @@ struct mock_phc *mock_phc_create(struct device *dev, int logical_clk_id) 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()); + timecounter_init(&phc->tc, &phc->cc, ktime_get_clocktai_ns()); phc->clock = ptp_clock_register(&phc->info, dev); if (IS_ERR(phc->clock)) { From 3ea6e3055849fbd99891e9b1434e2f5aa0157937 Mon Sep 17 00:00:00 2001 From: David Elie-Dit-Cosaque Date: Thu, 4 Jun 2026 15:43:34 +0000 Subject: [PATCH 03/16] Implement user-space control for DPLL lock status via sysfs - Added a writable sysfs interface to allow user-space applications to control the lock status of DPLL devices. - Introduced `lock_status_show` and `lock_status_store` functions to handle reading and writing the lock status. - Updated the `nsim_dpll` structure to include a `lock_status` field and a pointer to the sysfs device. - Ensured proper cleanup of sysfs entries during DPLL device exit. - Initialized the lock status to `DPLL_LOCK_STATUS_LOCKED_HO_ACQ` during DPLL initialization. --- netdevsim/dpll.c | 108 ++++++++++++++++++++++++++++++++++++++++-- netdevsim/netdevsim.h | 2 + 2 files changed, 106 insertions(+), 4 deletions(-) diff --git a/netdevsim/dpll.c b/netdevsim/dpll.c index aae4374..a73744a 100644 --- a/netdevsim/dpll.c +++ b/netdevsim/dpll.c @@ -2,8 +2,9 @@ /* * 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. @@ -11,6 +12,7 @@ #include +#include #include #include #include @@ -18,6 +20,7 @@ #include #include #include +#include #include #include #include "netdevsim.h" @@ -43,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 @@ -53,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; } @@ -126,6 +133,54 @@ static const struct dpll_pin_ops nsim_dpll_rclk_pin_ops = { .phase_offset_get = nsim_dpll_pin_phase_offset_get, }; +/* ---- sysfs: writable lock_status for user-space DPLL control ----------- */ + +static struct class *nsim_dpll_class; + +static ssize_t lock_status_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + struct nsim_dpll *ndpll = dev_get_drvdata(dev); + + 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 lock_status_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct nsim_dpll *ndpll = dev_get_drvdata(dev); + 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; + + if (new_status != ndpll->lock_status) { + ndpll->lock_status = new_status; + dpll_device_change_ntf(ndpll->pps_dpll); + dpll_device_change_ntf(ndpll->eec_dpll); + } + + return count; +} + +static DEVICE_ATTR_RW(lock_status); + /* ---- UBX protocol simulation ------------------------------------------- */ #define UBX_SYNC1 0xB5 @@ -530,6 +585,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 */ @@ -724,6 +780,26 @@ int nsim_dpll_init(struct nsim_dev *nsim_dev) gdev->id); } + /* Writable sysfs interface: /sys/class/nsim_dpll/dpll0/lock_status */ + nsim_dpll_class = class_create("nsim_dpll"); + if (IS_ERR(nsim_dpll_class)) { + err = PTR_ERR(nsim_dpll_class); + nsim_dpll_class = NULL; + goto err_gnss_cleanup; + } + + ndpll->sysfs_dev = device_create(nsim_dpll_class, NULL, + MKDEV(0, 0), ndpll, "dpll0"); + if (IS_ERR(ndpll->sysfs_dev)) { + err = PTR_ERR(ndpll->sysfs_dev); + ndpll->sysfs_dev = NULL; + goto err_class_destroy; + } + + err = device_create_file(ndpll->sysfs_dev, &dev_attr_lock_status); + if (err) + goto err_sysfs_dev_destroy; + /* * Emit change notifications so that userspace consumers (e.g. * linuxptp-daemon) that subscribe to DPLL multicast before we @@ -750,6 +826,20 @@ int nsim_dpll_init(struct nsim_dev *nsim_dev) ndpll->clock_id); return 0; +err_sysfs_dev_destroy: + device_destroy(nsim_dpll_class, MKDEV(0, 0)); + ndpll->sysfs_dev = NULL; +err_class_destroy: + class_destroy(nsim_dpll_class); + nsim_dpll_class = 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); @@ -805,6 +895,16 @@ void nsim_dpll_exit(struct nsim_dev *nsim_dev) if (!ndpll) return; + if (ndpll->sysfs_dev) { + device_remove_file(ndpll->sysfs_dev, &dev_attr_lock_status); + device_destroy(nsim_dpll_class, MKDEV(0, 0)); + ndpll->sysfs_dev = NULL; + } + if (nsim_dpll_class) { + class_destroy(nsim_dpll_class); + nsim_dpll_class = NULL; + } + hrtimer_cancel(&ndpll->ntf_timer); cancel_work_sync(&ndpll->ntf_work); diff --git a/netdevsim/netdevsim.h b/netdevsim/netdevsim.h index c28c640..7ae743f 100644 --- a/netdevsim/netdevsim.h +++ b/netdevsim/netdevsim.h @@ -102,6 +102,8 @@ struct nsim_dpll { spinlock_t gnss_lock; bool ubx_nav_enabled; u8 gnss_gps_fix; + enum dpll_lock_status lock_status; + struct device *sysfs_dev; }; struct nsim_ethtool_pauseparam { From f1e6dae2fe0dd1355e9163902cd837b3be3f5b49 Mon Sep 17 00:00:00 2001 From: David Elie-Dit-Cosaque Date: Thu, 4 Jun 2026 16:53:10 +0000 Subject: [PATCH 04/16] Enhance Makefile and README for DKMS module management and DPLL testing - Added `dkms-install` and `dkms-uninstall` targets to the Makefile for managing DKMS modules, including installation and removal procedures. - Introduced `test-dpll` target in the Makefile to facilitate running DPLL unit tests. - Updated README to include detailed instructions for running DPLL unit tests, including usage examples and test coverage details. - Added a new script `test-dpll.sh` for executing DPLL unit tests, covering various aspects of DPLL emulation and device management. --- .github/workflows/ci.yml | 8 +++++++- README.md | 35 ++++++++++++++++++++++++++++++++++- 2 files changed, 41 insertions(+), 2 deletions(-) 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/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. From 87d59ded1fbc0c2167cc1aab0945d5315b4f0eb4 Mon Sep 17 00:00:00 2001 From: David Elie-Dit-Cosaque Date: Thu, 4 Jun 2026 19:07:12 +0000 Subject: [PATCH 05/16] Refactor DPLL sysfs interface for improved device management - Replaced the global class-based sysfs interface with a per-instance kobject for DPLL devices, allowing multiple instances to coexist without naming conflicts. - Updated the `lock_status_show` and `lock_status_store` functions to use the new kobject structure. - Modified the `nsim_dpll` structure to include a kobject pointer and a kobj_attribute for lock status. - Enhanced the Makefile clean target to remove build artifacts more efficiently. - Updated the test script to reflect changes in the sysfs path for lock status, ensuring accurate testing of DPLL functionality. --- netdevsim/dpll.c | 90 +++++++++++++++++++++++-------------------- netdevsim/netdevsim.h | 3 +- 2 files changed, 50 insertions(+), 43 deletions(-) diff --git a/netdevsim/dpll.c b/netdevsim/dpll.c index a73744a..1a73ab5 100644 --- a/netdevsim/dpll.c +++ b/netdevsim/dpll.c @@ -135,12 +135,12 @@ static const struct dpll_pin_ops nsim_dpll_rclk_pin_ops = { /* ---- sysfs: writable lock_status for user-space DPLL control ----------- */ -static struct class *nsim_dpll_class; - -static ssize_t lock_status_show(struct device *dev, - struct device_attribute *attr, char *buf) +static ssize_t nsim_dpll_lock_status_show(struct kobject *kobj, + struct kobj_attribute *attr, + char *buf) { - struct nsim_dpll *ndpll = dev_get_drvdata(dev); + struct nsim_dpll *ndpll = container_of(attr, struct nsim_dpll, + lock_status_attr); switch (ndpll->lock_status) { case DPLL_LOCK_STATUS_LOCKED_HO_ACQ: @@ -154,11 +154,12 @@ static ssize_t lock_status_show(struct device *dev, } } -static ssize_t lock_status_store(struct device *dev, - struct device_attribute *attr, - const char *buf, size_t count) +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 = dev_get_drvdata(dev); + struct nsim_dpll *ndpll = container_of(attr, struct nsim_dpll, + lock_status_attr); enum dpll_lock_status new_status; if (sysfs_streq(buf, "locked")) @@ -179,8 +180,6 @@ static ssize_t lock_status_store(struct device *dev, return count; } -static DEVICE_ATTR_RW(lock_status); - /* ---- UBX protocol simulation ------------------------------------------- */ #define UBX_SYNC1 0xB5 @@ -780,25 +779,38 @@ int nsim_dpll_init(struct nsim_dev *nsim_dev) gdev->id); } - /* Writable sysfs interface: /sys/class/nsim_dpll/dpll0/lock_status */ - nsim_dpll_class = class_create("nsim_dpll"); - if (IS_ERR(nsim_dpll_class)) { - err = PTR_ERR(nsim_dpll_class); - nsim_dpll_class = NULL; - goto err_gnss_cleanup; - } + /* + * 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; - ndpll->sysfs_dev = device_create(nsim_dpll_class, NULL, - MKDEV(0, 0), ndpll, "dpll0"); - if (IS_ERR(ndpll->sysfs_dev)) { - err = PTR_ERR(ndpll->sysfs_dev); - ndpll->sysfs_dev = NULL; - goto err_class_destroy; - } + if (nsim_dev->fake_pci_dev) + parent_dev = &nsim_dev->fake_pci_dev->dev; + else + parent_dev = &nsim_dev->nsim_bus_dev->dev; - err = device_create_file(ndpll->sysfs_dev, &dev_attr_lock_status); - if (err) - goto err_sysfs_dev_destroy; + 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. @@ -826,12 +838,9 @@ int nsim_dpll_init(struct nsim_dev *nsim_dev) ndpll->clock_id); return 0; -err_sysfs_dev_destroy: - device_destroy(nsim_dpll_class, MKDEV(0, 0)); - ndpll->sysfs_dev = NULL; -err_class_destroy: - class_destroy(nsim_dpll_class); - nsim_dpll_class = NULL; +err_sysfs_kobj: + kobject_put(ndpll->sysfs_kobj); + ndpll->sysfs_kobj = NULL; err_gnss_cleanup: if (ndpll->gnss_dev) { ndpll->ubx_nav_enabled = false; @@ -895,14 +904,11 @@ void nsim_dpll_exit(struct nsim_dev *nsim_dev) if (!ndpll) return; - if (ndpll->sysfs_dev) { - device_remove_file(ndpll->sysfs_dev, &dev_attr_lock_status); - device_destroy(nsim_dpll_class, MKDEV(0, 0)); - ndpll->sysfs_dev = NULL; - } - if (nsim_dpll_class) { - class_destroy(nsim_dpll_class); - nsim_dpll_class = NULL; + 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); diff --git a/netdevsim/netdevsim.h b/netdevsim/netdevsim.h index 7ae743f..1775466 100644 --- a/netdevsim/netdevsim.h +++ b/netdevsim/netdevsim.h @@ -103,7 +103,8 @@ struct nsim_dpll { bool ubx_nav_enabled; u8 gnss_gps_fix; enum dpll_lock_status lock_status; - struct device *sysfs_dev; + struct kobject *sysfs_kobj; + struct kobj_attribute lock_status_attr; }; struct nsim_ethtool_pauseparam { From 28d30d7dbbac28c484838957308c637c3443a443 Mon Sep 17 00:00:00 2001 From: David Elie-Dit-Cosaque Date: Thu, 4 Jun 2026 21:28:15 +0000 Subject: [PATCH 06/16] Enhance mock PHC timekeeping and synchronization - Updated the mock PHC implementation to capture the internal counter at the PPS edge, improving accuracy in timestamping external events. - Resynchronized the PHC timecounter to current TAI upon enabling external timestamping, ensuring minimal offset for timekeeping. - Added necessary spin locks to protect timecounter reads and initialization, enhancing thread safety in the mock implementation. --- ptp/ptp_mock.c | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/ptp/ptp_mock.c b/ptp/ptp_mock.c index 98bc797..a364941 100644 --- a/ptp/ptp_mock.c +++ b/ptp/ptp_mock.c @@ -121,16 +121,22 @@ 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 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. + * Capture the PHC's own time at the PPS edge, just like real + * hardware latches its internal counter on the external 1PPS + * input. ts2phc compares this with the NMEA-derived time and + * steps/adjusts the PHC to converge. */ + spin_lock_irqsave(&phc->lock, flags); + ns = timecounter_read(&phc->tc); + spin_unlock_irqrestore(&phc->lock, flags); + event.type = PTP_CLOCK_EXTTS; event.index = phc->extts_channel; - event.timestamp = div64_u64(ktime_get_clocktai_ns(), - NSEC_PER_SEC) * NSEC_PER_SEC; + event.timestamp = ns; ptp_clock_event(phc->clock, &event); hrtimer_forward_now(timer, ns_to_ktime(NSEC_PER_SEC)); @@ -151,12 +157,24 @@ static int mock_phc_enable(struct ptp_clock_info *info, switch (rq->type) { case PTP_CLK_REQ_EXTTS: { u64 now_ns, ns_to_next; + unsigned long flags; if (rq->extts.index >= info->n_ext_ts) return -EINVAL; if (on) { phc->extts_channel = rq->extts.index; phc->extts_enabled = true; + /* + * Re-sync the PHC timecounter to current TAI so + * ts2phc starts with a near-zero offset regardless + * of how much wall-clock time elapsed since the + * device was created (boot-time NTP step, etc.). + */ + spin_lock_irqsave(&phc->lock, flags); + timecounter_init(&phc->tc, &phc->cc, + ktime_get_clocktai_ns()); + spin_unlock_irqrestore(&phc->lock, flags); + now_ns = ktime_get_real_ns(); ns_to_next = NSEC_PER_SEC - do_div(now_ns, NSEC_PER_SEC); From d641f431e717da9749676864285382eacb8db685 Mon Sep 17 00:00:00 2001 From: David Elie-Dit-Cosaque Date: Thu, 4 Jun 2026 21:42:21 +0000 Subject: [PATCH 07/16] fix --- ptp/ptp_mock.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ptp/ptp_mock.c b/ptp/ptp_mock.c index a364941..1c9cdef 100644 --- a/ptp/ptp_mock.c +++ b/ptp/ptp_mock.c @@ -22,7 +22,7 @@ * 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 +/* Timestamps from ktime_get_clocktai() 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. */ @@ -47,7 +47,7 @@ EXPORT_SYMBOL_GPL(mock_phc_get_ptp_info); static u64 mock_phc_cc_read(CYCLECOUNTER_READ_CONST struct cyclecounter *cc) { - return ktime_get_raw_ns(); + return ktime_get_clocktai_ns(); } static int mock_phc_adjfine(struct ptp_clock_info *info, long scaled_ppm) From f15bfc11f19ac7cc24e92d06f1e9761ced28c155 Mon Sep 17 00:00:00 2001 From: David Elie-Dit-Cosaque Date: Thu, 4 Jun 2026 22:33:36 +0000 Subject: [PATCH 08/16] Enhance mock PHC timekeeping with TAI-based synchronization - Introduced TAI-based timekeeping in the mock PHC implementation, allowing for more accurate time synchronization. - Added fields for offset, frequency correction, and last TAI snapshot to the mock_phc structure. - Updated timekeeping functions to utilize the new TAI-based approach, ensuring the PHC time remains closely aligned with real TAI. - Removed unnecessary timecounter and cyclecounter references, streamlining the code for better clarity and performance. --- include/linux/ptp_mock.h | 6 +- ptp/ptp_mock.c | 117 ++++++++++++++------------------------- 2 files changed, 47 insertions(+), 76 deletions(-) diff --git a/include/linux/ptp_mock.h b/include/linux/ptp_mock.h index be00dd9..007614f 100644 --- a/include/linux/ptp_mock.h +++ b/include/linux/ptp_mock.h @@ -16,11 +16,13 @@ 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; + /* TAI-based timekeeping */ + s64 offset_ns; /* PHC = TAI + offset_ns */ + s64 freq_ppb; /* rate correction from adjfine */ + u64 last_tai_ns; /* TAI 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). diff --git a/ptp/ptp_mock.c b/ptp/ptp_mock.c index 1c9cdef..225e05b 100644 --- a/ptp/ptp_mock.c +++ b/ptp/ptp_mock.c @@ -4,8 +4,12 @@ * * 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_TAI plus an accumulated offset. + * adjfine() applies a frequency correction on top of the TAI base so + * the PTP servo feedback loop operates normally. Because the base + * clock already tracks wall time, the servo converges at freq ≈ 0 and + * the PHC stays within microseconds of real TAI — exactly what CI + * tests need. * * 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 +19,9 @@ #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_clocktai() 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) +#define MOCK_PHC_MAX_ADJ_PPB 500000000 #define info_to_phc(d) container_of((d), struct mock_phc, info) @@ -45,23 +31,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 current TAI time. + * Returns the PHC time = TAI + accumulated offset. + * Must be called with phc->lock held. + */ +static u64 mock_phc_read_locked(struct mock_phc *phc) { - return ktime_get_clocktai_ns(); + u64 tai = ktime_get_clocktai_ns(); + s64 elapsed = (s64)(tai - phc->last_tai_ns); + s64 drift; + + if (elapsed > 0 && phc->freq_ppb != 0) { + drift = div_s64(elapsed * phc->freq_ppb, 1000000000LL); + phc->offset_ns += drift; + } + phc->last_tai_ns = tai; + + return (u64)((s64)tai + 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 +71,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 +82,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 tai; spin_lock_irqsave(&phc->lock, flags); - timecounter_init(&phc->tc, &phc->cc, ns); + tai = ktime_get_clocktai_ns(); + phc->last_tai_ns = tai; + phc->offset_ns = (s64)(timespec64_to_ns(ts)) - (s64)tai; + 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,15 +111,6 @@ 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; -} - static enum hrtimer_restart mock_phc_extts_timer(struct hrtimer *timer) { struct mock_phc *phc = container_of(timer, struct mock_phc, extts_timer); @@ -124,14 +118,8 @@ static enum hrtimer_restart mock_phc_extts_timer(struct hrtimer *timer) unsigned long flags; u64 ns; - /* - * Capture the PHC's own time at the PPS edge, just like real - * hardware latches its internal counter on the external 1PPS - * input. ts2phc compares this with the NMEA-derived time and - * steps/adjusts the PHC to converge. - */ spin_lock_irqsave(&phc->lock, flags); - ns = timecounter_read(&phc->tc); + ns = mock_phc_read_locked(phc); spin_unlock_irqrestore(&phc->lock, flags); event.type = PTP_CLOCK_EXTTS; @@ -157,23 +145,12 @@ static int mock_phc_enable(struct ptp_clock_info *info, switch (rq->type) { case PTP_CLK_REQ_EXTTS: { u64 now_ns, ns_to_next; - unsigned long flags; if (rq->extts.index >= info->n_ext_ts) return -EINVAL; if (on) { phc->extts_channel = rq->extts.index; phc->extts_enabled = true; - /* - * Re-sync the PHC timecounter to current TAI so - * ts2phc starts with a near-zero offset regardless - * of how much wall-clock time elapsed since the - * device was created (boot-time NTP step, etc.). - */ - spin_lock_irqsave(&phc->lock, flags); - timecounter_init(&phc->tc, &phc->cc, - ktime_get_clocktai_ns()); - spin_unlock_irqrestore(&phc->lock, flags); now_ns = ktime_get_real_ns(); ns_to_next = NSEC_PER_SEC - @@ -236,27 +213,21 @@ 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_clocktai_ns()); + phc->last_tai_ns = ktime_get_clocktai_ns(); + phc->offset_ns = 0; + phc->freq_ppb = 0; phc->clock = ptp_clock_register(&phc->info, dev); if (IS_ERR(phc->clock)) { @@ -264,8 +235,6 @@ struct mock_phc *mock_phc_create(struct device *dev, int logical_clk_id) goto out_free_phc; } - ptp_schedule_worker(phc->clock, MOCK_PHC_REFRESH_INTERVAL); - return phc; out_free_phc: @@ -286,12 +255,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"); From 29ff555c43c545ff0a930281c6ce66fd08b84434 Mon Sep 17 00:00:00 2001 From: David Elie-Dit-Cosaque Date: Fri, 5 Jun 2026 22:05:21 +0000 Subject: [PATCH 09/16] Enhance Makefile and add unit tests for GNSS and PHC - Updated the Makefile to include new test targets: `test-phc`, `test-gnss-ubx`, and `test-all`, facilitating comprehensive testing of the mock PHC and GNSS functionalities. - Introduced `test-gnss-ubx.sh` for unit testing GNSS device emulation and UBX protocol handling, covering various scenarios including signal blocking and NMEA parsing. - Added `test-phc.sh` for testing the mock PTP Hardware Clock, including time read/write, frequency adjustments, and event delivery. - Modified the mock PHC implementation to utilize CLOCK_MONOTONIC for improved timekeeping accuracy, ensuring resilience against time jumps. --- docs/netdevsim-ptp-operator-ci.drawio | 371 ++++++++++++++++++++++++++ include/linux/ptp_mock.h | 6 +- netdevsim/dpll.c | 75 +++++- netdevsim/netdevsim.h | 1 + ptp/ptp_mock.c | 72 +++-- 5 files changed, 501 insertions(+), 24 deletions(-) create mode 100644 docs/netdevsim-ptp-operator-ci.drawio 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/include/linux/ptp_mock.h b/include/linux/ptp_mock.h index 007614f..3cc7735 100644 --- a/include/linux/ptp_mock.h +++ b/include/linux/ptp_mock.h @@ -19,10 +19,10 @@ struct mock_phc { int logical_clk_id; struct kref ref; spinlock_t lock; - /* TAI-based timekeeping */ - s64 offset_ns; /* PHC = TAI + offset_ns */ + /* MONOTONIC-based timekeeping (immune to phc2sys steps) */ + s64 offset_ns; /* PHC = monotonic + offset_ns */ s64 freq_ppb; /* rate correction from adjfine */ - u64 last_tai_ns; /* TAI snapshot at last update */ + 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). diff --git a/netdevsim/dpll.c b/netdevsim/dpll.c index 1a73ab5..f95b7a3 100644 --- a/netdevsim/dpll.c +++ b/netdevsim/dpll.c @@ -171,6 +171,11 @@ static ssize_t nsim_dpll_lock_status_store(struct kobject *kobj, else return -EINVAL; + /* When signal is blocked via UBX CFG-VALSET, the kernel controls + * lock_status transitions; ignore sysfs writes from gnss-sim. */ + if (ndpll->signal_blocked) + return count; + if (new_status != ndpll->lock_status) { ndpll->lock_status = new_status; dpll_device_change_ntf(ndpll->pps_dpll); @@ -197,8 +202,12 @@ static ssize_t nsim_dpll_lock_status_store(struct kobject *kobj, #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 @@ -478,13 +487,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; @@ -492,6 +511,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) { @@ -516,6 +540,52 @@ 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; + ndpll->lock_status = DPLL_LOCK_STATUS_HOLDOVER; + pr_info("netdevsim: UBX CFG-VALSET INFIL_NCNOTHRS=%u → signal blocked, holdover\n", val); + } else { + ndpll->signal_blocked = false; + ndpll->gnss_gps_fix = 0x03; + ndpll->lock_status = DPLL_LOCK_STATUS_LOCKED_HO_ACQ; + pr_info("netdevsim: UBX CFG-VALSET INFIL_NCNOTHRS=0 → signal restored, locked\n"); + } + schedule_work(&ndpll->ntf_work); + kv++; + pos++; + break; + } + kv++; + pos++; + } + len = ubx_build_ack(resp, sizeof(resp), cls, id); if (len > 0) nsim_ubx_insert_locked(ndpll, resp, len); @@ -774,6 +844,7 @@ 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); diff --git a/netdevsim/netdevsim.h b/netdevsim/netdevsim.h index 1775466..446462e 100644 --- a/netdevsim/netdevsim.h +++ b/netdevsim/netdevsim.h @@ -101,6 +101,7 @@ struct nsim_dpll { 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; diff --git a/ptp/ptp_mock.c b/ptp/ptp_mock.c index 225e05b..21ad67c 100644 --- a/ptp/ptp_mock.c +++ b/ptp/ptp_mock.c @@ -4,12 +4,11 @@ * * Mock-up PTP Hardware Clock driver for virtual network devices * - * The PHC time is derived from CLOCK_TAI plus an accumulated offset. - * adjfine() applies a frequency correction on top of the TAI base so - * the PTP servo feedback loop operates normally. Because the base - * clock already tracks wall time, the servo converges at freq ≈ 0 and - * the PHC stays within microseconds of real TAI — exactly what CI - * tests need. + * 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. * * 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. @@ -33,23 +32,23 @@ EXPORT_SYMBOL_GPL(mock_phc_get_ptp_info); /* * Advance the internal bookkeeping: accumulate the frequency-induced - * drift since the last update and snapshot the current TAI time. - * Returns the PHC time = TAI + accumulated offset. + * drift since the last update and snapshot the monotonic time. + * Returns the PHC time = monotonic + accumulated offset. * Must be called with phc->lock held. */ static u64 mock_phc_read_locked(struct mock_phc *phc) { - u64 tai = ktime_get_clocktai_ns(); - s64 elapsed = (s64)(tai - phc->last_tai_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_tai_ns = tai; + phc->last_mono_ns = raw; - return (u64)((s64)tai + phc->offset_ns); + return (u64)((s64)raw + phc->offset_ns); } static int mock_phc_adjfine(struct ptp_clock_info *info, long scaled_ppm) @@ -83,12 +82,12 @@ static int mock_phc_settime64(struct ptp_clock_info *info, { struct mock_phc *phc = info_to_phc(info); unsigned long flags; - u64 tai; + u64 raw; spin_lock_irqsave(&phc->lock, flags); - tai = ktime_get_clocktai_ns(); - phc->last_tai_ns = tai; - phc->offset_ns = (s64)(timespec64_to_ns(ts)) - (s64)tai; + 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); @@ -117,17 +116,48 @@ static enum hrtimer_restart mock_phc_extts_timer(struct hrtimer *timer) struct ptp_clock_event event; unsigned long flags; u64 ns; + s64 freq; + u64 sec; + u32 frac; + s64 delay; + s64 divisor; spin_lock_irqsave(&phc->lock, flags); ns = mock_phc_read_locked(phc); + freq = phc->freq_ppb; spin_unlock_irqrestore(&phc->lock, flags); + /* Snap to the nearest second boundary. Real 1PPS hardware fires + * exactly at the boundary; the mock timer has jitter so we round. */ + sec = ns; + frac = do_div(sec, NSEC_PER_SEC); + + // #region agent log + pr_info("d753ad EXTTS: phc_ns=%llu frac_ns=%u offset_ns=%lld freq_ppb=%lld\n", + ns, frac, phc->offset_ns, freq); + // #endregion + + if (frac >= NSEC_PER_SEC / 2) + ns = (sec + 1) * NSEC_PER_SEC; + else + ns = sec * NSEC_PER_SEC; + event.type = PTP_CLOCK_EXTTS; event.index = phc->extts_channel; event.timestamp = ns; ptp_clock_event(phc->clock, &event); - hrtimer_forward_now(timer, ns_to_ktime(NSEC_PER_SEC)); + /* Schedule the next event 1 PHC second later. The PHC advances at + * rate (1 + freq_ppb/1e9) relative to CLOCK_MONOTONIC, so scale: + * mono_delay = 1e9 * 1e9 / (1e9 + freq). */ + divisor = 1000000000LL + freq; + if (divisor <= 0) + divisor = 1; + delay = div_s64((s64)NSEC_PER_SEC * 1000000000LL, divisor); + if (delay < NSEC_PER_SEC / 4) + delay = NSEC_PER_SEC / 4; + + hrtimer_forward_now(timer, ns_to_ktime((u64)delay)); return HRTIMER_RESTART; } @@ -149,10 +179,14 @@ static int mock_phc_enable(struct ptp_clock_info *info, if (rq->extts.index >= info->n_ext_ts) return -EINVAL; if (on) { + unsigned long flags; + phc->extts_channel = rq->extts.index; phc->extts_enabled = true; - now_ns = ktime_get_real_ns(); + spin_lock_irqsave(&phc->lock, flags); + now_ns = mock_phc_read_locked(phc); + spin_unlock_irqrestore(&phc->lock, flags); ns_to_next = NSEC_PER_SEC - do_div(now_ns, NSEC_PER_SEC); hrtimer_start(&phc->extts_timer, @@ -225,7 +259,7 @@ struct mock_phc *mock_phc_create(struct device *dev, int logical_clk_id) hrtimer_init(&phc->extts_timer, CLOCK_MONOTONIC, HRTIMER_MODE_REL); phc->extts_timer.function = mock_phc_extts_timer; - phc->last_tai_ns = ktime_get_clocktai_ns(); + phc->last_mono_ns = ktime_get_ns(); phc->offset_ns = 0; phc->freq_ppb = 0; From b45c280eba9813734e2363ba56e81475d08e01ea Mon Sep 17 00:00:00 2001 From: David Elie-Dit-Cosaque Date: Tue, 23 Jun 2026 21:25:44 +0000 Subject: [PATCH 10/16] Enhance PTP mock implementation and sysfs integration - Updated the PTP mock implementation to support a new pin layout, including GNSS-1PPS and four external connectors (SMA1, SMA2, U.FL1, U.FL2). - Introduced a sysfs symlink for the PTP clock, allowing tools to easily locate the PTP device under the PCI device path. - Added a new function to retrieve the kobject for the mock PHC, improving device management and integration with the sysfs interface. - Enhanced the netdevsim structure to include a kobject for PTP compatibility, ensuring proper cleanup during device exit. --- 99-nsim-ptp.rules | 6 +++++- include/linux/ptp_mock.h | 10 ++++++---- include/nsim_rename.h | 1 + netdevsim/netdev.c | 26 ++++++++++++++++++++++++++ netdevsim/netdevsim.h | 1 + ptp/ptp_mock.c | 29 ++++++++++++++++++++++++++--- 6 files changed, 65 insertions(+), 8 deletions(-) diff --git a/99-nsim-ptp.rules b/99-nsim-ptp.rules index cb5ad73..5ed1067 100644 --- a/99-nsim-ptp.rules +++ b/99-nsim-ptp.rules @@ -14,7 +14,11 @@ # 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" diff --git a/include/linux/ptp_mock.h b/include/linux/ptp_mock.h index 3cc7735..bd17127 100644 --- a/include/linux/ptp_mock.h +++ b/include/linux/ptp_mock.h @@ -24,10 +24,11 @@ struct mock_phc { 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; @@ -38,8 +39,9 @@ struct mock_phc { 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/netdev.c b/netdevsim/netdev.c index af5bfc0..96bb93b 100644 --- a/netdevsim/netdev.c +++ b/netdevsim/netdev.c @@ -540,7 +540,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 +583,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 446462e..865735e 100644 --- a/netdevsim/netdevsim.h +++ b/netdevsim/netdevsim.h @@ -130,6 +130,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 21ad67c..49aa726 100644 --- a/ptp/ptp_mock.c +++ b/ptp/ptp_mock.c @@ -20,6 +20,8 @@ #include #include +#include "ptp_private.h" + #define MOCK_PHC_MAX_ADJ_PPB 500000000 #define info_to_phc(d) container_of((d), struct mock_phc, info) @@ -215,6 +217,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; @@ -226,16 +234,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", From 7a04d84a999f0a70db492cab9b43043b30607f5d Mon Sep 17 00:00:00 2001 From: David Elie-Dit-Cosaque Date: Thu, 25 Jun 2026 20:58:27 +0000 Subject: [PATCH 11/16] Add net_device operations for nsim_open and nsim_stop - Implemented the nsim_open and nsim_stop functions to manage the network device's carrier state. - Updated the nsim_netdev_ops and nsim_vf_netdev_ops structures to include the new open and stop operations, enhancing the netdevsim functionality. --- netdevsim/netdev.c | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/netdevsim/netdev.c b/netdevsim/netdev.c index 96bb93b..ca44b5a 100644 --- a/netdevsim/netdev.c +++ b/netdevsim/netdev.c @@ -92,6 +92,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 +413,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 +438,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, From 5aa35cd88bdef9eee99d505b313538be2752216a Mon Sep 17 00:00:00 2001 From: David Elie-Dit-Cosaque Date: Fri, 26 Jun 2026 16:27:31 +0000 Subject: [PATCH 12/16] Remove multicast flag from net_device during setup in netdevsim - Eliminated the multicast flag from the net_device structure in the nsim_setup function, ensuring proper configuration for the simulated network device. --- netdevsim/netdev.c | 1 - 1 file changed, 1 deletion(-) diff --git a/netdevsim/netdev.c b/netdevsim/netdev.c index ca44b5a..099a3f7 100644 --- a/netdevsim/netdev.c +++ b/netdevsim/netdev.c @@ -456,7 +456,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; From a172e39ab3c21621a0fc369141bd7b83b2da399f Mon Sep 17 00:00:00 2001 From: David Elie-Dit-Cosaque Date: Mon, 29 Jun 2026 22:52:15 +0000 Subject: [PATCH 13/16] Enhance mock PHC external timestamping and event handling - Introduced a new field `last_extts_sec` in the `mock_phc` structure to prevent duplicate external timestamp events. - Updated the external timestamping logic to poll the PHC time and emit events only when the second counter increments, ensuring accurate event delivery. - Improved the timer rescheduling mechanism for better performance and reliability in timestamp generation. - Enhanced the `mock_phc_enable` function to properly initialize the external timestamping state. --- include/linux/ptp_mock.h | 1 + netdevsim/netdev.c | 21 ++++++++- ptp/ptp_mock.c | 93 +++++++++++++++++----------------------- 3 files changed, 60 insertions(+), 55 deletions(-) diff --git a/include/linux/ptp_mock.h b/include/linux/ptp_mock.h index bd17127..418960e 100644 --- a/include/linux/ptp_mock.h +++ b/include/linux/ptp_mock.h @@ -33,6 +33,7 @@ struct mock_phc { struct hrtimer extts_timer; bool extts_enabled; int extts_channel; + u64 last_extts_sec; /* dedup: last PHC second we reported */ }; #if IS_ENABLED(CONFIG_PTP_1588_CLOCK_MOCK) diff --git a/netdevsim/netdev.c b/netdevsim/netdev.c index 099a3f7..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) { diff --git a/ptp/ptp_mock.c b/ptp/ptp_mock.c index 49aa726..d50e876 100644 --- a/ptp/ptp_mock.c +++ b/ptp/ptp_mock.c @@ -112,54 +112,47 @@ static int mock_phc_gettime64(struct ptp_clock_info *info, return 0; } +/* + * Poll the PHC time at a high rate and emit one EXTTS event each time + * the PHC second counter increments. The event timestamp is always + * the exact PHC second boundary (sec * 1e9). + * + * Why polling? Computing the exact monotonic instant of the next PHC + * second requires arithmetic involving freq_ppb that is fragile under + * large offsets or frequency swings. Polling at 100 ms is cheap and + * bulletproof: hrtimer_forward_now + HRTIMER_RESTART never stops, and + * the last_extts_sec dedup means exactly one event per PHC second. + */ +#define EXTTS_POLL_NS (NSEC_PER_SEC / 10) /* 100 ms */ + 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 ns; - s64 freq; + u64 phc_ns; u64 sec; - u32 frac; - s64 delay; - s64 divisor; + + if (!READ_ONCE(phc->extts_enabled)) + goto reschedule; spin_lock_irqsave(&phc->lock, flags); - ns = mock_phc_read_locked(phc); - freq = phc->freq_ppb; + phc_ns = mock_phc_read_locked(phc); spin_unlock_irqrestore(&phc->lock, flags); - /* Snap to the nearest second boundary. Real 1PPS hardware fires - * exactly at the boundary; the mock timer has jitter so we round. */ - sec = ns; - frac = do_div(sec, NSEC_PER_SEC); - - // #region agent log - pr_info("d753ad EXTTS: phc_ns=%llu frac_ns=%u offset_ns=%lld freq_ppb=%lld\n", - ns, frac, phc->offset_ns, freq); - // #endregion - - if (frac >= NSEC_PER_SEC / 2) - ns = (sec + 1) * NSEC_PER_SEC; - else - ns = sec * NSEC_PER_SEC; - - event.type = PTP_CLOCK_EXTTS; - event.index = phc->extts_channel; - event.timestamp = ns; - ptp_clock_event(phc->clock, &event); - - /* Schedule the next event 1 PHC second later. The PHC advances at - * rate (1 + freq_ppb/1e9) relative to CLOCK_MONOTONIC, so scale: - * mono_delay = 1e9 * 1e9 / (1e9 + freq). */ - divisor = 1000000000LL + freq; - if (divisor <= 0) - divisor = 1; - delay = div_s64((s64)NSEC_PER_SEC * 1000000000LL, divisor); - if (delay < NSEC_PER_SEC / 4) - delay = NSEC_PER_SEC / 4; - - hrtimer_forward_now(timer, ns_to_ktime((u64)delay)); + sec = div_u64(phc_ns, NSEC_PER_SEC); + + if (sec != phc->last_extts_sec) { + phc->last_extts_sec = sec; + + event.timestamp = sec * NSEC_PER_SEC; + event.type = PTP_CLOCK_EXTTS; + event.index = phc->extts_channel; + ptp_clock_event(phc->clock, &event); + } + +reschedule: + hrtimer_forward_now(timer, ns_to_ktime(EXTTS_POLL_NS)); return HRTIMER_RESTART; } @@ -176,27 +169,16 @@ 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; - phc->extts_channel = rq->extts.index; - phc->extts_enabled = true; - - spin_lock_irqsave(&phc->lock, flags); - now_ns = mock_phc_read_locked(phc); - spin_unlock_irqrestore(&phc->lock, flags); - 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); + phc->last_extts_sec = 0; + /* 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; } @@ -287,6 +269,9 @@ 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_POLL_NS), + HRTIMER_MODE_REL); if (IS_ERR(phc->clock)) { err = PTR_ERR(phc->clock); goto out_free_phc; From cffb7b0aa8736b5d160a8b1a8b483be78a5a5dee Mon Sep 17 00:00:00 2001 From: David Elie-Dit-Cosaque Date: Fri, 24 Jul 2026 20:01:54 +0000 Subject: [PATCH 14/16] fix: reply to UBX NAV-STATUS/NAV-CLOCK polls with real payloads linuxptp-daemon needs NAV-STATUS gpsFix to leave GNSS freerun; ACK-only replies left T-GM stuck at s0 despite locked DPLL and ts2phc. --- netdevsim/dpll.c | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/netdevsim/dpll.c b/netdevsim/dpll.c index f95b7a3..066dfd1 100644 --- a/netdevsim/dpll.c +++ b/netdevsim/dpll.c @@ -591,6 +591,34 @@ static int nsim_gnss_write_raw(struct gnss_device *gdev, 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); + } + break; default: len = ubx_build_ack(resp, sizeof(resp), cls, id); if (len > 0) From 23ecc6f2036e70c40ef4c48e0e84ef16f7b19e92 Mon Sep 17 00:00:00 2001 From: David Elie-Dit-Cosaque Date: Fri, 24 Jul 2026 23:06:24 +0000 Subject: [PATCH 15/16] fix: stabilize Kind T-GM lock without phc2sys/sysfs writes Keep mock PHC on CLOCK_MONOTONIC with TAI-aligned EXTTS, drive DPLL holdover/freerun from NMEA GGA NoFix (Kind /sys is RO), and open GNSS devices with MODE=0666 for gpsd. --- 99-nsim-ptp.rules | 4 ++ include/linux/ptp_mock.h | 6 +- netdevsim/dpll.c | 131 +++++++++++++++++++++++++++++++++------ netdevsim/netdevsim.h | 1 + ptp/ptp_mock.c | 87 ++++++++++++++++++-------- 5 files changed, 182 insertions(+), 47 deletions(-) diff --git a/99-nsim-ptp.rules b/99-nsim-ptp.rules index 5ed1067..ca6f937 100644 --- a/99-nsim-ptp.rules +++ b/99-nsim-ptp.rules @@ -22,3 +22,7 @@ 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; 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/include/linux/ptp_mock.h b/include/linux/ptp_mock.h index 418960e..d7ae58a 100644 --- a/include/linux/ptp_mock.h +++ b/include/linux/ptp_mock.h @@ -19,8 +19,8 @@ struct mock_phc { int logical_clk_id; struct kref ref; spinlock_t lock; - /* MONOTONIC-based timekeeping (immune to phc2sys steps) */ - s64 offset_ns; /* PHC = monotonic + offset_ns */ + /* 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 */ /* @@ -33,7 +33,7 @@ struct mock_phc { struct hrtimer extts_timer; bool extts_enabled; int extts_channel; - u64 last_extts_sec; /* dedup: last PHC second we reported */ + u64 last_extts_sec; /* dedup: last CLOCK_TAI second we reported */ }; #if IS_ENABLED(CONFIG_PTP_1588_CLOCK_MOCK) diff --git a/netdevsim/dpll.c b/netdevsim/dpll.c index 066dfd1..2cbdff5 100644 --- a/netdevsim/dpll.c +++ b/netdevsim/dpll.c @@ -133,6 +133,56 @@ 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). */ +#define NSIM_DPLL_HOLDOVER_TIMEOUT_SEC 5 + +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, @@ -171,15 +221,21 @@ static ssize_t nsim_dpll_lock_status_store(struct kobject *kobj, else return -EINVAL; - /* When signal is blocked via UBX CFG-VALSET, the kernel controls - * lock_status transitions; ignore sysfs writes from gnss-sim. */ - if (ndpll->signal_blocked) + /* + * 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 != ndpll->lock_status) { - ndpll->lock_status = new_status; - dpll_device_change_ntf(ndpll->pps_dpll); - dpll_device_change_ntf(ndpll->eec_dpll); + 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; @@ -421,6 +477,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) @@ -444,13 +505,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; } } @@ -463,6 +540,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; } @@ -569,15 +660,14 @@ static int nsim_gnss_write_raw(struct gnss_device *gdev, if (val > 0) { ndpll->signal_blocked = true; ndpll->gnss_gps_fix = 0x00; - ndpll->lock_status = DPLL_LOCK_STATUS_HOLDOVER; + 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; - ndpll->lock_status = DPLL_LOCK_STATUS_LOCKED_HO_ACQ; + 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"); } - schedule_work(&ndpll->ntf_work); kv++; pos++; break; @@ -781,6 +871,8 @@ 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; @@ -1011,6 +1103,7 @@ void nsim_dpll_exit(struct nsim_dev *nsim_dev) } hrtimer_cancel(&ndpll->ntf_timer); + hrtimer_cancel(&ndpll->holdover_timer); cancel_work_sync(&ndpll->ntf_work); if (ndpll->gnss_dev) { diff --git a/netdevsim/netdevsim.h b/netdevsim/netdevsim.h index 865735e..5701f58 100644 --- a/netdevsim/netdevsim.h +++ b/netdevsim/netdevsim.h @@ -98,6 +98,7 @@ struct nsim_dpll { 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; diff --git a/ptp/ptp_mock.c b/ptp/ptp_mock.c index d50e876..b05f4f9 100644 --- a/ptp/ptp_mock.c +++ b/ptp/ptp_mock.c @@ -10,6 +10,10 @@ * 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. */ @@ -19,6 +23,7 @@ #include #include #include +#include #include "ptp_private.h" @@ -35,7 +40,7 @@ EXPORT_SYMBOL_GPL(mock_phc_get_ptp_info); /* * Advance the internal bookkeeping: accumulate the frequency-induced * drift since the last update and snapshot the monotonic time. - * Returns the PHC time = monotonic + accumulated offset. + * 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) @@ -113,46 +118,61 @@ static int mock_phc_gettime64(struct ptp_clock_info *info, } /* - * Poll the PHC time at a high rate and emit one EXTTS event each time - * the PHC second counter increments. The event timestamp is always - * the exact PHC second boundary (sec * 1e9). + * 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 polling? Computing the exact monotonic instant of the next PHC - * second requires arithmetic involving freq_ppb that is fragile under - * large offsets or frequency swings. Polling at 100 ms is cheap and - * bulletproof: hrtimer_forward_now + HRTIMER_RESTART never stops, and - * the last_extts_sec dedup means exactly one event per PHC second. + * 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_POLL_NS (NSEC_PER_SEC / 10) /* 100 ms */ +#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 phc_ns; - u64 sec; + u64 tai_ns, tai_sec, phc_ns, late_ns, next_delta_ns; - if (!READ_ONCE(phc->extts_enabled)) - goto reschedule; + if (!READ_ONCE(phc->extts_enabled)) { + hrtimer_forward_now(timer, ns_to_ktime(EXTTS_IDLE_POLL_NS)); + return HRTIMER_RESTART; + } - spin_lock_irqsave(&phc->lock, flags); - phc_ns = mock_phc_read_locked(phc); - spin_unlock_irqrestore(&phc->lock, flags); + tai_ns = ktime_get_clocktai_ns(); + tai_sec = div_u64(tai_ns, NSEC_PER_SEC); + late_ns = tai_ns - tai_sec * NSEC_PER_SEC; - sec = div_u64(phc_ns, NSEC_PER_SEC); + if (tai_sec != phc->last_extts_sec) { + phc->last_extts_sec = tai_sec; - if (sec != phc->last_extts_sec) { - phc->last_extts_sec = sec; + spin_lock_irqsave(&phc->lock, flags); + phc_ns = mock_phc_read_locked(phc); + spin_unlock_irqrestore(&phc->lock, flags); - event.timestamp = sec * NSEC_PER_SEC; + event.timestamp = phc_ns - late_ns; event.type = PTP_CLOCK_EXTTS; event.index = phc->extts_channel; ptp_clock_event(phc->clock, &event); } -reschedule: - hrtimer_forward_now(timer, ns_to_ktime(EXTTS_POLL_NS)); + /* 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; } @@ -172,8 +192,25 @@ static int mock_phc_enable(struct ptp_clock_info *info, 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->last_extts_sec = 0; + /* + * 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); @@ -270,7 +307,7 @@ struct mock_phc *mock_phc_create(struct device *dev, int logical_clk_id) phc->clock = ptp_clock_register(&phc->info, dev); if (!IS_ERR(phc->clock)) - hrtimer_start(&phc->extts_timer, ns_to_ktime(EXTTS_POLL_NS), + 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); From 364612402c1e8e8208ff8287726eb500d8a85152 Mon Sep 17 00:00:00 2001 From: David Elie-Dit-Cosaque Date: Sat, 25 Jul 2026 01:33:58 +0000 Subject: [PATCH 16/16] fix: extend simulated DPLL holdover to 30s Give TGMBC cascading-holdover tests enough time to observe GM CC7 on the BC before the DPLL drops to FREERUN/CC248. --- netdevsim/dpll.c | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/netdevsim/dpll.c b/netdevsim/dpll.c index 2cbdff5..64b7c2c 100644 --- a/netdevsim/dpll.c +++ b/netdevsim/dpll.c @@ -133,8 +133,11 @@ 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). */ -#define NSIM_DPLL_HOLDOVER_TIMEOUT_SEC 5 +/* 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)