root/drivers/leds/leds-bcm6328.c
// SPDX-License-Identifier: GPL-2.0-or-later
/*
 * Driver for BCM6328 memory-mapped LEDs, based on leds-syscon.c
 *
 * Copyright 2015 Álvaro Fernández Rojas <noltari@gmail.com>
 * Copyright 2015 Jonas Gorski <jogo@openwrt.org>
 */
#include <linux/io.h>
#include <linux/leds.h>
#include <linux/module.h>
#include <linux/of.h>
#include <linux/platform_device.h>
#include <linux/spinlock.h>

#define BCM6328_REG_INIT                0x00
#define BCM6328_REG_MODE_HI             0x04
#define BCM6328_REG_MODE_LO             0x08
#define BCM6328_REG_HWDIS               0x0c
#define BCM6328_REG_STROBE              0x10
#define BCM6328_REG_LNKACTSEL_HI        0x14
#define BCM6328_REG_LNKACTSEL_LO        0x18
#define BCM6328_REG_RBACK               0x1c
#define BCM6328_REG_SERMUX              0x20

#define BCM6328_LED_MAX_COUNT           24
#define BCM6328_LED_DEF_DELAY           500

#define BCM6328_LED_BLINK_DELAYS        2
#define BCM6328_LED_BLINK_MS            20

#define BCM6328_LED_BLINK_MASK          0x3f
#define BCM6328_LED_BLINK1_SHIFT        0
#define BCM6328_LED_BLINK1_MASK         (BCM6328_LED_BLINK_MASK << \
                                         BCM6328_LED_BLINK1_SHIFT)
#define BCM6328_LED_BLINK2_SHIFT        6
#define BCM6328_LED_BLINK2_MASK         (BCM6328_LED_BLINK_MASK << \
                                         BCM6328_LED_BLINK2_SHIFT)
#define BCM6328_SERIAL_LED_EN           BIT(12)
#define BCM6328_SERIAL_LED_MUX          BIT(13)
#define BCM6328_SERIAL_LED_CLK_NPOL     BIT(14)
#define BCM6328_SERIAL_LED_DATA_PPOL    BIT(15)
#define BCM6328_SERIAL_LED_SHIFT_DIR    BIT(16)
#define BCM6328_LED_SHIFT_TEST          BIT(30)
#define BCM6328_LED_TEST                BIT(31)
#define BCM6328_INIT_MASK               (BCM6328_SERIAL_LED_EN | \
                                         BCM6328_SERIAL_LED_MUX | \
                                         BCM6328_SERIAL_LED_CLK_NPOL | \
                                         BCM6328_SERIAL_LED_DATA_PPOL | \
                                         BCM6328_SERIAL_LED_SHIFT_DIR)

#define BCM6328_LED_MODE_MASK           3
#define BCM6328_LED_MODE_ON             0
#define BCM6328_LED_MODE_BLINK1         1
#define BCM6328_LED_MODE_BLINK2         2
#define BCM6328_LED_MODE_OFF            3
#define BCM6328_LED_SHIFT(X)            ((X) << 1)

/**
 * struct bcm6328_led - state container for bcm6328 based LEDs
 * @cdev: LED class device for this LED
 * @mem: memory resource
 * @lock: memory lock
 * @pin: LED pin number
 * @blink_leds: blinking LEDs
 * @blink_delay: blinking delay
 * @active_low: LED is active low
 */
struct bcm6328_led {
        struct led_classdev cdev;
        void __iomem *mem;
        spinlock_t *lock;
        unsigned long pin;
        unsigned long *blink_leds;
        unsigned long *blink_delay;
        bool active_low;
};

static void bcm6328_led_write(void __iomem *reg, unsigned long data)
{
#ifdef CONFIG_CPU_BIG_ENDIAN
        iowrite32be(data, reg);
#else
        writel(data, reg);
#endif
}

static unsigned long bcm6328_led_read(void __iomem *reg)
{
#ifdef CONFIG_CPU_BIG_ENDIAN
        return ioread32be(reg);
#else
        return readl(reg);
#endif
}

/*
 * LEDMode 64 bits / 24 LEDs
 * bits [31:0] -> LEDs 8-23
 * bits [47:32] -> LEDs 0-7
 * bits [63:48] -> unused
 */
static unsigned long bcm6328_pin2shift(unsigned long pin)
{
        if (pin < 8)
                return pin + 16; /* LEDs 0-7 (bits 47:32) */
        else
                return pin - 8; /* LEDs 8-23 (bits 31:0) */
}

