Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

3.6 PHC操作与时钟调整:与硬件时钟对话

从用户态到内核态

LinuxPTP运行在用户态,但需要操作内核的PTP硬件时钟(PHC)。

这是如何实现的?


Linux PHC子系统

PHC是什么

PTP Hardware Clock(PHC)是Linux内核提供的子系统:
- 抽象各种硬件时钟设备
- 提供统一的用户态接口
- 支持多种操作:读取、设置、调整

设备文件:
/dev/ptp0   # 第一个PHC设备
/dev/ptp1   # 第二个PHC设备
...

查看系统中的PHC:
$ ls -l /dev/ptp*
crw-rw---- 1 root root 248, 0 Apr  1 10:00 /dev/ptp0

PHC的内核接口

/* Linux内核头文件 <linux/ptp_clock.h> */

struct ptp_clock_caps {
    int max_adj;        /* 最大频率调整(ppb) */
    int n_alarm;        /* 告警数量 */
    int n_ext_ts;       /* 外部时间戳数量 */
    int n_per_out;      /* 周期性输出数量 */
    int pps;            /* 是否支持PPS */
    int n_pins;         /* 引脚数量 */
    int cross_timestamping; /* 是否支持交叉时间戳 */
    int adjust_phase;   /* 是否支持相位调整 */
};

/* IOCTL命令 */
#define PTP_CLOCK_GETCAPS  _IOR('P', 1, struct ptp_clock_caps)
#define PTP_PIN_SETFUNC2   _IOW('P', 20, struct ptp_pin_desc)

PHC操作函数

phc_open:打开PHC设备

/* phc.c, 第42-67行 */

clockid_t phc_open(const char *phc)
{
    clockid_t clkid;
    struct timespec ts;
    struct timex tx;
    int fd;

    memset(&tx, 0, sizeof(tx));

    /* 步骤1:打开设备文件 */
    fd = open(phc, O_RDWR);
    if (fd < 0)
        return CLOCK_INVALID;

    /* 步骤2:将文件描述符转换为clockid_t */
    clkid = FD_TO_CLOCKID(fd);
    
    /* 步骤3:验证clockid是否有效 */
    if (clock_gettime(clkid, &ts)) {
        close(fd);
        return CLOCK_INVALID;
    }
    
    /* 步骤4:验证是否可以调整时钟 */
    if (clock_adjtime(clkid, &tx)) {
        close(fd);
        return CLOCK_INVALID;
    }

    return clkid;
}

FD_TO_CLOCKID宏

/* Linux内核定义 */

#define CLOCKFD 3
#define FD_TO_CLOCKID(fd) ((~(clockid_t) (fd) << 3) | CLOCKFD)
#define CLOCKID_TO_FD(clk) ((unsigned long) ~((clk) >> 3))

工作原理

Linux时钟API使用clockid_t标识时钟:
- CLOCK_REALTIME:系统实时时钟
- CLOCK_MONOTONIC:单调递增时钟
- CLOCKFD:动态时钟(通过文件描述符)

PTP设备文件描述符 → clockid_t转换:
1. 打开/dev/ptp0 → 获得fd(如fd=10)
2. FD_TO_CLOCKID(10) → clockid_t
3. 用这个clockid调用clock_gettime等函数

这允许用户态程序像操作普通时钟一样操作PHC!

phc_close:关闭PHC设备

/* phc.c, 第69-75行 */

void phc_close(clockid_t clkid)
{
    if (clkid == CLOCK_INVALID)
        return;

    close(CLOCKID_TO_FD(clkid));
}

phc_max_adj:获取最大频率调整

/* phc.c, 第87-101行 */

int phc_max_adj(clockid_t clkid)
{
    int max;
    struct ptp_clock_caps caps;

    /* 通过ioctl获取能力 */
    if (phc_get_caps(clkid, &caps))
        return 0;

    max = caps.max_adj;

    /* 32位平台的特殊处理 */
    if (BITS_PER_LONG == 32 && max > MAX_PPB_32)
        max = MAX_PPB_32;

    return max;
}

