| // 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 kernel_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 kernel_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); | |
| 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 kernel_hwtstamp_config *config, | |
| struct netlink_ext_ack *extack) | |
| { | |
| struct hellcreek *hellcreek = ds->priv; | |
| struct hellcreek_port_hwtstamp *ps; | |
| int err; | |
| ps = &hellcreek->ports[port].port_hwtstamp; | |
| err = hellcreek_set_hwtstamp_config(hellcreek, port, config); | |
| if (err) | |
| return err; | |
| /* Save the chosen configuration to be returned later */ | |
| ps->tstamp_config = *config; | |
| return 0; | |
| } | |
| int hellcreek_port_hwtstamp_get(struct dsa_switch *ds, int port, | |
| struct kernel_hwtstamp_config *config) | |
| { | |
| struct hellcreek *hellcreek = ds->priv; | |
| struct hellcreek_port_hwtstamp *ps; | |
| ps = &hellcreek->ports[port].port_hwtstamp; | |
| *config = ps->tstamp_config; | |
| return 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; | |
| /* Construct Rx timestamps for all received PTP packets. */ | |
| __skb_queue_head_init(&received); | |
| spin_lock_irqsave(&rxq->lock, flags); | |
| 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(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 */ | |
| } |