| // 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", ®); |
| 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"); |