summaryrefslogtreecommitdiff
path: root/drivers/leds/rgb/leds-ncp5623.c
diff options
context:
space:
mode:
authorLinus Torvalds <torvalds@linux-foundation.org>2024-03-14 10:38:25 -0700
committerLinus Torvalds <torvalds@linux-foundation.org>2024-03-14 10:38:25 -0700
commitf5c31bcf604db54470868f3118a60dc4a9ba8813 (patch)
treea373d03db34cfd99ac2bd5b878c12a6e147049d1 /drivers/leds/rgb/leds-ncp5623.c
parentf3d8f29d1f59230b8c2a09e6dee7db7bd295e42c (diff)
parent45066c4bbe8ca25f9f282245b84568116c783f1d (diff)
Merge tag 'leds-next-6.9' of git://git.kernel.org/pub/scm/linux/kernel/git/lee/leds
Pull LED updates from Lee Jones: "Core Framework: - Introduce ExpressWire library New Drivers: - Add support for ON Semiconductor NCP5623 RGB LED Driver New Device Support: - Add support for PM660L to Qualcomm's LPG driver New Functionality: - Dynamically load modules required for the default-trigger - Add some support for suspend and resume - Allow LEDs to remain lit during suspend Fix-ups: - Device Tree binding adaptions/conversions/creation - Fix include lists; alphabetise, remove unused, explicitly add used - Add new led_match_default_trigger to avoid duplication - Add module alias' to aid auto-loading - Default to hw_control if no others are specified - De-bloat the supported link speed attribute lists - Remove superfluous code and simplify overall - Constify some variables Bug Fixes: - Prevent kernel panic when renaming the net interface - Fix Kconfig related build errors - Ensure mutexes are unlocked prior to destroying them - Provide clean-up between state changes to avoid invalid state - Fix some broken kernel-doc headers" * tag 'leds-next-6.9' of git://git.kernel.org/pub/scm/linux/kernel/git/lee/leds: (41 commits) leds: ncp5623: Add MS suffix to time defines leds: Add NCP5623 multi-led driver dt-bindings: leds: Add NCP5623 multi-LED Controller leds: mlxreg: Drop an excess struct mlxreg_led_data member leds: leds-mlxcpld: Fix struct mlxcpld_led_priv member name leds: lm3601x: Fix struct lm3601_led kernel-doc warnings leds: Fix ifdef check for gpio_led_register_device() dt-bindings: leds: qcom-lpg: Narrow nvmem for other variants dt-bindings: leds: qcom-lpg: Drop redundant qcom,pm8550-pwm in if:then: dt-bindings: leds: Add LED_FUNCTION_WAN_ONLINE for Internet access leds: sgm3140: Add missing timer cleanup and flash gpio control leds: expresswire: Don't depend on NEW_LEDS Revert "leds: Only descend into leds directory when CONFIG_NEW_LEDS is set" leds: aw2013: Unlock mutex before destroying it leds: qcom-lpg: Add QCOM_PBS dependency leds: rgb: leds-group-multicolor: Allow LEDs to stay on in suspend leds: trigger: netdev: Fix kernel panic on interface rename trig notify leds: qcom-lpg: Add PM660L configuration and compatible leds: spi-byte: Use devm_led_classdev_register_ext() leds: pca963x: Add support for suspend and resume ...
Diffstat (limited to 'drivers/leds/rgb/leds-ncp5623.c')
-rw-r--r--drivers/leds/rgb/leds-ncp5623.c271
1 files changed, 271 insertions, 0 deletions
diff --git a/drivers/leds/rgb/leds-ncp5623.c b/drivers/leds/rgb/leds-ncp5623.c
new file mode 100644
index 000000000000..2be4ff918516
--- /dev/null
+++ b/drivers/leds/rgb/leds-ncp5623.c
@@ -0,0 +1,271 @@
+// 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),
+ min(mc_cdev->subled_info[i].intensity,
+ NCP5623_MAX_BRIGHTNESS));
+ 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 = { };
+ int num_subleds = 0;
+ struct ncp5623 *ncp;
+ struct mc_subled *subled_info;
+ 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;
+
+ fwnode_for_each_child_node(mc_node, led_node)
+ num_subleds++;
+
+ subled_info = devm_kcalloc(dev, num_subleds, 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;
+ 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;
+}
+
+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");