让不同机器上的时间,保持一致,就是时间同步。多台机器下的任务需要在一个时间基准下工作,就需要时间同步。本文讨论的时间同步协议是PTP。如果所有业务都部署在一台机器上,那么当然也不需要时间同步,在同一台机器上,使用同一个时间基准。只有多态机器需要依赖同一个时间基准工作的时候,才需要时间同步。

时间同步在自动驾驶领域是至关重要的核心技术,其重要性不亚于传感器硬件和算法本身。它如同整个系统的“神经系统时钟”,确保各个部分协调一致地工作。

多传感器数据融合的基础

自动驾驶汽车依赖激光雷达、摄像头、毫米波雷达、惯性导航单元等多种传感器。这些传感器以不同的频率(如10Hz、50Hz、100Hz)采集数据。

  • 问题:如果没有精确到毫秒甚至微秒级的时间同步,一个“看到”障碍物的摄像头帧和一个“检测到”物体的激光雷达点云就无法准确对应到同一时刻的同一物体。

  • 后果:会导致融合算法产生“鬼影”或位置错乱,严重降低感知精度。高精度时间同步(如基于PTP或GPS PPS) 为所有数据打上统一的时间戳,是生成准确、一致的“世界模型”的前提。

决策与控制的时效性和一致性

从感知到规划再到控制执行,是一个严密的因果链条。

  • 问题:规划模块基于“过去某个时刻”的感知结果做决策,而车辆状态(速度、位置)却在实时变化。如果各模块间的处理时间戳不一致,规划出的轨迹可能基于过时或超前的信息。

  • 后果:导致控制指令(转向、刹车)不准确、不平稳,甚至产生危险。同步的时间戳确保了整个信号链的时序逻辑正确。

如何实现时间同步呢,如果让我们自己想,那么很容易想到,最简单的情况下,在master机器上发布时间,slave机器上订阅时间,slave收到的这个时间就是全局时间,这种最简单的情况有两个问题:

(1)没有考虑时间同步报文在链路上传输的耗时,如果报文传输耗时100μs,那么slave收到的时间加上100μs才是当前的实际时间。如果一个master,多个slave,那么master和各slave之间的链路延时还可能不一样。这就引入了时间同步协议中的最重要的内容,延时测量,延时测量方法有两种,E2E和P2P。

(2)如果master 100ms发布一次数据,那么slave每100ms就可以收到最新的时间,slave侧的用户获取时间是随时都可以获取的,那么怎么保证slave随时获取的时间是准确的呢,这就需要和本地的时钟进行结合,在订阅到master发布的数据时记录本地时钟的时间tl1,那么用户获取时间的时候可以获取本地时钟的时间tl2,那么上次订阅到的全局时间加上(tl2-tl1)就是当前的全局时间。但是这样有也有问题,就是本地时钟和全局时钟的频率不一致,也就是两边的时钟跑的不一样快,这就引入了时间同步的另一个重要的内容,不仅要同步时间,也要实时调整频率。

延时测量,时间同步,频率同步,是时间同步的3个重点内容。

PTP协议硬件时间戳功能依赖网卡硬件:

ethtool -T <网卡名>

Capabilities:
        hardware-transmit     (SOF_TIMESTAMPING_TX_HARDWARE)
        software-transmit     (SOF_TIMESTAMPING_TX_SOFTWARE)
        hardware-receive      (SOF_TIMESTAMPING_RX_HARDWARE)
        software-receive      (SOF_TIMESTAMPING_RX_SOFTWARE)
        software-system-clock (SOF_TIMESTAMPING_SOFTWARE)
        hardware-raw-clock    (SOF_TIMESTAMPING_RAW_HARDWARE)
PTP Hardware Clock: 0
Hardware Transmit Timestamp Modes:
        off                   (HWTSTAMP_TX_OFF)
        on                    (HWTSTAMP_TX_ON)
        one-step-sync         (HWTSTAMP_TX_ONESTEP_SYNC)
