前言
在很多应用场景有精确对时的需求,例如车载网络,音视频流,工业网络。本文档将会阐述对时的硬件需求。
协议
流行的协议为 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); // 获取时间戳
}
接收报文时间
与发送的类似,也是保存在描述符里,不赘述。
总结
至此,由网卡硬件实现的硬件精确时间对时的基础已经分析完毕。主要精髓是网卡会自动保存发送和接收的时间到描述符里,这个时刻及其精确,不受代码运行抖动的影响。