PTP协议精讲(4.4):从时钟程序实现——时间的“追随者“

4.4 从时钟程序实现:时间的"追随者"

源码版本说明

本章源码基于 ptp-lite v1.0.0 (2026-04-10)

⚠️ 重要提示

  • 完整、最新的源码请查看 ptp_lite/ 目录
  • 本章代码片段仅用于理解原理,可能不是最新版本
  • 如发现差异,以源码文件为准
  • 源码变更记录请查看 CHANGELOG.md

主要源码文件


从时钟的职责

从时钟需要精确地追随主时钟:

复制代码
从时钟任务:

1. 接收Announce消息
   - 发现主时钟
   - 提取主时钟信息

2. 接收Sync+Follow_Up消息
   - 记录Sync接收时间t2
   - 从Follow_Up获取t1
   - 计算偏差:offset = t2 - t1

3. 发送Delay_Req消息
   - 定期发送
   - 记录发送时间t3

4. 接收Delay_Resp消息
   - 获取t4
   - 计算路径延迟:delay = [(t2-t1) + (t4-t3)] / 2
   - 计算真实偏差:offset = (t2-t1) - delay

5. 调整系统时钟
   - 使用伺服算法
   - 渐进调整或跳变

伺服算法实现

ptp_servo.h

c 复制代码
/**
 * @file ptp_servo.h
 * @brief PI伺服控制器
 */

#ifndef PTP_SERVO_H
#define PTP_SERVO_H

#include <stdint.h>

/* PI控制器参数 */
#define SERVO_KP 0.7
#define SERVO_KI 0.3

/* 步进阈值(纳秒) - 10毫秒 */
#define SERVO_STEP_THRESHOLD 10000000LL

/* 伺服状态 */
typedef enum {
    SERVO_UNLOCKED,
    SERVO_JUMP,
    SERVO_LOCKED,
    SERVO_LOCKED_STABLE
} servo_state_t;

/* PI伺服结构 */
typedef struct {
    double kp;
    double ki;
    double integral;
    double last_freq;
    servo_state_t state;
    int count;
} pi_servo_t;

/* 函数声明 */
void pi_servo_init(pi_servo_t *s);
double pi_servo_sample(pi_servo_t *s, int64_t offset, servo_state_t *state);

#endif /* PTP_SERVO_H */

ptp_servo.c

c 复制代码
/**
 * @file ptp_servo.c
 * @brief PI伺服控制器实现
 */

#include <stdlib.h>
#include <stdint.h>
#include "ptp_servo.h"

void pi_servo_init(pi_servo_t *s)
{
    s->kp = SERVO_KP;
    s->ki = SERVO_KI;
    s->integral = 0.0;
    s->last_freq = 0.0;
    s->state = SERVO_UNLOCKED;
    s->count = 0;
}

double pi_servo_sample(pi_servo_t *s, int64_t offset, servo_state_t *state)
{
    double freq_adj;
    
    /* 状态转换逻辑 */
    if (llabs(offset) > SERVO_STEP_THRESHOLD) {
        /* 大偏差:跳变 */
        s->state = SERVO_JUMP;
        s->integral = 0;
        s->count = 0;
    } else {
        /* 小偏差:渐进调整 */
        s->count++;
        if (s->count > 1) {
            s->state = SERVO_LOCKED;
        }
        if (s->count > 10) {
            s->state = SERVO_LOCKED_STABLE;
        }
    }
    
    /* PI控制 - 只在LOCKED状态使用 */
    if (s->state == SERVO_LOCKED || s->state == SERVO_LOCKED_STABLE) {
        s->integral += offset;
        freq_adj = -s->kp * offset - s->ki * s->integral;
    } else {
        freq_adj = 0;
    }
    
    *state = s->state;
    s->last_freq = freq_adj;
    
    return freq_adj;
}

实现要点

  1. 状态转换

    • 当 |offset| > 10ms 时,进入 JUMP 状态(相位跳变)
    • 当 |offset| < 10ms 且 count > 1 时,进入 LOCKED 状态(频率调整)
    • 当 |offset| < 10ms 且 count > 10 时,进入 LOCKED_STABLE 状态
  2. PI控制器

    • 只在 LOCKED 和 LOCKED_STABLE 状态工作
    • 积分项累积历史偏差,实现精确控制
  3. 为什么选择 10ms 阈值

    • 小于 100ms(传统做法):更快进入频率调整
    • 大于 1ms:避免噪声导致的频繁跳变
    • 10ms 是一个平衡选择

