Contributors: 1
Author Tokens Token Proportion Commits Commit Proportion
Abdel Alkuor 1169 100.00% 2 100.00%
Total 1169 2


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