#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;
struct mutex lock;
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;
}
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;
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;
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;
if (repeat != -1)
return -EINVAL;
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) {
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;
}
}
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);
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;
}
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);
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);
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);
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;
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 },
{ },
};
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 },
{ },
};
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");