〖原创〗单调时钟也会变速:一次 NTP 调频影响 Xenomai 周期同步的定位过程

单调时钟为什么仍受 NTP 影响:一次 Xenomai 周期同步排查

一 问题从哪里来

做 Linux 周期任务时,我们通常会避开 CLOCK_REALTIME,改用 CLOCK_MONOTONIC。原因也很直接,系统时间可能被修改,而单调时钟不会随着一次手动校时突然倒退,用它计算超时和安排周期更合适。

但用了单调时钟和绝对时间等待,周期发帧就一定稳定了吗?

在一次基于 Xenomai 的 EtherCAT 同步排查中,应用由 Cobalt 实时核调度,却仍然出现了相位先偏移、再逐步回归的现象。我们从同步算法查到它依赖的时间基准,再通过时钟测量和源码分析定位影响路径。本文按这个思路展开,说明主机时钟与周期同步之间的关系。为方便讨论,统一使用设备 A、主站 M0 和主站 M1 表述。

1 运行环境与同步目标

设备 A 是一台 ARM64 嵌入式设备,运行 Linux 6.1.141、Dovetail 和 Xenomai 3.3 系列。实时应用通过 Cobalt POSIX 接口执行周期任务,并通过 RTDM 接口与 EtherCAT 主站交互。

本次讨论的周期为 500µs,也就是每秒 2000 个周期。我们选择 EtherCAT 参考从站的 DC 时钟作为同步基准,应用调整自己的周期去跟随参考 DC。主机上的 systemd-timesyncd 保持运行,负责主机系统时间与外部时间源同步。

这里实际上存在两个不同的同步目标:

  • 主机系统时间希望跟随网络时间源。
  • EtherCAT 应用周期希望跟随参考从站 DC。

本次排查关注的是第二个目标:应用的周期发帧能否稳定跟随参考 DC。

2 从相位波形看周期发帧时序

排查时,我们在驱动器调试软件的连续示波器中观察两个量:SYNC 信号的周期,以及 SYNC − IRQ 的相位差。这里的 IRQ 是 ESC 的 SM2 中断,在本次配置下,PDO 帧经过 ESC 时触发。因此,IRQ 为我们提供了一个在从站侧观察 PDO 帧到达时刻的位置。

下面这张截图展示了当时看到的现象:

20261103-212628

图中红色曲线为 SYNC 信号周期,青色曲线为 SYNC 时刻减去 IRQ 时刻。横轴约覆盖一分钟,光标间隔约为 59.924 秒;两个通道显示的时间量单位均为 µs。统计栏中,SYNC 周期约为 500µs,相位差最小值为 449µs、最大值为 480µs,变化幅度约为 31µs。

青色曲线先明显突降,随后逐步回升到原先附近,之后又出现了类似变化。在同一周期配对关系下,SYNC − IRQ 减小表示 IRQ 相对 SYNC 后移,也就是 PDO 帧到达 ESC 的时刻相对 SYNC 变晚;差值回升则表示它又逐步前移。

在传输路径延迟稳定的条件下,IRQ 相对 SYNC 的变化反映了主站周期发帧时刻的变化。约 31µs 是相位变化幅度,不能直接理解为某一个应用周期突然延长了 31µs。

应用本来就在通过 DC 同步算法调整周期,因此首先要检查相位计算与周期修正是否正确,以及实际发帧是否受到线程迟到或收发延迟的影响。

截图没有配套的逐事件参数和时间戳记录,本文以它说明现象;后面的软件相位与时钟采样来自另一组记录,不能逐点对应。

同步算法需要一段时间才能识别并补偿频差,因此还要判断:波形中的偏移和回升,是否与它的响应过程有关。

检查算法,也包括检查它依赖的时间基准。周期修正最终要落实到下一次等待目标上;如果生成目标所用的时钟改变了走时速率,实际周期和发帧时刻也会受到影响。

除了同步算法本身,我们还需要检查:Cobalt MONOTONIC 的走时速率是否发生了变化?如果变了,是什么改变了它?接下来先建立比较时钟速率的方法,再通过受控实验检验可能的影响因素。