static void bcm6328_led_mode(struct bcm6328_led *led, unsigned long value)
{
        void __iomem *mode;
        unsigned long val, shift;

        shift = bcm6328_pin2shift(led->pin);
        if (shift >= 16)
                mode = led->mem + BCM6328_REG_MODE_HI;
        else
                mode = led->mem + BCM6328_REG_MODE_LO;

        val = bcm6328_led_read(mode);
        val &= ~(BCM6328_LED_MODE_MASK << BCM6328_LED_SHIFT(shift % 16));
        val |= (value << BCM6328_LED_SHIFT(shift % 16));
        bcm6328_led_write(mode, val);
}

static void bcm6328_led_set(struct led_classdev *led_cdev,
                            enum led_brightness value)
{
        struct bcm6328_led *led =
                container_of(led_cdev, struct bcm6328_led, cdev);
        unsigned long flags;

        spin_lock_irqsave(led->lock, flags);

        /* Remove LED from cached HW blinking intervals */
        led->blink_leds[0] &= ~BIT(led->pin);
        led->blink_leds[1] &= ~BIT(led->pin);

        /* Set LED on/off */
        if ((led->active_low && value == LED_OFF) ||
            (!led->active_low && value != LED_OFF))
                bcm6328_led_mode(led, BCM6328_LED_MODE_ON);
        else
                bcm6328_led_mode(led, BCM6328_LED_MODE_OFF);

        spin_unlock_irqrestore(led->lock, flags);
}

static unsigned long bcm6328_blink_delay(unsigned long delay)
{
        unsigned long bcm6328_delay;

        bcm6328_delay = delay + BCM6328_LED_BLINK_MS / 2;
        bcm6328_delay = bcm6328_delay / BCM6328_LED_BLINK_MS;
        if (bcm6328_delay == 0)
                bcm6328_delay = 1;

        return bcm6328_delay;
}

static int bcm6328_blink_set(struct led_classdev *led_cdev,
                             unsigned long *delay_on, unsigned long *delay_off)
{
        struct bcm6328_led *led =
                container_of(led_cdev, struct bcm6328_led, cdev);
        unsigned long delay, flags;
        int rc;

        if (!*delay_on)
                *delay_on = BCM6328_LED_DEF_DELAY;
        if (!*delay_off)
                *delay_off = BCM6328_LED_DEF_DELAY;

        delay = bcm6328_blink_delay(*delay_on);
        if (delay != bcm6328_blink_delay(*delay_off)) {
                dev_dbg(led_cdev->dev,
                        "fallback to soft blinking (delay_on != delay_off)\n");
                return -EINVAL;
        }

        if (delay > BCM6328_LED_BLINK_MASK) {
                dev_dbg(led_cdev->dev,
                        "fallback to soft blinking (delay > %ums)\n",
                        BCM6328_LED_BLINK_MASK * BCM6328_LED_BLINK_MS);
                return -EINVAL;
        }

        spin_lock_irqsave(led->lock, flags);
        /*
         * Check if any of the two configurable HW blinking intervals is
         * available:
         *   1. No LEDs assigned to the HW blinking interval.
         *   2. Only this LED is assigned to the HW blinking interval.
         *   3. LEDs with the same delay assigned.
         */
        if (led->blink_leds[0] == 0 ||
            led->blink_leds[0] == BIT(led->pin) ||
            led->blink_delay[0] == delay) {
                unsigned long val;

                /* Add LED to the first HW blinking interval cache */
                led->blink_leds[0] |= BIT(led->pin);

                /* Remove LED from the second HW blinking interval cache */
                led->blink_leds[1] &= ~BIT(led->pin);

                /* Cache first HW blinking interval delay */
                led->blink_delay[0] = delay;

                /* Update the delay for the first HW blinking interval */
                val = bcm6328_led_read(led->mem + BCM6328_REG_INIT);
                val &= ~BCM6328_LED_BLINK1_MASK;
                val |= (delay << BCM6328_LED_BLINK1_SHIFT);
                bcm6328_led_write(led->mem + BCM6328_REG_INIT, val);

                /* Set the LED to first HW blinking interval */
                bcm6328_led_mode(led, BCM6328_LED_MODE_BLINK1);

                rc = 0;
        } else if (led->blink_leds[1] == 0 ||
                   led->blink_leds[1] == BIT(led->pin) ||
                   led->blink_delay[1] == delay) {
                unsigned long val;

                /* Remove LED from the first HW blinking interval */
                led->blink_leds[0] &= ~BIT(led->pin);

                /* Add LED to the second HW blinking interval */
                led->blink_leds[1] |= BIT(led->pin);

                /* Cache second HW blinking interval delay */
                led->blink_delay[1] = delay;

                /* Update the delay for the second HW blinking interval */
                val = bcm6328_led_read(led->mem + BCM6328_REG_INIT);
                val &= ~BCM6328_LED_BLINK2_MASK;
                val |= (delay << BCM6328_LED_BLINK2_SHIFT);
                bcm6328_led_write(led->mem + BCM6328_REG_INIT, val);

                /* Set the LED to second HW blinking interval */
                bcm6328_led_mode(led, BCM6328_LED_MODE_BLINK2);

                rc = 0;
        } else {
                dev_dbg(led_cdev->dev,
                        "fallback to soft blinking (delay already set)\n");
                rc = -EINVAL;
        }
        spin_unlock_irqrestore(led->lock, flags);

        return rc;
}