Hardware Receive Filter Modes:
        none                  (HWTSTAMP_FILTER_NONE)
        ptpv1-l4-sync         (HWTSTAMP_FILTER_PTP_V1_L4_SYNC)
        ptpv1-l4-delay-req    (HWTSTAMP_FILTER_PTP_V1_L4_DELAY_REQ)
        ptpv2-l4-sync         (HWTSTAMP_FILTER_PTP_V2_L4_SYNC)
        ptpv2-l4-delay-req    (HWTSTAMP_FILTER_PTP_V2_L4_DELAY_REQ)
        ptpv2-l2-sync         (HWTSTAMP_FILTER_PTP_V2_L2_SYNC)
        ptpv2-l2-delay-req    (HWTSTAMP_FILTER_PTP_V2_L2_DELAY_REQ)
        ptpv2-event           (HWTSTAMP_FILTER_PTP_V2_EVENT)
root@tegra-ubuntu:/maps/wyl/linuxptp#
 

Capabilities:

hardware-transmit网卡在发送报文时记录时间戳
software-transmit软件层在发送报文时记录时间戳
hardware-receive网卡在接收到一个报文时记录时间戳
software-receive软件在接收到一个报文时记录时间戳

Hardware Receive Filter Modes,控制网卡对什么报文打时间戳。

如下是示例,在发送侧,可以通过error queue获取发送报文时的时间戳。接收报文时,可以获取接收到该报文时的时间戳。需要通过ioctl设置网卡,将硬件时间戳开关打开,然后将filters都打开。

发送:

#include <arpa/inet.h>
#include <errno.h>
#include <linux/errqueue.h>
#include <linux/net_tstamp.h>
#include <linux/sockios.h>
#include <net/if.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <unistd.h>
#include <net/if.h>

#define UDP_PORT        320
#define UDP_MESSAGE     "Hello, timestamp!"
#define INTERFACE       "mgbe3_0"   // 请根据实际网卡名称修改

static int enable_hw_timestamping(int sock, const char *ifname)
{
    struct ifreq ifr;
    struct hwtstamp_config hwconfig;

    memset(&ifr, 0, sizeof(ifr));
    strncpy(ifr.ifr_name, ifname, IFNAMSIZ - 1);
    ifr.ifr_data = (char *)(void *)&hwconfig;

    memset(&hwconfig, 0, sizeof(hwconfig));
    hwconfig.tx_type = HWTSTAMP_TX_ON;
    hwconfig.rx_filter = HWTSTAMP_FILTER_ALL;

    if (ioctl(sock, SIOCSHWTSTAMP, &ifr) < 0) {
        perror("ioctl SIOCSHWTSTAMP failed");
        return -1;
    }
    return 0;
}

static int get_tx_timestamp(int sock, struct timespec *ts)
{
    struct msghdr msg;
    struct iovec iov;
    char ctrl_buf[CMSG_SPACE(sizeof(struct timespec) * 3)];
    char data_buf[1024];
    struct cmsghdr *cmsg;
    struct sock_extended_err *err;
    int ret;

    memset(&msg, 0, sizeof(msg));
    memset(&iov, 0, sizeof(iov));
    memset(ctrl_buf, 0, sizeof(ctrl_buf));

    iov.iov_base = data_buf;
    iov.iov_len = sizeof(data_buf);
    msg.msg_iov = &iov;
    msg.msg_iovlen = 1;
    msg.msg_control = ctrl_buf;
    msg.msg_controllen = sizeof(ctrl_buf);

    ret = recvmsg(sock, &msg, MSG_ERRQUEUE);
    if (ret < 0) {
        perror("recvmsg MSG_ERRQUEUE");
        return -1;
    }

    for (cmsg = CMSG_FIRSTHDR(&msg); cmsg; cmsg = CMSG_NXTHDR(&msg, cmsg)) {
        if (cmsg->cmsg_level == SOL_SOCKET && cmsg->cmsg_type == SCM_TIMESTAMPING) {
            struct scm_timestamping *tss = (struct scm_timestamping *)CMSG_DATA(cmsg);
            *ts = tss->ts[2];  // ts[2] 是原始硬件时间戳
            return 0;
        }
        if (cmsg->cmsg_level == SOL_IP && cmsg->cmsg_type == IP_RECVERR) {
            err = (struct sock_extended_err *)CMSG_DATA(cmsg);
            if (err->ee_errno != 0) {
                // 错误信息,继续查找时间戳
            }
        }
    }
    return -1;
}