二 测量 Cobalt MONOTONIC 的走时速率

要判断一个时钟是否变速,需要选一个比较基准。这里使用 Linux 的 CLOCK_MONOTONIC_RAW,后文简称 RAW;它不经 NTP 速率校正,适合用于观察软件时间尺度相对原始单调时间的变化。它并非外部标准时钟,这项比较也不用于判断绝对走时精度。

测量关注各时钟在同一段时间内推进了多少,而不是某次读数是否相等。

1 先分清我们在观察什么

周期同步问题中,至少有三个量:

相位误差:应用同步位置与参考时钟目标位置之间的偏差
频率误差:两个时钟单位时间内推进速度的差异
执行迟到:线程执行到某个检查点时,已经超过计划目标多少时间

这三个量可能互相影响,但不是同一个指标。只看到相位偏了几十微秒,无法立即判断是线程晚醒、报文晚发,还是两个时钟的速率关系发生变化。

2 把时钟一起采下来

我们把 Cobalt MONOTONIC、Linux MONOTONIC、Linux RAW 和日历时间 REALTIME 一起记录,并保留 DC 相位、周期校正量、deadline、参考样本及实时线程 MSW 计数。后面验证校时因素时,再结合 adjtimex 参数分析。

比较不同时间域时,不能把两次顺序读取当成同一个时刻。一个可用的采样方式是:

Cobalt MONOTONIC before
    Linux MONOTONIC
    Linux RAW
    Linux REALTIME
Cobalt MONOTONIC after

用前后两次 Cobalt 读数的中点近似中间时刻,再记录整个括号宽度。括号过宽时,样本的配对误差也变大,应当单独标记或者从精细速率分析中排除。

这里的 Linux 侧采样和 adjtimex 查询由独立诊断路径完成,不放进硬实时周期线程。单个进程同时使用 Linux 和 Cobalt 入口时,需要显式区分调用路径。

3 不比较起点,比较增量

不同时间域的起点可能相差很多。直接算某一时刻 MONOTONIC - RAW,得到的是一个混有起点差和累计频差的值,无法直接说明当前速率。

我们取两个有效配对样本,计算:

relative_ppm = (ΔMONOTONIC / ΔRAW - 1) × 1,000,000

对 Cobalt MONOTONIC 也作相同计算,就能比较两者相对 RAW 的速率是否一起变化。这里 ppm 表示百万分之一,数值为正表示相对 RAW 走得更快。

4 将主机校时列为候选因素

有了比较速率的方法,就可以检验可能的影响因素。主机上的 timesyncd 一直在运行:它用于校时的频率调整,是否也会传到 Cobalt?我们选取这一因素进行受控实验。

Linux 通过 adjtimex() 等接口维护时间校正参数。struct timex 中的 freq 不是直接以 ppm 表示,而是使用缩放后的单位:

freq_ppm = timex.freq / 65536.0

例如本次实测中的 freq=2574371,换算后约为 39.281784ppm。offset 的单位还要结合 STA_NANO 状态位判断,不能一律当作微秒。Linux adjtimex 手册

freq 只是校正参数之一,不能直接代替窗口内的实测速率,因此表中分别列出设定值与测量值。

5 一次受控调频

为检验主机调频是否会影响 Cobalt,我们保留 timesyncd,以当时的校正值为基线,先减去 60ppm,约 30 秒后恢复,再增加 60ppm,约 30 秒后再次恢复,并比较各阶段的时钟推进速率。

下面是记录中的设定值和窗口内平均速率。所有“相对 RAW”均表示相对本机 RAW,并非相对外部标准时钟:

阶段 adjtimex.freq 换算值 ppm Linux MONOTONIC 相对 RAW ppm Cobalt MONOTONIC 相对 RAW ppm
初始基线 39.282 42.765 42.839
基线减 60ppm -20.718 -17.195 -17.195
第一次恢复 39.282 42.620 42.620
基线加 60ppm 99.282 102.418 102.423
第二次恢复 39.282 42.255 42.255

