PCI: Work around PCIe link training failures
authorMaciej W. Rozycki <macro@orcam.me.uk>
Sun, 11 Jun 2023 17:20:10 +0000 (18:20 +0100)
committerBjorn Helgaas <bhelgaas@google.com>
Tue, 20 Jun 2023 15:58:53 +0000 (10:58 -0500)
Attempt to handle cases such as with a downstream port of the ASMedia
ASM2824 PCIe switch where link training never completes and the link
continues switching between speeds indefinitely with the data link layer
never reaching the active state.

It has been observed with a downstream port of the ASMedia ASM2824 Gen 3
switch wired to the upstream port of the Pericom PI7C9X2G304 Gen 2 switch,
using a Delock Riser Card PCI Express x1 > 2 x PCIe x1 device, P/N 41433,
wired to a SiFive HiFive Unmatched board.  In this setup the switches
should negotiate a link speed of 5.0GT/s, falling back to 2.5GT/s if
necessary.

Instead the link continues oscillating between the two speeds, at the rate
of 34-35 times per second, with link training reported repeatedly active
~84% of the time.  Limiting the target link speed to 2.5GT/s with the
upstream ASM2824 device makes the two switches communicate correctly.
Removing the speed restriction afterwards makes the two devices switch to
5.0GT/s then.

Make use of these observations and detect the inability to train the link
by checking for the Data Link Layer Link Active status bit being off while
the Link Bandwidth Management Status indicating that hardware has changed
the link speed or width in an attempt to correct unreliable link operation.

Restrict the speed to 2.5GT/s then with the Target Link Speed field,
request a retrain and wait 200ms for the data link to go up.  If this is
successful, lift the restriction, letting the devices negotiate a higher
speed.

Also check for a 2.5GT/s speed restriction the firmware may have already
arranged and lift it too with ports of devices known to continue working
afterwards (currently only ASM2824), that already report their data link
being up.

[bhelgaas: reorder and squash stubs from
https://lore.kernel.org/r/alpine.DEB.2.21.2306111619570.64925@angie.orcam.me.uk
to avoid adding stubs that do nothing]
Link: https://lore.kernel.org/r/alpine.DEB.2.21.2203022037020.56670@angie.orcam.me.uk/
Link: https://source.denx.de/u-boot/u-boot/-/commit/a398a51ccc68
Link: https://lore.kernel.org/r/alpine.DEB.2.21.2305310038540.59226@angie.orcam.me.uk
Signed-off-by: Maciej W. Rozycki <macro@orcam.me.uk>
Signed-off-by: Bjorn Helgaas <bhelgaas@google.com>
drivers/pci/pci.c
drivers/pci/pci.h
drivers/pci/probe.c
drivers/pci/quirks.c

