| 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");
Information contained on this website is for historical information purposes only and does not indicate or represent copyright ownership.
Created with Cregit http://github.com/cregit/cregit
Version 2.0-RC1