[PATCH v3] PCI/ASPM: Mask ASPM states based on Devicetree properties

From: Krishna Chaitanya Chundru

Date: Fri Jul 03 2026 - 22:50:27 EST


Some platforms require selectively disabling specific ASPM states on a
given PCIe link to avoid link instability or functional failures caused
by board-level connectivity constraints such as PCB routing, connectors,
slots, or external cabling.

Devicetree supports disabling ASPM L0s, L1, and L1 PM Substates via the
'aspm-no-l0s', 'aspm-no-l1' [1], and 'aspm-no-l1ss' [2] properties.
However, the ASPM driver does not currently honor these properties when
initializing the default link state.

When firmware enables L1 PM Substates before the kernel takes over,
masking aspm_support alone is insufficient to disable them in hardware.
pcie_config_aspm_link() guards L1SS configuration behind a check on
aspm_capable, which is derived from aspm_support. Once aspm_support is
masked, pcie_config_aspm_l1ss() is never called, leaving
firmware-enabled L1SS substates active in hardware.

Fix this by introducing pcie_link_has_aspm_override() to check for DT
override properties on either endpoint of the link. In
pcie_aspm_override_default_link_state(), use it to:

- Mask aspm_support and aspm_default for any disabled state.
- Explicitly call pcie_config_aspm_l1ss(link, 0) before masking
aspm_support when firmware has L1SS active and DT requests disabling
L1 or L1SS, since pcie_config_aspm_link() will no longer do so once
aspm_capable is derived from the masked aspm_support.

Move the aspm_default initialization and
pcie_aspm_override_default_link_state() call in pcie_aspm_cap_init() to
before the "Restore L0s/L1" block. pcie_aspm_cap_init() disables L1 in
hardware prior to aspm_l1ss_init() and re-enables it only in the
restore block. Calling pcie_config_aspm_l1ss() while L1 is already
disabled satisfies its precondition ("Caller must disable L1 first"),
whereas the previous placement after the restore violated it.

Move pcie_config_aspm_l1ss() earlier in the file so it can be called
from pcie_aspm_override_default_link_state().

Link [1]: https://github.com/devicetree-org/dt-schema/pull/188
Link [2]: https://github.com/devicetree-org/dt-schema/pull/190

Signed-off-by: Krishna Chaitanya Chundru <krishna.chundru@xxxxxxxxxxxxxxxx>
---
Changes in v3:
- Move pcie_aspm_override_default_link_state() call in pcie_aspm_cap_init() to
before the "Restore L0s/L1" block. pcie_aspm_cap_init() disables L1 in
hardware prior to aspm_l1ss_init() and re-enables it only in the
restore block. Calling pcie_config_aspm_l1ss() while L1 is already
disabled satisfies its precondition ("Caller must disable L1 first"),
whereas the previous placement after the restore violated it (sashiko).
- Link to v2: https://patch.msgid.link/20260624-aspm-v2-1-800a4151ba3a@xxxxxxxxxxxxxxxx

Changes in v2:
- Disable L1ss when L1 is disabled as pointed by sashiko.
- Disable L1ss if bootloader enables them but we are disabling via
devicetree pointed by sashiko.
- Link to v1: https://patch.msgid.link/20260511-aspm-v1-1-b4a9fe955cf9@xxxxxxxxxxxxxxxx
---
Changes in v3:
- Move pcie_aspm_override_default_link_state() to before the LNKCTL
restore block so that L1 is already disabled in hardware when
pcie_config_aspm_l1ss() is called, satisfying its precondition pointed
by sashiko.
- Link to v2: https://patch.msgid.link/20260624-aspm-v2-1-800a4151ba3a@xxxxxxxxxxxxxxxx

Changes in v2:
- Disable L1ss when L1 is disabled as pointed by sashiko.
- Disable L1ss if bootloader enables them but we are disabling via
devicetree pointed by sashiko.
- Link to v1: https://patch.msgid.link/20260511-aspm-v1-1-b4a9fe955cf9@xxxxxxxxxxxxxxxx
---
drivers/pci/pcie/aspm.c | 116 +++++++++++++++++++++++++++++++-----------------
1 file changed, 75 insertions(+), 41 deletions(-)

