// SPDX-License-Identifier: GPL-2.0+
/*
* TI Common Platform Time Sync
*
* Copyright (C) 2012 Richard Cochran <richardcochran@gmail.com>
*
*/
#include <linux/clk-provider.h>
#include <linux/err.h>
#include <linux/if.h>
#include <linux/hrtimer.h>
#include <linux/module.h>
#include <linux/net_tstamp.h>
#include <linux/ptp_classify.h>
#include <linux/time.h>
#include <linux/uaccess.h>
#include <linux/workqueue.h>
#include <linux/if_ether.h>
#include <linux/if_vlan.h>
#include "cpts.h"
#define CPTS_SKB_TX_WORK_TIMEOUT 1 /* jiffies */
#define CPTS_SKB_RX_TX_TMO 100 /*ms */
#define CPTS_EVENT_RX_TX_TIMEOUT (100) /* ms */
struct cpts_skb_cb_data {
u32 skb_mtype_seqid;
unsigned long tmo;
};
#define cpts_read32(c, r) readl_relaxed(&c->reg->r)
#define cpts_write32(c, v, r) writel_relaxed(v, &c->reg->r)
static int cpts_event_port(struct cpts_event *event)
{
return (event->high >> PORT_NUMBER_SHIFT) & PORT_NUMBER_MASK;
}
static int event_expired(struct cpts_event *event)
{
return time_after(jiffies, event->tmo);
}
static int event_type(struct cpts_event *event)
{
return (event->high >> EVENT_TYPE_SHIFT) & EVENT_TYPE_MASK;
}
static int cpts_fifo_pop(struct cpts *cpts, u32 *high, u32 *low)
{
u32 r = cpts_read32(cpts, intstat_raw);
if (r & TS_PEND_RAW) {
*high = cpts_read32(cpts, event_high);
*low = cpts_read32(cpts, event_low);
cpts_write32(cpts, EVENT_POP, event_pop);
return 0;
}
return -1;
}
static int cpts_purge_events(struct cpts *cpts)
{
struct list_head *this, *next;
struct cpts_event *event;
int removed = 0;
list_for_each_safe(this, next, &cpts->events) {
event = list_entry(this, struct cpts_event, list);
if (event_expired(event)) {
list_del_init(&event->list);
list_add(&event->list, &cpts->pool);
++removed;
}
}
if (removed)
dev_dbg(cpts->dev, "cpts: event pool cleaned up %d\n", removed);
return removed ? 0 : -1;
}
static void cpts_purge_txq(struct cpts *cpts)
{
struct cpts_skb_cb_data *skb_cb;
struct sk_buff *skb, *tmp;
int removed = 0;
skb_queue_walk_safe(&cpts->txq, skb, tmp) {
skb_cb = (struct cpts_skb_cb_data *)skb->cb;
if (time_after(jiffies, skb_cb->tmo)) {
__skb_unlink(skb, &cpts->txq);
dev_consume_skb_any(skb);
++removed;
}
}
if (removed)
dev_dbg(cpts->dev, "txq cleaned up %d\n", removed);
}
/*
* Returns zero if matching event type was found.
*/
static int cpts_fifo_read(struct cpts *cpts, int match)
{
struct ptp_clock_event pevent;
bool need_schedule = false;
struct cpts_event *event;
unsigned long flags;
int i, type = -1;
u32 hi, lo;
spin_lock_irqsave(&cpts->lock, flags);
for (i = 0; i < CPTS_FIFO_DEPTH; i++) {
if (cpts_fifo_pop(cpts, &hi, &lo))
break;
if (list_empty(&cpts->pool) && cpts_purge_events(cpts)) {
dev_warn(cpts->dev, "cpts: event pool empty\n");
break;
}
event = list_first_entry(&cpts->pool, struct cpts_event, list);
event->high = hi;
event->low = lo;
event->timestamp = timecounter_cyc2time(&cpts->tc, event->low);
type = event_type(event);
dev_dbg(cpts->dev, "CPTS_EV: %d high:%08X low:%08x\n",
type, event->high, event->low);
switch (type) {
case CPTS_EV_PUSH:
WRITE_ONCE(cpts->cur_timestamp, lo);
timecounter_read(&cpts->tc);
if (cpts->mult_new) {
cpts->cc.mult = cpts->mult_new;
cpts->mult_new = 0;
}
if (!cpts->irq_poll)
complete(&cpts->ts_push_complete);
break;
case CPTS_EV_TX:
case CPTS_EV_RX:
event->tmo = jiffies +
msecs_to_jiffies(CPTS_EVENT_RX_TX_TIMEOUT);
list_del_init(&event->list);
list_add_tail(&event->list, &cpts->events);
need_schedule = true;
break;
case CPTS_EV_ROLL:
case CPTS_EV_HALF:
break;
case CPTS_EV_HW:
pevent.timestamp = event->timestamp;
pevent.type = PTP_CLOCK_EXTTS;
pevent.index = cpts_event_port(event) - 1;
ptp_clock_event(cpts->clock, &pevent);
break;
default:
dev_err(cpts->dev, "cpts: unknown event type\n");
break;
}
if (type == match)
break;
}
spin_unlock_irqrestore(&cpts->lock, flags);