PTP 对时协议 IEEE1588 网络对时 硬件基础

发布于:2024-05-10 ⋅ 阅读:(31) ⋅ 点赞:(0)

前言

在很多应用场景有精确对时的需求,例如车载网络,音视频流,工业网络。本文档将会阐述对时的硬件需求。

协议

流行的协议为 IEEE1588 标准指定的对时方法,名为 PTP 对时协议。

网卡硬件要求

找到某型网卡的特性描述:Standard for a Precision Clock Synchronization Protocol for Networked Measurement and Control Systems, Standard 1588-2008, IEEE

网卡相关寄存器

  • MAC_Timestamp_Control
  • MAC_Sub_Second_Increment
  • MAC_System_Time_Seconds
  • MAC_System_Time_Nanoseconds
  • MAC_System_Time_Seconds_Update
  • MAC_System_Time_Nanoseconds_Update
  • MAC_Timestamp_Addend
  • MAC_System_Time_Higher_Word_Seconds
  • Timestamp Status register

MAC_Sub_Second_Increment 用于设置系统亚秒增加

Timestamp Addend register的值会被用于微调系统时间,在Fine Update模式下,系统会根据这个值以微小的步长来更新时间。这样可以实现对系统时间的精细调整,以满足精确时间同步的需求。

MAC_System_Time_Nanoseconds + MAC_System_Time_Seconds 从这2个寄存器读出时间

MAC_System_Time_Nanoseconds_Update + MAC_System_Time_Seconds_Update 写入这2个寄存器配置时间

配置代码

tstamp 的初始化代码

配置了 MAC_Sub_Second_Increment 和 Timestamp Addend register

/**
 * stmmac_init_tstamp_counter - init hardware timestamping counter
 * @priv: driver private structure
 * @systime_flags: timestamping flags
 * Description:
 * Initialize hardware counter for packet timestamping.
 * This is valid as long as the interface is open and not suspended.
 * Will be rerun after resuming from suspend, case in which the timestamping
 * flags updated by stmmac_hwtstamp_set() also need to be restored.
 */
int stmmac_init_tstamp_counter(struct stmmac_priv *priv, u32 systime_flags)
{
	bool xmac = priv->plat->has_gmac4 || priv->plat->has_xgmac;
	struct timespec64 now;
	u32 sec_inc = 0;
	u64 temp = 0;
	int ret;

	if (!(priv->dma_cap.time_stamp || priv->dma_cap.atime_stamp))
		return -EOPNOTSUPP;

	ret = clk_prepare_enable(priv->plat->clk_ptp_ref);
	if (ret < 0) {
		netdev_warn(priv->dev,
			    "failed to enable PTP reference clock: %pe\n",
			    ERR_PTR(ret));
		return ret;
	}

	stmmac_config_hw_tstamping(priv, priv->ptpaddr, systime_flags);
	priv->systime_flags = systime_flags;

	/* program Sub Second Increment reg */// 配置 MAC_Sub_Second_Increment 
	stmmac_config_sub_second_increment(priv, priv->ptpaddr,
					   priv->plat->clk_ptp_rate,
					   xmac, &sec_inc);
	temp = div_u64(1000000000ULL, sec_inc);

	/* Store sub second increment for later use */
	priv->sub_second_inc = sec_inc;

//配置 Timestamp Addend register
	/* calculate default added value:
	 * formula is :
	 * addend = (2^32)/freq_div_ratio;
	 * where, freq_div_ratio = 1e9ns/sec_inc
	 */
	temp = (u64)(temp << 32);
	priv->default_addend = div_u64(temp, priv->plat->clk_ptp_rate);
	stmmac_config_addend(priv, priv->ptpaddr, priv->default_addend);

	/* initialize system time */
	#ifdef CONFIG_SEMIDRIVE_TIME_SYNC
	now.tv_sec = 0;
	now.tv_nsec = 0;
	#else
	ktime_get_real_ts64(&now);
	#endif

	/* lower 32 bits of tv_sec are safe until y2106 */
	stmmac_init_systime(priv, priv->ptpaddr, (u32)now.tv_sec, now.tv_nsec);

	return 0;
}
更新时间的代码
#include "stmmac.h"
#include "stmmac_ptp.h"

/**
 * stmmac_adjust_freq
 *
 * @ptp: pointer to ptp_clock_info structure
 * @ppb: desired period change in parts ber billion
 *
 * Description: this function will adjust the frequency of hardware clock.
 */
