blob: 40b41c794dfacbb45a5ebba88714009e3452ae67 [file] [log] [blame]
// SPDX-License-Identifier: (GPL-2.0 OR MIT)
/*
* DSA driver for:
* Hirschmann Hellcreek TSN switch.
*
* Copyright (C) 2019,2020 Hochschule Offenburg
* Copyright (C) 2019,2020 Linutronix GmbH
* Authors: Kamil Alkhouri <kamil.alkhouri@hs-offenburg.de>
* Kurt Kanzenbach <kurt@linutronix.de>
*/
#include <linux/ptp_classify.h>
#include "hellcreek.h"
#include "hellcreek_hwtstamp.h"
#include "hellcreek_ptp.h"
int hellcreek_get_ts_info(struct dsa_switch *ds, int port,
struct ethtool_ts_info *info)
{
struct hellcreek *hellcreek = ds->priv;
info->phc_index = hellcreek->ptp_clock ?
ptp_clock_index(hellcreek->ptp_clock) : -1;
info->so_timestamping = SOF_TIMESTAMPING_TX_HARDWARE |
SOF_TIMESTAMPING_RX_HARDWARE |
SOF_TIMESTAMPING_RAW_HARDWARE;
/* enabled tx timestamping */
info->tx_types = BIT(HWTSTAMP_TX_ON);
/* L2 & L4 PTPv2 event rx messages are timestamped */
info->rx_filters = BIT(HWTSTAMP_FILTER_PTP_V2_EVENT);
return 0;
}
/* Enabling/disabling TX and RX HW timestamping for different PTP messages is
* not available in the switch. Thus, this function only serves as a check if
* the user requested what is actually available or not
*/
static int hellcreek_set_hwtstamp_config(struct hellcreek *hellcreek, int port,
struct hwtstamp_config *config)
{
struct hellcreek_port_hwtstamp *ps =
&hellcreek->ports[port].port_hwtstamp;
bool tx_tstamp_enable = false;
bool rx_tstamp_enable = false;
/* Interaction with the timestamp hardware is prevented here. It is
* enabled when this config function ends successfully
*/
clear_bit_unlock(HELLCREEK_HWTSTAMP_ENABLED, &ps->state);
/* Reserved for future extensions */
if (config->flags)
return -EINVAL;
switch (config->tx_type) {
case HWTSTAMP_TX_ON:
tx_tstamp_enable = true;
break;
/* TX HW timestamping can't be disabled on the switch */
case HWTSTAMP_TX_OFF:
config->tx_type = HWTSTAMP_TX_ON;
break;
default:
return -ERANGE;
}
switch (config->rx_filter) {
/* RX HW timestamping can't be disabled on the switch */
case HWTSTAMP_FILTER_NONE:
config->rx_filter = HWTSTAMP_FILTER_PTP_V2_EVENT;
break;
case HWTSTAMP_FILTER_PTP_V2_L4_EVENT:
case HWTSTAMP_FILTER_PTP_V2_L4_SYNC:
case HWTSTAMP_FILTER_PTP_V2_L4_DELAY_REQ:
case HWTSTAMP_FILTER_PTP_V2_L2_EVENT:
case HWTSTAMP_FILTER_PTP_V2_L2_SYNC:
case HWTSTAMP_FILTER_PTP_V2_L2_DELAY_REQ:
case HWTSTAMP_FILTER_PTP_V2_EVENT:
case HWTSTAMP_FILTER_PTP_V2_SYNC:
case HWTSTAMP_FILTER_PTP_V2_DELAY_REQ:
config->rx_filter = HWTSTAMP_FILTER_PTP_V2_EVENT;
rx_tstamp_enable = true;
break;
/* RX HW timestamping can't be enabled for all messages on the switch */
case HWTSTAMP_FILTER_ALL:
config->rx_filter = HWTSTAMP_FILTER_PTP_V2_EVENT;
break;
default:
return -ERANGE;
}
if (!tx_tstamp_enable)
return -ERANGE;
if (!rx_tstamp_enable)
return -ERANGE;
/* If this point is reached, then the requested hwtstamp config is
* compatible with the hwtstamp offered by the switch. Therefore,
* enable the interaction with the HW timestamping
*/
set_bit(HELLCREEK_HWTSTAMP_ENABLED, &ps->state);
return 0;
}
int hellcreek_port_hwtstamp_set(struct dsa_switch *ds, int port,
struct ifreq *ifr)
{
struct hellcreek *hellcreek = ds->priv;
struct hellcreek_port_hwtstamp *ps;
struct hwtstamp_config config;
int err;
ps = &hellcreek->ports[port].port_hwtstamp;
if (copy_from_user(&config, ifr->ifr_data, sizeof(config)))
return -EFAULT;
err = hellcreek_set_hwtstamp_config(hellcreek, port, &config);
if (err)
return err;
/* Save the chosen configuration to be returned later */
memcpy(&ps->tstamp_config, &config, sizeof(config));
return copy_to_user(ifr->ifr_data, &config, sizeof(config)) ?
-EFAULT : 0;
}
int hellcreek_port_hwtstamp_get(struct dsa_switch *ds, int port,
struct ifreq *ifr)
{
struct hellcreek *hellcreek = ds->priv;
struct hellcreek_port_hwtstamp *ps;
struct hwtstamp_config *config;
ps = &hellcreek->ports[port].port_hwtstamp;
config = &ps->tstamp_config;
return copy_to_user(ifr->ifr_data, config, sizeof(*config)) ?
-EFAULT : 0;
}
/* Returns a pointer to the PTP header if the caller should time stamp, or NULL
* if the caller should not.
*/
static struct ptp_header *hellcreek_should_tstamp(struct hellcreek *hellcreek,
int port, struct sk_buff *skb,
unsigned int type)
{
struct hellcreek_port_hwtstamp *ps =
&hellcreek->ports[port].port_hwtstamp;
struct ptp_header *hdr;
hdr = ptp_parse_header(skb, type);
if (!hdr)
return NULL;
if (!test_bit(HELLCREEK_HWTSTAMP_ENABLED, &ps->state))
return NULL;
return hdr;
}
static u64 hellcreek_get_reserved_field(const struct ptp_header *hdr)
{
return be32_to_cpu(hdr->reserved2);
}
static void hellcreek_clear_reserved_field(struct ptp_header *hdr)
{
hdr->reserved2 = 0;
}
static int hellcreek_ptp_hwtstamp_available(struct hellcreek *hellcreek,
unsigned int ts_reg)
{
u16 status;
status = hellcreek_ptp_read(hellcreek, ts_reg);
if (status & PR_TS_STATUS_TS_LOST)
dev_err(hellcreek->dev,
"Tx time stamp lost! This should never happen!\n");
/* If hwtstamp is not available, this means the previous hwtstamp was
* successfully read, and the one we need is not yet available
*/
return (status & PR_TS_STATUS_TS_AVAIL) ? 1 : 0;
}
/* Get nanoseconds timestamp from timestamping unit */
static u64 hellcreek_ptp_hwtstamp_read(struct hellcreek *hellcreek,
unsigned int ts_reg)
{
u16 nsl, nsh;
nsh = hellcreek_ptp_read(hellcreek, ts_reg);
nsh = hellcreek_ptp_read(hellcreek, ts_reg);
nsh = hellcreek_ptp_read(hellcreek, ts_reg);
nsh = hellcreek_ptp_read(hellcreek, ts_reg);
nsl = hellcreek_ptp_read(hellcreek, ts_reg);
return (u64)nsl | ((u64)nsh << 16);
}
static int hellcreek_txtstamp_work(struct hellcreek *hellcreek,
struct hellcreek_port_hwtstamp *ps, int port)
{
struct skb_shared_hwtstamps shhwtstamps;
unsigned int status_reg, data_reg;
struct sk_buff *tmp_skb;
int ts_status;
u64 ns = 0;
if (!ps->tx_skb)
return 0;
switch (port) {
case 2:
status_reg = PR_TS_TX_P1_STATUS_C;
data_reg = PR_TS_TX_P1_DATA_C;
break;
case 3:
status_reg = PR_TS_TX_P2_STATUS_C;
data_reg = PR_TS_TX_P2_DATA_C;
break;
default:
dev_err(hellcreek->dev, "Wrong port for timestamping!\n");
return 0;
}
ts_status = hellcreek_ptp_hwtstamp_available(hellcreek, status_reg);
/* Not available yet? */
if (ts_status == 0) {
/* Check whether the operation of reading the tx timestamp has
* exceeded its allowed period
*/
if (time_is_before_jiffies(ps->tx_tstamp_start +
TX_TSTAMP_TIMEOUT)) {
dev_err(hellcreek->dev,
"Timeout while waiting for Tx timestamp!\n");
goto free_and_clear_skb;
}
/* The timestamp should be available quickly, while getting it
* in high priority. Restart the work
*/
return 1;
}
mutex_lock(&hellcreek->ptp_lock);
ns = hellcreek_ptp_hwtstamp_read(hellcreek, data_reg);
ns += hellcreek_ptp_gettime_seconds(hellcreek, ns);
mutex_unlock(&hellcreek->ptp_lock);
/* Now we have the timestamp in nanoseconds, store it in the correct
* structure in order to send it to the user
*/
memset(&shhwtstamps, 0, sizeof(shhwtstamps));
shhwtstamps.hwtstamp = ns_to_ktime(ns);
tmp_skb = ps->tx_skb;
ps->tx_skb = NULL;
/* skb_complete_tx_timestamp() frees up the client to make another
* timestampable transmit. We have to be ready for it by clearing the
* ps->tx_skb "flag" beforehand
*/
clear_bit_unlock(HELLCREEK_HWTSTAMP_TX_IN_PROGRESS, &ps->state);
/* Deliver a clone of the original outgoing tx_skb with tx hwtstamp */
skb_complete_tx_timestamp(tmp_skb, &shhwtstamps);
return 0;
free_and_clear_skb:
dev_kfree_skb_any(ps->tx_skb);
ps->tx_skb = NULL;
clear_bit_unlock(HELLCREEK_HWTSTAMP_TX_IN_PROGRESS, &ps->state);
return 0;
}
static void hellcreek_get_rxts(struct hellcreek *hellcreek,
struct hellcreek_port_hwtstamp *ps,
struct sk_buff *skb, struct sk_buff_head *rxq,
int port)
{
struct skb_shared_hwtstamps *shwt;
struct sk_buff_head received;
unsigned long flags;
/* The latched timestamp belongs to one of the received frames. */
__skb_queue_head_init(&received);
/* Lock & disable interrupts */
spin_lock_irqsave(&rxq->lock, flags);
/* Add the reception queue "rxq" to the "received" queue an reintialize
* "rxq". From now on, we deal with "received" not with "rxq"
*/
skb_queue_splice_tail_init(rxq, &received);
spin_unlock_irqrestore(&rxq->lock, flags);
for (; skb; skb = __skb_dequeue(&received)) {
struct ptp_header *hdr;
unsigned int type;
u64 ns;
/* Get nanoseconds from ptp packet */
type = SKB_PTP_TYPE(skb);
hdr = ptp_parse_header(skb, type);
ns = hellcreek_get_reserved_field(hdr);
hellcreek_clear_reserved_field(hdr);
/* Add seconds part */
mutex_lock(&hellcreek->ptp_lock);
ns += hellcreek_ptp_gettime_seconds(hellcreek, ns);
mutex_unlock(&hellcreek->ptp_lock);
/* Save time stamp */
shwt = skb_hwtstamps(skb);
memset(shwt, 0, sizeof(*shwt));
shwt->hwtstamp = ns_to_ktime(ns);
netif_rx_ni(skb);
}
}
static void hellcreek_rxtstamp_work(struct hellcreek *hellcreek,
struct hellcreek_port_hwtstamp *ps,
int port)
{
struct sk_buff *skb;
skb = skb_dequeue(&ps->rx_queue);
if (skb)
hellcreek_get_rxts(hellcreek, ps, skb, &ps->rx_queue, port);
}
long hellcreek_hwtstamp_work(struct ptp_clock_info *ptp)
{
struct hellcreek *hellcreek = ptp_to_hellcreek(ptp);
struct dsa_switch *ds = hellcreek->ds;
int i, restart = 0;
for (i = 0; i < ds->num_ports; i++) {
struct hellcreek_port_hwtstamp *ps;
if (!dsa_is_user_port(ds, i))
continue;
ps = &hellcreek->ports[i].port_hwtstamp;
if (test_bit(HELLCREEK_HWTSTAMP_TX_IN_PROGRESS, &ps->state))
restart |= hellcreek_txtstamp_work(hellcreek, ps, i);
hellcreek_rxtstamp_work(hellcreek, ps, i);
}
return restart ? 1 : -1;
}
void hellcreek_port_txtstamp(struct dsa_switch *ds, int port,
struct sk_buff *skb)
{
struct hellcreek *hellcreek = ds->priv;
struct hellcreek_port_hwtstamp *ps;
struct ptp_header *hdr;
struct sk_buff *clone;
unsigned int type;
ps = &hellcreek->ports[port].port_hwtstamp;
type = ptp_classify_raw(skb);
if (type == PTP_CLASS_NONE)
return;
/* Make sure the message is a PTP message that needs to be timestamped
* and the interaction with the HW timestamping is enabled. If not, stop
* here
*/
hdr = hellcreek_should_tstamp(hellcreek, port, skb, type);
if (!hdr)
return;
clone = skb_clone_sk(skb);
if (!clone)
return;
if (test_and_set_bit_lock(HELLCREEK_HWTSTAMP_TX_IN_PROGRESS,
&ps->state)) {
kfree_skb(clone);
return;
}
ps->tx_skb = clone;
/* store the number of ticks occurred since system start-up till this
* moment
*/
ps->tx_tstamp_start = jiffies;
ptp_schedule_worker(hellcreek->ptp_clock, 0);
}
bool hellcreek_port_rxtstamp(struct dsa_switch *ds, int port,
struct sk_buff *skb, unsigned int type)
{
struct hellcreek *hellcreek = ds->priv;
struct hellcreek_port_hwtstamp *ps;
struct ptp_header *hdr;
ps = &hellcreek->ports[port].port_hwtstamp;
/* This check only fails if the user did not initialize hardware
* timestamping beforehand.
*/
if (ps->tstamp_config.rx_filter != HWTSTAMP_FILTER_PTP_V2_EVENT)
return false;
/* Make sure the message is a PTP message that needs to be timestamped
* and the interaction with the HW timestamping is enabled. If not, stop
* here
*/
hdr = hellcreek_should_tstamp(hellcreek, port, skb, type);
if (!hdr)
return false;
SKB_PTP_TYPE(skb) = type;
skb_queue_tail(&ps->rx_queue, skb);
ptp_schedule_worker(hellcreek->ptp_clock, 0);
return true;
}
static void hellcreek_hwtstamp_port_setup(struct hellcreek *hellcreek, int port)
{
struct hellcreek_port_hwtstamp *ps =
&hellcreek->ports[port].port_hwtstamp;
skb_queue_head_init(&ps->rx_queue);
}
int hellcreek_hwtstamp_setup(struct hellcreek *hellcreek)
{
struct dsa_switch *ds = hellcreek->ds;
int i;
/* Initialize timestamping ports. */
for (i = 0; i < ds->num_ports; ++i) {
if (!dsa_is_user_port(ds, i))
continue;
hellcreek_hwtstamp_port_setup(hellcreek, i);
}
/* Select the synchronized clock as the source timekeeper for the
* timestamps and enable inline timestamping.
*/
hellcreek_ptp_write(hellcreek, PR_SETTINGS_C_TS_SRC_TK_MASK |
PR_SETTINGS_C_RES3TS,
PR_SETTINGS_C);
return 0;
}
void hellcreek_hwtstamp_free(struct hellcreek *hellcreek)
{
/* Nothing todo */
}