leds: pca963x: Add multicolor LED class support

Allow grouping of individual PCA963x PWM channels into a single
multicolor LED device by adding support for the LED multicolor class.

A child node with sub-children is treated as a multicolor group,
others are treated as single leds, keeping full backwards compatibility.

Signed-off-by: Loic Poulain <loic.poulain@oss.qualcomm.com>
Link: https://patch.msgid.link/20260727-monza-leds-v8-3-6e7e93d44dba@oss.qualcomm.com
Signed-off-by: Lee Jones <lee@kernel.org>
This commit is contained in:
Loic Poulain
2026-08-06 15:13:41 +01:00
committed by Lee Jones
parent 160c78e69c
commit cd5d8dcd4a
2 changed files with 150 additions and 44 deletions
+1
View File
@@ -596,6 +596,7 @@ config LEDS_PCA963X
tristate "LED support for PCA963x I2C chip"
depends on LEDS_CLASS
depends on I2C
select LEDS_CLASS_MULTICOLOR
help
This option enables support for LEDs connected to the PCA963x
LED driver chip accessed via the I2C bus. Supported
+149 -44
View File
@@ -27,6 +27,7 @@
#include <linux/string.h>
#include <linux/ctype.h>
#include <linux/leds.h>
#include <linux/led-class-multicolor.h>
#include <linux/err.h>
#include <linux/i2c.h>
#include <linux/property.h>
@@ -101,8 +102,11 @@ struct pca963x;
struct pca963x_led {
struct pca963x *chip;
struct led_classdev led_cdev;
struct led_classdev_mc mc_cdev;
struct mc_subled subleds[4];
int led_num; /* 0 .. 15 potentially */
bool blinking;
bool is_mc;
u8 gdc;
u8 gfrq;
};
@@ -115,7 +119,7 @@ struct pca963x {
struct pca963x_led leds[];
};
static int pca963x_brightness(struct pca963x_led *led,
static int pca963x_brightness(struct pca963x_led *led, unsigned int led_num,
enum led_brightness brightness)
{
struct i2c_client *client = led->chip->client;
@@ -124,8 +128,8 @@ static int pca963x_brightness(struct pca963x_led *led,
int shift;
int ret;
ledout_addr = chipdef->ledout_base + (led->led_num / 4);
shift = 2 * (led->led_num % 4);
ledout_addr = chipdef->ledout_base + (led_num / 4);
shift = 2 * (led_num % 4);
mask = 0x3 << shift;
ledout = i2c_smbus_read_byte_data(client, ledout_addr);
@@ -135,7 +139,7 @@ static int pca963x_brightness(struct pca963x_led *led,
val = (ledout & ~mask) | (PCA963X_LED_GRP_PWM << shift);
ret = i2c_smbus_write_byte_data(client,
PCA963X_PWM_BASE +
led->led_num,
led_num,
LED_FULL);
} else {
val = (ledout & ~mask) | (PCA963X_LED_ON << shift);
@@ -150,7 +154,7 @@ static int pca963x_brightness(struct pca963x_led *led,
default:
ret = i2c_smbus_write_byte_data(client,
PCA963X_PWM_BASE +
led->led_num,
led_num,
brightness);
if (ret < 0)
return ret;
@@ -199,20 +203,24 @@ static void pca963x_blink(struct pca963x_led *led)
led->blinking = true;
}
static int pca963x_power_state(struct pca963x_led *led)
static void pca963x_track_power_state(struct pca963x_led *led, unsigned int led_num,
enum led_brightness brightness)
{
unsigned long *leds_on = &led->chip->leds_on;
if (brightness)
set_bit(led_num, leds_on);
else
clear_bit(led_num, leds_on);
}
static int pca963x_sync_power_state(struct pca963x_led *led, unsigned long cached_leds)
{
struct i2c_client *client = led->chip->client;
unsigned long *leds_on = &led->chip->leds_on;
unsigned long cached_leds = *leds_on;
if (led->led_cdev.brightness)
set_bit(led->led_num, leds_on);
else
clear_bit(led->led_num, leds_on);
if (!(*leds_on) != !cached_leds)
if (!led->chip->leds_on != !cached_leds)
return i2c_smbus_write_byte_data(client, PCA963X_MODE1,
*leds_on ? 0 : BIT(4));
led->chip->leds_on ? 0 : BIT(4));
return 0;
}
@@ -221,22 +229,60 @@ static int pca963x_led_set(struct led_classdev *led_cdev,
enum led_brightness value)
{
struct pca963x_led *led;
unsigned long cached_leds;
int ret;
led = container_of(led_cdev, struct pca963x_led, led_cdev);
mutex_lock(&led->chip->mutex);
ret = pca963x_brightness(led, value);
if (ret < 0)
cached_leds = led->chip->leds_on;
ret = pca963x_brightness(led, led->led_num, value);
if (ret)
goto unlock;
ret = pca963x_power_state(led);
pca963x_track_power_state(led, led->led_num, value);
ret = pca963x_sync_power_state(led, cached_leds);
unlock:
mutex_unlock(&led->chip->mutex);
return ret;
}
static int pca963x_led_mc_set(struct led_classdev *led_cdev,
enum led_brightness value)
{
struct led_classdev_mc *mc_cdev = lcdev_to_mccdev(led_cdev);
struct pca963x_led *led = container_of(mc_cdev, struct pca963x_led, mc_cdev);
unsigned long cached_leds;
int ret = 0, sync_ret;
led_mc_calc_color_components(mc_cdev, value);
guard(mutex)(&led->chip->mutex);
cached_leds = led->chip->leds_on;
for (unsigned int i = 0; i < mc_cdev->num_colors; i++) {
unsigned int channel = mc_cdev->subled_info[i].channel;
ret = pca963x_brightness(led, channel,
mc_cdev->subled_info[i].brightness);
if (ret)
break;
pca963x_track_power_state(led, channel,
mc_cdev->subled_info[i].brightness);
}
/*
* Some channels may already have been updated before the error, so
* still sync the global on/off state to reflect what actually changed.
*/
sync_ret = pca963x_sync_power_state(led, cached_leds);
return ret ? : sync_ret;
}
static unsigned int pca963x_period_scale(struct pca963x_led *led,
unsigned int val)
{
@@ -300,6 +346,81 @@ static int pca963x_blink_set(struct led_classdev *led_cdev,
return 0;
}
static int pca963x_parse_mc_subleds(struct device *dev, struct pca963x_led *led,
struct fwnode_handle *fwnode,
const struct pca963x_chipdef *chipdef)
{
unsigned int num_colors = 0;
int ret;
fwnode_for_each_child_node_scoped(fwnode, sub) {
u32 color, subreg;
if (num_colors >= ARRAY_SIZE(led->subleds))
return dev_err_probe(dev, -EINVAL, "Too many LEDs for node %pfw\n", fwnode);
ret = fwnode_property_read_u32(sub, "reg", &subreg);
if (ret)
return dev_err_probe(dev, ret, "Missing 'reg' for sub-LED %pfw\n", sub);
if (subreg >= chipdef->n_leds)
return dev_err_probe(dev, -EINVAL, "Invalid 'reg' for sub-LED %pfw\n", sub);
ret = fwnode_property_read_u32(sub, "color", &color);
if (ret)
return dev_err_probe(dev, ret, "Missing 'color' for sub-LED %pfw\n", sub);
led->subleds[num_colors].channel = subreg;
led->subleds[num_colors].color_index = color;
led->subleds[num_colors].intensity = LED_FULL;
num_colors++;
}
led->mc_cdev.subled_info = led->subleds;
led->mc_cdev.num_colors = num_colors;
led->mc_cdev.led_cdev.max_brightness = LED_FULL;
led->mc_cdev.led_cdev.brightness_set_blocking = pca963x_led_mc_set;
return 0;
}
static int pca963x_register_led(struct device *dev, struct pca963x_led *led,
u32 reg, struct fwnode_handle *fwnode,
const struct pca963x_chipdef *chipdef,
bool hw_blink)
{
struct i2c_client *client = led->chip->client;
struct led_init_data init_data = {};
char label[32];
int ret;
led->led_num = reg;
/* A node with sub-children groups several channels into a multicolor LED. */
led->is_mc = fwnode_get_child_node_count(fwnode) > 0;
if (led->is_mc) {
ret = pca963x_parse_mc_subleds(dev, led, fwnode, chipdef);
if (ret)
return ret;
} else {
led->led_cdev.brightness_set_blocking = pca963x_led_set;
if (hw_blink)
led->led_cdev.blink_set = pca963x_blink_set;
}
init_data.fwnode = fwnode;
/* Keep the legacy device name to preserve existing sysfs LED names. */
init_data.devicename = "pca963x";
snprintf(label, sizeof(label), "%d:%.2x:%u", client->adapter->nr, client->addr, reg);
init_data.default_label = label;
if (led->is_mc)
return devm_led_classdev_multicolor_register_ext(dev, &led->mc_cdev,
&init_data);
return devm_led_classdev_register_ext(dev, &led->led_cdev, &init_data);
}
static int pca963x_register_leds(struct i2c_client *client,
struct pca963x *chip)
{
@@ -338,37 +459,21 @@ static int pca963x_register_leds(struct i2c_client *client,
return ret;
device_for_each_child_node_scoped(dev, child) {
struct led_init_data init_data = {};
char default_label[32];
ret = fwnode_property_read_u32(child, "reg", &reg);
if (ret || reg >= chipdef->n_leds) {
dev_err(dev, "Invalid 'reg' property for node %pfw\n",
child);
return -EINVAL;
}
if (ret)
return dev_err_probe(dev, ret,
"Missing 'reg' property for node %pfw\n", child);
if (reg >= chipdef->n_leds)
return dev_err_probe(dev, -EINVAL,
"Invalid 'reg' property for node %pfw\n", child);
led->led_num = reg;
led->chip = chip;
led->led_cdev.brightness_set_blocking = pca963x_led_set;
if (hw_blink)
led->led_cdev.blink_set = pca963x_blink_set;
led->blinking = false;
init_data.fwnode = child;
/* for backwards compatibility */
init_data.devicename = "pca963x";
snprintf(default_label, sizeof(default_label), "%d:%.2x:%u",
client->adapter->nr, client->addr, reg);
init_data.default_label = default_label;
ret = devm_led_classdev_register_ext(dev, &led->led_cdev,
&init_data);
if (ret) {
dev_err(dev, "Failed to register LED for node %pfw\n",
child);
return ret;
}
ret = pca963x_register_led(dev, led, reg, child, chipdef, hw_blink);
if (ret)
return dev_err_probe(dev, ret, "Failed to register LED for node %pfw\n",
child);
++led;
}