[PATCH v5 1/6] PCI/bwctrl: Set host bridge OPP and optionally disable ASPM around link retraining
From: Krishna Chaitanya Chundru
Date: Wed Aug 19 2026 - 09:29:31 EST
PCIe host bridge controllers may need their operating point raised before
retraining to a higher link speed so that hardware resources (e.g., RPMh
votes on Qualcomm platforms) are available at the requested data rate.
After retraining, the operating point must be updated to reflect the
actual negotiated speed.
Add pcie_set_opp() to look up an OPP on the host bridge parent device
using a key of (per-lane frequency in kHz, LNKCTL2 Target Link Speed
level). Keying by generation rather than total bandwidth lets OPP tables
remain width-independent.
In pcie_set_target_speed(), call pcie_set_opp() before retraining only
when upscaling (speed_req > cur_bus_speed), since only raising the
operating point requires pre-staging hardware. After retraining, call
pcie_set_opp() unconditionally with the actual cur_bus_speed to settle
the votes. Both calls are skipped for downstream ports of PCIe switches,
as those are outside the host controller's scope.
Some controllers also require ASPM to be disabled around link retraining.
Add a disable_aspm_for_retrain flag to pci_host_bridge; when set,
pcie_set_target_speed() saves the child device's ASPM state, disables all
ASPM link states before retraining, and restores them afterward.
Signed-off-by: Krishna Chaitanya Chundru <krishna.chundru@xxxxxxxxxxxxxxxx>
---
drivers/pci/pcie/bwctrl.c | 63 ++++++++++++++++++++++++++++++++++++++++++++++-
include/linux/pci.h | 1 +
2 files changed, 63 insertions(+), 1 deletion(-)
diff --git a/drivers/pci/pcie/bwctrl.c b/drivers/pci/pcie/bwctrl.c
index c4c8d260bf96..6fa1522c47db 100644
--- a/drivers/pci/pcie/bwctrl.c
+++ b/drivers/pci/pcie/bwctrl.c
@@ -28,9 +28,11 @@
#include <linux/mutex.h>
#include <linux/pci.h>
#include <linux/pci-bwctrl.h>
+#include <linux/pm_opp.h>
#include <linux/rwsem.h>
#include <linux/slab.h>
#include <linux/types.h>
+#include <linux/units.h>
#include "../pci.h"
#include "portdrv.h"
@@ -120,6 +122,38 @@ static int pcie_bwctrl_change_speed(struct pci_dev *port, u16 target_speed, bool
return pcie_retrain_link(port, use_lt);
}
+static int pcie_set_opp(struct pci_dev *pdev, struct pci_host_bridge *host,
+ enum pci_bus_speed speed)
+{
+ struct device *dev = host->dev.parent;
+ struct dev_pm_opp_key key = {};
+ int ret, freq_mbps, width;
+ unsigned long freq_kbps;
+ struct dev_pm_opp *opp;
+ u16 lnksta;
+
+ pcie_capability_read_word(pdev, PCI_EXP_LNKSTA, &lnksta);
+ width = FIELD_GET(PCI_EXP_LNKSTA_NLW, lnksta);
+
+ freq_mbps = pcie_dev_speed_mbps(speed);
+ if (freq_mbps < 0)
+ return -EINVAL;
+
+ freq_kbps = freq_mbps * KILO;
+ key.freq = freq_kbps;
+ key.level = pci_bus_speed2lnkctl2(speed);
+ key.bw = 0;
+ opp = dev_pm_opp_find_key_exact(dev, &key, true);
+ if (!IS_ERR(opp)) {
+ ret = dev_pm_opp_set_opp(dev, opp);
+ if (ret)
+ dev_err(dev, "Failed to set OPP for freq (%lu): %d\n",
+ freq_kbps * width, ret);
+ dev_pm_opp_put(opp);
+ }
+ return 0;
+}
+
/**
* pcie_set_target_speed - Set downstream Link Speed for PCIe Port
* @port: PCIe Port
@@ -140,9 +174,12 @@ static int pcie_bwctrl_change_speed(struct pci_dev *port, u16 target_speed, bool
int pcie_set_target_speed(struct pci_dev *port, enum pci_bus_speed speed_req,
bool use_lt)
{
+ struct pci_host_bridge *host = pci_find_host_bridge(port->bus);
+ bool is_rootbus = pci_is_root_bus(port->bus);
struct pci_bus *bus = port->subordinate;
+ struct pci_dev *child = NULL;
+ int aspm_state = 0, ret;
u16 target_speed;
- int ret;
if (WARN_ON_ONCE(!pcie_valid_speed(speed_req)))
return -EINVAL;
@@ -152,6 +189,24 @@ int pcie_set_target_speed(struct pci_dev *port, enum pci_bus_speed speed_req,
target_speed = pcie_bwctrl_select_speed(port, speed_req);
+ /*
+ * The host bridge driver may need to be scaled for targeted speed
+ * otherwise link might not come up at requested speed.
+ */
+ if (is_rootbus && host && bus) {
+ /* Get function 0 of downstream device */
+ list_for_each_entry(child, &bus->devices, bus_list)
+ if (PCI_FUNC(child->devfn) == 0)
+ break;
+
+ if (child && host->disable_aspm_for_retrain) {
+ aspm_state = pcie_aspm_enabled(child);
+ pci_disable_link_state_locked(child, PCIE_LINK_STATE_ALL);
+ }
+ if (speed_req > bus->cur_bus_speed)
+ pcie_set_opp(port, host, speed_req);
+ }
+
scoped_guard(rwsem_read, &pcie_bwctrl_setspeed_rwsem) {
struct pcie_bwctrl_data *data = port->link_bwctrl;
@@ -176,6 +231,12 @@ int pcie_set_target_speed(struct pci_dev *port, enum pci_bus_speed speed_req,
!list_empty(&bus->devices))
ret = -EAGAIN;
+ if (bus && is_rootbus && host) {
+ if (child && host->disable_aspm_for_retrain)
+ pci_enable_link_state_locked(child, aspm_state);
+ pcie_set_opp(port, host, bus->cur_bus_speed);
+ }
+
return ret;
}
diff --git a/include/linux/pci.h b/include/linux/pci.h
index 18e814064b51..d1f2d382189d 100644
--- a/include/linux/pci.h
+++ b/include/linux/pci.h
@@ -662,6 +662,7 @@ struct pci_host_bridge {
unsigned int msi_domain:1; /* Bridge wants MSI domain */
unsigned int broken_l1ss_resume:1; /* Resuming from L1SS during
system suspend is broken */
+ unsigned int disable_aspm_for_retrain:1; /* Disable ASPM before link retain */
/* Resource alignment requirements */
resource_size_t (*align_resource)(struct pci_dev *dev,
--
2.34.1