static int phc_get_caps(clockid_t clkid, struct ptp_clock_caps *caps)
{
    int fd = CLOCKID_TO_FD(clkid), err;

    err = ioctl(fd, PTP_CLOCK_GETCAPS, caps);
    if (err)
        perror("PTP_CLOCK_GETCAPS");
    return err;
}

32位平台限制

/* phc.c, 第37-38行 */

#define BITS_PER_LONG   (sizeof(long)*8)
#define MAX_PPB_32      32767999  /* (2^31 - 1) / 65.536 */
为什么需要这个限制?

struct timex {
    long freq;    /* 频率调整,单位:ppb × 65.536 */
    ...
};

32位平台:long是32位,范围±2^31
最大值:(2^31 - 1) / 65.536 ≈ 32,767,999 ppb

如果PHC硬件支持更大的调整范围,需要限制到这个值。

phc_has_pps:检查PPS支持

/* phc.c, 第122-129行 */

int phc_has_pps(clockid_t clkid)
{
    struct ptp_clock_caps caps;

    if (phc_get_caps(clkid, &caps))
        return 0;
    
    return caps.pps;
}

PPS(Pulse Per Second)

PPS是一种硬件信号:
- 每秒产生一个脉冲
- 精度可达纳秒级
- 用于精确的时间同步

用途:
- 外部时间源(GPS)输入
- 时间戳触发
- 频率校准

LinuxPTP可以利用PPS:
- 如果PHC支持PPS
- 可以从外部源获取精确时间
- 提高同步精度

phc_has_writephase:检查相位调整支持

/* phc.c, 第131-139行 */

int phc_has_writephase(clockid_t clkid)
{
    struct ptp_clock_caps caps;

    if (phc_get_caps(clkid, &caps)) {
        return 0;
    }
    
    return caps.adjust_phase;
}

相位调整能力

传统频率调整:
- 通过调整振荡器频率
- 相位变化是累积的
- 适合长期稳定

相位调整(硬件写入相位):
- 直接修改时钟相位
- 不改变频率
- 立即生效,无累积误差

支持adjust_phase的硬件:
- 可以更精确地调整相位
- 实现"一步"时钟
- 适合高精度场景

时钟调整函数

clockadj_set_freq:设置频率

/* clockadj.c, 第51-70行 */

int clockadj_set_freq(clockid_t clkid, double freq)
{
    struct timex tx;
    memset(&tx, 0, sizeof(tx));

    /* 系统时钟的特殊处理 */
    if (clkid == CLOCK_REALTIME && realtime_nominal_tick) {
        tx.modes |= ADJ_TICK;
        tx.tick = round(freq / 1e3 / realtime_hz) + realtime_nominal_tick;
        freq -= 1e3 * realtime_hz * (tx.tick - realtime_nominal_tick);
    }

    /* 设置频率 */
    tx.modes |= ADJ_FREQUENCY;
    tx.freq = (long) (freq * 65.536);
    
    if (clock_adjtime(clkid, &tx) < 0) {
        pr_err("failed to adjust the clock: %m");
        return -1;
    }
    return 0;
}

频率调整的单位转换

输入:freq(ppb,十亿分之一)
内核:tx.freq(单位:ppb × 65.536)

转换公式:
tx.freq = freq × 65.536

为什么是65.536?

历史原因:NTP使用"SHIFT_HZ = 16"
freq字段是2^16缩放的ppm(百万分之一)
1 ppm = 1000 ppb
所以:tx.freq = freq_ppb × 2^16 / 1000
               = freq_ppb × 65.536

示例:
freq = 100 ppb
tx.freq = 100 × 65.536 = 6554

系统时钟的特殊处理

if (clkid == CLOCK_REALTIME && realtime_nominal_tick) {
    tx.modes |= ADJ_TICK;
    tx.tick = round(freq / 1e3 / realtime_hz) + realtime_nominal_tick;
    freq -= 1e3 * realtime_hz * (tx.tick - realtime_nominal_tick);
}
为什么系统时钟需要特殊处理?