static int bcm6328_hwled(struct device *dev, struct device_node *nc, u32 reg,
                         void __iomem *mem, spinlock_t *lock)
{
        int i, cnt;
        unsigned long flags, val;

        spin_lock_irqsave(lock, flags);
        val = bcm6328_led_read(mem + BCM6328_REG_HWDIS);
        val &= ~BIT(reg);
        bcm6328_led_write(mem + BCM6328_REG_HWDIS, val);
        spin_unlock_irqrestore(lock, flags);

        /* Only LEDs 0-7 can be activity/link controlled */
        if (reg >= 8)
                return 0;

        cnt = of_property_count_elems_of_size(nc, "brcm,link-signal-sources",
                                              sizeof(u32));
        for (i = 0; i < cnt; i++) {
                u32 sel;
                void __iomem *addr;

                if (reg < 4)
                        addr = mem + BCM6328_REG_LNKACTSEL_LO;
                else
                        addr = mem + BCM6328_REG_LNKACTSEL_HI;

                of_property_read_u32_index(nc, "brcm,link-signal-sources", i,
                                           &sel);

                if (reg / 4 != sel / 4) {
                        dev_warn(dev, "invalid link signal source\n");
                        continue;
                }

                spin_lock_irqsave(lock, flags);
                val = bcm6328_led_read(addr);
                val |= (BIT(reg % 4) << (((sel % 4) * 4) + 16));
                bcm6328_led_write(addr, val);
                spin_unlock_irqrestore(lock, flags);
        }

        cnt = of_property_count_elems_of_size(nc,
                                              "brcm,activity-signal-sources",
                                              sizeof(u32));
        for (i = 0; i < cnt; i++) {
                u32 sel;
                void __iomem *addr;

                if (reg < 4)
                        addr = mem + BCM6328_REG_LNKACTSEL_LO;
                else
                        addr = mem + BCM6328_REG_LNKACTSEL_HI;

                of_property_read_u32_index(nc, "brcm,activity-signal-sources",
                                           i, &sel);

                if (reg / 4 != sel / 4) {
                        dev_warn(dev, "invalid activity signal source\n");
                        continue;
                }

                spin_lock_irqsave(lock, flags);
                val = bcm6328_led_read(addr);
                val |= (BIT(reg % 4) << ((sel % 4) * 4));
                bcm6328_led_write(addr, val);
                spin_unlock_irqrestore(lock, flags);
        }

        return 0;
}

static int bcm6328_led(struct device *dev, struct device_node *nc, u32 reg,
                       void __iomem *mem, spinlock_t *lock,
                       unsigned long *blink_leds, unsigned long *blink_delay)
{
        struct led_init_data init_data = {};
        struct bcm6328_led *led;
        enum led_default_state state;
        unsigned long val, shift;
        void __iomem *mode;
        int rc;

        led = devm_kzalloc(dev, sizeof(*led), GFP_KERNEL);
        if (!led)
                return -ENOMEM;

        led->pin = reg;
        led->mem = mem;
        led->lock = lock;
        led->blink_leds = blink_leds;
        led->blink_delay = blink_delay;

        if (of_property_read_bool(nc, "active-low"))
                led->active_low = true;

        init_data.fwnode = of_fwnode_handle(nc);

        state = led_init_default_state_get(init_data.fwnode);
        switch (state) {
        case LEDS_DEFSTATE_ON:
                led->cdev.brightness = LED_FULL;
                break;
        case LEDS_DEFSTATE_KEEP:
                shift = bcm6328_pin2shift(led->pin);
                if (shift >= 16)
                        mode = mem + BCM6328_REG_MODE_HI;
                else
                        mode = mem + BCM6328_REG_MODE_LO;

                val = bcm6328_led_read(mode) >> BCM6328_LED_SHIFT(shift % 16);
                val &= BCM6328_LED_MODE_MASK;
                if ((led->active_low && val == BCM6328_LED_MODE_OFF) ||
                    (!led->active_low && val == BCM6328_LED_MODE_ON))
                        led->cdev.brightness = LED_FULL;
                else
                        led->cdev.brightness = LED_OFF;
                break;
        default:
                led->cdev.brightness = LED_OFF;
        }

        bcm6328_led_set(&led->cdev, led->cdev.brightness);

        led->cdev.brightness_set = bcm6328_led_set;
        led->cdev.blink_set = bcm6328_blink_set;

        rc = devm_led_classdev_register_ext(dev, &led->cdev, &init_data);
        if (rc < 0)
                return rc;

        dev_dbg(dev, "registered LED %s\n", led->cdev.name);

        return 0;
}

