root/drivers/net/ethernet/intel/igc/igc_leds.c
// SPDX-License-Identifier: GPL-2.0
/* Copyright (C) 2024 Linutronix GmbH */

#include <linux/bits.h>
#include <linux/leds.h>
#include <linux/netdevice.h>
#include <linux/pm_runtime.h>
#include <uapi/linux/uleds.h>

#include "igc.h"

#define IGC_NUM_LEDS                    3

#define IGC_LEDCTL_LED0_MODE_SHIFT      0
#define IGC_LEDCTL_LED0_MODE_MASK       GENMASK(3, 0)
#define IGC_LEDCTL_LED0_BLINK           BIT(7)
#define IGC_LEDCTL_LED1_MODE_SHIFT      8
#define IGC_LEDCTL_LED1_MODE_MASK       GENMASK(11, 8)
#define IGC_LEDCTL_LED1_BLINK           BIT(15)
#define IGC_LEDCTL_LED2_MODE_SHIFT      16
#define IGC_LEDCTL_LED2_MODE_MASK       GENMASK(19, 16)
#define IGC_LEDCTL_LED2_BLINK           BIT(23)

#define IGC_LEDCTL_MODE_ON              0x00
#define IGC_LEDCTL_MODE_OFF             0x01
#define IGC_LEDCTL_MODE_LINK_10         0x05
#define IGC_LEDCTL_MODE_LINK_100        0x06
#define IGC_LEDCTL_MODE_LINK_1000       0x07
#define IGC_LEDCTL_MODE_LINK_2500       0x08
#define IGC_LEDCTL_MODE_ACTIVITY        0x0b

#define IGC_SUPPORTED_MODES                                              \
        (BIT(TRIGGER_NETDEV_LINK_2500) | BIT(TRIGGER_NETDEV_LINK_1000) | \
         BIT(TRIGGER_NETDEV_LINK_100) | BIT(TRIGGER_NETDEV_LINK_10) |    \
         BIT(TRIGGER_NETDEV_RX) | BIT(TRIGGER_NETDEV_TX))

#define IGC_ACTIVITY_MODES                                      \
        (BIT(TRIGGER_NETDEV_RX) | BIT(TRIGGER_NETDEV_TX))

struct igc_led_classdev {
        struct net_device *netdev;
        struct led_classdev led;
        int index;
};

#define lcdev_to_igc_ldev(lcdev)                                \
        container_of(lcdev, struct igc_led_classdev, led)

static void igc_led_select(struct igc_adapter *adapter, int led,
                           u32 *mask, u32 *shift, u32 *blink)
{
        switch (led) {
        case 0:
                *mask  = IGC_LEDCTL_LED0_MODE_MASK;
                *shift = IGC_LEDCTL_LED0_MODE_SHIFT;
                *blink = IGC_LEDCTL_LED0_BLINK;
                break;
        case 1:
                *mask  = IGC_LEDCTL_LED1_MODE_MASK;
                *shift = IGC_LEDCTL_LED1_MODE_SHIFT;
                *blink = IGC_LEDCTL_LED1_BLINK;
                break;
        case 2:
                *mask  = IGC_LEDCTL_LED2_MODE_MASK;
                *shift = IGC_LEDCTL_LED2_MODE_SHIFT;
                *blink = IGC_LEDCTL_LED2_BLINK;
                break;
        default:
                *mask = *shift = *blink = 0;
                netdev_err(adapter->netdev, "Unknown LED %d selected!\n", led);
        }
}

static void igc_led_set(struct igc_adapter *adapter, int led, u32 mode,
                        bool blink)
{
        u32 shift, mask, blink_bit, ledctl;
        struct igc_hw *hw = &adapter->hw;

        igc_led_select(adapter, led, &mask, &shift, &blink_bit);

        pm_runtime_get_sync(&adapter->pdev->dev);
        mutex_lock(&adapter->led_mutex);

        /* Set mode */
        ledctl = rd32(IGC_LEDCTL);
        ledctl &= ~mask;
        ledctl |= mode << shift;

        /* Configure blinking */
        if (blink)
                ledctl |= blink_bit;
        else
                ledctl &= ~blink_bit;
        wr32(IGC_LEDCTL, ledctl);

        mutex_unlock(&adapter->led_mutex);
        pm_runtime_put(&adapter->pdev->dev);
}

