From: Kuppuswamy Sathyanarayanan <[email protected]>

As per PCI firmware specification v3.2 ECN
(https://members.pcisig.com/wg/PCI-SIG/document/12614), when firmware
owns Downstream Port Containment (DPC), its expected to use the "Error
Disconnect Recover" (EDR) notification to alert OSPM of a DPC event and
if OS supports EDR, its expected to handle the software state invalidation
and port recovery in OS and let firmware know the recovery status via _OST
ACPI call.

Since EDR is a hybrid service, DPC service shall be enabled in OS even
if AER is not natively enabled in OS.

Signed-off-by: Kuppuswamy Sathyanarayanan 
<[email protected]>
---
 drivers/pci/pcie/Kconfig        |  10 ++
 drivers/pci/pcie/dpc.c          | 206 ++++++++++++++++++++++++++++++++
 drivers/pci/pcie/portdrv_core.c |   9 +-
 3 files changed, 223 insertions(+), 2 deletions(-)

diff --git a/drivers/pci/pcie/Kconfig b/drivers/pci/pcie/Kconfig
index 362eb8cfa53b..9a05015af7cd 100644
--- a/drivers/pci/pcie/Kconfig
+++ b/drivers/pci/pcie/Kconfig
@@ -150,3 +150,13 @@ config PCIE_BW
          This enables PCI Express Bandwidth Change Notification.  If
          you know link width or rate changes occur only to correct
          unreliable links, you may answer Y.
+
+config PCIE_EDR
+       bool "PCI Express Error Disconnect Recover support"
+       default n
+       depends on PCIE_DPC && ACPI
+       help
+         This options adds Error Disconnect Recover support as specified
+         in PCI firmware specification v3.2 Downstream Port Containment
+         Related Enhancements ECN. Enable this if you want to support hybrid
+         DPC model which uses both firmware and OS to implement DPC.
diff --git a/drivers/pci/pcie/dpc.c b/drivers/pci/pcie/dpc.c
index 95810b4b0adc..131fadd29ae2 100644
--- a/drivers/pci/pcie/dpc.c
+++ b/drivers/pci/pcie/dpc.c
@@ -11,6 +11,7 @@
 #include <linux/interrupt.h>
 #include <linux/init.h>
 #include <linux/pci.h>
+#include <linux/acpi.h>
 
 #include "portdrv.h"
 #include "../pci.h"
@@ -22,8 +23,22 @@ struct dpc_dev {
        u8                      rp_log_size;
        /* Set True if DPC is handled in firmware */
        bool                    firmware_dpc;
+       pci_ers_result_t        error_state;
+#ifdef CONFIG_ACPI
+       struct acpi_device      *adev;
+#endif
 };
 
+#ifdef CONFIG_ACPI
+
+#define EDR_PORT_ENABLE_DSM     0x0C
+#define EDR_PORT_LOCATE_DSM     0x0D
+
+static const guid_t pci_acpi_dsm_guid =
+               GUID_INIT(0xe5c937d0, 0x3553, 0x4d7a,
+                         0x91, 0x17, 0xea, 0x4d, 0x19, 0xc3, 0x43, 0x4d);
+#endif
+
 static const char * const rp_pio_error_string[] = {
        "Configuration Request received UR Completion",  /* Bit Position 0  */
        "Configuration Request received CA Completion",  /* Bit Position 1  */
@@ -297,6 +312,167 @@ static irqreturn_t dpc_irq(int irq, void *context)
        return IRQ_HANDLED;
 }
 
+static void dpc_error_resume(struct pci_dev *dev)
+{
+       struct dpc_dev *dpc = to_dpc_dev(dev);
+
+       dpc->error_state = PCI_ERS_RESULT_RECOVERED;
+}
+
+#ifdef CONFIG_ACPI
+
+/*
+ * _DSM wrapper function to enable/disable DPC port.
+ * @dpc   : DPC device structure
+ * @enable: status of DPC port (0 or 1).
+ *
+ * returns 0 on success or errno on failure.
+ */
+static int acpi_enable_dpc_port(struct dpc_dev *dpc, bool enable)
+{
+       union acpi_object *obj;
+       int status;
+       union acpi_object argv4;
+       struct pci_dev *pdev = dpc->dev->port;
+
+       dev_dbg(&pdev->dev, "Enable DPC port, value:%d\n", enable);
+
+       argv4.type = ACPI_TYPE_INTEGER;
+       argv4.integer.value = enable;
+
+       obj = acpi_evaluate_dsm(dpc->adev->handle, &pci_acpi_dsm_guid, 1,
+                               EDR_PORT_ENABLE_DSM, &argv4);
+       if (!obj)
+               return -EIO;
+
+       if (obj->type == ACPI_TYPE_INTEGER && obj->integer.value == enable)
+               status = 0;
+       else
+               status = -EIO;
+
+       ACPI_FREE(obj);
+
+       return status;
+}
+
+/*
+ * _DSM wrapper function to locate DPC port.
+ * @dpc   : DPC device structure
+ *
+ * returns pci_dev.
+ */
+static struct pci_dev *acpi_locate_dpc_port(struct dpc_dev *dpc)
+{
+       union acpi_object *obj;
+       u16 port;
+       struct pci_dev *pdev = dpc->dev->port;
+
+       dev_dbg(&pdev->dev, "Locate DPC port\n");
+
+       obj = acpi_evaluate_dsm(dpc->adev->handle, &pci_acpi_dsm_guid, 1,
+                               EDR_PORT_LOCATE_DSM, NULL);
+       if (!obj)
+               return dpc->dev->port;
+
+       if (obj->type == ACPI_TYPE_INTEGER) {
+               /*
+                * Firmware returns DPC port BDF details in following format:
+                *      15:8 = bus
+                *      7:3 = device
+                *      2:0 = function
+                */
+               port = obj->integer.value;
+               ACPI_FREE(obj);
+       } else {
+               ACPI_FREE(obj);
+               return dpc->dev->port;
+       }
+
+       return pci_get_domain_bus_and_slot(0, PCI_BUS_NUM(port), port & 0xff);
+}
+
+/*
+ * _OST wrapper function to let firmware know the status of EDR event.
+ * @dpc   : DPC device structure.
+ * @status: Status of EDR event.
+ */
+static int acpi_send_edr_status(struct dpc_dev *dpc,  u16 status)
+{
+       u32 ost_status;
+       struct pci_dev *pdev = dpc->dev->port;
+
+       dev_dbg(&pdev->dev, "Sending EDR status :%x\n", status);
+
+       ost_status =  PCI_DEVID(pdev->bus->number, pdev->devfn);
+       ost_status = (ost_status << 16) | status;
+
+       status = acpi_evaluate_ost(dpc->adev->handle,
+                                  ACPI_NOTIFY_DISCONNECT_RECOVER,
+                                  ost_status, NULL);
+       if (ACPI_FAILURE(status))
+               return -EINVAL;
+
+       return 0;
+}
+
+static void edr_handle_event(acpi_handle handle, u32 event, void *data)
+{
+       struct dpc_dev *dpc = (struct dpc_dev *) data;
+       struct pci_dev *pdev = dpc->dev->port;
+       u16 status, cap;
+
+       dev_info(&pdev->dev, "ACPI event %x received\n", event);
+
+       if (event != ACPI_NOTIFY_DISCONNECT_RECOVER)
+               return;
+
+       dev_info(&pdev->dev, "Valid EDR event received\n");
+
+       /*
+        * Check if _DSM(0xD) is available, and if present locate the
+        * port which issued EDR event.
+        */
+       pdev = acpi_locate_dpc_port(dpc);
+       if (!pdev) {
+               pdev = dpc->dev->port;
+               dev_err(&pdev->dev, "No valid port found\n");
+               return;
+       }
+
+       dpc = to_dpc_dev(pdev);
+       dpc->error_state = PCI_ERS_RESULT_DISCONNECT;
+       cap = dpc->cap_pos;
+
+       /*
+        * Check if the port supports DPC:
+        *
+        * If port supports DPC, then fall back to default error
+        * recovery.
+        */
+       if (cap) {
+               /* Check if there is a valid DPC trigger */
+               pci_read_config_word(pdev, cap + PCI_EXP_DPC_STATUS, &status);
+               if (!(status & PCI_EXP_DPC_STATUS_TRIGGER)) {
+                       dev_err(&pdev->dev, "Invalid DPC trigger %x\n", status);
+                       return;
+               }
+               dpc_process_error(dpc);
+       }
+
+       /*
+        * If recovery is successful, send _OST(0xF, BDF << 16 | 0x80)
+        * to firmware. If not successful, send _OST(0xF, BDF << 16 | 0x81).
+        */
+       if (dpc->error_state == PCI_ERS_RESULT_RECOVERED)
+               status = 0x80;
+       else
+               status = 0x81;
+
+       acpi_send_edr_status(dpc, status);
+}
+
+#endif
+
 #define FLAG(x, y) (((x) & (y)) ? '+' : '-')
 static int dpc_probe(struct pcie_device *dev)
 {
@@ -305,6 +481,10 @@ static int dpc_probe(struct pcie_device *dev)
        struct device *device = &dev->device;
        int status;
        u16 ctl, cap;
+#ifdef CONFIG_ACPI
+       struct acpi_device *adev = ACPI_COMPANION(&pdev->dev);
+       acpi_status astatus;
+#endif
 
        dpc = devm_kzalloc(device, sizeof(*dpc), GFP_KERNEL);
        if (!dpc)
@@ -313,6 +493,7 @@ static int dpc_probe(struct pcie_device *dev)
        dpc->cap_pos = pci_find_ext_capability(pdev, PCI_EXT_CAP_ID_DPC);
        dpc->dev = dev;
        set_service_data(dev, dpc);
+       dpc->error_state = PCI_ERS_RESULT_NONE;
 
        if (pcie_aer_get_firmware_first(pdev))
                dpc->firmware_dpc = 1;
@@ -335,6 +516,30 @@ static int dpc_probe(struct pcie_device *dev)
                }
        }
 