NTP 调频前后 Linux 与 Cobalt MONOTONIC 相对 RAW 的窗口平均速率

图中每个点表示对应窗口的平均值,连线只表示试验顺序,不表示写入瞬间或窗口间的连续轨迹。

实测速率与 freq 参数相差几 ppm,还需要考虑内核的相位收敛和括号采样误差,不能仅凭这项差异判断写入是否成功。

表中最值得关注的是两列实测速率:Linux MONOTONIC 与 Cobalt MONOTONIC 相对 RAW 一起变速,变化量与注入方向、幅度一致。这台设备上的 Cobalt MONOTONIC,确实承接了主机调频。

各窗口的采样范围与筛选方法见附录“实验采样与统计方法”。

实验给出了时钟之间的关系,但还没有单独解释开头波形为什么呈现“突降后回升”。接下来先回答其中的基础问题:硬件计数器照常运行,软件时钟为什么还能变速?

三 单调时钟为什么还能变速

1 读取时间与触发唤醒是两件事

clocksource 提供持续运行的硬件计数,内核将它换算成当前时间;clockevent 则负责在指定时刻触发中断。读到了什么时间,与线程什么时候真正开始执行,需要分别分析。Linux 时钟源和时钟事件文档

clocksource 读取时间与 clockevent 触发事件的分工

2 硬件计数如何变成纳秒

为避免在高频路径上做复杂运算,Linux 通常用乘法和移位完成计数换算。忽略回绕等细节,可以把读取过程简化为:

delta_cycles = 当前硬件计数 - 上次记录的硬件计数
delta_ns     = delta_cycles × mult / 2^shift
当前时间      = 时间基值 + delta_ns

硬件计数器是底层输入,mult、shift 和时间基值是软件换算参数。即使硬件计数器一直正常运行,软件也可以通过调整换算关系,让“一个软件秒”相对原始计数略快一些或者略慢一些。

NTP 校时通常不需要改变硬件振荡器,更不是让 CPU 主频跟着变化。本文讨论的“调频”,主要是系统时间尺度的频率校正。

3 日历时间可以跳变,也可以渐进校正

CLOCK_REALTIME 表示与 Unix Epoch 对应的日历时间。直接把它设置到新的位置称为 step;调整走时速率、让误差逐步消除称为 slew。采用哪一种方式,取决于时间同步程序、配置和当时的误差。

因此,用两个 REALTIME 时间戳相减计算耗时,可能把校时动作也算进去。这正是周期任务通常选择 MONOTONIC 的原因。Linux clock_gettime 手册

4 CLOCK_MONOTONIC

CLOCK_MONOTONIC 不提供日历日期,也不允许应用直接把它设置成任意值。对于正常的用户态时钟接口,它保证时间不倒退,但两次连续读取可以相同,并不保证每次严格递增。

当 REALTIME 被向前或者向后跳变设置时,内核会处理两者之间的偏移关系,避免把这次日历时间跳变直接传给 MONOTONIC。不过,NTP 对系统走时速率的渐进校正仍然会影响它。

换句话说,MONOTONIC 解决了时间顺序的问题,却没有承诺“与未经校正的硬件计数保持固定比例”。Linux 手册对“不会受不连续校时影响”和“会受渐进频率调整影响”分别作了说明。Linux clock_gettime 手册

5 CLOCK_MONOTONIC_RAW

CLOCK_MONOTONIC_RAW 提供不经过 NTP 速率校正的原始单调时间。Linux timekeeper 中有分别维护的 tkr_mono 和 tkr_raw 读取基础,NTP 校正会作用于前者的换算与累积过程,RAW 保留未经过这种校正的时间尺度。

从 Linux 6.1 的实现可以看到,频率调整会更新 tkr_mono.mult,并补偿基值以维持时间连续;这与“突然给当前时间加上一个偏移”是两回事。Linux 6.1 timekeeping.c

RAW、MONOTONIC 与 REALTIME 的时间换算关系

这里画的是正常运行期间的简化关系,未展开休眠、闰秒等处理。RAW 也需要将计数换算为时间,并非直接返回硬件寄存器;它仍然受硬件自身的频率误差影响,不能理解成天然准确的标准时间。Linux timekeeping 接口说明