static u32 igc_led_get(struct igc_adapter *adapter, int led)
{
        u32 shift, mask, blink_bit, ledctl;
        struct igc_hw *hw = &adapter->hw;

        igc_led_select(adapter, led, &mask, &shift, &blink_bit);

        pm_runtime_get_sync(&adapter->pdev->dev);
        mutex_lock(&adapter->led_mutex);
        ledctl = rd32(IGC_LEDCTL);
        mutex_unlock(&adapter->led_mutex);
        pm_runtime_put(&adapter->pdev->dev);

        return (ledctl & mask) >> shift;
}

static int igc_led_brightness_set_blocking(struct led_classdev *led_cdev,
                                           enum led_brightness brightness)
{
        struct igc_led_classdev *ldev = lcdev_to_igc_ldev(led_cdev);
        struct igc_adapter *adapter = netdev_priv(ldev->netdev);
        u32 mode;

        if (brightness)
                mode = IGC_LEDCTL_MODE_ON;
        else
                mode = IGC_LEDCTL_MODE_OFF;

        netdev_dbg(adapter->netdev, "Set brightness for LED %d to mode %u!\n",
                   ldev->index, mode);

        igc_led_set(adapter, ldev->index, mode, false);

        return 0;
}

static int igc_led_hw_control_is_supported(struct led_classdev *led_cdev,
                                           unsigned long flags)
{
        if (flags & ~IGC_SUPPORTED_MODES)
                return -EOPNOTSUPP;

        /* If Tx and Rx selected, activity can be offloaded unless some other
         * mode is selected as well.
         */
        if ((flags & BIT(TRIGGER_NETDEV_TX)) &&
            (flags & BIT(TRIGGER_NETDEV_RX)) &&
            !(flags & ~IGC_ACTIVITY_MODES))
                return 0;

        /* Single Rx or Tx activity is not supported. */
        if (flags & IGC_ACTIVITY_MODES)
                return -EOPNOTSUPP;

        /* Only one mode can be active at a given time. */
        if (flags & (flags - 1))
                return -EOPNOTSUPP;

        return 0;
}

static int igc_led_hw_control_set(struct led_classdev *led_cdev,
                                  unsigned long flags)
{
        struct igc_led_classdev *ldev = lcdev_to_igc_ldev(led_cdev);
        struct igc_adapter *adapter = netdev_priv(ldev->netdev);
        u32 mode = IGC_LEDCTL_MODE_OFF;
        bool blink = false;

        if (flags & BIT(TRIGGER_NETDEV_LINK_10))
                mode = IGC_LEDCTL_MODE_LINK_10;
        if (flags & BIT(TRIGGER_NETDEV_LINK_100))
                mode = IGC_LEDCTL_MODE_LINK_100;
        if (flags & BIT(TRIGGER_NETDEV_LINK_1000))
                mode = IGC_LEDCTL_MODE_LINK_1000;
        if (flags & BIT(TRIGGER_NETDEV_LINK_2500))
                mode = IGC_LEDCTL_MODE_LINK_2500;
        if ((flags & BIT(TRIGGER_NETDEV_TX)) &&
            (flags & BIT(TRIGGER_NETDEV_RX)))
                mode = IGC_LEDCTL_MODE_ACTIVITY;

        netdev_dbg(adapter->netdev, "Set HW control for LED %d to mode %u!\n",
                   ldev->index, mode);

        /* blink is recommended for activity */
        if (mode == IGC_LEDCTL_MODE_ACTIVITY)
                blink = true;

        igc_led_set(adapter, ldev->index, mode, blink);

        return 0;
}