系统时钟(CLOCK_REALTIME):
- 基于内核的tick
- tick长度可以调整
- 允许更大的频率调整范围

方法:
1. 调整tick长度(粗调)
2. 调整freq字段(精调)
3. 两者结合,扩大调整范围

示例:
realtime_hz = 1000(USER_HZ)
realtime_nominal_tick = 1000(微秒)

freq = 100000 ppb(100 ppm)

粗调:
tx.tick = 100000 / 1e3 / 1000 + 1000 = 1100微秒
(tick增加100微秒,相当于+100 ppm)

精调:
freq -= 1e3 × 1000 × (1100 - 1000) = 0
(剩余部分由freq字段处理)

效果:
通过tick调整了100 ppm
freq字段只需要调整0 ppb

clockadj_get_freq:获取当前频率

/* clockadj.c, 第72-86行 */

double clockadj_get_freq(clockid_t clkid)
{
    double f = 0.0;
    struct timex tx;
    memset(&tx, 0, sizeof(tx));
    
    if (clock_adjtime(clkid, &tx) < 0) {
        pr_err("failed to read out the clock frequency adjustment: %m");
        exit(1);
    } else {
        f = tx.freq / 65.536;
        if (clkid == CLOCK_REALTIME && realtime_nominal_tick && tx.tick)
            f += 1e3 * realtime_hz * (tx.tick - realtime_nominal_tick);
    }
    return f;
}

逆转换

读取时的单位转换:
freq_ppb = tx.freq / 65.536

系统时钟需要加上tick调整:
freq_ppb += 1e3 × hz × (tick - nominal_tick)

clockadj_set_phase:设置相位偏移

/* clockadj.c, 第88-100行 */

int clockadj_set_phase(clockid_t clkid, long offset)
{
    struct timex tx;
    memset(&tx, 0, sizeof(tx));

    tx.modes = ADJ_OFFSET | ADJ_NANO;
    tx.offset = offset;
    
    if (clock_adjtime(clkid, &tx) < 0) {
        pr_err("failed to set the clock offset: %m");
        return -1;
    }
    return 0;
}

ADJ_OFFSET的作用

ADJ_OFFSET模式:
- 设置相位偏移
- 时钟逐渐调整,不是立即跳变
- 使用内核的PLL调整

参数:
tx.offset = offset(纳秒)
ADJ_NANO表示offset单位是纳秒

工作原理:
- 时钟偏移offset纳秒
- 内核逐渐调整,消除偏移
- 调整速度由time_constant控制

适用场景:
- 小偏差(微秒级)
- 平滑调整,不影响应用

clockadj_step:时钟跳变

/* clockadj.c, 第102-127行 */

int clockadj_step(clockid_t clkid, int64_t step)
{
    struct timex tx;
    int sign = 1;
    if (step < 0) {
        sign = -1;
        step *= -1;
    }
    memset(&tx, 0, sizeof(tx));
    
    tx.modes = ADJ_SETOFFSET | ADJ_NANO;
    tx.time.tv_sec  = sign * (step / NS_PER_SEC);
    tx.time.tv_usec = sign * (step % NS_PER_SEC);
    
    /* 确保tv_usec非负 */
    if (tx.time.tv_usec < 0) {
        tx.time.tv_sec  -= 1;
        tx.time.tv_usec += 1000000000;
    }
    
    if (clock_adjtime(clkid, &tx) < 0) {
        pr_err("failed to step clock: %m");
        return -1;
    }
    return 0;
}

时钟跳变的含义

ADJ_SETOFFSET模式:
- 立即修改时钟值
- 时间会"跳"到新值
- 可能导致应用问题

参数:
step = +1000000000:时钟向前跳1秒
step = -1000000000:时钟向后跳1秒

处理负数step:
1. step = -1500000000(-1.5秒)
2. sign = -1
3. step = 1500000000
4. tx.time.tv_sec = -1
5. tx.time.tv_usec = -500000000
6. 调整:tv_sec -= 1, tv_usec += 1000000000
   → tv_sec = -2, tv_usec = 500000000