6 每周期只差 30ns,为什么还要关注

回到前面的 60ppm 调频实验。相对 RAW,若一个时钟快了 60ppm,则 RAW 经过一秒,这个时钟大约会多走 60µs。

1 秒 × 60 / 1,000,000 = 60µs

假设这段时间内两者保持固定频差,500µs 的 MONOTONIC 间隔对应的 RAW 时长为:

T_raw = 500000ns / (1 + 60 × 10^-6)
      ≈ 499970ns

每个周期只差大约 30ns。对普通业务计时可能很小,对需要跟随另一个硬件时钟的周期同步,这就是一个持续存在的频差输入。

若完全不作其他校正,经过一秒便可累积到约 60µs。实际 DC 伺服会尝试补偿,因此不能拿这条简单计算直接预测闭环系统的相位峰值;它说明的是误差怎样进入系统。同步器多长时间更新一次频率修正,会影响这项频差怎样累积成相位变化;第五节将结合具体参数作量级估算。

四 Cobalt 为什么也会承接 Linux 校时

Linux MONOTONIC 的变速机制解释了实验中的一列数据。另一列来自 Cobalt:它为什么几乎同步变化?这要沿应用实际调用的路径继续看。

1 同一个函数名可能走不同路径

在 Xenomai 应用中,看到 clock_gettime() 并不能马上断定它调用的是普通 libc 服务。通过 xeno-config 提供的编译链接参数,POSIX 接口可以被包装到 Cobalt 实现。

睡眠接口 clock_nanosleep() 也需要核对最终的符号绑定,才能确定周期等待实际使用的服务。Xenomai POSIX 应用移植说明

2 本次 Dovetail 路径

设备 A 使用的是 Dovetail。查看对应实现后,关键关系很直接:

/* Dovetail 路径中的两个返回表达式。 */
return ktime_get_mono_fast_ns();
return ktime_get_real_fast_ns();

第一个来自 pipeline_read_cycle_counter(),第二个来自 pipeline_read_wallclock()。这里前一个函数虽然带着 cycle_counter 名字,返回的已经是 Linux 单调时间的纳秒值;不能仅凭名字把它当成未经校正的硬件计数。Xenomai 3.3.3 Dovetail 时钟适配

因此,这条路径上的 Cobalt 核心单调时间会承接 Linux 的速率校正,核心日历时间则读取 Linux 的 REALTIME 时间尺度。实时核仍然负责实时任务的调度和定时器处理,但时间尺度没有因此与 Linux 校时隔离。

需要说明的是,这些 fast accessor 有特定的内核上下文语义;这里仅用它们确认时间来源,不据此推导普通 POSIX 时钟的全部性质。

3 为什么旧 I-pipe 经验不能直接套用

在旧的 legacy TSC 路径中,Cobalt 读取硬件高分辨率计数,再使用自己的换算关系得到单调时间。该路径中的 MONOTONIC 和 MONOTONIC_RAW 可以走相同实现。

REALTIME 则在这条时间线上叠加 Cobalt 的墙上时间偏移。历史上还提供 CLOCK_HOST_REALTIME,用于取得 Linux 看到的日历时间。这解释了为什么早期 Xenomai 使用经验中,经常会说“Cobalt 的 MONOTONIC 不受 Linux NTP 调频影响”。

但这个认识有其实现背景。Xenomai 3.2.4 的用户库源码就同时包含 legacy TSC 和 vDSO 两条路径,不能只根据“3.x”版本号判断实际行为。Xenomai 3.2.4 用户态时钟源码

4 用户态读取还要再看一层

回到本次实验,采样来自用户态,仅确认核心时钟的来源还不够。

Xenomai 3.3.3 的用户库对常用时钟使用 Linux vDSO:MONOTONIC 与 RAW 将各自的时钟 ID 传给 vDSO;REALTIME 与 HOST_REALTIME 则读取 vDSO 的 REALTIME。Xenomai 3.3.3 用户态时钟实现