时钟调整实现

从时钟需要根据伺服状态调整系统时钟:

adjust_clock 函数

c 复制代码
static void adjust_clock(int64_t offset_ns, double freq_ppb, servo_state_t state)
{
    struct timex tx;
    int ret;
    struct timespec ts;
    
    switch (state) {
    case SERVO_JUMP:
        /* 使用clock_settime直接设置时间(更可靠) */
        clock_gettime(CLOCK_REALTIME, &ts);
        
        if (offset_ns < 0) {
            /* 从时钟落后,需要向前调整 */
            ts.tv_sec += (-offset_ns) / 1000000000LL;
            ts.tv_nsec += (-offset_ns) % 1000000000LL;
            if (ts.tv_nsec >= 1000000000LL) {
                ts.tv_sec++;
                ts.tv_nsec -= 1000000000LL;
            }
        } else {
            /* 从时钟超前,需要向后调整 */
            ts.tv_sec -= offset_ns / 1000000000LL;
            ts.tv_nsec -= offset_ns % 1000000000LL;
            if (ts.tv_nsec < 0) {
                ts.tv_sec--;
                ts.tv_nsec += 1000000000LL;
            }
        }
        
        ret = clock_settime(CLOCK_REALTIME, &ts);
        if (ret < 0) {
            perror("clock_settime failed");
            printf("Failed to adjust clock by %ld ns\n", offset_ns);
        } else {
            printf("CLOCK JUMP: %ld ns (new time: %ld.%09ld)\n", 
                   offset_ns, ts.tv_sec, ts.tv_nsec);
        }
        break;
        
    case SERVO_LOCKED:
    case SERVO_LOCKED_STABLE:
        memset(&tx, 0, sizeof(tx));
        tx.modes = ADJ_FREQUENCY;
        tx.freq = (long)(freq_ppb * 65.536);
        ret = clock_adjtime(CLOCK_REALTIME, &tx);
        if (ret < 0) {
            perror("clock_adjtime FREQ failed");
        } else {
            printf("FREQ ADJ: %.2f ppb\n", freq_ppb);
        }
        break;
        
    default:
        break;
    }
}

实现要点

  1. 相位跳变(SERVO_JUMP)

    • 使用 clock_settime 直接设置时间
    • clock_adjtime(ADJ_SETOFFSET) 更可靠
    • 适用于大偏差(如几十年的偏差)
  2. 频率调整(SERVO_LOCKED)

    • 使用 clock_adjtime(ADJ_FREQUENCY) 调整频率
    • 时钟会逐渐加速或减速
    • 平滑收敛,不影响应用程序
  3. 错误处理

    • 检查返回值,打印错误信息
    • 帮助排查权限或系统限制问题

双Socket实现

从时钟需要监听两个PTP端口:

c 复制代码
int main(int argc, char *argv[])
{
    int event_fd, general_fd;
    
    /* 创建两个socket:一个监听319,一个监听320 */
    event_fd = create_socket(argv[1], PTP_EVENT_PORT);   // 319
    general_fd = create_socket(argv[1], PTP_GENERAL_PORT); // 320
    
    /* 使用select监听两个socket */
    while (1) {
        fd_set readfds;
        
        FD_ZERO(&readfds);
        FD_SET(event_fd, &readfds);
        FD_SET(general_fd, &readfds);
        
        select(max_fd + 1, &readfds, NULL, NULL, &timeout);
        
        /* 处理event端口(319)的消息 */
        if (FD_ISSET(event_fd, &readfds)) {
            // 处理Sync和Delay_Resp
        }
        
        /* 处理general端口(320)的消息 */
        if (FD_ISSET(general_fd, &readfds)) {
            // 处理Follow_Up和Announce
        }
    }
}

为什么需要双Socket

根据IEEE 1588标准:

  • Event端口(319):Sync, Delay_Req, Delay_Resp(需要时间戳)
  • General端口(320):Follow_Up, Announce(不需要时间戳)

使用双Socket确保能收到所有PTP消息。


从时钟主程序

完整源码请查看 ptp_slave.c

以下是关键流程说明:

主程序框架