static int stmmac_adjust_freq(struct ptp_clock_info *ptp, s32 ppb)
{
	struct stmmac_priv *priv =
	    container_of(ptp, struct stmmac_priv, ptp_clock_ops);
	unsigned long flags;
	u32 diff, addend;
	int neg_adj = 0;
	u64 adj;

	if (ppb < 0) {
		neg_adj = 1;
		ppb = -ppb;
	}

	addend = priv->default_addend;
	adj = addend;
	adj *= ppb;
	diff = div_u64(adj, 1000000000ULL);
	addend = neg_adj ? (addend - diff) : (addend + diff);

	spin_lock_irqsave(&priv->ptp_lock, flags);
	stmmac_config_addend(priv, priv->ptpaddr, addend);
	spin_unlock_irqrestore(&priv->ptp_lock, flags);

	return 0;
}

/**
 * stmmac_adjust_time
 *
 * @ptp: pointer to ptp_clock_info structure
 * @delta: desired change in nanoseconds
 *
 * Description: this function will shift/adjust the hardware clock time.
 */
static int stmmac_adjust_time(struct ptp_clock_info *ptp, s64 delta)
{
	struct stmmac_priv *priv =
	    container_of(ptp, struct stmmac_priv, ptp_clock_ops);
	unsigned long flags;
	u32 sec, nsec;
	u32 quotient, reminder;
	int neg_adj = 0;
	bool xmac;

	xmac = priv->plat->has_gmac4 || priv->plat->has_xgmac;

	if (delta < 0) {
		neg_adj = 1;
		delta = -delta;
	}

	quotient = div_u64_rem(delta, 1000000000ULL, &reminder);
	sec = quotient;
	nsec = reminder;

	spin_lock_irqsave(&priv->ptp_lock, flags);
	stmmac_adjust_systime(priv, priv->ptpaddr, sec, nsec, neg_adj, xmac);
	spin_unlock_irqrestore(&priv->ptp_lock, flags);

	return 0;
}

/**
 * stmmac_get_time
 *
 * @ptp: pointer to ptp_clock_info structure
 * @ts: pointer to hold time/result
 *
 * Description: this function will read the current time from the
 * hardware clock and store it in @ts.
 */
static int stmmac_get_time(struct ptp_clock_info *ptp, struct timespec64 *ts)
{
	struct stmmac_priv *priv =
	    container_of(ptp, struct stmmac_priv, ptp_clock_ops);
	unsigned long flags;
	u64 ns = 0;

	spin_lock_irqsave(&priv->ptp_lock, flags);
	stmmac_get_systime(priv, priv->ptpaddr, &ns);
	spin_unlock_irqrestore(&priv->ptp_lock, flags);

	*ts = ns_to_timespec64(ns);

	return 0;
}

/**
 * stmmac_set_time
 *
 * @ptp: pointer to ptp_clock_info structure
 * @ts: time value to set
 *
 * Description: this function will set the current time on the
 * hardware clock.
 */
static int stmmac_set_time(struct ptp_clock_info *ptp,
			   const struct timespec64 *ts)
{
	struct stmmac_priv *priv =
	    container_of(ptp, struct stmmac_priv, ptp_clock_ops);
	unsigned long flags;

	spin_lock_irqsave(&priv->ptp_lock, flags);
	stmmac_init_systime(priv, priv->ptpaddr, ts->tv_sec, ts->tv_nsec);
	spin_unlock_irqrestore(&priv->ptp_lock, flags);

	return 0;
}

static int stmmac_enable(struct ptp_clock_info *ptp,
			 struct ptp_clock_request *rq, int on)
{
	struct stmmac_priv *priv =
	    container_of(ptp, struct stmmac_priv, ptp_clock_ops);
	struct stmmac_pps_cfg *cfg;
	int ret = -EOPNOTSUPP;
	unsigned long flags;

	switch (rq->type) {
	case PTP_CLK_REQ_PEROUT:
		/* Reject requests with unsupported flags */
		if (rq->perout.flags)
			return -EOPNOTSUPP;

		cfg = &priv->pps[rq->perout.index];

		cfg->start.tv_sec = rq->perout.start.sec;
		cfg->start.tv_nsec = rq->perout.start.nsec;
		cfg->period.tv_sec = rq->perout.period.sec;
		cfg->period.tv_nsec = rq->perout.period.nsec;

		spin_lock_irqsave(&priv->ptp_lock, flags);
		ret = stmmac_flex_pps_config(priv, priv->ioaddr,
					     rq->perout.index, cfg, on,
					     priv->sub_second_inc,
					     priv->systime_flags);
		spin_unlock_irqrestore(&priv->ptp_lock, flags);
		break;
	default:
		break;
	}

