[PATCH 2/2] leds: Add NCP5623 multi-led driver

From: Abdel Alkuor
Date: Sat Feb 03 2024 - 13:00:06 EST


NCP5623 is DC-DC multi-LEDs driver with 94% peak
efficiency. NCP5623 has three PWMs which can be
programmed up to 32 steps giving 32768 colors hue.

NCP5623 driver supports gradual dimming upward/downward
with programmable delay steps. Also, the driver supports
driving a single LED or multi-LED like RGB.

Signed-off-by: Abdel Alkuor <alkuor@xxxxxxxxx>
---
.../sysfs-class-led-multicolor-driver-ncp5623 | 46 +++
drivers/leds/rgb/Kconfig | 11 +
drivers/leds/rgb/Makefile | 1 +
drivers/leds/rgb/leds-ncp5623.c | 320 ++++++++++++++++++
4 files changed, 378 insertions(+)
create mode 100644 Documentation/ABI/testing/sysfs-class-led-multicolor-driver-ncp5623
create mode 100644 drivers/leds/rgb/leds-ncp5623.c

diff --git a/Documentation/ABI/testing/sysfs-class-led-multicolor-driver-ncp5623 b/Documentation/ABI/testing/sysfs-class-led-multicolor-driver-ncp5623
new file mode 100644
index 000000000000..6b9f4849852b
--- /dev/null
+++ b/Documentation/ABI/testing/sysfs-class-led-multicolor-driver-ncp5623
@@ -0,0 +1,46 @@
+What: /sys/class/leds/<led>/direction
+Date: Feb 2024
+KernelVersion: 6.8
+Contact: Abdel Alkuor <alkuor@xxxxxxxxx>
+Description:
+ Set gradual dimming direction which
+ can be either none, up, or down.
+ After setting the direction, brightness
+ can be set using one of 31 steps.
+
+ ==== ======== ==== ======== ==== ========
+ Step ILED(mA) Step ILED(mA) Step ILED(mA)
+ 0 0 11 1.38 22 3.06
+ 1 0.92 12 1.45 23 3.45
+ 2 0.95 13 1.53 24 3.94
+ 3 0.98 14 1.62 25 4.60
+ 4 1.02 15 1.72 26 5.52
+ 5 1.06 16 1.84 27 6.90
+ 6 1.10 17 1.97 28 9.20
+ 7 1.15 18 2.12 29 13.80
+ 8 1.20 19 2.30 30 27.60
+ 9 1.25 20 2.50 31 27.60
+ 10 1.31 21 2.76
+ ==== ======== ==== ======== ==== ========
+
+What: /sys/class/leds/<led>/dim_step
+Date: Feb 2024
+KernelVersion: 6.8
+Contact: Abdel Alkuor <alkuor@xxxxxxxxx>
+Description:
+ Set gradual dimming time.
+
+ ==== ======== ==== ======== ==== ========
+ Step Time(ms) Step Time(ms) Step Time(ms)
+ 0 0 11 88 22 176
+ 1 8 12 96 23 184
+ 2 16 13 104 24 192
+ 3 24 14 112 25 200
+ 4 32 15 120 26 208
+ 5 40 16 128 27 216
+ 6 48 17 136 28 224
+ 7 56 18 144 29 232
+ 8 64 19 152 30 240
+ 9 72 20 160 31 248
+ 10 80 21 168
+ ==== ======== ==== ======== ==== ========
diff --git a/drivers/leds/rgb/Kconfig b/drivers/leds/rgb/Kconfig
index a6a21f564673..81ab6a526a78 100644
--- a/drivers/leds/rgb/Kconfig
+++ b/drivers/leds/rgb/Kconfig
@@ -27,6 +27,17 @@ config LEDS_KTD202X
To compile this driver as a module, choose M here: the module
will be called leds-ktd202x.

+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
diff --git a/drivers/leds/rgb/Makefile b/drivers/leds/rgb/Makefile
index 243f31e4d70d..a501fd27f179 100644
--- a/drivers/leds/rgb/Makefile
+++ b/drivers/leds/rgb/Makefile
@@ -2,6 +2,7 @@

