[PATCH] PCI: dwc: Add sysfs for local loopback interface

Krishna Chaitanya Chundru <[email protected]> Mon, 03 Aug 2026 15:04:15 +0530
Newsgroups org.kernel.vger.linux-pci,org.kernel.vger.linux-kernel
Message-ID <[email protected]>
The PCIe Base Specification (r6.0, sec. 4.2.7.10) defines a Loopback
state in the LTSSM: the Loopback Master sets the Loopback bit in TS1
Ordered Sets, and the Loopback Slave reflects received data back
bit-for-bit.  DesignWare PCIe IP also exposes a local variant, entirely
within the controller and not requiring a connected endpoint, via
PCIE_PIPE_LOOPBACK_CONTROL and PORT_LINK_LOOPBACK_EN.

This patch focuses on local loopback only.  Remote (link-level)
loopback requires the link to be up while the test runs, which means
any client/endpoint drivers using the link would need to be told to
stop transfers beforehand -- up to and including a driver remove -- and
the controller/link would need to be reinitiated afterward.  That
coordination needs further discussion, so it is left for a follow-up.

Add a sysfs attribute group 'loopback/' on the platform device with:

  run      (RW) -- write "local" to run a synchronous, blocking
                   data-integrity test; reads back "idle" or "busy" to
                   indicate test state.  Refused with -EBUSY while the
                   link is up, so it cannot disrupt an active link or
                   other devices behind the same root port.

  buf_size (RW) -- transfer size in bytes, rounded up to the next
                   power-of-two and clamped to a minimum of 4 KiB and a
                   maximum of 1 MiB (default 4 KiB); cannot be changed
                   during a test

The test allocates a coherent DMA buffer, programs an inbound iATU
window to redirect PCIe writes to it, writes random data through the
PCIe window, and compares the result byte-by-byte.

On completion the loopback/link-control DBI bits are restored directly
rather than resetting the whole controller, and the LTSSM is polled for
DETECT_QUIET.  On some platforms it has been observed (and reproduced
on Qualcomm Eliza hardware) to land elsewhere instead and leave the
controller unable to service further runs; in that case the controller
is recovered via the same ops->deinit() + dw_pcie_resume_noirq()
sequence used across a system suspend/resume cycle, which has been
validated to reliably restore the controller across repeated
back-to-back test runs.

The sysfs group is added/removed via device_add_group()/
device_remove_group() rather than the devm_* variant, with removal
called explicitly at the start of dw_pcie_host_deinit() before the
rest of the bridge is torn down, since device_remove_group() blocks
until any in-flight run_store()/run_show() call returns -- closing a
race where driver unbind/remove could otherwise run concurrently with
a sysfs write.

Signed-off-by: Krishna Chaitanya Chundru <[email protected]>
---
 .../testing/sysfs-driver-pcie-designware-loopback  |  69 +++++
 MAINTAINERS                                        |   1 +
 drivers/pci/controller/dwc/pcie-designware-host.c  | 298 +++++++++++++++++++++
 drivers/pci/controller/dwc/pcie-designware.h       |  17 ++
 4 files changed, 385 insertions(+)