index 62c3a8b..f599d32 100644 (file)
@@ -4951,6 +4951,8 @@ static bool pcie_wait_for_link_delay(struct pci_dev *pdev, bool active,
        if (active)
                msleep(20);
        ret = pcie_wait_for_link_status(pdev, false, active);
+       if (active && !ret)
+               ret = pcie_failed_link_retrain(pdev);
        if (active && ret)
                msleep(delay);
 
index 6c257ac..e3a468a 100644 (file)
@@ -543,6 +543,7 @@ void pci_acs_init(struct pci_dev *dev);
 int pci_dev_specific_acs_enabled(struct pci_dev *dev, u16 acs_flags);
 int pci_dev_specific_enable_acs(struct pci_dev *dev);
 int pci_dev_specific_disable_acs_redir(struct pci_dev *dev);
+bool pcie_failed_link_retrain(struct pci_dev *dev);
 #else
 static inline int pci_dev_specific_acs_enabled(struct pci_dev *dev,
                                               u16 acs_flags)
@@ -557,6 +558,10 @@ static inline int pci_dev_specific_disable_acs_redir(struct pci_dev *dev)
 {
        return -ENOTTY;
 }
+static inline bool pcie_failed_link_retrain(struct pci_dev *dev)
+{
+       return false;
+}
 #endif
 
 /* PCI error reporting and recovery */
index 782925b..f547db0 100644 (file)
@@ -2550,6 +2550,8 @@ void pci_device_add(struct pci_dev *dev, struct pci_bus *bus)
        dma_set_max_seg_size(&dev->dev, 65536);
        dma_set_seg_boundary(&dev->dev, 0xffffffff);
 
+       pcie_failed_link_retrain(dev);
+
        /* Fix up broken headers */
        pci_fixup_device(pci_fixup_header, dev);
 
index 5c0a438..a466785 100644 (file)
 #include <linux/switchtec.h>
 #include "pci.h"
 
+/*
+ * Retrain the link of a downstream PCIe port by hand if necessary.
+ *
+ * This is needed at least where a downstream port of the ASMedia ASM2824
+ * Gen 3 switch is wired to the upstream port of the Pericom PI7C9X2G304
+ * Gen 2 switch, and observed with the Delock Riser Card PCI Express x1 >
+ * 2 x PCIe x1 device, P/N 41433, plugged into the SiFive HiFive Unmatched
+ * board.
+ *
+ * In such a configuration the switches are supposed to negotiate the link
+ * speed of preferably 5.0GT/s, falling back to 2.5GT/s.  However the link
+ * continues switching between the two speeds indefinitely and the data
+ * link layer never reaches the active state, with link training reported
+ * repeatedly active ~84% of the time.  Forcing the target link speed to
+ * 2.5GT/s with the upstream ASM2824 device makes the two switches talk to
+ * each other correctly however.  And more interestingly retraining with a
+ * higher target link speed afterwards lets the two successfully negotiate
+ * 5.0GT/s.
+ *
+ * With the ASM2824 we can rely on the otherwise optional Data Link Layer
+ * Link Active status bit and in the failed link training scenario it will
+ * be off along with the Link Bandwidth Management Status indicating that
+ * hardware has changed the link speed or width in an attempt to correct
+ * unreliable link operation.  For a port that has been left unconnected
+ * both bits will be clear.  So use this information to detect the problem
+ * rather than polling the Link Training bit and watching out for flips or
+ * at least the active status.
+ *
+ * Since the exact nature of the problem isn't known and in principle this
+ * could trigger where an ASM2824 device is downstream rather upstream,
+ * apply this erratum workaround to any downstream ports as long as they
+ * support Link Active reporting and have the Link Control 2 register.
+ * Restrict the speed to 2.5GT/s then with the Target Link Speed field,
+ * request a retrain and wait 200ms for the data link to go up.
+ *
+ * If this turns out successful and we know by the Vendor:Device ID it is
+ * safe to do so, then lift the restriction, letting the devices negotiate
+ * a higher speed.  Also check for a similar 2.5GT/s speed restriction the
+ * firmware may have already arranged and lift it with ports that already
+ * report their data link being up.
+ *
+ * Return TRUE if the link has been successfully retrained, otherwise FALSE.
+ */
+bool pcie_failed_link_retrain(struct pci_dev *dev)
+{
+       static const struct pci_device_id ids[] = {
+               { PCI_VDEVICE(ASMEDIA, 0x2824) }, /* ASMedia ASM2824 */
+               {}
+       };
+       u16 lnksta, lnkctl2;
+
+       if (!pci_is_pcie(dev) || !pcie_downstream_port(dev) ||
+           !pcie_cap_has_lnkctl2(dev) || !dev->link_active_reporting)
+               return false;
+
+       pcie_capability_read_word(dev, PCI_EXP_LNKCTL2, &lnkctl2);
+       pcie_capability_read_word(dev, PCI_EXP_LNKSTA, &lnksta);
+       if ((lnksta & (PCI_EXP_LNKSTA_LBMS | PCI_EXP_LNKSTA_DLLLA)) ==
+           PCI_EXP_LNKSTA_LBMS) {
+               pci_info(dev, "broken device, retraining non-functional downstream link at 2.5GT/s\n");
+
+               lnkctl2 &= ~PCI_EXP_LNKCTL2_TLS;
+               lnkctl2 |= PCI_EXP_LNKCTL2_TLS_2_5GT;
+               pcie_capability_write_word(dev, PCI_EXP_LNKCTL2, lnkctl2);
+
+               if (!pcie_retrain_link(dev, false)) {
+                       pci_info(dev, "retraining failed\n");
+                       return false;
+               }
+
+               pcie_capability_read_word(dev, PCI_EXP_LNKSTA, &lnksta);
+       }
+
+       if ((lnksta & PCI_EXP_LNKSTA_DLLLA) &&
+           (lnkctl2 & PCI_EXP_LNKCTL2_TLS) == PCI_EXP_LNKCTL2_TLS_2_5GT &&
+           pci_match_id(ids, dev)) {
+               u32 lnkcap;
+
+               pci_info(dev, "removing 2.5GT/s downstream link speed restriction\n");
+               pcie_capability_read_dword(dev, PCI_EXP_LNKCAP, &lnkcap);
+               lnkctl2 &= ~PCI_EXP_LNKCTL2_TLS;
+               lnkctl2 |= lnkcap & PCI_EXP_LNKCAP_SLS;
+               pcie_capability_write_word(dev, PCI_EXP_LNKCTL2, lnkctl2);
+
+               if (!pcie_retrain_link(dev, false)) {
+                       pci_info(dev, "retraining failed\n");
+                       return false;
+               }
+       }
+
+       return true;
+}
+
 static ktime_t fixup_debug_start(struct pci_dev *dev,
                                 void (*fn)(struct pci_dev *dev))
 {