c 复制代码
int main(int argc, char *argv[])
{
    int event_fd, general_fd;
    
    /* 初始化 */
    init_port_id();
    pi_servo_init(&servo);
    
    /* 创建双Socket */
    event_fd = create_socket(argv[1], PTP_EVENT_PORT);   // 319
    general_fd = create_socket(argv[1], PTP_GENERAL_PORT); // 320
    
    /* 主循环 */
    while (1) {
        // 定期发送Delay_Req
        // 接收并处理PTP消息
    }
}

消息处理流程

c 复制代码
/* 处理Sync消息 */
static void handle_sync(ptp_sync_msg_t *msg)
{
    clock_gettime(CLOCK_REALTIME, &t2);  // 记录接收时间
    printf("Received Sync seq=%u\n", ...);
}

/* 处理Follow_Up消息 */
static void handle_follow_up(ptp_follow_up_msg_t *msg)
{
    ptp_to_timespec(&msg->precise_origin_timestamp, &t1);  // 获取t1
    printf("Follow_Up seq=%u: t1=%ld.%09ld t2=%ld.%09ld\n", ...);
}

/* 处理Delay_Resp消息 */
static void handle_delay_resp(ptp_delay_resp_msg_t *msg)
{
    ptp_to_timespec(&msg->receive_timestamp, &t4);  // 获取t4
    
    // 计算路径延迟
    delay = ((t2-t1) + (t4-t3)) / 2;
    
    // 计算精确偏差
    offset = (t2-t1) - delay;
    
    // 调整时钟
    freq = pi_servo_sample(&servo, offset, &state);
    adjust_clock(offset, freq, state);
}

同步效果

第一次同步(大偏差)

bash 复制代码
# 从时钟落后主时钟38年
Follow_Up: t1=1775811745.xxx t2=575585765.xxx
Delay_Resp: offset=-1200225979905902560 ns
CLOCK JUMP: -1200225979905902560 ns (new time: 1775811745.xxx)

时间立即跳变到主时钟时间。

后续同步(已锁定)

bash 复制代码
# 偏差在微秒级
Follow_Up: t1=1775811746.xxx t2=1775811746.xxx
Delay_Resp: delay=178976 ns offset=50538 ns
FREQ ADJ: -0.35 ppb  ← 平滑调整

时钟平滑收敛,维持同步。


完整实现

本章实现了完整的PTP从时钟:

关键特性

  • ✅ 双Socket监听(符合IEEE 1588标准)
  • ✅ E2E延迟测量
  • ✅ 两阶段时钟调整(JUMP + FREQ)
  • ✅ PI伺服控制器
  • ✅ 支持超大偏差修正(如38年)

源码文件

下一节:编译运行与测试

📚 本文内容摘自本人的开源书《PTP技术书 - 从思想实验到协议实现》

全书从时间本质的思想实验出发,深度解析 IEEE 1588 协议、逐章分析 LinuxPTP 源码,并带你动手实现一个轻量级 PTP 程序(ptp-lite)。

🔗 在线阅读/下载:ptp-book

bash 复制代码
git clone https://github.com/Lularible/ptp-book.git

⭐ 如果对您有帮助,欢迎 Star 支持,也欢迎通过 GitHub Issues 交流讨论。

相关推荐
小辰记事本1 小时前
RDMA:AI算力集群的“网络命脉”
网络·人工智能·网络协议·rdma
缪懿1 小时前
javaEE:网络编程基础
java·网络·java-ee
BizViewStudio1 小时前
2026 年网站建设行业白皮书:AI 深度融合与合规驱动下的 6 大变革方向——附优质开发商
大数据·网络·人工智能·microsoft·媒体
切糕师学AI1 小时前
深入解析 gRPC:高性能开源 RPC 框架的原理与实战
网络协议·rpc·开源·grpc
500佰1 小时前
我唯一的一个变现产品,说说它的逻辑
网络·职场和发展·idea·个人开发·软件需求
飞凌嵌入式1 小时前
工业运维救星!飞凌嵌入式 FCU1501 自带物理复位键,IP 输错再也不用跑现场
嵌入式
浪客灿心2 小时前
Linux网络NAT
linux·网络
qcx232 小时前
开源首发:DocCenter — AI 时代的 HTML工作台深度解析
人工智能·开源·html
怀旧,2 小时前
【Linux网络编程】10. NAT技术、代理服务、内网穿透
linux·网络·智能路由器