本文中gptp部分理论介绍采用gPTP,自动驾驶时间同步里的“有趣灵魂” - 知乎文章中的描述

基础知识:

时钟源:

1.系统计数器:

CLOCK_MONOTONIC(启动后单调时间)、CLOCK_REALTIME(实时时间)使用的计数器,也可以称为系统计数器,一般linux默认使用的系统时间(systemtime)都是依赖于该计数器

2.PHY自带计数器:

phc(ptp hardware clock),实际为网口的PHY(如88q2112)自带的硬件定时器所对应的时间

下面基于imx8系列芯片的linux kernel driver分析下 ptp hardware clock

PTP hardware clock相关:

eth对应的PHY上的定时器,网络驱动加载时候创建的节点为/dev/ptp

ptp hardware clock驱动代码:

drivers/ptp目录下

ptp_chardev.c                    ----/dev/ptp设备节点的fileoperations相关接口,主要为相关ioctls

ptp_clock.c                        ----ptp_clock_register/ptp_clock_settime/ptp_clock_gettime

其中ptp_clock_register为注册函数,重点关注

会在drivers/net/ethernet/freescale/fec_ptp.c中的fec_ptp_init中调用,所以一般eth0对应的就是/dev/ptp0,eth1对应的就是/dev/ptp1,并且该函数最终会为ptp hardware clock创建一个posix clock

note:因为ptp hardware clock是对应的每个外网口上的PHY带的计数器,所以每个网口驱动挂载的时候会加载ptp hardware clock相关驱动

如上所述网络驱动代码fec_ptp.c中除了会调用ptp_clock_register注册ptp hardware clock外,还有几个地方值得关注:

fec_ptp_init函数中会将fec_ptp_adjfreq/fec_ptp_adjtime/fec_ptp_gettime/gec_ptp_settime等PHY时间相关函数接口注册给ptp_clock_info,后续ptp hardware clock的驱动就是通过这些函数来实现设置和获取时间等操作,同时也会给phy的timer做初始化

框图:

1.概述:

gPTP(generalized Precision Time Protocol,广义精确时间同步协议),基于PTP(IEEE 1588v2)协议进行了一系列优化,形成了更具有针对性的时间同步机制,可以实现μs级的同步精度,协议标准为802.1as,协议本身定义在OSI模型的第2层,链路层,所有报文出入端口的时间戳通过硬件来记录

gPTP定义有两种设备类型,Time-aware-end StationTime-aware Bridge。每种设备都具有本地时钟,本地时钟都是通过晶振的振荡周期进行度量的,设备内部硬件计数器负责对振荡周期进行计数。设备中用来发布时间同步报文的网络端口称为主端口,用来接收时间同步报文的端口称为从端口

(1)Time-aware-end Station,既可以作为主时钟,也可以作为从时钟

(2)Time-aware Bridge,既可以作为主时钟,也可以作为桥接设备,类似交换机。桥接类设备在收到gPTP报文后,会请报文搓个澡,然后再送出去。而报文在桥接设备内搓澡消耗的时间,称为驻留时间。gPTP要求桥接设备必须具有测量驻留时间的能力

下图为一个Taes、TaB的一个方案图:

 BMCA(Best Master Clock Algorithm):

gPTP中规定的主时钟动态分配机制为BMCA(Best Master Clock Algorithm,最佳主时钟选择算法)。系统上电唤醒之后,系统所有设备都可以通过发送一条报文(Announce Message)来参与主时钟竞选,报文中含有各自设备的时钟信息。每一个参选设备都会比较自己的时钟信息和其它设备的时钟信息,并判断是否具有优势,如果不具有,则退出竞选,直到综合能力最强的武林盟主诞生

note:车载系统中一般主设备和从设备的关系一般都是确定的,所以大部分时候不需要BMC算法来找Master,一般系统都可以直接指定主设备、从设备

TSN:

TSN的确定性和实时性优势是建立在精确的时间同步基础之上,而TSN中用于实现精确时间同步的协议是IEEE 802.1AS,也就是业界常说的gPTP

