root/drivers/net/ethernet/microchip/sparx5/sparx5_mirror.c
// SPDX-License-Identifier: GPL-2.0+
/* Microchip Sparx5 Switch driver
 *
 * Copyright (c) 2024 Microchip Technology Inc. and its subsidiaries.
 */

#include "sparx5_main.h"
#include "sparx5_main_regs.h"
#include "sparx5_tc.h"

#define SPX5_MIRROR_PROBE_MAX 3
#define SPX5_MIRROR_DISABLED 0
#define SPX5_MIRROR_EGRESS 1
#define SPX5_MIRROR_INGRESS 2
#define SPX5_QFWD_MP_OFFSET 9 /* Mirror port offset in the QFWD register */

/* Convert from bool ingress/egress to mirror direction */
static u32 sparx5_mirror_to_dir(bool ingress)
{
        return ingress ? SPX5_MIRROR_INGRESS : SPX5_MIRROR_EGRESS;
}

/* Get ports belonging to this mirror */
static u64 sparx5_mirror_port_get(struct sparx5 *sparx5, u32 idx)
{
        u64 val;

        val = spx5_rd(sparx5, ANA_AC_PROBE_PORT_CFG(idx));

        if (is_sparx5(sparx5))
                val |= (u64)spx5_rd(sparx5, ANA_AC_PROBE_PORT_CFG1(idx)) << 32;

        return val;
}

/* Add port to mirror (only front ports) */
static void sparx5_mirror_port_add(struct sparx5 *sparx5, u32 idx, u32 portno)
{
        u64 reg = portno;
        u32 val;

        val = BIT(do_div(reg, 32));

        if (reg == 0)
                return spx5_rmw(val, val, sparx5, ANA_AC_PROBE_PORT_CFG(idx));
        else
                return spx5_rmw(val, val, sparx5, ANA_AC_PROBE_PORT_CFG1(idx));
}

/* Delete port from mirror (only front ports) */
static void sparx5_mirror_port_del(struct sparx5 *sparx5, u32 idx, u32 portno)
{
        u64 reg = portno;
        u32 val;

        val = BIT(do_div(reg, 32));

        if (reg == 0)
                return spx5_rmw(0, val, sparx5, ANA_AC_PROBE_PORT_CFG(idx));
        else
                return spx5_rmw(0, val, sparx5, ANA_AC_PROBE_PORT_CFG1(idx));
}

/* Check if mirror contains port */
static bool sparx5_mirror_contains(struct sparx5 *sparx5, u32 idx, u32 portno)
{
        return (sparx5_mirror_port_get(sparx5, idx) & BIT_ULL(portno)) != 0;
}

/* Check if mirror is empty */
static bool sparx5_mirror_is_empty(struct sparx5 *sparx5, u32 idx)
{
        return sparx5_mirror_port_get(sparx5, idx) == 0;
}

/* Get direction of mirror */
static u32 sparx5_mirror_dir_get(struct sparx5 *sparx5, u32 idx)
{
        u32 val = spx5_rd(sparx5, ANA_AC_PROBE_CFG(idx));

        return ANA_AC_PROBE_CFG_PROBE_DIRECTION_GET(val);
}

/* Set direction of mirror */
static void sparx5_mirror_dir_set(struct sparx5 *sparx5, u32 idx, u32 dir)
{
        spx5_rmw(ANA_AC_PROBE_CFG_PROBE_DIRECTION_SET(dir),
                 ANA_AC_PROBE_CFG_PROBE_DIRECTION, sparx5,
                 ANA_AC_PROBE_CFG(idx));
}

/* Set the monitor port for this mirror */
static void sparx5_mirror_monitor_set(struct sparx5 *sparx5, u32 idx,
                                      u32 portno)
{
        spx5_rmw(QFWD_FRAME_COPY_CFG_FRMC_PORT_VAL_SET(portno),
                 QFWD_FRAME_COPY_CFG_FRMC_PORT_VAL, sparx5,
                 QFWD_FRAME_COPY_CFG(idx + SPX5_QFWD_MP_OFFSET));
}

/* Get the monitor port of this mirror */
static u32 sparx5_mirror_monitor_get(struct sparx5 *sparx5, u32 idx)
{
        u32 val = spx5_rd(sparx5,
                          QFWD_FRAME_COPY_CFG(idx + SPX5_QFWD_MP_OFFSET));

        return QFWD_FRAME_COPY_CFG_FRMC_PORT_VAL_GET(val);
}

