diff mbox series

[2/2] leds: bcmbca: Add new driver for Broadcom BCMBCA

Message ID 20240920-bcmbca-leds-v1-2-5f70e692c6ff@linaro.org (mailing list archive)
State New, archived
Headers show
Series leds: Add basic BCMBCA LEDs support | expand

Commit Message

Linus Walleij Sept. 20, 2024, 11:15 a.m. UTC
The Broadcom BCA (Broadband Access) SoCs have a LED control
block that can support either parallel (directly connected)
LEDs or serial (connected to 1-4 shift registers) LEDs.

Add a driver for that hardware.

Signed-off-by: Linus Walleij <linus.walleij@linaro.org>
---
 MAINTAINERS                |   7 +
 drivers/leds/Kconfig       |   9 ++
 drivers/leds/Makefile      |   1 +
 drivers/leds/leds-bcmbca.c | 391 +++++++++++++++++++++++++++++++++++++++++++++
 4 files changed, 408 insertions(+)

Comments

Rafał Miłecki Sept. 20, 2024, 11:30 a.m. UTC | #1
On 2024-09-20 13:15, Linus Walleij wrote:
> The Broadcom BCA (Broadband Access) SoCs have a LED control
> block that can support either parallel (directly connected)
> LEDs or serial (connected to 1-4 shift registers) LEDs.
> 
> Add a driver for that hardware.

There is an existing driver for this hw block, please see:
drivers/leds/blink/leds-bcm63138.c

It may need some work (I didn't compare your submission with my minimal
driver) and I think it would be preferred over adding a new driver for
the same hw.
Linus Walleij Sept. 20, 2024, 1:36 p.m. UTC | #2
On Fri, Sep 20, 2024 at 1:30 PM Rafał Miłecki <rafal@milecki.pl> wrote:
> On 2024-09-20 13:15, Linus Walleij wrote:
> > The Broadcom BCA (Broadband Access) SoCs have a LED control
> > block that can support either parallel (directly connected)
> > LEDs or serial (connected to 1-4 shift registers) LEDs.
> >
> > Add a driver for that hardware.
>
> There is an existing driver for this hw block, please see:
> drivers/leds/blink/leds-bcm63138.c

Gah how could I miss this ... I will test with this driver and then add
any stuff I need.

Thanks!
Linus Walleij
diff mbox series

Patch

diff --git a/MAINTAINERS b/MAINTAINERS
index cc40a9d9b8cd..0a603b72a6a0 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -4383,6 +4383,13 @@  N:	bcm[9]?6856
 N:	bcm[9]?6858
 N:	bcm[9]?6878
 
+BROADCOM BCMBCA LED DRIVER
+M:	Linus Walleij <linus.walleij@linaro.org>
+L:	linux-leds@vger.kernel.org
+S:	Maintained
+F:	Documentation/devicetree/bindings/leds/brcm,bcmbca-leds.yaml
+F:	drivers/leds/leds-bcmbca.c
+
 BROADCOM BDC DRIVER
 M:	Justin Chen <justin.chen@broadcom.com>
 M:	Al Cooper <alcooperx@gmail.com>
diff --git a/drivers/leds/Kconfig b/drivers/leds/Kconfig
index 8d9d8da376e4..e14c7fa587f0 100644
--- a/drivers/leds/Kconfig
+++ b/drivers/leds/Kconfig
@@ -146,6 +146,15 @@  config LEDS_BCM6358
 	  This option enables support for LEDs connected to the BCM6358
 	  LED HW controller accessed via MMIO registers.
 
+config LEDS_BCMBCA
+	tristate "LED Support for Broadcom BCMBCA"
+	depends on LEDS_CLASS
+	depends on HAS_IOMEM
+	depends on OF
+	help
+	  This option enables support for LEDs connected to the BCMBCA
+	  LED HW controller accessed via MMIO registers.
+
 config LEDS_CHT_WCOVE
 	tristate "LED support for Intel Cherry Trail Whiskey Cove PMIC"
 	depends on LEDS_CLASS