obj-$(CONFIG_LEDS_GROUP_MULTICOLOR) += leds-group-multicolor.o
obj-$(CONFIG_LEDS_KTD202X) += leds-ktd202x.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_MT6370_RGB) += leds-mt6370-rgb.o
diff --git a/drivers/leds/rgb/leds-ncp5623.c b/drivers/leds/rgb/leds-ncp5623.c
new file mode 100644
index 000000000000..e77dfca69ca3
--- /dev/null
+++ b/drivers/leds/rgb/leds-ncp5623.c
@@ -0,0 +1,320 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * leds-ncp5623.c - Multi-LED Driver
+ *
+ * Author: Abdel Alkuor <alkuor@xxxxxxxxx>
+ * Datasheet: https://www.onsemi.com/pdf/datasheet/ncp5623-d.pdf
+ */
+
+#include <linux/i2c.h>
+#include <linux/module.h>
+#include <linux/sysfs.h>
+#include <linux/workqueue.h>
+
+#include <linux/led-class-multicolor.h>
+
+#define NCP5623_REG(x) ((x) << 0x5)
+
+#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
+
+enum {
+ NCP5623_DIR_NONE,
+ NCP5623_DIR_UPWARD,
+ NCP5623_DIR_DOWNWARD,
+};
+
+static const char *const directions[] = {
+ [NCP5623_DIR_NONE] = "none",
+ [NCP5623_DIR_UPWARD] = "up",
+ [NCP5623_DIR_DOWNWARD] = "down",
+};
+
+struct ncp5623 {
+ struct i2c_client *client;
+ struct led_classdev_mc mc_dev;
+ struct mutex lock;
+
+ u8 direction;
+ u8 dim_step;
+ u8 old_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 inline unsigned long
+ncp5623_get_completion_steps(u8 dir, int brightness, int old_brightness)
+{
+ int diff = brightness - old_brightness;
+
+ if (dir == NCP5623_DIR_UPWARD)
+ return diff <= 1 ? NCP5623_MAX_BRIGHTNESS + diff : diff;
+
+ return diff >= -1 ? NCP5623_MAX_BRIGHTNESS - diff : -diff;
+
+}
+
+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;
+ u8 reg;
+ int i;
+
+ guard(mutex)(&ncp->lock);
+
+ if (ncp->delay && time_is_after_jiffies(ncp->delay))
+ return -EBUSY;
+
+ ncp->delay = 0;
+
+ for (i = 0; i < mc_cdev->num_colors; i++) {
+ ret = ncp5623_write(ncp->client,
+ NCP5623_PWM_REG(mc_cdev->subled_info[i].channel),
+ min(mc_cdev->subled_info[i].intensity,
+ NCP5623_MAX_BRIGHTNESS));
+ if (ret)
+ return ret;
+ }
+
+ switch (ncp->direction) {
+ case NCP5623_DIR_NONE:
+ reg = NCP5623_ILED_REG;
+ break;
+ case NCP5623_DIR_UPWARD:
+ reg = NCP5623_UPWARD_STEP_REG;
+ break;
+ case NCP5623_DIR_DOWNWARD:
+ reg = NCP5623_DOWNWARD_STEP_REG;
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ ret = ncp5623_write(ncp->client, reg, brightness);
+ if (ret)
+ return ret;
+
+ if ((ncp->direction != NCP5623_DIR_NONE) && ncp->dim_step) {
+ ret = ncp5623_write(ncp->client,
+ NCP5623_DIMMING_TIME_REG, ncp->dim_step);
+ if (ret)
+ return ret;
+
+ ncp->delay = ncp5623_get_completion_steps(ncp->direction,
+ brightness,
+ ncp->old_brightness);
+ /* dim step resolution is 8ms */
+ ncp->delay *= ncp->dim_step * 8;
+ ncp->delay = msecs_to_jiffies(ncp->delay) + jiffies;
+ }
+
+ ncp->old_brightness = brightness;
+
+ return 0;
+}
+
+static ssize_t dim_step_store(struct device *dev,
+ struct device_attribute *attr,
+ const char *buf, size_t count)
+{
+ struct led_classdev_mc *mc_cdev = lcdev_to_mccdev(dev_get_drvdata(dev));
+ struct ncp5623 *ncp = container_of(mc_cdev, struct ncp5623, mc_dev);
+ u8 value;
+ int ret;
+
+ ret = kstrtou8(buf, 0, &value);
+ if (ret)
+ return ret;
+
+ if (value > 0x1f)
+ return -EINVAL;
+
+ guard(mutex)(&ncp->lock);
+ ncp->dim_step = value;
+
+ return count;
+}
+
+static ssize_t dim_step_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ struct led_classdev_mc *mc_cdev = lcdev_to_mccdev(dev_get_drvdata(dev));
+ struct ncp5623 *ncp = container_of(mc_cdev, struct ncp5623, mc_dev);
+
+ guard(mutex)(&ncp->lock);
+
+ return sysfs_emit(buf, "%u\n", ncp->dim_step);
+}
+
+static ssize_t direction_store(struct device *dev,
+ struct device_attribute *attr,
+ const char *buf, size_t count)
+{
+ struct led_classdev_mc *mc_cdev = lcdev_to_mccdev(dev_get_drvdata(dev));
+ struct ncp5623 *ncp = container_of(mc_cdev, struct ncp5623, mc_dev);
+ int ret;
+
+ ret = __sysfs_match_string(directions, ARRAY_SIZE(directions), buf);
+
+ switch (ret) {
+ case NCP5623_DIR_NONE:
+ case NCP5623_DIR_UPWARD:
+ case NCP5623_DIR_DOWNWARD:
+ guard(mutex)(&ncp->lock);
+ ncp->direction = ret;
+ return count;
+ default:
+ return -EINVAL;
+ }
+}
+
+static ssize_t direction_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ struct led_classdev_mc *mc_cdev = lcdev_to_mccdev(dev_get_drvdata(dev));
+ struct ncp5623 *ncp = container_of(mc_cdev, struct ncp5623, mc_dev);
+
+ guard(mutex)(&ncp->lock);
+
+ return sysfs_emit(buf, "%s\n", directions[ncp->direction]);
+}
+
+
+static DEVICE_ATTR_RW(direction);
+static DEVICE_ATTR_RW(dim_step);
+
+static struct attribute *ncp5623_led_attrs[] = {
+ &dev_attr_direction.attr,
+ &dev_attr_dim_step.attr,
+ NULL
+};
+
+static const struct attribute_group ncp5623_led_group = {
+ .attrs = ncp5623_led_attrs,
+};
+
+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;
+ u32 color_index;
+ u32 reg;
+ int count = 0;
+ 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;
+
+ fwnode_for_each_child_node(mc_node, led_node)
+ count++;
+
+ subled_info = devm_kcalloc(dev, count, sizeof(*subled_info), GFP_KERNEL);
+ if (!subled_info) {
+ ret = -ENOMEM;
+ goto release_mc_node;
+ }
+
+ fwnode_for_each_available_child_node(mc_node, led_node) {
+ ret = fwnode_property_read_u32(led_node, "color", &color_index);
+ if (ret) {
+ fwnode_handle_put(led_node);
+ goto release_mc_node;
+ }
+
+ ret = fwnode_property_read_u32(led_node, "reg", &reg);
+ if (ret) {
+ fwnode_handle_put(led_node);
+ goto release_mc_node;
+ }
+
+ subled_info[ncp->mc_dev.num_colors].channel = reg;
+ 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;
+
+ mutex_init(&ncp->lock);
+ i2c_set_clientdata(client, ncp);
+
+ ret = devm_led_classdev_multicolor_register_ext(dev, &ncp->mc_dev, &init_data);
+ if (ret)
+ goto destroy_lock;
+
+ ret = sysfs_update_group(&ncp->mc_dev.led_cdev.dev->kobj, &ncp5623_led_group);
+ if (ret)
+ goto destroy_lock;
+
+ fwnode_handle_put(mc_node);
+
+ return 0;
+
+destroy_lock:
+ mutex_destroy(&ncp->lock);
+
+release_mc_node:
+ fwnode_handle_put(mc_node);
+
+ return ret;
+}
+
+static void ncp5623_remove(struct i2c_client *client)
+{
+ struct ncp5623 *ncp = i2c_get_clientdata(client);
+
+ ncp5623_write(client, NCP5623_SHUTDOWN_REG, 0);
+ mutex_destroy(&ncp->lock);
+}
+
+static void ncp5623_shutdown(struct i2c_client *client)
+{
+ ncp5623_write(client, NCP5623_SHUTDOWN_REG, 0);
+}
+
+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@xxxxxxxxx>");
+MODULE_DESCRIPTION("NCP5623 Multi-LED driver");
+MODULE_LICENSE("GPL");
--
2.34.1