diff --git a/Documentation/ABI/testing/sysfs-driver-pcie-designware-loopback b/Documentation/ABI/testing/sysfs-driver-pcie-designware-loopback
new file mode 100644
index 000000000000..096da7800549
--- /dev/null
+++ b/Documentation/ABI/testing/sysfs-driver-pcie-designware-loopback
@@ -0,0 +1,69 @@
+What:		/sys/devices/.../loopback/run
+Date:		August 2026
+KernelVersion:	7.2
+Contact:	[email protected]
+Description:
+		(RW) Diagnostic local loopback test interface for Synopsys
+		DesignWare PCIe root-port controllers.
+
+		Reading this file returns the current test state:
+
+		  - ``idle``  - no test is running
+		  - ``busy``  - a test is in progress
+
+		Writing ``local`` triggers a synchronous, blocking
+		data-integrity test performed entirely within the local
+		controller, without requiring a connected endpoint.  The
+		write does not return until the test completes, and logs
+		``PASSED`` or ``FAILED`` via dev_info().
+
+		The test allocates a coherent DMA buffer, configures an
+		inbound iATU window to redirect PCIe writes to that buffer,
+		writes random data through the PCIe window, and compares
+		the result byte-by-byte against what was written.
+
+		The test only runs while the PCIe link is down; writing
+		``local`` while the link is up returns -EBUSY.  This avoids
+		disrupting an active link and any devices behind it.
+
+		Only one test may run at a time.  Concurrent writes
+		return -EBUSY.
+
+		Example::
+
+		  # Run a local loopback test
+		  $ echo local  > /sys/devices/.../loopback/run
+
+		  # Check whether a test is in progress
+		  $ cat /sys/devices/.../loopback/run
+		  idle
+
+What:		/sys/devices/.../loopback/buf_size
+Date:		August 2026
+KernelVersion:	7.2
+Contact:	[email protected]
+Description:
+		(RW) Size in bytes of the data buffer used by the
+		loopback test.
+
+		Reading this file returns the current buffer size as a
+		decimal integer.
+
+		Writing a decimal integer sets a new buffer size.  The
+		value is rounded up to the next power of two and clamped
+		to a minimum of 4096 (4 KiB) and a maximum of 1048576
+		(1 MiB); values above the maximum are rejected with
+		-EINVAL.  The new size takes effect on the next test run;
+		it cannot be changed while a test is in progress (returns
+		-EBUSY).
+
+		The default value is 4096.
+
+		Example::
+
+		  # Set the test buffer to 64 KiB
+		  $ echo 65536 > /sys/devices/.../loopback/buf_size
+
+		  # Read back the (rounded) value
+		  $ cat /sys/devices/.../loopback/buf_size
+		  65536
diff --git a/MAINTAINERS b/MAINTAINERS
index 0d7987278c07..44a11de7cc8d 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -20761,6 +20761,7 @@ M:	Jingoo Han <[email protected]>
 M:	Manivannan Sadhasivam <[email protected]>
 L:	[email protected]
 S:	Maintained