int main(int argc, char *argv[])
{
    int sock;
    struct sockaddr_in dest_addr;
    struct timespec ts;
    ssize_t sent;
    int timestamp_flags;
    int ret;

    // 1. 创建UDP socket
    sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
    if (sock < 0) {
        perror("socket");
        exit(EXIT_FAILURE);
    }

    struct ifreq ifr;
    memset(&ifr, 0, sizeof(ifr));
    strncpy(ifr.ifr_name, INTERFACE, IFNAMSIZ - 1);
    if (setsockopt(sock, SOL_SOCKET, SO_BINDTODEVICE, (void *)&ifr, sizeof(ifr)) < 0) {
        perror("setsockopt SO_BINDTODEVICE");
        // 继续执行,但硬件时间戳可能不生效
    }

    // 2. 启用网卡硬件时间戳
    if (enable_hw_timestamping(sock, INTERFACE) < 0) {
        close(sock);
        exit(EXIT_FAILURE);
    }

    // 3. 设置socket时间戳选项
    timestamp_flags = SOF_TIMESTAMPING_TX_HARDWARE |
                      SOF_TIMESTAMPING_RAW_HARDWARE;
    if (setsockopt(sock, SOL_SOCKET, SO_TIMESTAMPING,
                   &timestamp_flags, sizeof(timestamp_flags)) < 0) {
        perror("setsockopt SO_TIMESTAMPING");
        close(sock);
        exit(EXIT_FAILURE);
    }

    // 4. 设置目标地址(本机回环,可根据需要修改)
    memset(&dest_addr, 0, sizeof(dest_addr));
    dest_addr.sin_family = AF_INET;
    dest_addr.sin_port = htons(UDP_PORT);
    if (inet_pton(AF_INET, "192.168.36.210", &dest_addr.sin_addr) <= 0) {
        perror("inet_pton");
        close(sock);
        exit(EXIT_FAILURE);
    }

    // 5. 发送UDP数据包
    sent = sendto(sock, UDP_MESSAGE, strlen(UDP_MESSAGE), 0,
                  (struct sockaddr *)&dest_addr, sizeof(dest_addr));
    if (sent < 0) {
        perror("sendto");
        close(sock);
        exit(EXIT_FAILURE);
    }
    printf("Sent %zd bytes\n", sent);

    // 6. 从错误队列获取发送时间戳
    ret = get_tx_timestamp(sock, &ts);
    if (ret == 0) {
        printf("TX Hardware Timestamp: %ld.%09ld\n", ts.tv_sec, ts.tv_nsec);
    } else {
        printf("Failed to get TX timestamp\n");
    }

    close(sock);
    return 0;
}

接收侧:

#include <arpa/inet.h>
#include <errno.h>
#include <linux/net_tstamp.h>
#include <linux/sockios.h>
#include <net/if.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <unistd.h>

#define UDP_PORT        320
#define BUFFER_SIZE     1500
#define INTERFACE       "mgbe3_0"   // 请根据实际网卡名称修改

static int enable_hw_timestamping(int sock, const char *ifname)
{
    struct ifreq ifr;
    struct hwtstamp_config hwconfig;

    memset(&ifr, 0, sizeof(ifr));
    strncpy(ifr.ifr_name, ifname, IFNAMSIZ - 1);
    ifr.ifr_data = (char *)(void *)&hwconfig;

    memset(&hwconfig, 0, sizeof(hwconfig));
    hwconfig.tx_type = HWTSTAMP_TX_OFF;
    hwconfig.rx_filter = HWTSTAMP_FILTER_ALL;

    if (ioctl(sock, SIOCSHWTSTAMP, &ifr) < 0) {
        perror("ioctl SIOCSHWTSTAMP failed");
        return -1;
    }
    return 0;
}

