summaryrefslogtreecommitdiffstats
path: root/drivers/leds/rgb
diff options
context:
space:
mode:
Diffstat (limited to 'drivers/leds/rgb')
-rw-r--r--drivers/leds/rgb/Kconfig127
-rw-r--r--drivers/leds/rgb/Makefile12
-rw-r--r--drivers/leds/rgb/leds-group-multicolor.c172
-rw-r--r--drivers/leds/rgb/leds-ktd202x.c635
-rw-r--r--drivers/leds/rgb/leds-lp5812.c642
-rw-r--r--drivers/leds/rgb/leds-lp5812.h172
-rw-r--r--drivers/leds/rgb/leds-lp5860-core.c233
-rw-r--r--drivers/leds/rgb/leds-lp5860-spi.c98
-rw-r--r--drivers/leds/rgb/leds-lp5860.h268
-rw-r--r--drivers/leds/rgb/leds-mt6370-rgb.c994
-rw-r--r--drivers/leds/rgb/leds-ncp5623.c270
-rw-r--r--drivers/leds/rgb/leds-pwm-multicolor.c199
-rw-r--r--drivers/leds/rgb/leds-qcom-lpg.c1850
-rw-r--r--drivers/leds/rgb/leds-s2m-rgb.c426
14 files changed, 6098 insertions, 0 deletions
diff --git a/drivers/leds/rgb/Kconfig b/drivers/leds/rgb/Kconfig
new file mode 100644
index 000000000..6e9ab5f60
--- /dev/null
+++ b/drivers/leds/rgb/Kconfig
@@ -0,0 +1,127 @@
+# SPDX-License-Identifier: GPL-2.0
+
+if LEDS_CLASS_MULTICOLOR
+
+config LEDS_GROUP_MULTICOLOR
+ tristate "LEDs group multi-color support"
+ depends on OF
+ help
+ This option enables support for monochrome LEDs that are grouped
+ into multicolor LEDs which is useful in the case where LEDs of
+ different colors are physically grouped in a single multi-color LED
+ and driven by a controller that doesn't have multi-color support.
+
+ To compile this driver as a module, choose M here: the module
+ will be called leds-group-multicolor.
+
+config LEDS_KTD202X
+ tristate "LED support for KTD202x Chips"
+ depends on I2C
+ select REGMAP_I2C
+ help
+ This option enables support for the Kinetic KTD2026/KTD2027
+ RGB/White LED driver found in different BQ mobile phones.
+ It is a 3 or 4 channel LED driver programmed via an I2C interface.
+
+ To compile this driver as a module, choose M here: the module
+ will be called leds-ktd202x.
+
+config LEDS_LP5812
+ tristate "LED support for Texas Instruments LP5812"
+ depends on I2C
+ help
+ If you say Y here you get support for TI LP5812 LED driver.
+ The LP5812 is a 4x3 matrix RGB LED driver with autonomous
+ animation engine control.
+
+ To compile this driver as a module, choose M here: the
+ module will be called leds-lp5812.
+
+ If unsure, say N.
+
+config LEDS_LP5860_CORE
+ tristate "Core Driver for TI LP5860"
+ depends on LEDS_CLASS
+ depends on OF
+ select REGMAP
+ help
+ This option supports common operations for LP5860 devices.
+ The LP5860 is a LED matrix driver with 18 constant current
+ sinks and 11 scan switches for 198 LED dots. Each dot can be
+ controlled individually and supports 8/16-bit PWM dimming.
+ The chip supports individual LED open and short detection.
+
+ The device can be used with SPI or I2C bus.
+
+config LEDS_LP5860_SPI
+ tristate "LED Support for TI LP5860 SPI LED driver chip"
+ depends on SPI
+ depends on OF
+ select LEDS_LP5860_CORE
+ help
+ If you say yes here you get support for the Texas Instruments
+ LP5860 LED driver for SPI bus connections.
+
+ To compile this driver as a module, choose M here: the
+ module will be called leds-lp5860-spi.
+
+config LEDS_NCP5623
+ tristate "LED support for NCP5623"
+ depends on I2C
+ depends on OF
+ help
+ This option enables support for ON semiconductor NCP5623
+ Triple Output I2C Controlled RGB LED Driver.
+
+ To compile this driver as a module, choose M here: the module
+ will be called leds-ncp5623.
+
+config LEDS_PWM_MULTICOLOR
+ tristate "PWM driven multi-color LED Support"
+ depends on PWM
+ help
+ This option enables support for PWM driven monochrome LEDs that are
+ grouped into multicolor LEDs.
+
+ To compile this driver as a module, choose M here: the module
+ will be called leds-pwm-multicolor.
+
+config LEDS_QCOM_LPG
+ tristate "LED support for Qualcomm LPG"
+ depends on OF
+ depends on PWM
+ depends on QCOM_PBS || !QCOM_PBS
+ depends on SPMI
+ help
+ This option enables support for the Light Pulse Generator found in a
+ wide variety of Qualcomm PMICs. The LPG consists of a number of PWM
+ channels and typically a shared pattern lookup table and a current
+ sink, intended to drive RGB LEDs. Each channel can either be used as
+ a LED, grouped to represent a RGB LED or exposed as PWM channels.
+
+ If compiled as a module, the module will be named leds-qcom-lpg.
+
+config LEDS_S2M_RGB
+ tristate "Samsung S2M series PMICs RGB LED support"
+ depends on LEDS_CLASS
+ depends on MFD_SEC_CORE
+ help
+ This option enables support for the S2MU005 RGB LEDs. These devices
+ have three LED channels, with 8-bit brightness control for each
+ channel. The S2MU005 is usually found in mobile phones as status
+ indicators.
+
+config LEDS_MT6370_RGB
+ tristate "LED Support for MediaTek MT6370 PMIC"
+ depends on MFD_MT6370
+ select LINEAR_RANGES
+ help
+ Say Y here to enable support for MT6370_RGB LED device.
+ In MT6370, there are four channel current-sink LED drivers that
+ support hardware pattern for constant current, PWM, and breath mode.
+ Isink4 channel can also be used as a CHG_VIN power good indicator.
+
+ This driver can also be built as a module. If so, the module
+ will be called "leds-mt6370-rgb".
+
+endif # LEDS_CLASS_MULTICOLOR
diff --git a/drivers/leds/rgb/Makefile b/drivers/leds/rgb/Makefile
new file mode 100644
index 000000000..cc0f2df66
--- /dev/null
+++ b/drivers/leds/rgb/Makefile
@@ -0,0 +1,12 @@
+# SPDX-License-Identifier: GPL-2.0
+
+obj-$(CONFIG_LEDS_GROUP_MULTICOLOR) += leds-group-multicolor.o
+obj-$(CONFIG_LEDS_KTD202X) += leds-ktd202x.o
+obj-$(CONFIG_LEDS_LP5812) += leds-lp5812.o
+obj-$(CONFIG_LEDS_LP5860_CORE) += leds-lp5860-core.o
+obj-$(CONFIG_LEDS_LP5860_SPI) += leds-lp5860-spi.o
+obj-$(CONFIG_LEDS_NCP5623) += leds-ncp5623.o
+obj-$(CONFIG_LEDS_PWM_MULTICOLOR) += leds-pwm-multicolor.o
+obj-$(CONFIG_LEDS_QCOM_LPG) += leds-qcom-lpg.o
+obj-$(CONFIG_LEDS_S2M_RGB) += leds-s2m-rgb.o
+obj-$(CONFIG_LEDS_MT6370_RGB) += leds-mt6370-rgb.o
diff --git a/drivers/leds/rgb/leds-group-multicolor.c b/drivers/leds/rgb/leds-group-multicolor.c
new file mode 100644
index 000000000..a707d51c6
--- /dev/null
+++ b/drivers/leds/rgb/leds-group-multicolor.c
@@ -0,0 +1,172 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Multi-color LED built with monochromatic LED devices
+ *
+ * This driver groups several monochromatic LED devices in a single multicolor LED device.
+ *
+ * Compared to handling this grouping in user-space, the benefits are:
+ * - The state of the monochromatic LED relative to each other is always consistent.
+ * - The sysfs interface of the LEDs can be used for the group as a whole.
+ *
+ * Copyright 2023 Jean-Jacques Hiblot <jjhiblot@traphandler.com>
+ */
+
+#include <linux/err.h>
+#include <linux/leds.h>
+#include <linux/led-class-multicolor.h>
+#include <linux/math.h>
+#include <linux/module.h>
+#include <linux/platform_device.h>
+#include <linux/property.h>
+
+struct leds_multicolor {
+ struct led_classdev_mc mc_cdev;
+ struct led_classdev **monochromatics;
+};
+
+static int leds_gmc_set(struct led_classdev *cdev, enum led_brightness brightness)
+{
+ struct led_classdev_mc *mc_cdev = lcdev_to_mccdev(cdev);
+ struct leds_multicolor *priv = container_of(mc_cdev, struct leds_multicolor, mc_cdev);
+ const unsigned int group_max_brightness = mc_cdev->led_cdev.max_brightness;
+ int i;
+
+ for (i = 0; i < mc_cdev->num_colors; i++) {
+ struct led_classdev *mono = priv->monochromatics[i];
+ const unsigned int mono_max_brightness = mono->max_brightness;
+ unsigned int intensity = mc_cdev->subled_info[i].intensity;
+ int mono_brightness;
+
+ /*
+ * Scale the brightness according to relative intensity of the
+ * color AND the max brightness of the monochromatic LED.
+ */
+ mono_brightness = DIV_ROUND_CLOSEST(brightness * intensity * mono_max_brightness,
+ group_max_brightness * group_max_brightness);
+
+ led_set_brightness(mono, mono_brightness);
+ }
+
+ return 0;
+}
+
+static void restore_sysfs_write_access(void *data)
+{
+ struct led_classdev *led_cdev = data;
+
+ /* Restore the write access to the LED */
+ mutex_lock(&led_cdev->led_access);
+ led_sysfs_enable(led_cdev);
+ mutex_unlock(&led_cdev->led_access);
+}
+
+static int leds_gmc_probe(struct platform_device *pdev)
+{
+ struct device *dev = &pdev->dev;
+ struct led_init_data init_data = {};
+ struct led_classdev *cdev;
+ struct mc_subled *subled;
+ struct leds_multicolor *priv;
+ unsigned int max_brightness = 0;
+ int i, ret, count = 0, common_flags = 0;
+
+ priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL);
+ if (!priv)
+ return -ENOMEM;
+
+ for (;;) {
+ struct led_classdev *led_cdev;
+
+ led_cdev = devm_of_led_get_optional(dev, count);
+ if (IS_ERR(led_cdev))
+ return dev_err_probe(dev, PTR_ERR(led_cdev), "Unable to get LED #%d",
+ count);
+ if (!led_cdev)
+ break;
+
+ priv->monochromatics = devm_krealloc_array(dev, priv->monochromatics,
+ count + 1, sizeof(*priv->monochromatics),
+ GFP_KERNEL);
+ if (!priv->monochromatics)
+ return -ENOMEM;
+
+ common_flags |= led_cdev->flags;
+ priv->monochromatics[count] = led_cdev;
+
+ max_brightness = max(max_brightness, led_cdev->max_brightness);
+
+ count++;
+ }
+
+ subled = devm_kcalloc(dev, count, sizeof(*subled), GFP_KERNEL);
+ if (!subled)
+ return -ENOMEM;
+ priv->mc_cdev.subled_info = subled;
+
+ for (i = 0; i < count; i++) {
+ struct led_classdev *led_cdev = priv->monochromatics[i];
+
+ subled[i].color_index = led_cdev->color;
+
+ /* Configure the LED intensity to its maximum */
+ subled[i].intensity = max_brightness;
+ }
+
+ /* Initialise the multicolor's LED class device */
+ cdev = &priv->mc_cdev.led_cdev;
+ cdev->brightness_set_blocking = leds_gmc_set;
+ cdev->max_brightness = max_brightness;
+ cdev->color = LED_COLOR_ID_MULTI;
+ priv->mc_cdev.num_colors = count;
+
+ /* we only need suspend/resume if a sub-led requests it */
+ if (common_flags & LED_CORE_SUSPENDRESUME)
+ cdev->flags = LED_CORE_SUSPENDRESUME;
+
+ init_data.fwnode = dev_fwnode(dev);
+ ret = devm_led_classdev_multicolor_register_ext(dev, &priv->mc_cdev, &init_data);
+ if (ret)
+ return dev_err_probe(dev, ret, "failed to register multicolor LED for %s.\n",
+ cdev->name);
+
+ ret = leds_gmc_set(cdev, cdev->brightness);
+ if (ret)
+ return dev_err_probe(dev, ret, "failed to set LED value for %s.", cdev->name);
+
+ for (i = 0; i < count; i++) {
+ struct led_classdev *led_cdev = priv->monochromatics[i];
+
+ /*
+ * Make the individual LED sysfs interface read-only to prevent the user
+ * to change the brightness of the individual LEDs of the group.
+ */
+ mutex_lock(&led_cdev->led_access);
+ led_sysfs_disable(led_cdev);
+ mutex_unlock(&led_cdev->led_access);
+
+ /* Restore the write access to the LED sysfs when the group is destroyed */
+ devm_add_action_or_reset(dev, restore_sysfs_write_access, led_cdev);
+ }
+
+ return 0;
+}
+
+static const struct of_device_id of_leds_group_multicolor_match[] = {
+ { .compatible = "leds-group-multicolor" },
+ {}
+};
+MODULE_DEVICE_TABLE(of, of_leds_group_multicolor_match);
+
+static struct platform_driver leds_group_multicolor_driver = {
+ .probe = leds_gmc_probe,
+ .driver = {
+ .name = "leds_group_multicolor",
+ .of_match_table = of_leds_group_multicolor_match,
+ }
+};
+module_platform_driver(leds_group_multicolor_driver);
+
+MODULE_AUTHOR("Jean-Jacques Hiblot <jjhiblot@traphandler.com>");
+MODULE_DESCRIPTION("LEDs group multicolor driver");
+MODULE_LICENSE("GPL");
+MODULE_ALIAS("platform:leds-group-multicolor");
diff --git a/drivers/leds/rgb/leds-ktd202x.c b/drivers/leds/rgb/leds-ktd202x.c
new file mode 100644
index 000000000..143020945
--- /dev/null
+++ b/drivers/leds/rgb/leds-ktd202x.c
@@ -0,0 +1,635 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Kinetic KTD2026/7 RGB/White LED driver with I2C interface
+ *
+ * Copyright 2023 André Apitzsch <git@apitzsch.eu>
+ *
+ * Datasheet: https://www.kinet-ic.com/uploads/KTD2026-7-04h.pdf
+ */
+
+#include <linux/i2c.h>
+#include <linux/led-class-multicolor.h>
+#include <linux/module.h>
+#include <linux/mutex.h>
+#include <linux/of.h>
+#include <linux/of_device.h>
+#include <linux/regmap.h>
+#include <linux/regulator/consumer.h>
+
+#define KTD2026_NUM_LEDS 3
+#define KTD2027_NUM_LEDS 4
+#define KTD202X_MAX_LEDS 4
+
+/* Register bank */
+#define KTD202X_REG_RESET_CONTROL 0x00
+#define KTD202X_REG_FLASH_PERIOD 0x01
+#define KTD202X_REG_PWM1_TIMER 0x02
+#define KTD202X_REG_PWM2_TIMER 0x03
+#define KTD202X_REG_CHANNEL_CTRL 0x04
+#define KTD202X_REG_TRISE_FALL 0x05
+#define KTD202X_REG_LED_IOUT(x) (0x06 + (x))
+
+/* Register 0 */
+#define KTD202X_TIMER_SLOT_CONTROL_TSLOT1 0x00
+#define KTD202X_TIMER_SLOT_CONTROL_TSLOT2 0x01
+#define KTD202X_TIMER_SLOT_CONTROL_TSLOT3 0x02
+#define KTD202X_TIMER_SLOT_CONTROL_TSLOT4 0x03
+#define KTD202X_RSTR_RESET 0x07
+
+#define KTD202X_ENABLE_CTRL_WAKE 0x00 /* SCL High & SDA High */
+#define KTD202X_ENABLE_CTRL_SLEEP 0x08 /* SCL High & SDA Toggling */
+
+#define KTD202X_TRISE_FALL_SCALE_NORMAL 0x00
+#define KTD202X_TRISE_FALL_SCALE_SLOW_X2 0x20
+#define KTD202X_TRISE_FALL_SCALE_SLOW_X4 0x40
+#define KTD202X_TRISE_FALL_SCALE_FAST_X8 0x60
+
+/* Register 1 */
+#define KTD202X_FLASH_PERIOD_256_MS_LOG_RAMP 0x00
+
+/* Register 2-3 */
+#define KTD202X_FLASH_ON_TIME_0_4_PERCENT 0x01
+
+/* Register 4 */
+#define KTD202X_CHANNEL_CTRL_MASK(x) (BIT(2 * (x)) | BIT(2 * (x) + 1))
+#define KTD202X_CHANNEL_CTRL_OFF 0x00
+#define KTD202X_CHANNEL_CTRL_ON(x) BIT(2 * (x))
+#define KTD202X_CHANNEL_CTRL_PWM1(x) BIT(2 * (x) + 1)
+#define KTD202X_CHANNEL_CTRL_PWM2(x) (BIT(2 * (x)) | BIT(2 * (x) + 1))
+
+/* Register 5 */
+#define KTD202X_RAMP_TIMES_2_MS 0x00
+
+/* Register 6-9 */
+#define KTD202X_LED_CURRENT_10_mA 0x4f
+
+#define KTD202X_FLASH_PERIOD_MIN_MS 256
+#define KTD202X_FLASH_PERIOD_STEP_MS 128
+#define KTD202X_FLASH_PERIOD_MAX_STEPS 126
+#define KTD202X_FLASH_ON_MAX 256
+
+#define KTD202X_MAX_BRIGHTNESS 192
+
+static const struct reg_default ktd202x_reg_defaults[] = {
+ { KTD202X_REG_RESET_CONTROL, KTD202X_TIMER_SLOT_CONTROL_TSLOT1 |
+ KTD202X_ENABLE_CTRL_WAKE | KTD202X_TRISE_FALL_SCALE_NORMAL },
+ { KTD202X_REG_FLASH_PERIOD, KTD202X_FLASH_PERIOD_256_MS_LOG_RAMP },
+ { KTD202X_REG_PWM1_TIMER, KTD202X_FLASH_ON_TIME_0_4_PERCENT },
+ { KTD202X_REG_PWM2_TIMER, KTD202X_FLASH_ON_TIME_0_4_PERCENT },
+ { KTD202X_REG_CHANNEL_CTRL, KTD202X_CHANNEL_CTRL_OFF },
+ { KTD202X_REG_TRISE_FALL, KTD202X_RAMP_TIMES_2_MS },
+ { KTD202X_REG_LED_IOUT(0), KTD202X_LED_CURRENT_10_mA },
+ { KTD202X_REG_LED_IOUT(1), KTD202X_LED_CURRENT_10_mA },
+ { KTD202X_REG_LED_IOUT(2), KTD202X_LED_CURRENT_10_mA },
+ { KTD202X_REG_LED_IOUT(3), KTD202X_LED_CURRENT_10_mA },
+};
+
+struct ktd202x_led {
+ struct ktd202x *chip;
+ union {
+ struct led_classdev cdev;
+ struct led_classdev_mc mcdev;
+ };
+ u32 index;
+};
+
+struct ktd202x {
+ struct mutex mutex;
+ struct regulator_bulk_data regulators[2];
+ struct device *dev;
+ struct regmap *regmap;
+ bool enabled;
+ unsigned long num_leds;
+ struct ktd202x_led leds[] __counted_by(num_leds);
+};
+
+static int ktd202x_chip_disable(struct ktd202x *chip)
+{
+ int ret;
+
+ if (!chip->enabled)
+ return 0;
+
+ regmap_write(chip->regmap, KTD202X_REG_RESET_CONTROL, KTD202X_ENABLE_CTRL_SLEEP);
+
+ ret = regulator_bulk_disable(ARRAY_SIZE(chip->regulators), chip->regulators);
+ if (ret) {
+ dev_err(chip->dev, "Failed to disable regulators: %d\n", ret);
+ return ret;
+ }
+
+ chip->enabled = false;
+ return 0;
+}
+
+static int ktd202x_chip_enable(struct ktd202x *chip)
+{
+ int ret;
+
+ if (chip->enabled)
+ return 0;
+
+ ret = regulator_bulk_enable(ARRAY_SIZE(chip->regulators), chip->regulators);
+ if (ret) {
+ dev_err(chip->dev, "Failed to enable regulators: %d\n", ret);
+ return ret;
+ }
+ chip->enabled = true;
+
+ ret = regmap_write(chip->regmap, KTD202X_REG_RESET_CONTROL, KTD202X_ENABLE_CTRL_WAKE);
+
+ if (ret) {
+ dev_err(chip->dev, "Failed to enable the chip: %d\n", ret);
+ ktd202x_chip_disable(chip);
+ }
+
+ return ret;
+}
+
+static bool ktd202x_chip_in_use(struct ktd202x *chip)
+{
+ int i;
+
+ for (i = 0; i < chip->num_leds; i++) {
+ if (chip->leds[i].cdev.brightness)
+ return true;
+ }
+
+ return false;
+}
+
+static int ktd202x_brightness_set(struct ktd202x_led *led,
+ struct mc_subled *subleds,
+ unsigned int num_channels)
+{
+ bool mode_blink = false;
+ int channel;
+ int state;
+ int ret;
+ int i;
+
+ if (ktd202x_chip_in_use(led->chip)) {
+ ret = ktd202x_chip_enable(led->chip);
+ if (ret)
+ return ret;
+ }
+
+ ret = regmap_read(led->chip->regmap, KTD202X_REG_CHANNEL_CTRL, &state);
+ if (ret)
+ return ret;
+
+ /*
+ * In multicolor case, assume blink mode if PWM is set for at least one
+ * channel because another channel cannot be in state ON at the same time
+ */
+ for (i = 0; i < num_channels; i++) {
+ int channel_state;
+
+ channel = subleds[i].channel;
+ channel_state = (state >> 2 * channel) & KTD202X_CHANNEL_CTRL_MASK(0);
+ if (channel_state == KTD202X_CHANNEL_CTRL_OFF)
+ continue;
+ mode_blink = channel_state == KTD202X_CHANNEL_CTRL_PWM1(0);
+ break;
+ }
+
+ for (i = 0; i < num_channels; i++) {
+ enum led_brightness brightness;
+ int mode;
+
+ brightness = subleds[i].brightness;
+ channel = subleds[i].channel;
+
+ if (brightness) {
+ /* Register expects brightness between 0 and MAX_BRIGHTNESS - 1 */
+ ret = regmap_write(led->chip->regmap, KTD202X_REG_LED_IOUT(channel),
+ brightness - 1);
+ if (ret)
+ return ret;
+
+ if (mode_blink)
+ mode = KTD202X_CHANNEL_CTRL_PWM1(channel);
+ else
+ mode = KTD202X_CHANNEL_CTRL_ON(channel);
+ } else {
+ mode = KTD202X_CHANNEL_CTRL_OFF;
+ }
+ ret = regmap_update_bits(led->chip->regmap, KTD202X_REG_CHANNEL_CTRL,
+ KTD202X_CHANNEL_CTRL_MASK(channel), mode);
+ if (ret)
+ return ret;
+ }
+
+ if (!ktd202x_chip_in_use(led->chip))
+ return ktd202x_chip_disable(led->chip);
+
+ return 0;
+}
+
+static int ktd202x_brightness_single_set(struct led_classdev *cdev,
+ enum led_brightness value)
+{
+ struct ktd202x_led *led = container_of(cdev, struct ktd202x_led, cdev);
+ struct mc_subled info;
+ int ret;
+
+ cdev->brightness = value;
+
+ mutex_lock(&led->chip->mutex);
+
+ info.brightness = value;
+ info.channel = led->index;
+ ret = ktd202x_brightness_set(led, &info, 1);
+
+ mutex_unlock(&led->chip->mutex);
+
+ return ret;
+}
+
+static int ktd202x_brightness_mc_set(struct led_classdev *cdev,
+ enum led_brightness value)
+{
+ struct led_classdev_mc *mc = lcdev_to_mccdev(cdev);
+ struct ktd202x_led *led = container_of(mc, struct ktd202x_led, mcdev);
+ int ret;
+
+ cdev->brightness = value;
+
+ mutex_lock(&led->chip->mutex);
+
+ led_mc_calc_color_components(mc, value);
+ ret = ktd202x_brightness_set(led, mc->subled_info, mc->num_colors);
+
+ mutex_unlock(&led->chip->mutex);
+
+ return ret;
+}
+
+static int ktd202x_blink_set(struct ktd202x_led *led, unsigned long *delay_on,
+ unsigned long *delay_off, struct mc_subled *subleds,
+ unsigned int num_channels)
+{
+ unsigned long delay_total_ms;
+ int ret, num_steps, on;
+ u8 ctrl_mask = 0;
+ u8 ctrl_pwm1 = 0;
+ u8 ctrl_on = 0;
+ int i;
+
+ mutex_lock(&led->chip->mutex);
+
+ for (i = 0; i < num_channels; i++) {
+ int channel = subleds[i].channel;
+
+ ctrl_mask |= KTD202X_CHANNEL_CTRL_MASK(channel);
+ ctrl_on |= KTD202X_CHANNEL_CTRL_ON(channel);
+ ctrl_pwm1 |= KTD202X_CHANNEL_CTRL_PWM1(channel);
+ }
+
+ /* Never off - brightness is already set, disable blinking */
+ if (!*delay_off) {
+ ret = regmap_update_bits(led->chip->regmap, KTD202X_REG_CHANNEL_CTRL,
+ ctrl_mask, ctrl_on);
+ goto out;
+ }
+
+ /* Convert into values the HW will understand. */
+
+ /* Integer representation of time of flash period */
+ num_steps = (*delay_on + *delay_off - KTD202X_FLASH_PERIOD_MIN_MS) /
+ KTD202X_FLASH_PERIOD_STEP_MS;
+ num_steps = clamp(num_steps, 0, KTD202X_FLASH_PERIOD_MAX_STEPS);
+
+ /* Integer representation of percentage of LED ON time */
+ on = (*delay_on * KTD202X_FLASH_ON_MAX) / (*delay_on + *delay_off);
+
+ /* Actually used delay_{on,off} values */
+ delay_total_ms = num_steps * KTD202X_FLASH_PERIOD_STEP_MS + KTD202X_FLASH_PERIOD_MIN_MS;
+ *delay_on = (delay_total_ms * on) / KTD202X_FLASH_ON_MAX;
+ *delay_off = delay_total_ms - *delay_on;
+
+ /* Set timings */
+ ret = regmap_write(led->chip->regmap, KTD202X_REG_FLASH_PERIOD, num_steps);
+ if (ret)
+ goto out;
+
+ ret = regmap_write(led->chip->regmap, KTD202X_REG_PWM1_TIMER, on);
+ if (ret)
+ goto out;
+
+ ret = regmap_update_bits(led->chip->regmap, KTD202X_REG_CHANNEL_CTRL,
+ ctrl_mask, ctrl_pwm1);
+out:
+ mutex_unlock(&led->chip->mutex);
+ return ret;
+}
+
+static int ktd202x_blink_single_set(struct led_classdev *cdev,
+ unsigned long *delay_on,
+ unsigned long *delay_off)
+{
+ struct ktd202x_led *led = container_of(cdev, struct ktd202x_led, cdev);
+ struct mc_subled info;
+ int ret;
+
+ if (!cdev->brightness) {
+ ret = ktd202x_brightness_single_set(cdev, KTD202X_MAX_BRIGHTNESS);
+ if (ret)
+ return ret;
+ }
+
+ /* If no blink specified, default to 1 Hz. */
+ if (!*delay_off && !*delay_on) {
+ *delay_off = 500;
+ *delay_on = 500;
+ }
+
+ /* Never on - just set to off */
+ if (!*delay_on)
+ return ktd202x_brightness_single_set(cdev, LED_OFF);
+
+ info.channel = led->index;
+
+ return ktd202x_blink_set(led, delay_on, delay_off, &info, 1);
+}
+
+static int ktd202x_blink_mc_set(struct led_classdev *cdev,
+ unsigned long *delay_on,
+ unsigned long *delay_off)
+{
+ struct led_classdev_mc *mc = lcdev_to_mccdev(cdev);
+ struct ktd202x_led *led = container_of(mc, struct ktd202x_led, mcdev);
+ int ret;
+
+ if (!cdev->brightness) {
+ ret = ktd202x_brightness_mc_set(cdev, KTD202X_MAX_BRIGHTNESS);
+ if (ret)
+ return ret;
+ }
+
+ /* If no blink specified, default to 1 Hz. */
+ if (!*delay_off && !*delay_on) {
+ *delay_off = 500;
+ *delay_on = 500;
+ }
+
+ /* Never on - just set to off */
+ if (!*delay_on)
+ return ktd202x_brightness_mc_set(cdev, LED_OFF);
+
+ return ktd202x_blink_set(led, delay_on, delay_off, mc->subled_info,
+ mc->num_colors);
+}
+
+static int ktd202x_setup_led_rgb(struct ktd202x *chip, struct fwnode_handle *fwnode,
+ struct ktd202x_led *led, struct led_init_data *init_data)
+{
+ struct fwnode_handle *child;
+ struct led_classdev *cdev;
+ struct mc_subled *info;
+ int num_channels;
+ int i = 0;
+
+ num_channels = 0;
+ fwnode_for_each_child_node(fwnode, child)
+ num_channels++;
+
+ if (!num_channels || num_channels > chip->num_leds)
+ return -EINVAL;
+
+ info = devm_kcalloc(chip->dev, num_channels, sizeof(*info), GFP_KERNEL);
+ if (!info)
+ return -ENOMEM;
+
+ fwnode_for_each_child_node(fwnode, child) {
+ u32 mono_color;
+ u32 reg;
+ int ret;
+
+ ret = fwnode_property_read_u32(child, "reg", &reg);
+ if (ret != 0 || reg >= chip->num_leds) {
+ dev_err(chip->dev, "invalid 'reg' of %pfw\n", child);
+ fwnode_handle_put(child);
+ return ret;
+ }
+
+ ret = fwnode_property_read_u32(child, "color", &mono_color);
+ if (ret < 0 && ret != -EINVAL) {
+ dev_err(chip->dev, "failed to parse 'color' of %pfw\n", child);
+ fwnode_handle_put(child);
+ return ret;
+ }
+
+ info[i].color_index = mono_color;
+ info[i].channel = reg;
+ info[i].intensity = KTD202X_MAX_BRIGHTNESS;
+ i++;
+ }
+
+ led->mcdev.subled_info = info;
+ led->mcdev.num_colors = num_channels;
+
+ cdev = &led->mcdev.led_cdev;
+ cdev->brightness_set_blocking = ktd202x_brightness_mc_set;
+ cdev->blink_set = ktd202x_blink_mc_set;
+
+ return devm_led_classdev_multicolor_register_ext(chip->dev, &led->mcdev, init_data);
+}
+
+static int ktd202x_setup_led_single(struct ktd202x *chip, struct fwnode_handle *fwnode,
+ struct ktd202x_led *led, struct led_init_data *init_data)
+{
+ struct led_classdev *cdev;
+ u32 reg;
+ int ret;
+
+ ret = fwnode_property_read_u32(fwnode, "reg", &reg);
+ if (ret != 0 || reg >= chip->num_leds) {
+ dev_err(chip->dev, "invalid 'reg' of %pfw\n", fwnode);
+ return -EINVAL;
+ }
+ led->index = reg;
+
+ cdev = &led->cdev;
+ cdev->brightness_set_blocking = ktd202x_brightness_single_set;
+ cdev->blink_set = ktd202x_blink_single_set;
+
+ return devm_led_classdev_register_ext(chip->dev, &led->cdev, init_data);
+}
+
+static int ktd202x_add_led(struct ktd202x *chip, struct fwnode_handle *fwnode, unsigned int index)
+{
+ struct ktd202x_led *led = &chip->leds[index];
+ struct led_init_data init_data = {};
+ struct led_classdev *cdev;
+ u32 color;
+ int ret;
+
+ /* Color property is optional in single color case */
+ ret = fwnode_property_read_u32(fwnode, "color", &color);
+ if (ret < 0 && ret != -EINVAL) {
+ dev_err(chip->dev, "failed to parse 'color' of %pfw\n", fwnode);
+ return ret;
+ }
+
+ led->chip = chip;
+ init_data.fwnode = fwnode;
+
+ if (color == LED_COLOR_ID_RGB) {
+ cdev = &led->mcdev.led_cdev;
+ ret = ktd202x_setup_led_rgb(chip, fwnode, led, &init_data);
+ } else {
+ cdev = &led->cdev;
+ ret = ktd202x_setup_led_single(chip, fwnode, led, &init_data);
+ }
+
+ if (ret) {
+ dev_err(chip->dev, "unable to register %s\n", cdev->name);
+ return ret;
+ }
+
+ cdev->max_brightness = KTD202X_MAX_BRIGHTNESS;
+
+ return 0;
+}
+
+static int ktd202x_probe_fw(struct ktd202x *chip)
+{
+ struct device *dev = chip->dev;
+ int count;
+ int i = 0;
+
+ count = device_get_child_node_count(dev);
+ if (!count || count > chip->num_leds)
+ return -EINVAL;
+
+ regmap_write(chip->regmap, KTD202X_REG_RESET_CONTROL, KTD202X_RSTR_RESET);
+
+ /* Allow the device to execute the complete reset */
+ usleep_range(200, 300);
+
+ device_for_each_child_node_scoped(dev, child) {
+ int ret = ktd202x_add_led(chip, child, i);
+
+ if (ret)
+ return ret;
+
+ i++;
+ }
+
+ return 0;
+}
+
+static const struct regmap_config ktd202x_regmap_config = {
+ .reg_bits = 8,
+ .val_bits = 8,
+ .max_register = 0x09,
+ .cache_type = REGCACHE_FLAT,
+ .reg_defaults = ktd202x_reg_defaults,
+ .num_reg_defaults = ARRAY_SIZE(ktd202x_reg_defaults),
+};
+
+static int ktd202x_probe(struct i2c_client *client)
+{
+ struct device *dev = &client->dev;
+ struct ktd202x *chip;
+ int count;
+ int ret;
+
+ count = device_get_child_node_count(dev);
+ if (!count || count > KTD202X_MAX_LEDS)
+ return dev_err_probe(dev, -EINVAL, "Incorrect number of leds (%d)", count);
+
+ chip = devm_kzalloc(dev, struct_size(chip, leds, count), GFP_KERNEL);
+ if (!chip)
+ return -ENOMEM;
+
+ chip->dev = dev;
+ i2c_set_clientdata(client, chip);
+
+ chip->regmap = devm_regmap_init_i2c(client, &ktd202x_regmap_config);
+ if (IS_ERR(chip->regmap)) {
+ ret = dev_err_probe(dev, PTR_ERR(chip->regmap),
+ "Failed to allocate register map.\n");
+ return ret;
+ }
+
+ ret = devm_mutex_init(dev, &chip->mutex);
+ if (ret)
+ return ret;
+
+ chip->num_leds = (unsigned long)i2c_get_match_data(client);
+
+ chip->regulators[0].supply = "vin";
+ chip->regulators[1].supply = "vio";
+ ret = devm_regulator_bulk_get(dev, ARRAY_SIZE(chip->regulators), chip->regulators);
+ if (ret < 0) {
+ dev_err_probe(dev, ret, "Failed to request regulators.\n");
+ return ret;
+ }
+
+ ret = regulator_bulk_enable(ARRAY_SIZE(chip->regulators), chip->regulators);
+ if (ret) {
+ dev_err_probe(dev, ret, "Failed to enable regulators.\n");
+ return ret;
+ }
+
+ ret = ktd202x_probe_fw(chip);
+ if (ret < 0) {
+ regulator_bulk_disable(ARRAY_SIZE(chip->regulators), chip->regulators);
+ return ret;
+ }
+
+ ret = regulator_bulk_disable(ARRAY_SIZE(chip->regulators), chip->regulators);
+ if (ret) {
+ dev_err_probe(dev, ret, "Failed to disable regulators.\n");
+ return ret;
+ }
+
+ return 0;
+}
+
+static void ktd202x_remove(struct i2c_client *client)
+{
+ struct ktd202x *chip = i2c_get_clientdata(client);
+
+ ktd202x_chip_disable(chip);
+}
+
+static void ktd202x_shutdown(struct i2c_client *client)
+{
+ struct ktd202x *chip = i2c_get_clientdata(client);
+
+ /* Reset registers to make sure all LEDs are off before shutdown */
+ regmap_write(chip->regmap, KTD202X_REG_RESET_CONTROL, KTD202X_RSTR_RESET);
+}
+
+static const struct i2c_device_id ktd202x_id[] = {
+ { .name = "ktd2026", .driver_data = KTD2026_NUM_LEDS },
+ { .name = "ktd2027", .driver_data = KTD2027_NUM_LEDS },
+ { }
+};
+MODULE_DEVICE_TABLE(i2c, ktd202x_id);
+
+static const struct of_device_id ktd202x_match_table[] = {
+ { .compatible = "kinetic,ktd2026", .data = (void *)KTD2026_NUM_LEDS },
+ { .compatible = "kinetic,ktd2027", .data = (void *)KTD2027_NUM_LEDS },
+ {}
+};
+MODULE_DEVICE_TABLE(of, ktd202x_match_table);
+
+static struct i2c_driver ktd202x_driver = {
+ .driver = {
+ .name = "leds-ktd202x",
+ .of_match_table = ktd202x_match_table,
+ },
+ .probe = ktd202x_probe,
+ .remove = ktd202x_remove,
+ .shutdown = ktd202x_shutdown,
+ .id_table = ktd202x_id,
+};
+module_i2c_driver(ktd202x_driver);
+
+MODULE_AUTHOR("André Apitzsch <git@apitzsch.eu>");
+MODULE_DESCRIPTION("Kinetic KTD2026/7 LED driver");
+MODULE_LICENSE("GPL");
diff --git a/drivers/leds/rgb/leds-lp5812.c b/drivers/leds/rgb/leds-lp5812.c
new file mode 100644
index 000000000..ce6d70364
--- /dev/null
+++ b/drivers/leds/rgb/leds-lp5812.c
@@ -0,0 +1,642 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * LP5812 LED driver
+ *
+ * Copyright (C) 2025 Texas Instruments
+ *
+ * Author: Jared Zhou <jared-zhou@ti.com>
+ */
+
+#include <linux/delay.h>
+#include <linux/i2c.h>
+#include <linux/init.h>
+#include <linux/kernel.h>
+#include <linux/led-class-multicolor.h>
+#include <linux/leds.h>
+#include <linux/module.h>
+#include <linux/mutex.h>
+#include <linux/sysfs.h>
+#include <linux/types.h>
+
+#include "leds-lp5812.h"
+
+static const struct lp5812_mode_mapping chip_mode_map[] = {
+ {"direct_mode", 0, 0, 0, 0, 0, 0},
+ {"tcm:1:0", 1, 0, 0, 0, 0, 0},
+ {"tcm:1:1", 1, 1, 0, 0, 0, 0},
+ {"tcm:1:2", 1, 2, 0, 0, 0, 0},
+ {"tcm:1:3", 1, 3, 0, 0, 0, 0},
+ {"tcm:2:0:1", 2, 0, 1, 0, 0, 0},
+ {"tcm:2:0:2", 2, 0, 2, 0, 0, 0},
+ {"tcm:2:0:3", 2, 0, 3, 0, 0, 0},
+ {"tcm:2:1:2", 2, 1, 2, 0, 0, 0},
+ {"tcm:2:1:3", 2, 1, 3, 0, 0, 0},
+ {"tcm:2:2:3", 2, 2, 3, 0, 0, 0},
+ {"tcm:3:0:1:2", 3, 0, 1, 2, 0, 0},
+ {"tcm:3:0:1:3", 3, 0, 1, 3, 0, 0},
+ {"tcm:3:0:2:3", 3, 0, 2, 3, 0, 0},
+ {"tcm:4:0:1:2:3", 4, 0, 1, 2, 3, 0},
+ {"mix:1:0:1", 5, 1, 0, 0, 0, 0},
+ {"mix:1:0:2", 5, 2, 0, 0, 0, 0},
+ {"mix:1:0:3", 5, 3, 0, 0, 0, 0},
+ {"mix:1:1:0", 5, 0, 0, 0, 0, 1},
+ {"mix:1:1:2", 5, 2, 0, 0, 0, 1},
+ {"mix:1:1:3", 5, 3, 0, 0, 0, 1},
+ {"mix:1:2:0", 5, 0, 0, 0, 0, 2},
+ {"mix:1:2:1", 5, 1, 0, 0, 0, 2},
+ {"mix:1:2:3", 5, 3, 0, 0, 0, 2},
+ {"mix:1:3:0", 5, 0, 0, 0, 0, 3},
+ {"mix:1:3:1", 5, 1, 0, 0, 0, 3},
+ {"mix:1:3:2", 5, 2, 0, 0, 0, 3},
+ {"mix:2:0:1:2", 6, 1, 2, 0, 0, 0},
+ {"mix:2:0:1:3", 6, 1, 3, 0, 0, 0},
+ {"mix:2:0:2:3", 6, 2, 3, 0, 0, 0},
+ {"mix:2:1:0:2", 6, 0, 2, 0, 0, 1},
+ {"mix:2:1:0:3", 6, 0, 3, 0, 0, 1},
+ {"mix:2:1:2:3", 6, 2, 3, 0, 0, 1},
+ {"mix:2:2:0:1", 6, 0, 1, 0, 0, 2},
+ {"mix:2:2:0:3", 6, 0, 3, 0, 0, 2},
+ {"mix:2:2:1:3", 6, 1, 3, 0, 0, 2},
+ {"mix:2:3:0:1", 6, 0, 1, 0, 0, 3},
+ {"mix:2:3:0:2", 6, 0, 2, 0, 0, 3},
+ {"mix:2:3:1:2", 6, 1, 2, 0, 0, 3},
+ {"mix:3:0:1:2:3", 7, 1, 2, 3, 0, 0},
+ {"mix:3:1:0:2:3", 7, 0, 2, 3, 0, 1},
+ {"mix:3:2:0:1:3", 7, 0, 1, 3, 0, 2},
+ {"mix:3:3:0:1:2", 7, 0, 1, 2, 0, 3}
+};
+
+static int lp5812_write(struct lp5812_chip *chip, u16 reg, u8 val)
+{
+ struct device *dev = &chip->client->dev;
+ struct i2c_msg msg;
+ u8 buf[LP5812_DATA_LENGTH];
+ u8 reg_addr_bit8_9;
+ int ret;
+
+ /* Extract register address bits 9 and 8 for Address Byte 1 */
+ reg_addr_bit8_9 = (reg >> LP5812_REG_ADDR_HIGH_SHIFT) & LP5812_REG_ADDR_BIT_8_9_MASK;
+
+ /* Prepare payload: Address Byte 2 (bits [7:0]) and value to write */
+ buf[LP5812_DATA_BYTE_0_IDX] = (u8)(reg & LP5812_REG_ADDR_LOW_MASK);
+ buf[LP5812_DATA_BYTE_1_IDX] = val;
+
+ /* Construct I2C message for a write operation */
+ msg.addr = (chip->client->addr << LP5812_CHIP_ADDR_SHIFT) | reg_addr_bit8_9;
+ msg.flags = 0;
+ msg.len = sizeof(buf);
+ msg.buf = buf;
+
+ ret = i2c_transfer(chip->client->adapter, &msg, 1);
+ if (ret == 1)
+ return 0;
+
+ dev_err(dev, "I2C write error, ret=%d\n", ret);
+ return ret < 0 ? ret : -EIO;
+}
+
+static int lp5812_read(struct lp5812_chip *chip, u16 reg, u8 *val)
+{
+ struct device *dev = &chip->client->dev;
+ struct i2c_msg msgs[LP5812_READ_MSG_LENGTH];
+ u8 ret_val;
+ u8 reg_addr_bit8_9;
+ u8 converted_reg;
+ int ret;
+
+ /* Extract register address bits 9 and 8 for Address Byte 1 */
+ reg_addr_bit8_9 = (reg >> LP5812_REG_ADDR_HIGH_SHIFT) & LP5812_REG_ADDR_BIT_8_9_MASK;
+
+ /* Lower 8 bits go in Address Byte 2 */
+ converted_reg = (u8)(reg & LP5812_REG_ADDR_LOW_MASK);
+
+ /* Prepare I2C write message to set register address */
+ msgs[LP5812_MSG_0_IDX].addr =
+ (chip->client->addr << LP5812_CHIP_ADDR_SHIFT) | reg_addr_bit8_9;
+ msgs[LP5812_MSG_0_IDX].flags = 0;
+ msgs[LP5812_MSG_0_IDX].len = 1;
+ msgs[LP5812_MSG_0_IDX].buf = &converted_reg;
+
+ /* Prepare I2C read message to retrieve register value */
+ msgs[LP5812_MSG_1_IDX].addr =
+ (chip->client->addr << LP5812_CHIP_ADDR_SHIFT) | reg_addr_bit8_9;
+ msgs[LP5812_MSG_1_IDX].flags = I2C_M_RD;
+ msgs[LP5812_MSG_1_IDX].len = 1;
+ msgs[LP5812_MSG_1_IDX].buf = &ret_val;
+
+ ret = i2c_transfer(chip->client->adapter, msgs, LP5812_READ_MSG_LENGTH);
+ if (ret == LP5812_READ_MSG_LENGTH) {
+ *val = ret_val;
+ return 0;
+ }
+
+ dev_err(dev, "I2C read error, ret=%d\n", ret);
+ *val = 0;
+ return ret < 0 ? ret : -EIO;
+}
+
+static int lp5812_read_tsd_config_status(struct lp5812_chip *chip, u8 *reg_val)
+{
+ return lp5812_read(chip, LP5812_TSD_CONFIG_STATUS, reg_val);
+}
+
+static int lp5812_update_regs_config(struct lp5812_chip *chip)
+{
+ u8 reg_val;
+ int ret;
+
+ ret = lp5812_write(chip, LP5812_CMD_UPDATE, LP5812_UPDATE_CMD_VAL);
+ if (ret)
+ return ret;
+
+ ret = lp5812_read_tsd_config_status(chip, &reg_val);
+ if (ret)
+ return ret;
+
+ return reg_val & LP5812_CFG_ERR_STATUS_MASK;
+}
+
+static ssize_t parse_drive_mode(struct lp5812_chip *chip, const char *str)
+{
+ int i;
+
+ chip->drive_mode.bits.mix_sel_led_0 = false;
+ chip->drive_mode.bits.mix_sel_led_1 = false;
+ chip->drive_mode.bits.mix_sel_led_2 = false;
+ chip->drive_mode.bits.mix_sel_led_3 = false;
+
+ if (sysfs_streq(str, LP5812_MODE_DIRECT_NAME)) {
+ chip->drive_mode.bits.led_mode = LP5812_MODE_DIRECT_VALUE;
+ return 0;
+ }
+
+ for (i = 0; i < ARRAY_SIZE(chip_mode_map); i++) {
+ if (!sysfs_streq(str, chip_mode_map[i].mode_name))
+ continue;
+
+ chip->drive_mode.bits.led_mode = chip_mode_map[i].mode;
+ chip->scan_order.bits.order0 = chip_mode_map[i].scan_order_0;
+ chip->scan_order.bits.order1 = chip_mode_map[i].scan_order_1;
+ chip->scan_order.bits.order2 = chip_mode_map[i].scan_order_2;
+ chip->scan_order.bits.order3 = chip_mode_map[i].scan_order_3;
+
+ switch (chip_mode_map[i].selection_led) {
+ case LP5812_MODE_MIX_SELECT_LED_0:
+ chip->drive_mode.bits.mix_sel_led_0 = true;
+ break;
+ case LP5812_MODE_MIX_SELECT_LED_1:
+ chip->drive_mode.bits.mix_sel_led_1 = true;
+ break;
+ case LP5812_MODE_MIX_SELECT_LED_2:
+ chip->drive_mode.bits.mix_sel_led_2 = true;
+ break;
+ case LP5812_MODE_MIX_SELECT_LED_3:
+ chip->drive_mode.bits.mix_sel_led_3 = true;
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ return 0;
+ }
+
+ return -EINVAL;
+}
+
+static int lp5812_set_drive_mode_scan_order(struct lp5812_chip *chip)
+{
+ u8 val;
+ int ret;
+
+ val = chip->drive_mode.val;
+ ret = lp5812_write(chip, LP5812_DEV_CONFIG1, val);
+ if (ret)
+ return ret;
+
+ val = chip->scan_order.val;
+ ret = lp5812_write(chip, LP5812_DEV_CONFIG2, val);
+
+ return ret;
+}
+
+static int lp5812_set_led_mode(struct lp5812_chip *chip, int led_number,
+ enum control_mode mode)
+{
+ u8 reg_val;
+ u16 reg;
+ int ret;
+
+ /*
+ * Select device configuration register.
+ * Reg3 for LED_0–LED_3, LED_A0–A2, LED_B0
+ * Reg4 for LED_B1–B2, LED_C0–C2, LED_D0–D2
+ */
+ if (led_number < LP5812_NUMBER_LED_IN_REG)
+ reg = LP5812_DEV_CONFIG3;
+ else
+ reg = LP5812_DEV_CONFIG4;
+
+ ret = lp5812_read(chip, reg, &reg_val);
+ if (ret)
+ return ret;
+
+ if (mode == LP5812_MODE_MANUAL)
+ reg_val &= ~(LP5812_ENABLE << (led_number % LP5812_NUMBER_LED_IN_REG));
+ else
+ reg_val |= (LP5812_ENABLE << (led_number % LP5812_NUMBER_LED_IN_REG));
+
+ ret = lp5812_write(chip, reg, reg_val);
+ if (ret)
+ return ret;
+
+ ret = lp5812_update_regs_config(chip);
+
+ return ret;
+}
+
+static int lp5812_manual_dc_pwm_control(struct lp5812_chip *chip, int led_number,
+ u8 val, enum dimming_type dimming_type)
+{
+ u16 led_base_reg;
+ int ret;
+
+ if (dimming_type == LP5812_DIMMING_ANALOG)
+ led_base_reg = LP5812_MANUAL_DC_BASE;
+ else
+ led_base_reg = LP5812_MANUAL_PWM_BASE;
+
+ ret = lp5812_write(chip, led_base_reg + led_number, val);
+
+ return ret;
+}
+
+static int lp5812_multicolor_brightness(struct lp5812_led *led)
+{
+ struct lp5812_chip *chip = led->chip;
+ int ret, i;
+
+ guard(mutex)(&chip->lock);
+ for (i = 0; i < led->mc_cdev.num_colors; i++) {
+ ret = lp5812_manual_dc_pwm_control(chip, led->mc_cdev.subled_info[i].channel,
+ led->mc_cdev.subled_info[i].brightness,
+ LP5812_DIMMING_PWM);
+ if (ret)
+ return ret;
+ }
+
+ return 0;
+}
+
+static int lp5812_led_brightness(struct lp5812_led *led)
+{
+ struct lp5812_chip *chip = led->chip;
+ struct lp5812_led_config *led_cfg;
+ int ret;
+
+ led_cfg = &chip->led_config[led->chan_nr];
+
+ guard(mutex)(&chip->lock);
+ ret = lp5812_manual_dc_pwm_control(chip, led_cfg->led_id[0],
+ led->brightness, LP5812_DIMMING_PWM);
+
+ return ret;
+}
+
+static int lp5812_set_brightness(struct led_classdev *cdev,
+ enum led_brightness brightness)
+{
+ struct lp5812_led *led = container_of(cdev, struct lp5812_led, cdev);
+
+ led->brightness = (u8)brightness;
+
+ return lp5812_led_brightness(led);
+}
+
+static int lp5812_set_mc_brightness(struct led_classdev *cdev,
+ enum led_brightness brightness)
+{
+ struct led_classdev_mc *mc_dev = lcdev_to_mccdev(cdev);
+ struct lp5812_led *led = container_of(mc_dev, struct lp5812_led, mc_cdev);
+
+ led_mc_calc_color_components(&led->mc_cdev, brightness);
+
+ return lp5812_multicolor_brightness(led);
+}
+
+static int lp5812_init_led(struct lp5812_led *led, struct lp5812_chip *chip, int chan)
+{
+ struct device *dev = &chip->client->dev;
+ struct mc_subled *mc_led_info;
+ struct led_classdev *led_cdev;
+ int i, ret;
+
+ if (chip->led_config[chan].name) {
+ led->cdev.name = chip->led_config[chan].name;
+ } else {
+ led->cdev.name = devm_kasprintf(dev, GFP_KERNEL, "%s:channel%d",
+ chip->label ? : chip->client->name, chan);
+ if (!led->cdev.name)
+ return -ENOMEM;
+ }
+
+ if (!chip->led_config[chan].is_sc_led) {
+ mc_led_info = devm_kcalloc(dev, chip->led_config[chan].num_colors,
+ sizeof(*mc_led_info), GFP_KERNEL);
+ if (!mc_led_info)
+ return -ENOMEM;
+
+ led_cdev = &led->mc_cdev.led_cdev;
+ led_cdev->name = led->cdev.name;
+ led_cdev->brightness_set_blocking = lp5812_set_mc_brightness;
+ led->mc_cdev.num_colors = chip->led_config[chan].num_colors;
+
+ for (i = 0; i < led->mc_cdev.num_colors; i++) {
+ mc_led_info[i].color_index = chip->led_config[chan].color_id[i];
+ mc_led_info[i].channel = chip->led_config[chan].led_id[i];
+ }
+
+ led->mc_cdev.subled_info = mc_led_info;
+ } else {
+ led->cdev.brightness_set_blocking = lp5812_set_brightness;
+ }
+
+ led->chan_nr = chan;
+
+ if (chip->led_config[chan].is_sc_led) {
+ ret = devm_led_classdev_register(dev, &led->cdev);
+ if (ret == 0)
+ led->cdev.dev->platform_data = led;
+ } else {
+ ret = devm_led_classdev_multicolor_register(dev, &led->mc_cdev);
+ if (ret == 0)
+ led->mc_cdev.led_cdev.dev->platform_data = led;
+ }
+
+ return ret;
+}
+
+static int lp5812_register_leds(struct lp5812_led *leds, struct lp5812_chip *chip)
+{
+ struct lp5812_led *led;
+ int num_channels = chip->num_channels;
+ u8 reg_val;
+ u16 reg;
+ int ret, i, j;
+
+ for (i = 0; i < num_channels; i++) {
+ led = &leds[i];
+ ret = lp5812_init_led(led, chip, i);
+ if (ret)
+ goto err_init_led;
+
+ led->chip = chip;
+
+ for (j = 0; j < chip->led_config[i].num_colors; j++) {
+ ret = lp5812_write(chip,
+ LP5812_AUTO_DC_BASE + chip->led_config[i].led_id[j],
+ chip->led_config[i].max_current[j]);
+ if (ret)
+ goto err_init_led;
+
+ ret = lp5812_manual_dc_pwm_control(chip, chip->led_config[i].led_id[j],
+ chip->led_config[i].max_current[j],
+ LP5812_DIMMING_ANALOG);
+ if (ret)
+ goto err_init_led;
+
+ ret = lp5812_set_led_mode(chip, chip->led_config[i].led_id[j],
+ LP5812_MODE_MANUAL);
+ if (ret)
+ goto err_init_led;
+
+ reg = (chip->led_config[i].led_id[j] < LP5812_NUMBER_LED_IN_REG) ?
+ LP5812_LED_EN_1 : LP5812_LED_EN_2;
+
+ ret = lp5812_read(chip, reg, &reg_val);
+ if (ret)
+ goto err_init_led;
+
+ reg_val |= (LP5812_ENABLE << (chip->led_config[i].led_id[j] %
+ LP5812_NUMBER_LED_IN_REG));
+
+ ret = lp5812_write(chip, reg, reg_val);
+ if (ret)
+ goto err_init_led;
+ }
+ }
+
+ return 0;
+
+err_init_led:
+ return ret;
+}
+
+static int lp5812_init_device(struct lp5812_chip *chip)
+{
+ int ret;
+
+ usleep_range(LP5812_WAIT_DEVICE_STABLE_MIN, LP5812_WAIT_DEVICE_STABLE_MAX);
+
+ ret = lp5812_write(chip, LP5812_REG_ENABLE, LP5812_ENABLE);
+ if (ret) {
+ dev_err(&chip->client->dev, "failed to enable LP5812 device\n");
+ return ret;
+ }
+
+ ret = lp5812_write(chip, LP5812_DEV_CONFIG12, LP5812_LSD_LOD_START_UP);
+ if (ret) {
+ dev_err(&chip->client->dev, "failed to configure device safety thresholds\n");
+ return ret;
+ }
+
+ ret = parse_drive_mode(chip, chip->scan_mode);
+ if (ret)
+ return ret;
+
+ ret = lp5812_set_drive_mode_scan_order(chip);
+ if (ret)
+ return ret;
+
+ ret = lp5812_update_regs_config(chip);
+ if (ret) {
+ dev_err(&chip->client->dev, "failed to apply configuration updates\n");
+ return ret;
+ }
+
+ return 0;
+}
+
+static void lp5812_deinit_device(struct lp5812_chip *chip)
+{
+ lp5812_write(chip, LP5812_LED_EN_1, LP5812_DISABLE);
+ lp5812_write(chip, LP5812_LED_EN_2, LP5812_DISABLE);
+ lp5812_write(chip, LP5812_REG_ENABLE, LP5812_DISABLE);
+}
+
+static int lp5812_parse_led_channel(struct device_node *np,
+ struct lp5812_led_config *cfg,
+ int color_number)
+{
+ int color_id, reg, ret;
+ u32 max_cur;
+
+ ret = of_property_read_u32(np, "reg", &reg);
+ if (ret)
+ return ret;
+
+ cfg->led_id[color_number] = reg;
+
+ ret = of_property_read_u32(np, "led-max-microamp", &max_cur);
+ if (ret)
+ max_cur = 0;
+ /* Convert microamps to driver units */
+ cfg->max_current[color_number] = max_cur / 100;
+
+ ret = of_property_read_u32(np, "color", &color_id);
+ if (ret)
+ color_id = 0;
+ cfg->color_id[color_number] = color_id;
+
+ return 0;
+}
+
+static int lp5812_parse_led(struct device_node *np,
+ struct lp5812_led_config *cfg,
+ int led_index)
+{
+ int num_colors, ret;
+
+ of_property_read_string(np, "label", &cfg[led_index].name);
+
+ ret = of_property_read_u32(np, "reg", &cfg[led_index].chan_nr);
+ if (ret)
+ return ret;
+
+ num_colors = 0;
+ for_each_available_child_of_node_scoped(np, child) {
+ ret = lp5812_parse_led_channel(child, &cfg[led_index], num_colors);
+ if (ret)
+ return ret;
+
+ num_colors++;
+ }
+
+ if (num_colors == 0) {
+ ret = lp5812_parse_led_channel(np, &cfg[led_index], 0);
+ if (ret)
+ return ret;
+
+ num_colors = 1;
+ cfg[led_index].is_sc_led = true;
+ } else {
+ cfg[led_index].is_sc_led = false;
+ }
+
+ cfg[led_index].num_colors = num_colors;
+
+ return 0;
+}
+
+static int lp5812_of_probe(struct device *dev,
+ struct device_node *np,
+ struct lp5812_chip *chip)
+{
+ struct lp5812_led_config *cfg;
+ int num_channels, i = 0, ret;
+
+ num_channels = of_get_available_child_count(np);
+ if (num_channels == 0) {
+ dev_err(dev, "no LED channels\n");
+ return -EINVAL;
+ }
+
+ cfg = devm_kcalloc(dev, num_channels, sizeof(*cfg), GFP_KERNEL);
+ if (!cfg)
+ return -ENOMEM;
+
+ chip->led_config = &cfg[0];
+ chip->num_channels = num_channels;
+
+ for_each_available_child_of_node_scoped(np, child) {
+ ret = lp5812_parse_led(child, cfg, i);
+ if (ret)
+ return -EINVAL;
+ i++;
+ }
+
+ ret = of_property_read_string(np, "ti,scan-mode", &chip->scan_mode);
+ if (ret)
+ chip->scan_mode = LP5812_MODE_DIRECT_NAME;
+
+ of_property_read_string(np, "label", &chip->label);
+
+ return 0;
+}
+
+static int lp5812_probe(struct i2c_client *client)
+{
+ struct lp5812_chip *chip;
+ struct device_node *np = dev_of_node(&client->dev);
+ struct lp5812_led *leds;
+ int ret;
+
+ if (!np)
+ return -EINVAL;
+
+ chip = devm_kzalloc(&client->dev, sizeof(*chip), GFP_KERNEL);
+ if (!chip)
+ return -ENOMEM;
+
+ ret = lp5812_of_probe(&client->dev, np, chip);
+ if (ret)
+ return ret;
+
+ leds = devm_kcalloc(&client->dev, chip->num_channels, sizeof(*leds), GFP_KERNEL);
+ if (!leds)
+ return -ENOMEM;
+
+ chip->client = client;
+ mutex_init(&chip->lock);
+ i2c_set_clientdata(client, chip);
+
+ ret = lp5812_init_device(chip);
+ if (ret)
+ return ret;
+
+ ret = lp5812_register_leds(leds, chip);
+ if (ret)
+ goto err_out;
+
+ return 0;
+
+err_out:
+ lp5812_deinit_device(chip);
+ return ret;
+}
+
+static void lp5812_remove(struct i2c_client *client)
+{
+ struct lp5812_chip *chip = i2c_get_clientdata(client);
+
+ lp5812_deinit_device(chip);
+}
+
+static const struct of_device_id of_lp5812_match[] = {
+ { .compatible = "ti,lp5812" },
+ { /* sentinel */ }
+};
+MODULE_DEVICE_TABLE(of, of_lp5812_match);
+
+static struct i2c_driver lp5812_driver = {
+ .driver = {
+ .name = "lp5812",
+ .of_match_table = of_lp5812_match,
+ },
+ .probe = lp5812_probe,
+ .remove = lp5812_remove,
+};
+module_i2c_driver(lp5812_driver);
+
+MODULE_DESCRIPTION("Texas Instruments LP5812 LED Driver");
+MODULE_AUTHOR("Jared Zhou <jared-zhou@ti.com>");
+MODULE_LICENSE("GPL");
diff --git a/drivers/leds/rgb/leds-lp5812.h b/drivers/leds/rgb/leds-lp5812.h
new file mode 100644
index 000000000..d8728ecd9
--- /dev/null
+++ b/drivers/leds/rgb/leds-lp5812.h
@@ -0,0 +1,172 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/*
+ * LP5812 Driver Header
+ *
+ * Copyright (C) 2025 Texas Instruments
+ *
+ * Author: Jared Zhou <jared-zhou@ti.com>
+ */
+
+#ifndef _LP5812_H_
+#define _LP5812_H_
+
+#include <linux/delay.h>
+#include <linux/i2c.h>
+#include <linux/kernel.h>
+#include <linux/led-class-multicolor.h>
+#include <linux/leds.h>
+#include <linux/mutex.h>
+#include <linux/sysfs.h>
+#include <linux/types.h>
+
+#define LP5812_REG_ENABLE 0x0000
+#define LP5812_REG_RESET 0x0023
+#define LP5812_DEV_CONFIG0 0x0001
+#define LP5812_DEV_CONFIG1 0x0002
+#define LP5812_DEV_CONFIG2 0x0003
+#define LP5812_DEV_CONFIG3 0x0004
+#define LP5812_DEV_CONFIG4 0x0005
+#define LP5812_DEV_CONFIG5 0x0006
+#define LP5812_DEV_CONFIG6 0x0007
+#define LP5812_DEV_CONFIG7 0x0008
+#define LP5812_DEV_CONFIG8 0x0009
+#define LP5812_DEV_CONFIG9 0x000A
+#define LP5812_DEV_CONFIG10 0x000B
+#define LP5812_DEV_CONFIG11 0x000c
+#define LP5812_DEV_CONFIG12 0x000D
+#define LP5812_CMD_UPDATE 0x0010
+#define LP5812_LED_EN_1 0x0020
+#define LP5812_LED_EN_2 0x0021
+#define LP5812_FAULT_CLEAR 0x0022
+#define LP5812_MANUAL_DC_BASE 0x0030
+#define LP5812_AUTO_DC_BASE 0x0050
+#define LP5812_MANUAL_PWM_BASE 0x0040
+
+#define LP5812_TSD_CONFIG_STATUS 0x0300
+#define LP5812_LOD_STATUS 0x0301
+#define LP5812_LSD_STATUS 0x0303
+
+#define LP5812_ENABLE 0x01
+#define LP5812_DISABLE 0x00
+#define FAULT_CLEAR_ALL 0x07
+#define TSD_CLEAR_VAL 0x04
+#define LSD_CLEAR_VAL 0x02
+#define LOD_CLEAR_VAL 0x01
+#define LP5812_RESET 0x66
+#define LP5812_DEV_CONFIG12_DEFAULT 0x08
+
+#define LP5812_UPDATE_CMD_VAL 0x55
+#define LP5812_REG_ADDR_HIGH_SHIFT 8
+#define LP5812_REG_ADDR_BIT_8_9_MASK 0x03
+#define LP5812_REG_ADDR_LOW_MASK 0xFF
+#define LP5812_CHIP_ADDR_SHIFT 2
+#define LP5812_DATA_LENGTH 2
+#define LP5812_DATA_BYTE_0_IDX 0
+#define LP5812_DATA_BYTE_1_IDX 1
+
+#define LP5812_READ_MSG_LENGTH 2
+#define LP5812_MSG_0_IDX 0
+#define LP5812_MSG_1_IDX 1
+#define LP5812_CFG_ERR_STATUS_MASK 0x01
+#define LP5812_CFG_TSD_STATUS_SHIFT 1
+#define LP5812_CFG_TSD_STATUS_MASK 0x01
+
+#define LP5812_FAULT_CLEAR_LOD 0
+#define LP5812_FAULT_CLEAR_LSD 1
+#define LP5812_FAULT_CLEAR_TSD 2
+#define LP5812_FAULT_CLEAR_ALL 3
+#define LP5812_NUMBER_LED_IN_REG 8
+
+#define LP5812_WAIT_DEVICE_STABLE_MIN 1000
+#define LP5812_WAIT_DEVICE_STABLE_MAX 1100
+
+#define LP5812_LSD_LOD_START_UP 0x0B
+#define LP5812_MODE_NAME_MAX_LEN 20
+#define LP5812_MODE_DIRECT_NAME "direct_mode"
+#define LP5812_MODE_DIRECT_VALUE 0
+#define LP5812_MODE_MIX_SELECT_LED_0 0
+#define LP5812_MODE_MIX_SELECT_LED_1 1
+#define LP5812_MODE_MIX_SELECT_LED_2 2
+#define LP5812_MODE_MIX_SELECT_LED_3 3
+
+enum control_mode {
+ LP5812_MODE_MANUAL = 0,
+ LP5812_MODE_AUTONOMOUS
+};
+
+enum dimming_type {
+ LP5812_DIMMING_ANALOG,
+ LP5812_DIMMING_PWM
+};
+
+union lp5812_scan_order {
+ struct {
+ u8 order0:2;
+ u8 order1:2;
+ u8 order2:2;
+ u8 order3:2;
+ } bits;
+ u8 val;
+};
+
+union lp5812_drive_mode {
+ struct {
+ u8 mix_sel_led_0:1;
+ u8 mix_sel_led_1:1;
+ u8 mix_sel_led_2:1;
+ u8 mix_sel_led_3:1;
+ u8 led_mode:3;
+ u8 pwm_fre:1;
+ } bits;
+ u8 val;
+};
+
+struct lp5812_reg {
+ u16 addr;
+ union {
+ u8 val;
+ u8 mask;
+ u8 shift;
+ };
+};
+
+struct lp5812_mode_mapping {
+ char mode_name[LP5812_MODE_NAME_MAX_LEN];
+ u8 mode;
+ u8 scan_order_0;
+ u8 scan_order_1;
+ u8 scan_order_2;
+ u8 scan_order_3;
+ u8 selection_led;
+};
+
+struct lp5812_led_config {
+ bool is_sc_led;
+ const char *name;
+ u8 color_id[LED_COLOR_ID_MAX];
+ u32 max_current[LED_COLOR_ID_MAX];
+ int chan_nr;
+ int num_colors;
+ int led_id[LED_COLOR_ID_MAX];
+};
+
+struct lp5812_chip {
+ u8 num_channels;
+ struct i2c_client *client;
+ struct mutex lock; /* Protects register access */
+ struct lp5812_led_config *led_config;
+ const char *label;
+ const char *scan_mode;
+ union lp5812_scan_order scan_order;
+ union lp5812_drive_mode drive_mode;
+};
+
+struct lp5812_led {
+ u8 brightness;
+ int chan_nr;
+ struct led_classdev cdev;
+ struct led_classdev_mc mc_cdev;
+ struct lp5812_chip *chip;
+};
+
+#endif /*_LP5812_H_*/
diff --git a/drivers/leds/rgb/leds-lp5860-core.c b/drivers/leds/rgb/leds-lp5860-core.c
new file mode 100644
index 000000000..e21d5f230
--- /dev/null
+++ b/drivers/leds/rgb/leds-lp5860-core.c
@@ -0,0 +1,233 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Copyright (c) 2025 Pengutronix
+ *
+ * Author: Steffen Trumtrar <kernel@pengutronix.de>
+ */
+
+#include <linux/led-class-multicolor.h>
+#include <linux/module.h>
+#include <linux/mutex.h>
+#include <linux/of_platform.h>
+#include <linux/property.h>
+#include <linux/regmap.h>
+
+#include "leds-lp5860.h"
+
+static struct lp5860_led *mcled_cdev_to_led(struct led_classdev_mc *mc_cdev)
+{
+ return container_of(mc_cdev, struct lp5860_led, mc_cdev);
+}
+
+static int lp5860_set_dot_onoff(struct lp5860_led *led, unsigned int dot, bool enable)
+{
+ unsigned int offset = dot / LP5860_MAX_DOT_ONOFF_GROUP_NUM;
+ unsigned int mask = BIT(dot % LP5860_MAX_DOT_ONOFF_GROUP_NUM);
+
+ if (dot > LP5860_MAX_LED)
+ return -EINVAL;
+
+ return regmap_update_bits(led->chip->regmap,
+ LP5860_REG_DOT_ONOFF_START + offset, mask,
+ enable ? LP5860_DOT_ALL_ON : LP5860_DOT_ALL_OFF);
+}
+
+static int lp5860_set_mc_brightness(struct led_classdev *cdev,
+ enum led_brightness brightness)
+{
+ struct led_classdev_mc *mc_cdev = lcdev_to_mccdev(cdev);
+ struct lp5860_led *led = mcled_cdev_to_led(mc_cdev);
+
+ led_mc_calc_color_components(mc_cdev, brightness);
+
+ guard(mutex)(&led->chip->lock);
+ for (int i = 0; i < led->mc_cdev.num_colors; i++) {
+ unsigned int channel = mc_cdev->subled_info[i].channel;
+ unsigned int led_brightness = mc_cdev->subled_info[i].brightness;
+ int ret;
+
+ ret = lp5860_set_dot_onoff(led, channel, !!led_brightness);
+ if (ret)
+ return ret;
+
+ ret = regmap_write(led->chip->regmap,
+ LP5860_REG_PWM_BRI_START + channel, led_brightness);
+ if (ret)
+ return ret;
+ }
+
+ return 0;
+}
+
+static int lp5860_chip_enable(struct lp5860 *lp, bool enable)
+{
+ guard(mutex)(&lp->lock);
+ return regmap_write(lp->regmap, LP5860_REG_CHIP_EN, enable);
+}
+
+static int lp5860_led_init(struct lp5860_led *led, struct fwnode_handle *fwnode,
+ unsigned int channel)
+{
+ enum led_default_state default_state;
+ unsigned int brightness;
+ int ret;
+
+ guard(mutex)(&led->chip->lock);
+ ret = regmap_read(led->chip->regmap, LP5860_REG_PWM_BRI_START + channel, &brightness);
+ if (ret)
+ return ret;
+
+ default_state = led_init_default_state_get(fwnode);
+
+ switch (default_state) {
+ case LEDS_DEFSTATE_ON:
+ led->brightness = LP5860_MAX_BRIGHTNESS;
+ break;
+ case LEDS_DEFSTATE_KEEP:
+ led->brightness = min(brightness, LP5860_MAX_BRIGHTNESS);
+ break;
+ default:
+ led->brightness = 0;
+ break;
+ }
+
+ return 0;
+}
+
+static int lp5860_iterate_subleds(struct lp5860_led *led, struct led_init_data *init_data)
+{
+ struct fwnode_handle *led_node = NULL;
+ struct fwnode_handle *multi_led = init_data->fwnode;
+ int subled = 0;
+
+ fwnode_for_each_child_node(multi_led, led_node) {
+ u32 channel;
+ u32 color_index;
+ int ret;
+
+ ret = fwnode_property_read_u32(led_node, "color", &color_index);
+ if (ret) {
+ dev_err_probe(led->chip->dev, ret,
+ "%pfwP: Cannot read 'color' property. Skipping.\n", led_node);
+ fwnode_handle_put(led_node);
+ return ret;
+ }
+
+ ret = fwnode_property_read_u32(led_node, "reg", &channel);
+ if (ret < 0 || channel > LP5860_MAX_LED) {
+ dev_err_probe(led->chip->dev, ret,
+ "%pfwP: 'reg' property is missing. Skipping.\n", led_node);
+ fwnode_handle_put(led_node);
+ return ret;
+ }
+
+ led->mc_cdev.subled_info[subled].color_index = color_index;
+ led->mc_cdev.subled_info[subled].channel = channel;
+ ret = lp5860_led_init(led, init_data->fwnode, channel);
+ if (ret) {
+ dev_err_probe(led->chip->dev, ret,
+ "%pfwP: Failed to init LED\n", led_node);
+ fwnode_handle_put(led_node);
+ return ret;
+ }
+
+ subled++;
+ }
+
+ return 0;
+}
+
+static int lp5860_init_dt(struct lp5860 *lp)
+{
+ struct led_init_data init_data = {};
+ struct led_classdev *led_cdev;
+ struct mc_subled *mc_led_info;
+ struct lp5860_led *led;
+ int led_index = 0;
+ int chan;
+ int ret;
+
+ device_for_each_child_node_scoped(lp->dev, multi_led) {
+ led = &lp->leds[led_index];
+
+ init_data.fwnode = multi_led;
+
+ /* Count the number of channels in this multi_led */
+ chan = fwnode_get_child_node_count(multi_led);
+ if (!chan || chan > LP5860_MAX_LED_CHANNELS)
+ return -EINVAL;
+
+ led->mc_cdev.num_colors = chan;
+
+ mc_led_info = devm_kcalloc(lp->dev, chan, sizeof(*mc_led_info), GFP_KERNEL);
+ if (!mc_led_info)
+ return -ENOMEM;
+
+ led->chip = lp;
+ led->mc_cdev.subled_info = mc_led_info;
+ led_cdev = &led->mc_cdev.led_cdev;
+ led_cdev->max_brightness = LP5860_MAX_BRIGHTNESS;
+ led_cdev->brightness_set_blocking = lp5860_set_mc_brightness;
+
+ ret = lp5860_iterate_subleds(led, &init_data);
+ if (ret)
+ continue;
+
+ ret = lp5860_set_mc_brightness(&led->mc_cdev.led_cdev, led->brightness);
+ if (ret)
+ return dev_err_probe(lp->dev, ret, "%pfwP: Failed to set Multi-Color brightness\n",
+ multi_led);
+
+ ret = devm_led_classdev_multicolor_register_ext(lp->dev, &led->mc_cdev, &init_data);
+ if (ret)
+ return dev_err_probe(lp->dev, ret, "%pfwP: Failed to register Multi-Color LEDs\n",
+ multi_led);
+ led_index++;
+ }
+
+ return 0;
+}
+
+int lp5860_device_init(struct device *dev)
+{
+ struct lp5860 *lp = dev_get_drvdata(dev);
+ int ret;
+
+ ret = lp5860_chip_enable(lp, LP5860_CHIP_ENABLE);
+ if (ret)
+ return ret;
+
+ /*
+ * Set to 8-bit PWM data without VSYNC.
+ * Data is sent out for display instantly after received.
+ */
+ mutex_lock(&lp->lock);
+ ret = regmap_update_bits(lp->regmap, LP5860_REG_DEV_INITIAL, LP5860_MODE_MASK,
+ LP5860_MODE_1 << LP5860_MODE_SHIFT);
+ mutex_unlock(&lp->lock);
+ if (ret)
+ goto err_disable;
+
+ ret = lp5860_init_dt(lp);
+ if (ret)
+ goto err_disable;
+
+ return 0;
+
+err_disable:
+ lp5860_chip_enable(lp, LP5860_CHIP_DISABLE);
+ return ret;
+}
+EXPORT_SYMBOL_GPL(lp5860_device_init);
+
+void lp5860_device_remove(struct device *dev)
+{
+ struct lp5860 *lp = dev_get_drvdata(dev);
+
+ lp5860_chip_enable(lp, LP5860_CHIP_DISABLE);
+}
+EXPORT_SYMBOL_GPL(lp5860_device_remove);
+
+MODULE_AUTHOR("Steffen Trumtrar <kernel@pengutronix.de>");
+MODULE_DESCRIPTION("TI LP5860 RGB LED core driver");
+MODULE_LICENSE("GPL");
diff --git a/drivers/leds/rgb/leds-lp5860-spi.c b/drivers/leds/rgb/leds-lp5860-spi.c
new file mode 100644
index 000000000..6bf6a625c
--- /dev/null
+++ b/drivers/leds/rgb/leds-lp5860-spi.c
@@ -0,0 +1,98 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Copyright (c) 2025 Pengutronix
+ *
+ * Author: Steffen Trumtrar <kernel@pengutronix.de>
+ */
+
+#include <linux/init.h>
+#include <linux/module.h>
+#include <linux/mutex.h>
+#include <linux/of.h>
+#include <linux/platform_device.h>
+#include <linux/regmap.h>
+#include <linux/spi/spi.h>
+
+#include "leds-lp5860.h"
+
+#define LP5860_SPI_WRITE_FLAG BIT(13)
+
+/*
+ * The lp5860 uses a rather uncommon SPI data format: The R/W flag is on BIT(5) in the two address
+ * bytes; BIT(4) to BIT(0) are don't care. Therefore it has 10 bits for the address and 6 bits for
+ * padding the address. The address bytes are sent MSB first. Matching the cores registers to regmap
+ * results in write_flag_mask being BIT(13).
+ */
+static const struct regmap_config lp5860_regmap_config = {
+ .name = "lp5860",
+ .reg_bits = 10,
+ .pad_bits = 6,
+ .val_bits = 8,
+ .write_flag_mask = LP5860_SPI_WRITE_FLAG,
+ .reg_format_endian = REGMAP_ENDIAN_BIG,
+ .max_register = LP5860_MAX_REG,
+};
+
+static int lp5860_probe(struct spi_device *spi)
+{
+ struct device *dev = &spi->dev;
+ struct lp5860 *lp5860;
+ unsigned int multi_leds;
+ int ret;
+
+ multi_leds = device_get_child_node_count(dev);
+ if (!multi_leds) {
+ dev_err(dev, "LEDs are not defined in Device Tree!");
+ return -ENODEV;
+ }
+
+ if (multi_leds > LP5860_MAX_LED) {
+ dev_err(dev, "Too many LEDs specified.\n");
+ return -EINVAL;
+ }
+
+ lp5860 = devm_kzalloc(dev, struct_size(lp5860, leds, multi_leds),
+ GFP_KERNEL);
+ if (!lp5860)
+ return -ENOMEM;
+
+ lp5860->regmap = devm_regmap_init_spi(spi, &lp5860_regmap_config);
+ if (IS_ERR(lp5860->regmap))
+ return dev_err_probe(&spi->dev, PTR_ERR(lp5860->regmap),
+ "Failed to initialise Regmap.\n");
+
+ lp5860->dev = dev;
+
+ ret = devm_mutex_init(dev, &lp5860->lock);
+ if (ret)
+ return ret;
+
+ spi_set_drvdata(spi, lp5860);
+
+ return lp5860_device_init(dev);
+}
+
+static void lp5860_remove(struct spi_device *spi)
+{
+ lp5860_device_remove(&spi->dev);
+}
+
+static const struct of_device_id lp5860_of_match[] = {
+ { .compatible = "ti,lp5860" },
+ {}
+};
+MODULE_DEVICE_TABLE(of, lp5860_of_match);
+
+static struct spi_driver lp5860_driver = {
+ .driver = {
+ .name = "lp5860-spi",
+ .of_match_table = lp5860_of_match,
+ },
+ .probe = lp5860_probe,
+ .remove = lp5860_remove,
+};
+module_spi_driver(lp5860_driver);
+
+MODULE_AUTHOR("Steffen Trumtrar <kernel@pengutronix.de>");
+MODULE_DESCRIPTION("TI LP5860 RGB LED SPI driver");
+MODULE_LICENSE("GPL");
diff --git a/drivers/leds/rgb/leds-lp5860.h b/drivers/leds/rgb/leds-lp5860.h
new file mode 100644
index 000000000..940be0c6e
--- /dev/null
+++ b/drivers/leds/rgb/leds-lp5860.h
@@ -0,0 +1,268 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+/*
+ * Copyright (c) 2025 Pengutronix
+ *
+ * Author: Steffen Trumtrar <kernel@pengutronix.de>
+ */
+
+#ifndef _DRIVERS_LEDS_RGB_LP5860_H
+#define _DRIVERS_LEDS_RGB_LP5860_H
+
+#include <linux/led-class-multicolor.h>
+#include <linux/regmap.h>
+
+#define LP5860_REG_CHIP_EN 0x00
+#define LP5860_REG_DEV_INITIAL 0x01
+#define LP5860_REG_DEV_CONFIG1 0x02
+#define LP5860_REG_DEV_CONFIG2 0x03
+#define LP5860_REG_DEV_CONFIG3 0x04
+#define LP5860_REG_GLOBAL_BRI 0x05
+#define LP5860_REG_GROUP0_BRI 0x06
+#define LP5860_REG_GROUP1_BRI 0x07
+#define LP5860_REG_GROUP2_BRI 0x08
+#define LP5860_REG_R_CURRENT_SET 0x09
+#define LP5860_REG_G_CURRENT_SET 0x0A
+#define LP5860_REG_B_CURRENT_SET 0x0B
+#define LP5860_REG_GRP_SEL_START 0x0C
+#define LP5860_REG_DOT_ONOFF_START 0x43
+#define LP5860_REG_DOT_ONOFF_MAX 0x63
+#define LP5860_REG_FAULT_STATE 0x64
+#define LP5860_REG_DOT_LOD_START 0x65
+#define LP5860_REG_DOT_LSD_START 0x86
+#define LP5860_REG_LOD_CLEAR 0xA7
+#define LP5860_REG_LSD_CLEAR 0xA8
+#define LP5860_REG_RESET 0xA9
+#define LP5860_REG_DC_START 0x0100
+#define LP5860_REG_PWM_BRI_START 0x0200
+#define LP5860_MAX_REG 0x038B
+
+/* Register chip_enable value */
+#define LP5860_CHIP_SHIFT 0
+#define LP5860_CHIP_MASK BIT(0)
+#define LP5860_CHIP_DISABLE false
+#define LP5860_CHIP_ENABLE true
+
+/* Register dev_initial value */
+#define LP5860_MAX_LINE_SHIFT 3
+#define LP5860_MAX_LINE_MASK GENMASK(6, 3)
+#define LP5860_MAX_LINE_11 0x0B
+#define LP5860_MAX_LINE_10 0x0A
+#define LP5860_MAX_LINE_9 0x09
+#define LP5860_MAX_LINE_8 0x08
+#define LP5860_MAX_LINE_7 0x07
+#define LP5860_MAX_LINE_6 0x06
+#define LP5860_MAX_LINE_5 0x05
+#define LP5860_MAX_LINE_4 0x04
+#define LP5860_MAX_LINE_3 0x03
+#define LP5860_MAX_LINE_2 0x02
+#define LP5860_MAX_LINE_1 0x01
+
+#define LP5860_MODE_SHIFT 1
+#define LP5860_MODE_MASK GENMASK(2, 1)
+#define LP5860_MODE_3_1 0x03
+#define LP5860_MODE_3 0x02
+#define LP5860_MODE_2 0x01
+#define LP5860_MODE_1 0x00
+
+#define LP5860_PWM_FREQUENCY_SHIFT 0
+#define LP5860_PWM_FREQUENCY_MASK BIT(0)
+#define LP5860_PWM_FREQUENCY_62_5K 0x01
+#define LP5860_PWM_FREQUENCY_125K 0x00
+
+/* Register dev_config1 value */
+#define LP5860_SW_BLK_SHIFT 3
+#define LP5860_SW_BLK_MASK BIT(3)
+#define LP5860_SW_BLK_05US 0x01
+#define LP5860_SW_BLK_1US 0x00
+
+#define LP5860_PWM_SCALE_MODE_SHIFT 2
+#define LP5860_PWM_SCALE_MODE_MASK BIT(2)
+#define LP5860_PWM_SCALE_EXPONENTIAL 0x01
+#define LP5860_PWM_SCALE_LINEAR 0x00
+
+#define LP5860_PWM_PHASESHIFT_SHIFT 1
+#define LP5860_PWM_PHASESHIFT_MASK BIT(1)
+#define LP5860_PWM_PHASESHIFT_ON 0x01
+#define LP5860_PWM_PHASESHIFT_OFF 0x00
+
+#define LP5860_CS_ON_SHIFT_SHIFT 0
+#define LP5860_CS_ON_SHIFT_MASK BIT(0)
+#define LP5860_CS_DELAY_ON 0x01
+#define LP5860_CS_DELAY_OFF 0x00
+
+/* Register dev_config2 value */
+#define LP5860_COMP_GROUP3_SHIFT 6
+#define LP5860_COMP_GROUP3_MASK GENMASK(7, 6)
+#define LP5860_COMP_GROUP3_3CLOCK 0x03
+#define LP5860_COMP_GROUP3_2CLOCK 0x02
+#define LP5860_COMP_GROUP3_1CLOCK 0x01
+#define LP5860_COMP_GROUP3_OFF 0x00
+
+#define LP5860_COMP_GROUP2_SHIFT 4
+#define LP5860_COMP_GROUP2_MASK GENMASK(5, 4)
+#define LP5860_COMP_GROUP2_3CLOCK 0x03
+#define LP5860_COMP_GROUP2_2CLOCK 0x02
+#define LP5860_COMP_GROUP2_1CLOCK 0x01
+#define LP5860_COMP_GROUP2_OFF 0x00
+
+#define LP5860_COMP_GROUP1_SHIFT 2
+#define LP5860_COMP_GROUP1_MASK GENMASK(3, 2)
+#define LP5860_COMP_GROUP1_3CLOCK 0x03
+#define LP5860_COMP_GROUP1_2CLOCK 0x02
+#define LP5860_COMP_GROUP1_1CLOCK 0x01
+#define LP5860_COMP_GROUP1_OFF 0x00
+
+#define LP5860_LOD_REMOVAL_SHIFT 1
+#define LP5860_LOD_REMOVAL_MASK BIT(1)
+#define LP5860_LOD_REMOVAL_EN 0x01
+#define LP5860_LOD_REMOVAL_OFF 0x00
+
+#define LP5860_LSD_REMOVAL_SHIFT 0
+#define LP5860_LSD_REMOVAL_MASK BIT(0)
+#define LP5860_LSD_REMOVAL_EN 0x01
+#define LP5860_LSD_REMOVAL_OFF 0x00
+
+/* Register dev_config3 value */
+#define LP5860_DOWN_DEGHOST_SHIFT 6
+#define LP5860_DOWN_DEGHOST_MASK GENMASK(7, 6)
+#define LP5860_DOWN_DEGHOST_STRONG 0x03
+#define LP5860_DOWN_DEGHOST_MEDIUM 0x02
+#define LP5860_DOWN_DEGHOST_WEAK 0x01
+#define LP5860_DOWN_DEGHOST_OFF 0x00
+
+#define LP5860_UP_DEGHOST_SHIFT 4
+#define LP5860_UP_DEGHOST_MASK GENMASK(5, 4)
+#define LP5860_UP_DEGHOST_GND 0x03
+#define LP5860_UP_DEGHOST_3 0x02
+#define LP5860_UP_DEGHOST_2_5 0x01
+#define LP5860_UP_DEGHOST_2 0x00
+
+#define LP5860_MAXIMUM_CURRENT_SHIFT 1
+#define LP5860_MAXIMUM_CURRENT_MASK GENMASK(3, 1)
+#define LP5860_MAXIMUM_CURRENT_50 0x07
+#define LP5860_MAXIMUM_CURRENT_40 0x06
+#define LP5860_MAXIMUM_CURRENT_30 0x05
+#define LP5860_MAXIMUM_CURRENT_20 0x04
+#define LP5860_MAXIMUM_CURRENT_15 0x03
+#define LP5860_MAXIMUM_CURRENT_10 0x02
+#define LP5860_MAXIMUM_CURRENT_5 0x01
+#define LP5860_MAXIMUM_CURRENT_3 0x00
+
+#define LP5860_UP_DEGHOST_ENABLE_SHIFT 0
+#define LP5860_UP_DEGHOST_ENABLE_MASK BIT(0)
+#define LP5860_UP_DEGHOST_ENABLE_EN 0x01
+#define LP5860_UP_DEGHOST_ENABLE_OFF 0x00
+
+/* Register PWM */
+#define LP5860_PWM_GLOBAL_MAX 0xff
+#define LP5860_PWM_GROUP_MAX 0xff
+
+/* Register CC group select */
+#define LP5860_CC_GROUP_MASK GENMASK(7, 0)
+#define LP5860_CC_GROUP_MAX 0x7F
+
+/* Register dot group select */
+#define LP5860_DOT_0_SHIFT 0
+#define LP5860_DOT_1_SHIFT 2
+#define LP5860_DOT_2_SHIFT 4
+#define LP5860_DOT_3_SHIFT 6
+
+#define LP5860_DOT_GROUP3 0x03
+#define LP5860_DOT_GROUP2 0x02
+#define LP5860_DOT_GROUP1 0x01
+#define LP5860_DOT_GROUP_NONE 0x00
+
+#define LP5860_DOT_ALL_ON 0xff
+#define LP5860_DOT_ALL_OFF 0x0
+#define LP5860_PWM_DOT_MAX 0xff
+/* Dot onoff value */
+#define LP5860_DOT_CS0_SHIFT 0
+#define LP5860_DOT_CS1_SHIFT 1
+#define LP5860_DOT_CS2_SHIFT 2
+#define LP5860_DOT_CS3_SHIFT 3
+#define LP5860_DOT_CS4_SHIFT 4
+#define LP5860_DOT_CS5_SHIFT 5
+#define LP5860_DOT_CS6_SHIFT 6
+#define LP5860_DOT_CS7_SHIFT 7
+
+#define LP5860_DOT_CS_ON 0x01
+#define LP5860_DOT_CS_OFF 0x00
+
+/* Dot lod value */
+#define LP5860_DOT_LOD0_SHIFT 0
+#define LP5860_DOT_LOD1_SHIFT 1
+#define LP5860_DOT_LOD2_SHIFT 2
+#define LP5860_DOT_LOD3_SHIFT 3
+#define LP5860_DOT_LOD4_SHIFT 4
+#define LP5860_DOT_LOD5_SHIFT 5
+#define LP5860_DOT_LOD6_SHIFT 6
+#define LP5860_DOT_LOD7_SHIFT 7
+
+#define LP5860_DOT_LOD_ON 0x01
+#define LP5860_DOT_LOD_OFF 0x00
+
+/* dot lsd value */
+#define LP5860_DOT_LSD0_SHIFT 0
+#define LP5860_DOT_LSD1_SHIFT 1
+#define LP5860_DOT_LSD2_SHIFT 2
+#define LP5860_DOT_LSD3_SHIFT 3
+#define LP5860_DOT_LSD4_SHIFT 4
+#define LP5860_DOT_LSD5_SHIFT 5
+#define LP5860_DOT_LSD6_SHIFT 6
+#define LP5860_DOT_LSD7_SHIFT 7
+
+#define LP5860_DOT_LSD_ON 0x01
+#define LP5860_DOT_LSD_OFF 0x00
+
+/* Register lod state */
+#define LP5860_GLOBAL_LOD_SHIFT 1
+#define LP5860_GLOBAL_LOD_STATE BIT(1)
+#define LP5860_GLOBAL_LSD_SHIFT 0
+#define LP5860_GLOBAL_LSD_STATE BIT(0)
+
+#define LP5860_FAULT_STATE_ON 0x01
+#define LP5860_FAULT_STATE_OFF 0x00
+
+#define LP5860_GLOBAL_LOD_CLEAR 0x00
+#define LP5860_GLOBAL_LSD_CLEAR 0x00
+
+
+#define LP5860_LOD_CLEAR_EN 0xff
+#define LP5860_LSD_CLEAR_EN 0xff
+#define LP5860_RESET_EN 0xff
+
+#define LP5860_MAX_BRIGHTNESS 255
+#define LP5860_REG_R_PWM 0x0
+#define LP5860_REG_G_PWM 0x1
+#define LP5860_REG_B_PWM 0x2
+
+#define LP5860_MAX_LED_CONSTANT 18
+#define LP5860_MAX_LED_SCAN 11
+#define LP5860_MAX_LED (LP5860_MAX_LED_CONSTANT * LP5860_MAX_LED_SCAN)
+
+#define LP5860_MAX_DOT_ONOFF_GROUP_NUM 8
+
+/*
+ * Theoretically, there is no max channel per LED,
+ * limit this to a reasonable value for RGBW LEDs
+ */
+#define LP5860_MAX_LED_CHANNELS 4
+
+struct lp5860_led {
+ struct lp5860 *chip;
+ struct led_classdev_mc mc_cdev;
+ u8 brightness;
+};
+
+struct lp5860 {
+ struct device *dev;
+ struct regmap *regmap;
+ struct mutex lock;
+
+ DECLARE_FLEX_ARRAY(struct lp5860_led, leds);
+};
+
+int lp5860_device_init(struct device *dev);
+void lp5860_device_remove(struct device *dev);
+
+#endif /* _DRIVERS_LEDS_RGB_LP5860_H */
diff --git a/drivers/leds/rgb/leds-mt6370-rgb.c b/drivers/leds/rgb/leds-mt6370-rgb.c
new file mode 100644
index 000000000..2c0ccd1ba
--- /dev/null
+++ b/drivers/leds/rgb/leds-mt6370-rgb.c
@@ -0,0 +1,994 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Copyright (C) 2023 Richtek Technology Corp.
+ *
+ * Authors:
+ * ChiYuan Huang <cy_huang@richtek.com>
+ * Alice Chen <alice_chen@richtek.com>
+ */
+
+#include <linux/bitfield.h>
+#include <linux/bitops.h>
+#include <linux/kernel.h>
+#include <linux/leds.h>
+#include <linux/led-class-multicolor.h>
+#include <linux/linear_range.h>
+#include <linux/module.h>
+#include <linux/mutex.h>
+#include <linux/platform_device.h>
+#include <linux/property.h>
+#include <linux/regmap.h>
+#include <linux/util_macros.h>
+
+#include <linux/unaligned.h>
+
+enum {
+ MT6370_LED_ISNK1 = 0,
+ MT6370_LED_ISNK2,
+ MT6370_LED_ISNK3,
+ MT6370_LED_ISNK4,
+ MT6370_MAX_LEDS
+};
+
+enum mt6370_led_mode {
+ MT6370_LED_PWM_MODE = 0,
+ MT6370_LED_BREATH_MODE,
+ MT6370_LED_REG_MODE,
+ MT6370_LED_MAX_MODE
+};
+
+enum mt6370_led_field {
+ F_RGB_EN = 0,
+ F_CHGIND_EN,
+ F_LED1_CURR,
+ F_LED2_CURR,
+ F_LED3_CURR,
+ F_LED4_CURR,
+ F_LED1_MODE,
+ F_LED2_MODE,
+ F_LED3_MODE,
+ F_LED4_MODE,
+ F_LED1_DUTY,
+ F_LED2_DUTY,
+ F_LED3_DUTY,
+ F_LED4_DUTY,
+ F_LED1_FREQ,
+ F_LED2_FREQ,
+ F_LED3_FREQ,
+ F_LED4_FREQ,
+ F_MAX_FIELDS
+};
+
+enum mt6370_led_ranges {
+ R_LED123_CURR = 0,
+ R_LED4_CURR,
+ R_LED_TRFON,
+ R_LED_TOFF,
+ R_MAX_RANGES
+};
+
+enum mt6370_pattern {
+ P_LED_TR1 = 0,
+ P_LED_TR2,
+ P_LED_TF1,
+ P_LED_TF2,
+ P_LED_TON,
+ P_LED_TOFF,
+ P_MAX_PATTERNS
+};
+
+#define MT6370_REG_DEV_INFO 0x100
+#define MT6370_REG_RGB1_DIM 0x182
+#define MT6370_REG_RGB2_DIM 0x183
+#define MT6370_REG_RGB3_DIM 0x184
+#define MT6370_REG_RGB_EN 0x185
+#define MT6370_REG_RGB1_ISNK 0x186
+#define MT6370_REG_RGB2_ISNK 0x187
+#define MT6370_REG_RGB3_ISNK 0x188
+#define MT6370_REG_RGB1_TR 0x189
+#define MT6370_REG_RGB_CHRIND_DIM 0x192
+#define MT6370_REG_RGB_CHRIND_CTRL 0x193
+#define MT6370_REG_RGB_CHRIND_TR 0x194
+
+#define MT6372_REG_RGB_EN 0x182
+#define MT6372_REG_RGB1_ISNK 0x183
+#define MT6372_REG_RGB2_ISNK 0x184
+#define MT6372_REG_RGB3_ISNK 0x185
+#define MT6372_REG_RGB4_ISNK 0x186
+#define MT6372_REG_RGB1_DIM 0x187
+#define MT6372_REG_RGB2_DIM 0x188
+#define MT6372_REG_RGB3_DIM 0x189
+#define MT6372_REG_RGB4_DIM 0x18A
+#define MT6372_REG_RGB12_FREQ 0x18B
+#define MT6372_REG_RGB34_FREQ 0x18C
+#define MT6372_REG_RGB1_TR 0x18D
+
+#define MT6370_VENDOR_ID_MASK GENMASK(7, 4)
+#define MT6372_VENDOR_ID 0x9
+#define MT6372C_VENDOR_ID 0xb
+#define MT6370_CHEN_BIT(id) BIT(MT6370_LED_ISNK4 - id)
+#define MT6370_VIRTUAL_MULTICOLOR 5
+#define MC_CHANNEL_NUM 3
+#define MT6370_PWM_DUTY (BIT(5) - 1)
+#define MT6372_PWM_DUTY (BIT(8) - 1)
+
+struct mt6370_led {
+ /*
+ * If the color of the LED in DT is set to
+ * - 'LED_COLOR_ID_RGB'
+ * - 'LED_COLOR_ID_MULTI'
+ * The member 'index' of this struct will be set to
+ * 'MT6370_VIRTUAL_MULTICOLOR'.
+ * If so, this LED will choose 'struct led_classdev_mc mc' to use.
+ * Instead, if the member 'index' of this struct is set to
+ * 'MT6370_LED_ISNK1' ~ 'MT6370_LED_ISNK4', then this LED will choose
+ * 'struct led_classdev isink' to use.
+ */
+ union {
+ struct led_classdev isink;
+ struct led_classdev_mc mc;
+ };
+ struct mt6370_priv *priv;
+ enum led_default_state default_state;
+ u32 index;
+};
+
+struct mt6370_pdata {
+ const unsigned int *tfreq;
+ unsigned int tfreq_len;
+ u16 reg_rgb1_tr;
+ s16 reg_rgb_chrind_tr;
+ u8 pwm_duty;
+};
+
+struct mt6370_priv {
+ /* Per LED access lock */
+ struct mutex lock;
+ struct regmap *regmap;
+ struct regmap_field *fields[F_MAX_FIELDS];
+ const struct reg_field *reg_fields;
+ const struct linear_range *ranges;
+ const struct mt6370_pdata *pdata;
+ unsigned int leds_count;
+ unsigned int leds_active;
+ struct mt6370_led leds[] __counted_by(leds_count);
+};
+
+static const struct reg_field common_reg_fields[F_MAX_FIELDS] = {
+ [F_RGB_EN] = REG_FIELD(MT6370_REG_RGB_EN, 4, 7),
+ [F_CHGIND_EN] = REG_FIELD(MT6370_REG_RGB_CHRIND_DIM, 7, 7),
+ [F_LED1_CURR] = REG_FIELD(MT6370_REG_RGB1_ISNK, 0, 2),
+ [F_LED2_CURR] = REG_FIELD(MT6370_REG_RGB2_ISNK, 0, 2),
+ [F_LED3_CURR] = REG_FIELD(MT6370_REG_RGB3_ISNK, 0, 2),
+ [F_LED4_CURR] = REG_FIELD(MT6370_REG_RGB_CHRIND_CTRL, 0, 1),
+ [F_LED1_MODE] = REG_FIELD(MT6370_REG_RGB1_DIM, 5, 6),
+ [F_LED2_MODE] = REG_FIELD(MT6370_REG_RGB2_DIM, 5, 6),
+ [F_LED3_MODE] = REG_FIELD(MT6370_REG_RGB3_DIM, 5, 6),
+ [F_LED4_MODE] = REG_FIELD(MT6370_REG_RGB_CHRIND_DIM, 5, 6),
+ [F_LED1_DUTY] = REG_FIELD(MT6370_REG_RGB1_DIM, 0, 4),
+ [F_LED2_DUTY] = REG_FIELD(MT6370_REG_RGB2_DIM, 0, 4),
+ [F_LED3_DUTY] = REG_FIELD(MT6370_REG_RGB3_DIM, 0, 4),
+ [F_LED4_DUTY] = REG_FIELD(MT6370_REG_RGB_CHRIND_DIM, 0, 4),
+ [F_LED1_FREQ] = REG_FIELD(MT6370_REG_RGB1_ISNK, 3, 5),
+ [F_LED2_FREQ] = REG_FIELD(MT6370_REG_RGB2_ISNK, 3, 5),
+ [F_LED3_FREQ] = REG_FIELD(MT6370_REG_RGB3_ISNK, 3, 5),
+ [F_LED4_FREQ] = REG_FIELD(MT6370_REG_RGB_CHRIND_CTRL, 2, 4),
+};
+
+static const struct reg_field mt6372_reg_fields[F_MAX_FIELDS] = {
+ [F_RGB_EN] = REG_FIELD(MT6372_REG_RGB_EN, 4, 7),
+ [F_CHGIND_EN] = REG_FIELD(MT6372_REG_RGB_EN, 3, 3),
+ [F_LED1_CURR] = REG_FIELD(MT6372_REG_RGB1_ISNK, 0, 3),
+ [F_LED2_CURR] = REG_FIELD(MT6372_REG_RGB2_ISNK, 0, 3),
+ [F_LED3_CURR] = REG_FIELD(MT6372_REG_RGB3_ISNK, 0, 3),
+ [F_LED4_CURR] = REG_FIELD(MT6372_REG_RGB4_ISNK, 0, 3),
+ [F_LED1_MODE] = REG_FIELD(MT6372_REG_RGB1_ISNK, 6, 7),
+ [F_LED2_MODE] = REG_FIELD(MT6372_REG_RGB2_ISNK, 6, 7),
+ [F_LED3_MODE] = REG_FIELD(MT6372_REG_RGB3_ISNK, 6, 7),
+ [F_LED4_MODE] = REG_FIELD(MT6372_REG_RGB4_ISNK, 6, 7),
+ [F_LED1_DUTY] = REG_FIELD(MT6372_REG_RGB1_DIM, 0, 7),
+ [F_LED2_DUTY] = REG_FIELD(MT6372_REG_RGB2_DIM, 0, 7),
+ [F_LED3_DUTY] = REG_FIELD(MT6372_REG_RGB3_DIM, 0, 7),
+ [F_LED4_DUTY] = REG_FIELD(MT6372_REG_RGB4_DIM, 0, 7),
+ [F_LED1_FREQ] = REG_FIELD(MT6372_REG_RGB12_FREQ, 5, 7),
+ [F_LED2_FREQ] = REG_FIELD(MT6372_REG_RGB12_FREQ, 2, 4),
+ [F_LED3_FREQ] = REG_FIELD(MT6372_REG_RGB34_FREQ, 5, 7),
+ [F_LED4_FREQ] = REG_FIELD(MT6372_REG_RGB34_FREQ, 2, 4),
+};
+
+/* Current unit: microamp, time unit: millisecond */
+static const struct linear_range common_led_ranges[R_MAX_RANGES] = {
+ [R_LED123_CURR] = LINEAR_RANGE(4000, 1, 6, 4000),
+ [R_LED4_CURR] = LINEAR_RANGE(2000, 1, 3, 2000),
+ [R_LED_TRFON] = LINEAR_RANGE(125, 0, 15, 200),
+ [R_LED_TOFF] = LINEAR_RANGE(250, 0, 15, 400),
+};
+
+static const struct linear_range mt6372_led_ranges[R_MAX_RANGES] = {
+ [R_LED123_CURR] = LINEAR_RANGE(2000, 1, 14, 2000),
+ [R_LED4_CURR] = LINEAR_RANGE(2000, 1, 14, 2000),
+ [R_LED_TRFON] = LINEAR_RANGE(125, 0, 15, 250),
+ [R_LED_TOFF] = LINEAR_RANGE(250, 0, 15, 500),
+};
+
+static const unsigned int common_tfreqs[] = {
+ 10000, 5000, 2000, 1000, 500, 200, 5, 1,
+};
+
+static const unsigned int mt6372_tfreqs[] = {
+ 8000, 4000, 2000, 1000, 500, 250, 8, 4,
+};
+
+static const struct mt6370_pdata common_pdata = {
+ .tfreq = common_tfreqs,
+ .tfreq_len = ARRAY_SIZE(common_tfreqs),
+ .pwm_duty = MT6370_PWM_DUTY,
+ .reg_rgb1_tr = MT6370_REG_RGB1_TR,
+ .reg_rgb_chrind_tr = MT6370_REG_RGB_CHRIND_TR,
+};
+
+static const struct mt6370_pdata mt6372_pdata = {
+ .tfreq = mt6372_tfreqs,
+ .tfreq_len = ARRAY_SIZE(mt6372_tfreqs),
+ .pwm_duty = MT6372_PWM_DUTY,
+ .reg_rgb1_tr = MT6372_REG_RGB1_TR,
+ .reg_rgb_chrind_tr = -1,
+};
+
+static enum mt6370_led_field mt6370_get_led_current_field(unsigned int led_no)
+{
+ switch (led_no) {
+ case MT6370_LED_ISNK1:
+ return F_LED1_CURR;
+ case MT6370_LED_ISNK2:
+ return F_LED2_CURR;
+ case MT6370_LED_ISNK3:
+ return F_LED3_CURR;
+ default:
+ return F_LED4_CURR;
+ }
+}
+
+static int mt6370_set_led_brightness(struct mt6370_priv *priv, unsigned int led_no,
+ unsigned int level)
+{
+ enum mt6370_led_field sel_field;
+
+ sel_field = mt6370_get_led_current_field(led_no);
+
+ return regmap_field_write(priv->fields[sel_field], level);
+}
+
+static int mt6370_get_led_brightness(struct mt6370_priv *priv, unsigned int led_no,
+ unsigned int *level)
+{
+ enum mt6370_led_field sel_field;
+
+ sel_field = mt6370_get_led_current_field(led_no);
+
+ return regmap_field_read(priv->fields[sel_field], level);
+}
+
+static int mt6370_set_led_duty(struct mt6370_priv *priv, unsigned int led_no, unsigned int ton,
+ unsigned int toff)
+{
+ const struct mt6370_pdata *pdata = priv->pdata;
+ enum mt6370_led_field sel_field;
+ unsigned int divisor, ratio;
+
+ divisor = pdata->pwm_duty;
+ ratio = ton * divisor / (ton + toff);
+
+ switch (led_no) {
+ case MT6370_LED_ISNK1:
+ sel_field = F_LED1_DUTY;
+ break;
+ case MT6370_LED_ISNK2:
+ sel_field = F_LED2_DUTY;
+ break;
+ case MT6370_LED_ISNK3:
+ sel_field = F_LED3_DUTY;
+ break;
+ default:
+ sel_field = F_LED4_DUTY;
+ break;
+ }
+
+ return regmap_field_write(priv->fields[sel_field], ratio);
+}
+
+static int mt6370_set_led_freq(struct mt6370_priv *priv, unsigned int led_no, unsigned int ton,
+ unsigned int toff)
+{
+ const struct mt6370_pdata *pdata = priv->pdata;
+ enum mt6370_led_field sel_field;
+ unsigned int tfreq_len = pdata->tfreq_len;
+ unsigned int tsum, sel;
+
+ tsum = ton + toff;
+
+ if (tsum > pdata->tfreq[0] || tsum < pdata->tfreq[tfreq_len - 1])
+ return -EOPNOTSUPP;
+
+ sel = find_closest_descending(tsum, pdata->tfreq, tfreq_len);
+
+ switch (led_no) {
+ case MT6370_LED_ISNK1:
+ sel_field = F_LED1_FREQ;
+ break;
+ case MT6370_LED_ISNK2:
+ sel_field = F_LED2_FREQ;
+ break;
+ case MT6370_LED_ISNK3:
+ sel_field = F_LED3_FREQ;
+ break;
+ default:
+ sel_field = F_LED4_FREQ;
+ break;
+ }
+
+ return regmap_field_write(priv->fields[sel_field], sel);
+}
+
+static void mt6370_get_breath_reg_base(struct mt6370_priv *priv, unsigned int led_no,
+ unsigned int *base)
+{
+ const struct mt6370_pdata *pdata = priv->pdata;
+
+ if (pdata->reg_rgb_chrind_tr < 0) {
+ *base = pdata->reg_rgb1_tr + led_no * 3;
+ return;
+ }
+
+ switch (led_no) {
+ case MT6370_LED_ISNK1:
+ case MT6370_LED_ISNK2:
+ case MT6370_LED_ISNK3:
+ *base = pdata->reg_rgb1_tr + led_no * 3;
+ break;
+ default:
+ *base = pdata->reg_rgb_chrind_tr;
+ break;
+ }
+}
+
+static int mt6370_gen_breath_pattern(struct mt6370_priv *priv, struct led_pattern *pattern, u32 len,
+ u8 *pattern_val, u32 val_len)
+{
+ enum mt6370_led_ranges sel_range;
+ struct led_pattern *curr;
+ unsigned int sel;
+ u32 val = 0;
+ int i;
+
+ if (len < P_MAX_PATTERNS && val_len < P_MAX_PATTERNS / 2)
+ return -EINVAL;
+
+ /*
+ * Pattern list
+ * tr1: byte 0, b'[7:4]
+ * tr2: byte 0, b'[3:0]
+ * tf1: byte 1, b'[7:4]
+ * tf2: byte 1, b'[3:0]
+ * ton: byte 2, b'[7:4]
+ * toff: byte 2, b'[3:0]
+ */
+ for (i = 0; i < P_MAX_PATTERNS; i++) {
+ curr = pattern + i;
+
+ sel_range = i == P_LED_TOFF ? R_LED_TOFF : R_LED_TRFON;
+
+ linear_range_get_selector_within(priv->ranges + sel_range, curr->delta_t, &sel);
+
+ if (i % 2) {
+ val |= sel;
+ } else {
+ val <<= 8;
+ val |= sel << 4;
+ }
+ }
+
+ put_unaligned_be24(val, pattern_val);
+
+ return 0;
+}
+
+static int mt6370_set_led_mode(struct mt6370_priv *priv, unsigned int led_no,
+ enum mt6370_led_mode mode)
+{
+ enum mt6370_led_field sel_field;
+
+ switch (led_no) {
+ case MT6370_LED_ISNK1:
+ sel_field = F_LED1_MODE;
+ break;
+ case MT6370_LED_ISNK2:
+ sel_field = F_LED2_MODE;
+ break;
+ case MT6370_LED_ISNK3:
+ sel_field = F_LED3_MODE;
+ break;
+ default:
+ sel_field = F_LED4_MODE;
+ break;
+ }
+
+ return regmap_field_write(priv->fields[sel_field], mode);
+}
+
+static int mt6370_mc_brightness_set(struct led_classdev *lcdev, enum led_brightness level)
+{
+ struct led_classdev_mc *mccdev = lcdev_to_mccdev(lcdev);
+ struct mt6370_led *led = container_of(mccdev, struct mt6370_led, mc);
+ struct mt6370_priv *priv = led->priv;
+ struct mc_subled *subled;
+ unsigned int enable, disable;
+ int i, ret;
+
+ mutex_lock(&priv->lock);
+
+ led_mc_calc_color_components(mccdev, level);
+
+ ret = regmap_field_read(priv->fields[F_RGB_EN], &enable);
+ if (ret)
+ goto out_unlock;
+
+ disable = enable;
+
+ for (i = 0; i < mccdev->num_colors; i++) {
+ u32 brightness;
+
+ subled = mccdev->subled_info + i;
+ brightness = min(subled->brightness, lcdev->max_brightness);
+ disable &= ~MT6370_CHEN_BIT(subled->channel);
+
+ if (level == 0) {
+ enable &= ~MT6370_CHEN_BIT(subled->channel);
+
+ ret = mt6370_set_led_mode(priv, subled->channel, MT6370_LED_REG_MODE);
+ if (ret)
+ goto out_unlock;
+
+ continue;
+ }
+
+ if (brightness == 0) {
+ enable &= ~MT6370_CHEN_BIT(subled->channel);
+ continue;
+ }
+
+ enable |= MT6370_CHEN_BIT(subled->channel);
+
+ ret = mt6370_set_led_brightness(priv, subled->channel, brightness);
+ if (ret)
+ goto out_unlock;
+ }
+
+ ret = regmap_field_write(priv->fields[F_RGB_EN], disable);
+ if (ret)
+ goto out_unlock;
+
+ ret = regmap_field_write(priv->fields[F_RGB_EN], enable);
+
+out_unlock:
+ mutex_unlock(&priv->lock);
+
+ return ret;
+}
+
+static int mt6370_mc_blink_set(struct led_classdev *lcdev,
+ unsigned long *delay_on,
+ unsigned long *delay_off)
+{
+ struct led_classdev_mc *mccdev = lcdev_to_mccdev(lcdev);
+ struct mt6370_led *led = container_of(mccdev, struct mt6370_led, mc);
+ struct mt6370_priv *priv = led->priv;
+ struct mc_subled *subled;
+ unsigned int enable, disable;
+ int i, ret;
+
+ mutex_lock(&priv->lock);
+
+ if (!*delay_on && !*delay_off)
+ *delay_on = *delay_off = 500;
+
+ ret = regmap_field_read(priv->fields[F_RGB_EN], &enable);
+ if (ret)
+ goto out_unlock;
+
+ disable = enable;
+
+ for (i = 0; i < mccdev->num_colors; i++) {
+ subled = mccdev->subled_info + i;
+
+ disable &= ~MT6370_CHEN_BIT(subled->channel);
+
+ ret = mt6370_set_led_duty(priv, subled->channel, *delay_on, *delay_off);
+ if (ret)
+ goto out_unlock;
+
+ ret = mt6370_set_led_freq(priv, subled->channel, *delay_on, *delay_off);
+ if (ret)
+ goto out_unlock;
+
+ ret = mt6370_set_led_mode(priv, subled->channel, MT6370_LED_PWM_MODE);
+ if (ret)
+ goto out_unlock;
+ }
+
+ /* Toggle to make pattern timing the same */
+ ret = regmap_field_write(priv->fields[F_RGB_EN], disable);
+ if (ret)
+ goto out_unlock;
+
+ ret = regmap_field_write(priv->fields[F_RGB_EN], enable);
+
+out_unlock:
+ mutex_unlock(&priv->lock);
+
+ return ret;
+}
+
+static int mt6370_mc_pattern_set(struct led_classdev *lcdev, struct led_pattern *pattern, u32 len,
+ int repeat)
+{
+ struct led_classdev_mc *mccdev = lcdev_to_mccdev(lcdev);
+ struct mt6370_led *led = container_of(mccdev, struct mt6370_led, mc);
+ struct mt6370_priv *priv = led->priv;
+ struct mc_subled *subled;
+ unsigned int reg_base, enable, disable;
+ u8 params[P_MAX_PATTERNS / 2];
+ int i, ret;
+
+ mutex_lock(&priv->lock);
+
+ ret = mt6370_gen_breath_pattern(priv, pattern, len, params, sizeof(params));
+ if (ret)
+ goto out_unlock;
+
+ ret = regmap_field_read(priv->fields[F_RGB_EN], &enable);
+ if (ret)
+ goto out_unlock;
+
+ disable = enable;
+
+ for (i = 0; i < mccdev->num_colors; i++) {
+ subled = mccdev->subled_info + i;
+
+ mt6370_get_breath_reg_base(priv, subled->channel, &reg_base);
+ disable &= ~MT6370_CHEN_BIT(subled->channel);
+
+ ret = regmap_raw_write(priv->regmap, reg_base, params, sizeof(params));
+ if (ret)
+ goto out_unlock;
+
+ ret = mt6370_set_led_mode(priv, subled->channel, MT6370_LED_BREATH_MODE);
+ if (ret)
+ goto out_unlock;
+ }
+
+ /* Toggle to make pattern timing be the same */
+ ret = regmap_field_write(priv->fields[F_RGB_EN], disable);
+ if (ret)
+ goto out_unlock;
+
+ ret = regmap_field_write(priv->fields[F_RGB_EN], enable);
+
+out_unlock:
+ mutex_unlock(&priv->lock);
+
+ return ret;
+}
+
+static inline int mt6370_mc_pattern_clear(struct led_classdev *lcdev)
+{
+ struct led_classdev_mc *mccdev = lcdev_to_mccdev(lcdev);
+ struct mt6370_led *led = container_of(mccdev, struct mt6370_led, mc);
+ struct mt6370_priv *priv = led->priv;
+ struct mc_subled *subled;
+ int i, ret = 0;
+
+ mutex_lock(&led->priv->lock);
+
+ for (i = 0; i < mccdev->num_colors; i++) {
+ subled = mccdev->subled_info + i;
+
+ ret = mt6370_set_led_mode(priv, subled->channel, MT6370_LED_REG_MODE);
+ if (ret)
+ break;
+ }
+
+ mutex_unlock(&led->priv->lock);
+
+ return ret;
+}
+
+static int mt6370_isnk_brightness_set(struct led_classdev *lcdev,
+ enum led_brightness level)
+{
+ struct mt6370_led *led = container_of(lcdev, struct mt6370_led, isink);
+ struct mt6370_priv *priv = led->priv;
+ unsigned int enable;
+ int ret;
+
+ mutex_lock(&priv->lock);
+
+ ret = regmap_field_read(priv->fields[F_RGB_EN], &enable);
+ if (ret)
+ goto out_unlock;
+
+ if (level == 0) {
+ enable &= ~MT6370_CHEN_BIT(led->index);
+
+ ret = mt6370_set_led_mode(priv, led->index, MT6370_LED_REG_MODE);
+ if (ret)
+ goto out_unlock;
+ } else {
+ enable |= MT6370_CHEN_BIT(led->index);
+
+ ret = mt6370_set_led_brightness(priv, led->index, level);
+ if (ret)
+ goto out_unlock;
+ }
+
+ ret = regmap_field_write(priv->fields[F_RGB_EN], enable);
+
+out_unlock:
+ mutex_unlock(&priv->lock);
+
+ return ret;
+}
+
+static int mt6370_isnk_blink_set(struct led_classdev *lcdev, unsigned long *delay_on,
+ unsigned long *delay_off)
+{
+ struct mt6370_led *led = container_of(lcdev, struct mt6370_led, isink);
+ struct mt6370_priv *priv = led->priv;
+ int ret;
+
+ mutex_lock(&priv->lock);
+
+ if (!*delay_on && !*delay_off)
+ *delay_on = *delay_off = 500;
+
+ ret = mt6370_set_led_duty(priv, led->index, *delay_on, *delay_off);
+ if (ret)
+ goto out_unlock;
+
+ ret = mt6370_set_led_freq(priv, led->index, *delay_on, *delay_off);
+ if (ret)
+ goto out_unlock;
+
+ ret = mt6370_set_led_mode(priv, led->index, MT6370_LED_PWM_MODE);
+
+out_unlock:
+ mutex_unlock(&priv->lock);
+
+ return ret;
+}
+
+static int mt6370_isnk_pattern_set(struct led_classdev *lcdev, struct led_pattern *pattern, u32 len,
+ int repeat)
+{
+ struct mt6370_led *led = container_of(lcdev, struct mt6370_led, isink);
+ struct mt6370_priv *priv = led->priv;
+ unsigned int reg_base;
+ u8 params[P_MAX_PATTERNS / 2];
+ int ret;
+
+ mutex_lock(&priv->lock);
+
+ ret = mt6370_gen_breath_pattern(priv, pattern, len, params, sizeof(params));
+ if (ret)
+ goto out_unlock;
+
+ mt6370_get_breath_reg_base(priv, led->index, &reg_base);
+
+ ret = regmap_raw_write(priv->regmap, reg_base, params, sizeof(params));
+ if (ret)
+ goto out_unlock;
+
+ ret = mt6370_set_led_mode(priv, led->index, MT6370_LED_BREATH_MODE);
+
+out_unlock:
+ mutex_unlock(&priv->lock);
+
+ return ret;
+}
+
+static inline int mt6370_isnk_pattern_clear(struct led_classdev *lcdev)
+{
+ struct mt6370_led *led = container_of(lcdev, struct mt6370_led, isink);
+ struct mt6370_priv *priv = led->priv;
+ int ret;
+
+ mutex_lock(&led->priv->lock);
+ ret = mt6370_set_led_mode(priv, led->index, MT6370_LED_REG_MODE);
+ mutex_unlock(&led->priv->lock);
+
+ return ret;
+}
+
+static int mt6370_assign_multicolor_info(struct device *dev, struct mt6370_led *led,
+ struct fwnode_handle *fwnode)
+{
+ struct mt6370_priv *priv = led->priv;
+ struct fwnode_handle *child;
+ struct mc_subled *sub_led;
+ u32 num_color = 0;
+ int ret;
+
+ sub_led = devm_kcalloc(dev, MC_CHANNEL_NUM, sizeof(*sub_led), GFP_KERNEL);
+ if (!sub_led)
+ return -ENOMEM;
+
+ fwnode_for_each_child_node(fwnode, child) {
+ u32 reg, color;
+
+ ret = fwnode_property_read_u32(child, "reg", &reg);
+ if (ret || reg > MT6370_LED_ISNK3 || priv->leds_active & BIT(reg)) {
+ fwnode_handle_put(child);
+ return -EINVAL;
+ }
+
+ ret = fwnode_property_read_u32(child, "color", &color);
+ if (ret) {
+ fwnode_handle_put(child);
+ return dev_err_probe(dev, ret, "LED %d, no color specified\n", led->index);
+ }
+
+ priv->leds_active |= BIT(reg);
+ sub_led[num_color].color_index = color;
+ sub_led[num_color].channel = reg;
+ sub_led[num_color].intensity = 0;
+ num_color++;
+ }
+
+ if (num_color < 2)
+ return dev_err_probe(dev, -EINVAL,
+ "Multicolor must include 2 or more LED channels\n");
+
+ led->mc.num_colors = num_color;
+ led->mc.subled_info = sub_led;
+
+ return 0;
+}
+
+static int mt6370_init_led_properties(struct device *dev, struct mt6370_led *led,
+ struct led_init_data *init_data)
+{
+ struct mt6370_priv *priv = led->priv;
+ struct led_classdev *lcdev;
+ enum mt6370_led_ranges sel_range;
+ u32 max_uA, max_level;
+ int ret;
+
+ if (led->index == MT6370_VIRTUAL_MULTICOLOR) {
+ ret = mt6370_assign_multicolor_info(dev, led, init_data->fwnode);
+ if (ret)
+ return ret;
+
+ lcdev = &led->mc.led_cdev;
+ lcdev->brightness_set_blocking = mt6370_mc_brightness_set;
+ lcdev->blink_set = mt6370_mc_blink_set;
+ lcdev->pattern_set = mt6370_mc_pattern_set;
+ lcdev->pattern_clear = mt6370_mc_pattern_clear;
+ } else {
+ lcdev = &led->isink;
+ lcdev->brightness_set_blocking = mt6370_isnk_brightness_set;
+ lcdev->blink_set = mt6370_isnk_blink_set;
+ lcdev->pattern_set = mt6370_isnk_pattern_set;
+ lcdev->pattern_clear = mt6370_isnk_pattern_clear;
+ }
+
+ ret = fwnode_property_read_u32(init_data->fwnode, "led-max-microamp", &max_uA);
+ if (ret) {
+ dev_warn(dev, "Not specified led-max-microamp, config to the minimum\n");
+ max_uA = 0;
+ }
+
+ if (led->index == MT6370_LED_ISNK4)
+ sel_range = R_LED4_CURR;
+ else
+ sel_range = R_LED123_CURR;
+
+ linear_range_get_selector_within(priv->ranges + sel_range, max_uA, &max_level);
+
+ lcdev->max_brightness = max_level;
+
+ led->default_state = led_init_default_state_get(init_data->fwnode);
+
+ return 0;
+}
+
+static int mt6370_isnk_init_default_state(struct mt6370_led *led)
+{
+ struct mt6370_priv *priv = led->priv;
+ unsigned int enable, level;
+ int ret;
+
+ ret = mt6370_get_led_brightness(priv, led->index, &level);
+ if (ret)
+ return ret;
+
+ ret = regmap_field_read(priv->fields[F_RGB_EN], &enable);
+ if (ret)
+ return ret;
+
+ if (!(enable & MT6370_CHEN_BIT(led->index)))
+ level = 0;
+
+ switch (led->default_state) {
+ case LEDS_DEFSTATE_ON:
+ led->isink.brightness = led->isink.max_brightness;
+ break;
+ case LEDS_DEFSTATE_KEEP:
+ led->isink.brightness = min(level, led->isink.max_brightness);
+ break;
+ default:
+ led->isink.brightness = 0;
+ break;
+ }
+
+ return mt6370_isnk_brightness_set(&led->isink, led->isink.brightness);
+}
+
+static int mt6370_multicolor_led_register(struct device *dev, struct mt6370_led *led,
+ struct led_init_data *init_data)
+{
+ int ret;
+
+ ret = mt6370_mc_brightness_set(&led->mc.led_cdev, 0);
+ if (ret)
+ return dev_err_probe(dev, ret, "Couldn't set multicolor brightness\n");
+
+ ret = devm_led_classdev_multicolor_register_ext(dev, &led->mc, init_data);
+ if (ret)
+ return dev_err_probe(dev, ret, "Couldn't register multicolor\n");
+
+ return 0;
+}
+
+static int mt6370_led_register(struct device *dev, struct mt6370_led *led,
+ struct led_init_data *init_data)
+{
+ struct mt6370_priv *priv = led->priv;
+ int ret;
+
+ if (led->index == MT6370_VIRTUAL_MULTICOLOR)
+ return mt6370_multicolor_led_register(dev, led, init_data);
+
+ /* If ISNK4 is declared, change its mode from HW auto to SW control */
+ if (led->index == MT6370_LED_ISNK4) {
+ ret = regmap_field_write(priv->fields[F_CHGIND_EN], 1);
+ if (ret)
+ return dev_err_probe(dev, ret, "Failed to set CHRIND to SW\n");
+ }
+
+ ret = mt6370_isnk_init_default_state(led);
+ if (ret)
+ return dev_err_probe(dev, ret, "Failed to init %d isnk state\n", led->index);
+
+ ret = devm_led_classdev_register_ext(dev, &led->isink, init_data);
+ if (ret)
+ return dev_err_probe(dev, ret, "Couldn't register isink %d\n", led->index);
+
+ return 0;
+}
+
+static int mt6370_check_vendor_info(struct mt6370_priv *priv)
+{
+ unsigned int devinfo, vid;
+ int ret;
+
+ ret = regmap_read(priv->regmap, MT6370_REG_DEV_INFO, &devinfo);
+ if (ret)
+ return ret;
+
+ vid = FIELD_GET(MT6370_VENDOR_ID_MASK, devinfo);
+ if (vid == MT6372_VENDOR_ID || vid == MT6372C_VENDOR_ID) {
+ priv->reg_fields = mt6372_reg_fields;
+ priv->ranges = mt6372_led_ranges;
+ priv->pdata = &mt6372_pdata;
+ } else {
+ /* Common for MT6370/71 */
+ priv->reg_fields = common_reg_fields;
+ priv->ranges = common_led_ranges;
+ priv->pdata = &common_pdata;
+ }
+
+ return 0;
+}
+
+static int mt6370_leds_probe(struct platform_device *pdev)
+{
+ struct device *dev = &pdev->dev;
+ struct mt6370_priv *priv;
+ size_t count;
+ unsigned int i = 0;
+ int ret;
+
+ count = device_get_child_node_count(dev);
+ if (!count || count > MT6370_MAX_LEDS)
+ return dev_err_probe(dev, -EINVAL,
+ "No child node or node count over max LED number %zu\n",
+ count);
+
+ priv = devm_kzalloc(dev, struct_size(priv, leds, count), GFP_KERNEL);
+ if (!priv)
+ return -ENOMEM;
+
+ priv->leds_count = count;
+ mutex_init(&priv->lock);
+
+ priv->regmap = dev_get_regmap(dev->parent, NULL);
+ if (!priv->regmap)
+ return dev_err_probe(dev, -ENODEV, "Failed to get parent regmap\n");
+
+ ret = mt6370_check_vendor_info(priv);
+ if (ret)
+ return dev_err_probe(dev, ret, "Failed to check vendor info\n");
+
+ ret = devm_regmap_field_bulk_alloc(dev, priv->regmap, priv->fields, priv->reg_fields,
+ F_MAX_FIELDS);
+ if (ret)
+ return dev_err_probe(dev, ret, "Failed to allocate regmap field\n");
+
+ device_for_each_child_node_scoped(dev, child) {
+ struct mt6370_led *led = priv->leds + i++;
+ struct led_init_data init_data = { .fwnode = child };
+ u32 reg, color;
+
+ ret = fwnode_property_read_u32(child, "reg", &reg);
+ if (ret)
+ dev_err_probe(dev, ret, "Failed to parse reg property\n");
+
+ if (reg >= MT6370_MAX_LEDS)
+ return dev_err_probe(dev, -EINVAL, "Error reg property number\n");
+
+ ret = fwnode_property_read_u32(child, "color", &color);
+ if (ret)
+ return dev_err_probe(dev, ret, "Failed to parse color property\n");
+
+ if (color == LED_COLOR_ID_RGB || color == LED_COLOR_ID_MULTI)
+ reg = MT6370_VIRTUAL_MULTICOLOR;
+
+ if (priv->leds_active & BIT(reg))
+ return dev_err_probe(dev, -EINVAL, "Duplicate reg property\n");
+
+ priv->leds_active |= BIT(reg);
+
+ led->index = reg;
+ led->priv = priv;
+
+ ret = mt6370_init_led_properties(dev, led, &init_data);
+ if (ret)
+ return ret;
+
+ ret = mt6370_led_register(dev, led, &init_data);
+ if (ret)
+ return ret;
+ }
+
+ return 0;
+}
+
+static const struct of_device_id mt6370_rgbled_device_table[] = {
+ { .compatible = "mediatek,mt6370-indicator" },
+ {}
+};
+MODULE_DEVICE_TABLE(of, mt6370_rgbled_device_table);
+
+static struct platform_driver mt6370_rgbled_driver = {
+ .driver = {
+ .name = "mt6370-indicator",
+ .of_match_table = mt6370_rgbled_device_table,
+ },
+ .probe = mt6370_leds_probe,
+};
+module_platform_driver(mt6370_rgbled_driver);
+
+MODULE_AUTHOR("Alice Chen <alice_chen@richtek.com>");
+MODULE_AUTHOR("ChiYuan Huang <cy_huang@richtek.com>");
+MODULE_DESCRIPTION("MediaTek MT6370 RGB LED Driver");
+MODULE_LICENSE("GPL");
diff --git a/drivers/leds/rgb/leds-ncp5623.c b/drivers/leds/rgb/leds-ncp5623.c
new file mode 100644
index 000000000..f2528f065
--- /dev/null
+++ b/drivers/leds/rgb/leds-ncp5623.c
@@ -0,0 +1,270 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * NCP5623 Multi-LED Driver
+ *
+ * Author: Abdel Alkuor <alkuor@gmail.com>
+ * Datasheet: https://www.onsemi.com/pdf/datasheet/ncp5623-d.pdf
+ */
+
+#include <linux/i2c.h>
+#include <linux/module.h>
+
+#include <linux/led-class-multicolor.h>
+
+#define NCP5623_FUNCTION_OFFSET 0x5
+#define NCP5623_REG(x) ((x) << NCP5623_FUNCTION_OFFSET)
+
+#define NCP5623_SHUTDOWN_REG NCP5623_REG(0x0)
+#define NCP5623_ILED_REG NCP5623_REG(0x1)
+#define NCP5623_PWM_REG(index) NCP5623_REG(0x2 + (index))
+#define NCP5623_UPWARD_STEP_REG NCP5623_REG(0x5)
+#define NCP5623_DOWNWARD_STEP_REG NCP5623_REG(0x6)
+#define NCP5623_DIMMING_TIME_REG NCP5623_REG(0x7)
+
+#define NCP5623_MAX_BRIGHTNESS 0x1f
+#define NCP5623_MAX_DIM_TIME_MS 240
+#define NCP5623_DIM_STEP_MS 8
+
+struct ncp5623 {
+ struct i2c_client *client;
+ struct led_classdev_mc mc_dev;
+ struct mutex lock;
+
+ int current_brightness;
+ unsigned long delay;
+};
+
+static int ncp5623_write(struct i2c_client *client, u8 reg, u8 data)
+{
+ return i2c_smbus_write_byte_data(client, reg | data, 0);
+}
+
+static int ncp5623_brightness_set(struct led_classdev *cdev,
+ enum led_brightness brightness)
+{
+ struct led_classdev_mc *mc_cdev = lcdev_to_mccdev(cdev);
+ struct ncp5623 *ncp = container_of(mc_cdev, struct ncp5623, mc_dev);
+ int ret;
+
+ guard(mutex)(&ncp->lock);
+
+ if (ncp->delay && time_is_after_jiffies(ncp->delay))
+ return -EBUSY;
+
+ ncp->delay = 0;
+
+ for (int i = 0; i < mc_cdev->num_colors; i++) {
+ ret = ncp5623_write(ncp->client,
+ NCP5623_PWM_REG(mc_cdev->subled_info[i].channel),
+ mc_cdev->subled_info[i].intensity);
+ if (ret)
+ return ret;
+ }
+
+ ret = ncp5623_write(ncp->client, NCP5623_DIMMING_TIME_REG, 0);
+ if (ret)
+ return ret;
+
+ ret = ncp5623_write(ncp->client, NCP5623_ILED_REG, brightness);
+ if (ret)
+ return ret;
+
+ ncp->current_brightness = brightness;
+
+ return 0;
+}
+
+static int ncp5623_pattern_set(struct led_classdev *cdev,
+ struct led_pattern *pattern,
+ u32 len, int repeat)
+{
+ struct led_classdev_mc *mc_cdev = lcdev_to_mccdev(cdev);
+ struct ncp5623 *ncp = container_of(mc_cdev, struct ncp5623, mc_dev);
+ int brightness_diff;
+ u8 reg;
+ int ret;
+
+ guard(mutex)(&ncp->lock);
+
+ if (ncp->delay && time_is_after_jiffies(ncp->delay))
+ return -EBUSY;
+
+ ncp->delay = 0;
+
+ if (pattern[0].delta_t > NCP5623_MAX_DIM_TIME_MS ||
+ (pattern[0].delta_t % NCP5623_DIM_STEP_MS) != 0)
+ return -EINVAL;
+
+ brightness_diff = pattern[0].brightness - ncp->current_brightness;
+
+ if (brightness_diff == 0)
+ return 0;
+
+ if (pattern[0].delta_t) {
+ if (brightness_diff > 0)
+ reg = NCP5623_UPWARD_STEP_REG;
+ else
+ reg = NCP5623_DOWNWARD_STEP_REG;
+ } else {
+ reg = NCP5623_ILED_REG;
+ }
+
+ ret = ncp5623_write(ncp->client, reg,
+ min(pattern[0].brightness, NCP5623_MAX_BRIGHTNESS));
+ if (ret)
+ return ret;
+
+ ret = ncp5623_write(ncp->client,
+ NCP5623_DIMMING_TIME_REG,
+ pattern[0].delta_t / NCP5623_DIM_STEP_MS);
+ if (ret)
+ return ret;
+
+ /*
+ * During testing, when the brightness difference is 1, for some
+ * unknown reason, the time factor it takes to change to the new
+ * value is the longest time possible. Otherwise, the time factor
+ * is simply the brightness difference.
+ *
+ * For example:
+ * current_brightness = 20 and new_brightness = 21 then the time it
+ * takes to set the new brightness increments to the maximum possible
+ * brightness from 20 then from 0 to 21.
+ * time_factor = max_brightness - 20 + 21
+ */
+ if (abs(brightness_diff) == 1)
+ ncp->delay = NCP5623_MAX_BRIGHTNESS + brightness_diff;
+ else
+ ncp->delay = abs(brightness_diff);
+
+ ncp->delay = msecs_to_jiffies(ncp->delay * pattern[0].delta_t) + jiffies;
+
+ ncp->current_brightness = pattern[0].brightness;
+
+ return 0;
+}
+
+static int ncp5623_pattern_clear(struct led_classdev *led_cdev)
+{
+ return 0;
+}
+
+static int ncp5623_probe(struct i2c_client *client)
+{
+ struct device *dev = &client->dev;
+ struct fwnode_handle *mc_node, *led_node;
+ struct led_init_data init_data = { };
+ struct ncp5623 *ncp;
+ struct mc_subled *subled_info;
+ unsigned int num_subleds;
+ u32 color_index;
+ u32 reg;
+ int ret;
+
+ ncp = devm_kzalloc(dev, sizeof(*ncp), GFP_KERNEL);
+ if (!ncp)
+ return -ENOMEM;
+
+ ncp->client = client;
+
+ mc_node = device_get_named_child_node(dev, "multi-led");
+ if (!mc_node)
+ return -EINVAL;
+
+ num_subleds = fwnode_get_child_node_count(mc_node);
+
+ subled_info = devm_kcalloc(dev, num_subleds, sizeof(*subled_info), GFP_KERNEL);
+ if (!subled_info) {
+ ret = -ENOMEM;
+ goto release_mc_node;
+ }
+
+ fwnode_for_each_child_node(mc_node, led_node) {
+ ret = fwnode_property_read_u32(led_node, "color", &color_index);
+ if (ret)
+ goto release_led_node;
+
+ ret = fwnode_property_read_u32(led_node, "reg", &reg);
+ if (ret)
+ goto release_led_node;
+
+ subled_info[ncp->mc_dev.num_colors].channel = reg;
+ subled_info[ncp->mc_dev.num_colors].max_intensity = NCP5623_MAX_BRIGHTNESS;
+ subled_info[ncp->mc_dev.num_colors++].color_index = color_index;
+ }
+
+ init_data.fwnode = mc_node;
+
+ ncp->mc_dev.led_cdev.max_brightness = NCP5623_MAX_BRIGHTNESS;
+ ncp->mc_dev.subled_info = subled_info;
+ ncp->mc_dev.led_cdev.brightness_set_blocking = ncp5623_brightness_set;
+ ncp->mc_dev.led_cdev.pattern_set = ncp5623_pattern_set;
+ ncp->mc_dev.led_cdev.pattern_clear = ncp5623_pattern_clear;
+ ncp->mc_dev.led_cdev.default_trigger = "pattern";
+
+ mutex_init(&ncp->lock);
+ i2c_set_clientdata(client, ncp);
+
+ ret = led_classdev_multicolor_register_ext(dev, &ncp->mc_dev, &init_data);
+ if (ret)
+ goto destroy_lock;
+
+ return 0;
+
+destroy_lock:
+ mutex_destroy(&ncp->lock);
+
+release_mc_node:
+ fwnode_handle_put(mc_node);
+
+ return ret;
+
+release_led_node:
+ fwnode_handle_put(led_node);
+ goto release_mc_node;
+}
+
+static void ncp5623_remove(struct i2c_client *client)
+{
+ struct ncp5623 *ncp = i2c_get_clientdata(client);
+
+ mutex_lock(&ncp->lock);
+ ncp->delay = 0;
+ mutex_unlock(&ncp->lock);
+
+ ncp5623_write(client, NCP5623_DIMMING_TIME_REG, 0);
+ led_classdev_multicolor_unregister(&ncp->mc_dev);
+ mutex_destroy(&ncp->lock);
+}
+
+static void ncp5623_shutdown(struct i2c_client *client)
+{
+ struct ncp5623 *ncp = i2c_get_clientdata(client);
+
+ if (!(ncp->mc_dev.led_cdev.flags & LED_RETAIN_AT_SHUTDOWN))
+ ncp5623_write(client, NCP5623_SHUTDOWN_REG, 0);
+
+ mutex_destroy(&ncp->lock);
+}
+
+static const struct of_device_id ncp5623_id[] = {
+ { .compatible = "onnn,ncp5623" },
+ { }
+};
+MODULE_DEVICE_TABLE(of, ncp5623_id);
+
+static struct i2c_driver ncp5623_i2c_driver = {
+ .driver = {
+ .name = "ncp5623",
+ .of_match_table = ncp5623_id,
+ },
+ .probe = ncp5623_probe,
+ .remove = ncp5623_remove,
+ .shutdown = ncp5623_shutdown,
+};
+
+module_i2c_driver(ncp5623_i2c_driver);
+
+MODULE_AUTHOR("Abdel Alkuor <alkuor@gmail.com>");
+MODULE_DESCRIPTION("NCP5623 Multi-LED driver");
+MODULE_LICENSE("GPL");
diff --git a/drivers/leds/rgb/leds-pwm-multicolor.c b/drivers/leds/rgb/leds-pwm-multicolor.c
new file mode 100644
index 000000000..6365459ec
--- /dev/null
+++ b/drivers/leds/rgb/leds-pwm-multicolor.c
@@ -0,0 +1,199 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * PWM-based multi-color LED control
+ *
+ * Copyright 2022 Sven Schwermer <sven.schwermer@disruptive-technologies.com>
+ */
+
+#include <linux/err.h>
+#include <linux/kernel.h>
+#include <linux/led-class-multicolor.h>
+#include <linux/leds.h>
+#include <linux/module.h>
+#include <linux/mutex.h>
+#include <linux/platform_device.h>
+#include <linux/property.h>
+#include <linux/pwm.h>
+
+struct pwm_led {
+ struct pwm_device *pwm;
+ struct pwm_state state;
+ bool active_low;
+};
+
+struct pwm_mc_led {
+ struct led_classdev_mc mc_cdev;
+ struct mutex lock;
+ struct pwm_led leds[];
+};
+
+static int led_pwm_mc_set(struct led_classdev *cdev,
+ enum led_brightness brightness)
+{
+ struct led_classdev_mc *mc_cdev = lcdev_to_mccdev(cdev);
+ struct pwm_mc_led *priv = container_of(mc_cdev, struct pwm_mc_led, mc_cdev);
+ unsigned long long duty;
+ int ret = 0;
+ int i;
+
+ led_mc_calc_color_components(mc_cdev, brightness);
+
+ mutex_lock(&priv->lock);
+
+ for (i = 0; i < mc_cdev->num_colors; i++) {
+ duty = priv->leds[i].state.period;
+ duty *= mc_cdev->subled_info[i].brightness;
+ do_div(duty, cdev->max_brightness);
+
+ if (priv->leds[i].active_low)
+ duty = priv->leds[i].state.period - duty;
+
+ priv->leds[i].state.duty_cycle = duty;
+ /*
+ * Disabling a PWM doesn't guarantee that it emits the inactive level.
+ * So keep it on. Only for suspending the PWM should be disabled because
+ * otherwise it refuses to suspend. The possible downside is that the
+ * LED might stay (or even go) on.
+ */
+ priv->leds[i].state.enabled = !(cdev->flags & LED_SUSPENDED);
+ ret = pwm_apply_might_sleep(priv->leds[i].pwm,
+ &priv->leds[i].state);
+ if (ret)
+ break;
+ }
+
+ mutex_unlock(&priv->lock);
+
+ return ret;
+}
+
+static int iterate_subleds(struct device *dev, struct pwm_mc_led *priv,
+ struct fwnode_handle *mcnode)
+{
+ struct mc_subled *subled = priv->mc_cdev.subled_info;
+ struct fwnode_handle *fwnode;
+ struct pwm_led *pwmled;
+ u32 color;
+ int ret;
+
+ /* iterate over the nodes inside the multi-led node */
+ fwnode_for_each_child_node(mcnode, fwnode) {
+ pwmled = &priv->leds[priv->mc_cdev.num_colors];
+ pwmled->pwm = devm_fwnode_pwm_get(dev, fwnode, NULL);
+ if (IS_ERR(pwmled->pwm)) {
+ ret = dev_err_probe(dev, PTR_ERR(pwmled->pwm), "unable to request PWM\n");
+ goto release_fwnode;
+ }
+ pwm_init_state(pwmled->pwm, &pwmled->state);
+ pwmled->active_low = fwnode_property_read_bool(fwnode, "active-low");
+
+ ret = fwnode_property_read_u32(fwnode, "color", &color);
+ if (ret) {
+ dev_err(dev, "cannot read color: %d\n", ret);
+ goto release_fwnode;
+ }
+
+ subled[priv->mc_cdev.num_colors].color_index = color;
+ fwnode_property_read_u32(fwnode, "default-intensity",
+ &subled[priv->mc_cdev.num_colors].intensity);
+ priv->mc_cdev.num_colors++;
+ }
+
+ return 0;
+
+release_fwnode:
+ fwnode_handle_put(fwnode);
+ return ret;
+}
+
+static int led_pwm_mc_probe(struct platform_device *pdev)
+{
+ struct fwnode_handle *mcnode;
+ struct led_init_data init_data = {};
+ struct led_classdev *cdev;
+ struct mc_subled *subled;
+ struct pwm_mc_led *priv;
+ unsigned int count;
+ int ret = 0;
+
+ mcnode = device_get_named_child_node(&pdev->dev, "multi-led");
+ if (!mcnode)
+ return dev_err_probe(&pdev->dev, -ENODEV,
+ "expected multi-led node\n");
+
+ /* count the nodes inside the multi-led node */
+ count = fwnode_get_child_node_count(mcnode);
+
+ priv = devm_kzalloc(&pdev->dev, struct_size(priv, leds, count),
+ GFP_KERNEL);
+ if (!priv) {
+ ret = -ENOMEM;
+ goto release_mcnode;
+ }
+ mutex_init(&priv->lock);
+
+ subled = devm_kcalloc(&pdev->dev, count, sizeof(*subled), GFP_KERNEL);
+ if (!subled) {
+ ret = -ENOMEM;
+ goto release_mcnode;
+ }
+ priv->mc_cdev.subled_info = subled;
+
+ /* init the multicolor's LED class device */
+ cdev = &priv->mc_cdev.led_cdev;
+ ret = fwnode_property_read_u32(mcnode, "max-brightness",
+ &cdev->max_brightness);
+ if (ret)
+ goto release_mcnode;
+
+ cdev->flags = LED_CORE_SUSPENDRESUME;
+ cdev->brightness_set_blocking = led_pwm_mc_set;
+
+ ret = iterate_subleds(&pdev->dev, priv, mcnode);
+ if (ret)
+ goto release_mcnode;
+
+ init_data.fwnode = mcnode;
+ ret = devm_led_classdev_multicolor_register_ext(&pdev->dev,
+ &priv->mc_cdev,
+ &init_data);
+ if (ret) {
+ dev_err(&pdev->dev,
+ "failed to register multicolor PWM led for %s: %d\n",
+ cdev->name, ret);
+ goto release_mcnode;
+ }
+
+ ret = led_pwm_mc_set(cdev, cdev->brightness);
+ if (ret)
+ return dev_err_probe(&pdev->dev, ret,
+ "failed to set led PWM value for %s\n",
+ cdev->name);
+
+ platform_set_drvdata(pdev, priv);
+ return 0;
+
+release_mcnode:
+ fwnode_handle_put(mcnode);
+ return ret;
+}
+
+static const struct of_device_id of_pwm_leds_mc_match[] = {
+ { .compatible = "pwm-leds-multicolor", },
+ {}
+};
+MODULE_DEVICE_TABLE(of, of_pwm_leds_mc_match);
+
+static struct platform_driver led_pwm_mc_driver = {
+ .probe = led_pwm_mc_probe,
+ .driver = {
+ .name = "leds_pwm_multicolor",
+ .of_match_table = of_pwm_leds_mc_match,
+ },
+};
+module_platform_driver(led_pwm_mc_driver);
+
+MODULE_AUTHOR("Sven Schwermer <sven.schwermer@disruptive-technologies.com>");
+MODULE_DESCRIPTION("multi-color PWM LED driver");
+MODULE_LICENSE("GPL v2");
+MODULE_ALIAS("platform:leds-pwm-multicolor");
diff --git a/drivers/leds/rgb/leds-qcom-lpg.c b/drivers/leds/rgb/leds-qcom-lpg.c
new file mode 100644
index 000000000..d7d6518de
--- /dev/null
+++ b/drivers/leds/rgb/leds-qcom-lpg.c
@@ -0,0 +1,1850 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Copyright (c) 2017-2022 Linaro Ltd
+ * Copyright (c) 2010-2012, The Linux Foundation. All rights reserved.
+ * Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries.
+ */
+#include <linux/bits.h>
+#include <linux/bitfield.h>
+#include <linux/led-class-multicolor.h>
+#include <linux/module.h>
+#include <linux/nvmem-consumer.h>
+#include <linux/of.h>
+#include <linux/platform_device.h>
+#include <linux/pwm.h>
+#include <linux/regmap.h>
+#include <linux/slab.h>
+#include <linux/soc/qcom/qcom-pbs.h>
+
+#define LPG_SUBTYPE_REG 0x05
+#define LPG_SUBTYPE_LPG 0x2
+#define LPG_SUBTYPE_PWM 0xb
+#define LPG_SUBTYPE_HI_RES_PWM 0xc
+#define LPG_SUBTYPE_LPG_LITE 0x11
+#define LPG_PATTERN_CONFIG_REG 0x40
+#define LPG_SIZE_CLK_REG 0x41
+#define PWM_CLK_SELECT_MASK GENMASK(1, 0)
+#define PWM_SIZE_SELECT_MASK BIT(2)
+#define PWM_CLK_SELECT_HI_RES_MASK GENMASK(2, 0)
+#define PWM_SIZE_HI_RES_MASK GENMASK(6, 4)
+#define LPG_PREDIV_CLK_REG 0x42
+#define PWM_FREQ_PRE_DIV_MASK GENMASK(6, 5)
+#define PWM_FREQ_EXP_MASK GENMASK(2, 0)
+#define PWM_TYPE_CONFIG_REG 0x43
+#define PWM_VALUE_REG 0x44
+#define PWM_ENABLE_CONTROL_REG 0x46
+#define PWM_SYNC_REG 0x47
+#define LPG_RAMP_DURATION_REG 0x50
+#define LPG_HI_PAUSE_REG 0x52
+#define LPG_LO_PAUSE_REG 0x54
+#define LPG_HI_IDX_REG 0x56
+#define LPG_LO_IDX_REG 0x57
+#define PWM_SEC_ACCESS_REG 0xd0
+#define PWM_DTEST_REG(x) (0xe2 + (x) - 1)
+
+#define SDAM_REG_PBS_SEQ_EN 0x42
+#define SDAM_PBS_TRIG_SET 0xe5
+#define SDAM_PBS_TRIG_CLR 0xe6
+
+#define TRI_LED_SRC_SEL 0x45
+#define TRI_LED_EN_CTL 0x46
+#define TRI_LED_ATC_CTL 0x47
+
+#define LPG_LUT_REG(x) (0x40 + (x) * 2)
+#define RAMP_CONTROL_REG 0xc8
+
+#define LPG_RESOLUTION_9BIT BIT(9)
+#define LPG_RESOLUTION_15BIT BIT(15)
+#define PPG_MAX_LED_BRIGHTNESS 255
+
+#define LPG_MAX_M 7
+#define LPG_MAX_PREDIV 6
+
+#define DEFAULT_TICK_DURATION_US 7800
+#define RAMP_STEP_DURATION(x) (((x) * 1000 / DEFAULT_TICK_DURATION_US) & 0xff)
+
+#define SDAM_MAX_DEVICES 2
+/* LPG common config settings for PPG */
+#define SDAM_START_BASE 0x40
+#define SDAM_REG_RAMP_STEP_DURATION 0x47
+
+#define SDAM_LUT_SDAM_LUT_PATTERN_OFFSET 0x45
+#define SDAM_LPG_SDAM_LUT_PATTERN_OFFSET 0x80
+
+/* LPG per channel config settings for PPG */
+#define SDAM_LUT_EN_OFFSET 0x0
+#define SDAM_PATTERN_CONFIG_OFFSET 0x1
+#define SDAM_END_INDEX_OFFSET 0x3
+#define SDAM_START_INDEX_OFFSET 0x4
+#define SDAM_PBS_SCRATCH_LUT_COUNTER_OFFSET 0x6
+#define SDAM_PAUSE_HI_MULTIPLIER_OFFSET 0x8
+#define SDAM_PAUSE_LO_MULTIPLIER_OFFSET 0x9
+
+struct lpg_data;
+
+/**
+ * struct lpg_channel - per channel data
+ * @lpg: reference to parent lpg
+ * @base: base address of the PWM channel
+ * @triled_mask: mask in TRILED to enable this channel
+ * @lut_mask: mask in LUT to start pattern generator for this channel
+ * @subtype: PMIC hardware block subtype
+ * @sdam_offset: channel offset in LPG SDAM
+ * @in_use: channel is exposed to LED framework
+ * @color: color of the LED attached to this channel
+ * @dtest_line: DTEST line for output, or 0 if disabled
+ * @dtest_value: DTEST line configuration
+ * @pwm_value: duty (in microseconds) of the generated pulses, overridden by LUT
+ * @enabled: output enabled?
+ * @period: period (in nanoseconds) of the generated pulses
+ * @clk_sel: reference clock frequency selector
+ * @pre_div_sel: divider selector of the reference clock
+ * @pre_div_exp: exponential divider of the reference clock
+ * @pwm_resolution_sel: pwm resolution selector
+ * @ramp_enabled: duty cycle is driven by iterating over lookup table
+ * @ramp_ping_pong: reverse through pattern, rather than wrapping to start
+ * @ramp_oneshot: perform only a single pass over the pattern
+ * @ramp_reverse: iterate over pattern backwards
+ * @ramp_tick_ms: length (in milliseconds) of one step in the pattern
+ * @ramp_lo_pause_ms: pause (in milliseconds) before iterating over pattern
+ * @ramp_hi_pause_ms: pause (in milliseconds) after iterating over pattern
+ * @pattern_lo_idx: start index of associated pattern
+ * @pattern_hi_idx: last index of associated pattern
+ */
+struct lpg_channel {
+ struct lpg *lpg;
+
+ u32 base;
+ unsigned int triled_mask;
+ unsigned int lut_mask;
+ unsigned int subtype;
+ u32 sdam_offset;
+
+ bool in_use;
+
+ int color;
+
+ u32 dtest_line;
+ u32 dtest_value;
+
+ u16 pwm_value;
+ bool enabled;
+
+ u64 period;
+ unsigned int clk_sel;
+ unsigned int pre_div_sel;
+ unsigned int pre_div_exp;
+ unsigned int pwm_resolution_sel;
+
+ bool ramp_enabled;
+ bool ramp_ping_pong;
+ bool ramp_oneshot;
+ bool ramp_reverse;
+ unsigned short ramp_tick_ms;
+ unsigned long ramp_lo_pause_ms;
+ unsigned long ramp_hi_pause_ms;
+
+ unsigned int pattern_lo_idx;
+ unsigned int pattern_hi_idx;
+};
+
+/**
+ * struct lpg_led - logical LED object
+ * @lpg: lpg context reference
+ * @cdev: LED class device
+ * @mcdev: Multicolor LED class device
+ * @channels: list of channels associated with the LED
+ * @num_channels: number of @channels
+ */
+struct lpg_led {
+ struct lpg *lpg;
+
+ struct led_classdev cdev;
+ struct led_classdev_mc mcdev;
+
+ unsigned int num_channels;
+ struct lpg_channel *channels[] __counted_by(num_channels);
+};
+
+/**
+ * struct lpg - LPG device context
+ * @dev: pointer to LPG device
+ * @map: regmap for register access
+ * @lock: used to synchronize LED and pwm callback requests
+ * @pwm: PWM-chip object, if operating in PWM mode
+ * @data: reference to version specific data
+ * @lut_base: base address of the LUT block (optional)
+ * @lut_size: number of entries in the LUT block
+ * @lut_bitmap: allocation bitmap for LUT entries
+ * @pbs_dev: PBS device
+ * @lpg_chan_sdam: LPG SDAM peripheral device
+ * @lut_sdam: LUT SDAM peripheral device
+ * @pbs_en_bitmap: bitmap for tracking PBS triggers
+ * @triled_base: base address of the TRILED block (optional)
+ * @triled_src: power-source for the TRILED
+ * @triled_has_atc_ctl: true if there is TRI_LED_ATC_CTL register
+ * @triled_has_src_sel: true if there is TRI_LED_SRC_SEL register
+ * @num_channels: number of @channels
+ * @channels: list of PWM channels
+ */
+struct lpg {
+ struct device *dev;
+ struct regmap *map;
+
+ struct mutex lock;
+
+ struct pwm_chip *pwm;
+
+ const struct lpg_data *data;
+
+ u32 lut_base;
+ u32 lut_size;
+ unsigned long *lut_bitmap;
+
+ struct pbs_dev *pbs_dev;
+ struct nvmem_device *lpg_chan_sdam;
+ struct nvmem_device *lut_sdam;
+ unsigned long pbs_en_bitmap;
+
+ u32 triled_base;
+ u32 triled_src;
+ bool triled_has_atc_ctl;
+ bool triled_has_src_sel;
+
+ unsigned int num_channels;
+ struct lpg_channel channels[] __counted_by(num_channels);
+};
+
+/**
+ * struct lpg_channel_data - per channel initialization data
+ * @sdam_offset: Channel offset in LPG SDAM
+ * @base: base address for PWM channel registers
+ * @triled_mask: bitmask for controlling this channel in TRILED
+ */
+struct lpg_channel_data {
+ unsigned int sdam_offset;
+ unsigned int base;
+ u8 triled_mask;
+};
+
+/**
+ * struct lpg_data - initialization data
+ * @lut_base: base address of LUT block
+ * @lut_size: number of entries in LUT
+ * @triled_base: base address of TRILED
+ * @triled_has_atc_ctl: true if there is TRI_LED_ATC_CTL register
+ * @triled_has_src_sel: true if there is TRI_LED_SRC_SEL register
+ * @num_channels: number of channels in LPG
+ * @channels: list of channel initialization data
+ */
+struct lpg_data {
+ unsigned int lut_base;
+ unsigned int lut_size;
+ unsigned int triled_base;
+ bool triled_has_atc_ctl;
+ bool triled_has_src_sel;
+ int num_channels;
+ const struct lpg_channel_data *channels;
+};
+
+#define PBS_SW_TRIG_BIT BIT(0)
+
+static int lpg_clear_pbs_trigger(struct lpg *lpg, unsigned int lut_mask)
+{
+ u8 val = 0;
+ int rc;
+
+ if (!lpg->lpg_chan_sdam)
+ return 0;
+
+ lpg->pbs_en_bitmap &= (~lut_mask);
+ if (!lpg->pbs_en_bitmap) {
+ rc = nvmem_device_write(lpg->lpg_chan_sdam, SDAM_REG_PBS_SEQ_EN, 1, &val);
+ if (rc < 0)
+ return rc;
+
+ if (lpg->lut_sdam) {
+ val = PBS_SW_TRIG_BIT;
+ rc = nvmem_device_write(lpg->lpg_chan_sdam, SDAM_PBS_TRIG_CLR, 1, &val);
+ if (rc < 0)
+ return rc;
+ }
+ }
+
+ return 0;
+}
+
+static int lpg_set_pbs_trigger(struct lpg *lpg, unsigned int lut_mask)
+{
+ u8 val = PBS_SW_TRIG_BIT;
+ int rc;
+
+ if (!lpg->lpg_chan_sdam)
+ return 0;
+
+ if (!lpg->pbs_en_bitmap) {
+ rc = nvmem_device_write(lpg->lpg_chan_sdam, SDAM_REG_PBS_SEQ_EN, 1, &val);
+ if (rc < 0)
+ return rc;
+
+ if (lpg->lut_sdam) {
+ rc = nvmem_device_write(lpg->lpg_chan_sdam, SDAM_PBS_TRIG_SET, 1, &val);
+ if (rc < 0)
+ return rc;
+ } else {
+ rc = qcom_pbs_trigger_event(lpg->pbs_dev, val);
+ if (rc < 0)
+ return rc;
+ }
+ }
+ lpg->pbs_en_bitmap |= lut_mask;
+
+ return 0;
+}
+
+static int lpg_sdam_configure_triggers(struct lpg_channel *chan, u8 set_trig)
+{
+ u32 addr = SDAM_LUT_EN_OFFSET + chan->sdam_offset;
+
+ if (!chan->lpg->lpg_chan_sdam)
+ return 0;
+
+ return nvmem_device_write(chan->lpg->lpg_chan_sdam, addr, 1, &set_trig);
+}
+
+static int triled_set(struct lpg *lpg, unsigned int mask, unsigned int enable)
+{
+ /* Skip if we don't have a triled block */
+ if (!lpg->triled_base)
+ return 0;
+
+ return regmap_update_bits(lpg->map, lpg->triled_base + TRI_LED_EN_CTL,
+ mask, enable);
+}
+
+static int lpg_lut_store_sdam(struct lpg *lpg, struct led_pattern *pattern,
+ size_t len, unsigned int *lo_idx, unsigned int *hi_idx)
+{
+ unsigned int idx;
+ u8 brightness;
+ int i, rc;
+ u16 addr;
+
+ if (len > lpg->lut_size) {
+ dev_err(lpg->dev, "Pattern length (%zu) exceeds maximum pattern length (%d)\n",
+ len, lpg->lut_size);
+ return -EINVAL;
+ }
+
+ idx = bitmap_find_next_zero_area(lpg->lut_bitmap, lpg->lut_size, 0, len, 0);
+ if (idx >= lpg->lut_size)
+ return -ENOSPC;
+
+ for (i = 0; i < len; i++) {
+ brightness = pattern[i].brightness;
+
+ if (lpg->lut_sdam) {
+ addr = SDAM_LUT_SDAM_LUT_PATTERN_OFFSET + i + idx;
+ rc = nvmem_device_write(lpg->lut_sdam, addr, 1, &brightness);
+ } else {
+ addr = SDAM_LPG_SDAM_LUT_PATTERN_OFFSET + i + idx;
+ rc = nvmem_device_write(lpg->lpg_chan_sdam, addr, 1, &brightness);
+ }
+
+ if (rc < 0)
+ return rc;
+ }
+
+ bitmap_set(lpg->lut_bitmap, idx, len);
+
+ *lo_idx = idx;
+ *hi_idx = idx + len - 1;
+
+ return 0;
+}
+
+static int lpg_lut_store(struct lpg *lpg, struct led_pattern *pattern,
+ size_t len, unsigned int *lo_idx, unsigned int *hi_idx)
+{
+ unsigned int idx;
+ u16 val;
+ int i, ret;
+
+ idx = bitmap_find_next_zero_area(lpg->lut_bitmap, lpg->lut_size,
+ 0, len, 0);
+ if (idx >= lpg->lut_size)
+ return -ENOMEM;
+
+ for (i = 0; i < len; i++) {
+ val = pattern[i].brightness;
+
+ ret = regmap_bulk_write(lpg->map, lpg->lut_base + LPG_LUT_REG(idx + i),
+ &val, sizeof(val));
+ if (ret)
+ return ret;
+ }
+
+ bitmap_set(lpg->lut_bitmap, idx, len);
+
+ *lo_idx = idx;
+ *hi_idx = idx + len - 1;
+
+ return 0;
+}
+
+static void lpg_lut_free(struct lpg *lpg, unsigned int lo_idx, unsigned int hi_idx)
+{
+ int len;
+
+ len = hi_idx - lo_idx + 1;
+ if (len == 1)
+ return;
+
+ bitmap_clear(lpg->lut_bitmap, lo_idx, len);
+}
+
+static int lpg_lut_sync(struct lpg *lpg, unsigned int mask)
+{
+ if (!lpg->lut_base)
+ return 0;
+
+ return regmap_write(lpg->map, lpg->lut_base + RAMP_CONTROL_REG, mask);
+}
+
+static const unsigned int lpg_clk_rates[] = {0, 1024, 32768, 19200000};
+static const unsigned int lpg_clk_rates_hi_res[] = {0, 1024, 32768, 19200000, 76800000};
+static const unsigned int lpg_pre_divs[] = {1, 3, 5, 6};
+static const unsigned int lpg_pwm_resolution[] = {6, 9};
+static const unsigned int lpg_pwm_resolution_hi_res[] = {8, 9, 10, 11, 12, 13, 14, 15};
+
+static int lpg_calc_freq(struct lpg_channel *chan, uint64_t period)
+{
+ unsigned int i, pwm_resolution_count, best_pwm_resolution_sel = 0;
+ const unsigned int *clk_rate_arr, *pwm_resolution_arr;
+ unsigned int clk_sel, clk_len, best_clk = 0;
+ unsigned int div, best_div = 0;
+ unsigned int m, best_m = 0;
+ unsigned int resolution;
+ unsigned int error;
+ unsigned int best_err = UINT_MAX;
+ u64 max_period, min_period;
+ u64 best_period = 0;
+ u64 max_res;
+
+ /*
+ * The PWM period is determined by:
+ *
+ * resolution * pre_div * 2^M
+ * period = --------------------------
+ * refclk
+ *
+ * Resolution = 2^{6 or 9} bits for PWM or
+ * 2^{8, 9, 10, 11, 12, 13, 14, 15} bits for high resolution PWM
+ * pre_div = {1, 3, 5, 6} and
+ * M = [0..7].
+ *
+ * This allows for periods between 3uS and 384s for PWM channels and periods between
+ * 3uS and 24576s for high resolution PWMs.
+ * The PWM framework wants a period of equal or lower length than requested,
+ * reject anything below minimum period.
+ */
+
+ if (chan->subtype == LPG_SUBTYPE_HI_RES_PWM) {
+ clk_rate_arr = lpg_clk_rates_hi_res;
+ clk_len = ARRAY_SIZE(lpg_clk_rates_hi_res);
+ pwm_resolution_arr = lpg_pwm_resolution_hi_res;
+ pwm_resolution_count = ARRAY_SIZE(lpg_pwm_resolution_hi_res);
+ max_res = LPG_RESOLUTION_15BIT;
+ } else {
+ clk_rate_arr = lpg_clk_rates;
+ clk_len = ARRAY_SIZE(lpg_clk_rates);
+ pwm_resolution_arr = lpg_pwm_resolution;
+ pwm_resolution_count = ARRAY_SIZE(lpg_pwm_resolution);
+ max_res = LPG_RESOLUTION_9BIT;
+ }
+
+ min_period = div64_u64((u64)NSEC_PER_SEC * ((1 << pwm_resolution_arr[0]) - 1),
+ clk_rate_arr[clk_len - 1]);
+ if (period <= min_period)
+ return -EINVAL;
+
+ /* Limit period to largest possible value, to avoid overflows */
+ max_period = div64_u64((u64)NSEC_PER_SEC * max_res * LPG_MAX_PREDIV * (1 << LPG_MAX_M),
+ 1024);
+ if (period > max_period)
+ period = max_period;
+
+ /*
+ * Search for the pre_div, refclk, resolution and M by solving the rewritten formula
+ * for each refclk, resolution and pre_div value:
+ *
+ * period * refclk
+ * M = log2 -------------------------------------
+ * NSEC_PER_SEC * pre_div * resolution
+ */
+
+ for (i = 0; i < pwm_resolution_count; i++) {
+ resolution = (1 << pwm_resolution_arr[i]) - 1;
+ for (clk_sel = 1; clk_sel < clk_len; clk_sel++) {
+ u64 numerator = period * clk_rate_arr[clk_sel];
+
+ for (div = 0; div < ARRAY_SIZE(lpg_pre_divs); div++) {
+ u64 denominator = (u64)NSEC_PER_SEC * lpg_pre_divs[div] *
+ resolution;
+ u64 actual;
+ u64 ratio;
+
+ if (numerator < denominator)
+ continue;
+
+ ratio = div64_u64(numerator, denominator);
+ m = ilog2(ratio);
+ if (m > LPG_MAX_M)
+ m = LPG_MAX_M;
+
+ actual = DIV_ROUND_UP_ULL(denominator * (1 << m),
+ clk_rate_arr[clk_sel]);
+ error = period - actual;
+ if (error < best_err) {
+ best_err = error;
+ best_div = div;
+ best_m = m;
+ best_clk = clk_sel;
+ best_period = actual;
+ best_pwm_resolution_sel = i;
+ }
+ }
+ }
+ }
+ chan->clk_sel = best_clk;
+ chan->pre_div_sel = best_div;
+ chan->pre_div_exp = best_m;
+ chan->period = best_period;
+ chan->pwm_resolution_sel = best_pwm_resolution_sel;
+ return 0;
+}
+
+static void lpg_calc_duty(struct lpg_channel *chan, uint64_t duty)
+{
+ unsigned int max;
+ unsigned int val;
+ unsigned int clk_rate;
+
+ if (chan->subtype == LPG_SUBTYPE_HI_RES_PWM) {
+ max = BIT(lpg_pwm_resolution_hi_res[chan->pwm_resolution_sel]) - 1;
+ clk_rate = lpg_clk_rates_hi_res[chan->clk_sel];
+ } else {
+ max = BIT(lpg_pwm_resolution[chan->pwm_resolution_sel]) - 1;
+ clk_rate = lpg_clk_rates[chan->clk_sel];
+ }
+
+ val = div64_u64(duty * clk_rate,
+ (u64)NSEC_PER_SEC * lpg_pre_divs[chan->pre_div_sel] * (1 << chan->pre_div_exp));
+
+ chan->pwm_value = min(val, max);
+}
+
+static void lpg_apply_freq(struct lpg_channel *chan)
+{
+ unsigned long val;
+ struct lpg *lpg = chan->lpg;
+
+ if (!chan->enabled)
+ return;
+
+ val = chan->clk_sel;
+
+ /* Specify resolution, based on the subtype of the channel */
+ switch (chan->subtype) {
+ case LPG_SUBTYPE_LPG:
+ val |= GENMASK(5, 4);
+ break;
+ case LPG_SUBTYPE_PWM:
+ val |= FIELD_PREP(PWM_SIZE_SELECT_MASK, chan->pwm_resolution_sel);
+ break;
+ case LPG_SUBTYPE_HI_RES_PWM:
+ val |= FIELD_PREP(PWM_SIZE_HI_RES_MASK, chan->pwm_resolution_sel);
+ break;
+ case LPG_SUBTYPE_LPG_LITE:
+ default:
+ val |= BIT(4);
+ break;
+ }
+
+ regmap_write(lpg->map, chan->base + LPG_SIZE_CLK_REG, val);
+
+ val = FIELD_PREP(PWM_FREQ_PRE_DIV_MASK, chan->pre_div_sel) |
+ FIELD_PREP(PWM_FREQ_EXP_MASK, chan->pre_div_exp);
+ regmap_write(lpg->map, chan->base + LPG_PREDIV_CLK_REG, val);
+}
+
+#define LPG_ENABLE_GLITCH_REMOVAL BIT(5)
+
+static void lpg_enable_glitch(struct lpg_channel *chan)
+{
+ struct lpg *lpg = chan->lpg;
+
+ regmap_update_bits(lpg->map, chan->base + PWM_TYPE_CONFIG_REG,
+ LPG_ENABLE_GLITCH_REMOVAL, 0);
+}
+
+static void lpg_disable_glitch(struct lpg_channel *chan)
+{
+ struct lpg *lpg = chan->lpg;
+
+ regmap_update_bits(lpg->map, chan->base + PWM_TYPE_CONFIG_REG,
+ LPG_ENABLE_GLITCH_REMOVAL,
+ LPG_ENABLE_GLITCH_REMOVAL);
+}
+
+static void lpg_apply_pwm_value(struct lpg_channel *chan)
+{
+ struct lpg *lpg = chan->lpg;
+ u16 val = chan->pwm_value;
+
+ if (!chan->enabled)
+ return;
+
+ regmap_bulk_write(lpg->map, chan->base + PWM_VALUE_REG, &val, sizeof(val));
+}
+
+#define LPG_PATTERN_CONFIG_LO_TO_HI BIT(4)
+#define LPG_PATTERN_CONFIG_REPEAT BIT(3)
+#define LPG_PATTERN_CONFIG_TOGGLE BIT(2)
+#define LPG_PATTERN_CONFIG_PAUSE_HI BIT(1)
+#define LPG_PATTERN_CONFIG_PAUSE_LO BIT(0)
+
+static void lpg_sdam_apply_lut_control(struct lpg_channel *chan)
+{
+ struct nvmem_device *lpg_chan_sdam = chan->lpg->lpg_chan_sdam;
+ unsigned int lo_idx = chan->pattern_lo_idx;
+ unsigned int hi_idx = chan->pattern_hi_idx;
+ u8 val = 0, conf = 0, lut_offset = 0;
+ unsigned int hi_pause, lo_pause;
+ struct lpg *lpg = chan->lpg;
+
+ if (!chan->ramp_enabled || chan->pattern_lo_idx == chan->pattern_hi_idx)
+ return;
+
+ hi_pause = DIV_ROUND_UP(chan->ramp_hi_pause_ms, chan->ramp_tick_ms);
+ lo_pause = DIV_ROUND_UP(chan->ramp_lo_pause_ms, chan->ramp_tick_ms);
+
+ if (!chan->ramp_oneshot)
+ conf |= LPG_PATTERN_CONFIG_REPEAT;
+ if (chan->ramp_hi_pause_ms && lpg->lut_sdam)
+ conf |= LPG_PATTERN_CONFIG_PAUSE_HI;
+ if (chan->ramp_lo_pause_ms && lpg->lut_sdam)
+ conf |= LPG_PATTERN_CONFIG_PAUSE_LO;
+
+ if (lpg->lut_sdam) {
+ lut_offset = SDAM_LUT_SDAM_LUT_PATTERN_OFFSET - SDAM_START_BASE;
+ hi_idx += lut_offset;
+ lo_idx += lut_offset;
+ }
+
+ nvmem_device_write(lpg_chan_sdam, SDAM_PBS_SCRATCH_LUT_COUNTER_OFFSET + chan->sdam_offset, 1, &val);
+ nvmem_device_write(lpg_chan_sdam, SDAM_PATTERN_CONFIG_OFFSET + chan->sdam_offset, 1, &conf);
+ nvmem_device_write(lpg_chan_sdam, SDAM_END_INDEX_OFFSET + chan->sdam_offset, 1, &hi_idx);
+ nvmem_device_write(lpg_chan_sdam, SDAM_START_INDEX_OFFSET + chan->sdam_offset, 1, &lo_idx);
+
+ val = RAMP_STEP_DURATION(chan->ramp_tick_ms);
+ nvmem_device_write(lpg_chan_sdam, SDAM_REG_RAMP_STEP_DURATION, 1, &val);
+
+ if (lpg->lut_sdam) {
+ nvmem_device_write(lpg_chan_sdam, SDAM_PAUSE_HI_MULTIPLIER_OFFSET + chan->sdam_offset, 1, &hi_pause);
+ nvmem_device_write(lpg_chan_sdam, SDAM_PAUSE_LO_MULTIPLIER_OFFSET + chan->sdam_offset, 1, &lo_pause);
+ }
+
+}
+
+static void lpg_apply_lut_control(struct lpg_channel *chan)
+{
+ struct lpg *lpg = chan->lpg;
+ unsigned int hi_pause;
+ unsigned int lo_pause;
+ unsigned int conf = 0;
+ unsigned int lo_idx = chan->pattern_lo_idx;
+ unsigned int hi_idx = chan->pattern_hi_idx;
+ u16 step = chan->ramp_tick_ms;
+
+ if (!chan->ramp_enabled || chan->pattern_lo_idx == chan->pattern_hi_idx)
+ return;
+
+ hi_pause = DIV_ROUND_UP(chan->ramp_hi_pause_ms, step);
+ lo_pause = DIV_ROUND_UP(chan->ramp_lo_pause_ms, step);
+
+ if (!chan->ramp_reverse)
+ conf |= LPG_PATTERN_CONFIG_LO_TO_HI;
+ if (!chan->ramp_oneshot)
+ conf |= LPG_PATTERN_CONFIG_REPEAT;
+ if (chan->ramp_ping_pong)
+ conf |= LPG_PATTERN_CONFIG_TOGGLE;
+ if (chan->ramp_hi_pause_ms)
+ conf |= LPG_PATTERN_CONFIG_PAUSE_HI;
+ if (chan->ramp_lo_pause_ms)
+ conf |= LPG_PATTERN_CONFIG_PAUSE_LO;
+
+ regmap_write(lpg->map, chan->base + LPG_PATTERN_CONFIG_REG, conf);
+ regmap_write(lpg->map, chan->base + LPG_HI_IDX_REG, hi_idx);
+ regmap_write(lpg->map, chan->base + LPG_LO_IDX_REG, lo_idx);
+
+ regmap_bulk_write(lpg->map, chan->base + LPG_RAMP_DURATION_REG, &step, sizeof(step));
+ regmap_write(lpg->map, chan->base + LPG_HI_PAUSE_REG, hi_pause);
+ regmap_write(lpg->map, chan->base + LPG_LO_PAUSE_REG, lo_pause);
+}
+
+#define LPG_ENABLE_CONTROL_OUTPUT BIT(7)
+#define LPG_ENABLE_CONTROL_BUFFER_TRISTATE BIT(5)
+#define LPG_ENABLE_CONTROL_SRC_PWM BIT(2)
+#define LPG_ENABLE_CONTROL_RAMP_GEN BIT(1)
+
+static void lpg_apply_control(struct lpg_channel *chan)
+{
+ unsigned int ctrl;
+ struct lpg *lpg = chan->lpg;
+
+ ctrl = LPG_ENABLE_CONTROL_BUFFER_TRISTATE;
+
+ if (chan->enabled)
+ ctrl |= LPG_ENABLE_CONTROL_OUTPUT;
+
+ if (chan->pattern_lo_idx != chan->pattern_hi_idx)
+ ctrl |= LPG_ENABLE_CONTROL_RAMP_GEN;
+ else
+ ctrl |= LPG_ENABLE_CONTROL_SRC_PWM;
+
+ regmap_write(lpg->map, chan->base + PWM_ENABLE_CONTROL_REG, ctrl);
+
+ /*
+ * Due to LPG hardware bug, in the PWM mode, having enabled PWM,
+ * We have to write PWM values one more time.
+ */
+ if (chan->enabled)
+ lpg_apply_pwm_value(chan);
+}
+
+#define LPG_SYNC_PWM BIT(0)
+
+static void lpg_apply_sync(struct lpg_channel *chan)
+{
+ struct lpg *lpg = chan->lpg;
+
+ regmap_write(lpg->map, chan->base + PWM_SYNC_REG, LPG_SYNC_PWM);
+}
+
+static int lpg_parse_dtest(struct lpg *lpg)
+{
+ struct lpg_channel *chan;
+ struct device_node *np = lpg->dev->of_node;
+ int count;
+ int ret;
+ int i;
+
+ count = of_property_count_u32_elems(np, "qcom,dtest");
+ if (count == -EINVAL) {
+ return 0;
+ } else if (count < 0) {
+ ret = count;
+ goto err_malformed;
+ } else if (count != lpg->data->num_channels * 2) {
+ return dev_err_probe(lpg->dev, -EINVAL,
+ "qcom,dtest needs to be %d items\n",
+ lpg->data->num_channels * 2);
+ }
+
+ for (i = 0; i < lpg->data->num_channels; i++) {
+ chan = &lpg->channels[i];
+
+ ret = of_property_read_u32_index(np, "qcom,dtest", i * 2,
+ &chan->dtest_line);
+ if (ret)
+ goto err_malformed;
+
+ ret = of_property_read_u32_index(np, "qcom,dtest", i * 2 + 1,
+ &chan->dtest_value);
+ if (ret)
+ goto err_malformed;
+ }
+
+ return 0;
+
+err_malformed:
+ return dev_err_probe(lpg->dev, ret, "malformed qcom,dtest\n");
+}
+
+static void lpg_apply_dtest(struct lpg_channel *chan)
+{
+ struct lpg *lpg = chan->lpg;
+
+ if (!chan->dtest_line)
+ return;
+
+ regmap_write(lpg->map, chan->base + PWM_SEC_ACCESS_REG, 0xa5);
+ regmap_write(lpg->map, chan->base + PWM_DTEST_REG(chan->dtest_line),
+ chan->dtest_value);
+}
+
+static void lpg_apply(struct lpg_channel *chan)
+{
+ lpg_disable_glitch(chan);
+ lpg_apply_freq(chan);
+ lpg_apply_pwm_value(chan);
+ lpg_apply_control(chan);
+ lpg_apply_sync(chan);
+ if (chan->lpg->lpg_chan_sdam)
+ lpg_sdam_apply_lut_control(chan);
+ else
+ lpg_apply_lut_control(chan);
+ lpg_enable_glitch(chan);
+}
+
+static void lpg_brightness_set(struct lpg_led *led, struct led_classdev *cdev,
+ struct mc_subled *subleds)
+{
+ enum led_brightness brightness;
+ struct lpg_channel *chan;
+ unsigned int triled_enabled = 0;
+ unsigned int triled_mask = 0;
+ unsigned int lut_mask = 0;
+ unsigned int duty;
+ struct lpg *lpg = led->lpg;
+ int i;
+
+ for (i = 0; i < led->num_channels; i++) {
+ chan = led->channels[i];
+ brightness = subleds[i].brightness;
+
+ if (brightness == LED_OFF) {
+ chan->enabled = false;
+ chan->ramp_enabled = false;
+ } else if (chan->pattern_lo_idx != chan->pattern_hi_idx) {
+ lpg_calc_freq(chan, NSEC_PER_MSEC);
+ lpg_sdam_configure_triggers(chan, 1);
+
+ chan->enabled = true;
+ chan->ramp_enabled = true;
+
+ lut_mask |= chan->lut_mask;
+ triled_enabled |= chan->triled_mask;
+ } else {
+ lpg_calc_freq(chan, NSEC_PER_MSEC);
+
+ duty = div_u64(brightness * chan->period, cdev->max_brightness);
+ lpg_calc_duty(chan, duty);
+ chan->enabled = true;
+ chan->ramp_enabled = false;
+
+ triled_enabled |= chan->triled_mask;
+ }
+
+ triled_mask |= chan->triled_mask;
+
+ lpg_apply(chan);
+ }
+
+ /* Toggle triled lines */
+ if (triled_mask)
+ triled_set(lpg, triled_mask, triled_enabled);
+
+ /* Trigger start of ramp generator(s) */
+ if (lut_mask) {
+ lpg_lut_sync(lpg, lut_mask);
+ lpg_set_pbs_trigger(lpg, lut_mask);
+ }
+}
+
+static int lpg_brightness_single_set(struct led_classdev *cdev,
+ enum led_brightness value)
+{
+ struct lpg_led *led = container_of(cdev, struct lpg_led, cdev);
+ struct mc_subled info;
+
+ mutex_lock(&led->lpg->lock);
+
+ info.brightness = value;
+ lpg_brightness_set(led, cdev, &info);
+
+ mutex_unlock(&led->lpg->lock);
+
+ return 0;
+}
+
+static int lpg_brightness_mc_set(struct led_classdev *cdev,
+ enum led_brightness value)
+{
+ struct led_classdev_mc *mc = lcdev_to_mccdev(cdev);
+ struct lpg_led *led = container_of(mc, struct lpg_led, mcdev);
+
+ mutex_lock(&led->lpg->lock);
+
+ led_mc_calc_color_components(mc, value);
+ lpg_brightness_set(led, cdev, mc->subled_info);
+
+ mutex_unlock(&led->lpg->lock);
+
+ return 0;
+}
+
+static int lpg_blink_set(struct lpg_led *led,
+ unsigned long *delay_on, unsigned long *delay_off)
+{
+ struct lpg_channel *chan;
+ unsigned int period;
+ unsigned int triled_mask = 0;
+ struct lpg *lpg = led->lpg;
+ u64 duty;
+ int i;
+
+ if (!*delay_on && !*delay_off) {
+ *delay_on = 500;
+ *delay_off = 500;
+ }
+
+ duty = *delay_on * NSEC_PER_MSEC;
+ period = (*delay_on + *delay_off) * NSEC_PER_MSEC;
+
+ for (i = 0; i < led->num_channels; i++) {
+ chan = led->channels[i];
+
+ lpg_calc_freq(chan, period);
+ lpg_calc_duty(chan, duty);
+
+ chan->enabled = true;
+ chan->ramp_enabled = false;
+
+ triled_mask |= chan->triled_mask;
+
+ lpg_apply(chan);
+ }
+
+ /* Enable triled lines */
+ triled_set(lpg, triled_mask, triled_mask);
+
+ chan = led->channels[0];
+ duty = div_u64(chan->pwm_value * chan->period, LPG_RESOLUTION_9BIT);
+ *delay_on = div_u64(duty, NSEC_PER_MSEC);
+ *delay_off = div_u64(chan->period - duty, NSEC_PER_MSEC);
+
+ return 0;
+}
+
+static int lpg_blink_single_set(struct led_classdev *cdev,
+ unsigned long *delay_on, unsigned long *delay_off)
+{
+ struct lpg_led *led = container_of(cdev, struct lpg_led, cdev);
+ int ret;
+
+ mutex_lock(&led->lpg->lock);
+
+ ret = lpg_blink_set(led, delay_on, delay_off);
+
+ mutex_unlock(&led->lpg->lock);
+
+ return ret;
+}
+
+static int lpg_blink_mc_set(struct led_classdev *cdev,
+ unsigned long *delay_on, unsigned long *delay_off)
+{
+ struct led_classdev_mc *mc = lcdev_to_mccdev(cdev);
+ struct lpg_led *led = container_of(mc, struct lpg_led, mcdev);
+ int ret;
+
+ mutex_lock(&led->lpg->lock);
+
+ ret = lpg_blink_set(led, delay_on, delay_off);
+
+ mutex_unlock(&led->lpg->lock);
+
+ return ret;
+}
+
+static int lpg_pattern_set(struct lpg_led *led, struct led_pattern *led_pattern,
+ u32 len, int repeat)
+{
+ struct lpg_channel *chan;
+ struct lpg *lpg = led->lpg;
+ struct led_pattern *pattern;
+ unsigned int brightness_a;
+ unsigned int brightness_b;
+ unsigned int hi_pause = 0;
+ unsigned int lo_pause = 0;
+ unsigned int actual_len;
+ unsigned int delta_t;
+ unsigned int lo_idx;
+ unsigned int hi_idx;
+ unsigned int i;
+ bool ping_pong = true;
+ int ret = -EINVAL;
+
+ /* Hardware only support oneshot or indefinite loops */
+ if (repeat != -1 && repeat != 1)
+ return -EINVAL;
+
+ /*
+ * The standardized leds-trigger-pattern format defines that the
+ * brightness of the LED follows a linear transition from one entry
+ * in the pattern to the next, over the given delta_t time. It
+ * describes that the way to perform instant transitions a zero-length
+ * entry should be added following a pattern entry.
+ *
+ * The LPG hardware is only able to perform the latter (no linear
+ * transitions), so require each entry in the pattern to be followed by
+ * a zero-length transition.
+ */
+ if (len % 2)
+ return -EINVAL;
+
+ pattern = kzalloc_objs(*pattern, len / 2);
+ if (!pattern)
+ return -ENOMEM;
+
+ for (i = 0; i < len; i += 2) {
+ if (led_pattern[i].brightness != led_pattern[i + 1].brightness)
+ goto out_free_pattern;
+ if (led_pattern[i + 1].delta_t != 0)
+ goto out_free_pattern;
+
+ pattern[i / 2].brightness = led_pattern[i].brightness;
+ pattern[i / 2].delta_t = led_pattern[i].delta_t;
+ }
+
+ len /= 2;
+
+ /*
+ * Specifying a pattern of length 1 causes the hardware to iterate
+ * through the entire LUT, so prohibit this.
+ */
+ if (len < 2)
+ goto out_free_pattern;
+
+ /*
+ * The LPG plays patterns with at a fixed pace, a "low pause" can be
+ * used to stretch the first delay of the pattern and a "high pause"
+ * the last one.
+ *
+ * In order to save space the pattern can be played in "ping pong"
+ * mode, in which the pattern is first played forward, then "high
+ * pause" is applied, then the pattern is played backwards and finally
+ * the "low pause" is applied.
+ *
+ * The middle elements of the pattern are used to determine delta_t and
+ * the "low pause" and "high pause" multipliers are derrived from this.
+ *
+ * The first element in the pattern is used to determine "low pause".
+ *
+ * If the specified pattern is a palindrome the ping pong mode is
+ * enabled. In this scenario the delta_t of the middle entry (i.e. the
+ * last in the programmed pattern) determines the "high pause".
+ *
+ * SDAM-based devices do not support "ping pong", and only supports
+ * "low pause" and "high pause" with a dedicated SDAM LUT.
+ */
+
+ /* Detect palindromes and use "ping pong" to reduce LUT usage */
+ if (lpg->lut_base) {
+ for (i = 0; i < len / 2; i++) {
+ brightness_a = pattern[i].brightness;
+ brightness_b = pattern[len - i - 1].brightness;
+
+ if (brightness_a != brightness_b) {
+ ping_pong = false;
+ break;
+ }
+ }
+ } else
+ ping_pong = false;
+
+ /* The pattern length to be written to the LUT */
+ if (ping_pong)
+ actual_len = (len + 1) / 2;
+ else
+ actual_len = len;
+
+ /*
+ * Validate that all delta_t in the pattern are the same, with the
+ * exception of the middle element in case of ping_pong.
+ */
+ delta_t = pattern[1].delta_t;
+ for (i = 2; i < len; i++) {
+ if (pattern[i].delta_t != delta_t) {
+ /*
+ * Allow last entry in the full or shortened pattern to
+ * specify hi pause. Reject other variations.
+ */
+ if (i != actual_len - 1)
+ goto out_free_pattern;
+ }
+ }
+
+ /* LPG_RAMP_DURATION_REG is a 9bit */
+ if (delta_t >= BIT(9))
+ goto out_free_pattern;
+
+ /*
+ * Find "low pause" and "high pause" in the pattern in the LUT case.
+ * SDAM-based devices without dedicated LUT SDAM require equal
+ * duration of all steps.
+ */
+ if (lpg->lut_base || lpg->lut_sdam) {
+ lo_pause = pattern[0].delta_t;
+ hi_pause = pattern[actual_len - 1].delta_t;
+ } else {
+ if (delta_t != pattern[0].delta_t || delta_t != pattern[actual_len - 1].delta_t)
+ goto out_free_pattern;
+ }
+
+
+ mutex_lock(&lpg->lock);
+
+ if (lpg->lut_base)
+ ret = lpg_lut_store(lpg, pattern, actual_len, &lo_idx, &hi_idx);
+ else
+ ret = lpg_lut_store_sdam(lpg, pattern, actual_len, &lo_idx, &hi_idx);
+
+ if (ret < 0)
+ goto out_unlock;
+
+ for (i = 0; i < led->num_channels; i++) {
+ chan = led->channels[i];
+
+ chan->ramp_tick_ms = delta_t;
+ chan->ramp_ping_pong = ping_pong;
+ chan->ramp_oneshot = repeat != -1;
+
+ chan->ramp_lo_pause_ms = lo_pause;
+ chan->ramp_hi_pause_ms = hi_pause;
+
+ chan->pattern_lo_idx = lo_idx;
+ chan->pattern_hi_idx = hi_idx;
+ }
+
+out_unlock:
+ mutex_unlock(&lpg->lock);
+out_free_pattern:
+ kfree(pattern);
+
+ return ret;
+}
+
+static int lpg_pattern_single_set(struct led_classdev *cdev,
+ struct led_pattern *pattern, u32 len,
+ int repeat)
+{
+ struct lpg_led *led = container_of(cdev, struct lpg_led, cdev);
+ int ret;
+
+ ret = lpg_pattern_set(led, pattern, len, repeat);
+ if (ret < 0)
+ return ret;
+
+ lpg_brightness_single_set(cdev, LED_FULL);
+
+ return 0;
+}
+
+static int lpg_pattern_mc_set(struct led_classdev *cdev,
+ struct led_pattern *pattern, u32 len,
+ int repeat)
+{
+ struct led_classdev_mc *mc = lcdev_to_mccdev(cdev);
+ struct lpg_led *led = container_of(mc, struct lpg_led, mcdev);
+ unsigned int triled_mask = 0;
+ int ret, i;
+
+ for (i = 0; i < led->num_channels; i++)
+ triled_mask |= led->channels[i]->triled_mask;
+ triled_set(led->lpg, triled_mask, 0);
+
+ ret = lpg_pattern_set(led, pattern, len, repeat);
+ if (ret < 0)
+ return ret;
+
+ led_mc_calc_color_components(mc, LED_FULL);
+ lpg_brightness_set(led, cdev, mc->subled_info);
+
+ return 0;
+}
+
+static int lpg_pattern_clear(struct lpg_led *led)
+{
+ struct lpg_channel *chan;
+ struct lpg *lpg = led->lpg;
+ int i;
+
+ mutex_lock(&lpg->lock);
+
+ chan = led->channels[0];
+ lpg_lut_free(lpg, chan->pattern_lo_idx, chan->pattern_hi_idx);
+
+ for (i = 0; i < led->num_channels; i++) {
+ chan = led->channels[i];
+ lpg_sdam_configure_triggers(chan, 0);
+ lpg_clear_pbs_trigger(chan->lpg, chan->lut_mask);
+ chan->pattern_lo_idx = 0;
+ chan->pattern_hi_idx = 0;
+ }
+
+ mutex_unlock(&lpg->lock);
+
+ return 0;
+}
+
+static int lpg_pattern_single_clear(struct led_classdev *cdev)
+{
+ struct lpg_led *led = container_of(cdev, struct lpg_led, cdev);
+
+ return lpg_pattern_clear(led);
+}
+
+static int lpg_pattern_mc_clear(struct led_classdev *cdev)
+{
+ struct led_classdev_mc *mc = lcdev_to_mccdev(cdev);
+ struct lpg_led *led = container_of(mc, struct lpg_led, mcdev);
+
+ return lpg_pattern_clear(led);
+}
+
+static inline struct lpg *lpg_pwm_from_chip(struct pwm_chip *chip)
+{
+ return pwmchip_get_drvdata(chip);
+}
+
+static int lpg_pwm_request(struct pwm_chip *chip, struct pwm_device *pwm)
+{
+ struct lpg *lpg = lpg_pwm_from_chip(chip);
+ struct lpg_channel *chan = &lpg->channels[pwm->hwpwm];
+
+ return chan->in_use ? -EBUSY : 0;
+}
+
+/*
+ * Limitations:
+ * - Updating both duty and period is not done atomically, so the output signal
+ * will momentarily be a mix of the settings.
+ * - Changed parameters takes effect immediately.
+ * - A disabled channel outputs a logical 0.
+ */
+static int lpg_pwm_apply(struct pwm_chip *chip, struct pwm_device *pwm,
+ const struct pwm_state *state)
+{
+ struct lpg *lpg = lpg_pwm_from_chip(chip);
+ struct lpg_channel *chan = &lpg->channels[pwm->hwpwm];
+ int ret = 0;
+
+ if (state->polarity != PWM_POLARITY_NORMAL)
+ return -EINVAL;
+
+ mutex_lock(&lpg->lock);
+
+ if (state->enabled) {
+ ret = lpg_calc_freq(chan, state->period);
+ if (ret < 0)
+ goto out_unlock;
+
+ lpg_calc_duty(chan, state->duty_cycle);
+ }
+ chan->enabled = state->enabled;
+
+ lpg_apply(chan);
+
+out_unlock:
+ mutex_unlock(&lpg->lock);
+
+ return ret;
+}
+
+static int lpg_pwm_get_state(struct pwm_chip *chip, struct pwm_device *pwm,
+ struct pwm_state *state)
+{
+ struct lpg *lpg = lpg_pwm_from_chip(chip);
+ struct lpg_channel *chan = &lpg->channels[pwm->hwpwm];
+ unsigned int resolution;
+ unsigned int pre_div;
+ unsigned int refclk;
+ unsigned int val;
+ unsigned int m;
+ u16 pwm_value;
+ int ret;
+
+ ret = regmap_read(lpg->map, chan->base + LPG_SIZE_CLK_REG, &val);
+ if (ret)
+ return ret;
+
+ if (chan->subtype == LPG_SUBTYPE_HI_RES_PWM) {
+ unsigned int clk_idx = FIELD_GET(PWM_CLK_SELECT_HI_RES_MASK, val);
+
+ if (clk_idx >= ARRAY_SIZE(lpg_clk_rates_hi_res))
+ return -EINVAL;
+
+ refclk = lpg_clk_rates_hi_res[clk_idx];
+ resolution = lpg_pwm_resolution_hi_res[FIELD_GET(PWM_SIZE_HI_RES_MASK, val)];
+ } else {
+ refclk = lpg_clk_rates[FIELD_GET(PWM_CLK_SELECT_MASK, val)];
+ resolution = lpg_pwm_resolution[FIELD_GET(PWM_SIZE_SELECT_MASK, val)];
+ }
+
+ if (refclk) {
+ ret = regmap_read(lpg->map, chan->base + LPG_PREDIV_CLK_REG, &val);
+ if (ret)
+ return ret;
+
+ pre_div = lpg_pre_divs[FIELD_GET(PWM_FREQ_PRE_DIV_MASK, val)];
+ m = FIELD_GET(PWM_FREQ_EXP_MASK, val);
+
+ ret = regmap_bulk_read(lpg->map, chan->base + PWM_VALUE_REG, &pwm_value, sizeof(pwm_value));
+ if (ret)
+ return ret;
+
+ state->period = DIV_ROUND_UP_ULL((u64)NSEC_PER_SEC * ((1 << resolution) - 1) *
+ pre_div * (1 << m), refclk);
+ state->duty_cycle = DIV_ROUND_UP_ULL((u64)NSEC_PER_SEC * pwm_value * pre_div * (1 << m), refclk);
+ } else {
+ state->period = 0;
+ state->duty_cycle = 0;
+ }
+
+ ret = regmap_read(lpg->map, chan->base + PWM_ENABLE_CONTROL_REG, &val);
+ if (ret)
+ return ret;
+
+ state->enabled = FIELD_GET(LPG_ENABLE_CONTROL_OUTPUT, val);
+ state->polarity = PWM_POLARITY_NORMAL;
+
+ if (state->duty_cycle > state->period)
+ state->duty_cycle = state->period;
+
+ return 0;
+}
+
+static const struct pwm_ops lpg_pwm_ops = {
+ .request = lpg_pwm_request,
+ .apply = lpg_pwm_apply,
+ .get_state = lpg_pwm_get_state,
+};
+
+static int lpg_add_pwm(struct lpg *lpg)
+{
+ struct pwm_chip *chip;
+ int ret;
+
+ lpg->pwm = chip = devm_pwmchip_alloc(lpg->dev, lpg->num_channels, 0);
+ if (IS_ERR(chip))
+ return PTR_ERR(chip);
+
+ chip->ops = &lpg_pwm_ops;
+ pwmchip_set_drvdata(chip, lpg);
+
+ ret = devm_pwmchip_add(lpg->dev, chip);
+ if (ret)
+ dev_err_probe(lpg->dev, ret, "failed to add PWM chip\n");
+
+ return ret;
+}
+
+static int lpg_parse_channel(struct lpg *lpg, struct device_node *np,
+ struct lpg_channel **channel)
+{
+ struct lpg_channel *chan;
+ u32 color = LED_COLOR_ID_GREEN;
+ u32 reg;
+ int ret;
+
+ ret = of_property_read_u32(np, "reg", &reg);
+ if (ret || !reg || reg > lpg->num_channels)
+ return dev_err_probe(lpg->dev, -EINVAL, "invalid \"reg\" of %pOFn\n", np);
+
+ chan = &lpg->channels[reg - 1];
+ chan->in_use = true;
+
+ ret = of_property_read_u32(np, "color", &color);
+ if (ret < 0 && ret != -EINVAL)
+ return dev_err_probe(lpg->dev, ret,
+ "failed to parse \"color\" of %pOF\n", np);
+
+ chan->color = color;
+
+ *channel = chan;
+
+ return 0;
+}
+
+static int lpg_add_led(struct lpg *lpg, struct device_node *np)
+{
+ struct led_init_data init_data = {};
+ struct led_classdev *cdev;
+ struct mc_subled *info;
+ struct lpg_led *led;
+ const char *state;
+ int num_channels;
+ u32 color = 0;
+ int ret;
+ int i;
+
+ ret = of_property_read_u32(np, "color", &color);
+ if (ret < 0 && ret != -EINVAL)
+ return dev_err_probe(lpg->dev, ret,
+ "failed to parse \"color\" of %pOF\n", np);
+
+ if (color == LED_COLOR_ID_RGB || color == LED_COLOR_ID_MULTI)
+ num_channels = of_get_available_child_count(np);
+ else
+ num_channels = 1;
+
+ led = devm_kzalloc(lpg->dev, struct_size(led, channels, num_channels), GFP_KERNEL);
+ if (!led)
+ return -ENOMEM;
+
+ led->lpg = lpg;
+ led->num_channels = num_channels;
+
+ if (color == LED_COLOR_ID_RGB || color == LED_COLOR_ID_MULTI) {
+ info = devm_kcalloc(lpg->dev, num_channels, sizeof(*info), GFP_KERNEL);
+ if (!info)
+ return -ENOMEM;
+ i = 0;
+ for_each_available_child_of_node_scoped(np, child) {
+ ret = lpg_parse_channel(lpg, child, &led->channels[i]);
+ if (ret < 0)
+ return ret;
+
+ info[i].color_index = led->channels[i]->color;
+ info[i].intensity = 0;
+ i++;
+ }
+
+ led->mcdev.subled_info = info;
+ led->mcdev.num_colors = num_channels;
+
+ cdev = &led->mcdev.led_cdev;
+ cdev->brightness_set_blocking = lpg_brightness_mc_set;
+ cdev->blink_set = lpg_blink_mc_set;
+
+ /* Register pattern accessors if we have a LUT block or when using PPG */
+ if (lpg->lut_base || lpg->lpg_chan_sdam) {
+ cdev->pattern_set = lpg_pattern_mc_set;
+ cdev->pattern_clear = lpg_pattern_mc_clear;
+ }
+ } else {
+ ret = lpg_parse_channel(lpg, np, &led->channels[0]);
+ if (ret < 0)
+ return ret;
+
+ cdev = &led->cdev;
+ cdev->brightness_set_blocking = lpg_brightness_single_set;
+ cdev->blink_set = lpg_blink_single_set;
+
+ /* Register pattern accessors if we have a LUT block or when using PPG */
+ if (lpg->lut_base || lpg->lpg_chan_sdam) {
+ cdev->pattern_set = lpg_pattern_single_set;
+ cdev->pattern_clear = lpg_pattern_single_clear;
+ }
+ }
+
+ cdev->default_trigger = of_get_property(np, "linux,default-trigger", NULL);
+
+ if (lpg->lpg_chan_sdam)
+ cdev->max_brightness = PPG_MAX_LED_BRIGHTNESS;
+ else
+ cdev->max_brightness = LPG_RESOLUTION_9BIT - 1;
+
+ if (!of_property_read_string(np, "default-state", &state) &&
+ !strcmp(state, "on"))
+ cdev->brightness = cdev->max_brightness;
+ else
+ cdev->brightness = LED_OFF;
+
+ cdev->brightness_set_blocking(cdev, cdev->brightness);
+
+ init_data.fwnode = of_fwnode_handle(np);
+
+ if (color == LED_COLOR_ID_RGB || color == LED_COLOR_ID_MULTI)
+ ret = devm_led_classdev_multicolor_register_ext(lpg->dev, &led->mcdev, &init_data);
+ else
+ ret = devm_led_classdev_register_ext(lpg->dev, &led->cdev, &init_data);
+ if (ret)
+ dev_err_probe(lpg->dev, ret, "unable to register %s\n", cdev->name);
+
+ return ret;
+}
+
+static int lpg_init_channels(struct lpg *lpg)
+{
+ const struct lpg_data *data = lpg->data;
+ struct lpg_channel *chan;
+ int i;
+
+ for (i = 0; i < data->num_channels; i++) {
+ chan = &lpg->channels[i];
+
+ chan->lpg = lpg;
+ chan->base = data->channels[i].base;
+ chan->triled_mask = data->channels[i].triled_mask;
+ chan->lut_mask = BIT(i);
+ chan->sdam_offset = data->channels[i].sdam_offset;
+
+ regmap_read(lpg->map, chan->base + LPG_SUBTYPE_REG, &chan->subtype);
+ }
+
+ return 0;
+}
+
+static int lpg_init_triled(struct lpg *lpg)
+{
+ struct device_node *np = lpg->dev->of_node;
+ int ret;
+
+ /* Skip initialization if we don't have a triled block */
+ if (!lpg->data->triled_base)
+ return 0;
+
+ lpg->triled_base = lpg->data->triled_base;
+ lpg->triled_has_atc_ctl = lpg->data->triled_has_atc_ctl;
+ lpg->triled_has_src_sel = lpg->data->triled_has_src_sel;
+
+ if (lpg->triled_has_src_sel) {
+ ret = of_property_read_u32(np, "qcom,power-source", &lpg->triled_src);
+ if (ret || lpg->triled_src == 2 || lpg->triled_src > 3)
+ return dev_err_probe(lpg->dev, -EINVAL,
+ "invalid power source\n");
+ }
+
+ /* Disable automatic trickle charge LED */
+ if (lpg->triled_has_atc_ctl)
+ regmap_write(lpg->map, lpg->triled_base + TRI_LED_ATC_CTL, 0);
+
+ /* Configure power source */
+ if (lpg->triled_has_src_sel)
+ regmap_write(lpg->map, lpg->triled_base + TRI_LED_SRC_SEL, lpg->triled_src);
+
+ /* Default all outputs to off */
+ regmap_write(lpg->map, lpg->triled_base + TRI_LED_EN_CTL, 0);
+
+ return 0;
+}
+
+static int lpg_init_lut(struct lpg *lpg)
+{
+ const struct lpg_data *data = lpg->data;
+
+ if (!data->lut_size)
+ return 0;
+
+ lpg->lut_size = data->lut_size;
+ if (data->lut_base)
+ lpg->lut_base = data->lut_base;
+
+ lpg->lut_bitmap = devm_bitmap_zalloc(lpg->dev, lpg->lut_size, GFP_KERNEL);
+ if (!lpg->lut_bitmap)
+ return -ENOMEM;
+
+ return 0;
+}
+
+static int lpg_init_sdam(struct lpg *lpg)
+{
+ int i, sdam_count, rc;
+ u8 val = 0;
+
+ sdam_count = of_property_count_strings(lpg->dev->of_node, "nvmem-names");
+ if (sdam_count <= 0)
+ return 0;
+ if (sdam_count > SDAM_MAX_DEVICES)
+ return -EINVAL;
+
+ /* Get the 1st SDAM device for LPG/LUT config */
+ lpg->lpg_chan_sdam = devm_nvmem_device_get(lpg->dev, "lpg_chan_sdam");
+ if (IS_ERR(lpg->lpg_chan_sdam))
+ return dev_err_probe(lpg->dev, PTR_ERR(lpg->lpg_chan_sdam),
+ "Failed to get LPG chan SDAM device\n");
+
+ if (sdam_count == 1) {
+ /* Get PBS device node if single SDAM device */
+ lpg->pbs_dev = get_pbs_client_device(lpg->dev);
+ if (IS_ERR(lpg->pbs_dev))
+ return dev_err_probe(lpg->dev, PTR_ERR(lpg->pbs_dev),
+ "Failed to get PBS client device\n");
+ } else if (sdam_count == 2) {
+ /* Get the 2nd SDAM device for LUT pattern */
+ lpg->lut_sdam = devm_nvmem_device_get(lpg->dev, "lut_sdam");
+ if (IS_ERR(lpg->lut_sdam))
+ return dev_err_probe(lpg->dev, PTR_ERR(lpg->lut_sdam),
+ "Failed to get LPG LUT SDAM device\n");
+ }
+
+ for (i = 0; i < lpg->num_channels; i++) {
+ struct lpg_channel *chan = &lpg->channels[i];
+
+ if (chan->sdam_offset) {
+ rc = nvmem_device_write(lpg->lpg_chan_sdam,
+ SDAM_PBS_SCRATCH_LUT_COUNTER_OFFSET + chan->sdam_offset, 1, &val);
+ if (rc < 0)
+ return rc;
+
+ rc = lpg_sdam_configure_triggers(chan, 0);
+ if (rc < 0)
+ return rc;
+
+ rc = lpg_clear_pbs_trigger(chan->lpg, chan->lut_mask);
+ if (rc < 0)
+ return rc;
+ }
+ }
+
+ return 0;
+}
+
+static int lpg_probe(struct platform_device *pdev)
+{
+ const struct lpg_data *data;
+ struct lpg *lpg;
+ int ret;
+ int i;
+
+ data = of_device_get_match_data(&pdev->dev);
+ if (!data)
+ return -EINVAL;
+
+ lpg = devm_kzalloc(&pdev->dev, struct_size(lpg, channels, data->num_channels), GFP_KERNEL);
+ if (!lpg)
+ return -ENOMEM;
+
+ lpg->num_channels = data->num_channels;
+ lpg->data = data;
+ lpg->dev = &pdev->dev;
+ mutex_init(&lpg->lock);
+
+ lpg->map = dev_get_regmap(pdev->dev.parent, NULL);
+ if (!lpg->map)
+ return dev_err_probe(&pdev->dev, -ENXIO, "parent regmap unavailable\n");
+
+ ret = lpg_init_channels(lpg);
+ if (ret < 0)
+ return ret;
+
+ ret = lpg_parse_dtest(lpg);
+ if (ret < 0)
+ return ret;
+
+ ret = lpg_init_triled(lpg);
+ if (ret < 0)
+ return ret;
+
+ ret = lpg_init_sdam(lpg);
+ if (ret < 0)
+ return ret;
+
+ ret = lpg_init_lut(lpg);
+ if (ret < 0)
+ return ret;
+
+ for_each_available_child_of_node_scoped(pdev->dev.of_node, np) {
+ ret = lpg_add_led(lpg, np);
+ if (ret)
+ return ret;
+ }
+
+ for (i = 0; i < lpg->num_channels; i++)
+ lpg_apply_dtest(&lpg->channels[i]);
+
+ return lpg_add_pwm(lpg);
+}
+
+static const struct lpg_data pm660l_lpg_data = {
+ .lut_base = 0xb000,
+ .lut_size = 49,
+
+ .triled_base = 0xd000,
+ .triled_has_atc_ctl = true,
+ .triled_has_src_sel = true,
+
+ .num_channels = 4,
+ .channels = (const struct lpg_channel_data[]) {
+ { .base = 0xb100, .triled_mask = BIT(5) },
+ { .base = 0xb200, .triled_mask = BIT(6) },
+ { .base = 0xb300, .triled_mask = BIT(7) },
+ { .base = 0xb400 },
+ },
+};
+
+static const struct lpg_data pm8916_pwm_data = {
+ .num_channels = 1,
+ .channels = (const struct lpg_channel_data[]) {
+ { .base = 0xbc00 },
+ },
+};
+
+static const struct lpg_data pm8941_lpg_data = {
+ .lut_base = 0xb000,
+ .lut_size = 64,
+
+ .triled_base = 0xd000,
+ .triled_has_atc_ctl = true,
+ .triled_has_src_sel = true,
+
+ .num_channels = 8,
+ .channels = (const struct lpg_channel_data[]) {
+ { .base = 0xb100 },
+ { .base = 0xb200 },
+ { .base = 0xb300 },
+ { .base = 0xb400 },
+ { .base = 0xb500, .triled_mask = BIT(5) },
+ { .base = 0xb600, .triled_mask = BIT(6) },
+ { .base = 0xb700, .triled_mask = BIT(7) },
+ { .base = 0xb800 },
+ },
+};
+
+static const struct lpg_data pmi8950_pwm_data = {
+ .num_channels = 1,
+ .channels = (const struct lpg_channel_data[]) {
+ { .base = 0xb000 },
+ },
+};
+
+static const struct lpg_data pm8994_lpg_data = {
+ .lut_base = 0xb000,
+ .lut_size = 64,
+
+ .num_channels = 6,
+ .channels = (const struct lpg_channel_data[]) {
+ { .base = 0xb100 },
+ { .base = 0xb200 },
+ { .base = 0xb300 },
+ { .base = 0xb400 },
+ { .base = 0xb500 },
+ { .base = 0xb600 },
+ },
+};
+
+/* PMI632 uses SDAM instead of LUT for pattern */
+static const struct lpg_data pmi632_lpg_data = {
+ .triled_base = 0xd000,
+
+ .lut_size = 64,
+
+ .num_channels = 5,
+ .channels = (const struct lpg_channel_data[]) {
+ { .base = 0xb300, .triled_mask = BIT(7), .sdam_offset = 0x48 },
+ { .base = 0xb400, .triled_mask = BIT(6), .sdam_offset = 0x56 },
+ { .base = 0xb500, .triled_mask = BIT(5), .sdam_offset = 0x64 },
+ { .base = 0xb600 },
+ { .base = 0xb700 },
+ },
+};
+
+static const struct lpg_data pmi8994_lpg_data = {
+ .lut_base = 0xb000,
+ .lut_size = 24,
+
+ .triled_base = 0xd000,
+ .triled_has_atc_ctl = true,
+ .triled_has_src_sel = true,
+
+ .num_channels = 4,
+ .channels = (const struct lpg_channel_data[]) {
+ { .base = 0xb100, .triled_mask = BIT(5) },
+ { .base = 0xb200, .triled_mask = BIT(6) },
+ { .base = 0xb300, .triled_mask = BIT(7) },
+ { .base = 0xb400 },
+ },
+};
+
+static const struct lpg_data pmi8998_lpg_data = {
+ .lut_base = 0xb000,
+ .lut_size = 49,
+
+ .triled_base = 0xd000,
+
+ .num_channels = 6,
+ .channels = (const struct lpg_channel_data[]) {
+ { .base = 0xb100 },
+ { .base = 0xb200 },
+ { .base = 0xb300, .triled_mask = BIT(5) },
+ { .base = 0xb400, .triled_mask = BIT(6) },
+ { .base = 0xb500, .triled_mask = BIT(7) },
+ { .base = 0xb600 },
+ },
+};
+
+static const struct lpg_data pm8150b_lpg_data = {
+ .lut_base = 0xb000,
+ .lut_size = 24,
+
+ .triled_base = 0xd000,
+
+ .num_channels = 2,
+ .channels = (const struct lpg_channel_data[]) {
+ { .base = 0xb100, .triled_mask = BIT(7) },
+ { .base = 0xb200, .triled_mask = BIT(6) },
+ },
+};
+
+static const struct lpg_data pm8150l_lpg_data = {
+ .lut_base = 0xb000,
+ .lut_size = 48,
+
+ .triled_base = 0xd000,
+
+ .num_channels = 5,
+ .channels = (const struct lpg_channel_data[]) {
+ { .base = 0xb100, .triled_mask = BIT(7) },
+ { .base = 0xb200, .triled_mask = BIT(6) },
+ { .base = 0xb300, .triled_mask = BIT(5) },
+ { .base = 0xbc00 },
+ { .base = 0xbd00 },
+
+ },
+};
+
+static const struct lpg_data pm8350c_pwm_data = {
+ .triled_base = 0xef00,
+
+ .lut_size = 122,
+
+ .num_channels = 4,
+ .channels = (const struct lpg_channel_data[]) {
+ { .base = 0xe800, .triled_mask = BIT(7), .sdam_offset = 0x48 },
+ { .base = 0xe900, .triled_mask = BIT(6), .sdam_offset = 0x56 },
+ { .base = 0xea00, .triled_mask = BIT(5), .sdam_offset = 0x64 },
+ { .base = 0xeb00 },
+ },
+};
+
+static const struct lpg_data pmk8550_pwm_data = {
+ .num_channels = 2,
+ .channels = (const struct lpg_channel_data[]) {
+ { .base = 0xe800 },
+ { .base = 0xe900 },
+ },
+};
+
+static const struct of_device_id lpg_of_table[] = {
+ { .compatible = "qcom,pm660l-lpg", .data = &pm660l_lpg_data },
+ { .compatible = "qcom,pm8150b-lpg", .data = &pm8150b_lpg_data },
+ { .compatible = "qcom,pm8150l-lpg", .data = &pm8150l_lpg_data },
+ { .compatible = "qcom,pm8350c-pwm", .data = &pm8350c_pwm_data },
+ { .compatible = "qcom,pm8916-pwm", .data = &pm8916_pwm_data },
+ { .compatible = "qcom,pm8941-lpg", .data = &pm8941_lpg_data },
+ { .compatible = "qcom,pm8994-lpg", .data = &pm8994_lpg_data },
+ { .compatible = "qcom,pmi632-lpg", .data = &pmi632_lpg_data },
+ { .compatible = "qcom,pmi8950-pwm", .data = &pmi8950_pwm_data },
+ { .compatible = "qcom,pmi8994-lpg", .data = &pmi8994_lpg_data },
+ { .compatible = "qcom,pmi8998-lpg", .data = &pmi8998_lpg_data },
+ { .compatible = "qcom,pmc8180c-lpg", .data = &pm8150l_lpg_data },
+ { .compatible = "qcom,pmk8550-pwm", .data = &pmk8550_pwm_data },
+ {}
+};
+MODULE_DEVICE_TABLE(of, lpg_of_table);
+
+static struct platform_driver lpg_driver = {
+ .probe = lpg_probe,
+ .driver = {
+ .name = "qcom-spmi-lpg",
+ .of_match_table = lpg_of_table,
+ },
+};
+module_platform_driver(lpg_driver);
+
+MODULE_DESCRIPTION("Qualcomm LPG LED driver");
+MODULE_LICENSE("GPL v2");
diff --git a/drivers/leds/rgb/leds-s2m-rgb.c b/drivers/leds/rgb/leds-s2m-rgb.c
new file mode 100644
index 000000000..d239f54ee
--- /dev/null
+++ b/drivers/leds/rgb/leds-s2m-rgb.c
@@ -0,0 +1,426 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * RGB LED Driver for Samsung S2M series PMICs.
+ *
+ * Copyright (c) 2015 Samsung Electronics Co., Ltd
+ * Copyright (c) 2026 Kaustabh Chakraborty <kauschluss@disroot.org>
+ */
+
+#include <linux/container_of.h>
+#include <linux/led-class-multicolor.h>
+#include <linux/mfd/samsung/core.h>
+#include <linux/mfd/samsung/s2mu005.h>
+#include <linux/minmax.h>
+#include <linux/module.h>
+#include <linux/mutex.h>
+#include <linux/of.h>
+#include <linux/platform_device.h>
+#include <linux/regmap.h>
+
+struct s2m_rgb {
+ struct device *dev;
+ struct regmap *regmap;
+ struct led_classdev_mc mc;
+ /*
+ * The mutex object prevents race conditions when evaluation and
+ * application of LED pattern state.
+ */
+ struct mutex lock;
+ /*
+ * State variables representing the current LED pattern, these only to
+ * be accessed when lock is held.
+ */
+ u8 ramp_up;
+ u8 ramp_dn;
+ u8 stay_hi;
+ u8 stay_lo;
+};
+
+static struct led_classdev_mc *to_s2m_mc(struct led_classdev *cdev)
+{
+ return container_of(cdev, struct led_classdev_mc, led_cdev);
+}
+
+static struct s2m_rgb *to_s2m_rgb(struct led_classdev_mc *mc)
+{
+ return container_of(mc, struct s2m_rgb, mc);
+}
+
+static const u32 s2mu005_rgb_lut_ramp[] = {
+ 0, 100, 200, 300, 400, 500, 600, 700,
+ 800, 1000, 1200, 1400, 1600, 1800, 2000, 2200,
+};
+
+static const u32 s2mu005_rgb_lut_stay_hi[] = {
+ 100, 200, 300, 400, 500, 750, 1000, 1250,
+ 1500, 1750, 2000, 2250, 2500, 2750, 3000, 3250,
+};
+
+static const u32 s2mu005_rgb_lut_stay_lo[] = {
+ 0, 500, 1000, 1500, 2000, 2500, 3000, 3500,
+ 4000, 4500, 5000, 6000, 7000, 8000, 10000, 12000,
+};
+
+static int s2mu005_rgb_apply_params(struct s2m_rgb *rgb)
+{
+ struct regmap *regmap = rgb->regmap;
+ unsigned int ramp_val = 0;
+ unsigned int stay_val = 0;
+ int ret;
+
+ ramp_val |= FIELD_PREP(S2MU005_RGB_CH_RAMP_UP, rgb->ramp_up);
+ ramp_val |= FIELD_PREP(S2MU005_RGB_CH_RAMP_DN, rgb->ramp_dn);
+
+ stay_val |= FIELD_PREP(S2MU005_RGB_CH_STAY_HI, rgb->stay_hi);
+ stay_val |= FIELD_PREP(S2MU005_RGB_CH_STAY_LO, rgb->stay_lo);
+
+ ret = regmap_write(regmap, S2MU005_REG_RGB_EN, S2MU005_RGB_RESET);
+ if (ret) {
+ dev_err(rgb->dev, "failed to reset RGB LEDs\n");
+ return ret;
+ }
+
+ for (int i = 0; i < rgb->mc.num_colors; i++) {
+ ret = regmap_write(regmap, S2MU005_REG_RGB_CH_CTRL(i),
+ rgb->mc.subled_info[i].brightness);
+ if (ret) {
+ dev_err(rgb->dev, "failed to set LED brightness\n");
+ return ret;
+ }
+
+ ret = regmap_write(regmap, S2MU005_REG_RGB_CH_RAMP(i), ramp_val);
+ if (ret) {
+ dev_err(rgb->dev, "failed to set ramp timings\n");
+ return ret;
+ }
+
+ ret = regmap_write(regmap, S2MU005_REG_RGB_CH_STAY(i), stay_val);
+ if (ret) {
+ dev_err(rgb->dev, "failed to set stay timings\n");
+ return ret;
+ }
+ }
+
+ ret = regmap_write(regmap, S2MU005_REG_RGB_EN, S2MU005_RGB_SLOPE_SMOOTH);
+ if (ret) {
+ dev_err(rgb->dev, "failed to set ramp slope\n");
+ return ret;
+ }
+
+ return 0;
+}
+
+static int s2mu005_rgb_reset_params(struct s2m_rgb *rgb)
+{
+ struct regmap *regmap = rgb->regmap;
+ int ret;
+
+ ret = regmap_write(regmap, S2MU005_REG_RGB_EN, S2MU005_RGB_RESET);
+ if (ret) {
+ dev_err(rgb->dev, "failed to reset RGB LEDs\n");
+ return ret;
+ }
+
+ rgb->ramp_up = 0;
+ rgb->ramp_dn = 0;
+ rgb->stay_hi = 0;
+ rgb->stay_lo = 0;
+
+ return 0;
+}
+
+/*
+ * s2m_rgb_lut_get_closest_duration - find closest duration in look-up table
+ * @lut: the look-up table to search for the closest timing
+ * @len: number of elements in the look-up table array
+ * @duration: the timing duration requested
+ *
+ * This function does a binary search on the given array, and finds the closest
+ * value to the requested timing. It is expected that the look-up table to be
+ * provided, is already sorted.
+ *
+ * This function returns a negative error code, or a non-negative index of the
+ * value in the look-up table closest to the one requested.
+ */
+static int s2m_rgb_lut_get_closest_duration(const u32 *lut, const size_t len, const u32 duration)
+{
+ u32 closest_distance = abs(duration - lut[0]);
+ int closest_index = 0;
+ int lo = 0;
+ int hi = len - 1;
+
+ /*
+ * Allow a small amount of extrapolation beyond the highest timing value.
+ *
+ * Consider x and y to be the two last values in the table, and x < y.
+ * Since (y - x) / 2 integers, in the range [x + (y - x) / 2, y)
+ * returns y as the closest, allow extrapolation for the succeeding
+ * (y - x) / 2 integers as well, viz, up to (y, y + (y - x) / 2].
+ * Anything beyond that is invalid.
+ */
+ if (len >= 2 && duration > lut[len - 1] + (lut[len - 1] - lut[len - 2]) / 2)
+ return -EINVAL;
+
+ while (lo <= hi) {
+ int mid = lo + (hi - lo) / 2;
+
+ /* Narrow down search window as per binary-search algorithm. */
+ if (duration < lut[mid])
+ hi = mid - 1;
+ else
+ lo = mid + 1;
+
+ if (abs(duration - lut[mid]) < closest_distance) {
+ closest_distance = abs(duration - lut[mid]);
+ closest_index = mid;
+ }
+ }
+
+ return closest_index;
+}
+
+static int s2m_rgb_pattern_set(struct led_classdev *cdev, struct led_pattern *pattern, u32 len,
+ int repeat)
+{
+ struct s2m_rgb *rgb = to_s2m_rgb(to_s2m_mc(cdev));
+ const u32 *lut_ramp_up, *lut_ramp_dn, *lut_stay_hi, *lut_stay_lo;
+ size_t lut_ramp_up_len, lut_ramp_dn_len, lut_stay_hi_len, lut_stay_lo_len;
+ int brightness_peak = 0;
+ u32 time_hi = 0, time_lo = 0;
+ bool ramp_up_en = false, ramp_dn_en = false;
+ int ret;
+
+ /*
+ * The typical pattern supported by this device can be represented with
+ * the following graph:
+ *
+ * 255 T ''''''-. .-'''''''-.
+ * | '. .' '.
+ * | \ / \
+ * | '. .' '.
+ * | '-...........-' '-
+ * 0 +----------------------------------------------------> time (s)
+ *
+ * <---- HIGH ----><-- LOW --><-------- HIGH --------->
+ * <-----><-------><---------><-------><-----><------->
+ * stay_hi ramp_dn stay_lo ramp_up stay_hi ramp_dn
+ *
+ * There are two states, named HIGH and LOW. HIGH has a non-zero
+ * brightness level, while LOW is of zero brightness. The pattern
+ * provided should mention only one zero and non-zero brightness level.
+ * The hardware always starts the pattern from the HIGH state, as shown
+ * in the graph.
+ *
+ * The HIGH state can be divided in three somewhat equal timings:
+ * ramp_up, stay_hi, and ramp_dn. The LOW state has only one timing:
+ * stay_lo.
+ */
+
+ /* Only indefinitely looping patterns are supported. */
+ if (repeat != -1)
+ return -EINVAL;
+
+ /* Pattern should consist of at least two tuples. */
+ if (len < 2)
+ return -EINVAL;
+
+ for (int i = 0; i < len; i++) {
+ int brightness = pattern[i].brightness;
+ u32 delta_t = pattern[i].delta_t;
+
+ if (brightness) {
+ /*
+ * The pattern should define only one non-zero
+ * brightness in the HIGH state. The device doesn't
+ * have any provisions to handle multiple peak
+ * brightness levels.
+ */
+ if (brightness_peak && brightness_peak != brightness)
+ return -EINVAL;
+
+ brightness_peak = brightness;
+ time_hi += delta_t;
+ ramp_dn_en = !!delta_t;
+ } else {
+ time_lo += delta_t;
+ ramp_up_en = !!delta_t;
+ }
+ }
+
+ /* LUTs are specific to device variant. */
+ lut_ramp_up = s2mu005_rgb_lut_ramp;
+ lut_ramp_up_len = ARRAY_SIZE(s2mu005_rgb_lut_ramp);
+ lut_ramp_dn = s2mu005_rgb_lut_ramp;
+ lut_ramp_dn_len = ARRAY_SIZE(s2mu005_rgb_lut_ramp);
+ lut_stay_hi = s2mu005_rgb_lut_stay_hi;
+ lut_stay_hi_len = ARRAY_SIZE(s2mu005_rgb_lut_stay_hi);
+ lut_stay_lo = s2mu005_rgb_lut_stay_lo;
+ lut_stay_lo_len = ARRAY_SIZE(s2mu005_rgb_lut_stay_lo);
+
+ mutex_lock(&rgb->lock);
+
+ /*
+ * The timings ramp_up, stay_hi, and ramp_dn of the HIGH state are
+ * roughly equal. Firstly, calculate and set timings for ramp_up and
+ * ramp_dn (making sure they're exactly equal).
+ */
+ rgb->ramp_up = 0;
+ rgb->ramp_dn = 0;
+
+ if (ramp_up_en) {
+ ret = s2m_rgb_lut_get_closest_duration(lut_ramp_up, lut_ramp_up_len, time_hi / 3);
+ if (ret < 0)
+ goto param_fail;
+ rgb->ramp_up = (u8)ret;
+ }
+
+ if (ramp_dn_en) {
+ ret = s2m_rgb_lut_get_closest_duration(lut_ramp_dn, lut_ramp_dn_len, time_hi / 3);
+ if (ret < 0)
+ goto param_fail;
+ rgb->ramp_dn = (u8)ret;
+ }
+
+ /*
+ * Subtract the allocated ramp timings from time_hi (and also making
+ * sure it doesn't underflow!). The remaining time is allocated to
+ * stay_hi.
+ */
+ time_hi -= min(time_hi, lut_ramp_up[rgb->ramp_up]);
+ time_hi -= min(time_hi, lut_ramp_dn[rgb->ramp_dn]);
+
+ ret = s2m_rgb_lut_get_closest_duration(lut_stay_hi, lut_stay_hi_len, time_hi);
+ if (ret < 0)
+ goto param_fail;
+ rgb->stay_hi = (u8)ret;
+
+ ret = s2m_rgb_lut_get_closest_duration(lut_stay_lo, lut_stay_lo_len, time_lo);
+ if (ret < 0)
+ goto param_fail;
+ rgb->stay_lo = (u8)ret;
+
+ led_mc_calc_color_components(&rgb->mc, brightness_peak);
+ /* Apply params with variant-specific implementation. */
+ ret = s2mu005_rgb_apply_params(rgb);
+ if (ret)
+ goto param_fail;
+
+ mutex_unlock(&rgb->lock);
+
+ return 0;
+
+param_fail:
+ rgb->ramp_up = 0;
+ rgb->ramp_dn = 0;
+ rgb->stay_hi = 0;
+ rgb->stay_lo = 0;
+
+ mutex_unlock(&rgb->lock);
+
+ return ret;
+}
+
+static int s2m_rgb_pattern_clear(struct led_classdev *cdev)
+{
+ struct s2m_rgb *rgb = to_s2m_rgb(to_s2m_mc(cdev));
+ int ret = 0;
+
+ mutex_lock(&rgb->lock);
+
+ /* Reset params with variant-specific implementation. */
+ ret = s2mu005_rgb_reset_params(rgb);
+
+ mutex_unlock(&rgb->lock);
+
+ return ret;
+}
+
+static int s2m_rgb_brightness_set(struct led_classdev *cdev, enum led_brightness value)
+{
+ struct s2m_rgb *rgb = to_s2m_rgb(to_s2m_mc(cdev));
+ int ret = 0;
+
+ if (!value)
+ return s2m_rgb_pattern_clear(cdev);
+
+ mutex_lock(&rgb->lock);
+
+ led_mc_calc_color_components(&rgb->mc, value);
+ /* Apply params with variant-specific implementation. */
+ ret = s2mu005_rgb_apply_params(rgb);
+
+ mutex_unlock(&rgb->lock);
+
+ return ret;
+}
+
+static const struct mc_subled s2mu005_rgb_subled_info[] = {
+ { .channel = 0, .color_index = LED_COLOR_ID_BLUE },
+ { .channel = 1, .color_index = LED_COLOR_ID_GREEN },
+ { .channel = 2, .color_index = LED_COLOR_ID_RED },
+};
+
+static int s2m_rgb_probe(struct platform_device *pdev)
+{
+ struct device *dev = &pdev->dev;
+ struct sec_pmic_dev *pmic_drvdata = dev_get_drvdata(dev->parent);
+ struct s2m_rgb *rgb;
+ struct led_init_data init_data = {};
+ int ret;
+
+ rgb = devm_kzalloc(dev, sizeof(*rgb), GFP_KERNEL);
+ if (!rgb)
+ return -ENOMEM;
+
+ platform_set_drvdata(pdev, rgb);
+ rgb->dev = dev;
+ rgb->regmap = pmic_drvdata->regmap_pmic;
+
+ /* Configure variant-specific details. */
+ rgb->mc.num_colors = ARRAY_SIZE(s2mu005_rgb_subled_info);
+ rgb->mc.subled_info = devm_kmemdup(dev, s2mu005_rgb_subled_info,
+ sizeof(s2mu005_rgb_subled_info), GFP_KERNEL);
+ if (!rgb->mc.subled_info)
+ return -ENOMEM;
+
+ rgb->mc.led_cdev.max_brightness = 255;
+ rgb->mc.led_cdev.brightness_set_blocking = s2m_rgb_brightness_set;
+ rgb->mc.led_cdev.pattern_set = s2m_rgb_pattern_set;
+ rgb->mc.led_cdev.pattern_clear = s2m_rgb_pattern_clear;
+
+ ret = devm_mutex_init(dev, &rgb->lock);
+ if (ret)
+ return dev_err_probe(dev, ret, "failed to create mutex lock\n");
+
+ init_data.fwnode = of_fwnode_handle(dev->of_node);
+ ret = devm_led_classdev_multicolor_register_ext(dev, &rgb->mc, &init_data);
+ if (ret)
+ return dev_err_probe(dev, ret, "failed to create LED device\n");
+
+ return 0;
+}
+
+static const struct platform_device_id s2m_rgb_id_table[] = {
+ { "s2mu005-rgb", S2MU005 },
+ { /* sentinel */ },
+};
+MODULE_DEVICE_TABLE(platform, s2m_rgb_id_table);
+
+static const struct of_device_id s2m_rgb_of_match_table[] = {
+ { .compatible = "samsung,s2mu005-rgb", .data = (void *)S2MU005 },
+ { /* sentinel */ },
+};
+MODULE_DEVICE_TABLE(of, s2m_rgb_of_match_table);
+
+static struct platform_driver s2m_rgb_driver = {
+ .driver = {
+ .name = "s2m-rgb",
+ },
+ .probe = s2m_rgb_probe,
+ .id_table = s2m_rgb_id_table,
+};
+module_platform_driver(s2m_rgb_driver);
+
+MODULE_DESCRIPTION("RGB LED Driver for Samsung S2M Series PMICs");
+MODULE_AUTHOR("Kaustabh Chakraborty <kauschluss@disroot.org>");
+MODULE_LICENSE("GPL");