+F:	Documentation/ABI/testing/sysfs-driver-pcie-designware-loopback
 F:	Documentation/devicetree/bindings/pci/snps,dw-pcie-ep.yaml
 F:	Documentation/devicetree/bindings/pci/snps,dw-pcie.yaml
 F:	drivers/pci/controller/dwc/*designware*
diff --git a/drivers/pci/controller/dwc/pcie-designware-host.c b/drivers/pci/controller/dwc/pcie-designware-host.c
index 06722259d2e3..f0cfe9e88520 100644
--- a/drivers/pci/controller/dwc/pcie-designware-host.c
+++ b/drivers/pci/controller/dwc/pcie-designware-host.c
@@ -19,6 +19,8 @@
 #include <linux/pci.h>
 #include <linux/pci_regs.h>
 #include <linux/platform_device.h>
+#include <linux/random.h>
+#include <linux/sizes.h>
 
 #include "../pci-host-common.h"
 #include "../../pci.h"
@@ -567,6 +569,297 @@ static int dw_pcie_host_get_resources(struct dw_pcie_rp *pp)
 	return 0;
 }
 
+#define DW_PCIE_LB_BUF_SIZE_MIN	SZ_4K
+#define DW_PCIE_LB_BUF_SIZE_MAX	SZ_1M
+#define DW_PCIE_LB_SETTLE_MS		100
+#define DW_PCIE_LB_LTSSM_SETTLE_US	(DW_PCIE_LB_SETTLE_MS * USEC_PER_MSEC)
+#define DW_PCIE_LB_LTSSM_POLL_US	1000
+
+static int dw_pcie_loopback_run(struct dw_pcie_rp *pp, size_t buf_size)
+{
+	struct resource lb_res = { .name = "pcie-loopback",
+				   .flags = IORESOURCE_MEM };
+	struct dw_pcie *pci = to_dw_pcie_from_pp(pp);
+	enum dw_pcie_ltssm ltssm;
+	void __iomem *src_base;
+	u32 plc, gen3, pipe_lb;
+	dma_addr_t dst_dma;
+	void *dst_virt;
+	int ret, rret;
+	void *tx_buf;
+	int ib_index;
+
+	ib_index = pci->num_ib_windows - 1;
+
+	ret = pci_bus_alloc_resource(pp->bridge->bus, &lb_res,
+				     buf_size, buf_size,
+				     PCIBIOS_MIN_MEM, 0,
+				     pcibios_align_resource,
+				     &pp->bridge->dev);
+	if (ret) {
+		dev_err(pci->dev, "loopback: failed to alloc PCIe MEM resource: %d\n", ret);
+		return ret;
+	}
+
+	src_base = ioremap(lb_res.start, buf_size);
+	if (!src_base) {
+		dev_err(pci->dev, "loopback: ioremap of PCIe source window failed\n");
+		ret = -ENOMEM;
+		goto err_release_res;
+	}
+
+	dst_virt = dma_alloc_coherent(pci->dev, buf_size, &dst_dma, GFP_KERNEL);
+	if (!dst_virt) {
+		ret = -ENOMEM;
+		goto err_iounmap;
+	}
+
+	ret = dw_pcie_prog_inbound_atu(pci, ib_index, PCIE_TLP_TYPE_MEM_RDWR,
+				       dst_dma, lb_res.start, buf_size);
+	if (ret) {
+		dev_err(pci->dev, "loopback: inbound iATU programming failed: %d\n", ret);
+		goto err_free_dma;
+	}
+
+	plc = dw_pcie_readl_dbi(pci, PCIE_PORT_LINK_CONTROL);
+
+	gen3 = dw_pcie_readl_dbi(pci, GEN3_RELATED_OFF);
+	dw_pcie_writel_dbi(pci, GEN3_RELATED_OFF,
+			   gen3 | GEN3_RELATED_OFF_GEN3_EQ_DISABLE);
+
+	pipe_lb = dw_pcie_readl_dbi(pci, PCIE_PIPE_LOOPBACK_CONTROL);
+	dw_pcie_writel_dbi(pci, PCIE_PIPE_LOOPBACK_CONTROL,
+			   pipe_lb | PCIE_PIPE_LOOPBACK_EN);
+
+	dw_pcie_writel_dbi(pci, PCIE_PORT_LINK_CONTROL, plc | PORT_LINK_LOOPBACK_EN);
+
+	msleep(DW_PCIE_LB_SETTLE_MS);
+
+	tx_buf = kmalloc(buf_size, GFP_KERNEL);
+	if (!tx_buf) {
+		ret = -ENOMEM;
+		goto err_restore_link;
+	}
+
+	get_random_bytes(tx_buf, buf_size);
+	memcpy_toio(src_base, tx_buf, buf_size);
+	msleep(DW_PCIE_LB_SETTLE_MS);
+
+	if (memcmp(tx_buf, dst_virt, buf_size))
+		ret = -EIO;
+
+	dev_info(pci->dev, "PCIe local loopback test %s\n", ret ? "FAILED" : "PASSED");
+
+	kfree(tx_buf);
+
+err_restore_link:
+	dw_pcie_writel_dbi(pci, PCIE_PORT_LINK_CONTROL, plc);
+	dw_pcie_writel_dbi(pci, PCIE_PIPE_LOOPBACK_CONTROL, pipe_lb);
+	dw_pcie_writel_dbi(pci, GEN3_RELATED_OFF, gen3);
+
+	if (read_poll_timeout(dw_pcie_get_ltssm, ltssm,
+			      ltssm == DW_PCIE_LTSSM_DETECT_QUIET,
+			      DW_PCIE_LB_LTSSM_POLL_US, DW_PCIE_LB_LTSSM_SETTLE_US,
+			      false, pci)) {
+		/*
+		 * LTSSM has been observed stuck outside DETECT_QUIET on some
+		 * platforms; only a full controller reinit (mirroring system
+		 * suspend/resume) reliably recovers it.
+		 */
+		dev_warn(pci->dev,
+			 "loopback: LTSSM did not settle in DETECT_QUIET (in %s), reinitializing controller\n",
+			 dw_pcie_ltssm_status_string(ltssm));
+
+		if (pp->ops->deinit)
+			pp->ops->deinit(pp);
+
+		pci->suspended = true;
+
+		rret = dw_pcie_resume_noirq(pci);
+		if (rret) {
+			dev_err(pci->dev, "loopback: controller reinit failed: %d\n", rret);
+			if (!ret)
+				ret = rret;
+		}
+
+		ltssm = dw_pcie_get_ltssm(pci);
+	} else {
+		dw_pcie_disable_atu(pci, PCIE_ATU_REGION_DIR_IB, ib_index);
+	}
+
+	dev_info(pci->dev, "PCIe LTSSM state after loopback exit: %s\n",
+		 dw_pcie_ltssm_status_string(ltssm));
+
+err_free_dma:
+	dma_free_coherent(pci->dev, buf_size, dst_virt, dst_dma);
+err_iounmap:
+	iounmap(src_base);
+err_release_res:
+	release_resource(&lb_res);
+
+	return ret;
+}
+
+static ssize_t buf_size_show(struct device *dev,
+			     struct device_attribute *attr, char *buf)
+{
+	struct dw_pcie_loopback *lb =
+		container_of(attr, struct dw_pcie_loopback, attr_buf_size);
+
+	return sysfs_emit(buf, "%zu\n", lb->buf_size);
+}
+
+static ssize_t buf_size_store(struct device *dev,
+			      struct device_attribute *attr,
+			      const char *buf, size_t count)
+{
+	struct dw_pcie_loopback *lb =
+		container_of(attr, struct dw_pcie_loopback, attr_buf_size);
+	struct dw_pcie *pci = to_dw_pcie_from_pp(lb->pp);
+	unsigned long req;
+	size_t new_size;
+	int ret;
+
+	ret = kstrtoul(buf, 0, &req);
+	if (ret)
+		return ret;
+
+	if (!req)
+		return -EINVAL;
+
+	if (req > DW_PCIE_LB_BUF_SIZE_MAX)
+		return -EINVAL;
+
+	/* Round up to next power-of-two, then enforce the 4 KiB minimum */
+	new_size = max_t(size_t, DW_PCIE_LB_BUF_SIZE_MIN,
+			 roundup_pow_of_two((size_t)req));
+
+	if (mutex_lock_interruptible(&lb->lock))
+		return -ERESTARTSYS;
+
+	if (lb->busy) {
+		mutex_unlock(&lb->lock);
+		return -EBUSY;
+	}
+
+	lb->buf_size = new_size;
+	mutex_unlock(&lb->lock);
+
+	dev_dbg(pci->dev, "loopback: buf_size set to %zu bytes\n", new_size);
+
+	return count;
+}
+
+static ssize_t run_show(struct device *dev,
+			struct device_attribute *attr, char *buf)
+{
+	struct dw_pcie_loopback *lb =
+		container_of(attr, struct dw_pcie_loopback, attr_run);
+
+	return sysfs_emit(buf, "%s\n", lb->busy ? "busy" : "idle");
+}
+
+static ssize_t run_store(struct device *dev,
+			 struct device_attribute *attr,
+			 const char *buf, size_t count)
+{
+	struct dw_pcie_loopback *lb =
+		container_of(attr, struct dw_pcie_loopback, attr_run);
+	struct dw_pcie_rp *pp = lb->pp;
+	struct dw_pcie *pci = to_dw_pcie_from_pp(pp);
+	size_t buf_size;
+	int ret;
+
+	if (!sysfs_streq(buf, "local"))
+		return -EINVAL;
+
+	if (dw_pcie_link_up(pci)) {
+		dev_err(pci->dev,
+			"loopback: refusing to run while the link is up\n");
+		return -EBUSY;
+	}
+
+	if (mutex_lock_interruptible(&lb->lock))
+		return -ERESTARTSYS;
+
+	if (lb->busy) {
+		dev_warn(pci->dev, "loopback: test already in progress\n");
+		mutex_unlock(&lb->lock);
+		return -EBUSY;
+	}
+
+	lb->busy  = true;
+	buf_size  = lb->buf_size;
+	mutex_unlock(&lb->lock);
+
+	ret = dw_pcie_loopback_run(pp, buf_size);
+
+	mutex_lock(&lb->lock);
+	lb->busy = false;
+	mutex_unlock(&lb->lock);
+
+	return ret ? ret : count;
+}
+
+static int dw_pcie_loopback_sysfs_init(struct dw_pcie_rp *pp)
+{
+	struct dw_pcie *pci = to_dw_pcie_from_pp(pp);
+	struct dw_pcie_loopback *lb;
+	int ret;
+
+	lb = devm_kzalloc(pci->dev, sizeof(*lb), GFP_KERNEL);
+	if (!lb)
+		return -ENOMEM;
+
+	lb->pp = pp;
+
+	mutex_init(&lb->lock);
+	lb->buf_size = DW_PCIE_LB_BUF_SIZE_MIN;
+
+	lb->attr_run      = (struct device_attribute)
+		__ATTR(run, 0644, run_show, run_store);
+	lb->attr_buf_size = (struct device_attribute)
+		__ATTR(buf_size, 0644, buf_size_show, buf_size_store);
+
+	sysfs_attr_init(&lb->attr_run.attr);
+	sysfs_attr_init(&lb->attr_buf_size.attr);
+
+	lb->attrs[0] = &lb->attr_run.attr;
+	lb->attrs[1] = &lb->attr_buf_size.attr;
+	lb->attrs[2] = NULL;
+	lb->attr_group.name  = "loopback";
+	lb->attr_group.attrs = lb->attrs;
+
+	pp->loopback = lb;
+
+	ret = device_add_group(pci->dev, &lb->attr_group);
+	if (ret) {
+		dev_err(pci->dev, "loopback: device_add_group failed: %d\n", ret);
+		pp->loopback = NULL;
+		return ret;
+	}
+
+	return 0;
+}
+
+static void dw_pcie_loopback_sysfs_deinit(struct dw_pcie_rp *pp)
+{
+	struct dw_pcie *pci = to_dw_pcie_from_pp(pp);
+	struct dw_pcie_loopback *lb = pp->loopback;
+
+	if (!lb)
+		return;
+
+	/*
+	 * device_remove_group() blocks until any in-flight run_store()/
+	 * run_show() has returned, so pp/pci/bridge are guaranteed not to be
+	 * accessed by the loopback code once this returns. Must run before
+	 * the rest of dw_pcie_host_deinit() tears down pp->bridge.
+	 */
+	device_remove_group(pci->dev, &lb->attr_group);
+	pp->loopback = NULL;
+}
+
 int dw_pcie_host_init(struct dw_pcie_rp *pp)
 {
 	struct dw_pcie *pci = to_dw_pcie_from_pp(pp);
@@ -675,6 +968,9 @@ int dw_pcie_host_init(struct dw_pcie_rp *pp)
 
 	dwc_pcie_debugfs_init(pci, DW_PCIE_RC_TYPE);
 
+	if (dw_pcie_loopback_sysfs_init(pp))
+		dev_warn(dev, "failed to create loopback sysfs entry\n");
+
 	return 0;
 
 err_stop_link:
@@ -703,6 +999,8 @@ void dw_pcie_host_deinit(struct dw_pcie_rp *pp)
 {
 	struct dw_pcie *pci = to_dw_pcie_from_pp(pp);
 
+	dw_pcie_loopback_sysfs_deinit(pp);
+
 	dwc_pcie_debugfs_deinit(pci);
 
 	pci_lock_rescan_remove();
diff --git a/drivers/pci/controller/dwc/pcie-designware.h b/drivers/pci/controller/dwc/pcie-designware.h
index de4b245b1758..dfcbcdf822da 100644
--- a/drivers/pci/controller/dwc/pcie-designware.h
+++ b/drivers/pci/controller/dwc/pcie-designware.h
@@ -88,6 +88,7 @@
 #define PORT_AFR_L1_ENTRANCE_LAT_MASK	GENMASK(29, 27)
 
 #define PCIE_PORT_LINK_CONTROL		0x710
+#define PORT_LINK_LOOPBACK_EN		BIT(2)
 #define PORT_LINK_DLL_LINK_EN		BIT(5)
 #define PORT_LINK_FAST_LINK_MODE	BIT(7)
 #define PORT_LINK_MODE_MASK		GENMASK(21, 16)
@@ -173,6 +174,9 @@
 #define COHERENCY_CONTROL_2_OFF			0x8E4
 #define COHERENCY_CONTROL_3_OFF			0x8E8
 
+#define PCIE_PIPE_LOOPBACK_CONTROL	0x8B8
+#define PCIE_PIPE_LOOPBACK_EN		BIT(31)
+
 #define PCIE_PORT_MULTI_LANE_CTRL	0x8C0
 #define PORT_MLTI_UPCFG_SUPPORT		BIT(7)
 
@@ -471,6 +475,19 @@ struct dw_pcie_rp {
 	bool			native_ecam;
 	bool                    skip_l23_ready;
 	bool			skip_pwrctrl_off;
+	struct dw_pcie_loopback	*loopback;
+};
+
+struct dw_pcie_loopback {
+	struct dw_pcie_rp	*pp;
+	/* Protects busy and buf_size against concurrent sysfs access */
+	struct mutex		lock;
+	bool			busy;
+	size_t			buf_size;
+	struct device_attribute	attr_run;
+	struct device_attribute	attr_buf_size;
+	struct attribute	*attrs[3];	/* run, buf_size, NULL */
+	struct attribute_group	attr_group;
 };
 
 struct dw_pcie_ep_ops {

---
base-commit: 8ba098e6b6ff0db8edf28528d1552be261af30d4
change-id: 20250130-loopback-60e4cd7bbfe7

Best regards,
--  
Krishna Chaitanya Chundru <[email protected]>