285 lines
		
	
	
		
			6.5 KiB
		
	
	
	
		
			C
		
	
	
	
	
	
			
		
		
	
	
			285 lines
		
	
	
		
			6.5 KiB
		
	
	
	
		
			C
		
	
	
	
	
	
| // 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");
 |