/* Check if port is the monitor port of this mirror */
static bool sparx5_mirror_has_monitor(struct sparx5 *sparx5, u32 idx,
                                      u32 portno)
{
        return sparx5_mirror_monitor_get(sparx5, idx) == portno;
}

/* Get a suitable mirror for this port */
static int sparx5_mirror_get(struct sparx5_port *sport,
                             struct sparx5_port *mport, u32 dir, u32 *idx)
{
        struct sparx5 *sparx5 = sport->sparx5;
        u32 i;

        /* Check if this port is already used as a monitor port */
        for (i = 0; i < SPX5_MIRROR_PROBE_MAX; i++)
                if (sparx5_mirror_has_monitor(sparx5, i, sport->portno))
                        return -EINVAL;

        /* Check if existing mirror can be reused
         * (same direction and monitor port).
         */
        for (i = 0; i < SPX5_MIRROR_PROBE_MAX; i++) {
                if (sparx5_mirror_dir_get(sparx5, i) == dir &&
                    sparx5_mirror_has_monitor(sparx5, i, mport->portno)) {
                        *idx = i;
                        return 0;
                }
        }

        /* Return free mirror */
        for (i = 0; i < SPX5_MIRROR_PROBE_MAX; i++) {
                if (sparx5_mirror_is_empty(sparx5, i)) {
                        *idx = i;
                        return 0;
                }
        }

        return -ENOENT;
}

int sparx5_mirror_add(struct sparx5_mall_entry *entry)
{
        u32 mirror_idx, dir = sparx5_mirror_to_dir(entry->ingress);
        struct sparx5_port *sport, *mport;
        struct sparx5 *sparx5;
        int err;

        /* Source port */
        sport = entry->port;
        /* monitor port */
        mport = entry->mirror.port;
        sparx5 = sport->sparx5;

        if (sport->portno == mport->portno)
                return -EINVAL;

        err = sparx5_mirror_get(sport, mport, dir, &mirror_idx);
        if (err)
                return err;

        if (sparx5_mirror_contains(sparx5, mirror_idx, sport->portno))
                return -EEXIST;

        /* Add port to mirror */
        sparx5_mirror_port_add(sparx5, mirror_idx, sport->portno);

        /* Set direction of mirror */
        sparx5_mirror_dir_set(sparx5, mirror_idx, dir);

        /* Set monitor port for mirror */
        sparx5_mirror_monitor_set(sparx5, mirror_idx, mport->portno);

        entry->mirror.idx = mirror_idx;

        return 0;
}

void sparx5_mirror_del(struct sparx5_mall_entry *entry)
{
        struct sparx5_port *port = entry->port;
        struct sparx5 *sparx5 = port->sparx5;
        u32 mirror_idx = entry->mirror.idx;

        sparx5_mirror_port_del(sparx5, mirror_idx, port->portno);
        if (!sparx5_mirror_is_empty(sparx5, mirror_idx))
                return;

        sparx5_mirror_dir_set(sparx5, mirror_idx, SPX5_MIRROR_DISABLED);

        sparx5_mirror_monitor_set(sparx5,
                                  mirror_idx,
                                  sparx5->data->consts->n_ports);
}

void sparx5_mirror_stats(struct sparx5_mall_entry *entry,
                         struct flow_stats *fstats)
{
        struct sparx5_port *port = entry->port;
        struct rtnl_link_stats64 new_stats;
        struct flow_stats *old_stats;

        old_stats = &entry->port->mirror_stats;
        sparx5_get_stats64(port->ndev, &new_stats);

        if (entry->ingress) {
                flow_stats_update(fstats,
                                  new_stats.rx_bytes - old_stats->bytes,
                                  new_stats.rx_packets - old_stats->pkts,
                                  new_stats.rx_dropped - old_stats->drops,
                                  old_stats->lastused,
                                  FLOW_ACTION_HW_STATS_IMMEDIATE);

                old_stats->bytes = new_stats.rx_bytes;
                old_stats->pkts = new_stats.rx_packets;
                old_stats->drops = new_stats.rx_dropped;
                old_stats->lastused = jiffies;
        } else {
                flow_stats_update(fstats,
                                  new_stats.tx_bytes - old_stats->bytes,
                                  new_stats.tx_packets - old_stats->pkts,
                                  new_stats.tx_dropped - old_stats->drops,
                                  old_stats->lastused,
                                  FLOW_ACTION_HW_STATS_IMMEDIATE);

                old_stats->bytes = new_stats.tx_bytes;
                old_stats->pkts = new_stats.tx_packets;
                old_stats->drops = new_stats.tx_dropped;
                old_stats->lastused = jiffies;
        }
}