[PATCH] Add support for PCIe SSD status LED management

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

 



This patch adds support for the PCIe SSD Status LED Management
interface, as described in the "_DSM Additions for PCIe SSD Status LED
Management" ECN to the PCI Firmware Specification revision 3.2.

It will add a single (led_classdev) LED for any PCIe device that has the
relevant _DSM. The ten possible status states are exposed using
attributes current_states and supported_states. Reading current_states
(and supported_states) will show the definition and value of each bit:

>cat /sys/class/leds/0000:88:00.0::pcie_ssd_status/supported_states
ok                              0x0004 [ ]
locate                          0x0008 [*]
fail                            0x0010 [ ]
rebuild                         0x0020 [ ]
pfa                             0x0040 [ ]
hotspare                        0x0080 [ ]
criticalarray                   0x0100 [ ]
failedarray                     0x0200 [ ]
invaliddevice                   0x0400 [ ]
disabled                        0x0800 [ ]
--
supported_states = 0x0008

>cat /sys/class/leds/0000:88:00.0::pcie_ssd_status/current_states
locate                          0x0008 [ ]
--
current_states = 0x0000

Signed-off-by: Stuart Hayes <stuart.w.hayes@xxxxxxxxx>
---
 .../sysfs-class-led-driver-pcie-ssd-leds      |  30 ++
 drivers/pci/Kconfig                           |  12 +
 drivers/pci/Makefile                          |   1 +
 drivers/pci/pcie-ssd-leds.c                   | 421 ++++++++++++++++++
 4 files changed, 464 insertions(+)
 create mode 100644 Documentation/ABI/testing/sysfs-class-led-driver-pcie-ssd-leds
 create mode 100644 drivers/pci/pcie-ssd-leds.c

diff --git a/Documentation/ABI/testing/sysfs-class-led-driver-pcie-ssd-leds b/Documentation/ABI/testing/sysfs-class-led-driver-pcie-ssd-leds
new file mode 100644
index 000000000000..856f5a078568
--- /dev/null
+++ b/Documentation/ABI/testing/sysfs-class-led-driver-pcie-ssd-leds
@@ -0,0 +1,30 @@
+What:		/sys/class/leds/<led>/supported_states
+Date:		April 2021
+Contact:	linux-pci@xxxxxxxxxxxxxxx
+Description:
+		This attribute indicates the status states supported by this
+		display.  Reading will show a verbose output with the value on
+		the final line (example below), writing only takes a single
+		numerical value.
+
+		>cat /sys/class/leds/0000:88:00.0::pcie_ssd_status/supported_states
+		ok                              0x0004 [ ]
+		locate                          0x0008 [*]
+		fail                            0x0010 [ ]
+		rebuild                         0x0020 [ ]
+		pfa                             0x0040 [ ]
+		hotspare                        0x0080 [ ]
+		criticalarray                   0x0100 [ ]
+		failedarray                     0x0200 [ ]
+		invaliddevice                   0x0400 [ ]
+		disabled                        0x0800 [ ]
+		--
+		supported_states = 0x0008
+
+What:		/sys/class/leds/<led>/current_states
+Date:		April 2021
+Contact:	linux-pci@xxxxxxxxxxxxxxx
+Description:
+		This attribute indicates the currently active status states on
+		this display.  Reading will show a verbose output with the value
+		the final line, writing only takes a single numerical value.
diff --git a/drivers/pci/Kconfig b/drivers/pci/Kconfig
index 0c473d75e625..f4acf1ad0fb5 100644
--- a/drivers/pci/Kconfig
+++ b/drivers/pci/Kconfig
@@ -190,6 +190,18 @@ config PCI_HYPERV
 	  The PCI device frontend driver allows the kernel to import arbitrary
 	  PCI devices from a PCI backend to support PCI driver domains.
 
+config PCIE_SSD_LEDS
+	tristate "PCIe SSD status LED support"
+	depends on ACPI && NEW_LEDS
+	help
+	  Driver for PCIe SSD status LED management as described in a PCI
+	  Firmware Specification, Revision 3.2 ECN.
+
+	  When enabled, an LED interface will be created for each PCIe device
+	  that has the ACPI method described in the referenced specification,
+	  to allow the device status LEDs for that PCIe device (presumably a
+	  solid state storage device or its slot) to be seen and controlled.
+
 choice
 	prompt "PCI Express hierarchy optimization setting"
 	default PCIE_BUS_DEFAULT