diff --git a/drivers/leds/Makefile b/drivers/leds/Makefile
index 18afbb5a23ee..96e036f04e18 100644
--- a/drivers/leds/Makefile
+++ b/drivers/leds/Makefile
@@ -18,6 +18,7 @@  obj-$(CONFIG_LEDS_AW200XX)		+= leds-aw200xx.o
 obj-$(CONFIG_LEDS_AW2013)		+= leds-aw2013.o
 obj-$(CONFIG_LEDS_BCM6328)		+= leds-bcm6328.o
 obj-$(CONFIG_LEDS_BCM6358)		+= leds-bcm6358.o
+obj-$(CONFIG_LEDS_BCMBCA)		+= leds-bcmbca.o
 obj-$(CONFIG_LEDS_BD2606MVV)		+= leds-bd2606mvv.o
 obj-$(CONFIG_LEDS_BD2802)		+= leds-bd2802.o
 obj-$(CONFIG_LEDS_BLINKM)		+= leds-blinkm.o
diff --git a/drivers/leds/leds-bcmbca.c b/drivers/leds/leds-bcmbca.c
new file mode 100644
index 000000000000..74dc4245bea4
--- /dev/null
+++ b/drivers/leds/leds-bcmbca.c
@@ -0,0 +1,391 @@ 
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Driver for BCMBCA memory-mapped LEDs
+ *
+ * Copyright 2024 Linus Walleij <linus.walleij@linaro.org>
+ */
+#include <linux/bits.h>
+#include <linux/cleanup.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 BCMBCA_CTRL			0x00
+#define BCMBCA_CTRL_SERIAL_POL_HIGH	BIT(1)
+#define BCMBCA_CTRL_ENABLE		BIT(3) /* Uncertain about this bit */
+#define BCMBCA_CTRL_SERIAL_MSB_FIRST	BIT(4)
+
+#define BCMBCA_MASK			0x04
+#define BCMBCA_HW_EN			0x08
+/*
+ * In the serial shift selector, set bits to 1 from BIT(0) and upward all
+ * set to one until the last LED including any unused slots in the shift
+ * register.
+ */
+#define BCMBCA_SERIAL_SHIFT_SEL		0x0c
+#define BCMBCA_FLASH_RATE		0x10 /* 4 bits per LED so -> 1c */
+#define BCMBCA_BRIGHTNESS		0x20 /* 4 bits per LED so -> 2c */
+#define BCMBCA_POWER_LED_CFG		0x30
+#define BCMBCA_POWER_LUT		0x34 /* -> b0 */
+#define BCMBCA_HW_POLARITY		0xb4
+#define BCMBCA_SW_DATA			0xb8 /* 1 bit on/off for each LED */
+#define BCMBCA_SW_POLARITY		0xbc
+#define BCMBCA_PARALLEL_POLARITY	0xc0
+#define BCMBCA_SERIAL_POLARITY		0xc4
+
+#define BCMBCA_LED_MAX_COUNT		32
+#define BCMBCA_LED_MAX_BRIGHTNESS	8
+
+enum bcmbca_led_type {
+	BCMBCA_LED_SERIAL,
+	BCMBCA_LED_PARALLEL,
+};
+
+/**
+ * struct bcmbca_led - state container for bcmbca based LEDs
+ * @cdev: LED class device for this LED
+ * @base: memory base address
+ * @lock: memory lock
+ * @idx: LED index number
+ * @active_low: LED is active low
+ * @num_serial_shifters: number of serial shift registers
+ * @led_type: whether this is a serial or parallel LED
+ */
+struct bcmbca_led {
+	struct led_classdev cdev;
+	void __iomem *base;
+	spinlock_t *lock;
+	unsigned int idx;
+	bool active_low;
+	u32 num_serial_shifters;
+	enum bcmbca_led_type led_type;
+};
+
+static void bcmbca_led_blink_disable(struct led_classdev *ldev)
+{
+	struct bcmbca_led *led =
+		container_of(ldev, struct bcmbca_led, cdev);
+	/* 8 LEDs per register so integer-divide by 8 */
+	u8 led_offset = (led->idx >> 3);
+	/* Find the 4 bits for each LED */
+	u32 led_mask = 0xf << ((led->idx & 0x07) << 2);
+	u32 val;
+
+	/* Write registers */
+	guard(spinlock_irqsave)(led->lock);
+	val = readl(led->base + BCMBCA_FLASH_RATE + led_offset);
+	val &= led_mask;
+	writel(val, led->base + BCMBCA_FLASH_RATE + led_offset);
+}
+
+static int bcmbca_led_blink_set(struct led_classdev *ldev,
+				unsigned long *delay_on,
+				unsigned long *delay_off)
+{
+	struct bcmbca_led *led =
+		container_of(ldev, struct bcmbca_led, cdev);
+	u8 led_offset = (led->idx >> 3);
+	u32 led_mask = 0xf << ((led->idx & 0x07) << 2);
+	unsigned long period;
+	u32 led_val;
+	u32 val;
+
+	/* Friendly defaults as specified in the documentation */
+	if (*delay_on == 0 && *delay_off == 0) {
+		*delay_on = 240;
+		*delay_off = 240;
+	}
+
+	if (*delay_on != *delay_off) {
+		dev_dbg(ldev->dev, "only square blink supported\n");
+		return -EINVAL;
+	}
+
+	period = *delay_on + *delay_off;
+	if (period > 2000) {
+		dev_dbg(ldev->dev, "period too long, %lu max 2000 ms\n",
+			period);
+		return -EINVAL;
+	}
+
+	if (period <= 60)
+		led_val = 1;
+	else if (period <= 120)
+		led_val = 2;
+	else if (period <= 240)
+		led_val = 3;
+	else if (period <= 480)
+		led_val = 4;
+	else if (period <= 960)
+		led_val = 5;
+	else if (period <= 1920)
+		led_val = 6;
+	else
+		led_val = 7;
+
+	led_val = led_val << ((led->idx & 0x07) << 2);
+
+	/* Write registers */
+	guard(spinlock_irqsave)(led->lock);
+	val = readl(led->base + BCMBCA_FLASH_RATE + led_offset);
+	val &= led_mask;
+	val |= led_val;
+	writel(val, led->base + BCMBCA_FLASH_RATE + led_offset);
+
+	return 0;
+}
+
+static void bcmbca_led_set(struct led_classdev *ldev,
+			    enum led_brightness value)
+{
+	struct bcmbca_led *led =
+		container_of(ldev, struct bcmbca_led, cdev);
+	u8 led_offset = (led->idx >> 3);
+	u32 led_mask = 0xf << ((led->idx & 0x07) << 2);
+	u32 led_val = value << ((led->idx & 0x07) << 2);
+	u32 val;
+
+	dev_dbg(ldev->dev, "LED%u, register %08x, mask %08x, value %08x\n",
+		led->idx, BCMBCA_BRIGHTNESS + led_offset, led_mask, led_val);
+
+	/* Write registers */
+	guard(spinlock_irqsave)(led->lock);
+
+	/* Parallel LEDs support brightness control */
+	if (led->led_type == BCMBCA_LED_PARALLEL) {
+		val = readl(led->base + BCMBCA_BRIGHTNESS + led_offset);
+		val &= led_mask;
+		val |= led_val;
+		writel(val, led->base + BCMBCA_BRIGHTNESS + led_offset);
+	}
+
+	/* Software control on/off */
+	if (value == LED_OFF) {
+		val = readl(led->base + BCMBCA_SW_DATA);
+		val &= ~BIT(led->idx);
+		writel(val, led->base + BCMBCA_SW_DATA);
+		bcmbca_led_blink_disable(ldev);
+	} else {
+		val = readl(led->base + BCMBCA_SW_DATA);
+		val |= BIT(led->idx);
+		writel(val, led->base + BCMBCA_SW_DATA);
+	}
+}
+
+static u8 bcmbca_led_get(void __iomem *base, int idx)
+{
+	u8 led_offset = (idx >> 3);
+	u32 led_mask = 0xf << ((idx & 0x07) << 2);
+	u32 val;
+
+	/* Called marshalled so no lock needed */
+	val = readl(base + BCMBCA_BRIGHTNESS + led_offset);
+	return ((val & led_mask) >> ((idx & 0x07) << 2));
+}
+
+static int bcmbca_led_probe(struct device *dev, struct device_node *np, u32 reg,
+			    void __iomem *base, spinlock_t *lock,
+			    u32 num_shifters, enum bcmbca_led_type led_type)
+{
+	struct led_init_data init_data = {};
+	struct bcmbca_led *led;
+	enum led_default_state state;
+	u32 val;
+	int rc;
+
+	led = devm_kzalloc(dev, sizeof(*led), GFP_KERNEL);
+	if (!led)
+		return -ENOMEM;
+
+	led->idx = reg;
+	led->base = base;
+	led->lock = lock;
+
+	if (of_property_read_bool(np, "active-low"))
+		led->active_low = true;
+
+	init_data.fwnode = of_fwnode_handle(np);
+
+	if (led->led_type == BCMBCA_LED_PARALLEL)
+		led->cdev.max_brightness = BCMBCA_LED_MAX_BRIGHTNESS;
+	else
+		led->cdev.max_brightness = LED_ON;
+
+	state = led_init_default_state_get(init_data.fwnode);
+
+	switch (state) {
+	case LEDS_DEFSTATE_ON:
+		led->cdev.brightness = led->cdev.max_brightness;
+		break;
+	case LEDS_DEFSTATE_KEEP:
+		val = bcmbca_led_get(base, led->idx);
+		if (val)
+			led->cdev.brightness = val;
+		else
+			led->cdev.brightness = LED_OFF;
+		break;
+	default:
+		led->cdev.brightness = LED_OFF;
+		break;
+	}
+
+	/*
+	 * Polarity inversion setting per-LED
+	 * The default is actually active low, we set a bit to 1
+	 * in the register to make it active high.
+	 */
+	if (!of_property_read_bool(np, "active-low")) {
+		switch (led_type) {
+		case BCMBCA_LED_SERIAL:
+			val = readl(base + BCMBCA_SERIAL_POLARITY);
+			val |= BIT(led->idx);
+			writel(val, base + BCMBCA_SERIAL_POLARITY);
+			break;
+		case BCMBCA_LED_PARALLEL:
+			val = readl(base + BCMBCA_PARALLEL_POLARITY);
+			val |= BIT(led->idx);
+			writel(val, base + BCMBCA_PARALLEL_POLARITY);
+			break;
+		default:
+			break;
+		}
+	}
+
+	/* Initial brightness setup */
+	bcmbca_led_set(&led->cdev, led->cdev.brightness);
+
+	led->cdev.brightness_set = bcmbca_led_set;
+	led->cdev.blink_set = bcmbca_led_blink_set;
+	/* TODO: implement HW control */
+
+	rc = devm_led_classdev_register_ext(dev, &led->cdev, &init_data);
+	if (rc < 0)
+		return rc;
+
+	dev_info(dev, "registered LED %s\n", led->cdev.name);
+
+	return 0;
+}
+
+static int bcmbca_leds_probe(struct platform_device *pdev)
+{
+	unsigned int max_leds = BCMBCA_LED_MAX_COUNT;
+	struct device *dev = &pdev->dev;
+	struct device_node *np = dev_of_node(&pdev->dev);
+	enum bcmbca_led_type led_type;
+	void __iomem *base;
+	spinlock_t *lock; /* memory lock */
+	u32 num_shifters;
+	u32 val;
+	int ret;
+	int i;
+
+	base = devm_platform_ioremap_resource(pdev, 0);
+	if (IS_ERR(base))
+		return PTR_ERR(base);
+
+	lock = devm_kzalloc(dev, sizeof(*lock), GFP_KERNEL);
+	if (!lock)
+		return -ENOMEM;
+
+	ret = of_property_read_u32(np, "brcm,serial-shifters", &num_shifters);
+	if (!ret) {
+		/* Serial LEDs */
+		dev_dbg(dev, "serial LEDs, %d shift registers used\n", num_shifters);
+		led_type = BCMBCA_LED_SERIAL;
+		/*
+		 * Each shifter can handle maximum 8 LEDs so we cap the
+		 * maximum LEDs we can handle at that.
+		 */
+		max_leds = num_shifters * 8;
+	} else {
+		/* Parallel LEDs */
+		led_type = BCMBCA_LED_PARALLEL;
+		dev_info(dev, "parallel LEDs requested, this is untested\n");
+	}
+
+	spin_lock_init(lock);
+
+	/* Turn off all LEDs and let the driver deal with them */
+	writel(0, base + BCMBCA_HW_EN);
+	writel(0, base + BCMBCA_SERIAL_POLARITY);
+	writel(0, base + BCMBCA_PARALLEL_POLARITY);
+
+	/* Set up serial shift register */
+	switch (num_shifters) {
+	case 0:
+		val = 0;
+		break;
+	case 1:
+		val = 0x000000ff;
+		break;
+	case 2:
+		val = 0x0000ffff;
+		break;
+	case 3:
+		val = 0x00ffffff;
+		break;
+	case 4:
+		val = 0xffffffff;
+		break;
+	}
+	writel(val, base + BCMBCA_SERIAL_SHIFT_SEL);
+
+	/* ??? */
+	writel(0xc0000000, base + BCMBCA_HW_POLARITY);
+	/* Initialize to max brightness */
+	for (i = 0; i < BCMBCA_LED_MAX_COUNT/8; i++) {
+		writel(0x88888888, base + BCMBCA_BRIGHTNESS + 4*i);
+		writel(0, base + BCMBCA_BRIGHTNESS + BCMBCA_FLASH_RATE + 4*i);
+	}
+
+	for_each_available_child_of_node_scoped(np, child) {
+		u32 reg;
+
+		if (of_property_read_u32(child, "reg", &reg))
+			continue;
+
+		if (reg >= max_leds) {
+			dev_err(dev, "invalid LED (%u >= %d)\n", reg,
+				max_leds);
+			continue;
+		}
+
+		ret = bcmbca_led_probe(dev, child, reg, base, lock,
+				       num_shifters, led_type);
+		if (ret < 0)
+			return ret;
+	}
+
+	/* Enable the LEDs */
+	val = BCMBCA_CTRL_ENABLE | BCMBCA_CTRL_SERIAL_POL_HIGH;
+	if (of_property_read_bool(np, "brcm,serial-active-low"))
+		val &= ~BCMBCA_CTRL_SERIAL_POL_HIGH;
+	writel(val, base + BCMBCA_CTRL);
+
+	return 0;
+}
+
+static const struct of_device_id bcmbca_leds_of_match[] = {
+	{ .compatible = "brcm,bcmbca-leds", },
+	{ },
+};
+MODULE_DEVICE_TABLE(of, bcmbca_leds_of_match);
+
+static struct platform_driver bcmbca_leds_driver = {
+	.probe = bcmbca_leds_probe,
+	.driver = {
+		.name = "leds-bcmbca",
+		.of_match_table = bcmbca_leds_of_match,
+	},
+};
+
+module_platform_driver(bcmbca_leds_driver);
+
+MODULE_AUTHOR("Linus Walleij <linus.walleij@linaro.org>");
+MODULE_DESCRIPTION("LED driver for BCMBCA LED controllers");
+MODULE_LICENSE("GPL");
+MODULE_ALIAS("platform:leds-bcmbca");