Contributors: 1
Author Tokens Token Proportion Commits Commit Proportion
Kaustabh Chakraborty 1775 100.00% 1 100.00%
Total 1775 1


// SPDX-License-Identifier: GPL-2.0
/*
 * RGB LED Driver for Samsung S2M series PMICs.
 *
 * Copyright (c) 2015 Samsung Electronics Co., Ltd
 * Copyright (c) 2026 Kaustabh Chakraborty <kauschluss@disroot.org>
 */

#include <linux/container_of.h>
#include <linux/led-class-multicolor.h>
#include <linux/mfd/samsung/core.h>
#include <linux/mfd/samsung/s2mu005.h>
#include <linux/minmax.h>
#include <linux/module.h>
#include <linux/mutex.h>
#include <linux/of.h>
#include <linux/platform_device.h>
#include <linux/regmap.h>

struct s2m_rgb {
	struct device *dev;
	struct regmap *regmap;
	struct led_classdev_mc mc;
	/*
	 * The mutex object prevents race conditions when evaluation and
	 * application of LED pattern state.
	 */
	struct mutex lock;
	/*
	 * State variables representing the current LED pattern, these only to
	 * be accessed when lock is held.
	 */
	u8 ramp_up;
	u8 ramp_dn;
	u8 stay_hi;
	u8 stay_lo;
};

static struct led_classdev_mc *to_s2m_mc(struct led_classdev *cdev)
{
	return container_of(cdev, struct led_classdev_mc, led_cdev);
}

static struct s2m_rgb *to_s2m_rgb(struct led_classdev_mc *mc)
{
	return container_of(mc, struct s2m_rgb, mc);
}

static const u32 s2mu005_rgb_lut_ramp[] = {
	0,	100,	200,	300,	400,	500,	600,	700,
	800,	1000,	1200,	1400,	1600,	1800,	2000,	2200,
};

static const u32 s2mu005_rgb_lut_stay_hi[] = {
	100,	200,	300,	400,	500,	750,	1000,	1250,
	1500,	1750,	2000,	2250,	2500,	2750,	3000,	3250,
};

static const u32 s2mu005_rgb_lut_stay_lo[] = {
	0,	500,	1000,	1500,	2000,	2500,	3000,	3500,
	4000,	4500,	5000,	6000,	7000,	8000,	10000,	12000,
};

static int s2mu005_rgb_apply_params(struct s2m_rgb *rgb)
{
	struct regmap *regmap = rgb->regmap;
	unsigned int ramp_val = 0;
	unsigned int stay_val = 0;
	int ret;

	ramp_val |= FIELD_PREP(S2MU005_RGB_CH_RAMP_UP, rgb->ramp_up);
	ramp_val |= FIELD_PREP(S2MU005_RGB_CH_RAMP_DN, rgb->ramp_dn);

	stay_val |= FIELD_PREP(S2MU005_RGB_CH_STAY_HI, rgb->stay_hi);
	stay_val |= FIELD_PREP(S2MU005_RGB_CH_STAY_LO, rgb->stay_lo);

	ret = regmap_write(regmap, S2MU005_REG_RGB_EN, S2MU005_RGB_RESET);
	if (ret) {
		dev_err(rgb->dev, "failed to reset RGB LEDs\n");
		return ret;
	}

	for (int i = 0; i < rgb->mc.num_colors; i++) {
		ret = regmap_write(regmap, S2MU005_REG_RGB_CH_CTRL(i),
				   rgb->mc.subled_info[i].brightness);
		if (ret) {
			dev_err(rgb->dev, "failed to set LED brightness\n");
			return ret;
		}

		ret = regmap_write(regmap, S2MU005_REG_RGB_CH_RAMP(i), ramp_val);
		if (ret) {
			dev_err(rgb->dev, "failed to set ramp timings\n");
			return ret;
		}

		ret = regmap_write(regmap, S2MU005_REG_RGB_CH_STAY(i), stay_val);
		if (ret) {
			dev_err(rgb->dev, "failed to set stay timings\n");
			return ret;
		}
	}

	ret = regmap_write(regmap, S2MU005_REG_RGB_EN, S2MU005_RGB_SLOPE_SMOOTH);
	if (ret) {
		dev_err(rgb->dev, "failed to set ramp slope\n");
		return ret;
	}

	return 0;
}

