| /* | 
 |  * Copyright (c) 2017 Sebastian Reichel <sre@kernel.org> | 
 |  * | 
 |  * This program is free software; you can redistribute it and/or modify | 
 |  * it under the terms of the GNU General Public License version 2 or | 
 |  * later as published by the Free Software Foundation. | 
 |  * | 
 |  * This program is distributed in the hope that it will be useful, | 
 |  * but WITHOUT ANY WARRANTY; without even the implied warranty of | 
 |  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | 
 |  * GNU General Public License for more details. | 
 |  */ | 
 |  | 
 | #include <linux/leds.h> | 
 | #include <linux/mfd/motorola-cpcap.h> | 
 | #include <linux/module.h> | 
 | #include <linux/mutex.h> | 
 | #include <linux/of_device.h> | 
 | #include <linux/platform_device.h> | 
 | #include <linux/regmap.h> | 
 | #include <linux/regulator/consumer.h> | 
 |  | 
 | #define CPCAP_LED_NO_CURRENT 0x0001 | 
 |  | 
 | struct cpcap_led_info { | 
 | 	u16 reg; | 
 | 	u16 mask; | 
 | 	u16 limit; | 
 | 	u16 init_mask; | 
 | 	u16 init_val; | 
 | }; | 
 |  | 
 | static const struct cpcap_led_info cpcap_led_red = { | 
 | 	.reg	= CPCAP_REG_REDC, | 
 | 	.mask	= 0x03FF, | 
 | 	.limit	= 31, | 
 | }; | 
 |  | 
 | static const struct cpcap_led_info cpcap_led_green = { | 
 | 	.reg	= CPCAP_REG_GREENC, | 
 | 	.mask	= 0x03FF, | 
 | 	.limit	= 31, | 
 | }; | 
 |  | 
 | static const struct cpcap_led_info cpcap_led_blue = { | 
 | 	.reg	= CPCAP_REG_BLUEC, | 
 | 	.mask	= 0x03FF, | 
 | 	.limit	= 31, | 
 | }; | 
 |  | 
 | /* aux display light */ | 
 | static const struct cpcap_led_info cpcap_led_adl = { | 
 | 	.reg		= CPCAP_REG_ADLC, | 
 | 	.mask		= 0x000F, | 
 | 	.limit		= 1, | 
 | 	.init_mask	= 0x7FFF, | 
 | 	.init_val	= 0x5FF0, | 
 | }; | 
 |  | 
 | /* camera privacy led */ | 
 | static const struct cpcap_led_info cpcap_led_cp = { | 
 | 	.reg		= CPCAP_REG_CLEDC, | 
 | 	.mask		= 0x0007, | 
 | 	.limit		= 1, | 
 | 	.init_mask	= 0x03FF, | 
 | 	.init_val	= 0x0008, | 
 | }; | 
 |  | 
 | struct cpcap_led { | 
 | 	struct led_classdev led; | 
 | 	const struct cpcap_led_info *info; | 
 | 	struct device *dev; | 
 | 	struct regmap *regmap; | 
 | 	struct mutex update_lock; | 
 | 	struct regulator *vdd; | 
 | 	bool powered; | 
 |  | 
 | 	u32 current_limit; | 
 | }; | 
 |  | 
 | static u16 cpcap_led_val(u8 current_limit, u8 duty_cycle) | 
 | { | 
 | 	current_limit &= 0x1f; /* 5 bit */ | 
 | 	duty_cycle &= 0x0f; /* 4 bit */ | 
 |  | 
 | 	return current_limit << 4 | duty_cycle; | 
 | } | 
 |  | 
 | static int cpcap_led_set_power(struct cpcap_led *led, bool status) | 
 | { | 
 | 	int err; | 
 |  | 
 | 	if (status == led->powered) | 
 | 		return 0; | 
 |  | 
 | 	if (status) | 
 | 		err = regulator_enable(led->vdd); | 
 | 	else | 
 | 		err = regulator_disable(led->vdd); | 
 |  | 
 | 	if (err) { | 
 | 		dev_err(led->dev, "regulator failure: %d", err); | 
 | 		return err; | 
 | 	} | 
 |  | 
 | 	led->powered = status; | 
 |  | 
 | 	return 0; | 
 | } | 
 |  | 
 | static int cpcap_led_set(struct led_classdev *ledc, enum led_brightness value) | 
 | { | 
 | 	struct cpcap_led *led = container_of(ledc, struct cpcap_led, led); | 
 | 	int brightness; | 
 | 	int err; | 
 |  | 
 | 	mutex_lock(&led->update_lock); | 
 |  | 
 | 	if (value > LED_OFF) { | 
 | 		err = cpcap_led_set_power(led, true); | 
 | 		if (err) | 
 | 			goto exit; | 
 | 	} | 
 |  | 
 | 	if (value == LED_OFF) { | 
 | 		/* Avoid HW issue by turning off current before duty cycle */ | 
 | 		err = regmap_update_bits(led->regmap, | 
 | 			led->info->reg, led->info->mask, CPCAP_LED_NO_CURRENT); | 
 | 		if (err) { | 
 | 			dev_err(led->dev, "regmap failed: %d", err); | 
 | 			goto exit; | 
 | 		} | 
 |  | 
 | 		brightness = cpcap_led_val(value, LED_OFF); | 
 | 	} else { | 
 | 		brightness = cpcap_led_val(value, LED_ON); | 
 | 	} | 
 |  | 
 | 	err = regmap_update_bits(led->regmap, led->info->reg, led->info->mask, | 
 | 		brightness); | 
 | 	if (err) { | 
 | 		dev_err(led->dev, "regmap failed: %d", err); | 
 | 		goto exit; | 
 | 	} | 
 |  | 
 | 	if (value == LED_OFF) { | 
 | 		err = cpcap_led_set_power(led, false); | 
 | 		if (err) | 
 | 			goto exit; | 
 | 	} | 
 |  | 
 | exit: | 
 | 	mutex_unlock(&led->update_lock); | 
 | 	return err; | 
 | } | 
 |  | 
 | static const struct of_device_id cpcap_led_of_match[] = { | 
 | 	{ .compatible = "motorola,cpcap-led-red", .data = &cpcap_led_red }, | 
 | 	{ .compatible = "motorola,cpcap-led-green", .data = &cpcap_led_green }, | 
 | 	{ .compatible = "motorola,cpcap-led-blue",  .data = &cpcap_led_blue }, | 
 | 	{ .compatible = "motorola,cpcap-led-adl", .data = &cpcap_led_adl }, | 
 | 	{ .compatible = "motorola,cpcap-led-cp", .data = &cpcap_led_cp }, | 
 | 	{}, | 
 | }; | 
 | MODULE_DEVICE_TABLE(of, cpcap_led_of_match); | 
 |  | 
 | static int cpcap_led_probe(struct platform_device *pdev) | 
 | { | 
 | 	const struct of_device_id *match; | 
 | 	struct cpcap_led *led; | 
 | 	int err; | 
 |  | 
 | 	match = of_match_device(of_match_ptr(cpcap_led_of_match), &pdev->dev); | 
 | 	if (!match || !match->data) | 
 | 		return -EINVAL; | 
 |  | 
 | 	led = devm_kzalloc(&pdev->dev, sizeof(*led), GFP_KERNEL); | 
 | 	if (!led) | 
 | 		return -ENOMEM; | 
 | 	platform_set_drvdata(pdev, led); | 
 | 	led->info = match->data; | 
 | 	led->dev = &pdev->dev; | 
 |  | 
 | 	if (led->info->reg == 0x0000) { | 
 | 		dev_err(led->dev, "Unsupported LED"); | 
 | 		return -ENODEV; | 
 | 	} | 
 |  | 
 | 	led->regmap = dev_get_regmap(pdev->dev.parent, NULL); | 
 | 	if (!led->regmap) | 
 | 		return -ENODEV; | 
 |  | 
 | 	led->vdd = devm_regulator_get(&pdev->dev, "vdd"); | 
 | 	if (IS_ERR(led->vdd)) { | 
 | 		err = PTR_ERR(led->vdd); | 
 | 		dev_err(led->dev, "Couldn't get regulator: %d", err); | 
 | 		return err; | 
 | 	} | 
 |  | 
 | 	err = device_property_read_string(&pdev->dev, "label", &led->led.name); | 
 | 	if (err) { | 
 | 		dev_err(led->dev, "Couldn't read LED label: %d", err); | 
 | 		return err; | 
 | 	} | 
 |  | 
 | 	if (led->info->init_mask) { | 
 | 		err = regmap_update_bits(led->regmap, led->info->reg, | 
 | 			led->info->init_mask, led->info->init_val); | 
 | 		if (err) { | 
 | 			dev_err(led->dev, "regmap failed: %d", err); | 
 | 			return err; | 
 | 		} | 
 | 	} | 
 |  | 
 | 	mutex_init(&led->update_lock); | 
 |  | 
 | 	led->led.max_brightness = led->info->limit; | 
 | 	led->led.brightness_set_blocking = cpcap_led_set; | 
 | 	err = devm_led_classdev_register(&pdev->dev, &led->led); | 
 | 	if (err) { | 
 | 		dev_err(led->dev, "Couldn't register LED: %d", err); | 
 | 		return err; | 
 | 	} | 
 |  | 
 | 	return 0; | 
 | } | 
 |  | 
 | static struct platform_driver cpcap_led_driver = { | 
 | 	.probe = cpcap_led_probe, | 
 | 	.driver = { | 
 | 		.name = "cpcap-led", | 
 | 		.of_match_table = cpcap_led_of_match, | 
 | 	}, | 
 | }; | 
 | module_platform_driver(cpcap_led_driver); | 
 |  | 
 | MODULE_DESCRIPTION("CPCAP LED driver"); | 
 | MODULE_AUTHOR("Sebastian Reichel <sre@kernel.org>"); | 
 | MODULE_LICENSE("GPL"); |