注意:
tx.time是timeval结构:
struct timeval {
    time_t tv_sec;    /* 秒 */
    suseconds_t tv_usec; /* 微秒 */
};

虽然使用了ADJ_NANO标志,但实际传递的是微秒。
内核会根据ADJ_NANO标志将其视为纳秒。

set_phase vs step

set_phase:
- 渐进调整
- 用PLL逐渐消除偏差
- 无时间跳变
- 适合小偏差

step:
- 立即跳变
- 时间直接修改
- 可能导致应用问题
- 适合大偏差

选择依据:
- |offset| < step_threshold → set_phase
- |offset| > step_threshold → step

clock_adjtime系统调用

函数原型

#include <sys/timex.h>

int clock_adjtime(clockid_t clkid, struct timex *tx);

这是Linux特有的系统调用

标准POSIX:
clock_gettime:读取时间
clock_settime:设置时间

Linux扩展:
clock_adjtime:高级时钟调整
- 设置频率
- 设置相位
- 时钟跳变
- 读取状态

这允许用户态程序精确控制时钟!

struct timex结构

/* <sys/timex.h> */

struct timex {
    int  modes;           /* 模式选择 */
    long offset;         /* 相位偏移(纳秒) */
    long freq;           /* 频率偏移(scaled ppm) */
    long maxerror;        /* 最大误差 */
    long esterror;        /* 估计误差 */
    int  status;          /* 时钟状态 */
    long constant;        /* PLL时间常数 */
    long precision;       /* 时钟精度 */
    long tolerance;       /* 时钟容差(最大频率偏差) */
    struct timeval time;  /* 当前时间 */
    long tick;            /* 微秒/时钟tick */
    long ppsfreq;         /* PPS频率 */
    long jitter;          /* PPS抖动 */
    int  shift;           /* PPS间隔时长 */
    long stabil;          /* PPS稳定性 */
    long jitcnt;          /* PPS抖动超限计数 */
    long calcnt;          /* PPS校准间隔 */
    long errcnt;          /* PPS校准错误 */
    long stbcnt;          /* PPS稳定性超限 */
    int  tai;             /* TAI偏移 */
};

modes字段详解

/* modes的位定义 */

#define ADJ_OFFSET        0x0001  /* 设置相位偏移 */
#define ADJ_FREQUENCY     0x0002  /* 设置频率 */
#define ADJ_MAXERROR      0x0004  /* 设置最大误差 */
#define ADJ_ESTERROR      0x0008  /* 设置估计误差 */
#define ADJ_STATUS        0x0010  /* 设置状态 */
#define ADJ_TIMECONST     0x0020  /* 设置时间常数 */
#define ADJ_TAI           0x0080  /* 设置TAI偏移 */
#define ADJ_SETOFFSET     0x0100  /* 时钟跳变 */
#define ADJ_MICRO         0x1000  /* 微秒分辨率 */
#define ADJ_NANO          0x2000  /* 纳秒分辨率 */
#define ADJ_TICK          0x4000  /* 设置tick长度 */

常用模式组合

/* 设置频率 */
tx.modes = ADJ_FREQUENCY;

/* 设置相位(纳秒) */
tx.modes = ADJ_OFFSET | ADJ_NANO;

/* 时钟跳变(纳秒) */
tx.modes = ADJ_SETOFFSET | ADJ_NANO;

/* 读取当前状态 */
tx.modes = 0;  /* 不修改任何参数,只读取 */

PHC与系统时钟同步

phc2sys工具

LinuxPTP提供了phc2sys工具,用于PHC与系统时钟之间的同步。

使用场景:
1. PTP从时钟:PHC同步到网络主时钟
2. 系统时钟:需要从PHC获取时间
3. phc2sys:将PHC时间传递给系统时钟

工作原理:
1. 读取PHC时间
2. 读取系统时钟时间
3. 计算偏差
4. 调整系统时钟

phc2sys配置示例

# 将PHC(/dev/ptp0)同步到系统时钟
phc2sys -s /dev/ptp0 -c CLOCK_REALTIME -w