static int s2mu005_rgb_reset_params(struct s2m_rgb *rgb)
{
	struct regmap *regmap = rgb->regmap;
	int ret;

	ret = regmap_write(regmap, S2MU005_REG_RGB_EN, S2MU005_RGB_RESET);
	if (ret) {
		dev_err(rgb->dev, "failed to reset RGB LEDs\n");
		return ret;
	}

	rgb->ramp_up = 0;
	rgb->ramp_dn = 0;
	rgb->stay_hi = 0;
	rgb->stay_lo = 0;

	return 0;
}

/*
 * s2m_rgb_lut_get_closest_duration - find closest duration in look-up table
 * @lut: the look-up table to search for the closest timing
 * @len: number of elements in the look-up table array
 * @duration: the timing duration requested
 *
 * This function does a binary search on the given array, and finds the closest
 * value to the requested timing. It is expected that the look-up table to be
 * provided, is already sorted.
 *
 * This function returns a negative error code, or a non-negative index of the
 * value in the look-up table closest to the one requested.
 */
static int s2m_rgb_lut_get_closest_duration(const u32 *lut, const size_t len, const u32 duration)
{
	u32 closest_distance = abs(duration - lut[0]);
	int closest_index = 0;
	int lo = 0;
	int hi = len - 1;

	/*
	 * Allow a small amount of extrapolation beyond the highest timing value.
	 *
	 * Consider x and y to be the two last values in the table, and x < y.
	 * Since (y - x) / 2 integers, in the range [x + (y - x) / 2, y)
	 * returns y as the closest, allow extrapolation for the succeeding
	 * (y - x) / 2 integers as well, viz, up to (y, y + (y - x) / 2].
	 * Anything beyond that is invalid.
	 */
	if (len >= 2 && duration > lut[len - 1] + (lut[len - 1] - lut[len - 2]) / 2)
		return -EINVAL;

	while (lo <= hi) {
		int mid = lo + (hi - lo) / 2;

		/* Narrow down search window as per binary-search algorithm. */
		if (duration < lut[mid])
			hi = mid - 1;
		else
			lo = mid + 1;

		if (abs(duration - lut[mid]) < closest_distance) {
			closest_distance = abs(duration - lut[mid]);
			closest_index = mid;
		}
	}

	return closest_index;
}