vDSO 是映射到进程中的用户态代码,结合内核发布的时间数据和硬件计数完成读取,通常不必陷入系统调用。用户态直接读到时间,不代表这个时间与 Linux timekeeper 无关。NTP 更新了换算数据,后续 vDSO 读取照样会使用更新后的数据。

vDSO 使用内核发布的时间数据与硬件计数完成用户态读时

图中展示支持的快速读取路径,具体能力取决于架构和时钟源。Linux vDSO 手册

这条路径解释了实验中的 Cobalt MONOTONIC 与 RAW 数据来源。不过,成功读到时间值还不足以证明没有后备路径或域切换;用于实时循环前,仍需确认实际架构、clocksource、库版本及运行状态。

把本次涉及的读取关系放在一起就容易理解了:

读取路径 MONOTONIC REALTIME RAW
普通 Linux 用户态 不跟随日历时间 step,受 NTP 速率校正影响 日历时间,可 step,也可渐进校正 不经 NTP 速率校正
旧 Cobalt legacy TSC 用户态路径 独立计数换算 单调时间叠加 Cobalt 墙上偏移 与该路径的 MONOTONIC 等价
本次 Cobalt 用户态 vDSO 路径 Linux MONOTONIC 时间尺度 Linux REALTIME 时间尺度 Linux RAW 时间尺度

表中最后一行指用户态读取。内核系统调用、RTDM 时钟接口和睡眠接口还要沿各自源码继续确认,尤其不能把 RAW 的读取结果直接推广到睡眠实现。

例如,该版本内核的 __cobalt_clock_gettime() 会把 MONOTONIC 与 RAW 合并到核心单调时间读取分支。这与用户库直接把 RAW ID 交给 Linux vDSO 的路径不同。因此,分析采样结果时必须确认实际调用路径,不能把用户态 vDSO 读取与直接调用 Cobalt 内核接口视为等价。Cobalt 内核时钟读取实现

五 频差怎样进入 DC 同步环

时钟实验和源码说明了主机调频能够到达 Cobalt MONOTONIC。接下来还要看,周期算法怎样使用这条时间线,以及出现频差后怎样响应。

1 应用时间、参考样本与等待目标

application time 是应用交给主站的时间坐标,用于 DC 初始化等时间关系的建立;在本例用户态同步器中,它还提供一个按名义周期推进的相位比较基准。它与实际等待目标各自推进。先不限定等待目标使用哪个时钟域,两者的关系可以写成:

名义应用时间 A[k+1] = A[k] + 500000ns
实际等待目标 W[k+1] = W[k] + 500000ns + a[k]

其中 a[k] 是同步器输出的周期修正。准备参考报文时,还会把当前时刻相对本周期等待目标的已过时间加到名义应用时间上,并将这个应用时间保存下来;等参考响应返回,再用保存值与对应参考时间比较。这样可以避免拿当前周期的应用时间去比较上一周期的参考响应。接口取得的参考 DC 时间已按主站约定扣除传输延迟,并非在主机读取接口那一刻的“DC 当前值”。

相位计算可概括为:

d[k] = low32(A_sample[k]) - D_ref32[k]    // 按 32 位模数相减
e[k] = wrap_T(d[k])                      // 按周期 T 归一相位

源码会先按有符号差处理 32 位回绕,后续用相邻有效差值推进相位,不能简单地把无符号大数直接对周期取模。500µs 周期下,相位区间为 [-250µs, 250µs)。这里定义的正方向是应用时间相位领先参考 DC,与开头调试通道的正方向不能直接画等号。

例如,应用相位比参考 DC 领先 100ns,给下一周期增加 1ns 等待时间,应用的名义时间仍只增加 500000ns,而参考 DC 在实际多等待的 1ns 内继续前进,正误差便向减小的方向变化。负误差则通过缩短等待间隔来修正。这就是本例“应用跟随 DC”的方向。