gptp的开源代码实现:

avnu-gptp : GitHub - Avnu/gptp

avnu-gptp是avnu开源的gptp协议栈,主要基于c++代码实现,通过pipe来分享时间信息

linux-ptp : https://git.code.sf.net/p/linuxptp/code

liunxptp既支持ptp也支持gptpd,本文重点以该代码实现为主

2.gptp理论基础

同步过程:

gPTP定义有两类报文,事件类型报文(包括Sync、Pdelay_Req、Pdelay_Resp三条)和一般类型报文(包括Follow_UP、Pdelay_Resp_Follow_UP二条)。gPTP定义设备工作在网络七层模型中的第二层数据链路层的MAC(Media Acess Control,媒介访问控制)子层。

当设备MAC层接收或发送事件类型报文时,会触发对硬件计数器进行采样,从而获得时钟振荡周期计数值,结合时钟振荡频率及基准时间,可获得此时的时间戳。而一般类型报文仅用来携带信息,不会触发内部硬件计数器的采样操作

2.1 时钟偏差测量

  • gPTP定义的五条报文中,Sync和Follow_UP为一组报文,周期发送(一般125ms),主要用来测量时钟偏差
  • Sync由主端口发送,在报文离开主端口MAC层时,触发主端口记录此时的时间戳t1
  • 从端口MAC层收到Sync报文后会记录此时的时间戳t2
  • 随后,主端口将t1值附到Follow_UP报文里发送给从端口

图示:

如果没有网络传输延迟或延迟、可以忽略,则从端口将本地时钟值加上时钟偏差(t1-t2的值)就完成时间同步,也就没有后面的碎碎念了。但是对于μs级时间同步精度的gPTP来说,传输延迟显然无法视若不见

2.2 传输延时测量

gPTP采用P2P(Peer to Peer)的方法来测量传输延迟。在P2P方法中,测量的是相邻设备间的传输延迟,报文不允许跨设备传输,这也就要求gPTP网络内的所有设备都需要支持gPTP功能。同时定义一组独立的报文专门负责传输延迟测量,分别为周期发送的Pdelay_Req、Pdelay_Resp和Pdelay_Resp_Follow_UP

从端口首先发送Pdelay_Req报文,标志传输延迟测量的开始,在报文离开从端口MAC层时,触发从端口记录此时的时间戳t3。主端口MAC层收到Pdelay_Req报文后会记录此时的时间戳t4,随后,主端口通过Pdelay_Resp报文将值t4发送给从端口,同时在Pdelay_Resp报文离开主端口的MAC层时,触发主端口记录此时的时间戳t5,从端口MAC层收到Pdelay_Resp报文后记录此时的时间戳t6。随后,相同的套路,主端口通过Pdelay_Resp_Follow_Up报文将值t5发送给从端口。至此,一次传输延迟测量过程已经结束。在假设路径传输延迟是对称的前提下,可由如下公式计算相邻设备间的传输延迟。

2.3 频率同步

上文的传输延迟测量是基于从端口与主端口的时钟振荡频率一致的前提下得到的。现在我们考虑一下如果主从端口时钟振荡频率不一致的时候,会导致什么灵异事件发生。

假设从端口的时钟振荡频率是25MHz,主端口的时钟振荡频率是100MHz,这个时候参考时钟本身的频率比=4,但主从端口的rate ratio还是为1。这意味着从端口在收到一次振荡的时候,原有时间会加40ns,而主端口则加10ns。 如果这个时候从端口由于温度、老化等原因,振荡频率变为26MHz,从端口收到一次振荡的时候再加40ns,显然是不准确的。

而频率同步就是要解决主从端口晶振精度不准带来的误差,通过测量报文可以发现此时rate ratio不为1,则从端口可以基于这个值调整自己的时基了

频率同步复用传输延迟测量过程的Pdelay_Resp和Pdelay_Resp_Follow_UP报文。通过采用两组答复,最终可以获得t5,t6,t9,t10的值,由下面公式可得主从端口的频率比

