root/drivers/net/ethernet/renesas/rswitch_l2.c
// SPDX-License-Identifier: GPL-2.0
/* Renesas Ethernet Switch device driver
 *
 * Copyright (C) 2025 - 2026 Renesas Electronics Corporation
 */

#include <linux/err.h>
#include <linux/etherdevice.h>
#include <linux/if_bridge.h>
#include <linux/kernel.h>
#include <net/switchdev.h>

#include "rswitch.h"
#include "rswitch_l2.h"

static bool rdev_for_l2_offload(struct rswitch_device *rdev)
{
        return rdev->priv->offload_brdev &&
               rdev->brdev == rdev->priv->offload_brdev &&
               (test_bit(rdev->port, rdev->priv->opened_ports));
}

static void rswitch_change_l2_hw_offloading(struct rswitch_device *rdev,
                                            bool start, bool learning)
{
        u32 bits = learning ? FWPC0_MACSSA | FWPC0_MACHLA | FWPC0_MACHMA : FWPC0_MACDSA;
        u32 clear = start ? 0 : bits;
        u32 set = start ? bits : 0;

        if ((learning && rdev->learning_offloaded == start) ||
            (!learning && rdev->forwarding_offloaded == start))
                return;

        rswitch_modify(rdev->priv->addr, FWPC0(rdev->port), clear, set);

        if (learning)
                rdev->learning_offloaded = start;
        else
                rdev->forwarding_offloaded = start;

        netdev_info(rdev->ndev, "%s hw %s\n", start ? "starting" : "stopping",
                    learning ? "learning" : "forwarding");
}

static void rswitch_update_l2_hw_learning(struct rswitch_private *priv)
{
        struct rswitch_device *rdev;
        bool learning_needed;

        rswitch_for_all_ports(priv, rdev) {
                if (rdev_for_l2_offload(rdev))
                        learning_needed = rdev->learning_requested;
                else
                        learning_needed = false;

                rswitch_change_l2_hw_offloading(rdev, learning_needed, true);
        }
}

static void rswitch_update_l2_hw_forwarding(struct rswitch_private *priv)
{
        struct rswitch_device *rdev;
        bool new_forwarding_offload;
        unsigned int fwd_mask;

        /* calculate fwd_mask with zeroes in bits corresponding to ports that
         * shall participate in hardware forwarding
         */
        fwd_mask = GENMASK(RSWITCH_NUM_AGENTS - 1, 0);

        rswitch_for_all_ports(priv, rdev) {
                if (rdev_for_l2_offload(rdev) && rdev->forwarding_requested)
                        fwd_mask &= ~BIT(rdev->port);
        }

        rswitch_for_all_ports(priv, rdev) {
                new_forwarding_offload = (rdev_for_l2_offload(rdev) && rdev->forwarding_requested);

                if (new_forwarding_offload || rdev->forwarding_offloaded) {
                        /* Update allowed offload destinations even for ports
                         * with L2 offload enabled earlier.
                         *
                         * Do not allow L2 forwarding to self for hw port.
                         */
                        iowrite32(FIELD_PREP(FWCP2_LTWFW_MASK, fwd_mask | BIT(rdev->port)),
                                  priv->addr + FWPC2(rdev->port));
                }

                if (new_forwarding_offload && !rdev->forwarding_offloaded)
                        rswitch_change_l2_hw_offloading(rdev, true, false);
                else if (!new_forwarding_offload && rdev->forwarding_offloaded)
                        rswitch_change_l2_hw_offloading(rdev, false, false);
        }
}

void rswitch_update_l2_offload(struct rswitch_private *priv)
{
        rswitch_update_l2_hw_learning(priv);
        rswitch_update_l2_hw_forwarding(priv);
}

static void rswitch_update_offload_brdev(struct rswitch_private *priv)
{
        struct net_device *offload_brdev = NULL;
        struct rswitch_device *rdev, *rdev2;

        rswitch_for_all_ports(priv, rdev) {
                if (!rdev->brdev)
                        continue;
                rswitch_for_all_ports(priv, rdev2) {
                        if (rdev2 == rdev)
                                break;
                        if (rdev2->brdev == rdev->brdev) {
                                offload_brdev = rdev->brdev;
                                break;
                        }
                }
                if (offload_brdev)
                        break;
        }

        if (offload_brdev == priv->offload_brdev)
                dev_dbg(&priv->pdev->dev,
                        "changing l2 offload from %s to %s\n",
                        netdev_name(priv->offload_brdev),
                        netdev_name(offload_brdev));
        else if (offload_brdev)
                dev_dbg(&priv->pdev->dev, "starting l2 offload for %s\n",
                        netdev_name(offload_brdev));
        else if (!offload_brdev)
                dev_dbg(&priv->pdev->dev, "stopping l2 offload for %s\n",
                        netdev_name(priv->offload_brdev));

        priv->offload_brdev = offload_brdev;

        rswitch_update_l2_offload(priv);
}

static bool rswitch_port_check(const struct net_device *ndev)
{
        return is_rdev(ndev);
}

static void rswitch_port_update_brdev(struct net_device *ndev,
                                      struct net_device *brdev)
{
        struct rswitch_device *rdev;

        if (!is_rdev(ndev))
                return;

        rdev = netdev_priv(ndev);
        rdev->brdev = brdev;
        rswitch_update_offload_brdev(rdev->priv);
}

