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");