3. P2P/E2E

PTP构成的网络各节点称为时钟节点,协议定义了如下三类:

OC(ordinary clock)只有一个PTP通信端口,一般指最终设备

BC(boundary clock)具有多个PTP端口的时钟,通过其中一个从端口与主时钟同步,并支持其他从端口与自身同步

TC(transparent clock)可测量PTP报文在其内部传输的时间,并在转发时提供相应的矫正

E2E(End-to-End)通过Delay_Request和Delay_Response报文进行延迟计算的一种方法

P2P(Peer-to-Peer)通过Pdelay_Req和Pdelay_Resp报文进行延迟计算的一种方法

P2P和E2E对比:

E2E中有个非常关键的假设:既双向链路对等,但是这个假设是针对整个网络架构而言的,如果网络较大,中间交换设备较多那这个假设容易出现问题,换言之,如果缩小规模,甚至对单条链路假设对称更为靠谱,这就是E2E算法基本原理。

P2P的算法机制要求网络中每台交换机都运行P2P算法,也就是网络中不能存在普通交换机,因为普通交换机无法识别和相应Pdelay报文,无法测量它与周边设备的链路延迟,将导致全网时钟计算出现偏误。如果系统中已经存在大量已购的普通交换机,采用E2E是更好的选择

4.时间收敛算法(Servo)

非常关键,一般称之为Servo。其收敛速度以及能达到的精度是关键指标。相比而言,PTP协议栈的实现很简单

linuxptp支持的servo算法如下:

enum servo_type

{
CLOCK_SERVO_PI,                                                          ----默认servo算法,类似PI控制器的线性调整算法
CLOCK_SERVO_LINREG,                                                   ----使用线性回归的自适应控制器
CLOCK_SERVO_NTPSHM,                                                
CLOCK_SERVO_NULLF,
};

5.gPTP与PTP之间的差异

(1)传输延时测量方式

gPTP仅支持P2P的传输延时测量方式,PTP除了支持上文提到的P2P方法,还支持E2E(End-to-End)方法。在E2E方法中,测量的是网络中任意两个支持PTP设备之间的传输延迟,而在这两个设备之间允许存在普通交换机等可以透传PTP报文的设备。这导致P2P和E2E方式在如下方面存在差异。

(a)测量精度:P2P方法中,报文在桥接设备的驻留时间可以被测量,且会和传输延时时间一同发给后面链路上的设备,故测量精度可控且足够高。E2E方法中,报文在普通交换机的驻留时间具有随机性且不可测量,导致测量精度不可控且波动范围大。

(b)架构灵活性:P2P方法中,测量报文不跨设备传输,主时钟变化或新增从时钟,仅对物理上相邻的设备有影响,有利于网络拓展;E2E方法中,无论主时钟变化还是从时钟变化,都需要重新测量整个网络的传输延迟,且在网络比较复杂时,网络开销会比较大,因此网络拓展性较差

(2)时间戳采样方式

gPTP只能工作在MAC子层,PTP除了可以工作在MAC子层,还可以工作在传输层。工作在传输层时,报文要经历协议栈缓存、操作系统调度等过程,这两个过程都会带来传输延时的增加且大小不可控。而工作在MAC子层时,离物理层只有一步之遥,既能减缓协议栈缓存带来延时的不确定性,又能缩短报文传输延时。

工作在MAC子层时,报文要么直接发给物理层要么从物理层收到,因此时间戳可以选择由物理层硬件打或由软件打。通过硬件的方式打,可以消除操作系统调度带来的延时不确定性。PTP工作在MAC子层时,既支持硬件打时间戳,也支持软件打时间戳。而gPTP从延时可控,延时减少的角度考虑,只允许打硬件时间戳。

(3)时钟类型

PTP时钟支持两种时钟类型,One-Step Clock和Two-Step Clock。在One-Step Clock中,事件报文发送时,同时将本身记录的时间戳发送给从端口,如下图左半部分所示。在Two-Step Clock中,事件报文不携带时间戳信息,需要一条专门的一般类型报文来给从端口发送时间戳,如下图右半部分所示。