static int s2m_rgb_pattern_set(struct led_classdev *cdev, struct led_pattern *pattern, u32 len,
			       int repeat)
{
	struct s2m_rgb *rgb = to_s2m_rgb(to_s2m_mc(cdev));
	const u32 *lut_ramp_up, *lut_ramp_dn, *lut_stay_hi, *lut_stay_lo;
	size_t lut_ramp_up_len, lut_ramp_dn_len, lut_stay_hi_len, lut_stay_lo_len;
	int brightness_peak = 0;
	u32 time_hi = 0, time_lo = 0;
	bool ramp_up_en = false, ramp_dn_en = false;
	int ret;

	/*
	 * The typical pattern supported by this device can be represented with
	 * the following graph:
	 *
	 *  255 T ''''''-.                         .-'''''''-.
	 *      |         '.                     .'           '.
	 *      |           \                   /               \
	 *      |            '.               .'                 '.
	 *      |              '-...........-'                     '-
	 *    0 +----------------------------------------------------> time (s)
	 *
	 *       <---- HIGH ----><-- LOW --><-------- HIGH --------->
	 *       <-----><-------><---------><-------><-----><------->
	 *       stay_hi ramp_dn   stay_lo   ramp_up stay_hi ramp_dn
	 *
	 * There are two states, named HIGH and LOW. HIGH has a non-zero
	 * brightness level, while LOW is of zero brightness. The pattern
	 * provided should mention only one zero and non-zero brightness level.
	 * The hardware always starts the pattern from the HIGH state, as shown
	 * in the graph.
	 *
	 * The HIGH state can be divided in three somewhat equal timings:
	 * ramp_up, stay_hi, and ramp_dn. The LOW state has only one timing:
	 * stay_lo.
	 */

	/* Only indefinitely looping patterns are supported. */
	if (repeat != -1)
		return -EINVAL;

	/* Pattern should consist of at least two tuples. */
	if (len < 2)
		return -EINVAL;

	for (int i = 0; i < len; i++) {
		int brightness = pattern[i].brightness;
		u32 delta_t = pattern[i].delta_t;

		if (brightness) {
			/*
			 * The pattern should define only one non-zero
			 * brightness in the HIGH state. The device doesn't
			 * have any provisions to handle multiple peak
			 * brightness levels.
			 */
			if (brightness_peak && brightness_peak != brightness)
				return -EINVAL;

			brightness_peak = brightness;
			time_hi += delta_t;
			ramp_dn_en = !!delta_t;
		} else {
			time_lo += delta_t;
			ramp_up_en = !!delta_t;
		}
	}

	/* LUTs are specific to device variant. */
	lut_ramp_up = s2mu005_rgb_lut_ramp;
	lut_ramp_up_len = ARRAY_SIZE(s2mu005_rgb_lut_ramp);
	lut_ramp_dn = s2mu005_rgb_lut_ramp;
	lut_ramp_dn_len = ARRAY_SIZE(s2mu005_rgb_lut_ramp);
	lut_stay_hi = s2mu005_rgb_lut_stay_hi;
	lut_stay_hi_len = ARRAY_SIZE(s2mu005_rgb_lut_stay_hi);
	lut_stay_lo = s2mu005_rgb_lut_stay_lo;
	lut_stay_lo_len = ARRAY_SIZE(s2mu005_rgb_lut_stay_lo);

	mutex_lock(&rgb->lock);

	/*
	 * The timings ramp_up, stay_hi, and ramp_dn of the HIGH state are
	 * roughly equal. Firstly, calculate and set timings for ramp_up and
	 * ramp_dn (making sure they're exactly equal).
	 */
	rgb->ramp_up = 0;
	rgb->ramp_dn = 0;

	if (ramp_up_en) {
		ret = s2m_rgb_lut_get_closest_duration(lut_ramp_up, lut_ramp_up_len, time_hi / 3);
		if (ret < 0)
			goto param_fail;
		rgb->ramp_up = (u8)ret;
	}

	if (ramp_dn_en) {
		ret = s2m_rgb_lut_get_closest_duration(lut_ramp_dn, lut_ramp_dn_len, time_hi / 3);
		if (ret < 0)
			goto param_fail;
		rgb->ramp_dn = (u8)ret;
	}

	/*
	 * Subtract the allocated ramp timings from time_hi (and also making
	 * sure it doesn't underflow!). The remaining time is allocated to
	 * stay_hi.
	 */
	time_hi -= min(time_hi, lut_ramp_up[rgb->ramp_up]);
	time_hi -= min(time_hi, lut_ramp_dn[rgb->ramp_dn]);

	ret = s2m_rgb_lut_get_closest_duration(lut_stay_hi, lut_stay_hi_len, time_hi);
	if (ret < 0)
		goto param_fail;
	rgb->stay_hi = (u8)ret;

	ret = s2m_rgb_lut_get_closest_duration(lut_stay_lo, lut_stay_lo_len, time_lo);
	if (ret < 0)
		goto param_fail;
	rgb->stay_lo = (u8)ret;

	led_mc_calc_color_components(&rgb->mc, brightness_peak);
	/* Apply params with variant-specific implementation. */
	ret = s2mu005_rgb_apply_params(rgb);
	if (ret)
		goto param_fail;

	mutex_unlock(&rgb->lock);

	return 0;

param_fail:
	rgb->ramp_up = 0;
	rgb->ramp_dn = 0;
	rgb->stay_hi = 0;
	rgb->stay_lo = 0;

	mutex_unlock(&rgb->lock);

	return ret;
}