static int bcm6328_leds_probe(struct platform_device *pdev)
{
        struct device *dev = &pdev->dev;
        struct device_node *np = dev_of_node(&pdev->dev);
        void __iomem *mem;
        spinlock_t *lock; /* memory lock */
        unsigned long val, *blink_leds, *blink_delay;

        mem = devm_platform_ioremap_resource(pdev, 0);
        if (IS_ERR(mem))
                return PTR_ERR(mem);

        lock = devm_kzalloc(dev, sizeof(*lock), GFP_KERNEL);
        if (!lock)
                return -ENOMEM;

        blink_leds = devm_kcalloc(dev, BCM6328_LED_BLINK_DELAYS,
                                  sizeof(*blink_leds), GFP_KERNEL);
        if (!blink_leds)
                return -ENOMEM;

        blink_delay = devm_kcalloc(dev, BCM6328_LED_BLINK_DELAYS,
                                   sizeof(*blink_delay), GFP_KERNEL);
        if (!blink_delay)
                return -ENOMEM;

        spin_lock_init(lock);

        bcm6328_led_write(mem + BCM6328_REG_HWDIS, ~0);
        bcm6328_led_write(mem + BCM6328_REG_LNKACTSEL_HI, 0);
        bcm6328_led_write(mem + BCM6328_REG_LNKACTSEL_LO, 0);

        val = bcm6328_led_read(mem + BCM6328_REG_INIT);
        val &= ~(BCM6328_INIT_MASK);
        if (of_property_read_bool(np, "brcm,serial-leds"))
                val |= BCM6328_SERIAL_LED_EN;
        if (of_property_read_bool(np, "brcm,serial-mux"))
                val |= BCM6328_SERIAL_LED_MUX;
        if (of_property_read_bool(np, "brcm,serial-clk-low"))
                val |= BCM6328_SERIAL_LED_CLK_NPOL;
        if (!of_property_read_bool(np, "brcm,serial-dat-low"))
                val |= BCM6328_SERIAL_LED_DATA_PPOL;
        if (!of_property_read_bool(np, "brcm,serial-shift-inv"))
                val |= BCM6328_SERIAL_LED_SHIFT_DIR;
        bcm6328_led_write(mem + BCM6328_REG_INIT, val);

        for_each_available_child_of_node_scoped(np, child) {
                int rc;
                u32 reg;

                if (of_property_read_u32(child, "reg", &reg))
                        continue;

                if (reg >= BCM6328_LED_MAX_COUNT) {
                        dev_err(dev, "invalid LED (%u >= %d)\n", reg,
                                BCM6328_LED_MAX_COUNT);
                        continue;
                }

                if (of_property_read_bool(child, "brcm,hardware-controlled"))
                        rc = bcm6328_hwled(dev, child, reg, mem, lock);
                else
                        rc = bcm6328_led(dev, child, reg, mem, lock,
                                         blink_leds, blink_delay);

                if (rc < 0)
                        return rc;
        }

        return 0;
}

static const struct of_device_id bcm6328_leds_of_match[] = {
        { .compatible = "brcm,bcm6328-leds", },
        { },
};
MODULE_DEVICE_TABLE(of, bcm6328_leds_of_match);

static struct platform_driver bcm6328_leds_driver = {
        .probe = bcm6328_leds_probe,
        .driver = {
                .name = "leds-bcm6328",
                .of_match_table = bcm6328_leds_of_match,
        },
};

module_platform_driver(bcm6328_leds_driver);

MODULE_AUTHOR("Álvaro Fernández Rojas <noltari@gmail.com>");
MODULE_AUTHOR("Jonas Gorski <jogo@openwrt.org>");
MODULE_DESCRIPTION("LED driver for BCM6328 controllers");
MODULE_LICENSE("GPL v2");
MODULE_ALIAS("platform:leds-bcm6328");