| // SPDX-License-Identifier: GPL-2.0-or-later |
| /* |
| * Driver for BCM6358 memory-mapped LEDs, based on leds-syscon.c |
| * |
| * Copyright 2015 Álvaro Fernández Rojas <noltari@gmail.com> |
| */ |
| #include <linux/delay.h> |
| #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 BCM6358_REG_MODE 0x0 |
| #define BCM6358_REG_CTRL 0x4 |
| |
| #define BCM6358_SLED_CLKDIV_MASK 3 |
| #define BCM6358_SLED_CLKDIV_1 0 |
| #define BCM6358_SLED_CLKDIV_2 1 |
| #define BCM6358_SLED_CLKDIV_4 2 |
| #define BCM6358_SLED_CLKDIV_8 3 |
| |
| #define BCM6358_SLED_POLARITY BIT(2) |
| #define BCM6358_SLED_BUSY BIT(3) |
| |
| #define BCM6358_SLED_MAX_COUNT 32 |
| #define BCM6358_SLED_WAIT 100 |
| |
| /** |
| * struct bcm6358_led - state container for bcm6358 based LEDs |
| * @cdev: LED class device for this LED |
| * @mem: memory resource |
| * @lock: memory lock |
| * @pin: LED pin number |
| * @active_low: LED is active low |
| */ |
| struct bcm6358_led { |
| struct led_classdev cdev; |
| void __iomem *mem; |
| spinlock_t *lock; |
| unsigned long pin; |
| bool active_low; |
| }; |
| |
| static void bcm6358_led_write(void __iomem *reg, unsigned long data) |
| { |
| #ifdef CONFIG_CPU_BIG_ENDIAN |
| iowrite32be(data, reg); |
| #else |
| writel(data, reg); |
| #endif |
| } |
| |
| static unsigned long bcm6358_led_read(void __iomem *reg) |
| { |
| #ifdef CONFIG_CPU_BIG_ENDIAN |
| return ioread32be(reg); |
| #else |
| return readl(reg); |
| #endif |
| } |
| |
| static unsigned long bcm6358_led_busy(void __iomem *mem) |
| { |
| unsigned long val; |
| |
| while ((val = bcm6358_led_read(mem + BCM6358_REG_CTRL)) & |
| BCM6358_SLED_BUSY) |
| udelay(BCM6358_SLED_WAIT); |
| |
| return val; |
| } |
| |
| static void bcm6358_led_set(struct led_classdev *led_cdev, |
| enum led_brightness value) |
| { |
| struct bcm6358_led *led = |
| container_of(led_cdev, struct bcm6358_led, cdev); |
| unsigned long flags, val; |
| |
| spin_lock_irqsave(led->lock, flags); |
| bcm6358_led_busy(led->mem); |
| val = bcm6358_led_read(led->mem + BCM6358_REG_MODE); |
| if ((led->active_low && value == LED_OFF) || |
| (!led->active_low && value != LED_OFF)) |
| val |= BIT(led->pin); |
| else |
| val &= ~(BIT(led->pin)); |
| bcm6358_led_write(led->mem + BCM6358_REG_MODE, val); |
| spin_unlock_irqrestore(led->lock, flags); |
| } |
| |
| static int bcm6358_led(struct device *dev, struct device_node *nc, u32 reg, |
| void __iomem *mem, spinlock_t *lock) |
| { |
| struct led_init_data init_data = {}; |
| struct bcm6358_led *led; |
| enum led_default_state state; |
| unsigned long val; |
| int rc; |
| |
| led = devm_kzalloc(dev, sizeof(*led), GFP_KERNEL); |
| if (!led) |
| return -ENOMEM; |
| |
| led->pin = reg; |
| led->mem = mem; |
| led->lock = lock; |
| |
| 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: |
| val = bcm6358_led_read(led->mem + BCM6358_REG_MODE); |
| val &= BIT(led->pin); |
| if ((led->active_low && !val) || (!led->active_low && val)) |
| led->cdev.brightness = LED_FULL; |
| else |
| led->cdev.brightness = LED_OFF; |
| break; |
| default: |
| led->cdev.brightness = LED_OFF; |
| } |
| |
| bcm6358_led_set(&led->cdev, led->cdev.brightness); |
| |
| led->cdev.brightness_set = bcm6358_led_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 bcm6358_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; |
| u32 clk_div; |
| |
| 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; |
| |
| spin_lock_init(lock); |
| |
| val = bcm6358_led_busy(mem); |
| val &= ~(BCM6358_SLED_POLARITY | BCM6358_SLED_CLKDIV_MASK); |
| if (of_property_read_bool(np, "brcm,clk-dat-low")) |
| val |= BCM6358_SLED_POLARITY; |
| of_property_read_u32(np, "brcm,clk-div", &clk_div); |
| switch (clk_div) { |
| case 8: |
| val |= BCM6358_SLED_CLKDIV_8; |
| break; |
| case 4: |
| val |= BCM6358_SLED_CLKDIV_4; |
| break; |
| case 2: |
| val |= BCM6358_SLED_CLKDIV_2; |
| break; |
| default: |
| val |= BCM6358_SLED_CLKDIV_1; |
| break; |
| } |
| bcm6358_led_write(mem + BCM6358_REG_CTRL, val); |
| |
| for_each_available_child_of_node_scoped(np, child) { |
| int rc; |
| u32 reg; |
| |
| if (of_property_read_u32(child, "reg", ®)) |
| continue; |
| |
| if (reg >= BCM6358_SLED_MAX_COUNT) { |
| dev_err(dev, "invalid LED (%u >= %d)\n", reg, |
| BCM6358_SLED_MAX_COUNT); |
| continue; |
| } |
| |
| rc = bcm6358_led(dev, child, reg, mem, lock); |
| if (rc < 0) |
| return rc; |
| } |
| |
| return 0; |
| } |
| |
| static const struct of_device_id bcm6358_leds_of_match[] = { |
| { .compatible = "brcm,bcm6358-leds", }, |
| { }, |
| }; |
| MODULE_DEVICE_TABLE(of, bcm6358_leds_of_match); |
| |
| static struct platform_driver bcm6358_leds_driver = { |
| .probe = bcm6358_leds_probe, |
| .driver = { |
| .name = "leds-bcm6358", |
| .of_match_table = bcm6358_leds_of_match, |
| }, |
| }; |
| |
| module_platform_driver(bcm6358_leds_driver); |
| |
| MODULE_AUTHOR("Álvaro Fernández Rojas <noltari@gmail.com>"); |
| MODULE_DESCRIPTION("LED driver for BCM6358 controllers"); |
| MODULE_LICENSE("GPL v2"); |
| MODULE_ALIAS("platform:leds-bcm6358"); |