# 参数说明:
# -s /dev/ptp0:源时钟(PHC)
# -c CLOCK_REALTIME:目标时钟(系统时钟)
# -w:等待ptp4l锁定

实际应用示例

在LinuxPTP中使用PHC

/* clock.c中的时钟创建(简化) */

struct clock *clock_create(enum clock_type type, struct config *config,
                           const char *phc_device)
{
    struct clock *c;
    
    /* 分配内存 */
    c = calloc(1, sizeof(*c));
    
    /* 打开PHC设备 */
    if (phc_device) {
        c->clkid = phc_open(phc_device);
    } else {
        /* 自动检测PHC */
        c->clkid = phc_open("/dev/ptp0");
    }
    
    if (c->clkid == CLOCK_INVALID) {
        /* 使用系统时钟 */
        c->clkid = CLOCK_REALTIME;
    }
    
    /* 获取最大频率调整 */
    c->max_freq = phc_max_adj(c->clkid);
    
    /* 创建伺服 */
    c->servo = servo_create(config, type, fadj, c->max_freq, sw_ts);
    
    return c;
}

时钟调整流程

/* clock.c中的同步函数(简化) */

enum servo_state clock_synchronize(struct clock *c, tmv_t ingress,
                                   tmv_t origin)
{
    int64_t offset;
    double freq_adj;
    enum servo_state state;
    
    /* 计算偏差 */
    offset = tmv_to_nanoseconds(tmv_sub(ingress, origin));
    
    /* 调用伺服 */
    freq_adj = servo_sample(c->servo, offset, local_ts, 1.0, &state);
    
    /* 应用频率调整 */
    switch (state) {
    case SERVO_JUMP:
        /* 时钟跳变 */
        clockadj_step(c->clkid, -offset);
        break;
    
    case SERVO_LOCKED:
    case SERVO_LOCKED_STABLE:
        /* 频率调整 */
        clockadj_set_freq(c->clkid, freq_adj);
        break;
    
    default:
        break;
    }
    
    return state;
}

调试和诊断

检查PHC设备

# 查看PHC设备
$ ls -l /dev/ptp*

# 查看PHC能力
$ ethtool -T eth0
Time stamping parameters for eth0:
Capabilities:
        hardware-transmit     (SOF_TIMESTAMPING_TX_HARDWARE)
        hardware-receive      (SOF_TIMESTAMPING_RX_HARDWARE)
        hardware-raw-clock    (SOF_TIMESTAMPING_RAW_HARDWARE)
PTP Hardware Clock: 0

读取PHC时间

# 使用phc_ctl工具
$ phc_ctl /dev/ptp0 -- get

# 输出:
phc_ctl[1234.567]: clock time is 1640995200.123456789 or Thu Dec 31 2021 12:00:00 UTC

设置PHC时间

# 设置PHC时间
$ phc_ctl /dev/ptp0 -- set T 1640995200.000000000

# 调整PHC频率
$ phc_ctl /dev/ptp0 -- freq 100.0  # +100 ppb

小结:PHC操作的核心要点

用户态到内核态的桥梁

  • 文件描述符 → clockid_t
  • 标准时钟API操作PHC

关键函数

  • phc_open/close:打开/关闭设备
  • clockadj_set_freq:设置频率
  • clockadj_set_phase:设置相位
  • clockadj_step:时钟跳变

系统调用

  • clock_adjtime:核心系统调用
  • struct timex:丰富的控制参数

两种调整方式

  • 渐进调整(set_phase)
  • 立即跳变(step)

下集预告

PHC操作解决了“如何调整时钟“,但PTP报文如何在网络中传输?

下一节,我们将分析传输层实现——看看LinuxPTP如何处理UDP和以太网传输。

【悬念留给3.7】

PTP可以在不同传输层运行:

  • UDP/IPv4
  • UDP/IPv6
  • 原始以太网(Layer 2)

LinuxPTP使用统一接口抽象这些传输。

如何实现一个接口支持多种传输?

下一节,我们详细解读传输层实现。