Resguardo inicial

This commit is contained in:
lastdragon
2026-09-06 12:53:21 -06:00
parent 662efe3c2a
commit ad2088d178
94853 changed files with 43200404 additions and 0 deletions
+127
View File
@@ -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
+12
View File
@@ -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
@@ -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");
+635
View File
@@ -0,0 +1,635 @@
// SPDX-License-Identifier: GPL-2.0-or-later
/*
* Kinetic KTD2026/7 RGB/White LED driver with I2C interface
*
* Copyright 2023 André Apitzsch <git@apitzsch.eu>
*
* Datasheet: https://www.kinet-ic.com/uploads/KTD2026-7-04h.pdf
*/
#include <linux/i2c.h>
#include <linux/led-class-multicolor.h>
#include <linux/module.h>
#include <linux/mutex.h>
#include <linux/of.h>
#include <linux/of_device.h>
#include <linux/regmap.h>
#include <linux/regulator/consumer.h>
#define KTD2026_NUM_LEDS 3
#define KTD2027_NUM_LEDS 4
#define KTD202X_MAX_LEDS 4
/* Register bank */
#define KTD202X_REG_RESET_CONTROL 0x00
#define KTD202X_REG_FLASH_PERIOD 0x01
#define KTD202X_REG_PWM1_TIMER 0x02
#define KTD202X_REG_PWM2_TIMER 0x03
#define KTD202X_REG_CHANNEL_CTRL 0x04
#define KTD202X_REG_TRISE_FALL 0x05
#define KTD202X_REG_LED_IOUT(x) (0x06 + (x))
/* Register 0 */
#define KTD202X_TIMER_SLOT_CONTROL_TSLOT1 0x00
#define KTD202X_TIMER_SLOT_CONTROL_TSLOT2 0x01
#define KTD202X_TIMER_SLOT_CONTROL_TSLOT3 0x02
#define KTD202X_TIMER_SLOT_CONTROL_TSLOT4 0x03
#define KTD202X_RSTR_RESET 0x07
#define KTD202X_ENABLE_CTRL_WAKE 0x00 /* SCL High & SDA High */
#define KTD202X_ENABLE_CTRL_SLEEP 0x08 /* SCL High & SDA Toggling */
#define KTD202X_TRISE_FALL_SCALE_NORMAL 0x00
#define KTD202X_TRISE_FALL_SCALE_SLOW_X2 0x20
#define KTD202X_TRISE_FALL_SCALE_SLOW_X4 0x40
#define KTD202X_TRISE_FALL_SCALE_FAST_X8 0x60
/* Register 1 */
#define KTD202X_FLASH_PERIOD_256_MS_LOG_RAMP 0x00
/* Register 2-3 */
#define KTD202X_FLASH_ON_TIME_0_4_PERCENT 0x01
/* Register 4 */
#define KTD202X_CHANNEL_CTRL_MASK(x) (BIT(2 * (x)) | BIT(2 * (x) + 1))
#define KTD202X_CHANNEL_CTRL_OFF 0x00
#define KTD202X_CHANNEL_CTRL_ON(x) BIT(2 * (x))
#define KTD202X_CHANNEL_CTRL_PWM1(x) BIT(2 * (x) + 1)
#define KTD202X_CHANNEL_CTRL_PWM2(x) (BIT(2 * (x)) | BIT(2 * (x) + 1))
/* Register 5 */
#define KTD202X_RAMP_TIMES_2_MS 0x00
/* Register 6-9 */
#define KTD202X_LED_CURRENT_10_mA 0x4f
#define KTD202X_FLASH_PERIOD_MIN_MS 256
#define KTD202X_FLASH_PERIOD_STEP_MS 128
#define KTD202X_FLASH_PERIOD_MAX_STEPS 126
#define KTD202X_FLASH_ON_MAX 256
#define KTD202X_MAX_BRIGHTNESS 192
static const struct reg_default ktd202x_reg_defaults[] = {
{ KTD202X_REG_RESET_CONTROL, KTD202X_TIMER_SLOT_CONTROL_TSLOT1 |
KTD202X_ENABLE_CTRL_WAKE | KTD202X_TRISE_FALL_SCALE_NORMAL },
{ KTD202X_REG_FLASH_PERIOD, KTD202X_FLASH_PERIOD_256_MS_LOG_RAMP },
{ KTD202X_REG_PWM1_TIMER, KTD202X_FLASH_ON_TIME_0_4_PERCENT },
{ KTD202X_REG_PWM2_TIMER, KTD202X_FLASH_ON_TIME_0_4_PERCENT },
{ KTD202X_REG_CHANNEL_CTRL, KTD202X_CHANNEL_CTRL_OFF },
{ KTD202X_REG_TRISE_FALL, KTD202X_RAMP_TIMES_2_MS },
{ KTD202X_REG_LED_IOUT(0), KTD202X_LED_CURRENT_10_mA },
{ KTD202X_REG_LED_IOUT(1), KTD202X_LED_CURRENT_10_mA },
{ KTD202X_REG_LED_IOUT(2), KTD202X_LED_CURRENT_10_mA },
{ KTD202X_REG_LED_IOUT(3), KTD202X_LED_CURRENT_10_mA },
};
struct ktd202x_led {
struct ktd202x *chip;
union {
struct led_classdev cdev;
struct led_classdev_mc mcdev;
};
u32 index;
};
struct ktd202x {
struct mutex mutex;
struct regulator_bulk_data regulators[2];
struct device *dev;
struct regmap *regmap;
bool enabled;
unsigned long num_leds;
struct ktd202x_led leds[] __counted_by(num_leds);
};
static int ktd202x_chip_disable(struct ktd202x *chip)
{
int ret;
if (!chip->enabled)
return 0;
regmap_write(chip->regmap, KTD202X_REG_RESET_CONTROL, KTD202X_ENABLE_CTRL_SLEEP);
ret = regulator_bulk_disable(ARRAY_SIZE(chip->regulators), chip->regulators);
if (ret) {
dev_err(chip->dev, "Failed to disable regulators: %d\n", ret);
return ret;
}
chip->enabled = false;
return 0;
}
static int ktd202x_chip_enable(struct ktd202x *chip)
{
int ret;
if (chip->enabled)
return 0;
ret = regulator_bulk_enable(ARRAY_SIZE(chip->regulators), chip->regulators);
if (ret) {
dev_err(chip->dev, "Failed to enable regulators: %d\n", ret);
return ret;
}
chip->enabled = true;
ret = regmap_write(chip->regmap, KTD202X_REG_RESET_CONTROL, KTD202X_ENABLE_CTRL_WAKE);
if (ret) {
dev_err(chip->dev, "Failed to enable the chip: %d\n", ret);
ktd202x_chip_disable(chip);
}
return ret;
}
static bool ktd202x_chip_in_use(struct ktd202x *chip)
{
int i;
for (i = 0; i < chip->num_leds; i++) {
if (chip->leds[i].cdev.brightness)
return true;
}
return false;
}
static int ktd202x_brightness_set(struct ktd202x_led *led,
struct mc_subled *subleds,
unsigned int num_channels)
{
bool mode_blink = false;
int channel;
int state;
int ret;
int i;
if (ktd202x_chip_in_use(led->chip)) {
ret = ktd202x_chip_enable(led->chip);
if (ret)
return ret;
}
ret = regmap_read(led->chip->regmap, KTD202X_REG_CHANNEL_CTRL, &state);
if (ret)
return ret;
/*
* In multicolor case, assume blink mode if PWM is set for at least one
* channel because another channel cannot be in state ON at the same time
*/
for (i = 0; i < num_channels; i++) {
int channel_state;
channel = subleds[i].channel;
channel_state = (state >> 2 * channel) & KTD202X_CHANNEL_CTRL_MASK(0);
if (channel_state == KTD202X_CHANNEL_CTRL_OFF)
continue;
mode_blink = channel_state == KTD202X_CHANNEL_CTRL_PWM1(0);
break;
}
for (i = 0; i < num_channels; i++) {
enum led_brightness brightness;
int mode;
brightness = subleds[i].brightness;
channel = subleds[i].channel;
if (brightness) {
/* Register expects brightness between 0 and MAX_BRIGHTNESS - 1 */
ret = regmap_write(led->chip->regmap, KTD202X_REG_LED_IOUT(channel),
brightness - 1);
if (ret)
return ret;
if (mode_blink)
mode = KTD202X_CHANNEL_CTRL_PWM1(channel);
else
mode = KTD202X_CHANNEL_CTRL_ON(channel);
} else {
mode = KTD202X_CHANNEL_CTRL_OFF;
}
ret = regmap_update_bits(led->chip->regmap, KTD202X_REG_CHANNEL_CTRL,
KTD202X_CHANNEL_CTRL_MASK(channel), mode);
if (ret)
return ret;
}
if (!ktd202x_chip_in_use(led->chip))
return ktd202x_chip_disable(led->chip);
return 0;
}
static int ktd202x_brightness_single_set(struct led_classdev *cdev,
enum led_brightness value)
{
struct ktd202x_led *led = container_of(cdev, struct ktd202x_led, cdev);
struct mc_subled info;
int ret;
cdev->brightness = value;
mutex_lock(&led->chip->mutex);
info.brightness = value;
info.channel = led->index;
ret = ktd202x_brightness_set(led, &info, 1);
mutex_unlock(&led->chip->mutex);
return ret;
}
static int ktd202x_brightness_mc_set(struct led_classdev *cdev,
enum led_brightness value)
{
struct led_classdev_mc *mc = lcdev_to_mccdev(cdev);
struct ktd202x_led *led = container_of(mc, struct ktd202x_led, mcdev);
int ret;
cdev->brightness = value;
mutex_lock(&led->chip->mutex);
led_mc_calc_color_components(mc, value);
ret = ktd202x_brightness_set(led, mc->subled_info, mc->num_colors);
mutex_unlock(&led->chip->mutex);
return ret;
}
static int ktd202x_blink_set(struct ktd202x_led *led, unsigned long *delay_on,
unsigned long *delay_off, struct mc_subled *subleds,
unsigned int num_channels)
{
unsigned long delay_total_ms;
int ret, num_steps, on;
u8 ctrl_mask = 0;
u8 ctrl_pwm1 = 0;
u8 ctrl_on = 0;
int i;
mutex_lock(&led->chip->mutex);
for (i = 0; i < num_channels; i++) {
int channel = subleds[i].channel;
ctrl_mask |= KTD202X_CHANNEL_CTRL_MASK(channel);
ctrl_on |= KTD202X_CHANNEL_CTRL_ON(channel);
ctrl_pwm1 |= KTD202X_CHANNEL_CTRL_PWM1(channel);
}
/* Never off - brightness is already set, disable blinking */
if (!*delay_off) {
ret = regmap_update_bits(led->chip->regmap, KTD202X_REG_CHANNEL_CTRL,
ctrl_mask, ctrl_on);
goto out;
}
/* Convert into values the HW will understand. */
/* Integer representation of time of flash period */
num_steps = (*delay_on + *delay_off - KTD202X_FLASH_PERIOD_MIN_MS) /
KTD202X_FLASH_PERIOD_STEP_MS;
num_steps = clamp(num_steps, 0, KTD202X_FLASH_PERIOD_MAX_STEPS);
/* Integer representation of percentage of LED ON time */
on = (*delay_on * KTD202X_FLASH_ON_MAX) / (*delay_on + *delay_off);
/* Actually used delay_{on,off} values */
delay_total_ms = num_steps * KTD202X_FLASH_PERIOD_STEP_MS + KTD202X_FLASH_PERIOD_MIN_MS;
*delay_on = (delay_total_ms * on) / KTD202X_FLASH_ON_MAX;
*delay_off = delay_total_ms - *delay_on;
/* Set timings */
ret = regmap_write(led->chip->regmap, KTD202X_REG_FLASH_PERIOD, num_steps);
if (ret)
goto out;
ret = regmap_write(led->chip->regmap, KTD202X_REG_PWM1_TIMER, on);
if (ret)
goto out;
ret = regmap_update_bits(led->chip->regmap, KTD202X_REG_CHANNEL_CTRL,
ctrl_mask, ctrl_pwm1);
out:
mutex_unlock(&led->chip->mutex);
return ret;
}
static int ktd202x_blink_single_set(struct led_classdev *cdev,
unsigned long *delay_on,
unsigned long *delay_off)
{
struct ktd202x_led *led = container_of(cdev, struct ktd202x_led, cdev);
struct mc_subled info;
int ret;
if (!cdev->brightness) {
ret = ktd202x_brightness_single_set(cdev, KTD202X_MAX_BRIGHTNESS);
if (ret)
return ret;
}
/* If no blink specified, default to 1 Hz. */
if (!*delay_off && !*delay_on) {
*delay_off = 500;
*delay_on = 500;
}
/* Never on - just set to off */
if (!*delay_on)
return ktd202x_brightness_single_set(cdev, LED_OFF);
info.channel = led->index;
return ktd202x_blink_set(led, delay_on, delay_off, &info, 1);
}
static int ktd202x_blink_mc_set(struct led_classdev *cdev,
unsigned long *delay_on,
unsigned long *delay_off)
{
struct led_classdev_mc *mc = lcdev_to_mccdev(cdev);
struct ktd202x_led *led = container_of(mc, struct ktd202x_led, mcdev);
int ret;
if (!cdev->brightness) {
ret = ktd202x_brightness_mc_set(cdev, KTD202X_MAX_BRIGHTNESS);
if (ret)
return ret;
}
/* If no blink specified, default to 1 Hz. */
if (!*delay_off && !*delay_on) {
*delay_off = 500;
*delay_on = 500;
}
/* Never on - just set to off */
if (!*delay_on)
return ktd202x_brightness_mc_set(cdev, LED_OFF);
return ktd202x_blink_set(led, delay_on, delay_off, mc->subled_info,
mc->num_colors);
}
static int ktd202x_setup_led_rgb(struct ktd202x *chip, struct fwnode_handle *fwnode,
struct ktd202x_led *led, struct led_init_data *init_data)
{
struct fwnode_handle *child;
struct led_classdev *cdev;
struct mc_subled *info;
int num_channels;
int i = 0;
num_channels = 0;
fwnode_for_each_child_node(fwnode, child)
num_channels++;
if (!num_channels || num_channels > chip->num_leds)
return -EINVAL;
info = devm_kcalloc(chip->dev, num_channels, sizeof(*info), GFP_KERNEL);
if (!info)
return -ENOMEM;
fwnode_for_each_child_node(fwnode, child) {
u32 mono_color;
u32 reg;
int ret;
ret = fwnode_property_read_u32(child, "reg", &reg);
if (ret != 0 || reg >= chip->num_leds) {
dev_err(chip->dev, "invalid 'reg' of %pfw\n", child);
fwnode_handle_put(child);
return ret;
}
ret = fwnode_property_read_u32(child, "color", &mono_color);
if (ret < 0 && ret != -EINVAL) {
dev_err(chip->dev, "failed to parse 'color' of %pfw\n", child);
fwnode_handle_put(child);
return ret;
}
info[i].color_index = mono_color;
info[i].channel = reg;
info[i].intensity = KTD202X_MAX_BRIGHTNESS;
i++;
}
led->mcdev.subled_info = info;
led->mcdev.num_colors = num_channels;
cdev = &led->mcdev.led_cdev;
cdev->brightness_set_blocking = ktd202x_brightness_mc_set;
cdev->blink_set = ktd202x_blink_mc_set;
return devm_led_classdev_multicolor_register_ext(chip->dev, &led->mcdev, init_data);
}
static int ktd202x_setup_led_single(struct ktd202x *chip, struct fwnode_handle *fwnode,
struct ktd202x_led *led, struct led_init_data *init_data)
{
struct led_classdev *cdev;
u32 reg;
int ret;
ret = fwnode_property_read_u32(fwnode, "reg", &reg);
if (ret != 0 || reg >= chip->num_leds) {
dev_err(chip->dev, "invalid 'reg' of %pfw\n", fwnode);
return -EINVAL;
}
led->index = reg;
cdev = &led->cdev;
cdev->brightness_set_blocking = ktd202x_brightness_single_set;
cdev->blink_set = ktd202x_blink_single_set;
return devm_led_classdev_register_ext(chip->dev, &led->cdev, init_data);
}
static int ktd202x_add_led(struct ktd202x *chip, struct fwnode_handle *fwnode, unsigned int index)
{
struct ktd202x_led *led = &chip->leds[index];
struct led_init_data init_data = {};
struct led_classdev *cdev;
u32 color;
int ret;
/* Color property is optional in single color case */
ret = fwnode_property_read_u32(fwnode, "color", &color);
if (ret < 0 && ret != -EINVAL) {
dev_err(chip->dev, "failed to parse 'color' of %pfw\n", fwnode);
return ret;
}
led->chip = chip;
init_data.fwnode = fwnode;
if (color == LED_COLOR_ID_RGB) {
cdev = &led->mcdev.led_cdev;
ret = ktd202x_setup_led_rgb(chip, fwnode, led, &init_data);
} else {
cdev = &led->cdev;
ret = ktd202x_setup_led_single(chip, fwnode, led, &init_data);
}
if (ret) {
dev_err(chip->dev, "unable to register %s\n", cdev->name);
return ret;
}
cdev->max_brightness = KTD202X_MAX_BRIGHTNESS;
return 0;
}
static int ktd202x_probe_fw(struct ktd202x *chip)
{
struct device *dev = chip->dev;
int count;
int i = 0;
count = device_get_child_node_count(dev);
if (!count || count > chip->num_leds)
return -EINVAL;
regmap_write(chip->regmap, KTD202X_REG_RESET_CONTROL, KTD202X_RSTR_RESET);
/* Allow the device to execute the complete reset */
usleep_range(200, 300);
device_for_each_child_node_scoped(dev, child) {
int ret = ktd202x_add_led(chip, child, i);
if (ret)
return ret;
i++;
}
return 0;
}
static const struct regmap_config ktd202x_regmap_config = {
.reg_bits = 8,
.val_bits = 8,
.max_register = 0x09,
.cache_type = REGCACHE_FLAT,
.reg_defaults = ktd202x_reg_defaults,
.num_reg_defaults = ARRAY_SIZE(ktd202x_reg_defaults),
};
static int ktd202x_probe(struct i2c_client *client)
{
struct device *dev = &client->dev;
struct ktd202x *chip;
int count;
int ret;
count = device_get_child_node_count(dev);
if (!count || count > KTD202X_MAX_LEDS)
return dev_err_probe(dev, -EINVAL, "Incorrect number of leds (%d)", count);
chip = devm_kzalloc(dev, struct_size(chip, leds, count), GFP_KERNEL);
if (!chip)
return -ENOMEM;
chip->dev = dev;
i2c_set_clientdata(client, chip);
chip->regmap = devm_regmap_init_i2c(client, &ktd202x_regmap_config);
if (IS_ERR(chip->regmap)) {
ret = dev_err_probe(dev, PTR_ERR(chip->regmap),
"Failed to allocate register map.\n");
return ret;
}
ret = devm_mutex_init(dev, &chip->mutex);
if (ret)
return ret;
chip->num_leds = (unsigned long)i2c_get_match_data(client);
chip->regulators[0].supply = "vin";
chip->regulators[1].supply = "vio";
ret = devm_regulator_bulk_get(dev, ARRAY_SIZE(chip->regulators), chip->regulators);
if (ret < 0) {
dev_err_probe(dev, ret, "Failed to request regulators.\n");
return ret;
}
ret = regulator_bulk_enable(ARRAY_SIZE(chip->regulators), chip->regulators);
if (ret) {
dev_err_probe(dev, ret, "Failed to enable regulators.\n");
return ret;
}
ret = ktd202x_probe_fw(chip);
if (ret < 0) {
regulator_bulk_disable(ARRAY_SIZE(chip->regulators), chip->regulators);
return ret;
}
ret = regulator_bulk_disable(ARRAY_SIZE(chip->regulators), chip->regulators);
if (ret) {
dev_err_probe(dev, ret, "Failed to disable regulators.\n");
return ret;
}
return 0;
}
static void ktd202x_remove(struct i2c_client *client)
{
struct ktd202x *chip = i2c_get_clientdata(client);
ktd202x_chip_disable(chip);
}
static void ktd202x_shutdown(struct i2c_client *client)
{
struct ktd202x *chip = i2c_get_clientdata(client);
/* Reset registers to make sure all LEDs are off before shutdown */
regmap_write(chip->regmap, KTD202X_REG_RESET_CONTROL, KTD202X_RSTR_RESET);
}
static const struct i2c_device_id ktd202x_id[] = {
{ .name = "ktd2026", .driver_data = KTD2026_NUM_LEDS },
{ .name = "ktd2027", .driver_data = KTD2027_NUM_LEDS },
{ }
};
MODULE_DEVICE_TABLE(i2c, ktd202x_id);
static const struct of_device_id ktd202x_match_table[] = {
{ .compatible = "kinetic,ktd2026", .data = (void *)KTD2026_NUM_LEDS },
{ .compatible = "kinetic,ktd2027", .data = (void *)KTD2027_NUM_LEDS },
{}
};
MODULE_DEVICE_TABLE(of, ktd202x_match_table);
static struct i2c_driver ktd202x_driver = {
.driver = {
.name = "leds-ktd202x",
.of_match_table = ktd202x_match_table,
},
.probe = ktd202x_probe,
.remove = ktd202x_remove,
.shutdown = ktd202x_shutdown,
.id_table = ktd202x_id,
};
module_i2c_driver(ktd202x_driver);
MODULE_AUTHOR("André Apitzsch <git@apitzsch.eu>");
MODULE_DESCRIPTION("Kinetic KTD2026/7 LED driver");
MODULE_LICENSE("GPL");
+642
View File
@@ -0,0 +1,642 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* LP5812 LED driver
*
* Copyright (C) 2025 Texas Instruments
*
* Author: Jared Zhou <jared-zhou@ti.com>
*/
#include <linux/delay.h>
#include <linux/i2c.h>
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/led-class-multicolor.h>
#include <linux/leds.h>
#include <linux/module.h>
#include <linux/mutex.h>
#include <linux/sysfs.h>
#include <linux/types.h>
#include "leds-lp5812.h"
static const struct lp5812_mode_mapping chip_mode_map[] = {
{"direct_mode", 0, 0, 0, 0, 0, 0},
{"tcm:1:0", 1, 0, 0, 0, 0, 0},
{"tcm:1:1", 1, 1, 0, 0, 0, 0},
{"tcm:1:2", 1, 2, 0, 0, 0, 0},
{"tcm:1:3", 1, 3, 0, 0, 0, 0},
{"tcm:2:0:1", 2, 0, 1, 0, 0, 0},
{"tcm:2:0:2", 2, 0, 2, 0, 0, 0},
{"tcm:2:0:3", 2, 0, 3, 0, 0, 0},
{"tcm:2:1:2", 2, 1, 2, 0, 0, 0},
{"tcm:2:1:3", 2, 1, 3, 0, 0, 0},
{"tcm:2:2:3", 2, 2, 3, 0, 0, 0},
{"tcm:3:0:1:2", 3, 0, 1, 2, 0, 0},
{"tcm:3:0:1:3", 3, 0, 1, 3, 0, 0},
{"tcm:3:0:2:3", 3, 0, 2, 3, 0, 0},
{"tcm:4:0:1:2:3", 4, 0, 1, 2, 3, 0},
{"mix:1:0:1", 5, 1, 0, 0, 0, 0},
{"mix:1:0:2", 5, 2, 0, 0, 0, 0},
{"mix:1:0:3", 5, 3, 0, 0, 0, 0},
{"mix:1:1:0", 5, 0, 0, 0, 0, 1},
{"mix:1:1:2", 5, 2, 0, 0, 0, 1},
{"mix:1:1:3", 5, 3, 0, 0, 0, 1},
{"mix:1:2:0", 5, 0, 0, 0, 0, 2},
{"mix:1:2:1", 5, 1, 0, 0, 0, 2},
{"mix:1:2:3", 5, 3, 0, 0, 0, 2},
{"mix:1:3:0", 5, 0, 0, 0, 0, 3},
{"mix:1:3:1", 5, 1, 0, 0, 0, 3},
{"mix:1:3:2", 5, 2, 0, 0, 0, 3},
{"mix:2:0:1:2", 6, 1, 2, 0, 0, 0},
{"mix:2:0:1:3", 6, 1, 3, 0, 0, 0},
{"mix:2:0:2:3", 6, 2, 3, 0, 0, 0},
{"mix:2:1:0:2", 6, 0, 2, 0, 0, 1},
{"mix:2:1:0:3", 6, 0, 3, 0, 0, 1},
{"mix:2:1:2:3", 6, 2, 3, 0, 0, 1},
{"mix:2:2:0:1", 6, 0, 1, 0, 0, 2},
{"mix:2:2:0:3", 6, 0, 3, 0, 0, 2},
{"mix:2:2:1:3", 6, 1, 3, 0, 0, 2},
{"mix:2:3:0:1", 6, 0, 1, 0, 0, 3},
{"mix:2:3:0:2", 6, 0, 2, 0, 0, 3},
{"mix:2:3:1:2", 6, 1, 2, 0, 0, 3},
{"mix:3:0:1:2:3", 7, 1, 2, 3, 0, 0},
{"mix:3:1:0:2:3", 7, 0, 2, 3, 0, 1},
{"mix:3:2:0:1:3", 7, 0, 1, 3, 0, 2},
{"mix:3:3:0:1:2", 7, 0, 1, 2, 0, 3}
};
static int lp5812_write(struct lp5812_chip *chip, u16 reg, u8 val)
{
struct device *dev = &chip->client->dev;
struct i2c_msg msg;
u8 buf[LP5812_DATA_LENGTH];
u8 reg_addr_bit8_9;
int ret;
/* Extract register address bits 9 and 8 for Address Byte 1 */
reg_addr_bit8_9 = (reg >> LP5812_REG_ADDR_HIGH_SHIFT) & LP5812_REG_ADDR_BIT_8_9_MASK;
/* Prepare payload: Address Byte 2 (bits [7:0]) and value to write */
buf[LP5812_DATA_BYTE_0_IDX] = (u8)(reg & LP5812_REG_ADDR_LOW_MASK);
buf[LP5812_DATA_BYTE_1_IDX] = val;
/* Construct I2C message for a write operation */
msg.addr = (chip->client->addr << LP5812_CHIP_ADDR_SHIFT) | reg_addr_bit8_9;
msg.flags = 0;
msg.len = sizeof(buf);
msg.buf = buf;
ret = i2c_transfer(chip->client->adapter, &msg, 1);
if (ret == 1)
return 0;
dev_err(dev, "I2C write error, ret=%d\n", ret);
return ret < 0 ? ret : -EIO;
}
static int lp5812_read(struct lp5812_chip *chip, u16 reg, u8 *val)
{
struct device *dev = &chip->client->dev;
struct i2c_msg msgs[LP5812_READ_MSG_LENGTH];
u8 ret_val;
u8 reg_addr_bit8_9;
u8 converted_reg;
int ret;
/* Extract register address bits 9 and 8 for Address Byte 1 */
reg_addr_bit8_9 = (reg >> LP5812_REG_ADDR_HIGH_SHIFT) & LP5812_REG_ADDR_BIT_8_9_MASK;
/* Lower 8 bits go in Address Byte 2 */
converted_reg = (u8)(reg & LP5812_REG_ADDR_LOW_MASK);
/* Prepare I2C write message to set register address */
msgs[LP5812_MSG_0_IDX].addr =
(chip->client->addr << LP5812_CHIP_ADDR_SHIFT) | reg_addr_bit8_9;
msgs[LP5812_MSG_0_IDX].flags = 0;
msgs[LP5812_MSG_0_IDX].len = 1;
msgs[LP5812_MSG_0_IDX].buf = &converted_reg;
/* Prepare I2C read message to retrieve register value */
msgs[LP5812_MSG_1_IDX].addr =
(chip->client->addr << LP5812_CHIP_ADDR_SHIFT) | reg_addr_bit8_9;
msgs[LP5812_MSG_1_IDX].flags = I2C_M_RD;
msgs[LP5812_MSG_1_IDX].len = 1;
msgs[LP5812_MSG_1_IDX].buf = &ret_val;
ret = i2c_transfer(chip->client->adapter, msgs, LP5812_READ_MSG_LENGTH);
if (ret == LP5812_READ_MSG_LENGTH) {
*val = ret_val;
return 0;
}
dev_err(dev, "I2C read error, ret=%d\n", ret);
*val = 0;
return ret < 0 ? ret : -EIO;
}
static int lp5812_read_tsd_config_status(struct lp5812_chip *chip, u8 *reg_val)
{
return lp5812_read(chip, LP5812_TSD_CONFIG_STATUS, reg_val);
}
static int lp5812_update_regs_config(struct lp5812_chip *chip)
{
u8 reg_val;
int ret;
ret = lp5812_write(chip, LP5812_CMD_UPDATE, LP5812_UPDATE_CMD_VAL);
if (ret)
return ret;
ret = lp5812_read_tsd_config_status(chip, &reg_val);
if (ret)
return ret;
return reg_val & LP5812_CFG_ERR_STATUS_MASK;
}
static ssize_t parse_drive_mode(struct lp5812_chip *chip, const char *str)
{
int i;
chip->drive_mode.bits.mix_sel_led_0 = false;
chip->drive_mode.bits.mix_sel_led_1 = false;
chip->drive_mode.bits.mix_sel_led_2 = false;
chip->drive_mode.bits.mix_sel_led_3 = false;
if (sysfs_streq(str, LP5812_MODE_DIRECT_NAME)) {
chip->drive_mode.bits.led_mode = LP5812_MODE_DIRECT_VALUE;
return 0;
}
for (i = 0; i < ARRAY_SIZE(chip_mode_map); i++) {
if (!sysfs_streq(str, chip_mode_map[i].mode_name))
continue;
chip->drive_mode.bits.led_mode = chip_mode_map[i].mode;
chip->scan_order.bits.order0 = chip_mode_map[i].scan_order_0;
chip->scan_order.bits.order1 = chip_mode_map[i].scan_order_1;
chip->scan_order.bits.order2 = chip_mode_map[i].scan_order_2;
chip->scan_order.bits.order3 = chip_mode_map[i].scan_order_3;
switch (chip_mode_map[i].selection_led) {
case LP5812_MODE_MIX_SELECT_LED_0:
chip->drive_mode.bits.mix_sel_led_0 = true;
break;
case LP5812_MODE_MIX_SELECT_LED_1:
chip->drive_mode.bits.mix_sel_led_1 = true;
break;
case LP5812_MODE_MIX_SELECT_LED_2:
chip->drive_mode.bits.mix_sel_led_2 = true;
break;
case LP5812_MODE_MIX_SELECT_LED_3:
chip->drive_mode.bits.mix_sel_led_3 = true;
break;
default:
return -EINVAL;
}
return 0;
}
return -EINVAL;
}
static int lp5812_set_drive_mode_scan_order(struct lp5812_chip *chip)
{
u8 val;
int ret;
val = chip->drive_mode.val;
ret = lp5812_write(chip, LP5812_DEV_CONFIG1, val);
if (ret)
return ret;
val = chip->scan_order.val;
ret = lp5812_write(chip, LP5812_DEV_CONFIG2, val);
return ret;
}
static int lp5812_set_led_mode(struct lp5812_chip *chip, int led_number,
enum control_mode mode)
{
u8 reg_val;
u16 reg;
int ret;
/*
* Select device configuration register.
* Reg3 for LED_0–LED_3, LED_A0–A2, LED_B0
* Reg4 for LED_B1–B2, LED_C0–C2, LED_D0–D2
*/
if (led_number < LP5812_NUMBER_LED_IN_REG)
reg = LP5812_DEV_CONFIG3;
else
reg = LP5812_DEV_CONFIG4;
ret = lp5812_read(chip, reg, &reg_val);
if (ret)
return ret;
if (mode == LP5812_MODE_MANUAL)
reg_val &= ~(LP5812_ENABLE << (led_number % LP5812_NUMBER_LED_IN_REG));
else
reg_val |= (LP5812_ENABLE << (led_number % LP5812_NUMBER_LED_IN_REG));
ret = lp5812_write(chip, reg, reg_val);
if (ret)
return ret;
ret = lp5812_update_regs_config(chip);
return ret;
}
static int lp5812_manual_dc_pwm_control(struct lp5812_chip *chip, int led_number,
u8 val, enum dimming_type dimming_type)
{
u16 led_base_reg;
int ret;
if (dimming_type == LP5812_DIMMING_ANALOG)
led_base_reg = LP5812_MANUAL_DC_BASE;
else
led_base_reg = LP5812_MANUAL_PWM_BASE;
ret = lp5812_write(chip, led_base_reg + led_number, val);
return ret;
}
static int lp5812_multicolor_brightness(struct lp5812_led *led)
{
struct lp5812_chip *chip = led->chip;
int ret, i;
guard(mutex)(&chip->lock);
for (i = 0; i < led->mc_cdev.num_colors; i++) {
ret = lp5812_manual_dc_pwm_control(chip, led->mc_cdev.subled_info[i].channel,
led->mc_cdev.subled_info[i].brightness,
LP5812_DIMMING_PWM);
if (ret)
return ret;
}
return 0;
}
static int lp5812_led_brightness(struct lp5812_led *led)
{
struct lp5812_chip *chip = led->chip;
struct lp5812_led_config *led_cfg;
int ret;
led_cfg = &chip->led_config[led->chan_nr];
guard(mutex)(&chip->lock);
ret = lp5812_manual_dc_pwm_control(chip, led_cfg->led_id[0],
led->brightness, LP5812_DIMMING_PWM);
return ret;
}
static int lp5812_set_brightness(struct led_classdev *cdev,
enum led_brightness brightness)
{
struct lp5812_led *led = container_of(cdev, struct lp5812_led, cdev);
led->brightness = (u8)brightness;
return lp5812_led_brightness(led);
}
static int lp5812_set_mc_brightness(struct led_classdev *cdev,
enum led_brightness brightness)
{
struct led_classdev_mc *mc_dev = lcdev_to_mccdev(cdev);
struct lp5812_led *led = container_of(mc_dev, struct lp5812_led, mc_cdev);
led_mc_calc_color_components(&led->mc_cdev, brightness);
return lp5812_multicolor_brightness(led);
}
static int lp5812_init_led(struct lp5812_led *led, struct lp5812_chip *chip, int chan)
{
struct device *dev = &chip->client->dev;
struct mc_subled *mc_led_info;
struct led_classdev *led_cdev;
int i, ret;
if (chip->led_config[chan].name) {
led->cdev.name = chip->led_config[chan].name;
} else {
led->cdev.name = devm_kasprintf(dev, GFP_KERNEL, "%s:channel%d",
chip->label ? : chip->client->name, chan);
if (!led->cdev.name)
return -ENOMEM;
}
if (!chip->led_config[chan].is_sc_led) {
mc_led_info = devm_kcalloc(dev, chip->led_config[chan].num_colors,
sizeof(*mc_led_info), GFP_KERNEL);
if (!mc_led_info)
return -ENOMEM;
led_cdev = &led->mc_cdev.led_cdev;
led_cdev->name = led->cdev.name;
led_cdev->brightness_set_blocking = lp5812_set_mc_brightness;
led->mc_cdev.num_colors = chip->led_config[chan].num_colors;
for (i = 0; i < led->mc_cdev.num_colors; i++) {
mc_led_info[i].color_index = chip->led_config[chan].color_id[i];
mc_led_info[i].channel = chip->led_config[chan].led_id[i];
}
led->mc_cdev.subled_info = mc_led_info;
} else {
led->cdev.brightness_set_blocking = lp5812_set_brightness;
}
led->chan_nr = chan;
if (chip->led_config[chan].is_sc_led) {
ret = devm_led_classdev_register(dev, &led->cdev);
if (ret == 0)
led->cdev.dev->platform_data = led;
} else {
ret = devm_led_classdev_multicolor_register(dev, &led->mc_cdev);
if (ret == 0)
led->mc_cdev.led_cdev.dev->platform_data = led;
}
return ret;
}
static int lp5812_register_leds(struct lp5812_led *leds, struct lp5812_chip *chip)
{
struct lp5812_led *led;
int num_channels = chip->num_channels;
u8 reg_val;
u16 reg;
int ret, i, j;
for (i = 0; i < num_channels; i++) {
led = &leds[i];
ret = lp5812_init_led(led, chip, i);
if (ret)
goto err_init_led;
led->chip = chip;
for (j = 0; j < chip->led_config[i].num_colors; j++) {
ret = lp5812_write(chip,
LP5812_AUTO_DC_BASE + chip->led_config[i].led_id[j],
chip->led_config[i].max_current[j]);
if (ret)
goto err_init_led;
ret = lp5812_manual_dc_pwm_control(chip, chip->led_config[i].led_id[j],
chip->led_config[i].max_current[j],
LP5812_DIMMING_ANALOG);
if (ret)
goto err_init_led;
ret = lp5812_set_led_mode(chip, chip->led_config[i].led_id[j],
LP5812_MODE_MANUAL);
if (ret)
goto err_init_led;
reg = (chip->led_config[i].led_id[j] < LP5812_NUMBER_LED_IN_REG) ?
LP5812_LED_EN_1 : LP5812_LED_EN_2;
ret = lp5812_read(chip, reg, &reg_val);
if (ret)
goto err_init_led;
reg_val |= (LP5812_ENABLE << (chip->led_config[i].led_id[j] %
LP5812_NUMBER_LED_IN_REG));
ret = lp5812_write(chip, reg, reg_val);
if (ret)
goto err_init_led;
}
}
return 0;
err_init_led:
return ret;
}
static int lp5812_init_device(struct lp5812_chip *chip)
{
int ret;
usleep_range(LP5812_WAIT_DEVICE_STABLE_MIN, LP5812_WAIT_DEVICE_STABLE_MAX);
ret = lp5812_write(chip, LP5812_REG_ENABLE, LP5812_ENABLE);
if (ret) {
dev_err(&chip->client->dev, "failed to enable LP5812 device\n");
return ret;
}
ret = lp5812_write(chip, LP5812_DEV_CONFIG12, LP5812_LSD_LOD_START_UP);
if (ret) {
dev_err(&chip->client->dev, "failed to configure device safety thresholds\n");
return ret;
}
ret = parse_drive_mode(chip, chip->scan_mode);
if (ret)
return ret;
ret = lp5812_set_drive_mode_scan_order(chip);
if (ret)
return ret;
ret = lp5812_update_regs_config(chip);
if (ret) {
dev_err(&chip->client->dev, "failed to apply configuration updates\n");
return ret;
}
return 0;
}
static void lp5812_deinit_device(struct lp5812_chip *chip)
{
lp5812_write(chip, LP5812_LED_EN_1, LP5812_DISABLE);
lp5812_write(chip, LP5812_LED_EN_2, LP5812_DISABLE);
lp5812_write(chip, LP5812_REG_ENABLE, LP5812_DISABLE);
}
static int lp5812_parse_led_channel(struct device_node *np,
struct lp5812_led_config *cfg,
int color_number)
{
int color_id, reg, ret;
u32 max_cur;
ret = of_property_read_u32(np, "reg", &reg);
if (ret)
return ret;
cfg->led_id[color_number] = reg;
ret = of_property_read_u32(np, "led-max-microamp", &max_cur);
if (ret)
max_cur = 0;
/* Convert microamps to driver units */
cfg->max_current[color_number] = max_cur / 100;
ret = of_property_read_u32(np, "color", &color_id);
if (ret)
color_id = 0;
cfg->color_id[color_number] = color_id;
return 0;
}
static int lp5812_parse_led(struct device_node *np,
struct lp5812_led_config *cfg,
int led_index)
{
int num_colors, ret;
of_property_read_string(np, "label", &cfg[led_index].name);
ret = of_property_read_u32(np, "reg", &cfg[led_index].chan_nr);
if (ret)
return ret;
num_colors = 0;
for_each_available_child_of_node_scoped(np, child) {
ret = lp5812_parse_led_channel(child, &cfg[led_index], num_colors);
if (ret)
return ret;
num_colors++;
}
if (num_colors == 0) {
ret = lp5812_parse_led_channel(np, &cfg[led_index], 0);
if (ret)
return ret;
num_colors = 1;
cfg[led_index].is_sc_led = true;
} else {
cfg[led_index].is_sc_led = false;
}
cfg[led_index].num_colors = num_colors;
return 0;
}
static int lp5812_of_probe(struct device *dev,
struct device_node *np,
struct lp5812_chip *chip)
{
struct lp5812_led_config *cfg;
int num_channels, i = 0, ret;
num_channels = of_get_available_child_count(np);
if (num_channels == 0) {
dev_err(dev, "no LED channels\n");
return -EINVAL;
}
cfg = devm_kcalloc(dev, num_channels, sizeof(*cfg), GFP_KERNEL);
if (!cfg)
return -ENOMEM;
chip->led_config = &cfg[0];
chip->num_channels = num_channels;
for_each_available_child_of_node_scoped(np, child) {
ret = lp5812_parse_led(child, cfg, i);
if (ret)
return -EINVAL;
i++;
}
ret = of_property_read_string(np, "ti,scan-mode", &chip->scan_mode);
if (ret)
chip->scan_mode = LP5812_MODE_DIRECT_NAME;
of_property_read_string(np, "label", &chip->label);
return 0;
}
static int lp5812_probe(struct i2c_client *client)
{
struct lp5812_chip *chip;
struct device_node *np = dev_of_node(&client->dev);
struct lp5812_led *leds;
int ret;
if (!np)
return -EINVAL;
chip = devm_kzalloc(&client->dev, sizeof(*chip), GFP_KERNEL);
if (!chip)
return -ENOMEM;
ret = lp5812_of_probe(&client->dev, np, chip);
if (ret)
return ret;
leds = devm_kcalloc(&client->dev, chip->num_channels, sizeof(*leds), GFP_KERNEL);
if (!leds)
return -ENOMEM;
chip->client = client;
mutex_init(&chip->lock);
i2c_set_clientdata(client, chip);
ret = lp5812_init_device(chip);
if (ret)
return ret;
ret = lp5812_register_leds(leds, chip);
if (ret)
goto err_out;
return 0;
err_out:
lp5812_deinit_device(chip);
return ret;
}
static void lp5812_remove(struct i2c_client *client)
{
struct lp5812_chip *chip = i2c_get_clientdata(client);
lp5812_deinit_device(chip);
}
static const struct of_device_id of_lp5812_match[] = {
{ .compatible = "ti,lp5812" },
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, of_lp5812_match);
static struct i2c_driver lp5812_driver = {
.driver = {
.name = "lp5812",
.of_match_table = of_lp5812_match,
},
.probe = lp5812_probe,
.remove = lp5812_remove,
};
module_i2c_driver(lp5812_driver);
MODULE_DESCRIPTION("Texas Instruments LP5812 LED Driver");
MODULE_AUTHOR("Jared Zhou <jared-zhou@ti.com>");
MODULE_LICENSE("GPL");
+172
View File
@@ -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_*/
@@ -0,0 +1,234 @@
// 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);
if (ret)
goto err_disable;
mutex_unlock(&lp->lock);
ret = lp5860_init_dt(lp);
if (ret)
goto err_disable;
return 0;
err_disable:
mutex_unlock(&lp->lock);
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");
@@ -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;
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;
mutex_init(&lp5860->lock);
spi_set_drvdata(spi, lp5860);
return lp5860_device_init(dev);
}
static void lp5860_remove(struct spi_device *spi)
{
struct lp5860 *lp5860 = spi_get_drvdata(spi);
mutex_destroy(&lp5860->lock);
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");
+268
View File
@@ -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 */
@@ -0,0 +1,994 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* Copyright (C) 2023 Richtek Technology Corp.
*
* Authors:
* ChiYuan Huang <cy_huang@richtek.com>
* Alice Chen <alice_chen@richtek.com>
*/
#include <linux/bitfield.h>
#include <linux/bitops.h>
#include <linux/kernel.h>
#include <linux/leds.h>
#include <linux/led-class-multicolor.h>
#include <linux/linear_range.h>
#include <linux/module.h>
#include <linux/mutex.h>
#include <linux/platform_device.h>
#include <linux/property.h>
#include <linux/regmap.h>
#include <linux/util_macros.h>
#include <linux/unaligned.h>
enum {
MT6370_LED_ISNK1 = 0,
MT6370_LED_ISNK2,
MT6370_LED_ISNK3,
MT6370_LED_ISNK4,
MT6370_MAX_LEDS
};
enum mt6370_led_mode {
MT6370_LED_PWM_MODE = 0,
MT6370_LED_BREATH_MODE,
MT6370_LED_REG_MODE,
MT6370_LED_MAX_MODE
};
enum mt6370_led_field {
F_RGB_EN = 0,
F_CHGIND_EN,
F_LED1_CURR,
F_LED2_CURR,
F_LED3_CURR,
F_LED4_CURR,
F_LED1_MODE,
F_LED2_MODE,
F_LED3_MODE,
F_LED4_MODE,
F_LED1_DUTY,
F_LED2_DUTY,
F_LED3_DUTY,
F_LED4_DUTY,
F_LED1_FREQ,
F_LED2_FREQ,
F_LED3_FREQ,
F_LED4_FREQ,
F_MAX_FIELDS
};
enum mt6370_led_ranges {
R_LED123_CURR = 0,
R_LED4_CURR,
R_LED_TRFON,
R_LED_TOFF,
R_MAX_RANGES
};
enum mt6370_pattern {
P_LED_TR1 = 0,
P_LED_TR2,
P_LED_TF1,
P_LED_TF2,
P_LED_TON,
P_LED_TOFF,
P_MAX_PATTERNS
};
#define MT6370_REG_DEV_INFO 0x100
#define MT6370_REG_RGB1_DIM 0x182
#define MT6370_REG_RGB2_DIM 0x183
#define MT6370_REG_RGB3_DIM 0x184
#define MT6370_REG_RGB_EN 0x185
#define MT6370_REG_RGB1_ISNK 0x186
#define MT6370_REG_RGB2_ISNK 0x187
#define MT6370_REG_RGB3_ISNK 0x188
#define MT6370_REG_RGB1_TR 0x189
#define MT6370_REG_RGB_CHRIND_DIM 0x192
#define MT6370_REG_RGB_CHRIND_CTRL 0x193
#define MT6370_REG_RGB_CHRIND_TR 0x194
#define MT6372_REG_RGB_EN 0x182
#define MT6372_REG_RGB1_ISNK 0x183
#define MT6372_REG_RGB2_ISNK 0x184
#define MT6372_REG_RGB3_ISNK 0x185
#define MT6372_REG_RGB4_ISNK 0x186
#define MT6372_REG_RGB1_DIM 0x187
#define MT6372_REG_RGB2_DIM 0x188
#define MT6372_REG_RGB3_DIM 0x189
#define MT6372_REG_RGB4_DIM 0x18A
#define MT6372_REG_RGB12_FREQ 0x18B
#define MT6372_REG_RGB34_FREQ 0x18C
#define MT6372_REG_RGB1_TR 0x18D
#define MT6370_VENDOR_ID_MASK GENMASK(7, 4)
#define MT6372_VENDOR_ID 0x9
#define MT6372C_VENDOR_ID 0xb
#define MT6370_CHEN_BIT(id) BIT(MT6370_LED_ISNK4 - id)
#define MT6370_VIRTUAL_MULTICOLOR 5
#define MC_CHANNEL_NUM 3
#define MT6370_PWM_DUTY (BIT(5) - 1)
#define MT6372_PWM_DUTY (BIT(8) - 1)
struct mt6370_led {
/*
* If the color of the LED in DT is set to
* - 'LED_COLOR_ID_RGB'
* - 'LED_COLOR_ID_MULTI'
* The member 'index' of this struct will be set to
* 'MT6370_VIRTUAL_MULTICOLOR'.
* If so, this LED will choose 'struct led_classdev_mc mc' to use.
* Instead, if the member 'index' of this struct is set to
* 'MT6370_LED_ISNK1' ~ 'MT6370_LED_ISNK4', then this LED will choose
* 'struct led_classdev isink' to use.
*/
union {
struct led_classdev isink;
struct led_classdev_mc mc;
};
struct mt6370_priv *priv;
enum led_default_state default_state;
u32 index;
};
struct mt6370_pdata {
const unsigned int *tfreq;
unsigned int tfreq_len;
u16 reg_rgb1_tr;
s16 reg_rgb_chrind_tr;
u8 pwm_duty;
};
struct mt6370_priv {
/* Per LED access lock */
struct mutex lock;
struct regmap *regmap;
struct regmap_field *fields[F_MAX_FIELDS];
const struct reg_field *reg_fields;
const struct linear_range *ranges;
const struct mt6370_pdata *pdata;
unsigned int leds_count;
unsigned int leds_active;
struct mt6370_led leds[] __counted_by(leds_count);
};
static const struct reg_field common_reg_fields[F_MAX_FIELDS] = {
[F_RGB_EN] = REG_FIELD(MT6370_REG_RGB_EN, 4, 7),
[F_CHGIND_EN] = REG_FIELD(MT6370_REG_RGB_CHRIND_DIM, 7, 7),
[F_LED1_CURR] = REG_FIELD(MT6370_REG_RGB1_ISNK, 0, 2),
[F_LED2_CURR] = REG_FIELD(MT6370_REG_RGB2_ISNK, 0, 2),
[F_LED3_CURR] = REG_FIELD(MT6370_REG_RGB3_ISNK, 0, 2),
[F_LED4_CURR] = REG_FIELD(MT6370_REG_RGB_CHRIND_CTRL, 0, 1),
[F_LED1_MODE] = REG_FIELD(MT6370_REG_RGB1_DIM, 5, 6),
[F_LED2_MODE] = REG_FIELD(MT6370_REG_RGB2_DIM, 5, 6),
[F_LED3_MODE] = REG_FIELD(MT6370_REG_RGB3_DIM, 5, 6),
[F_LED4_MODE] = REG_FIELD(MT6370_REG_RGB_CHRIND_DIM, 5, 6),
[F_LED1_DUTY] = REG_FIELD(MT6370_REG_RGB1_DIM, 0, 4),
[F_LED2_DUTY] = REG_FIELD(MT6370_REG_RGB2_DIM, 0, 4),
[F_LED3_DUTY] = REG_FIELD(MT6370_REG_RGB3_DIM, 0, 4),
[F_LED4_DUTY] = REG_FIELD(MT6370_REG_RGB_CHRIND_DIM, 0, 4),
[F_LED1_FREQ] = REG_FIELD(MT6370_REG_RGB1_ISNK, 3, 5),
[F_LED2_FREQ] = REG_FIELD(MT6370_REG_RGB2_ISNK, 3, 5),
[F_LED3_FREQ] = REG_FIELD(MT6370_REG_RGB3_ISNK, 3, 5),
[F_LED4_FREQ] = REG_FIELD(MT6370_REG_RGB_CHRIND_CTRL, 2, 4),
};
static const struct reg_field mt6372_reg_fields[F_MAX_FIELDS] = {
[F_RGB_EN] = REG_FIELD(MT6372_REG_RGB_EN, 4, 7),
[F_CHGIND_EN] = REG_FIELD(MT6372_REG_RGB_EN, 3, 3),
[F_LED1_CURR] = REG_FIELD(MT6372_REG_RGB1_ISNK, 0, 3),
[F_LED2_CURR] = REG_FIELD(MT6372_REG_RGB2_ISNK, 0, 3),
[F_LED3_CURR] = REG_FIELD(MT6372_REG_RGB3_ISNK, 0, 3),
[F_LED4_CURR] = REG_FIELD(MT6372_REG_RGB4_ISNK, 0, 3),
[F_LED1_MODE] = REG_FIELD(MT6372_REG_RGB1_ISNK, 6, 7),
[F_LED2_MODE] = REG_FIELD(MT6372_REG_RGB2_ISNK, 6, 7),
[F_LED3_MODE] = REG_FIELD(MT6372_REG_RGB3_ISNK, 6, 7),
[F_LED4_MODE] = REG_FIELD(MT6372_REG_RGB4_ISNK, 6, 7),
[F_LED1_DUTY] = REG_FIELD(MT6372_REG_RGB1_DIM, 0, 7),
[F_LED2_DUTY] = REG_FIELD(MT6372_REG_RGB2_DIM, 0, 7),
[F_LED3_DUTY] = REG_FIELD(MT6372_REG_RGB3_DIM, 0, 7),
[F_LED4_DUTY] = REG_FIELD(MT6372_REG_RGB4_DIM, 0, 7),
[F_LED1_FREQ] = REG_FIELD(MT6372_REG_RGB12_FREQ, 5, 7),
[F_LED2_FREQ] = REG_FIELD(MT6372_REG_RGB12_FREQ, 2, 4),
[F_LED3_FREQ] = REG_FIELD(MT6372_REG_RGB34_FREQ, 5, 7),
[F_LED4_FREQ] = REG_FIELD(MT6372_REG_RGB34_FREQ, 2, 4),
};
/* Current unit: microamp, time unit: millisecond */
static const struct linear_range common_led_ranges[R_MAX_RANGES] = {
[R_LED123_CURR] = LINEAR_RANGE(4000, 1, 6, 4000),
[R_LED4_CURR] = LINEAR_RANGE(2000, 1, 3, 2000),
[R_LED_TRFON] = LINEAR_RANGE(125, 0, 15, 200),
[R_LED_TOFF] = LINEAR_RANGE(250, 0, 15, 400),
};
static const struct linear_range mt6372_led_ranges[R_MAX_RANGES] = {
[R_LED123_CURR] = LINEAR_RANGE(2000, 1, 14, 2000),
[R_LED4_CURR] = LINEAR_RANGE(2000, 1, 14, 2000),
[R_LED_TRFON] = LINEAR_RANGE(125, 0, 15, 250),
[R_LED_TOFF] = LINEAR_RANGE(250, 0, 15, 500),
};
static const unsigned int common_tfreqs[] = {
10000, 5000, 2000, 1000, 500, 200, 5, 1,
};
static const unsigned int mt6372_tfreqs[] = {
8000, 4000, 2000, 1000, 500, 250, 8, 4,
};
static const struct mt6370_pdata common_pdata = {
.tfreq = common_tfreqs,
.tfreq_len = ARRAY_SIZE(common_tfreqs),
.pwm_duty = MT6370_PWM_DUTY,
.reg_rgb1_tr = MT6370_REG_RGB1_TR,
.reg_rgb_chrind_tr = MT6370_REG_RGB_CHRIND_TR,
};
static const struct mt6370_pdata mt6372_pdata = {
.tfreq = mt6372_tfreqs,
.tfreq_len = ARRAY_SIZE(mt6372_tfreqs),
.pwm_duty = MT6372_PWM_DUTY,
.reg_rgb1_tr = MT6372_REG_RGB1_TR,
.reg_rgb_chrind_tr = -1,
};
static enum mt6370_led_field mt6370_get_led_current_field(unsigned int led_no)
{
switch (led_no) {
case MT6370_LED_ISNK1:
return F_LED1_CURR;
case MT6370_LED_ISNK2:
return F_LED2_CURR;
case MT6370_LED_ISNK3:
return F_LED3_CURR;
default:
return F_LED4_CURR;
}
}
static int mt6370_set_led_brightness(struct mt6370_priv *priv, unsigned int led_no,
unsigned int level)
{
enum mt6370_led_field sel_field;
sel_field = mt6370_get_led_current_field(led_no);
return regmap_field_write(priv->fields[sel_field], level);
}
static int mt6370_get_led_brightness(struct mt6370_priv *priv, unsigned int led_no,
unsigned int *level)
{
enum mt6370_led_field sel_field;
sel_field = mt6370_get_led_current_field(led_no);
return regmap_field_read(priv->fields[sel_field], level);
}
static int mt6370_set_led_duty(struct mt6370_priv *priv, unsigned int led_no, unsigned int ton,
unsigned int toff)
{
const struct mt6370_pdata *pdata = priv->pdata;
enum mt6370_led_field sel_field;
unsigned int divisor, ratio;
divisor = pdata->pwm_duty;
ratio = ton * divisor / (ton + toff);
switch (led_no) {
case MT6370_LED_ISNK1:
sel_field = F_LED1_DUTY;
break;
case MT6370_LED_ISNK2:
sel_field = F_LED2_DUTY;
break;
case MT6370_LED_ISNK3:
sel_field = F_LED3_DUTY;
break;
default:
sel_field = F_LED4_DUTY;
break;
}
return regmap_field_write(priv->fields[sel_field], ratio);
}
static int mt6370_set_led_freq(struct mt6370_priv *priv, unsigned int led_no, unsigned int ton,
unsigned int toff)
{
const struct mt6370_pdata *pdata = priv->pdata;
enum mt6370_led_field sel_field;
unsigned int tfreq_len = pdata->tfreq_len;
unsigned int tsum, sel;
tsum = ton + toff;
if (tsum > pdata->tfreq[0] || tsum < pdata->tfreq[tfreq_len - 1])
return -EOPNOTSUPP;
sel = find_closest_descending(tsum, pdata->tfreq, tfreq_len);
switch (led_no) {
case MT6370_LED_ISNK1:
sel_field = F_LED1_FREQ;
break;
case MT6370_LED_ISNK2:
sel_field = F_LED2_FREQ;
break;
case MT6370_LED_ISNK3:
sel_field = F_LED3_FREQ;
break;
default:
sel_field = F_LED4_FREQ;
break;
}
return regmap_field_write(priv->fields[sel_field], sel);
}
static void mt6370_get_breath_reg_base(struct mt6370_priv *priv, unsigned int led_no,
unsigned int *base)
{
const struct mt6370_pdata *pdata = priv->pdata;
if (pdata->reg_rgb_chrind_tr < 0) {
*base = pdata->reg_rgb1_tr + led_no * 3;
return;
}
switch (led_no) {
case MT6370_LED_ISNK1:
case MT6370_LED_ISNK2:
case MT6370_LED_ISNK3:
*base = pdata->reg_rgb1_tr + led_no * 3;
break;
default:
*base = pdata->reg_rgb_chrind_tr;
break;
}
}
static int mt6370_gen_breath_pattern(struct mt6370_priv *priv, struct led_pattern *pattern, u32 len,
u8 *pattern_val, u32 val_len)
{
enum mt6370_led_ranges sel_range;
struct led_pattern *curr;
unsigned int sel;
u32 val = 0;
int i;
if (len < P_MAX_PATTERNS && val_len < P_MAX_PATTERNS / 2)
return -EINVAL;
/*
* Pattern list
* tr1: byte 0, b'[7:4]
* tr2: byte 0, b'[3:0]
* tf1: byte 1, b'[7:4]
* tf2: byte 1, b'[3:0]
* ton: byte 2, b'[7:4]
* toff: byte 2, b'[3:0]
*/
for (i = 0; i < P_MAX_PATTERNS; i++) {
curr = pattern + i;
sel_range = i == P_LED_TOFF ? R_LED_TOFF : R_LED_TRFON;
linear_range_get_selector_within(priv->ranges + sel_range, curr->delta_t, &sel);
if (i % 2) {
val |= sel;
} else {
val <<= 8;
val |= sel << 4;
}
}
put_unaligned_be24(val, pattern_val);
return 0;
}
static int mt6370_set_led_mode(struct mt6370_priv *priv, unsigned int led_no,
enum mt6370_led_mode mode)
{
enum mt6370_led_field sel_field;
switch (led_no) {
case MT6370_LED_ISNK1:
sel_field = F_LED1_MODE;
break;
case MT6370_LED_ISNK2:
sel_field = F_LED2_MODE;
break;
case MT6370_LED_ISNK3:
sel_field = F_LED3_MODE;
break;
default:
sel_field = F_LED4_MODE;
break;
}
return regmap_field_write(priv->fields[sel_field], mode);
}
static int mt6370_mc_brightness_set(struct led_classdev *lcdev, enum led_brightness level)
{
struct led_classdev_mc *mccdev = lcdev_to_mccdev(lcdev);
struct mt6370_led *led = container_of(mccdev, struct mt6370_led, mc);
struct mt6370_priv *priv = led->priv;
struct mc_subled *subled;
unsigned int enable, disable;
int i, ret;
mutex_lock(&priv->lock);
led_mc_calc_color_components(mccdev, level);
ret = regmap_field_read(priv->fields[F_RGB_EN], &enable);
if (ret)
goto out_unlock;
disable = enable;
for (i = 0; i < mccdev->num_colors; i++) {
u32 brightness;
subled = mccdev->subled_info + i;
brightness = min(subled->brightness, lcdev->max_brightness);
disable &= ~MT6370_CHEN_BIT(subled->channel);
if (level == 0) {
enable &= ~MT6370_CHEN_BIT(subled->channel);
ret = mt6370_set_led_mode(priv, subled->channel, MT6370_LED_REG_MODE);
if (ret)
goto out_unlock;
continue;
}
if (brightness == 0) {
enable &= ~MT6370_CHEN_BIT(subled->channel);
continue;
}
enable |= MT6370_CHEN_BIT(subled->channel);
ret = mt6370_set_led_brightness(priv, subled->channel, brightness);
if (ret)
goto out_unlock;
}
ret = regmap_field_write(priv->fields[F_RGB_EN], disable);
if (ret)
goto out_unlock;
ret = regmap_field_write(priv->fields[F_RGB_EN], enable);
out_unlock:
mutex_unlock(&priv->lock);
return ret;
}
static int mt6370_mc_blink_set(struct led_classdev *lcdev,
unsigned long *delay_on,
unsigned long *delay_off)
{
struct led_classdev_mc *mccdev = lcdev_to_mccdev(lcdev);
struct mt6370_led *led = container_of(mccdev, struct mt6370_led, mc);
struct mt6370_priv *priv = led->priv;
struct mc_subled *subled;
unsigned int enable, disable;
int i, ret;
mutex_lock(&priv->lock);
if (!*delay_on && !*delay_off)
*delay_on = *delay_off = 500;
ret = regmap_field_read(priv->fields[F_RGB_EN], &enable);
if (ret)
goto out_unlock;
disable = enable;
for (i = 0; i < mccdev->num_colors; i++) {
subled = mccdev->subled_info + i;
disable &= ~MT6370_CHEN_BIT(subled->channel);
ret = mt6370_set_led_duty(priv, subled->channel, *delay_on, *delay_off);
if (ret)
goto out_unlock;
ret = mt6370_set_led_freq(priv, subled->channel, *delay_on, *delay_off);
if (ret)
goto out_unlock;
ret = mt6370_set_led_mode(priv, subled->channel, MT6370_LED_PWM_MODE);
if (ret)
goto out_unlock;
}
/* Toggle to make pattern timing the same */
ret = regmap_field_write(priv->fields[F_RGB_EN], disable);
if (ret)
goto out_unlock;
ret = regmap_field_write(priv->fields[F_RGB_EN], enable);
out_unlock:
mutex_unlock(&priv->lock);
return ret;
}
static int mt6370_mc_pattern_set(struct led_classdev *lcdev, struct led_pattern *pattern, u32 len,
int repeat)
{
struct led_classdev_mc *mccdev = lcdev_to_mccdev(lcdev);
struct mt6370_led *led = container_of(mccdev, struct mt6370_led, mc);
struct mt6370_priv *priv = led->priv;
struct mc_subled *subled;
unsigned int reg_base, enable, disable;
u8 params[P_MAX_PATTERNS / 2];
int i, ret;
mutex_lock(&priv->lock);
ret = mt6370_gen_breath_pattern(priv, pattern, len, params, sizeof(params));
if (ret)
goto out_unlock;
ret = regmap_field_read(priv->fields[F_RGB_EN], &enable);
if (ret)
goto out_unlock;
disable = enable;
for (i = 0; i < mccdev->num_colors; i++) {
subled = mccdev->subled_info + i;
mt6370_get_breath_reg_base(priv, subled->channel, &reg_base);
disable &= ~MT6370_CHEN_BIT(subled->channel);
ret = regmap_raw_write(priv->regmap, reg_base, params, sizeof(params));
if (ret)
goto out_unlock;
ret = mt6370_set_led_mode(priv, subled->channel, MT6370_LED_BREATH_MODE);
if (ret)
goto out_unlock;
}
/* Toggle to make pattern timing be the same */
ret = regmap_field_write(priv->fields[F_RGB_EN], disable);
if (ret)
goto out_unlock;
ret = regmap_field_write(priv->fields[F_RGB_EN], enable);
out_unlock:
mutex_unlock(&priv->lock);
return ret;
}
static inline int mt6370_mc_pattern_clear(struct led_classdev *lcdev)
{
struct led_classdev_mc *mccdev = lcdev_to_mccdev(lcdev);
struct mt6370_led *led = container_of(mccdev, struct mt6370_led, mc);
struct mt6370_priv *priv = led->priv;
struct mc_subled *subled;
int i, ret = 0;
mutex_lock(&led->priv->lock);
for (i = 0; i < mccdev->num_colors; i++) {
subled = mccdev->subled_info + i;
ret = mt6370_set_led_mode(priv, subled->channel, MT6370_LED_REG_MODE);
if (ret)
break;
}
mutex_unlock(&led->priv->lock);
return ret;
}
static int mt6370_isnk_brightness_set(struct led_classdev *lcdev,
enum led_brightness level)
{
struct mt6370_led *led = container_of(lcdev, struct mt6370_led, isink);
struct mt6370_priv *priv = led->priv;
unsigned int enable;
int ret;
mutex_lock(&priv->lock);
ret = regmap_field_read(priv->fields[F_RGB_EN], &enable);
if (ret)
goto out_unlock;
if (level == 0) {
enable &= ~MT6370_CHEN_BIT(led->index);
ret = mt6370_set_led_mode(priv, led->index, MT6370_LED_REG_MODE);
if (ret)
goto out_unlock;
} else {
enable |= MT6370_CHEN_BIT(led->index);
ret = mt6370_set_led_brightness(priv, led->index, level);
if (ret)
goto out_unlock;
}
ret = regmap_field_write(priv->fields[F_RGB_EN], enable);
out_unlock:
mutex_unlock(&priv->lock);
return ret;
}
static int mt6370_isnk_blink_set(struct led_classdev *lcdev, unsigned long *delay_on,
unsigned long *delay_off)
{
struct mt6370_led *led = container_of(lcdev, struct mt6370_led, isink);
struct mt6370_priv *priv = led->priv;
int ret;
mutex_lock(&priv->lock);
if (!*delay_on && !*delay_off)
*delay_on = *delay_off = 500;
ret = mt6370_set_led_duty(priv, led->index, *delay_on, *delay_off);
if (ret)
goto out_unlock;
ret = mt6370_set_led_freq(priv, led->index, *delay_on, *delay_off);
if (ret)
goto out_unlock;
ret = mt6370_set_led_mode(priv, led->index, MT6370_LED_PWM_MODE);
out_unlock:
mutex_unlock(&priv->lock);
return ret;
}
static int mt6370_isnk_pattern_set(struct led_classdev *lcdev, struct led_pattern *pattern, u32 len,
int repeat)
{
struct mt6370_led *led = container_of(lcdev, struct mt6370_led, isink);
struct mt6370_priv *priv = led->priv;
unsigned int reg_base;
u8 params[P_MAX_PATTERNS / 2];
int ret;
mutex_lock(&priv->lock);
ret = mt6370_gen_breath_pattern(priv, pattern, len, params, sizeof(params));
if (ret)
goto out_unlock;
mt6370_get_breath_reg_base(priv, led->index, &reg_base);
ret = regmap_raw_write(priv->regmap, reg_base, params, sizeof(params));
if (ret)
goto out_unlock;
ret = mt6370_set_led_mode(priv, led->index, MT6370_LED_BREATH_MODE);
out_unlock:
mutex_unlock(&priv->lock);
return ret;
}
static inline int mt6370_isnk_pattern_clear(struct led_classdev *lcdev)
{
struct mt6370_led *led = container_of(lcdev, struct mt6370_led, isink);
struct mt6370_priv *priv = led->priv;
int ret;
mutex_lock(&led->priv->lock);
ret = mt6370_set_led_mode(priv, led->index, MT6370_LED_REG_MODE);
mutex_unlock(&led->priv->lock);
return ret;
}
static int mt6370_assign_multicolor_info(struct device *dev, struct mt6370_led *led,
struct fwnode_handle *fwnode)
{
struct mt6370_priv *priv = led->priv;
struct fwnode_handle *child;
struct mc_subled *sub_led;
u32 num_color = 0;
int ret;
sub_led = devm_kcalloc(dev, MC_CHANNEL_NUM, sizeof(*sub_led), GFP_KERNEL);
if (!sub_led)
return -ENOMEM;
fwnode_for_each_child_node(fwnode, child) {
u32 reg, color;
ret = fwnode_property_read_u32(child, "reg", &reg);
if (ret || reg > MT6370_LED_ISNK3 || priv->leds_active & BIT(reg)) {
fwnode_handle_put(child);
return -EINVAL;
}
ret = fwnode_property_read_u32(child, "color", &color);
if (ret) {
fwnode_handle_put(child);
return dev_err_probe(dev, ret, "LED %d, no color specified\n", led->index);
}
priv->leds_active |= BIT(reg);
sub_led[num_color].color_index = color;
sub_led[num_color].channel = reg;
sub_led[num_color].intensity = 0;
num_color++;
}
if (num_color < 2)
return dev_err_probe(dev, -EINVAL,
"Multicolor must include 2 or more LED channels\n");
led->mc.num_colors = num_color;
led->mc.subled_info = sub_led;
return 0;
}
static int mt6370_init_led_properties(struct device *dev, struct mt6370_led *led,
struct led_init_data *init_data)
{
struct mt6370_priv *priv = led->priv;
struct led_classdev *lcdev;
enum mt6370_led_ranges sel_range;
u32 max_uA, max_level;
int ret;
if (led->index == MT6370_VIRTUAL_MULTICOLOR) {
ret = mt6370_assign_multicolor_info(dev, led, init_data->fwnode);
if (ret)
return ret;
lcdev = &led->mc.led_cdev;
lcdev->brightness_set_blocking = mt6370_mc_brightness_set;
lcdev->blink_set = mt6370_mc_blink_set;
lcdev->pattern_set = mt6370_mc_pattern_set;
lcdev->pattern_clear = mt6370_mc_pattern_clear;
} else {
lcdev = &led->isink;
lcdev->brightness_set_blocking = mt6370_isnk_brightness_set;
lcdev->blink_set = mt6370_isnk_blink_set;
lcdev->pattern_set = mt6370_isnk_pattern_set;
lcdev->pattern_clear = mt6370_isnk_pattern_clear;
}
ret = fwnode_property_read_u32(init_data->fwnode, "led-max-microamp", &max_uA);
if (ret) {
dev_warn(dev, "Not specified led-max-microamp, config to the minimum\n");
max_uA = 0;
}
if (led->index == MT6370_LED_ISNK4)
sel_range = R_LED4_CURR;
else
sel_range = R_LED123_CURR;
linear_range_get_selector_within(priv->ranges + sel_range, max_uA, &max_level);
lcdev->max_brightness = max_level;
led->default_state = led_init_default_state_get(init_data->fwnode);
return 0;
}
static int mt6370_isnk_init_default_state(struct mt6370_led *led)
{
struct mt6370_priv *priv = led->priv;
unsigned int enable, level;
int ret;
ret = mt6370_get_led_brightness(priv, led->index, &level);
if (ret)
return ret;
ret = regmap_field_read(priv->fields[F_RGB_EN], &enable);
if (ret)
return ret;
if (!(enable & MT6370_CHEN_BIT(led->index)))
level = 0;
switch (led->default_state) {
case LEDS_DEFSTATE_ON:
led->isink.brightness = led->isink.max_brightness;
break;
case LEDS_DEFSTATE_KEEP:
led->isink.brightness = min(level, led->isink.max_brightness);
break;
default:
led->isink.brightness = 0;
break;
}
return mt6370_isnk_brightness_set(&led->isink, led->isink.brightness);
}
static int mt6370_multicolor_led_register(struct device *dev, struct mt6370_led *led,
struct led_init_data *init_data)
{
int ret;
ret = mt6370_mc_brightness_set(&led->mc.led_cdev, 0);
if (ret)
return dev_err_probe(dev, ret, "Couldn't set multicolor brightness\n");
ret = devm_led_classdev_multicolor_register_ext(dev, &led->mc, init_data);
if (ret)
return dev_err_probe(dev, ret, "Couldn't register multicolor\n");
return 0;
}
static int mt6370_led_register(struct device *dev, struct mt6370_led *led,
struct led_init_data *init_data)
{
struct mt6370_priv *priv = led->priv;
int ret;
if (led->index == MT6370_VIRTUAL_MULTICOLOR)
return mt6370_multicolor_led_register(dev, led, init_data);
/* If ISNK4 is declared, change its mode from HW auto to SW control */
if (led->index == MT6370_LED_ISNK4) {
ret = regmap_field_write(priv->fields[F_CHGIND_EN], 1);
if (ret)
return dev_err_probe(dev, ret, "Failed to set CHRIND to SW\n");
}
ret = mt6370_isnk_init_default_state(led);
if (ret)
return dev_err_probe(dev, ret, "Failed to init %d isnk state\n", led->index);
ret = devm_led_classdev_register_ext(dev, &led->isink, init_data);
if (ret)
return dev_err_probe(dev, ret, "Couldn't register isink %d\n", led->index);
return 0;
}
static int mt6370_check_vendor_info(struct mt6370_priv *priv)
{
unsigned int devinfo, vid;
int ret;
ret = regmap_read(priv->regmap, MT6370_REG_DEV_INFO, &devinfo);
if (ret)
return ret;
vid = FIELD_GET(MT6370_VENDOR_ID_MASK, devinfo);
if (vid == MT6372_VENDOR_ID || vid == MT6372C_VENDOR_ID) {
priv->reg_fields = mt6372_reg_fields;
priv->ranges = mt6372_led_ranges;
priv->pdata = &mt6372_pdata;
} else {
/* Common for MT6370/71 */
priv->reg_fields = common_reg_fields;
priv->ranges = common_led_ranges;
priv->pdata = &common_pdata;
}
return 0;
}
static int mt6370_leds_probe(struct platform_device *pdev)
{
struct device *dev = &pdev->dev;
struct mt6370_priv *priv;
size_t count;
unsigned int i = 0;
int ret;
count = device_get_child_node_count(dev);
if (!count || count > MT6370_MAX_LEDS)
return dev_err_probe(dev, -EINVAL,
"No child node or node count over max LED number %zu\n",
count);
priv = devm_kzalloc(dev, struct_size(priv, leds, count), GFP_KERNEL);
if (!priv)
return -ENOMEM;
priv->leds_count = count;
mutex_init(&priv->lock);
priv->regmap = dev_get_regmap(dev->parent, NULL);
if (!priv->regmap)
return dev_err_probe(dev, -ENODEV, "Failed to get parent regmap\n");
ret = mt6370_check_vendor_info(priv);
if (ret)
return dev_err_probe(dev, ret, "Failed to check vendor info\n");
ret = devm_regmap_field_bulk_alloc(dev, priv->regmap, priv->fields, priv->reg_fields,
F_MAX_FIELDS);
if (ret)
return dev_err_probe(dev, ret, "Failed to allocate regmap field\n");
device_for_each_child_node_scoped(dev, child) {
struct mt6370_led *led = priv->leds + i++;
struct led_init_data init_data = { .fwnode = child };
u32 reg, color;
ret = fwnode_property_read_u32(child, "reg", &reg);
if (ret)
dev_err_probe(dev, ret, "Failed to parse reg property\n");
if (reg >= MT6370_MAX_LEDS)
return dev_err_probe(dev, -EINVAL, "Error reg property number\n");
ret = fwnode_property_read_u32(child, "color", &color);
if (ret)
return dev_err_probe(dev, ret, "Failed to parse color property\n");
if (color == LED_COLOR_ID_RGB || color == LED_COLOR_ID_MULTI)
reg = MT6370_VIRTUAL_MULTICOLOR;
if (priv->leds_active & BIT(reg))
return dev_err_probe(dev, -EINVAL, "Duplicate reg property\n");
priv->leds_active |= BIT(reg);
led->index = reg;
led->priv = priv;
ret = mt6370_init_led_properties(dev, led, &init_data);
if (ret)
return ret;
ret = mt6370_led_register(dev, led, &init_data);
if (ret)
return ret;
}
return 0;
}
static const struct of_device_id mt6370_rgbled_device_table[] = {
{ .compatible = "mediatek,mt6370-indicator" },
{}
};
MODULE_DEVICE_TABLE(of, mt6370_rgbled_device_table);
static struct platform_driver mt6370_rgbled_driver = {
.driver = {
.name = "mt6370-indicator",
.of_match_table = mt6370_rgbled_device_table,
},
.probe = mt6370_leds_probe,
};
module_platform_driver(mt6370_rgbled_driver);
MODULE_AUTHOR("Alice Chen <alice_chen@richtek.com>");
MODULE_AUTHOR("ChiYuan Huang <cy_huang@richtek.com>");
MODULE_DESCRIPTION("MediaTek MT6370 RGB LED Driver");
MODULE_LICENSE("GPL");
+270
View File
@@ -0,0 +1,270 @@
// SPDX-License-Identifier: GPL-2.0-only
/*
* NCP5623 Multi-LED Driver
*
* Author: Abdel Alkuor <alkuor@gmail.com>
* Datasheet: https://www.onsemi.com/pdf/datasheet/ncp5623-d.pdf
*/
#include <linux/i2c.h>
#include <linux/module.h>
#include <linux/led-class-multicolor.h>
#define NCP5623_FUNCTION_OFFSET 0x5
#define NCP5623_REG(x) ((x) << NCP5623_FUNCTION_OFFSET)
#define NCP5623_SHUTDOWN_REG NCP5623_REG(0x0)
#define NCP5623_ILED_REG NCP5623_REG(0x1)
#define NCP5623_PWM_REG(index) NCP5623_REG(0x2 + (index))
#define NCP5623_UPWARD_STEP_REG NCP5623_REG(0x5)
#define NCP5623_DOWNWARD_STEP_REG NCP5623_REG(0x6)
#define NCP5623_DIMMING_TIME_REG NCP5623_REG(0x7)
#define NCP5623_MAX_BRIGHTNESS 0x1f
#define NCP5623_MAX_DIM_TIME_MS 240
#define NCP5623_DIM_STEP_MS 8
struct ncp5623 {
struct i2c_client *client;
struct led_classdev_mc mc_dev;
struct mutex lock;
int current_brightness;
unsigned long delay;
};
static int ncp5623_write(struct i2c_client *client, u8 reg, u8 data)
{
return i2c_smbus_write_byte_data(client, reg | data, 0);
}
static int ncp5623_brightness_set(struct led_classdev *cdev,
enum led_brightness brightness)
{
struct led_classdev_mc *mc_cdev = lcdev_to_mccdev(cdev);
struct ncp5623 *ncp = container_of(mc_cdev, struct ncp5623, mc_dev);
int ret;
guard(mutex)(&ncp->lock);
if (ncp->delay && time_is_after_jiffies(ncp->delay))
return -EBUSY;
ncp->delay = 0;
for (int i = 0; i < mc_cdev->num_colors; i++) {
ret = ncp5623_write(ncp->client,
NCP5623_PWM_REG(mc_cdev->subled_info[i].channel),
mc_cdev->subled_info[i].intensity);
if (ret)
return ret;
}
ret = ncp5623_write(ncp->client, NCP5623_DIMMING_TIME_REG, 0);
if (ret)
return ret;
ret = ncp5623_write(ncp->client, NCP5623_ILED_REG, brightness);
if (ret)
return ret;
ncp->current_brightness = brightness;
return 0;
}
static int ncp5623_pattern_set(struct led_classdev *cdev,
struct led_pattern *pattern,
u32 len, int repeat)
{
struct led_classdev_mc *mc_cdev = lcdev_to_mccdev(cdev);
struct ncp5623 *ncp = container_of(mc_cdev, struct ncp5623, mc_dev);
int brightness_diff;
u8 reg;
int ret;
guard(mutex)(&ncp->lock);
if (ncp->delay && time_is_after_jiffies(ncp->delay))
return -EBUSY;
ncp->delay = 0;
if (pattern[0].delta_t > NCP5623_MAX_DIM_TIME_MS ||
(pattern[0].delta_t % NCP5623_DIM_STEP_MS) != 0)
return -EINVAL;
brightness_diff = pattern[0].brightness - ncp->current_brightness;
if (brightness_diff == 0)
return 0;
if (pattern[0].delta_t) {
if (brightness_diff > 0)
reg = NCP5623_UPWARD_STEP_REG;
else
reg = NCP5623_DOWNWARD_STEP_REG;
} else {
reg = NCP5623_ILED_REG;
}
ret = ncp5623_write(ncp->client, reg,
min(pattern[0].brightness, NCP5623_MAX_BRIGHTNESS));
if (ret)
return ret;
ret = ncp5623_write(ncp->client,
NCP5623_DIMMING_TIME_REG,
pattern[0].delta_t / NCP5623_DIM_STEP_MS);
if (ret)
return ret;
/*
* During testing, when the brightness difference is 1, for some
* unknown reason, the time factor it takes to change to the new
* value is the longest time possible. Otherwise, the time factor
* is simply the brightness difference.
*
* For example:
* current_brightness = 20 and new_brightness = 21 then the time it
* takes to set the new brightness increments to the maximum possible
* brightness from 20 then from 0 to 21.
* time_factor = max_brightness - 20 + 21
*/
if (abs(brightness_diff) == 1)
ncp->delay = NCP5623_MAX_BRIGHTNESS + brightness_diff;
else
ncp->delay = abs(brightness_diff);
ncp->delay = msecs_to_jiffies(ncp->delay * pattern[0].delta_t) + jiffies;
ncp->current_brightness = pattern[0].brightness;
return 0;
}
static int ncp5623_pattern_clear(struct led_classdev *led_cdev)
{
return 0;
}
static int ncp5623_probe(struct i2c_client *client)
{
struct device *dev = &client->dev;
struct fwnode_handle *mc_node, *led_node;
struct led_init_data init_data = { };
struct ncp5623 *ncp;
struct mc_subled *subled_info;
unsigned int num_subleds;
u32 color_index;
u32 reg;
int ret;
ncp = devm_kzalloc(dev, sizeof(*ncp), GFP_KERNEL);
if (!ncp)
return -ENOMEM;
ncp->client = client;
mc_node = device_get_named_child_node(dev, "multi-led");
if (!mc_node)
return -EINVAL;
num_subleds = fwnode_get_child_node_count(mc_node);
subled_info = devm_kcalloc(dev, num_subleds, sizeof(*subled_info), GFP_KERNEL);
if (!subled_info) {
ret = -ENOMEM;
goto release_mc_node;
}
fwnode_for_each_child_node(mc_node, led_node) {
ret = fwnode_property_read_u32(led_node, "color", &color_index);
if (ret)
goto release_led_node;
ret = fwnode_property_read_u32(led_node, "reg", &reg);
if (ret)
goto release_led_node;
subled_info[ncp->mc_dev.num_colors].channel = reg;
subled_info[ncp->mc_dev.num_colors].max_intensity = NCP5623_MAX_BRIGHTNESS;
subled_info[ncp->mc_dev.num_colors++].color_index = color_index;
}
init_data.fwnode = mc_node;
ncp->mc_dev.led_cdev.max_brightness = NCP5623_MAX_BRIGHTNESS;
ncp->mc_dev.subled_info = subled_info;
ncp->mc_dev.led_cdev.brightness_set_blocking = ncp5623_brightness_set;
ncp->mc_dev.led_cdev.pattern_set = ncp5623_pattern_set;
ncp->mc_dev.led_cdev.pattern_clear = ncp5623_pattern_clear;
ncp->mc_dev.led_cdev.default_trigger = "pattern";
mutex_init(&ncp->lock);
i2c_set_clientdata(client, ncp);
ret = led_classdev_multicolor_register_ext(dev, &ncp->mc_dev, &init_data);
if (ret)
goto destroy_lock;
return 0;
destroy_lock:
mutex_destroy(&ncp->lock);
release_mc_node:
fwnode_handle_put(mc_node);
return ret;
release_led_node:
fwnode_handle_put(led_node);
goto release_mc_node;
}
static void ncp5623_remove(struct i2c_client *client)
{
struct ncp5623 *ncp = i2c_get_clientdata(client);
mutex_lock(&ncp->lock);
ncp->delay = 0;
mutex_unlock(&ncp->lock);
ncp5623_write(client, NCP5623_DIMMING_TIME_REG, 0);
led_classdev_multicolor_unregister(&ncp->mc_dev);
mutex_destroy(&ncp->lock);
}
static void ncp5623_shutdown(struct i2c_client *client)
{
struct ncp5623 *ncp = i2c_get_clientdata(client);
if (!(ncp->mc_dev.led_cdev.flags & LED_RETAIN_AT_SHUTDOWN))
ncp5623_write(client, NCP5623_SHUTDOWN_REG, 0);
mutex_destroy(&ncp->lock);
}
static const struct of_device_id ncp5623_id[] = {
{ .compatible = "onnn,ncp5623" },
{ }
};
MODULE_DEVICE_TABLE(of, ncp5623_id);
static struct i2c_driver ncp5623_i2c_driver = {
.driver = {
.name = "ncp5623",
.of_match_table = ncp5623_id,
},
.probe = ncp5623_probe,
.remove = ncp5623_remove,
.shutdown = ncp5623_shutdown,
};
module_i2c_driver(ncp5623_i2c_driver);
MODULE_AUTHOR("Abdel Alkuor <alkuor@gmail.com>");
MODULE_DESCRIPTION("NCP5623 Multi-LED driver");
MODULE_LICENSE("GPL");
@@ -0,0 +1,197 @@
// 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;
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");
File diff suppressed because it is too large Load Diff
+426
View File
@@ -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");