mirror of
https://mirrors.bfsu.edu.cn/git/linux.git
synced 2024-11-15 08:14:15 +08:00
e1524a6299
Add a label so that a bit of exception handling can be better reused at the end of this function implementation. Signed-off-by: Markus Elfring <elfring@users.sourceforge.net> Link: https://lore.kernel.org/r/5faec5de-fc36-4b38-abcb-c61954a824cd@web.de Signed-off-by: Lee Jones <lee@kernel.org>
272 lines
6.6 KiB
C
272 lines
6.6 KiB
C
// 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)
|
|
goto release_led_node;
|
|
|
|
ret = fwnode_property_read_u32(led_node, "reg", ®);
|
|
if (ret)
|
|
goto release_led_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;
|
|
|
|
release_led_node:
|
|
fwnode_handle_put(led_node);
|
|
goto release_mc_node;
|
|
}
|
|
|
|
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");
|