int main(int argc, char *argv[])
{
    int sock;
    struct sockaddr_in local_addr;
    struct sockaddr_in src_addr;
    socklen_t addr_len = sizeof(src_addr);
    char buffer[BUFFER_SIZE];
    struct msghdr msg;
    struct iovec iov;
    char ctrl_buf[CMSG_SPACE(sizeof(struct timespec) * 3)];
    struct cmsghdr *cmsg;
    ssize_t recv_len;
    int timestamp_flags;
    struct timespec ts;
    int ret;

    // 1. 创建UDP socket
    sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
    if (sock < 0) {
        perror("socket");
        exit(EXIT_FAILURE);
    }

    // 2. 启用网卡硬件时间戳
    if (enable_hw_timestamping(sock, INTERFACE) < 0) {
        close(sock);
        exit(EXIT_FAILURE);
    }

    // 3. 设置socket时间戳选项
    timestamp_flags = SOF_TIMESTAMPING_RX_HARDWARE |
                      SOF_TIMESTAMPING_RAW_HARDWARE;
    if (setsockopt(sock, SOL_SOCKET, SO_TIMESTAMPING,
                   &timestamp_flags, sizeof(timestamp_flags)) < 0) {
        perror("setsockopt SO_TIMESTAMPING");
        close(sock);
        exit(EXIT_FAILURE);
    }

    // 4. 绑定端口
    memset(&local_addr, 0, sizeof(local_addr));
    local_addr.sin_family = AF_INET;
    local_addr.sin_port = htons(UDP_PORT);
    local_addr.sin_addr.s_addr = INADDR_ANY;

    if (bind(sock, (struct sockaddr *)&local_addr, sizeof(local_addr)) < 0) {
        perror("bind");
        close(sock);
        exit(EXIT_FAILURE);
    }
    printf("Receiver listening on port %d\n", UDP_PORT);

    // 5. 准备接收
    memset(&msg, 0, sizeof(msg));
    memset(&iov, 0, sizeof(iov));
    memset(ctrl_buf, 0, sizeof(ctrl_buf));

    iov.iov_base = buffer;
    iov.iov_len = sizeof(buffer);
    msg.msg_name = &src_addr;
    msg.msg_namelen = addr_len;
    msg.msg_iov = &iov;
    msg.msg_iovlen = 1;
    msg.msg_control = ctrl_buf;
    msg.msg_controllen = sizeof(ctrl_buf);

    // 6. 接收数据
    recv_len = recvmsg(sock, &msg, 0);
    if (recv_len < 0) {
        perror("recvmsg");
        close(sock);
        exit(EXIT_FAILURE);
    }
    printf("Received %zd bytes\n", recv_len);

    // 7. 从辅助数据中提取时间戳
    for (cmsg = CMSG_FIRSTHDR(&msg); cmsg; cmsg = CMSG_NXTHDR(&msg, cmsg)) {
        if (cmsg->cmsg_level == SOL_SOCKET && cmsg->cmsg_type == SCM_TIMESTAMPING) {
            struct timespec *tss = (struct timespec *)CMSG_DATA(cmsg);
            ts = tss[2];  // ts[2] 是原始硬件时间戳
            printf("RX Hardware Timestamp: %ld.%09ld\n", ts.tv_sec, ts.tv_nsec);
            break;
        }
    }

    close(sock);
    return 0;
}

HWTSTAMP_FILTER_ALL 的含义就是“时间戳任何传入数据包”。它不限于 PTP 报文,而是让网卡对所有收到的报文都启用硬件时间戳功能。

内核文档也确认了这一点:HWTSTAMP_FILTER_ALL 表示时间戳所有数据包。

PTP 过滤器是另一回事

你列出的网卡能力(Capabilities)里的:

  • ptpv1-l4-sync

  • ptpv2-l4-delay-req

  • ptpv2-event

这些是更精细的 PTP 专用过滤器。它们的作用是:只对特定类型的 PTP 报文打时间戳。

比如 ptp4l 程序启动时,默认就会把网卡设置为 HWTSTAMP_FILTER_PTP_V2_EVENT,只对 PTPv2 事件报文打时间戳。

domain的概念:

domain(域)是 PTP 的逻辑隔离机制。同一个物理网络上可以运行多个独立的 PTP 同步系统,通过 domain 号区分互不干扰。打个比方:同一栋楼里有多个会议室,每个房间用门牌号(domain号)区分。你在 0 号房间讨论的话题不会传到 1 号房间,即使两个房间在同一个走廊里。很多概念里都有domain的概念,比如dds中有域和域参与者的概念,udp组播有组播组的概念,和这里域的概念是类似的。ptp各报文中有domain字段。

1PTP

1.1示意

Follow_Up和Delay_Resp是专门用来传输时间的。Follow_Up用来传递Sync报文离开Master的时间(在单步模式下,Flllow_Up报文则没必要了),Delay_Resp用来传递Delay_Req到达的时间。

时间同步通过主时钟和从时钟之间的报文交互来完成。

主时钟:

时钟源,发布时间,其它从时钟都要和主时钟的时间进行同步

从时钟:

与主时钟的时间保持同步

t1:

Sync报文离开主时钟时的时间戳

t2:

从时钟接收到Sync报文时的时间戳

t3:

从时钟发送Delay_Req报文时的时间戳

t4:

主时钟接收到Delay_Req报文时的时间戳

从时钟要基于t1、t2、t3、t4进行计算,计算的核心目标是计算出此时主时钟的真实时间。t1和t4都是主时钟的真实时间,但是当从时钟收到Delay_Resp报文时,t4已经发生了一段时间,主要是经过了Delay_Resp在网络上传输的时间。

t2 = t1 + delay + offset

t4 = t3 + delay - offset

通过上边两个公式计算得出:

delay = ((t2 - t1) + (t4 - t3))/2

offset = ((t2 - t1) - (t4 - t3))/2

可以看到,这种算法假设master和slave之间的传播链路是对称的,耗时是对称的。

delay和offset分别表示什么意思,为什么要计算这两个值呢?

delay表示报文在链路上的传输延时,PTP协议假设主时钟->从时钟,以及从时钟->主时钟的链路是对称的,也就是说双向的链路延时是相同的。

offset表示从时钟相对于主时钟的偏移。在某一时刻,比如在t1时刻,主时钟的时间是t1,那么此时从时钟的时间不一定是t1,如果每一时刻,两者的时间都是相同,那么便不需要时间同步,从时钟的时间和主时钟的时间肯定在每时每刻都有一个差值,这个差值就是offset。

时间同步就是将offset收敛到目标精度之内,比如1μs以内。

1.2ptp在协议栈第几层之上:以太网、udp

ptp协议可以工作在以太网之上,也可以工作在udp,一个链路层,一个传输层。

使用ptp4l命令可以-2指定使用以太网,-4使用udp。

1.2.1udp:320和319

1、Announce、Follow-Up、Delay_Resp报文,使用端口320;Sync、Delay_Req使用端口319

2、使用目标ip地址,224.0.1.129

1.2.2以太网

以太网的ptp协议号是0x88f7。

1.3时钟类型

Master:主时钟,时钟源

Slave:从时钟,同步主时钟的时间

Boundary:边界时钟,边界时钟包含一个Slave和Master,Slave从上游的Master同步时间;Master向下游的Slave同步时间,那么下游的Slave就只认识Boundary中的Master,不认识上游的Master了。跟一种路由协议很相似,有工作在边界上的,有工作在边界内部的,IGP和BGP。

Transparent:同透明时钟,只做转发。

为了简单,本文只讨论Master和Slave之间存在透明时钟的情况。

1.4延时测量机制:E2E和P2P

在PTP中,测量延时是非常重要的工作,延时测量的精确与否,直接影响时间同步是否精确。延时测量机制有两种:E2E和P2P。

E2E假设双向链路对称,如果实际不对称,则计算结果不精确;P2P消除了双向链路对称的假设依赖。

E2E适用场景:
网络拓扑简单、稳定的线性或星型网络。
不需要极高精度(亚微秒级)的应用,例如企业网、某些音视频流媒体系统。
网络设备较旧或仅支持普通二层/三层交换,无法部署P2P透明时钟。
主从时钟之间跳数较少。