修正落在应用的下一次等待目标上,不把参考 DC 反向拉向主机 NTP 时间。运行期间,参考 DC 同步报文读取参考时钟并将其传播给其他从站;它与把主机时间写入参考时钟的操作不同。初始化时建立 DC 偏移、配置 SYNC 起点,也应与运行期间的周期跟踪分开理解。

当等待目标使用 MONOTONIC 时,主机调频会改变 500000ns + a[k] 对应的实际时长,进而改变参考采样时刻,给同步环增加一项频差输入。这就是前面测得的时钟变速与周期同步之间的联系。

2 约 30µs 的变化可能怎样形成

前面算出,60ppm 在单个 500µs 周期内只产生约 30ns 的差异。要理解它怎样积累到几十微秒,需要考虑同步器更新频率估计所用的时间窗口。

下面按所核对同步器的默认配置——1024 个有效样本,以及 60ppm 的频率变化作估算。原截图那次运行的完整配置和逐次调频边界没有全部留存,因此这项计算用于说明可能的机制与量级,不能视为对截图的逐点还原。

以每周期取得一个有效参考样本计算,窗口时长为:

T_window = 1024 × 500µs = 0.512s

lib/dc_sync_user.c 中的控制有两部分:每个有效周期都根据当前相位输出微调;频率修正则在窗口结束时,利用相邻相位差的平均变化更新。省略取整、限幅和异常样本处理,可写成:

每满 1024 个有效样本:
    频率修正 += 窗口内差值增量的平均值
              + sign(窗口平均相位) × 1ns

每个有效周期:
    周期修正 = 频率修正 + sign(当前相位) × 1ns

这里“频率修正”的单位是每周期增加或减少的纳秒数。它需要用一段时间的数据估计频差,无法在频率参数变化的同一时刻就完成全部补偿。主机时间尺度突然变化后,在新频率修正逐步生效之前,剩余频差会继续累积成相位偏移。

为估算量级,暂不计窗口内的反馈补偿,假设新增的 60ppm 频差持续一个完整窗口:

每周期的时间差 ≈ 500000ns × 60 × 10^-6 = 30ns

一个滤波窗口内的相位累积量
    ≈ 30ns × 1024
    = 30720ns
    = 30.72µs

在上述假设下,单周期约 30ns 的差异,经过约半秒可累积到约 30µs,与截图中的变化处于同一量级。这提供了一种理解快速相位偏移的思路:软件时间不必发生跳变,持续频差也能在短时间内形成可见偏移。不过,仅凭量级相近,还不能确认截图中的突降就是这一过程。

随后,窗口更新使频率修正逐渐抵消新的频差,相位不再继续同方向积累,但已经形成的相位偏移还要拉回来。单看每周期 sign(phase) × 1ns 这一直接相位项,在 2kHz 下的修正能力约为:

1ns/周期 × 2000 周期/秒 = 2µs/秒
30µs ÷ 2µs/秒 ≈ 15秒

这提供了“先较快偏离,再缓慢回归”的一种可能机制。完整控制器的频率项中也含有窗口平均相位的符号修正,所以不能把 15 秒当成固定恢复时间;实际斜率还受窗口内阶跃位置、整数取整、原有频差和采样连续性影响。

滤波长度可以配置,有效样本不连续也会延长窗口,因此上述数值不是固定响应。这里揭示的是一条影响路径:主机时间尺度变速,会给周期同步环增加频差输入,而同步器的响应速度决定了相位如何随之变化。

六 结语

这次排查最容易让人走偏的地方,是把“单调”理解成“走时速率固定”,又把过去在 I-pipe 上成立的经验,直接带到了 Dovetail 系统。

对设备 A 而言,源码和受控测量相互对应:Linux NTP 校正改变了 MONOTONIC 相对 RAW 的速率,Cobalt 使用的 MONOTONIC 也随之变化。周期由谁调度、周期时间由谁计量,是需要分别确认的两件事。

本文确认的是主机调频能够进入周期时间基准这条路径。同步器的窗口估算提供了理解几十微秒相位变化的一种可能机制,但量级吻合不等于确认了原图的具体成因。截图各拐点与调频事件的对应关系,仍需更完整的记录。

