| // 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 = 0; |
| |
| 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(struct_size(npem, leds, supported_cnt), GFP_KERNEL); |
| 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); |
| } |