+#ifdef CONFIG_ACPI
+       if (dpc->firmware_dpc) {
+               if (!adev) {
+                       dev_err(device, "No valid ACPI device found\n");
+                       return -ENODEV;
+               }
+
+               dpc->adev = adev;
+
+               /* Register notifier for ACPI_SYSTEM_NOTIFY events */
+               astatus = acpi_install_notify_handler(adev->handle,
+                                                     ACPI_SYSTEM_NOTIFY,
+                                                     edr_handle_event,
+                                                     dpc);
+
+               if (ACPI_FAILURE(astatus)) {
+                       dev_err(device,
+                               "Install ACPI_SYSTEM_NOTIFY handler failed\n");
+                       return -EBUSY;
+               }
+
+               acpi_enable_dpc_port(dpc, true);
+       }
+#endif
        pci_read_config_word(pdev, dpc->cap_pos + PCI_EXP_DPC_CAP, &cap);
        pci_read_config_word(pdev, dpc->cap_pos + PCI_EXP_DPC_CTL, &ctl);
 
@@ -385,6 +590,7 @@ static struct pcie_port_service_driver dpcdriver = {
        .probe          = dpc_probe,
        .remove         = dpc_remove,
        .reset_link     = dpc_reset_link,
+       .error_resume   = dpc_error_resume,
 };
 
 int __init pcie_dpc_init(void)
diff --git a/drivers/pci/pcie/portdrv_core.c b/drivers/pci/pcie/portdrv_core.c
index 0a59ac574be1..1b54a39df795 100644
--- a/drivers/pci/pcie/portdrv_core.c
+++ b/drivers/pci/pcie/portdrv_core.c
@@ -250,9 +250,14 @@ static int get_port_device_capability(struct pci_dev *dev)
                pcie_pme_interrupt_enable(dev, false);
        }
 
+       /*
+        * If EDR support is enabled in OS, then even if AER is not handled in
+        * OS, DPC service can be enabled.
+        */
        if (pci_find_ext_capability(dev, PCI_EXT_CAP_ID_DPC) &&
-           pci_aer_available() && services & PCIE_PORT_SERVICE_AER &&
-           (pcie_ports_native || host->native_dpc))
+           ((IS_ENABLED(CONFIG_PCIE_EDR) && !host->native_dpc) ||
+           (pci_aer_available() && services & PCIE_PORT_SERVICE_AER &&
+           (pcie_ports_native || host->native_dpc))))
                services |= PCIE_PORT_SERVICE_DPC;
 
        if (pci_pcie_type(dev) == PCI_EXP_TYPE_DOWNSTREAM ||
-- 
2.20.1

Reply via email to