@@ -743,7 +743,7 @@ static void __init opal_irq_init(struct device_node *dn)
static int __init opal_init(void)
{
- struct device_node *np, *consoles;
+ struct device_node *np, *consoles, *led;
int rc;
opal_node = of_find_node_by_path("/ibm,opal");
@@ -769,6 +769,13 @@ static int __init opal_init(void)
/* Create i2c platform devices */
opal_i2c_create_devs();
+ /* Create led platform devices */
+ led = of_find_node_by_path("/ibm,opal/led");
+ if (led) {
+ of_platform_device_create(led, "opal_led", NULL);
+ of_node_put(led);
+ }
+
/* Find all OPAL interrupts and request them */
opal_irq_init(opal_node);
@@ -900,3 +907,6 @@ EXPORT_SYMBOL_GPL(opal_rtc_write);
EXPORT_SYMBOL_GPL(opal_tpo_read);
EXPORT_SYMBOL_GPL(opal_tpo_write);
EXPORT_SYMBOL_GPL(opal_i2c_request);
+/* Export these symbols for PowerNV LED class driver */
+EXPORT_SYMBOL_GPL(opal_leds_get_ind);
+EXPORT_SYMBOL_GPL(opal_leds_set_ind);
@@ -508,6 +508,15 @@ config LEDS_BLINKM
This option enables support for the BlinkM RGB LED connected
through I2C. Say Y to enable support for the BlinkM LED.
+config LEDS_POWERNV
+ tristate "LED support for PowerNV Platform"
+ depends on LEDS_CLASS
+ depends on PPC_POWERNV
+ help
+ This option enables support for the system LEDs present on
+ PowerNV platforms. Say 'y' to enable this support in kernel.
+ Say 'm' enable this support as module.
+
config LEDS_SYSCON
bool "LED support for LEDs on system controllers"
depends on LEDS_CLASS=y
@@ -58,6 +58,7 @@ obj-$(CONFIG_LEDS_BLINKM) += leds-blinkm.o
obj-$(CONFIG_LEDS_SYSCON) += leds-syscon.o
obj-$(CONFIG_LEDS_VERSATILE) += leds-versatile.o
obj-$(CONFIG_LEDS_MENF21BMC) += leds-menf21bmc.o
+obj-$(CONFIG_LEDS_POWERNV) += leds-powernv.o
# LED SPI Drivers
obj-$(CONFIG_LEDS_DAC124S085) += leds-dac124s085.o
new file mode 100644
@@ -0,0 +1,599 @@
+/*
+ * PowerNV LED Driver
+ *
+ * Copyright 2015 Anshuman Khandual, IBM Corporation.
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version
+ * 2 of the License, or (at your option) any later version.
+ */
+
+#define PREFIX "POWERNV_LED"
+#define pr_fmt(fmt) PREFIX ": " fmt
+
+#include <linux/platform_device.h>
+#include <linux/leds.h>
+#include <linux/module.h>
+#include <linux/of.h>
+#include <linux/slab.h>
+#include <linux/list.h>
+
+#include <asm/opal.h>
+
+#define LED_STR_IDENT ":IDENTIFY"
+#define LED_STR_FAULT ":FAULT"
+
+/* LED operation state
+ *
+ * led_classdev_unregister resets the brightness values. However
+ * we want to retain the LED state across boot. Hence disable
+ * LED operation before calling led_classdev_unregister.
+ *
+ */
+static bool led_disabled = false;
+
+/*
+ * LED type map
+ *
+ * Converts LED event type into it's description.
+ */
+static const char *led_type_map[OPAL_SLOT_LED_TYPE_MAX] = {
+ "Identify", "Fault"
+};
+
+/*
+ * LED set routines have been implemented as work queue tasks scheduled
+ * on the global work queue. Individual task calls OPAL interface to set
+ * the LED state which might sleep for some time. A lot of work queue
+ * tasks attempting to set the LED states can prevent each other from
+ * completing their task. So there is a need to restrict the total number
+ * of work queue tasks at any point in time related to LED.
+ */
+#define MAX_LED_REQ_COUNT 100
+static int led_req_count;
+
+/*
+ * LED classdev
+ *
+ * Each LED classdev structure registered on the platform will be put into
+ * a list which will be eventually used for de-registration purpose.
+ */
+struct powernv_led {
+ struct led_classdev cdev;
+ struct list_head link;
+};
+static LIST_HEAD(powernv_led_list);
+static DEFINE_SPINLOCK(powernv_led_spinlock);
+
+/*
+ * LED set state command
+ *
+ * Each set LED state request will be saved and put into a list to be
+ * processed later by a work queue task.
+ */
+struct powernv_led_cmd {
+ struct led_classdev *led_cdev; /* Points to classdev structure */
+ enum led_brightness value; /* Brightness value */
+ u64 led_type; /* Identify or Fault */
+ struct list_head link;
+};
+static LIST_HEAD(powernv_led_cmd_list);
+static DEFINE_SPINLOCK(powernv_led_cmd_spinlock);
+
+/*
+ * LED set state task
+ *
+ * Each set LED state request added into the request list for post
+ * processing will be followed by one of this structure. The work
+ * struct here will be scheduled on the global work queue to process
+ * one of the requests from the request list. Though the order of
+ * execution is not guaraneteed.
+ */
+struct powernv_led_work {
+ struct work_struct work; /* Individual task */
+ bool deletion; /* Need deletion */
+ struct list_head link;
+};
+static LIST_HEAD(powernv_led_work_list);
+static DEFINE_SPINLOCK(powernv_led_work_spinlock);
+
+/*
+ * powernv_led_compact_work_list
+ *
+ * This function goes through the entire list of scheduled powernv_led_work
+ * nodes and removes the nodes which have already processed one set LED
+ * state request from request list and has been marked for deletion. This is
+ * essential for cleaning the list before adding new elements into it. This
+ * also tracks the total number of pending tasks. Once it reaches the
+ * threshold the function will throttle till all the scheduled tasks completes
+ * execution during which the user space thread will block and will be
+ * prevented from queuing up more LED state change requests.
+ */
+static void powernv_led_compact_work_list(void)
+{
+ struct powernv_led_work *pwork, *pwork_ne;
+ unsigned long flags;
+
+ spin_lock_irqsave(&powernv_led_work_spinlock, flags);
+ led_req_count = 0;
+ list_for_each_entry_safe(pwork, pwork_ne,
+ &powernv_led_work_list, link) {
+ if (pwork->deletion) {
+ list_del(&pwork->link);
+ kfree(pwork);
+ continue;
+ }
+ led_req_count++;
+ }
+ spin_unlock_irqrestore(&powernv_led_work_spinlock, flags);
+
+ /* Throttle if the threshold reached */
+ if (led_req_count == MAX_LED_REQ_COUNT) {
+ list_for_each_entry(pwork, &powernv_led_work_list, link)
+ flush_work(&pwork->work);
+ }
+}
+
+/*
+ * commit_led_state
+ *
+ * This commits the state change of the requested LED through an OPAL call.
+ * This function is called from work queue task context when ever it gets
+ * scheduled. This function can sleep at opal_async_wait_response call.
+ */
+static void commit_led_state(struct led_classdev *led_cdev,
+ enum led_brightness value, u64 led_type)
+{
+ char *loc_code;
+ int rc, token;
+ u64 led_mask, max_led_type, led_value = 0;
+ struct opal_msg msg;
+
+ /* Location code of the LED */
+ loc_code = kasprintf(GFP_KERNEL, "%s", led_cdev->name);
+ if (!loc_code) {
+ pr_err(PREFIX "Memory allocation failed at %s\n", __func__);
+ return;
+ }
+
+ if (led_type == OPAL_SLOT_LED_TYPE_ID)
+ loc_code[strlen(loc_code) - strlen(LED_STR_IDENT)] = '\0';
+ else if (led_type == OPAL_SLOT_LED_TYPE_FAULT)
+ loc_code[strlen(loc_code) - strlen(LED_STR_FAULT)] = '\0';
+ else /* Unknown LED type */
+ goto out_loc;
+
+ /* Prepare for the OPAL call */
+ max_led_type = cpu_to_be64(OPAL_SLOT_LED_TYPE_MAX);
+ led_mask = OPAL_SLOT_LED_STATE_ON << led_type;
+ if (value)
+ led_value = OPAL_SLOT_LED_STATE_ON << led_type;
+
+ /* OPAL async call */
+ token = opal_async_get_token_interruptible();
+ if (token < 0) {
+ if (token != -ERESTARTSYS)
+ pr_err("%s: Couldn't get the token, returning\n",
+ __func__);
+ goto out_loc;
+ }
+
+ rc = opal_leds_set_ind(token, loc_code,
+ led_mask, led_value, &max_led_type);
+ if (rc != OPAL_ASYNC_COMPLETION) {
+ pr_err("OPAL call opal_leds_set_ind failed for %s with %d\n",
+ loc_code, rc);
+ goto out_token;
+ }
+
+ rc = opal_async_wait_response(token, &msg);
+ if (rc) {
+ pr_err("%s: Failed to wait for the async response, %d\n",
+ __func__, rc);
+ goto out_token;
+ }
+
+ rc = be64_to_cpu(msg.params[1]);
+ if (rc != OPAL_SUCCESS)
+ pr_err("Async call returned with failed status: %d\n", rc);
+
+out_token:
+ opal_async_release_token(token);
+
+out_loc:
+ kfree(loc_code);
+}
+
+/*
+ * powernv_led_get
+ *
+ * This function fetches the LED state for a given LED type for
+ * mentioned LED classdev structure.
+ */
+static enum led_brightness powernv_led_get(struct led_classdev *led_cdev,
+ u64 led_type)
+{
+ char *loc_code;
+ int rc;
+ u64 led_mask, led_value, max_led_type;
+
+ /* LED location code */
+ loc_code = kasprintf(GFP_KERNEL, "%s", led_cdev->name);
+ if (!loc_code) {
+ pr_err("Memory allocation failed at: %s\n", __func__);
+ return -ENOMEM;
+ }
+
+ if (led_type == OPAL_SLOT_LED_TYPE_ID)
+ loc_code[strlen(loc_code) - strlen(LED_STR_IDENT)] = '\0';
+ else if (led_type == OPAL_SLOT_LED_TYPE_FAULT)
+ loc_code[strlen(loc_code) - strlen(LED_STR_FAULT)] = '\0';
+ else /* Unsupported LED type */
+ goto led_fail;
+
+ /* Fetch all LED status */
+ led_mask = cpu_to_be64(0);
+ led_value = cpu_to_be64(0);
+ max_led_type = cpu_to_be64(OPAL_SLOT_LED_TYPE_MAX);
+
+ rc = opal_leds_get_ind(loc_code, &led_mask, &led_value, &max_led_type);
+ if (rc != OPAL_SUCCESS && rc != OPAL_PARTIAL) {
+ pr_err("OPAL call opal_leds_get_ind failed with %d\n", rc);
+ goto led_fail;
+ }
+
+ led_mask = be64_to_cpu(led_mask);
+ led_value = be64_to_cpu(led_value);
+
+ /* LED status available */
+ if (!((led_mask >> led_type) & OPAL_SLOT_LED_STATE_ON)) {
+ pr_err("%s LED status not available for %s\n",
+ led_type_map[led_type], loc_code);
+ goto led_fail;
+ }
+
+ /* LED status value */
+ if ((led_value >> led_type) & OPAL_SLOT_LED_STATE_ON) {
+ kfree(loc_code);
+ return LED_FULL;
+ }
+
+led_fail:
+ kfree(loc_code);
+ return LED_OFF;
+}
+
+/*
+ * powernv_led_work_func
+ *
+ * This the function which will be executed by any LED work task on the
+ * global work queue. This function de-queues one of the request node
+ * from the request list, processes it and then deletes the request node.
+ * This also accesses it's own work list node and sets the deletion flag
+ * in there making itself a candidate for removal the next time the
+ * compact function gets called.
+ */
+static void powernv_led_work_func(struct work_struct *work)
+{
+ struct powernv_led_work *pwork;
+ struct powernv_led_cmd *req;
+ unsigned long flags;
+
+ /* De-queue one request, process it and then delete */
+ spin_lock_irqsave(&powernv_led_cmd_spinlock, flags);
+ if (list_empty(&powernv_led_cmd_list)) {
+ pr_err("Request list empty, but work queue task queued\n");
+ spin_unlock_irqrestore(&powernv_led_cmd_spinlock, flags);
+ return;
+ }
+
+ req = list_first_entry(&powernv_led_cmd_list,
+ struct powernv_led_cmd, link);
+ list_del(&req->link);
+ spin_unlock_irqrestore(&powernv_led_cmd_spinlock, flags);
+
+ commit_led_state(req->led_cdev, req->value, req->led_type);
+ kfree(req);
+
+ /* Mark the work queue task for deletion */
+ spin_lock_irqsave(&powernv_led_work_spinlock, flags);
+ pwork = container_of(work, struct powernv_led_work, work);
+ pwork->deletion = true;
+ spin_unlock_irqrestore(&powernv_led_work_spinlock, flags);
+}
+
+/*
+ * powernv_led_set_ident
+ *
+ * LED classdev 'brightness_set' function for identify LED types.
+ */
+static void powernv_led_set_ident(struct led_classdev *led_cdev,
+ enum led_brightness value)
+{
+ struct powernv_led_work *pwork;
+ struct powernv_led_cmd *ident;
+ unsigned long flags;
+
+ if (led_disabled)
+ return;
+
+ /* Allocate the request */
+ ident = kzalloc(sizeof(struct powernv_led_cmd), GFP_KERNEL);
+ if (!ident) {
+ pr_err("Memory allocation failed at: %s\n", __func__);
+ return;
+ }
+
+ /* Allocate the work */
+ pwork = kzalloc(sizeof(struct powernv_led_work), GFP_KERNEL);
+ if (!pwork) {
+ pr_err("Memory allocation failed at: %s\n", __func__);
+ kfree(ident);
+ return;
+ }
+
+ /* Prepare the request */
+ ident->led_cdev = led_cdev;
+ ident->value = value;
+ ident->led_type = OPAL_SLOT_LED_TYPE_ID;
+
+ /* Queue the request */
+ spin_lock_irqsave(&powernv_led_cmd_spinlock, flags);
+ list_add_tail(&ident->link, &powernv_led_cmd_list);
+ spin_unlock_irqrestore(&powernv_led_cmd_spinlock, flags);
+
+ /* Compact the work list */
+ powernv_led_compact_work_list();
+
+ /* Add the new task into the work list */
+ spin_lock_irqsave(&powernv_led_work_spinlock, flags);
+ list_add_tail(&pwork->link, &powernv_led_work_list);
+ spin_unlock_irqrestore(&powernv_led_work_spinlock, flags);
+
+ /* Schedule the new task */
+ INIT_WORK(&pwork->work, powernv_led_work_func);
+ schedule_work(&pwork->work);
+}
+
+/*
+ * powernv_led_get_ident
+ *
+ * LED classdev 'brightness_get' function for identify LED types.
+ */
+
+static enum led_brightness powernv_led_get_ident(struct led_classdev *led_cdev)
+{
+ return powernv_led_get(led_cdev, OPAL_SLOT_LED_TYPE_ID);
+}
+
+/*
+ * powernv_led_set_fault
+ *
+ * LED classdev 'brightness_set' function for fault LED types.
+ */
+static void powernv_led_set_fault(struct led_classdev *led_cdev,
+ enum led_brightness value)
+{
+ struct powernv_led_work *pwork;
+ struct powernv_led_cmd *fault;
+ unsigned long flags;
+
+ if (led_disabled)
+ return;
+
+ /* Allocate the request */
+ fault = kzalloc(sizeof(struct powernv_led_cmd), GFP_KERNEL);
+ if (!fault) {
+ pr_err("Memory allocation failed at: %s\n", __func__);
+ return;
+ }
+
+ /* Allocate the work */
+ pwork = kzalloc(sizeof(struct powernv_led_work), GFP_KERNEL);
+ if (!pwork) {
+ pr_err("Memory allocation failed at: %s\n", __func__);
+ kfree(fault);
+ return;
+ }
+
+ /* Prepare the request */
+ fault->led_cdev = led_cdev;
+ fault->value = value;
+ fault->led_type = OPAL_SLOT_LED_TYPE_FAULT;
+
+ /* Queue the request */
+ spin_lock_irqsave(&powernv_led_cmd_spinlock, flags);
+ list_add_tail(&fault->link, &powernv_led_cmd_list);
+ spin_unlock_irqrestore(&powernv_led_cmd_spinlock, flags);
+
+ /* Compact the work list */
+ powernv_led_compact_work_list();
+
+ /* Add the new task into the work list */
+ spin_lock_irqsave(&powernv_led_work_spinlock, flags);
+ list_add_tail(&pwork->link, &powernv_led_work_list);
+ spin_unlock_irqrestore(&powernv_led_work_spinlock, flags);
+
+ /* Schedule the new task */
+ INIT_WORK(&pwork->work, powernv_led_work_func);
+ schedule_work(&pwork->work);
+}
+
+/*
+ * powernv_led_get_fault
+ *
+ * LED classdev 'brightness_get' function for fault LED types.
+ */
+static enum led_brightness powernv_led_get_fault(struct led_classdev *led_cdev)
+{
+ return powernv_led_get(led_cdev, OPAL_SLOT_LED_TYPE_FAULT);
+}
+
+/*
+ * has_led_type
+ *
+ * This function verifies whether the child LED device node supports certain
+ * type of LED or not. This will be used to register LEDclassdev structures
+ * for that particual type of LED for a given device tree node.
+ */
+static bool has_led_type(struct device_node *cled, const char *led_type)
+{
+ bool result = false;
+
+ if (of_property_match_string(cled, "led-types", led_type) >= 0)
+ result = true;
+
+ return result;
+}
+
+/*
+ * power_led_classdev
+ *
+ * This function registers classdev structure for any given type of LED on
+ * a given child LED device node.
+ */
+static int power_led_classdev(struct platform_device *pdev,
+ struct device_node *cled, u64 led_type)
+{
+ int rc;
+ unsigned long flags;
+ struct powernv_led *cpled;
+
+ cpled = kzalloc(sizeof(struct powernv_led), GFP_KERNEL);
+ if (!cpled) {
+ pr_err("Memory allocation failed at %s\n", __func__);
+ return -ENOMEM;
+ }
+
+ /* Create the name for classdev */
+ if (led_type == OPAL_SLOT_LED_TYPE_ID) {
+ cpled->cdev.name = kasprintf(GFP_KERNEL, "%s%s",
+ cled->name, LED_STR_IDENT);
+ cpled->cdev.brightness_set = powernv_led_set_ident;
+ cpled->cdev.brightness_get = powernv_led_get_ident;
+ } else if (led_type == OPAL_SLOT_LED_TYPE_FAULT) {
+ cpled->cdev.name = kasprintf(GFP_KERNEL, "%s%s",
+ cled->name, LED_STR_FAULT);
+ cpled->cdev.brightness_set = powernv_led_set_fault;
+ cpled->cdev.brightness_get = powernv_led_get_fault;
+ } else { /* Unsupported LED type */
+ return -EINVAL;
+ }
+
+ if (!cpled->cdev.name) {
+ pr_err("Memory allocation failed for classdev name\n");
+ return -ENOMEM;
+ }
+
+ cpled->cdev.brightness = LED_OFF;
+ cpled->cdev.max_brightness = LED_FULL;
+ cpled->cdev.flags = LED_CORE_SUSPENDRESUME;
+
+ /* Register the classdev */
+ rc = led_classdev_register(&pdev->dev, &cpled->cdev);
+ if (rc) {
+ pr_err("Classdev registration failed for %s\n",
+ cpled->cdev.name);
+ } else {
+ spin_lock_irqsave(&powernv_led_spinlock, flags);
+ list_add_tail(&cpled->link, &powernv_led_list);
+ spin_unlock_irqrestore(&powernv_led_spinlock, flags);
+ pr_debug("Classdev registration successful for %s\n",
+ cpled->cdev.name);
+ }
+ return rc;
+}
+
+/* Platform driver probe */
+static int powernv_led_probe(struct platform_device *pdev)
+{
+ struct device_node *led, *cled;
+ int rc = 0;
+
+ led = of_find_node_by_path("/ibm,opal/led");
+ if (!led) {
+ pr_err("LED parent device node not found\n");
+ return -EINVAL;
+ }
+
+ for_each_child_of_node(led, cled) {
+ if (has_led_type(cled, "identify"))
+ rc = power_led_classdev(pdev, cled,
+ OPAL_SLOT_LED_TYPE_ID);
+
+ if (has_led_type(cled, "fault"))
+ rc = power_led_classdev(pdev, cled,
+ OPAL_SLOT_LED_TYPE_FAULT);
+ }
+ return rc;
+}
+
+/* Platform driver remove */
+static int powernv_led_remove(struct platform_device *pdev)
+{
+ struct powernv_led *pled;
+ struct powernv_led_work *pwork;
+ unsigned long flags;
+
+ /* Disable LED operation */
+ led_disabled = true;
+
+ pr_info("Unregister all classdev structures\n");
+ list_for_each_entry(pled, &powernv_led_list, link)
+ led_classdev_unregister(&pled->cdev);
+
+ pr_info("Wait for all work tasks to finish\n");
+ list_for_each_entry(pwork, &powernv_led_work_list, link)
+ flush_work(&pwork->work);
+
+ /* Free nodes from the work list */
+ powernv_led_compact_work_list();
+
+ /* Free nodes from the classdev list */
+ spin_lock_irqsave(&powernv_led_spinlock, flags);
+ while (!list_empty(&powernv_led_list)) {
+ pled = list_first_entry(&powernv_led_list,
+ struct powernv_led, link);
+ list_del(&pled->link);
+ kfree(pled);
+ }
+ spin_unlock_irqrestore(&powernv_led_spinlock, flags);
+
+ /* Check for memory leaks */
+ if (!list_empty(&powernv_led_work_list))
+ pr_warn("Work list not empty, memory leak\n");
+
+ if (!list_empty(&powernv_led_cmd_list))
+ pr_warn("Request list not empty, memory leak\n");
+
+ if (!list_empty(&powernv_led_list))
+ pr_warn("Classdev list not empty, memory leak\n");
+
+ return 0;
+}
+
+/* Platform driver property match */
+static struct of_device_id powernv_led_match[] = {
+ {
+ .compatible = "ibm,opal-v3-led",
+ },
+ {},
+};
+MODULE_DEVICE_TABLE(of, powernv_led_match);
+
+static struct platform_driver powernv_led_driver = {
+ .probe = powernv_led_probe,
+ .remove = powernv_led_remove,
+ .driver = {
+ .name = "powernv-led-driver",
+ .owner = THIS_MODULE,
+ .of_match_table = powernv_led_match,
+ },
+};
+
+module_platform_driver(powernv_led_driver);
+
+MODULE_LICENSE("GPL");
+MODULE_DESCRIPTION("PowerNV LED driver");
+MODULE_AUTHOR("Anshuman Khandual <khandual@linux.vnet.ibm.com>");