427 lines
12 KiB
C
427 lines
12 KiB
C
// 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");
|