P2P适用场景:

对同步精度要求较高的应用。

大规模,多跳的复杂网络。

E2E和P2P都会计算在节点上的驻留时间以及节点之间的传输时间。E2E通过Pdeley_req和Pdelay_resp计算整体延时,整体延时就包含了驻留时间和链路时间。P2P会将链路时间和驻留时间放在correctionField中,不需要专门的报文来测量延时。如果master和slave之间有多条路径,那么,如果报文传输路径发生了变化,那么E2E的计算结果也会变化,收敛慢;P2P只影响某一跳,收敛快。

1.4.1E2E

端到端,slave发送Delay_Req报文到Master,Master发送Delay_Resp报文到slave。

Delay_Req离开slave的时刻是t3,Delay_Req到达Master的时刻是t4。

1.4.2P2P

peer to peer,不是测量slave和master之间的delay,所有peer之间的延时。


每个节点都会测量和上游peer的链路延时,然后加到correctioField中。

相对于E2E,多了一个报文,Resp_Fup。

1.4.3correctionField

在E2E场景下:

Transparent1和Transparent2会将报文在本节点中的驻留时间加到correctionField中。对于Sync报文、Follow_up报文,和Delay_Resp报文,均会做这样的操作。

当接收到Follow_up报文时,也就得到了t1和t2两个值,t1会用correction进行纠正,后边计算delay或者offset均使用纠正后的t1,即t1c。

这里有个疑问,为什么t1要使用sync和follow up报文中的correction进行修正,两个correction,不会重复吗?在如下代码中,透明时钟转发sync和follow up报文的时候,都加上了correction。

在函数tc_complete_syfup中可以看到,correction只是加到了follow up中的correction field中,并没有加到sync的correction field中。

static void tc_complete(struct port *q, struct port *p,

            struct ptp_message *msg, tmv_t residence)

{

    switch (msg_type(msg)) {

    case SYNC:

    case FOLLOW_UP:

        tc_complete_syfup(q, p, msg, residence);

        break;

    case DELAY_REQ:

        tc_complete_request(q, p, msg, residence);

        break;

    case DELAY_RESP:

        tc_complete_response(q, p, msg, residence);

        break;

    }

}

收到Delay_Resp报文时,t4会用Delay_Resp的correction field进行修正。

在P2P模式下:

correctionField已经包含了所有链路的延时和每个透明时钟的驻留时间,但是缺少slave和它的peer的链路延时,这个延时由slave和它的peer通过Pdelay_Req、Pdelay_Resp、Pdelay_Resp_Fup来完成。

在如下的计算offset的代码中,兼容E2E和P2P两种模式。

int tsproc_update_offset(struct tsproc *tsp, tmv_t *offset, double *weight)

{

   ...

    /* offset = t2 - t1 - delay */

    *offset = tmv_sub(tmv_sub(tsp->t2, tsp->t1), delay);

}

在E2E模式下,t2是Slave接收到Sync报文的时间,t1是修正后的Master发送Sync的时间,两者相减,再减去delay,就是offset。在E2E模式下,delay是path delay。

在P2P模式下,t2与E2E模式下意思相同;t1也是修正后的时间,但是修正t1的时间是包含了所有的驻留时间和链路延时,缺少Slave也它的peer的链路延时。这里的delay是Slave和它的peer的链路延时。

 correction计算:源correction+驻留时间+链路delay

static void tc_complete_syfup(struct port *q, struct port *p,
			      struct ptp_message *msg, tmv_t residence)
{
	...
	c1 = net2host64(fup->header.correction);
	c2 = c1 + tmv_to_TimeInterval(residence);
	c2 += tmv_to_TimeInterval(q->peer_delay);
	c2 += q->asymmetry;
	fup->header.correction = host2net64(c2);
    ...
}

1.4.4目的mac

如果工作在以太网协议上。

1.5一步模式和两步模式

一步模式下,t1包含在Sync报文中,不需要Follow_up报文;两步模式,t1在Follow_up中。

两步模式的出现,是为了兼容那些无法在报文发送瞬间将时间戳写入报文的早期硬件。