diff --git a/drivers/pci/Makefile b/drivers/pci/Makefile
index d62c4ac4ae1b..ea0a0f351ad2 100644
--- a/drivers/pci/Makefile
+++ b/drivers/pci/Makefile
@@ -29,6 +29,7 @@ obj-$(CONFIG_PCI_PF_STUB)	+= pci-pf-stub.o
 obj-$(CONFIG_PCI_ECAM)		+= ecam.o
 obj-$(CONFIG_PCI_P2PDMA)	+= p2pdma.o
 obj-$(CONFIG_XEN_PCIDEV_FRONTEND) += xen-pcifront.o
+obj-$(CONFIG_PCIE_SSD_LEDS)	+= pcie-ssd-leds.o
 
 # Endpoint library must be initialized before its users
 obj-$(CONFIG_PCI_ENDPOINT)	+= endpoint/
diff --git a/drivers/pci/pcie-ssd-leds.c b/drivers/pci/pcie-ssd-leds.c
new file mode 100644
index 000000000000..cd00b7dd1a8e
--- /dev/null
+++ b/drivers/pci/pcie-ssd-leds.c
@@ -0,0 +1,421 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Module to expose interface to control PCIe SSD LEDs as defined in the
+ * "_DSM additions for PCIe SSD Status LED Management" ECN to the PCI
+ * Firmware Specification Revision 3.2, dated 12 February 2020.
+ *
+ *  Copyright (c) 2021 Dell Inc.
+ */
+
+#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
+
+#include <linux/acpi.h>
+#include <linux/leds.h>
+#include <linux/module.h>
+#include <linux/mutex.h>
+#include <linux/pci.h>
+#include <uapi/linux/uleds.h>
+
+#define DRIVER_NAME	"pcie-ssd-leds"
+#define DRIVER_VERSION	"v1.0"
+
+struct led_state {
+	char *name;
+	u32 mask;
+};
+
+static struct led_state led_states[] = {
+	{ .name = "ok",			.mask = 1 << 2 },
+	{ .name = "locate",		.mask = 1 << 3 },
+	{ .name = "fail",		.mask = 1 << 4 },
+	{ .name = "rebuild",		.mask = 1 << 5 },
+	{ .name = "pfa",		.mask = 1 << 6 },
+	{ .name = "hotspare",		.mask = 1 << 7 },
+	{ .name = "criticalarray",	.mask = 1 << 8 },
+	{ .name = "failedarray",	.mask = 1 << 9 },
+	{ .name = "invaliddevice",	.mask = 1 << 10 },
+	{ .name = "disabled",		.mask = 1 << 11 },
+};
+
+/*
+ * ssd_status_dev could be the SSD's PCIe dev or its hotplug slot
+ */
+struct ssd_status_dev {
+	struct list_head ssd_list;
+	struct pci_dev *pdev;
+	u32 supported_states;
+	struct led_classdev led_cdev;
+	enum led_brightness brightness;
+};
+
+struct mutex ssd_list_lock;
+struct list_head ssd_list;
+
+const guid_t pcie_ssdleds_dsm_guid =
+	GUID_INIT(0x5d524d9d, 0xfff9, 0x4d4b,
+		  0x8c, 0xb7, 0x74, 0x7e, 0xd5, 0x1e, 0x19, 0x4d);
+
+#define GET_SUPPORTED_STATES_DSM	0x01
+#define GET_STATE_DSM			0x02
+#define SET_STATE_DSM			0x03
+
+struct pci_ssdleds_dsm_output {
+	u16 status;
+	u8 function_specific_err;
+	u8 vendor_specific_err;
+	u32 state;
+};
+
+static void dsm_status_err_print(struct device *dev,
+			     struct pci_ssdleds_dsm_output *output)
+{
+	switch (output->status) {
+	case 0:
+		break;
+	case 1:
+		dev_dbg(dev, "_DSM not supported\n");
+		break;
+	case 2:
+		dev_dbg(dev, "_DSM invalid input parameters\n");
+		break;
+	case 3:
+		dev_dbg(dev, "_DSM communication error\n");
+		break;
+	case 4:
+		dev_dbg(dev, "_DSM function-specific error 0x%x\n",
+			output->function_specific_err);
+		break;
+	case 5:
+		dev_dbg(dev, "_DSM vendor-specific error 0x%x\n",
+			output->vendor_specific_err);
+		break;
+	default:
+		dev_dbg(dev, "_DSM returned unknown status 0x%x\n",
+			output->status);
+	}
+}
+
+static int dsm_set(struct device *dev, u32 value)
+{
+	acpi_handle handle;
+	union acpi_object *out_obj, arg3[2];
+	struct pci_ssdleds_dsm_output *dsm_output;
+
+	handle = ACPI_HANDLE(dev);
+	if (!handle)
+		return -ENODEV;
+
+	arg3[0].type = ACPI_TYPE_PACKAGE;
+	arg3[0].package.count = 1;
+	arg3[0].package.elements = &arg3[1];
+
+	arg3[1].type = ACPI_TYPE_BUFFER;
+	arg3[1].buffer.length = 4;
+	arg3[1].buffer.pointer = (u8 *)&value;
+
+	out_obj = acpi_evaluate_dsm_typed(handle, &pcie_ssdleds_dsm_guid,
+				1, SET_STATE_DSM, &arg3[0], ACPI_TYPE_BUFFER);
+	if (!out_obj)
+		return -EIO;
+
+	if (out_obj->buffer.length < 8) {
+		ACPI_FREE(out_obj);
+		return -EIO;
+	}
+
+	dsm_output = (struct pci_ssdleds_dsm_output *)out_obj->buffer.pointer;
+
+	if (dsm_output->status != 0) {
+		dsm_status_err_print(dev, dsm_output);
+		ACPI_FREE(out_obj);
+		return -EIO;
+	}
+	ACPI_FREE(out_obj);
+	return 0;
+}
+
+static int dsm_get(struct device *dev, u64 dsm_func, u32 *output)
+{
+	acpi_handle handle;
+	union acpi_object *out_obj;
+	struct pci_ssdleds_dsm_output *dsm_output;
+
+	handle = ACPI_HANDLE(dev);
+	if (!handle)
+		return -ENODEV;
+
+	out_obj = acpi_evaluate_dsm_typed(handle, &pcie_ssdleds_dsm_guid, 0x1,
+					  dsm_func, NULL, ACPI_TYPE_BUFFER);
+	if (!out_obj)
+		return -EIO;
+
+	if (out_obj->buffer.length < 8) {
+		ACPI_FREE(out_obj);
+		return -EIO;
+	}
+
+	dsm_output = (struct pci_ssdleds_dsm_output *)out_obj->buffer.pointer;
+	if (dsm_output->status != 0) {
+		dsm_status_err_print(dev, dsm_output);
+		ACPI_FREE(out_obj);
+		return -EIO;
+	}
+
+	*output = dsm_output->state;
+	ACPI_FREE(out_obj);
+	return 0;
+}
+
+static bool pdev_has_dsm(struct pci_dev *pdev)
+{
+	acpi_handle handle;
+
+	handle = ACPI_HANDLE(&pdev->dev);
+	if (!handle)
+		return false;
+
+	return !!acpi_check_dsm(handle, &pcie_ssdleds_dsm_guid, 0x1,
+		1 << GET_SUPPORTED_STATES_DSM ||
+		1 << GET_STATE_DSM ||
+		1 << SET_STATE_DSM);
+}
+
+static ssize_t current_states_show(struct device *dev,
+				   struct device_attribute *attr, char *buf)
+{
+	struct led_classdev *led_cdev = dev_get_drvdata(dev);
+	struct device *dsm_dev = led_cdev->dev->parent;
+	struct ssd_status_dev *ssd;
+	u32 val;
+	int err, i, result = 0;
+
+	ssd = container_of(led_cdev, struct ssd_status_dev, led_cdev);
+
+	err = dsm_get(dsm_dev, GET_STATE_DSM, &val);
+	if (err < 0)
+		return err;
+	for (i = 0; i < ARRAY_SIZE(led_states); i++)
+		if (led_states[i].mask & ssd->supported_states)
+			result += sprintf(buf + result, "%-25s\t0x%04X [%c]\n",
+					  led_states[i].name,
+					  led_states[i].mask,
+					  (val & led_states[i].mask)
+					  ? '*' : ' ');
+
+	result += sprintf(buf + result, "--\ncurrent_states = 0x%04X\n", val);
+	return result;
+}
+
+static ssize_t current_states_store(struct device *dev,
+				    struct device_attribute *attr,
+				    const char *buf, size_t size)
+{
+	struct led_classdev *led_cdev = dev_get_drvdata(dev);
+	struct device *dsm_dev = led_cdev->dev->parent;
+	struct ssd_status_dev *ssd;
+	u32 val;
+	int err;
+
+	ssd = container_of(led_cdev, struct ssd_status_dev, led_cdev);
+
+	err = kstrtou32(buf, 10, &val);
+	if (err)
+		return err;
+
+	val &= ssd->supported_states;
+	if (val)
+		ssd->brightness = LED_ON;
+	err = dsm_set(dsm_dev, val);
+	if (err < 0)
+		return err;
+
+	return size;
+}
+
+static ssize_t supported_states_show(struct device *dev,
+				     struct device_attribute *attr, char *buf)
+{
+	struct led_classdev *led_cdev = dev_get_drvdata(dev);
+	struct ssd_status_dev *ssd;
+	int i, result = 0;
+
+	ssd = container_of(led_cdev, struct ssd_status_dev, led_cdev);
+
+	for (i = 0; i < ARRAY_SIZE(led_states); i++)
+		result += sprintf(buf + result, "%-25s\t0x%04X [%c]\n",
+				  led_states[i].name,
+				  led_states[i].mask,
+				  (ssd->supported_states & led_states[i].mask)
+				  ? '*' : ' ');
+
+	result += sprintf(buf + result, "--\nsupported_states = 0x%04X\n",
+			  ssd->supported_states);
+	return result;
+}
+
+static DEVICE_ATTR_RW(current_states);
+static DEVICE_ATTR_RO(supported_states);
+
+static struct attribute *pcie_ssd_status_attrs[] = {
+	&dev_attr_current_states.attr,
+	&dev_attr_supported_states.attr,
+	NULL
+};
+
+ATTRIBUTE_GROUPS(pcie_ssd_status);
+
+static int ssdleds_set_brightness(struct led_classdev *led_cdev,
+				  enum led_brightness brightness)
+{
+	struct device *dsm_dev = led_cdev->dev->parent;
+	struct ssd_status_dev *ssd;
+	int err;
+
+	ssd = container_of(led_cdev, struct ssd_status_dev, led_cdev);
+	ssd->brightness = brightness;
+
+	if (brightness == LED_OFF) {
+		err = dsm_set(dsm_dev, 0);
+		if (err < 0)
+			return err;
+	}
+	return 0;
+}
+
+static enum led_brightness ssdleds_get_brightness(struct led_classdev *led_cdev)
+{
+	struct ssd_status_dev *ssd;
+
+	ssd = container_of(led_cdev, struct ssd_status_dev, led_cdev);
+	return ssd->brightness;
+}
+
+static void remove_ssd(struct ssd_status_dev *ssd)
+{
+	mutex_lock(&ssd_list_lock);
+	list_del(&ssd->ssd_list);
+	mutex_unlock(&ssd_list_lock);
+	led_classdev_unregister(&ssd->led_cdev);
+	kfree(ssd);
+}
+
+static struct ssd_status_dev *pci_dev_to_ssd_status_dev(struct pci_dev *pdev)
+{
+	struct ssd_status_dev *ssd;
+
+	mutex_lock(&ssd_list_lock);
+	list_for_each_entry(ssd, &ssd_list, ssd_list)
+		if (pdev == ssd->pdev) {
+			mutex_unlock(&ssd_list_lock);
+			return ssd;
+		}
+	mutex_unlock(&ssd_list_lock);
+	return NULL;
+}
+
+static void remove_ssd_for_pdev(struct pci_dev *pdev)
+{
+	struct ssd_status_dev *ssd = pci_dev_to_ssd_status_dev(pdev);
+
+	if (ssd)
+		remove_ssd(ssd);
+}
+
+static void add_ssd(struct pci_dev *pdev)
+{
+	u32 supported_states;
+	int ret;
+	struct ssd_status_dev *ssd;
+	char name[LED_MAX_NAME_SIZE];
+
+	if (dsm_get(&pdev->dev, GET_SUPPORTED_STATES_DSM, &supported_states) < 0)
+		return;
+
+	ssd = kzalloc(sizeof(*ssd), GFP_KERNEL);
+	if (!ssd)
+		return;
+
+	ssd->pdev = pdev;
+	ssd->supported_states = supported_states;
+	ssd->brightness = LED_ON;
+	snprintf(name, sizeof(name), "%s::%s",
+		 dev_name(&pdev->dev), "pcie_ssd_status");
+	ssd->led_cdev.name = name;
+	ssd->led_cdev.max_brightness = LED_ON;
+	ssd->led_cdev.brightness_set_blocking = ssdleds_set_brightness;
+	ssd->led_cdev.brightness_get = ssdleds_get_brightness;
+	ssd->led_cdev.groups = pcie_ssd_status_groups;
+
+	ret = led_classdev_register(&pdev->dev, &ssd->led_cdev);
+	if (ret) {
+		pr_warn("Failed to register LED %s\n", ssd->led_cdev.name);
+		remove_ssd(ssd);
+		return;
+	}
+	mutex_lock(&ssd_list_lock);
+	list_add_tail(&ssd->ssd_list, &ssd_list);
+	mutex_unlock(&ssd_list_lock);
+}
+
+static void probe_pdev(struct pci_dev *pdev)
+{
+	if (pci_dev_to_ssd_status_dev(pdev))
+		/*
+		 * leds have already been added for this pdev
+		 */
+		return;
+
+	if (pdev_has_dsm(pdev))
+		add_ssd(pdev);
+}
+
+static int pciessdleds_pci_bus_notifier_cb(struct notifier_block *nb,
+					   unsigned long action, void *data)
+{
+	struct pci_dev *pdev = to_pci_dev(data);
+
+	if (action == BUS_NOTIFY_ADD_DEVICE)
+		probe_pdev(pdev);
+	else if (action == BUS_NOTIFY_REMOVED_DEVICE)
+		remove_ssd_for_pdev(pdev);
+	return NOTIFY_DONE;
+}
+
+static struct notifier_block pciessdleds_pci_bus_nb = {
+	.notifier_call = pciessdleds_pci_bus_notifier_cb,
+	.priority = INT_MIN,
+};
+
+static void initial_scan_for_leds(void)
+{
+	struct pci_dev *pdev = NULL;
+
+	for_each_pci_dev(pdev)
+		probe_pdev(pdev);
+}
+
+static int __init pciessdleds_init(void)
+{
+	mutex_init(&ssd_list_lock);
+	INIT_LIST_HEAD(&ssd_list);
+
+	bus_register_notifier(&pci_bus_type, &pciessdleds_pci_bus_nb);
+	initial_scan_for_leds();
+	return 0;
+}
+
+static void __exit pciessdleds_exit(void)
+{
+	struct ssd_status_dev *ssd, *temp;
+
+	bus_unregister_notifier(&pci_bus_type, &pciessdleds_pci_bus_nb);
+	list_for_each_entry_safe(ssd, temp, &ssd_list, ssd_list)
+		remove_ssd(ssd);
+}
+
+module_init(pciessdleds_init);
+module_exit(pciessdleds_exit);
+
+MODULE_AUTHOR("Stuart Hayes <stuart.w.hayes@xxxxxxxxx>");
+MODULE_DESCRIPTION("Support for PCIe SSD Status LED Management _DSM");
+MODULE_LICENSE("GPL");
-- 
2.27.0




[Index of Archives]     [DMA Engine]     [Linux Coverity]     [Linux USB]     [Video for Linux]     [Linux Audio Users]     [Yosemite News]     [Linux Kernel]     [Linux SCSI]     [Greybus]

  Powered by Linux