diff --git a/drivers/pci/pcie/aspm.c b/drivers/pci/pcie/aspm.c
index 172783e7f519..f00285ee6be6 100644
--- a/drivers/pci/pcie/aspm.c
+++ b/drivers/pci/pcie/aspm.c
@@ -839,6 +839,49 @@ static void aspm_l1ss_init(struct pcie_link_state *link)

#define FLAG(x, y, d) (((x) & (PCIE_LINK_STATE_##y)) ? d : "")

+/* Configure the ASPM L1 substates. Caller must disable L1 first. */
+static void pcie_config_aspm_l1ss(struct pcie_link_state *link, u32 state)
+{
+ u32 val = 0;
+ struct pci_dev *child = link->downstream, *parent = link->pdev;
+
+ if (state & PCIE_LINK_STATE_L1_1)
+ val |= PCI_L1SS_CTL1_ASPM_L1_1;
+ if (state & PCIE_LINK_STATE_L1_2)
+ val |= PCI_L1SS_CTL1_ASPM_L1_2;
+ if (state & PCIE_LINK_STATE_L1_1_PCIPM)
+ val |= PCI_L1SS_CTL1_PCIPM_L1_1;
+ if (state & PCIE_LINK_STATE_L1_2_PCIPM)
+ val |= PCI_L1SS_CTL1_PCIPM_L1_2;
+
+ /*
+ * PCIe r6.2, sec 5.5.4, rules for enabling L1 PM Substates:
+ * - Clear L1.x enable bits at child first, then at parent
+ * - Set L1.x enable bits at parent first, then at child
+ * - ASPM/PCIPM L1.2 must be disabled while programming timing
+ * parameters
+ */
+
+ /* Disable all L1 substates */
+ pci_clear_and_set_config_dword(child, child->l1ss + PCI_L1SS_CTL1,
+ PCI_L1SS_CTL1_L1SS_MASK, 0);
+ pci_clear_and_set_config_dword(parent, parent->l1ss + PCI_L1SS_CTL1,
+ PCI_L1SS_CTL1_L1SS_MASK, 0);
+
+ /* Enable what we need to enable */
+ pci_clear_and_set_config_dword(parent, parent->l1ss + PCI_L1SS_CTL1,
+ PCI_L1SS_CTL1_L1SS_MASK, val);
+ pci_clear_and_set_config_dword(child, child->l1ss + PCI_L1SS_CTL1,
+ PCI_L1SS_CTL1_L1SS_MASK, val);
+}
+
+static bool pcie_link_has_aspm_override(const struct pcie_link_state *link,
+ const char *aspm)
+{
+ return (device_property_present(&link->pdev->dev, aspm) ||
+ device_property_present(&link->downstream->dev, aspm));
+}
+
static void pcie_aspm_override_default_link_state(struct pcie_link_state *link)
{
struct pci_dev *pdev = link->downstream;
@@ -846,6 +889,33 @@ static void pcie_aspm_override_default_link_state(struct pcie_link_state *link)

/* For devicetree platforms, enable L0s and L1 by default */
if (of_have_populated_dt()) {
+ bool no_l0s = pcie_link_has_aspm_override(link, "aspm-no-l0s");
+ bool no_l1 = pcie_link_has_aspm_override(link, "aspm-no-l1");
+ bool no_l1ss = pcie_link_has_aspm_override(link, "aspm-no-l1ss");
+
+ if (no_l0s) {
+ link->aspm_support &= ~PCIE_LINK_STATE_L0S;
+ link->aspm_default &= ~PCIE_LINK_STATE_L0S;
+ }
+
+ /*
+ * Clear L1SS in hardware before updating aspm_support. Once
+ * aspm_capable is derived from aspm_support, pcie_config_aspm_link()
+ * skips pcie_config_aspm_l1ss() entirely via the aspm_capable guard,
+ * leaving firmware-enabled L1SS substates active in hardware.
+ * This applies equally when disabling L1 (which implies L1SS).
+ */
+ if ((no_l1 || no_l1ss) && (link->aspm_enabled & PCIE_LINK_STATE_L1SS))
+ pcie_config_aspm_l1ss(link, 0);
+
+ if (no_l1) {
+ link->aspm_support &= ~(PCIE_LINK_STATE_L1 | PCIE_LINK_STATE_L1SS);
+ link->aspm_default &= ~(PCIE_LINK_STATE_L1 | PCIE_LINK_STATE_L1SS);
+ } else if (no_l1ss) {
+ link->aspm_support &= ~PCIE_LINK_STATE_L1SS;
+ link->aspm_default &= ~PCIE_LINK_STATE_L1SS;
+ }
+
if (link->aspm_support & PCIE_LINK_STATE_L0S)
link->aspm_default |= PCIE_LINK_STATE_L0S;
if (link->aspm_support & PCIE_LINK_STATE_L1)
@@ -924,6 +994,11 @@ static void pcie_aspm_cap_init(struct pcie_link_state *link, int blacklist)

aspm_l1ss_init(link);

+ /* Save default state */
+ link->aspm_default = link->aspm_enabled;
+
+ pcie_aspm_override_default_link_state(link);
+
/* Restore L0s/L1 if they were enabled */
if (FIELD_GET(PCI_EXP_LNKCTL_ASPMC, child_lnkctl) ||
FIELD_GET(PCI_EXP_LNKCTL_ASPMC, parent_lnkctl)) {
@@ -931,11 +1006,6 @@ static void pcie_aspm_cap_init(struct pcie_link_state *link, int blacklist)
pcie_capability_write_word(child, PCI_EXP_LNKCTL, child_lnkctl);
}

- /* Save default state */
- link->aspm_default = link->aspm_enabled;
-
- pcie_aspm_override_default_link_state(link);
-
/* Setup initial capable state. Will be updated later */
link->aspm_capable = link->aspm_support;

@@ -949,42 +1019,6 @@ static void pcie_aspm_cap_init(struct pcie_link_state *link, int blacklist)
}
}