1.2ptp4l测试结果

如下是slave侧的打印信息:

master offset:slave相对于master的偏移,相位同步

freq:slave相对于master的频率偏移,频率同步


ptp4l[541.135]: port 1 (ens33): INITIALIZING to LISTENING on INIT_COMPLETE
ptp4l[541.135]: port 0 (/var/run/ptp/ptp4l): INITIALIZING to LISTENING on INIT_COMPLETE
ptp4l[541.136]: port 0 (/var/run/ptp/ptp4lro): INITIALIZING to LISTENING on INIT_COMPLETE
ptp4l[547.144]: selected local clock 000c29.fffe.092928 as best master
ptp4l[562.962]: port 1 (ens33): new foreign master 000c29.fffe.a13faa-1
ptp4l[566.963]: selected best master clock 000c29.fffe.a13faa
ptp4l[566.963]: foreign master not using PTP timescale
ptp4l[566.963]: port 1 (ens33): LISTENING to UNCALIBRATED on RS_SLAVE
ptp4l[567.963]: master offset   18513043 s0 freq     +81 path delay    236121
ptp4l[568.964]: master offset   18510264 s0 freq     +81 path delay    227600
ptp4l[569.965]: master offset   18731866 s0 freq     +81 path delay    221137
ptp4l[570.965]: master offset   19175564 s0 freq     +81 path delay    197590
ptp4l[571.967]: master offset   18936113 s0 freq     +81 path delay    197590
ptp4l[572.967]: master offset   18910735 s0 freq     +81 path delay    199041
ptp4l[573.967]: master offset   19049961 s0 freq     +81 path delay    199041
ptp4l[574.967]: master offset   19078099 s0 freq     +81 path delay    208858
ptp4l[575.968]: master offset   19214035 s0 freq     +81 path delay    192378
ptp4l[576.968]: master offset   19471511 s0 freq     +81 path delay    181110
ptp4l[577.968]: master offset   19382949 s0 freq     +81 path delay    192378
ptp4l[578.970]: master offset   19503267 s0 freq     +81 path delay    192378
ptp4l[579.970]: master offset   19451555 s0 freq     +81 path delay    192378
ptp4l[580.971]: master offset   19594674 s0 freq     +81 path delay    192378
ptp4l[581.971]: master offset   19627512 s0 freq     +81 path delay    178572
ptp4l[582.972]: master offset   19730776 s0 freq     +81 path delay    171225
ptp4l[583.974]: master offset   19781335 s1 freq  +79299 path delay    167683
ptp4l[584.976]: clockcheck: clock frequency changed unexpectedly!
ptp4l[584.976]: master offset    1668894 s2 freq +247858 path delay    167683
ptp4l[584.976]: port 1 (ens33): UNCALIBRATED to SLAVE on MASTER_CLOCK_SELECTED
ptp4l[585.979]: master offset    3541232 s2 freq +438633 path delay    171225
ptp4l[586.980]: master offset    4889923 s2 freq +578392 path delay    189939
ptp4l[587.984]: master offset    5915394 s2 freq +686854 path delay    189939
ptp4l[588.984]: master offset    6695837 s2 freq +771594 path delay    189939
ptp4l[589.986]: master offset    7138014 s2 freq +822950 path delay    194640
ptp4l[590.986]: master offset    7464812 s2 freq +863095 path delay    194640
ptp4l[591.987]: master offset    7529117 s2 freq +877054 path delay    194640
ptp4l[592.987]: master offset    7564216 s2 freq +888129 path delay    194640
ptp4l[593.988]: master offset    7680397 s2 freq +907427 path delay    212139
ptp4l[594.988]: master offset    7409660 s2 freq +887763 path delay    212929
ptp4l[595.988]: master offset    6931089 s2 freq +846837 path delay    195431
ptp4l[596.988]: master offset    6864990 s2 freq +847092 path delay    195431
ptp4l[597.988]: master offset    6283599 s2 freq +795237 path delay    179609
ptp4l[598.988]: master offset    5964667 s2 freq +769308 path delay    179609
ptp4l[599.988]: master offset    5558496 s2 freq +734249 path delay    175563
ptp4l[600.989]: master offset    5146096 s2 freq +698155 path delay    191385
 