	return ret;
}

时间使用

上面配置了网卡相关的硬件时间寄存器,那么网卡具有了硬件计时的能力了,在 PTP 对时中如何使用这个能力呢?

发送报文时间

如何获得报文从网卡发送的精确时间?很容易想到的办法就是在触发网卡DMA发送的时刻马上读上面提到的时间寄存器。这种方法可能引入几个问题:若是这个连续操作被中断打断了怎么办?若是网卡队列里还有其他没有发送的其他包,软件触发了DMA发送,但是硬件并没有及时发送出去怎么办?
实际解决办法是:由网卡的硬件实现的,网卡在把网络包发送出去的同时,往指定的内存里保存发送的时间数据,这个指定的内存就是发送描述符。
在这里插入图片描述
代码里这样读取

static inline void dwmac4_get_timestamp(void *desc, u32 ats, u64 *ts)
{
	struct dma_desc *p = (struct dma_desc *)desc;
	u64 ns;

	ns = le32_to_cpu(p->des0);  // 读描述符
	/* convert high/sec time stamp value to nanosecond */
	ns += le32_to_cpu(p->des1) * 1000000000ULL;

	*ts = ns;
}


/* stmmac_get_tx_hwtstamp - get HW TX timestamps
 * @priv: driver private structure
 * @p : descriptor pointer
 * @skb : the socket buffer
 * Description :
 * This function will read timestamp from the descriptor & pass it to stack.
 * and also perform some sanity checks.
 */
static void stmmac_get_tx_hwtstamp(struct stmmac_priv *priv,
				   struct dma_desc *p, struct sk_buff *skb)
{
	struct skb_shared_hwtstamps shhwtstamp;
	bool found = false;
	u64 ns = 0;

	if (!priv->hwts_tx_en)
		return;

	/* exit if skb doesn't support hw tstamp */
	if (likely(!skb || !(skb_shinfo(skb)->tx_flags & SKBTX_IN_PROGRESS)))
		return;

	/* check tx tstamp status */
	if (stmmac_get_tx_timestamp_status(priv, p)) {
		stmmac_get_timestamp(priv, p, priv->adv_ts, &ns);
		found = true;
	} else if (!stmmac_get_mac_tx_timestamp(priv, priv->hw, &ns)) {
		found = true;
	}

	if (found) {
		memset(&shhwtstamp, 0, sizeof(struct skb_shared_hwtstamps));
		shhwtstamp.hwtstamp = ns_to_ktime(ns);

		netdev_dbg(priv->dev, "get valid TX hw timestamp %llu\n", ns);
		/* pass tstamp to stack */
		skb_tstamp_tx(skb, &shhwtstamp); // 保存到协议栈里
	}
}

/**
 * stmmac_tx_clean - to manage th
 * e transmission completion
 * @priv: driver private structure
 * @budget: napi budget limiting this functions packet handling
 * @queue: TX queue index
 * Description: it reclaims the transmit resources after transmission completes.
 */
static int stmmac_tx_clean(struct stmmac_priv *priv, int budget, u32 queue)
{
	struct stmmac_tx_queue *tx_q = &priv->tx_queue[queue];
	unsigned int bytes_compl = 0, pkts_compl = 0;
	unsigned int entry, count = 0;


省略部分代码
		/* Make sure descriptor fields are read after reading
		 * the own bit.
		 */
		dma_rmb();

		/* Just consider the last segment and ...*/
		if (likely(!(status & tx_not_ls))) {
			/* ... verify the status error condition */
			if (unlikely(status & tx_err)) {
				priv->dev->stats.tx_errors++;
			} else {
				priv->dev->stats.tx_packets++;
				priv->xstats.tx_pkt_n++;
			}
			stmmac_get_tx_hwtstamp(priv, p, skb); // 获取时间戳
		}

接收报文时间

与发送的类似,也是保存在描述符里,不赘述。

总结

至此,由网卡硬件实现的硬件精确时间对时的基础已经分析完毕。主要精髓是网卡会自动保存发送和接收的时间到描述符里,这个时刻及其精确,不受代码运行抖动的影响。