| // SPDX-License-Identifier: GPL-2.0-or-later | 
 | /* | 
 |  * leds-ns2.c - Driver for the Network Space v2 (and parents) dual-GPIO LED | 
 |  * | 
 |  * Copyright (C) 2010 LaCie | 
 |  * | 
 |  * Author: Simon Guinot <sguinot@lacie.com> | 
 |  * | 
 |  * Based on leds-gpio.c by Raphael Assenat <raph@8d.com> | 
 |  */ | 
 |  | 
 | #include <linux/kernel.h> | 
 | #include <linux/platform_device.h> | 
 | #include <linux/slab.h> | 
 | #include <linux/gpio/consumer.h> | 
 | #include <linux/leds.h> | 
 | #include <linux/module.h> | 
 | #include <linux/of.h> | 
 | #include "leds.h" | 
 |  | 
 | enum ns2_led_modes { | 
 | 	NS_V2_LED_OFF, | 
 | 	NS_V2_LED_ON, | 
 | 	NS_V2_LED_SATA, | 
 | }; | 
 |  | 
 | /* | 
 |  * If the size of this structure or types of its members is changed, | 
 |  * the filling of array modval in function ns2_led_register must be changed | 
 |  * accordingly. | 
 |  */ | 
 | struct ns2_led_modval { | 
 | 	u32			mode; | 
 | 	u32			cmd_level; | 
 | 	u32			slow_level; | 
 | } __packed; | 
 |  | 
 | /* | 
 |  * The Network Space v2 dual-GPIO LED is wired to a CPLD. Three different LED | 
 |  * modes are available: off, on and SATA activity blinking. The LED modes are | 
 |  * controlled through two GPIOs (command and slow): each combination of values | 
 |  * for the command/slow GPIOs corresponds to a LED mode. | 
 |  */ | 
 |  | 
 | struct ns2_led { | 
 | 	struct led_classdev	cdev; | 
 | 	struct gpio_desc	*cmd; | 
 | 	struct gpio_desc	*slow; | 
 | 	bool			can_sleep; | 
 | 	unsigned char		sata; /* True when SATA mode active. */ | 
 | 	rwlock_t		rw_lock; /* Lock GPIOs. */ | 
 | 	int			num_modes; | 
 | 	struct ns2_led_modval	*modval; | 
 | }; | 
 |  | 
 | static int ns2_led_get_mode(struct ns2_led *led, enum ns2_led_modes *mode) | 
 | { | 
 | 	int i; | 
 | 	int cmd_level; | 
 | 	int slow_level; | 
 |  | 
 | 	cmd_level = gpiod_get_value_cansleep(led->cmd); | 
 | 	slow_level = gpiod_get_value_cansleep(led->slow); | 
 |  | 
 | 	for (i = 0; i < led->num_modes; i++) { | 
 | 		if (cmd_level == led->modval[i].cmd_level && | 
 | 		    slow_level == led->modval[i].slow_level) { | 
 | 			*mode = led->modval[i].mode; | 
 | 			return 0; | 
 | 		} | 
 | 	} | 
 |  | 
 | 	return -EINVAL; | 
 | } | 
 |  | 
 | static void ns2_led_set_mode(struct ns2_led *led, enum ns2_led_modes mode) | 
 | { | 
 | 	int i; | 
 | 	unsigned long flags; | 
 |  | 
 | 	for (i = 0; i < led->num_modes; i++) | 
 | 		if (mode == led->modval[i].mode) | 
 | 			break; | 
 |  | 
 | 	if (i == led->num_modes) | 
 | 		return; | 
 |  | 
 | 	write_lock_irqsave(&led->rw_lock, flags); | 
 |  | 
 | 	if (!led->can_sleep) { | 
 | 		gpiod_set_value(led->cmd, led->modval[i].cmd_level); | 
 | 		gpiod_set_value(led->slow, led->modval[i].slow_level); | 
 | 		goto exit_unlock; | 
 | 	} | 
 |  | 
 | 	gpiod_set_value_cansleep(led->cmd, led->modval[i].cmd_level); | 
 | 	gpiod_set_value_cansleep(led->slow, led->modval[i].slow_level); | 
 |  | 
 | exit_unlock: | 
 | 	write_unlock_irqrestore(&led->rw_lock, flags); | 
 | } | 
 |  | 
 | static void ns2_led_set(struct led_classdev *led_cdev, | 
 | 			enum led_brightness value) | 
 | { | 
 | 	struct ns2_led *led = container_of(led_cdev, struct ns2_led, cdev); | 
 | 	enum ns2_led_modes mode; | 
 |  | 
 | 	if (value == LED_OFF) | 
 | 		mode = NS_V2_LED_OFF; | 
 | 	else if (led->sata) | 
 | 		mode = NS_V2_LED_SATA; | 
 | 	else | 
 | 		mode = NS_V2_LED_ON; | 
 |  | 
 | 	ns2_led_set_mode(led, mode); | 
 | } | 
 |  | 
 | static int ns2_led_set_blocking(struct led_classdev *led_cdev, | 
 | 			enum led_brightness value) | 
 | { | 
 | 	ns2_led_set(led_cdev, value); | 
 | 	return 0; | 
 | } | 
 |  | 
 | static ssize_t ns2_led_sata_store(struct device *dev, | 
 | 				  struct device_attribute *attr, | 
 | 				  const char *buff, size_t count) | 
 | { | 
 | 	struct led_classdev *led_cdev = dev_get_drvdata(dev); | 
 | 	struct ns2_led *led = container_of(led_cdev, struct ns2_led, cdev); | 
 | 	int ret; | 
 | 	unsigned long enable; | 
 |  | 
 | 	ret = kstrtoul(buff, 10, &enable); | 
 | 	if (ret < 0) | 
 | 		return ret; | 
 |  | 
 | 	enable = !!enable; | 
 |  | 
 | 	if (led->sata == enable) | 
 | 		goto exit; | 
 |  | 
 | 	led->sata = enable; | 
 |  | 
 | 	if (!led_get_brightness(led_cdev)) | 
 | 		goto exit; | 
 |  | 
 | 	if (enable) | 
 | 		ns2_led_set_mode(led, NS_V2_LED_SATA); | 
 | 	else | 
 | 		ns2_led_set_mode(led, NS_V2_LED_ON); | 
 |  | 
 | exit: | 
 | 	return count; | 
 | } | 
 |  | 
 | static ssize_t ns2_led_sata_show(struct device *dev, | 
 | 				 struct device_attribute *attr, char *buf) | 
 | { | 
 | 	struct led_classdev *led_cdev = dev_get_drvdata(dev); | 
 | 	struct ns2_led *led = container_of(led_cdev, struct ns2_led, cdev); | 
 |  | 
 | 	return sprintf(buf, "%d\n", led->sata); | 
 | } | 
 |  | 
 | static DEVICE_ATTR(sata, 0644, ns2_led_sata_show, ns2_led_sata_store); | 
 |  | 
 | static struct attribute *ns2_led_attrs[] = { | 
 | 	&dev_attr_sata.attr, | 
 | 	NULL | 
 | }; | 
 | ATTRIBUTE_GROUPS(ns2_led); | 
 |  | 
 | static int ns2_led_register(struct device *dev, struct fwnode_handle *node, | 
 | 			    struct ns2_led *led) | 
 | { | 
 | 	struct led_init_data init_data = {}; | 
 | 	struct ns2_led_modval *modval; | 
 | 	enum ns2_led_modes mode; | 
 | 	int nmodes, ret; | 
 |  | 
 | 	led->cmd = devm_fwnode_gpiod_get_index(dev, node, "cmd", 0, GPIOD_ASIS, | 
 | 					       fwnode_get_name(node)); | 
 | 	if (IS_ERR(led->cmd)) | 
 | 		return PTR_ERR(led->cmd); | 
 |  | 
 | 	led->slow = devm_fwnode_gpiod_get_index(dev, node, "slow", 0, | 
 | 						GPIOD_ASIS, | 
 | 						fwnode_get_name(node)); | 
 | 	if (IS_ERR(led->slow)) | 
 | 		return PTR_ERR(led->slow); | 
 |  | 
 | 	ret = fwnode_property_count_u32(node, "modes-map"); | 
 | 	if (ret < 0 || ret % 3) { | 
 | 		dev_err(dev, "Missing or malformed modes-map for %pfw\n", node); | 
 | 		return -EINVAL; | 
 | 	} | 
 |  | 
 | 	nmodes = ret / 3; | 
 | 	modval = devm_kcalloc(dev, nmodes, sizeof(*modval), GFP_KERNEL); | 
 | 	if (!modval) | 
 | 		return -ENOMEM; | 
 |  | 
 | 	fwnode_property_read_u32_array(node, "modes-map", (void *)modval, | 
 | 				       nmodes * 3); | 
 |  | 
 | 	rwlock_init(&led->rw_lock); | 
 |  | 
 | 	led->cdev.blink_set = NULL; | 
 | 	led->cdev.flags |= LED_CORE_SUSPENDRESUME; | 
 | 	led->cdev.groups = ns2_led_groups; | 
 | 	led->can_sleep = gpiod_cansleep(led->cmd) || gpiod_cansleep(led->slow); | 
 | 	if (led->can_sleep) | 
 | 		led->cdev.brightness_set_blocking = ns2_led_set_blocking; | 
 | 	else | 
 | 		led->cdev.brightness_set = ns2_led_set; | 
 | 	led->num_modes = nmodes; | 
 | 	led->modval = modval; | 
 |  | 
 | 	ret = ns2_led_get_mode(led, &mode); | 
 | 	if (ret < 0) | 
 | 		return ret; | 
 |  | 
 | 	/* Set LED initial state. */ | 
 | 	led->sata = (mode == NS_V2_LED_SATA) ? 1 : 0; | 
 | 	led->cdev.brightness = (mode == NS_V2_LED_OFF) ? LED_OFF : LED_FULL; | 
 |  | 
 | 	init_data.fwnode = node; | 
 |  | 
 | 	ret = devm_led_classdev_register_ext(dev, &led->cdev, &init_data); | 
 | 	if (ret) | 
 | 		dev_err(dev, "Failed to register LED for node %pfw\n", node); | 
 |  | 
 | 	return ret; | 
 | } | 
 |  | 
 | static int ns2_led_probe(struct platform_device *pdev) | 
 | { | 
 | 	struct device *dev = &pdev->dev; | 
 | 	struct fwnode_handle *child; | 
 | 	struct ns2_led *leds; | 
 | 	int count; | 
 | 	int ret; | 
 |  | 
 | 	count = device_get_child_node_count(dev); | 
 | 	if (!count) | 
 | 		return -ENODEV; | 
 |  | 
 | 	leds = devm_kzalloc(dev, array_size(sizeof(*leds), count), GFP_KERNEL); | 
 | 	if (!leds) | 
 | 		return -ENOMEM; | 
 |  | 
 | 	device_for_each_child_node(dev, child) { | 
 | 		ret = ns2_led_register(dev, child, leds++); | 
 | 		if (ret) { | 
 | 			fwnode_handle_put(child); | 
 | 			return ret; | 
 | 		} | 
 | 	} | 
 |  | 
 | 	return 0; | 
 | } | 
 |  | 
 | static const struct of_device_id of_ns2_leds_match[] = { | 
 | 	{ .compatible = "lacie,ns2-leds", }, | 
 | 	{}, | 
 | }; | 
 | MODULE_DEVICE_TABLE(of, of_ns2_leds_match); | 
 |  | 
 | static struct platform_driver ns2_led_driver = { | 
 | 	.probe		= ns2_led_probe, | 
 | 	.driver		= { | 
 | 		.name		= "leds-ns2", | 
 | 		.of_match_table	= of_ns2_leds_match, | 
 | 	}, | 
 | }; | 
 |  | 
 | module_platform_driver(ns2_led_driver); | 
 |  | 
 | MODULE_AUTHOR("Simon Guinot <sguinot@lacie.com>"); | 
 | MODULE_DESCRIPTION("Network Space v2 LED driver"); | 
 | MODULE_LICENSE("GPL"); | 
 | MODULE_ALIAS("platform:leds-ns2"); |