offset表示slave时间减去master的时间,整数表示slave比master快;

freq是正数,表示将slave的频率调大了。

s0、s1、s2:

代表从时钟的伺服状态,其实还有s4,但是测试环境达不到同步精度,所以没有打印出来。

/**

 * Defines the caller visible states of a clock servo.

 */

enum servo_state {

    /**

     * The servo is not yet ready to track the master clock.

     */

    SERVO_UNLOCKED,

    /**

     * The servo is ready to track and requests a clock jump to

     * immediately correct the estimated offset.

     */

    SERVO_JUMP,

    /**

     * The servo is tracking the master clock.

     */

    SERVO_LOCKED,

    /**

     * The Servo has stabilized. The last 'servo_num_offset_values' values

     * of the estimated threshold are less than servo_offset_threshold.

     */

    SERVO_LOCKED_STABLE,

};

1. SERVO_UNLOCKED(未锁定)

  • 初始状态或失步状态

  • 伺服器尚未准备好跟踪主时钟

  • 通常发生在:

    • 刚启动时

    • 失去主时钟连接时

    • 同步质量太差无法跟踪时

2. SERVO_JUMP(跳变状态)

  • 过渡状态,准备开始跟踪

  • 检测到时间偏移过大,需要立即校正

  • 伺服器请求时钟跳变(clock jump)来快速纠正时间差

  • 跳变后进入锁定状态

3. SERVO_LOCKED(已锁定)

  • 正常跟踪状态

  • 伺服器正在调整本地时钟频率来跟踪主时钟

  • 使用PI(比例-积分)控制算法进行微调

  • 时间同步已建立但可能还不够稳定

4. SERVO_LOCKED_STABLE(稳定锁定)

  • 最佳工作状态

  • 满足两个条件:

    1. 最近 servo_num_offset_values 个偏移测量值

    2. 都小于 servo_offset_threshold 阈值

  • 表明时钟同步已经稳定可靠

2gPTP

gPTP是在ptp协议上做了更具体的要求,删除一些灵活的配置,是TSN网络工作的基础。在TSN网络中,gPTP协议,应具有最高的优先级。

(1)只使用以太网进行传输,不使用udp进行传输,减少协议栈引入的抖动

(2)delay测量方式,只使用P2P,P2P协议有自己专用的目的MAC地址

.TP
.B ptp_dst_mac
The MAC address to which PTP messages should be sent.
Relevant only with L2 transport. The default is 01:1B:19:00:00:00.

.TP
.B p2p_dst_mac
The MAC address to which peer delay messages should be sent.
Relevant only with L2 transport. The default is 01:80:C2:00:00:0E.

(3)只能工作在局域网内

3踩坑记录

(1)linuxptp中是针对具体使用的协议来针对新打开filter的,而有些设备只有打开所有的filter才能生效,HWTSTAMP_FILTER_ALL,修改filter类型,需要修改sk.c文件。不生效,则会看到sync报文或者其他报文中缺少时间戳信息。

(2)linuxptp默认是在两个网卡之间进行时间同步,如果需要将时间同步到系统时间,那么需要用phc2sys来进行同步。

./phc2sys -s /dev/ptp0 -c CLOCK_REALTIME -O 0 -m --step_threshold 0.0001

--step_threshold 0.0001 表示偏移超过 100µs 才步进,否则只做频率微调(slew)。因为系统时钟已经手动设到接近 PHC,剩余偏移很小,不会产生大冲击。

在有些平台下,网卡的 PHC 和CPU架构定时器(CLOCK_REALTIME 的底层硬件)都派生自同一个 SoC 振荡器/系统计数器(TSC)。它们对用户态表现为两个独立时钟,但驱动层存在关联。如果这个时候,网卡时钟和系统时钟差值很大,此时将网卡时钟同步到系统时,会对网卡时钟造成冲击。这个时候需要手动设置系统时钟,缩小系统时钟和网卡时钟偏差,然后再进行同步。

更多推荐