| // SPDX-License-Identifier: GPL-2.0 | |
| /* | |
| * PCIe Enclosure management driver created for LED interfaces based on | |
| * indications. It says *what indications* blink but does not specify *how* | |
| * they blink - it is hardware defined. | |
| * | |
| * The driver name refers to Native PCIe Enclosure Management. It is | |
| * first indication oriented standard with specification. | |
| * | |
| * Native PCIe Enclosure Management (NPEM) | |
| * PCIe Base Specification r6.1 sec 6.28, 7.9.19 | |
| * | |
| * _DSM Definitions for PCIe SSD Status LED | |
| * PCI Firmware Specification, r3.3 sec 4.7 | |
| * | |
| * Two backends are supported to manipulate indications: Direct NPEM register | |
| * access (npem_ops) and indirect access through the ACPI _DSM (dsm_ops). | |
| * _DSM is used if supported, else NPEM. | |
| * | |
| * Copyright (c) 2021-2022 Dell Inc. | |
| * Copyright (c) 2023-2024 Intel Corporation | |
| * Mariusz Tkaczyk <mariusz.tkaczyk@linux.intel.com> | |
| */ | |
| #include <linux/acpi.h> | |
| #include <linux/bitops.h> | |
| #include <linux/errno.h> | |
| #include <linux/iopoll.h> | |
| #include <linux/leds.h> | |
| #include <linux/mutex.h> | |
| #include <linux/pci.h> | |
| #include <linux/pci_regs.h> | |
| #include <linux/types.h> | |
| #include <linux/uleds.h> | |
| #include "pci.h" | |
| struct indication { | |
| u32 bit; | |
| const char *name; | |
| }; | |
| static const struct indication npem_indications[] = { | |
| {PCI_NPEM_IND_OK, "enclosure:ok"}, | |
| {PCI_NPEM_IND_LOCATE, "enclosure:locate"}, | |
| {PCI_NPEM_IND_FAIL, "enclosure:fail"}, | |
| {PCI_NPEM_IND_REBUILD, "enclosure:rebuild"}, | |
| {PCI_NPEM_IND_PFA, "enclosure:pfa"}, | |
| {PCI_NPEM_IND_HOTSPARE, "enclosure:hotspare"}, | |
| {PCI_NPEM_IND_ICA, "enclosure:ica"}, | |
| {PCI_NPEM_IND_IFA, "enclosure:ifa"}, | |
| {PCI_NPEM_IND_IDT, "enclosure:idt"}, | |
| {PCI_NPEM_IND_DISABLED, "enclosure:disabled"}, | |
| {PCI_NPEM_IND_SPEC_0, "enclosure:specific_0"}, | |
| {PCI_NPEM_IND_SPEC_1, "enclosure:specific_1"}, | |
| {PCI_NPEM_IND_SPEC_2, "enclosure:specific_2"}, | |
| {PCI_NPEM_IND_SPEC_3, "enclosure:specific_3"}, | |
| {PCI_NPEM_IND_SPEC_4, "enclosure:specific_4"}, | |
| {PCI_NPEM_IND_SPEC_5, "enclosure:specific_5"}, | |
| {PCI_NPEM_IND_SPEC_6, "enclosure:specific_6"}, | |
| {PCI_NPEM_IND_SPEC_7, "enclosure:specific_7"}, | |
| {0, NULL} | |
| }; | |
| /* _DSM PCIe SSD LED States correspond to NPEM register values */ | |
| static const struct indication dsm_indications[] = { | |
| {PCI_NPEM_IND_OK, "enclosure:ok"}, | |
| {PCI_NPEM_IND_LOCATE, "enclosure:locate"}, | |
| {PCI_NPEM_IND_FAIL, "enclosure:fail"}, | |
| {PCI_NPEM_IND_REBUILD, "enclosure:rebuild"}, | |
| {PCI_NPEM_IND_PFA, "enclosure:pfa"}, | |
| {PCI_NPEM_IND_HOTSPARE, "enclosure:hotspare"}, | |
| {PCI_NPEM_IND_ICA, "enclosure:ica"}, | |
| {PCI_NPEM_IND_IFA, "enclosure:ifa"}, | |
| {PCI_NPEM_IND_IDT, "enclosure:idt"}, | |
| {PCI_NPEM_IND_DISABLED, "enclosure:disabled"}, | |
| {0, NULL} | |
| }; | |
| #define for_each_indication(ind, inds) \ | |
| for (ind = inds; ind->bit; ind++) | |
| /* | |
| * The driver has internal list of supported indications. Ideally, the driver | |
| * should not touch bits that are not defined and for which LED devices are | |
| * not exposed but in reality, it needs to turn them off. | |
| * | |
| * Otherwise, there will be no possibility to turn off indications turned on by | |
| * other utilities or turned on by default and it leads to bad user experience. | |
| * | |
| * Additionally, it excludes NPEM commands like RESET or ENABLE. | |
| */ | |
| static u32 reg_to_indications(u32 caps, const struct indication *inds) | |
| { | |
| const struct indication *ind; | |
| u32 supported_indications = 0; | |
| for_each_indication(ind, inds) | |
| supported_indications |= ind->bit; | |
| return caps & supported_indications; | |
| } | |
| /** | |
| * struct npem_led - LED details | |
| * @indication: indication details | |
| * @npem: NPEM device | |
| * @name: LED name | |
| * @led: LED device | |
| */ | |
| struct npem_led { | |
| const struct indication *indication; | |
| struct npem *npem; | |
| char name[LED_MAX_NAME_SIZE]; | |
| struct led_classdev led; | |
| }; | |
| /** | |
| * struct npem_ops - backend specific callbacks | |
| * @get_active_indications: get active indications | |
| * npem: NPEM device | |
| * inds: response buffer | |
| * @set_active_indications: set new indications | |
| * npem: npem device | |
| * inds: bit mask to set | |
| * @inds: supported indications array, set of indications is backend specific | |
| * @name: backend name | |
| */ | |
| struct npem_ops { | |
| int (*get_active_indications)(struct npem *npem, u32 *inds); | |
| int (*set_active_indications)(struct npem *npem, u32 inds); | |
| const struct indication *inds; | |
| const char *name; | |
| }; | |
| /** | |
| * struct npem - NPEM device properties | |
| * @dev: PCI device this driver is attached to | |
| * @ops: backend specific callbacks | |
| * @lock: serializes concurrent access to NPEM device by multiple LED devices | |
| * @pos: cached offset of NPEM Capability Register in Configuration Space; | |
| * only used if NPEM registers are accessed directly and not through _DSM | |
| * @supported_indications: cached bit mask of supported indications; | |
| * non-indication and reserved bits in the NPEM Capability Register are | |
| * cleared in this bit mask | |
| * @active_indications: cached bit mask of active indications; | |
| * non-indication and reserved bits in the NPEM Control Register are | |
| * cleared in this bit mask | |
| * @active_inds_initialized: whether @active_indications has been initialized; | |
| * On Dell platforms, it is required that IPMI drivers are loaded before | |
| * the GET_STATE_DSM method is invoked: They use an IPMI OpRegion to | |
| * get/set the active LEDs. By initializing @active_indications lazily | |
| * (on first access to an LED), IPMI drivers are given a chance to load. | |
| * If they are not loaded in time, users will see various errors on LED | |
| * access in dmesg. Once they are loaded, the errors go away and LED | |
| * access becomes possible. | |
| * @led_cnt: size of @leds array | |
| * @leds: array containing LED class devices of all supported LEDs | |
| */ | |
| struct npem { | |
| struct pci_dev *dev; | |
| const struct npem_ops *ops; | |
| struct mutex lock; | |
| u16 pos; | |
| u32 supported_indications; | |
| u32 active_indications; | |
| unsigned int active_inds_initialized:1; | |
| int led_cnt; | |
| struct npem_led leds[]; | |
| }; | |
| static int npem_read_reg(struct npem *npem, u16 reg, u32 *val) | |
| { | |
| int ret = pci_read_config_dword(npem->dev, npem->pos + reg, val); | |
| return pcibios_err_to_errno(ret); | |
| } | |
| static int npem_write_ctrl(struct npem *npem, u32 reg) | |
| { | |
| int pos = npem->pos + PCI_NPEM_CTRL; | |
| int ret = pci_write_config_dword(npem->dev, pos, reg); | |
| return pcibios_err_to_errno(ret); | |
| } | |
| static int npem_get_active_indications(struct npem *npem, u32 *inds) | |
| { | |
| u32 ctrl; | |
| int ret; | |
| ret = npem_read_reg(npem, PCI_NPEM_CTRL, &ctrl); | |
| if (ret) | |
| return ret; | |
| /* If PCI_NPEM_CTRL_ENABLE is not set then no indication should blink */ | |
| if (!(ctrl & PCI_NPEM_CTRL_ENABLE)) { | |
| *inds = 0; | |
| return 0; | |
| } | |
| *inds = ctrl & npem->supported_indications; | |
| return 0; | |
| } | |
| static int npem_set_active_indications(struct npem *npem, u32 inds) | |
| { | |
| int ctrl, ret, ret_val; | |
| u32 cc_status; | |
| lockdep_assert_held(&npem->lock); | |
| /* This bit is always required */ | |
| ctrl = inds | PCI_NPEM_CTRL_ENABLE; | |
| ret = npem_write_ctrl(npem, ctrl); | |
| if (ret) | |
| return ret; | |
| /* | |
| * For the case where a NPEM command has not completed immediately, | |
| * it is recommended that software not continuously "spin" on polling | |
| * the status register, but rather poll under interrupt at a reduced | |
| * rate; for example at 10 ms intervals. | |
| * | |
| * PCIe r6.1 sec 6.28 "Implementation Note: Software Polling of NPEM | |
| * Command Completed" | |
| */ | |
| ret = read_poll_timeout(npem_read_reg, ret_val, | |
| ret_val || (cc_status & PCI_NPEM_STATUS_CC), | |
| 10 * USEC_PER_MSEC, USEC_PER_SEC, false, npem, | |
| PCI_NPEM_STATUS, &cc_status); | |
| if (ret) | |
| return ret; | |
| if (ret_val) | |
| return ret_val; | |
| /* | |
| * All writes to control register, including writes that do not change | |
| * the register value, are NPEM commands and should eventually result | |
| * in a command completion indication in the NPEM Status Register. | |
| * | |
| * PCIe Base Specification r6.1 sec 7.9.19.3 | |
| * | |
| * Register may not be updated, or other conflicting bits may be | |
| * cleared. Spec is not strict here. Read NPEM Control register after | |
| * write to keep cache in-sync. | |
| */ | |
| return npem_get_active_indications(npem, &npem->active_indications); | |
| } | |
| static const struct npem_ops npem_ops = { | |
| .get_active_indications = npem_get_active_indications, | |
| .set_active_indications = npem_set_active_indications, | |
| .name = "Native PCIe Enclosure Management", | |
| .inds = npem_indications, | |
| }; | |
| #define DSM_GUID GUID_INIT(0x5d524d9d, 0xfff9, 0x4d4b, 0x8c, 0xb7, 0x74, 0x7e,\ | |
| 0xd5, 0x1e, 0x19, 0x4d) | |
| #define GET_SUPPORTED_STATES_DSM 1 | |
| #define GET_STATE_DSM 2 | |
| #define SET_STATE_DSM 3 | |
| static const guid_t dsm_guid = DSM_GUID; | |
| static bool npem_has_dsm(struct pci_dev *pdev) | |
| { | |
| acpi_handle handle; | |
| handle = ACPI_HANDLE(&pdev->dev); | |
| if (!handle) | |
| return false; | |
| return acpi_check_dsm(handle, &dsm_guid, 0x1, | |
| BIT(GET_SUPPORTED_STATES_DSM) | | |
| BIT(GET_STATE_DSM) | BIT(SET_STATE_DSM)); | |
| } | |
| struct dsm_output { | |
| u16 status; | |
| u8 function_specific_err; | |
| u8 vendor_specific_err; | |
| u32 state; | |
| }; | |
| /** | |
| * dsm_evaluate() - send DSM PCIe SSD Status LED command | |
| * @pdev: PCI device | |
| * @dsm_func: DSM LED Function | |
| * @output: buffer to copy DSM Response | |
| * @value_to_set: value for SET_STATE_DSM function | |
| * | |
| * To not bother caller with ACPI context, the returned _DSM Output Buffer is | |
| * copied. | |
| */ | |
| static int dsm_evaluate(struct pci_dev *pdev, u64 dsm_func, | |
| struct dsm_output *output, u32 value_to_set) | |
| { | |
| acpi_handle handle = ACPI_HANDLE(&pdev->dev); | |
| union acpi_object *out_obj, arg3[2]; | |
| union acpi_object *arg3_p = NULL; | |
| if (dsm_func == SET_STATE_DSM) { | |
| 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_to_set; | |
| arg3_p = arg3; | |
| } | |
| out_obj = acpi_evaluate_dsm_typed(handle, &dsm_guid, 0x1, dsm_func, | |
| arg3_p, ACPI_TYPE_BUFFER); | |
| if (!out_obj) | |
| return -EIO; | |
| if (out_obj->buffer.length < sizeof(struct dsm_output)) { | |
| ACPI_FREE(out_obj); | |
| return -EIO; | |
| } | |
| memcpy(output, out_obj->buffer.pointer, sizeof(struct dsm_output)); | |
| ACPI_FREE(out_obj); | |
| return 0; | |
| } | |
| static int dsm_get(struct pci_dev *pdev, u64 dsm_func, u32 *buf) | |
| { | |
| struct dsm_output output; | |
| int ret = dsm_evaluate(pdev, dsm_func, &output, 0); | |
| if (ret) | |
| return ret; | |
| if (output.status != 0) | |
| return -EIO; | |
| *buf = output.state; | |
| return 0; | |
| } | |
| static int dsm_get_active_indications(struct npem *npem, u32 *buf) | |
| { | |
| int ret = dsm_get(npem->dev, GET_STATE_DSM, buf); | |
| /* Filter out not supported indications in response */ | |
| *buf &= npem->supported_indications; | |
| return ret; | |
| } | |
| static int dsm_set_active_indications(struct npem *npem, u32 value) | |
| { | |
| struct dsm_output output; | |
| int ret = dsm_evaluate(npem->dev, SET_STATE_DSM, &output, value); | |
| if (ret) | |
| return ret; | |
| switch (output.status) { | |
| case 4: | |
| /* | |
| * Not all bits are set. If this bit is set, the platform | |
| * disregarded some or all of the request state changes. OSPM | |
| * should check the resulting PCIe SSD Status LED States to see | |
| * what, if anything, has changed. | |
| * | |
| * PCI Firmware Specification, r3.3 Table 4-19. | |
| */ | |
| if (output.function_specific_err != 1) | |
| return -EIO; | |
| fallthrough; | |
| case 0: | |
| break; | |
| default: | |
| return -EIO; | |
| } | |
| npem->active_indications = output.state; | |
| return 0; | |
| } | |
| static const struct npem_ops dsm_ops = { | |
| .get_active_indications = dsm_get_active_indications, | |
| .set_active_indications = dsm_set_active_indications, | |
| .name = "_DSM PCIe SSD Status LED Management", | |
| .inds = dsm_indications, | |
| }; | |
| static int npem_initialize_active_indications(struct npem *npem) | |
| { | |
| int ret; | |
| lockdep_assert_held(&npem->lock); | |
| if (npem->active_inds_initialized) | |
| return 0; | |
| ret = npem->ops->get_active_indications(npem, | |
| &npem->active_indications); | |
| if (ret) | |
| return ret; | |
| npem->active_inds_initialized = true; | |
| return 0; | |
| } | |
| /* | |
| * The status of each indicator is cached on first brightness_ get/set time | |
| * and updated at write time. brightness_get() is only responsible for | |
| * reflecting the last written/cached value. | |
| */ | |
| static enum led_brightness brightness_get(struct led_classdev *led) | |
| { | |
| struct npem_led *nled = container_of(led, struct npem_led, led); | |
| struct npem *npem = nled->npem; | |
| int ret, val = 0; | |
| ret = mutex_lock_interruptible(&npem->lock); | |
| if (ret) | |
| return ret; | |
| ret = npem_initialize_active_indications(npem); | |
| if (ret) | |
| goto out; | |
| if (npem->active_indications & nled->indication->bit) | |
| val = 1; | |
| out: | |
| mutex_unlock(&npem->lock); | |
| return val; | |
| } | |
| static int brightness_set(struct led_classdev *led, | |
| enum led_brightness brightness) | |
| { | |
| struct npem_led *nled = container_of(led, struct npem_led, led); | |
| struct npem *npem = nled->npem; | |
| u32 indications; | |
| int ret; | |
| ret = mutex_lock_interruptible(&npem->lock); | |
| if (ret) | |
| return ret; | |
| ret = npem_initialize_active_indications(npem); | |
| if (ret) | |
| goto out; | |
| if (brightness == 0) | |
| indications = npem->active_indications & ~(nled->indication->bit); | |
| else | |
| indications = npem->active_indications | nled->indication->bit; | |
| ret = npem->ops->set_active_indications(npem, indications); | |
| out: | |
| mutex_unlock(&npem->lock); | |
| return ret; | |
| } | |
| static void npem_free(struct npem *npem) | |
| { | |
| struct npem_led *nled; | |
| int cnt; | |
| if (!npem) | |
| return; | |
| for (cnt = 0; cnt < npem->led_cnt; cnt++) { | |
| nled = &npem->leds[cnt]; | |
| if (nled->name[0]) | |
| led_classdev_unregister(&nled->led); | |
| } | |
| mutex_destroy(&npem->lock); | |
| kfree(npem); | |
| } | |
| static int pci_npem_set_led_classdev(struct npem *npem, struct npem_led *nled) | |
| { | |
| struct led_classdev *led = &nled->led; | |
| struct led_init_data init_data = {}; | |
| char *name = nled->name; | |
| int ret; | |
| init_data.devicename = pci_name(npem->dev); | |
| init_data.default_label = nled->indication->name; | |
| ret = led_compose_name(&npem->dev->dev, &init_data, name); | |
| if (ret) | |
| return ret; | |
| led->name = name; | |
| led->brightness_set_blocking = brightness_set; | |
| led->brightness_get = brightness_get; | |
| led->max_brightness = 1; | |
| led->default_trigger = "none"; | |
| led->flags = LED_HW_PLUGGABLE; | |
| ret = led_classdev_register(&npem->dev->dev, led); | |
| if (ret) | |
| /* Clear the name to indicate that it is not registered. */ | |
| name[0] = 0; | |
| return ret; | |
| } | |
| static int pci_npem_init(struct pci_dev *dev, const struct npem_ops *ops, | |
| int pos, u32 caps) | |
| { | |
| u32 supported = reg_to_indications(caps, ops->inds); | |
| int supported_cnt = hweight32(supported); | |
| const struct indication *indication; | |
| struct npem_led *nled; | |
| struct npem *npem; | |
| int led_idx = 0; | |
| int ret; | |
| npem = kzalloc_flex(*npem, leds, supported_cnt); | |
| if (!npem) | |
| return -ENOMEM; | |
| npem->supported_indications = supported; | |
| npem->led_cnt = supported_cnt; | |
| npem->pos = pos; | |
| npem->dev = dev; | |
| npem->ops = ops; | |
| mutex_init(&npem->lock); | |
| for_each_indication(indication, npem_indications) { | |
| if (!(npem->supported_indications & indication->bit)) | |
| continue; | |
| nled = &npem->leds[led_idx++]; | |
| nled->indication = indication; | |
| nled->npem = npem; | |
| ret = pci_npem_set_led_classdev(npem, nled); | |
| if (ret) { | |
| npem_free(npem); | |
| return ret; | |
| } | |
| } | |
| dev->npem = npem; | |
| return 0; | |
| } | |
| void pci_npem_remove(struct pci_dev *dev) | |
| { | |
| npem_free(dev->npem); | |
| } | |
| void pci_npem_create(struct pci_dev *dev) | |
| { | |
| const struct npem_ops *ops = &npem_ops; | |
| int pos = 0, ret; | |
| u32 cap; | |
| if (npem_has_dsm(dev)) { | |
| /* | |
| * OS should use the DSM for LED control if it is available | |
| * PCI Firmware Spec r3.3 sec 4.7. | |
| */ | |
| ret = dsm_get(dev, GET_SUPPORTED_STATES_DSM, &cap); | |
| if (ret) | |
| return; | |
| ops = &dsm_ops; | |
| } else { | |
| pos = pci_find_ext_capability(dev, PCI_EXT_CAP_ID_NPEM); | |
| if (pos == 0) | |
| return; | |
| if (pci_read_config_dword(dev, pos + PCI_NPEM_CAP, &cap) != 0 || | |
| (cap & PCI_NPEM_CAP_CAPABLE) == 0) | |
| return; | |
| } | |
| pci_info(dev, "Configuring %s\n", ops->name); | |
| ret = pci_npem_init(dev, ops, pos, cap); | |
| if (ret) | |
| pci_err(dev, "Failed to register %s, err: %d\n", ops->name, | |
| ret); | |
| } |