root/net/dsa/tag_yt921x.c
// SPDX-License-Identifier: GPL-2.0-or-later
/*
 * Motorcomm YT921x Switch Extended CPU Port Tagging
 *
 * Copyright (c) 2025 David Yang <mmyangfl@gmail.com>
 *
 * +----+----+-------+-----+----+---------
 * | DA | SA | TagET | Tag | ET | Payload ...
 * +----+----+-------+-----+----+---------
 *   6    6      2      6    2       N
 *
 * Tag Ethertype: CPU_TAG_TPID_TPID (default: ETH_P_YT921X = 0x9988)
 *   * Hardcoded for the moment, but still configurable. Discuss it if there
 *     are conflicts somewhere and/or you want to change it for some reason.
 * Tag:
 *   2: VLAN Tag
 *   2:
 *     15b: Rx Port Valid
 *     14b-11b: Rx Port
 *     10b-8b: Tx/Rx Priority
 *     7b: Tx/Rx Code Valid
 *     6b-1b: Tx/Rx Code
 *     0b: ? (unset)
 *   2:
 *     15b: Tx Port(s) Valid
 *     10b-0b: Tx Port(s) Mask
 */

#include <linux/etherdevice.h>

#include "tag.h"

#define YT921X_TAG_NAME "yt921x"

#define YT921X_TAG_LEN  8

#define YT921X_TAG_PORT_EN              BIT(15)
#define YT921X_TAG_RX_PORT_M            GENMASK(14, 11)
#define YT921X_TAG_PRIO_M               GENMASK(10, 8)
#define  YT921X_TAG_PRIO(x)                     FIELD_PREP(YT921X_TAG_PRIO_M, (x))
#define YT921X_TAG_CODE_EN              BIT(7)
#define YT921X_TAG_CODE_M               GENMASK(6, 1)
#define  YT921X_TAG_CODE(x)                     FIELD_PREP(YT921X_TAG_CODE_M, (x))
#define YT921X_TAG_TX_PORTS_M           GENMASK(10, 0)
#define  YT921X_TAG_TX_PORTS(x)                 FIELD_PREP(YT921X_TAG_TX_PORTS_M, (x))

/* Incomplete. Some are configurable via RMA_CTRL_CPU_CODE, the meaning of
 * others remains unknown.
 */
enum yt921x_tag_code {
        YT921X_TAG_CODE_FORWARD = 0,
        YT921X_TAG_CODE_UNK_UCAST = 0x19,
        YT921X_TAG_CODE_UNK_MCAST = 0x1a,
        YT921X_TAG_CODE_PORT_COPY = 0x1b,
        YT921X_TAG_CODE_FDB_COPY = 0x1c,
};

static struct sk_buff *
yt921x_tag_xmit(struct sk_buff *skb, struct net_device *netdev)
{
        __be16 *tag;
        u16 ctrl;

        skb_push(skb, YT921X_TAG_LEN);
        dsa_alloc_etype_header(skb, YT921X_TAG_LEN);

        tag = dsa_etype_header_pos_tx(skb);

        tag[0] = htons(ETH_P_YT921X);
        /* VLAN tag unrelated when TX */
        tag[1] = 0;
        ctrl = YT921X_TAG_CODE(YT921X_TAG_CODE_FORWARD) | YT921X_TAG_CODE_EN |
               YT921X_TAG_PRIO(skb->priority);
        tag[2] = htons(ctrl);
        ctrl = YT921X_TAG_TX_PORTS(dsa_xmit_port_mask(skb, netdev)) |
               YT921X_TAG_PORT_EN;
        tag[3] = htons(ctrl);

        return skb;
}

static struct sk_buff *
yt921x_tag_rcv(struct sk_buff *skb, struct net_device *netdev)
{
        unsigned int port;
        __be16 *tag;
        u16 rx;

        if (unlikely(!pskb_may_pull(skb, YT921X_TAG_LEN)))
                return NULL;

        tag = dsa_etype_header_pos_rx(skb);

        if (unlikely(tag[0] != htons(ETH_P_YT921X))) {
                dev_warn_ratelimited(&netdev->dev,
                                     "Unexpected EtherType 0x%04x\n",
                                     ntohs(tag[0]));
                return NULL;
        }

        /* Locate which port this is coming from */
        rx = ntohs(tag[2]);
        if (unlikely((rx & YT921X_TAG_PORT_EN) == 0)) {
                dev_warn_ratelimited(&netdev->dev,
                                     "Unexpected rx tag 0x%04x\n", rx);
                return NULL;
        }

        port = FIELD_GET(YT921X_TAG_RX_PORT_M, rx);
        skb->dev = dsa_conduit_find_user(netdev, 0, port);
        if (unlikely(!skb->dev)) {
                dev_warn_ratelimited(&netdev->dev,
                                     "Couldn't decode source port %u\n", port);
                return NULL;
        }

        skb->priority = FIELD_GET(YT921X_TAG_PRIO_M, rx);

        if (!(rx & YT921X_TAG_CODE_EN)) {
                dev_warn_ratelimited(&netdev->dev,
                                     "Tag code not enabled in rx packet\n");
        } else {
                u16 code = FIELD_GET(YT921X_TAG_CODE_M, rx);

                switch (code) {
                case YT921X_TAG_CODE_FORWARD:
                case YT921X_TAG_CODE_PORT_COPY:
                case YT921X_TAG_CODE_FDB_COPY:
                        /* Already forwarded by hardware */
                        dsa_default_offload_fwd_mark(skb);
                        break;
                case YT921X_TAG_CODE_UNK_UCAST:
                case YT921X_TAG_CODE_UNK_MCAST:
                        /* NOTE: hardware doesn't distinguish between TRAP (copy
                         * to CPU only) and COPY (forward and copy to CPU). In
                         * order to perform a soft switch, NEVER use COPY action
                         * in the switch driver.
                         */
                        break;
                default:
                        dev_warn_ratelimited(&netdev->dev,
                                             "Unknown code 0x%02x\n", code);
                        break;
                }
        }

        /* Remove YT921x tag and update checksum */
        skb_pull_rcsum(skb, YT921X_TAG_LEN);
        dsa_strip_etype_header(skb, YT921X_TAG_LEN);

        return skb;
}

static const struct dsa_device_ops yt921x_netdev_ops = {
        .name   = YT921X_TAG_NAME,
        .proto  = DSA_TAG_PROTO_YT921X,
        .xmit   = yt921x_tag_xmit,
        .rcv    = yt921x_tag_rcv,
        .needed_headroom = YT921X_TAG_LEN,
};

MODULE_DESCRIPTION("DSA tag driver for Motorcomm YT921x switches");
MODULE_LICENSE("GPL");
MODULE_ALIAS_DSA_TAG_DRIVER(DSA_TAG_PROTO_YT921X, YT921X_TAG_NAME);

module_dsa_tag_driver(yt921x_netdev_ops);