-/* Configure the ASPM L1 substates. Caller must disable L1 first. */
-static void pcie_config_aspm_l1ss(struct pcie_link_state *link, u32 state)
-{
- u32 val = 0;
- struct pci_dev *child = link->downstream, *parent = link->pdev;
-
- if (state & PCIE_LINK_STATE_L1_1)
- val |= PCI_L1SS_CTL1_ASPM_L1_1;
- if (state & PCIE_LINK_STATE_L1_2)
- val |= PCI_L1SS_CTL1_ASPM_L1_2;
- if (state & PCIE_LINK_STATE_L1_1_PCIPM)
- val |= PCI_L1SS_CTL1_PCIPM_L1_1;
- if (state & PCIE_LINK_STATE_L1_2_PCIPM)
- val |= PCI_L1SS_CTL1_PCIPM_L1_2;
-
- /*
- * PCIe r6.2, sec 5.5.4, rules for enabling L1 PM Substates:
- * - Clear L1.x enable bits at child first, then at parent
- * - Set L1.x enable bits at parent first, then at child
- * - ASPM/PCIPM L1.2 must be disabled while programming timing
- * parameters
- */
-
- /* Disable all L1 substates */
- pci_clear_and_set_config_dword(child, child->l1ss + PCI_L1SS_CTL1,
- PCI_L1SS_CTL1_L1SS_MASK, 0);
- pci_clear_and_set_config_dword(parent, parent->l1ss + PCI_L1SS_CTL1,
- PCI_L1SS_CTL1_L1SS_MASK, 0);
-
- /* Enable what we need to enable */
- pci_clear_and_set_config_dword(parent, parent->l1ss + PCI_L1SS_CTL1,
- PCI_L1SS_CTL1_L1SS_MASK, val);
- pci_clear_and_set_config_dword(child, child->l1ss + PCI_L1SS_CTL1,
- PCI_L1SS_CTL1_L1SS_MASK, val);
-}
-
static void pcie_config_aspm_dev(struct pci_dev *pdev, u32 val)
{
pcie_capability_clear_and_set_word(pdev, PCI_EXP_LNKCTL,

---
base-commit: 71dfdfb0209b43dfd6f494f84f5548e4cfd18cb5
change-id: 20260429-aspm-da55a54279e4

Best regards,
--
Krishna Chaitanya Chundru <krishna.chundru@xxxxxxxxxxxxxxxx>