当应用还要跟随外部 DC、PHC 或其他独立时钟时,时间域之间的关系就是同步算法的一部分。读时间、计算剩余量、安排睡眠、判断迟到,都要知道自己站在哪条时间线上。这样再看到相位突变或者 deadline miss,才能继续沿着具体证据往下定位。

附录:如何检查自己的系统

遇到类似问题,可以从下面几项只读检查开始,先确认时间来源和频差,再决定是否需要受控实验。

1 确认实际版本和时钟源

uname -a
xeno-config --version
cat /sys/devices/system/clocksource/clocksource0/current_clocksource
cat /proc/xenomai/clock/coreclk

记录内核、Xenomai 用户库以及 I-pipe 或 Dovetail 路径。版本字符串相同也可能包含不同补丁,分析关键行为时仍应核对实际构建源码。

2 先作只读频差测量

下面这段 Python 仅用于普通 Linux 诊断,比较本机 MONOTONIC 与 RAW,不调整任何时钟,也不是实时周期程序:

import time

def sample():
    before = time.clock_gettime_ns(time.CLOCK_MONOTONIC)
    raw = time.clock_gettime_ns(time.CLOCK_MONOTONIC_RAW)
    after = time.clock_gettime_ns(time.CLOCK_MONOTONIC)
    return (before + after) // 2, raw, after - before

m0, r0, width0 = sample()
time.sleep(10)
m1, r1, width1 = sample()

ppm = ((m1 - m0) / (r1 - r0) - 1) * 1_000_000
print(f"MONOTONIC relative to RAW: {ppm:.6f} ppm")
print(f"sampling bracket: {width0} ns, {width1} ns")

这里用的是实际 RAW 增量,因此不要求 sleep(10) 恰好持续十秒。单次测量也不足以描述动态调频过程,应连续记录并结合采样括号宽度分析。

接下来再从 Cobalt 程序中取得对应时间戳。不能拿 Python 的 Linux MONOTONIC 结果,直接代替 Cobalt 实际路径的验证。

3 只读查询 adjtimex

在普通 Linux 诊断进程中,可以将 modes 置零查询当前状态:

#include <stdio.h>
#include <sys/timex.h>

int main(void)
{
    struct timex tx = {0};
    int state = adjtimex(&tx);

    if (state < 0) {
        perror("adjtimex");
        return 1;
    }
    printf("state=%d freq=%.9fppm offset=%ld status=0x%x\n",
            state, tx.freq / 65536.0, tx.offset,
            (unsigned int)tx.status);
    return 0;
}

该查询不写入时间参数。若要作受控调频实验,应使用独立测试环境,保存基线、明确每次写入边界和恢复结果,同时识别时间同步服务是否在试验中进行了其他写入。只读排查阶段不需要先停掉 NTP 服务。

附录:实验采样与统计方法

这组数据采集按写入后的读回时间划分窗口,记录如下:

阶段 起止时间 用于速率计算的有效配对样本数 首末有效样本的 RAW 跨度 s 有效样本最大括号宽度 µs
初始基线 03:23:40.684—03:23:45.685 40 3.905 9.334
基线减 60ppm 03:23:45.685—03:24:15.689 286 28.941 19.250
第一次恢复 03:24:15.689—03:24:45.693 285 28.734 10.792
基线加 60ppm 03:24:45.693—03:25:15.697 289 28.943 16.919
第二次恢复 03:25:15.697—03:25:45.701 289 28.937 10.501

统计时,时钟以约 10Hz 采样,用 Cobalt 前后读数的中点选择窗口;各窗口首尾分别去掉 0.5 秒,再剔除括号宽度超过 20µs 的样本。最后以首末有效样本的增量计算窗口平均速率,结果与上表一致。有效样本数不表示把所有相邻样本的 ppm 再作算术平均。

实验中,两次恢复均读回原基线,各测量窗口内未观察到额外的频率参数变化。内核原有的相位渐进校正仍可能继续,因此 freq 设定值与实测速率不必完全相同。

posted @ 2026-11-03 23:44  沐多  阅读(30)  评论(0)    收藏  举报