|  | // SPDX-License-Identifier: GPL-2.0 | 
|  | /* | 
|  | * Intel Vendor Specific Extended Capabilities auxiliary bus driver | 
|  | * | 
|  | * Copyright (c) 2021, Intel Corporation. | 
|  | * All Rights Reserved. | 
|  | * | 
|  | * Author: David E. Box <david.e.box@linux.intel.com> | 
|  | * | 
|  | * This driver discovers and creates auxiliary devices for Intel defined PCIe | 
|  | * "Vendor Specific" and "Designated Vendor Specific" Extended Capabilities, | 
|  | * VSEC and DVSEC respectively. The driver supports features on specific PCIe | 
|  | * endpoints that exist primarily to expose them. | 
|  | */ | 
|  |  | 
|  | #include <linux/auxiliary_bus.h> | 
|  | #include <linux/bits.h> | 
|  | #include <linux/delay.h> | 
|  | #include <linux/kernel.h> | 
|  | #include <linux/idr.h> | 
|  | #include <linux/module.h> | 
|  | #include <linux/pci.h> | 
|  | #include <linux/types.h> | 
|  |  | 
|  | #include "vsec.h" | 
|  |  | 
|  | /* Intel DVSEC offsets */ | 
|  | #define INTEL_DVSEC_ENTRIES		0xA | 
|  | #define INTEL_DVSEC_SIZE		0xB | 
|  | #define INTEL_DVSEC_TABLE		0xC | 
|  | #define INTEL_DVSEC_TABLE_BAR(x)	((x) & GENMASK(2, 0)) | 
|  | #define INTEL_DVSEC_TABLE_OFFSET(x)	((x) & GENMASK(31, 3)) | 
|  | #define TABLE_OFFSET_SHIFT		3 | 
|  | #define PMT_XA_START			0 | 
|  | #define PMT_XA_MAX			INT_MAX | 
|  | #define PMT_XA_LIMIT			XA_LIMIT(PMT_XA_START, PMT_XA_MAX) | 
|  |  | 
|  | static DEFINE_IDA(intel_vsec_ida); | 
|  | static DEFINE_IDA(intel_vsec_sdsi_ida); | 
|  | static DEFINE_XARRAY_ALLOC(auxdev_array); | 
|  |  | 
|  | /** | 
|  | * struct intel_vsec_header - Common fields of Intel VSEC and DVSEC registers. | 
|  | * @rev:         Revision ID of the VSEC/DVSEC register space | 
|  | * @length:      Length of the VSEC/DVSEC register space | 
|  | * @id:          ID of the feature | 
|  | * @num_entries: Number of instances of the feature | 
|  | * @entry_size:  Size of the discovery table for each feature | 
|  | * @tbir:        BAR containing the discovery tables | 
|  | * @offset:      BAR offset of start of the first discovery table | 
|  | */ | 
|  | struct intel_vsec_header { | 
|  | u8	rev; | 
|  | u16	length; | 
|  | u16	id; | 
|  | u8	num_entries; | 
|  | u8	entry_size; | 
|  | u8	tbir; | 
|  | u32	offset; | 
|  | }; | 
|  |  | 
|  | enum intel_vsec_id { | 
|  | VSEC_ID_TELEMETRY	= 2, | 
|  | VSEC_ID_WATCHER		= 3, | 
|  | VSEC_ID_CRASHLOG	= 4, | 
|  | VSEC_ID_SDSI		= 65, | 
|  | }; | 
|  |  | 
|  | static enum intel_vsec_id intel_vsec_allow_list[] = { | 
|  | VSEC_ID_TELEMETRY, | 
|  | VSEC_ID_WATCHER, | 
|  | VSEC_ID_CRASHLOG, | 
|  | VSEC_ID_SDSI, | 
|  | }; | 
|  |  | 
|  | static const char *intel_vsec_name(enum intel_vsec_id id) | 
|  | { | 
|  | switch (id) { | 
|  | case VSEC_ID_TELEMETRY: | 
|  | return "telemetry"; | 
|  |  | 
|  | case VSEC_ID_WATCHER: | 
|  | return "watcher"; | 
|  |  | 
|  | case VSEC_ID_CRASHLOG: | 
|  | return "crashlog"; | 
|  |  | 
|  | case VSEC_ID_SDSI: | 
|  | return "sdsi"; | 
|  |  | 
|  | default: | 
|  | return NULL; | 
|  | } | 
|  | } | 
|  |  | 
|  | static bool intel_vsec_allowed(u16 id) | 
|  | { | 
|  | int i; | 
|  |  | 
|  | for (i = 0; i < ARRAY_SIZE(intel_vsec_allow_list); i++) | 
|  | if (intel_vsec_allow_list[i] == id) | 
|  | return true; | 
|  |  | 
|  | return false; | 
|  | } | 
|  |  | 
|  | static bool intel_vsec_disabled(u16 id, unsigned long quirks) | 
|  | { | 
|  | switch (id) { | 
|  | case VSEC_ID_WATCHER: | 
|  | return !!(quirks & VSEC_QUIRK_NO_WATCHER); | 
|  |  | 
|  | case VSEC_ID_CRASHLOG: | 
|  | return !!(quirks & VSEC_QUIRK_NO_CRASHLOG); | 
|  |  | 
|  | default: | 
|  | return false; | 
|  | } | 
|  | } | 
|  |  | 
|  | static void intel_vsec_remove_aux(void *data) | 
|  | { | 
|  | auxiliary_device_delete(data); | 
|  | auxiliary_device_uninit(data); | 
|  | } | 
|  |  | 
|  | static void intel_vsec_dev_release(struct device *dev) | 
|  | { | 
|  | struct intel_vsec_device *intel_vsec_dev = dev_to_ivdev(dev); | 
|  |  | 
|  | ida_free(intel_vsec_dev->ida, intel_vsec_dev->auxdev.id); | 
|  | kfree(intel_vsec_dev->resource); | 
|  | kfree(intel_vsec_dev); | 
|  | } | 
|  |  | 
|  | static int intel_vsec_add_aux(struct pci_dev *pdev, struct intel_vsec_device *intel_vsec_dev, | 
|  | const char *name) | 
|  | { | 
|  | struct auxiliary_device *auxdev = &intel_vsec_dev->auxdev; | 
|  | int ret, id; | 
|  |  | 
|  | ret = ida_alloc(intel_vsec_dev->ida, GFP_KERNEL); | 
|  | if (ret < 0) { | 
|  | kfree(intel_vsec_dev->resource); | 
|  | kfree(intel_vsec_dev); | 
|  | return ret; | 
|  | } | 
|  |  | 
|  | auxdev->id = ret; | 
|  | auxdev->name = name; | 
|  | auxdev->dev.parent = &pdev->dev; | 
|  | auxdev->dev.release = intel_vsec_dev_release; | 
|  |  | 
|  | ret = auxiliary_device_init(auxdev); | 
|  | if (ret < 0) { | 
|  | ida_free(intel_vsec_dev->ida, auxdev->id); | 
|  | kfree(intel_vsec_dev->resource); | 
|  | kfree(intel_vsec_dev); | 
|  | return ret; | 
|  | } | 
|  |  | 
|  | ret = auxiliary_device_add(auxdev); | 
|  | if (ret < 0) { | 
|  | auxiliary_device_uninit(auxdev); | 
|  | return ret; | 
|  | } | 
|  |  | 
|  | ret = devm_add_action_or_reset(&pdev->dev, intel_vsec_remove_aux, | 
|  | auxdev); | 
|  | if (ret < 0) | 
|  | return ret; | 
|  |  | 
|  | /* Add auxdev to list */ | 
|  | ret = xa_alloc(&auxdev_array, &id, intel_vsec_dev, PMT_XA_LIMIT, | 
|  | GFP_KERNEL); | 
|  | if (ret) | 
|  | return ret; | 
|  |  | 
|  | return 0; | 
|  | } | 
|  |  | 
|  | static int intel_vsec_add_dev(struct pci_dev *pdev, struct intel_vsec_header *header, | 
|  | struct intel_vsec_platform_info *info) | 
|  | { | 
|  | struct intel_vsec_device *intel_vsec_dev; | 
|  | struct resource *res, *tmp; | 
|  | unsigned long quirks = info->quirks; | 
|  | int i; | 
|  |  | 
|  | if (!intel_vsec_allowed(header->id) || intel_vsec_disabled(header->id, quirks)) | 
|  | return -EINVAL; | 
|  |  | 
|  | if (!header->num_entries) { | 
|  | dev_dbg(&pdev->dev, "Invalid 0 entry count for header id %d\n", header->id); | 
|  | return -EINVAL; | 
|  | } | 
|  |  | 
|  | if (!header->entry_size) { | 
|  | dev_dbg(&pdev->dev, "Invalid 0 entry size for header id %d\n", header->id); | 
|  | return -EINVAL; | 
|  | } | 
|  |  | 
|  | intel_vsec_dev = kzalloc(sizeof(*intel_vsec_dev), GFP_KERNEL); | 
|  | if (!intel_vsec_dev) | 
|  | return -ENOMEM; | 
|  |  | 
|  | res = kcalloc(header->num_entries, sizeof(*res), GFP_KERNEL); | 
|  | if (!res) { | 
|  | kfree(intel_vsec_dev); | 
|  | return -ENOMEM; | 
|  | } | 
|  |  | 
|  | if (quirks & VSEC_QUIRK_TABLE_SHIFT) | 
|  | header->offset >>= TABLE_OFFSET_SHIFT; | 
|  |  | 
|  | /* | 
|  | * The DVSEC/VSEC contains the starting offset and count for a block of | 
|  | * discovery tables. Create a resource array of these tables to the | 
|  | * auxiliary device driver. | 
|  | */ | 
|  | for (i = 0, tmp = res; i < header->num_entries; i++, tmp++) { | 
|  | tmp->start = pdev->resource[header->tbir].start + | 
|  | header->offset + i * (header->entry_size * sizeof(u32)); | 
|  | tmp->end = tmp->start + (header->entry_size * sizeof(u32)) - 1; | 
|  | tmp->flags = IORESOURCE_MEM; | 
|  | } | 
|  |  | 
|  | intel_vsec_dev->pcidev = pdev; | 
|  | intel_vsec_dev->resource = res; | 
|  | intel_vsec_dev->num_resources = header->num_entries; | 
|  | intel_vsec_dev->info = info; | 
|  |  | 
|  | if (header->id == VSEC_ID_SDSI) | 
|  | intel_vsec_dev->ida = &intel_vsec_sdsi_ida; | 
|  | else | 
|  | intel_vsec_dev->ida = &intel_vsec_ida; | 
|  |  | 
|  | return intel_vsec_add_aux(pdev, intel_vsec_dev, intel_vsec_name(header->id)); | 
|  | } | 
|  |  | 
|  | static bool intel_vsec_walk_header(struct pci_dev *pdev, | 
|  | struct intel_vsec_platform_info *info) | 
|  | { | 
|  | struct intel_vsec_header **header = info->capabilities; | 
|  | bool have_devices = false; | 
|  | int ret; | 
|  |  | 
|  | for ( ; *header; header++) { | 
|  | ret = intel_vsec_add_dev(pdev, *header, info); | 
|  | if (ret) | 
|  | dev_info(&pdev->dev, "Could not add device for DVSEC id %d\n", | 
|  | (*header)->id); | 
|  | else | 
|  | have_devices = true; | 
|  | } | 
|  |  | 
|  | return have_devices; | 
|  | } | 
|  |  | 
|  | static bool intel_vsec_walk_dvsec(struct pci_dev *pdev, | 
|  | struct intel_vsec_platform_info *info) | 
|  | { | 
|  | bool have_devices = false; | 
|  | int pos = 0; | 
|  |  | 
|  | do { | 
|  | struct intel_vsec_header header; | 
|  | u32 table, hdr; | 
|  | u16 vid; | 
|  | int ret; | 
|  |  | 
|  | pos = pci_find_next_ext_capability(pdev, pos, PCI_EXT_CAP_ID_DVSEC); | 
|  | if (!pos) | 
|  | break; | 
|  |  | 
|  | pci_read_config_dword(pdev, pos + PCI_DVSEC_HEADER1, &hdr); | 
|  | vid = PCI_DVSEC_HEADER1_VID(hdr); | 
|  | if (vid != PCI_VENDOR_ID_INTEL) | 
|  | continue; | 
|  |  | 
|  | /* Support only revision 1 */ | 
|  | header.rev = PCI_DVSEC_HEADER1_REV(hdr); | 
|  | if (header.rev != 1) { | 
|  | dev_info(&pdev->dev, "Unsupported DVSEC revision %d\n", header.rev); | 
|  | continue; | 
|  | } | 
|  |  | 
|  | header.length = PCI_DVSEC_HEADER1_LEN(hdr); | 
|  |  | 
|  | pci_read_config_byte(pdev, pos + INTEL_DVSEC_ENTRIES, &header.num_entries); | 
|  | pci_read_config_byte(pdev, pos + INTEL_DVSEC_SIZE, &header.entry_size); | 
|  | pci_read_config_dword(pdev, pos + INTEL_DVSEC_TABLE, &table); | 
|  |  | 
|  | header.tbir = INTEL_DVSEC_TABLE_BAR(table); | 
|  | header.offset = INTEL_DVSEC_TABLE_OFFSET(table); | 
|  |  | 
|  | pci_read_config_dword(pdev, pos + PCI_DVSEC_HEADER2, &hdr); | 
|  | header.id = PCI_DVSEC_HEADER2_ID(hdr); | 
|  |  | 
|  | ret = intel_vsec_add_dev(pdev, &header, info); | 
|  | if (ret) | 
|  | continue; | 
|  |  | 
|  | have_devices = true; | 
|  | } while (true); | 
|  |  | 
|  | return have_devices; | 
|  | } | 
|  |  | 
|  | static bool intel_vsec_walk_vsec(struct pci_dev *pdev, | 
|  | struct intel_vsec_platform_info *info) | 
|  | { | 
|  | bool have_devices = false; | 
|  | int pos = 0; | 
|  |  | 
|  | do { | 
|  | struct intel_vsec_header header; | 
|  | u32 table, hdr; | 
|  | int ret; | 
|  |  | 
|  | pos = pci_find_next_ext_capability(pdev, pos, PCI_EXT_CAP_ID_VNDR); | 
|  | if (!pos) | 
|  | break; | 
|  |  | 
|  | pci_read_config_dword(pdev, pos + PCI_VNDR_HEADER, &hdr); | 
|  |  | 
|  | /* Support only revision 1 */ | 
|  | header.rev = PCI_VNDR_HEADER_REV(hdr); | 
|  | if (header.rev != 1) { | 
|  | dev_info(&pdev->dev, "Unsupported VSEC revision %d\n", header.rev); | 
|  | continue; | 
|  | } | 
|  |  | 
|  | header.id = PCI_VNDR_HEADER_ID(hdr); | 
|  | header.length = PCI_VNDR_HEADER_LEN(hdr); | 
|  |  | 
|  | /* entry, size, and table offset are the same as DVSEC */ | 
|  | pci_read_config_byte(pdev, pos + INTEL_DVSEC_ENTRIES, &header.num_entries); | 
|  | pci_read_config_byte(pdev, pos + INTEL_DVSEC_SIZE, &header.entry_size); | 
|  | pci_read_config_dword(pdev, pos + INTEL_DVSEC_TABLE, &table); | 
|  |  | 
|  | header.tbir = INTEL_DVSEC_TABLE_BAR(table); | 
|  | header.offset = INTEL_DVSEC_TABLE_OFFSET(table); | 
|  |  | 
|  | ret = intel_vsec_add_dev(pdev, &header, info); | 
|  | if (ret) | 
|  | continue; | 
|  |  | 
|  | have_devices = true; | 
|  | } while (true); | 
|  |  | 
|  | return have_devices; | 
|  | } | 
|  |  | 
|  | static int intel_vsec_pci_probe(struct pci_dev *pdev, const struct pci_device_id *id) | 
|  | { | 
|  | struct intel_vsec_platform_info *info; | 
|  | bool have_devices = false; | 
|  | int ret; | 
|  |  | 
|  | ret = pcim_enable_device(pdev); | 
|  | if (ret) | 
|  | return ret; | 
|  |  | 
|  | pci_save_state(pdev); | 
|  | info = (struct intel_vsec_platform_info *)id->driver_data; | 
|  | if (!info) | 
|  | return -EINVAL; | 
|  |  | 
|  | if (intel_vsec_walk_dvsec(pdev, info)) | 
|  | have_devices = true; | 
|  |  | 
|  | if (intel_vsec_walk_vsec(pdev, info)) | 
|  | have_devices = true; | 
|  |  | 
|  | if (info && (info->quirks & VSEC_QUIRK_NO_DVSEC) && | 
|  | intel_vsec_walk_header(pdev, info)) | 
|  | have_devices = true; | 
|  |  | 
|  | if (!have_devices) | 
|  | return -ENODEV; | 
|  |  | 
|  | return 0; | 
|  | } | 
|  |  | 
|  | /* TGL info */ | 
|  | static const struct intel_vsec_platform_info tgl_info = { | 
|  | .quirks = VSEC_QUIRK_NO_WATCHER | VSEC_QUIRK_NO_CRASHLOG | | 
|  | VSEC_QUIRK_TABLE_SHIFT | VSEC_QUIRK_EARLY_HW, | 
|  | }; | 
|  |  | 
|  | /* DG1 info */ | 
|  | static struct intel_vsec_header dg1_telemetry = { | 
|  | .length = 0x10, | 
|  | .id = 2, | 
|  | .num_entries = 1, | 
|  | .entry_size = 3, | 
|  | .tbir = 0, | 
|  | .offset = 0x466000, | 
|  | }; | 
|  |  | 
|  | static struct intel_vsec_header *dg1_capabilities[] = { | 
|  | &dg1_telemetry, | 
|  | NULL | 
|  | }; | 
|  |  | 
|  | static const struct intel_vsec_platform_info dg1_info = { | 
|  | .capabilities = dg1_capabilities, | 
|  | .quirks = VSEC_QUIRK_NO_DVSEC | VSEC_QUIRK_EARLY_HW, | 
|  | }; | 
|  |  | 
|  | #define PCI_DEVICE_ID_INTEL_VSEC_ADL		0x467d | 
|  | #define PCI_DEVICE_ID_INTEL_VSEC_DG1		0x490e | 
|  | #define PCI_DEVICE_ID_INTEL_VSEC_OOBMSM		0x09a7 | 
|  | #define PCI_DEVICE_ID_INTEL_VSEC_RPL		0xa77d | 
|  | #define PCI_DEVICE_ID_INTEL_VSEC_TGL		0x9a0d | 
|  | static const struct pci_device_id intel_vsec_pci_ids[] = { | 
|  | { PCI_DEVICE_DATA(INTEL, VSEC_ADL, &tgl_info) }, | 
|  | { PCI_DEVICE_DATA(INTEL, VSEC_DG1, &dg1_info) }, | 
|  | { PCI_DEVICE_DATA(INTEL, VSEC_OOBMSM, &(struct intel_vsec_platform_info) {}) }, | 
|  | { PCI_DEVICE_DATA(INTEL, VSEC_RPL, &tgl_info) }, | 
|  | { PCI_DEVICE_DATA(INTEL, VSEC_TGL, &tgl_info) }, | 
|  | { } | 
|  | }; | 
|  | MODULE_DEVICE_TABLE(pci, intel_vsec_pci_ids); | 
|  |  | 
|  | static pci_ers_result_t intel_vsec_pci_error_detected(struct pci_dev *pdev, | 
|  | pci_channel_state_t state) | 
|  | { | 
|  | pci_ers_result_t status = PCI_ERS_RESULT_NEED_RESET; | 
|  |  | 
|  | dev_info(&pdev->dev, "PCI error detected, state %d", state); | 
|  |  | 
|  | if (state == pci_channel_io_perm_failure) | 
|  | status = PCI_ERS_RESULT_DISCONNECT; | 
|  | else | 
|  | pci_disable_device(pdev); | 
|  |  | 
|  | return status; | 
|  | } | 
|  |  | 
|  | static pci_ers_result_t intel_vsec_pci_slot_reset(struct pci_dev *pdev) | 
|  | { | 
|  | struct intel_vsec_device *intel_vsec_dev; | 
|  | pci_ers_result_t status = PCI_ERS_RESULT_DISCONNECT; | 
|  | const struct pci_device_id *pci_dev_id; | 
|  | unsigned long index; | 
|  |  | 
|  | dev_info(&pdev->dev, "Resetting PCI slot\n"); | 
|  |  | 
|  | msleep(2000); | 
|  | if (pci_enable_device(pdev)) { | 
|  | dev_info(&pdev->dev, | 
|  | "Failed to re-enable PCI device after reset.\n"); | 
|  | goto out; | 
|  | } | 
|  |  | 
|  | status = PCI_ERS_RESULT_RECOVERED; | 
|  |  | 
|  | xa_for_each(&auxdev_array, index, intel_vsec_dev) { | 
|  | /* check if pdev doesn't match */ | 
|  | if (pdev != intel_vsec_dev->pcidev) | 
|  | continue; | 
|  | devm_release_action(&pdev->dev, intel_vsec_remove_aux, | 
|  | &intel_vsec_dev->auxdev); | 
|  | } | 
|  | pci_disable_device(pdev); | 
|  | pci_restore_state(pdev); | 
|  | pci_dev_id = pci_match_id(intel_vsec_pci_ids, pdev); | 
|  | intel_vsec_pci_probe(pdev, pci_dev_id); | 
|  |  | 
|  | out: | 
|  | return status; | 
|  | } | 
|  |  | 
|  | static void intel_vsec_pci_resume(struct pci_dev *pdev) | 
|  | { | 
|  | dev_info(&pdev->dev, "Done resuming PCI device\n"); | 
|  | } | 
|  |  | 
|  | static const struct pci_error_handlers intel_vsec_pci_err_handlers = { | 
|  | .error_detected = intel_vsec_pci_error_detected, | 
|  | .slot_reset = intel_vsec_pci_slot_reset, | 
|  | .resume = intel_vsec_pci_resume, | 
|  | }; | 
|  |  | 
|  | static struct pci_driver intel_vsec_pci_driver = { | 
|  | .name = "intel_vsec", | 
|  | .id_table = intel_vsec_pci_ids, | 
|  | .probe = intel_vsec_pci_probe, | 
|  | .err_handler = &intel_vsec_pci_err_handlers, | 
|  | }; | 
|  | module_pci_driver(intel_vsec_pci_driver); | 
|  |  | 
|  | MODULE_AUTHOR("David E. Box <david.e.box@linux.intel.com>"); | 
|  | MODULE_DESCRIPTION("Intel Extended Capabilities auxiliary bus driver"); | 
|  | MODULE_LICENSE("GPL v2"); |