static int s2m_rgb_pattern_clear(struct led_classdev *cdev)
{
	struct s2m_rgb *rgb = to_s2m_rgb(to_s2m_mc(cdev));
	int ret = 0;

	mutex_lock(&rgb->lock);

	/* Reset params with variant-specific implementation. */
	ret = s2mu005_rgb_reset_params(rgb);

	mutex_unlock(&rgb->lock);

	return ret;
}

static int s2m_rgb_brightness_set(struct led_classdev *cdev, enum led_brightness value)
{
	struct s2m_rgb *rgb = to_s2m_rgb(to_s2m_mc(cdev));
	int ret = 0;

	if (!value)
		return s2m_rgb_pattern_clear(cdev);

	mutex_lock(&rgb->lock);

	led_mc_calc_color_components(&rgb->mc, value);
	/* Apply params with variant-specific implementation. */
	ret = s2mu005_rgb_apply_params(rgb);

	mutex_unlock(&rgb->lock);

	return ret;
}

static const struct mc_subled s2mu005_rgb_subled_info[] = {
	{ .channel = 0, .color_index = LED_COLOR_ID_BLUE },
	{ .channel = 1, .color_index = LED_COLOR_ID_GREEN },
	{ .channel = 2, .color_index = LED_COLOR_ID_RED },
};

static int s2m_rgb_probe(struct platform_device *pdev)
{
	struct device *dev = &pdev->dev;
	struct sec_pmic_dev *pmic_drvdata = dev_get_drvdata(dev->parent);
	struct s2m_rgb *rgb;
	struct led_init_data init_data = {};
	int ret;

	rgb = devm_kzalloc(dev, sizeof(*rgb), GFP_KERNEL);
	if (!rgb)
		return -ENOMEM;

	platform_set_drvdata(pdev, rgb);
	rgb->dev = dev;
	rgb->regmap = pmic_drvdata->regmap_pmic;

	/* Configure variant-specific details. */
	rgb->mc.num_colors = ARRAY_SIZE(s2mu005_rgb_subled_info);
	rgb->mc.subled_info = devm_kmemdup(dev, s2mu005_rgb_subled_info,
					   sizeof(s2mu005_rgb_subled_info), GFP_KERNEL);
	if (!rgb->mc.subled_info)
		return -ENOMEM;

	rgb->mc.led_cdev.max_brightness = 255;
	rgb->mc.led_cdev.brightness_set_blocking = s2m_rgb_brightness_set;
	rgb->mc.led_cdev.pattern_set = s2m_rgb_pattern_set;
	rgb->mc.led_cdev.pattern_clear = s2m_rgb_pattern_clear;

	ret = devm_mutex_init(dev, &rgb->lock);
	if (ret)
		return dev_err_probe(dev, ret, "failed to create mutex lock\n");

	init_data.fwnode = of_fwnode_handle(dev->of_node);
	ret = devm_led_classdev_multicolor_register_ext(dev, &rgb->mc, &init_data);
	if (ret)
		return dev_err_probe(dev, ret, "failed to create LED device\n");

	return 0;
}

static const struct platform_device_id s2m_rgb_id_table[] = {
	{ "s2mu005-rgb", S2MU005 },
	{ /* sentinel */ },
};
MODULE_DEVICE_TABLE(platform, s2m_rgb_id_table);

static const struct of_device_id s2m_rgb_of_match_table[] = {
	{ .compatible = "samsung,s2mu005-rgb", .data = (void *)S2MU005 },
	{ /* sentinel */ },
};
MODULE_DEVICE_TABLE(of, s2m_rgb_of_match_table);

static struct platform_driver s2m_rgb_driver = {
	.driver = {
		.name = "s2m-rgb",
	},
	.probe = s2m_rgb_probe,
	.id_table = s2m_rgb_id_table,
};
module_platform_driver(s2m_rgb_driver);

MODULE_DESCRIPTION("RGB LED Driver for Samsung S2M Series PMICs");
MODULE_AUTHOR("Kaustabh Chakraborty <kauschluss@disroot.org>");
MODULE_LICENSE("GPL");