static int rswitch_port_update_stp_state(struct net_device *ndev, u8 stp_state)
{
        struct rswitch_device *rdev;

        if (!is_rdev(ndev))
                return -ENODEV;

        rdev = netdev_priv(ndev);
        rdev->learning_requested = (stp_state == BR_STATE_LEARNING ||
                                    stp_state == BR_STATE_FORWARDING);
        rdev->forwarding_requested = (stp_state == BR_STATE_FORWARDING);
        rswitch_update_l2_offload(rdev->priv);

        return 0;
}

static int rswitch_netdevice_event(struct notifier_block *nb,
                                   unsigned long event, void *ptr)
{
        struct net_device *ndev = netdev_notifier_info_to_dev(ptr);
        struct netdev_notifier_changeupper_info *info;
        struct net_device *brdev;

        if (!rswitch_port_check(ndev))
                return NOTIFY_DONE;
        if (event != NETDEV_CHANGEUPPER)
                return NOTIFY_DONE;

        info = ptr;

        if (netif_is_bridge_master(info->upper_dev)) {
                brdev = info->linking ? info->upper_dev : NULL;
                rswitch_port_update_brdev(ndev, brdev);
        }

        return NOTIFY_OK;
}

static int rswitch_update_ageing_time(struct net_device *ndev, clock_t time)
{
        struct rswitch_device *rdev = netdev_priv(ndev);
        u32 reg_val;

        if (!is_rdev(ndev))
                return -ENODEV;

        if (!FIELD_FIT(FWMACAGC_MACAGT, time))
                return -EINVAL;

        reg_val = FIELD_PREP(FWMACAGC_MACAGT, time);
        reg_val |= FWMACAGC_MACAGE | FWMACAGC_MACAGSL;
        iowrite32(reg_val, rdev->priv->addr + FWMACAGC);

        return 0;
}

static int rswitch_port_attr_set(struct net_device *ndev, const void *ctx,
                                 const struct switchdev_attr *attr,
                                 struct netlink_ext_ack *extack)
{
        switch (attr->id) {
        case SWITCHDEV_ATTR_ID_PORT_STP_STATE:
                return rswitch_port_update_stp_state(ndev, attr->u.stp_state);
        case SWITCHDEV_ATTR_ID_BRIDGE_AGEING_TIME:
                return rswitch_update_ageing_time(ndev, attr->u.ageing_time);
        default:
                return -EOPNOTSUPP;
        }
}

static int rswitch_switchdev_event(struct notifier_block *nb,
                                   unsigned long event, void *ptr)
{
        struct net_device *ndev = switchdev_notifier_info_to_dev(ptr);
        int ret;

        if (event == SWITCHDEV_PORT_ATTR_SET) {
                ret = switchdev_handle_port_attr_set(ndev, ptr,
                                                     rswitch_port_check,
                                                     rswitch_port_attr_set);
                return notifier_from_errno(ret);
        }

        if (!rswitch_port_check(ndev))
                return NOTIFY_DONE;

        return notifier_from_errno(-EOPNOTSUPP);
}

static int rswitch_switchdev_blocking_event(struct notifier_block *nb,
                                            unsigned long event, void *ptr)
{
        struct net_device *ndev = switchdev_notifier_info_to_dev(ptr);
        int ret;

        switch (event) {
        case SWITCHDEV_PORT_OBJ_ADD:
                return -EOPNOTSUPP;
        case SWITCHDEV_PORT_OBJ_DEL:
                return -EOPNOTSUPP;
        case SWITCHDEV_PORT_ATTR_SET:
                ret = switchdev_handle_port_attr_set(ndev, ptr,
                                                     rswitch_port_check,
                                                     rswitch_port_attr_set);
                break;
        default:
                if (!rswitch_port_check(ndev))
                        return NOTIFY_DONE;
                ret = -EOPNOTSUPP;
        }

        return notifier_from_errno(ret);
}

static struct notifier_block rswitch_netdevice_nb = {
        .notifier_call = rswitch_netdevice_event,
};

static struct notifier_block rswitch_switchdev_nb = {
        .notifier_call = rswitch_switchdev_event,
};

static struct notifier_block rswitch_switchdev_blocking_nb = {
        .notifier_call = rswitch_switchdev_blocking_event,
};

int rswitch_register_notifiers(void)
{
        int ret;

        ret = register_netdevice_notifier(&rswitch_netdevice_nb);
        if (ret)
                goto register_netdevice_notifier_failed;

        ret = register_switchdev_notifier(&rswitch_switchdev_nb);
        if (ret)
                goto register_switchdev_notifier_failed;

        ret = register_switchdev_blocking_notifier(&rswitch_switchdev_blocking_nb);
        if (ret)
                goto register_switchdev_blocking_notifier_failed;

        return 0;

register_switchdev_blocking_notifier_failed:
        unregister_switchdev_notifier(&rswitch_switchdev_nb);
register_switchdev_notifier_failed:
        unregister_netdevice_notifier(&rswitch_netdevice_nb);
register_netdevice_notifier_failed:

        return ret;
}

void rswitch_unregister_notifiers(void)
{
        unregister_switchdev_blocking_notifier(&rswitch_switchdev_blocking_nb);
        unregister_switchdev_notifier(&rswitch_switchdev_nb);
        unregister_netdevice_notifier(&rswitch_netdevice_nb);
}