One-Step Clock虽然可以比Two-Step Clock节省一条报文,但对硬件要求很高,且硬件成本高,不利于网络扩展和应用普及。在两者精度没有区别的前提下,Two-Step Clock类型显然是gPTP的更优选择,这也是gPTP协议里规定的类型

6. gPTP报文分析

Ethernet Header:

Destination MAC             8bytes    固定值01:80:c2:00:00:0e,链路层广播地址

Source MAC                    8bytes   具体设备地址

Type                                2bytes   0x88f7为gptp

gPTP报文由3部分组成:

  1. Header (对所有gPTP都一样)
  2. Body (取决于gPTP 报文类型)
  3. TLV(type length value)

Header详情:

body:取决于gPTP 报文类型messageID

报文头中的messageID标识了gPTP报文的类型,类型取值范围如下图所示:

7.linuxptp代码详细分析

7.1系统对于硬件时钟的支持:

ethtool -T eth0:

 

 1) 软件时间戳需要包括参数

        SOF_TIMESTAMPING_SOFTWARE

        SOF_TIMESTAMPING_TX_SOFTWARE

        SOF_TIMESTAMPING_RX_SOFTWARE  

2) 硬件时间戳需要包括参数    

        SOF_TIMESTAMPING_RAW_HARDWARE

        SOF_TIMESTAMPING_TX_HARDWARE

        SOF_TIMESTAMPING_RX_HARDWARE 

详情描述见kernel中Documentation/networking/imestamping.txt(该文档中包括很多对于时间类型和获取办法的详细描述),如下:

7.2 linuxptp代码概述

linuxptp可以编译出来多个bin,我们重点关心如下两个

ptp4l:主要用来计算两个设备之间的误差,频率误差

phc2sys : 主要是把两个时钟进行同步,比如把systime同步到phc时间(ptp hardware clock)

ptp4l代码简述:

1.getopt_long()                                                             ----获取配置参数

2.clock_create()                                                             ----创建时钟处理,socket创建、初始化等

3.clock_poll()                                                                 ----实时处理clock过来的相关事件,并结合事件做时间调整等相关处理 

7.3 RAW socket链路层数据通信

由于gptp工作在网络模型第二层,所有通信需要用到以太网链路层的数据收发,所以需要使用socket raw原始数据,ptp则采用常规socket,走组播。

raw socket和常规的udp/tcp socket比较不同,主要操作链路层,不经过网络协议栈,常见的tcpdump/网络流量统计等底层都是走的raw socket,本文挑其中重点函数解析

raw_open函数详解:                                            创建raw socket相关fd,配置基础特性 

 open_socket函数详解:

raw_configure函数:

sk_timestamping_init函数: 

硬件时间戳类型如下:

SOF_TIMESTAMPING_TX_HARDWARE

SOF_TIMESTAMPING_RX_HARDWARE

SOF_TIMESTAMPING_RAW_HARDWARE;

关于时间戳相关选项option的说明:        SO_TIMESTAMP/SO_TIMESTAMPNS/SO_TIMESTAMPING

 

 7.4 数据包时间戳获取

sk_receive函数: 

note:

2.1.1.5 Blocking Read

Reading from the error queue is always a non-blocking operation. To block waiting on a timestamp, use poll or select. poll() will return POLLERR in pollfd.revents if any data is ready on the error queue.
There is no need to pass this flag in pollfd.events. This flag is ignored on request. See also `man 2 poll`.

2.1.2 Receive timestamps

On reception, there is no reason to read from the socket error queue. The SCM_TIMESTAMPING ancillary data is sent along with the packet data on a normal recvmsg(). Since this is not a socket error, it is not accompanied by a message SOL_IP(V6)/IP(V6)_RECVERROR. In this case,the meaning of the three fields in struct scm_timestamping is implicitly defined.

ts[0] holds a software timestamp if set, ts[1] is again deprecated and ts[2] holds a hardware timestamp if set.