static int igc_led_hw_control_get(struct led_classdev *led_cdev,
                                  unsigned long *flags)
{
        struct igc_led_classdev *ldev = lcdev_to_igc_ldev(led_cdev);
        struct igc_adapter *adapter = netdev_priv(ldev->netdev);
        u32 mode;

        mode = igc_led_get(adapter, ldev->index);

        switch (mode) {
        case IGC_LEDCTL_MODE_ACTIVITY:
                *flags = BIT(TRIGGER_NETDEV_TX) | BIT(TRIGGER_NETDEV_RX);
                break;
        case IGC_LEDCTL_MODE_LINK_10:
                *flags = BIT(TRIGGER_NETDEV_LINK_10);
                break;
        case IGC_LEDCTL_MODE_LINK_100:
                *flags = BIT(TRIGGER_NETDEV_LINK_100);
                break;
        case IGC_LEDCTL_MODE_LINK_1000:
                *flags = BIT(TRIGGER_NETDEV_LINK_1000);
                break;
        case IGC_LEDCTL_MODE_LINK_2500:
                *flags = BIT(TRIGGER_NETDEV_LINK_2500);
                break;
        }

        return 0;
}

static struct device *igc_led_hw_control_get_device(struct led_classdev *led_cdev)
{
        struct igc_led_classdev *ldev = lcdev_to_igc_ldev(led_cdev);

        return &ldev->netdev->dev;
}

static void igc_led_get_name(struct igc_adapter *adapter, int index, char *buf,
                             size_t buf_len)
{
        snprintf(buf, buf_len, "igc-%x%x-led%d",
                 pci_domain_nr(adapter->pdev->bus),
                 pci_dev_id(adapter->pdev), index);
}

static int igc_setup_ldev(struct igc_led_classdev *ldev,
                          struct net_device *netdev, int index)
{
        struct igc_adapter *adapter = netdev_priv(netdev);
        struct led_classdev *led_cdev = &ldev->led;
        char led_name[LED_MAX_NAME_SIZE];

        ldev->netdev = netdev;
        ldev->index = index;

        igc_led_get_name(adapter, index, led_name, LED_MAX_NAME_SIZE);
        led_cdev->name = led_name;
        led_cdev->flags |= LED_RETAIN_AT_SHUTDOWN;
        led_cdev->max_brightness = 1;
        led_cdev->brightness_set_blocking = igc_led_brightness_set_blocking;
        led_cdev->hw_control_trigger = "netdev";
        led_cdev->hw_control_is_supported = igc_led_hw_control_is_supported;
        led_cdev->hw_control_set = igc_led_hw_control_set;
        led_cdev->hw_control_get = igc_led_hw_control_get;
        led_cdev->hw_control_get_device = igc_led_hw_control_get_device;

        return led_classdev_register(&netdev->dev, led_cdev);
}

int igc_led_setup(struct igc_adapter *adapter)
{
        struct net_device *netdev = adapter->netdev;
        struct igc_led_classdev *leds;
        int i, err;

        mutex_init(&adapter->led_mutex);

        leds = kzalloc_objs(*leds, IGC_NUM_LEDS);
        if (!leds)
                return -ENOMEM;

        for (i = 0; i < IGC_NUM_LEDS; i++) {
                err = igc_setup_ldev(leds + i, netdev, i);
                if (err)
                        goto err;
        }

        adapter->leds = leds;

        return 0;

err:
        for (i--; i >= 0; i--)
                led_classdev_unregister(&((leds + i)->led));

        kfree(leds);
        return err;
}

void igc_led_free(struct igc_adapter *adapter)
{
        struct igc_led_classdev *leds = adapter->leds;
        int i;

        for (i = 0; i < IGC_NUM_LEDS; i++)
                led_classdev_unregister(&((leds + i)->led));

        kfree(leds);
}