[PATCH v1] PCI: pciehp: Make sure DPC trigger status is reset in PDC handler

From: Kuppuswamy Sathyanarayanan
Date: Thu Jun 15 2023 - 02:26:20 EST


During the EDR-based DPC recovery process, for devices with persistent
issues, the firmware may choose not to handle the DPC error and leave
the port in DPC triggered state. In such scenarios, if the user
replaces the faulty device with a new one, the OS is expected to clear
the DPC trigger status in the hotplug error handler to enable the new
device enumeration. More details about this issue can be found in PCIe
firmware specification, r3.3, sec titled "DPC Event Handling"
Implementation note.

Similar issue might also happen if the DPC or EDR recovery handler
exits before clearing the trigger status. To fix this issue, clear the
DPC trigger status in PDC interrupt handler.

Signed-off-by: Kuppuswamy Sathyanarayanan <sathyanarayanan.kuppuswamy@xxxxxxxxxxxxxxx>
Reported-by: Thatchanamurthy Satish <Satish.Thatchanamurt@xxxxxxxx>
---
drivers/pci/hotplug/pciehp_hpc.c | 6 +++++
drivers/pci/pci.h | 4 ++++
drivers/pci/pcie/dpc.c | 40 ++++++++++++++++++++++++++++++++
3 files changed, 50 insertions(+)

diff --git a/drivers/pci/hotplug/pciehp_hpc.c b/drivers/pci/hotplug/pciehp_hpc.c
index f8c70115b691..03f8f18a6cf5 100644
--- a/drivers/pci/hotplug/pciehp_hpc.c
+++ b/drivers/pci/hotplug/pciehp_hpc.c
@@ -735,6 +735,12 @@ static irqreturn_t pciehp_ist(int irq, void *dev_id)
PCI_EXP_SLTCTL_ATTN_IND_ON);
}

+ /* Clear DPC trigger status */
+ if ((events & PCI_EXP_SLTSTA_PDC) && pci_dpc_is_triggered(pdev)) {
+ ctrl_dbg(ctrl, "Slot(%s): Clear DPC trigger\n", slot_name(ctrl));
+ pci_dpc_reset_trigger(pdev);
+ }
+
/*
* Ignore Link Down/Up events caused by Downstream Port Containment
* if recovery from the error succeeded.
diff --git a/drivers/pci/pci.h b/drivers/pci/pci.h
index 2475098f6518..bbe70e9ab747 100644
--- a/drivers/pci/pci.h
+++ b/drivers/pci/pci.h
@@ -426,11 +426,15 @@ void pci_dpc_init(struct pci_dev *pdev);
void dpc_process_error(struct pci_dev *pdev);
pci_ers_result_t dpc_reset_link(struct pci_dev *pdev);
bool pci_dpc_recovered(struct pci_dev *pdev);
+bool pci_dpc_is_triggered(struct pci_dev *pdev);
+void pci_dpc_reset_trigger(struct pci_dev *pdev);
#else
static inline void pci_save_dpc_state(struct pci_dev *dev) {}
static inline void pci_restore_dpc_state(struct pci_dev *dev) {}
static inline void pci_dpc_init(struct pci_dev *pdev) {}
static inline bool pci_dpc_recovered(struct pci_dev *pdev) { return false; }
+static inline bool pci_dpc_is_triggered(struct pci_dev *pdev) { return false; }
+static inline void pci_dpc_reset_trigger(struct pci_dev *pdev) {}
#endif

#ifdef CONFIG_PCIEPORTBUS
diff --git a/drivers/pci/pcie/dpc.c b/drivers/pci/pcie/dpc.c
index 3ceed8e3de41..ccdc90d37f6d 100644
--- a/drivers/pci/pcie/dpc.c
+++ b/drivers/pci/pcie/dpc.c
@@ -88,6 +88,46 @@ static bool dpc_completed(struct pci_dev *pdev)
return true;
}

+/**
+ * pci_dpc_is_triggered - Check for DPC trigger status
+ * @pdev: PCI device
+ *
+ * It is called from the PCIe hotplug driver to check for DPC
+ * trigger status.
+ *
+ * Return True if DPC is triggered or false for other cases.
+ */
+bool pci_dpc_is_triggered(struct pci_dev *pdev)
+{
+ u16 status;
+
+ if (!pdev->dpc_cap)
+ return false;
+
+ pci_read_config_word(pdev, pdev->dpc_cap + PCI_EXP_DPC_STATUS, &status);
+
+ if ((!PCI_POSSIBLE_ERROR(status)) && (status & PCI_EXP_DPC_STATUS_TRIGGER))
+ return true;
+
+ return false;
+}
+
+/**
+ * pci_reset_trigger - Clear DPC trigger status
+ * @pdev: PCI device
+ *
+ * It is called from the PCIe hotplug driver to clean the DPC
+ * trigger status in the PDC interrupt handler.
+ */
+void pci_dpc_reset_trigger(struct pci_dev *pdev)
+{
+ if (!pdev->dpc_cap)
+ return;
+
+ pci_write_config_word(pdev, pdev->dpc_cap + PCI_EXP_DPC_STATUS,
+ PCI_EXP_DPC_STATUS_TRIGGER);
+}
+
/**
* pci_dpc_recovered - whether DPC triggered and has recovered successfully
* @pdev: PCI device
--
2.34.1