diff options
Diffstat (limited to 'drivers/leds/rgb')
| -rw-r--r-- | drivers/leds/rgb/Kconfig | 127 | ||||
| -rw-r--r-- | drivers/leds/rgb/Makefile | 12 | ||||
| -rw-r--r-- | drivers/leds/rgb/leds-group-multicolor.c | 172 | ||||
| -rw-r--r-- | drivers/leds/rgb/leds-ktd202x.c | 635 | ||||
| -rw-r--r-- | drivers/leds/rgb/leds-lp5812.c | 642 | ||||
| -rw-r--r-- | drivers/leds/rgb/leds-lp5812.h | 172 | ||||
| -rw-r--r-- | drivers/leds/rgb/leds-lp5860-core.c | 233 | ||||
| -rw-r--r-- | drivers/leds/rgb/leds-lp5860-spi.c | 98 | ||||
| -rw-r--r-- | drivers/leds/rgb/leds-lp5860.h | 268 | ||||
| -rw-r--r-- | drivers/leds/rgb/leds-mt6370-rgb.c | 994 | ||||
| -rw-r--r-- | drivers/leds/rgb/leds-ncp5623.c | 270 | ||||
| -rw-r--r-- | drivers/leds/rgb/leds-pwm-multicolor.c | 199 | ||||
| -rw-r--r-- | drivers/leds/rgb/leds-qcom-lpg.c | 1850 | ||||
| -rw-r--r-- | drivers/leds/rgb/leds-s2m-rgb.c | 426 |
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", ®); + 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", ®); + 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, ®_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, ®_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, ®_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", ®); + 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, ®_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, ®_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", ®); + 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", ®); + 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", ®); + 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", ®); + 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"); |
