diff mbox series

PCI: mvebu: Add support for PCI_EXP_SLTSTA_DLLSC via hot plug interrupt

Message ID 20220905184737.22080-1-pali@kernel.org
State New
Headers show
Series PCI: mvebu: Add support for PCI_EXP_SLTSTA_DLLSC via hot plug interrupt | expand

Commit Message

Pali Rohár Sept. 5, 2022, 6:47 p.m. UTC
If link up/down state is changed in mvebu_pcie_link_up() then trigger
hot plug interrupt with DLLSC state change on the emulated PCIe Root Port.

Also triggers hot plug interrupt on emulated PCIe Root Port when mvebu
reports via mvebu error interrupt Link Failure event (indicates that link
was changed from active state; with exception of Hot Reset and Link Disable
states) or TxReq No Link event (indicates that link is down while trying to
transmit PCIe transaction).

And triggers this hot plug interrupt also when explicit Link Disable or
PCIe Host Reset on emulated PCIe Root Port is issued via config space
access as mvebu HW does not signal Link Failure when dropping to Detect via
Hot Reset or Link Disable. This is because compliant hotplug-capable PCIe
Root Port should report DLLSC event via hot plug interrupt every time
when link is changed independently of the source of event change.

This new code heavily depends on kernel PCI hotplug driver, so enable it
when compiling pci-mvebu.c driver.

Signed-off-by: Pali Rohár <pali@kernel.org>
---
This patch depends on patch:
"PCI: mvebu: Implement support for interrupts on emulated bridge"
https://lore.kernel.org/linux-pci/20220817230036.817-3-pali@kernel.org/
---
 drivers/pci/controller/Kconfig     |   3 +
 drivers/pci/controller/pci-mvebu.c | 147 ++++++++++++++++++++++++++++-
 2 files changed, 149 insertions(+), 1 deletion(-)
diff mbox series

Patch

diff --git a/drivers/pci/controller/Kconfig b/drivers/pci/controller/Kconfig
index d1c5fcf00a8a..8da2efdc5177 100644
--- a/drivers/pci/controller/Kconfig
+++ b/drivers/pci/controller/Kconfig
@@ -10,6 +10,9 @@  config PCI_MVEBU
 	depends on ARM
 	depends on OF
 	select PCI_BRIDGE_EMUL
+	select PCIEPORTBUS
+	select HOTPLUG_PCI
+	select HOTPLUG_PCI_PCIE
 	help
 	 Add support for Marvell EBU PCIe controller. This PCIe controller
 	 is used on 32-bit Marvell ARM SoCs: Dove, Kirkwood, Armada 370,
diff --git a/drivers/pci/controller/pci-mvebu.c b/drivers/pci/controller/pci-mvebu.c
index ddd5ba8b265e..8bde4727aca4 100644
--- a/drivers/pci/controller/pci-mvebu.c
+++ b/drivers/pci/controller/pci-mvebu.c
@@ -56,12 +56,14 @@ 
 #define PCIE_CONF_DATA_OFF	0x18fc
 #define PCIE_INT_CAUSE_OFF	0x1900
 #define PCIE_INT_UNMASK_OFF	0x1910
+#define  PCIE_INT_TXREQ_NOLINK		BIT(0)
 #define  PCIE_INT_DET_COR		BIT(8)
 #define  PCIE_INT_DET_NONFATAL		BIT(9)
 #define  PCIE_INT_DET_FATAL		BIT(10)
 #define  PCIE_INT_ERR_FATAL		BIT(16)
 #define  PCIE_INT_ERR_NONFATAL		BIT(17)
 #define  PCIE_INT_ERR_COR		BIT(18)
+#define  PCIE_INT_LINK_FAIL		BIT(23)
 #define  PCIE_INT_INTX(i)		BIT(24+i)
 #define  PCIE_INT_PM_PME		BIT(28)
 #define  PCIE_INT_DET_MASK		(PCIE_INT_DET_COR | PCIE_INT_DET_NONFATAL | PCIE_INT_DET_FATAL)
@@ -134,6 +136,8 @@  struct mvebu_pcie_port {
 	int error_irq;
 	int intx_irq;
 	bool pme_pending;
+	struct timer_list link_irq_timer;
+	bool link_was_up;
 };
 
 static inline void mvebu_writel(struct mvebu_pcie_port *port, u32 val, u32 reg)
@@ -153,7 +157,26 @@  static inline bool mvebu_has_ioport(struct mvebu_pcie_port *port)
 
 static bool mvebu_pcie_link_up(struct mvebu_pcie_port *port)
 {
-	return !(mvebu_readl(port, PCIE_STAT_OFF) & PCIE_STAT_LINK_DOWN);
+	bool link_is_up;
+	u16 slotsta;
+
+	link_is_up = !(mvebu_readl(port, PCIE_STAT_OFF) & PCIE_STAT_LINK_DOWN);
+
+	if (link_is_up != port->link_was_up) {
+		port->link_was_up = link_is_up;
+		/*
+		 * Link IRQ timer/handler is available only when "error"
+		 * interrupt was specified in DT.
+		 */
+		if (port->error_irq > 0) {
+			slotsta = le16_to_cpu(port->bridge.pcie_conf.slotsta);
+			port->bridge.pcie_conf.slotsta =
+				cpu_to_le16(slotsta | PCI_EXP_SLTSTA_DLLSC);
+			mod_timer(&port->link_irq_timer, jiffies + 1);
+		}
+	}
+
+	return link_is_up;
 }
 
 static u8 mvebu_pcie_get_local_bus_nr(struct mvebu_pcie_port *port)
@@ -346,6 +369,19 @@  static void mvebu_pcie_setup_hw(struct mvebu_pcie_port *port)
 		mvebu_writel(port, unmask, PCIE_INT_UNMASK_OFF);
 	}
 
+	/*
+	 * Unmask No Link and Link Failure interrupts to process Link Down
+	 * events. These events are reported as Data Link Layer State Changed
+	 * notification via Hot Plug Interrupt. Other parts of Link change
+	 * events are available only when "error" interrupt was specified in DT.
+	 * So enable these interrupts under same conditions.
+	 */
+	if (port->error_irq > 0) {
+		unmask = mvebu_readl(port, PCIE_INT_UNMASK_OFF);
+		unmask |= PCIE_INT_TXREQ_NOLINK | PCIE_INT_LINK_FAIL;
+		mvebu_writel(port, unmask, PCIE_INT_UNMASK_OFF);
+	}
+
 	/*
 	 * Fallback code when "intx" interrupt was not specified in DT:
 	 * Unmask all legacy INTx interrupts as driver does not provide a way
@@ -692,6 +728,14 @@  mvebu_pci_bridge_emul_pcie_conf_read(struct pci_bridge_emul *bridge,
 			val |= slotctl & PCI_EXP_SLTCTL_ASPL_DISABLE;
 		else if (!(mvebu_readl(port, PCIE_SSPL_OFF) & PCIE_SSPL_ENABLE))
 			val |= PCI_EXP_SLTCTL_ASPL_DISABLE;
+		/*
+		 * HPIE and DLLSCE bits are stored only in emulated config
+		 * space buffer and are supported only when "error" interrupt
+		 * was specified in DT.
+		 */
+		if (port->error_irq > 0)
+			val |= slotctl & (PCI_EXP_SLTCTL_HPIE |
+					  PCI_EXP_SLTCTL_DLLSCE);
 		/* This callback is 32-bit and in high bits is slot status. */
 		val |= slotsta << 16;
 		*value = val;
@@ -823,6 +867,25 @@  mvebu_pci_bridge_emul_base_conf_write(struct pci_bridge_emul *bridge,
 			else
 				ctrl &= ~PCIE_CTRL_MASTER_HOT_RESET;
 			mvebu_writel(port, ctrl, PCIE_CTRL_OFF);
+			/*
+			 * When dropping to Detect via Hot Reset, Disable Link
+			 * or Loopback states, the Link Failure interrupt is not
+			 * asserted. So when setting Secondary Bus Reset / Hot
+			 * Reset bit, call link IRQ timer/handler manually.
+			 */
+			if ((ctrl & PCIE_CTRL_MASTER_HOT_RESET) && port->link_was_up) {
+				port->link_was_up = false;
+				/*
+				 * Link IRQ timer/handler is available only when
+				 * "error" interrupt was specified in DT.
+				 */
+				if (port->error_irq > 0) {
+					u16 slotsta = le16_to_cpu(port->bridge.pcie_conf.slotsta);
+					port->bridge.pcie_conf.slotsta =
+						cpu_to_le16(slotsta | PCI_EXP_SLTSTA_DLLSC);
+					mod_timer(&port->link_irq_timer, jiffies + 1);
+				}
+			}
 		}
 		break;
 
@@ -851,6 +914,25 @@  mvebu_pci_bridge_emul_pcie_conf_write(struct pci_bridge_emul *bridge,
 		new &= ~PCI_EXP_LNKCTL_CLKREQ_EN;
 
 		mvebu_writel(port, new, PCIE_CAP_PCIEXP + PCI_EXP_LNKCTL);
+		/*
+		 * When dropping to Detect via Hot Reset, Disable Link
+		 * or Loopback states, the Link Failure interrupt is not
+		 * asserted. So when setting Link Disable bit, call link
+		 * IRQ timer/handler manually.
+		 */
+		if ((new & PCI_EXP_LNKCTL_LD) && port->link_was_up) {
+			port->link_was_up = false;
+			/*
+			 * Link IRQ timer/handler is available only when
+			 * "error" interrupt was specified in DT.
+			 */
+			if (port->error_irq > 0) {
+				u16 slotsta = le16_to_cpu(port->bridge.pcie_conf.slotsta);
+				port->bridge.pcie_conf.slotsta =
+					cpu_to_le16(slotsta | PCI_EXP_SLTSTA_DLLSC);
+				mod_timer(&port->link_irq_timer, jiffies + 1);
+			}
+		}
 		break;
 
 	case PCI_EXP_SLTCTL:
@@ -991,6 +1073,15 @@  static int mvebu_pci_bridge_emul_init(struct mvebu_pcie_port *port)
 	bridge->pcie_conf.cap = cpu_to_le16(pcie_cap_ver | PCI_EXP_FLAGS_SLOT);
 
 	/*
+	 * When "error" interrupt was specified in DT then driver is able to
+	 * deliver Data Link Layer State Change interrupt. So in this case mark
+	 * bridge as Hot Plug Capable as this is the way how to enable
+	 * delivering of Data Link Layer State Change interrupts.
+	 *
+	 * No Command Completed Support is set because bridge does not support
+	 * Command Completed Interrupt. Every command is executed immediately
+	 * without any delay.
+	 *
 	 * Set Presence Detect State bit permanently as there is no support for
 	 * unplugging PCIe card from the slot. Assume that PCIe card is always
 	 * connected in slot.
@@ -1002,6 +1093,8 @@  static int mvebu_pci_bridge_emul_init(struct mvebu_pcie_port *port)
 	 * Also set correct slot power limit.
 	 */
 	bridge->pcie_conf.slotcap = cpu_to_le32(
+		PCI_EXP_SLTCAP_NCCS |
+		(port->error_irq > 0 ? PCI_EXP_SLTCAP_HPC : 0) |
 		FIELD_PREP(PCI_EXP_SLTCAP_SPLV, port->slot_power_limit_value) |
 		FIELD_PREP(PCI_EXP_SLTCAP_SPLS, port->slot_power_limit_scale) |
 		FIELD_PREP(PCI_EXP_SLTCAP_PSN, port->port+1));
@@ -1191,11 +1284,29 @@  static int mvebu_pcie_init_irq_domain(struct mvebu_pcie_port *port)
 	return 0;
 }
 
+static void mvebu_pcie_link_irq_handler(struct timer_list *timer)
+{
+	struct mvebu_pcie_port *port = from_timer(port, timer, link_irq_timer);
+	struct device *dev = &port->pcie->pdev->dev;
+	u16 slotctl;
+
+	dev_info(dev, "%s: link %s\n", port->name, port->link_was_up ? "up" : "down");
+
+	slotctl = le16_to_cpu(port->bridge.pcie_conf.slotctl);
+	if (!(slotctl & PCI_EXP_SLTCTL_DLLSCE) ||
+	    !(slotctl & PCI_EXP_SLTCTL_HPIE))
+		return;
+
+	if (generic_handle_domain_irq(port->rp_irq_domain, 0) == -EINVAL)
+		dev_err_ratelimited(dev, "unhandled HP IRQ\n");
+}
+
 static irqreturn_t mvebu_pcie_error_irq_handler(int irq, void *arg)
 {
 	struct mvebu_pcie_port *port = arg;
 	struct device *dev = &port->pcie->pdev->dev;
 	u32 cause, unmask, status;
+	u16 slotsta;
 
 	cause = mvebu_readl(port, PCIE_INT_CAUSE_OFF);
 	unmask = mvebu_readl(port, PCIE_INT_UNMASK_OFF);
@@ -1233,6 +1344,25 @@  static irqreturn_t mvebu_pcie_error_irq_handler(int irq, void *arg)
 			dev_err_ratelimited(dev, "unhandled ERR IRQ\n");
 	}
 
+	/* Process No Link and Link Failure interrupts as HP IRQ */
+	if (status & (PCIE_INT_TXREQ_NOLINK | PCIE_INT_LINK_FAIL)) {
+		mvebu_writel(port,
+			     ~(PCIE_INT_TXREQ_NOLINK | PCIE_INT_LINK_FAIL),
+			     PCIE_INT_CAUSE_OFF);
+		if (port->link_was_up) {
+			port->link_was_up = false;
+			slotsta = le16_to_cpu(port->bridge.pcie_conf.slotsta);
+			port->bridge.pcie_conf.slotsta =
+				cpu_to_le16(slotsta | PCI_EXP_SLTSTA_DLLSC);
+			/*
+			 * Deactivate timer and call mvebu_pcie_link_irq_handler()
+			 * function directly as we are in the interrupt context.
+			 */
+			del_timer_sync(&port->link_irq_timer);
+			mvebu_pcie_link_irq_handler(&port->link_irq_timer);
+		}
+	}
+
 	return status ? IRQ_HANDLED : IRQ_NONE;
 }
 
@@ -1796,6 +1926,18 @@  static int mvebu_pcie_probe(struct platform_device *pdev)
 			}
 		}
 
+		/*
+		 * Function mvebu_pcie_link_irq_handler() calls function
+		 * generic_handle_irq() and it expects local IRQs to be disabled
+		 * as normally generic_handle_irq() is called from the interrupt
+		 * context. So use TIMER_IRQSAFE flag for this link_irq_timer.
+		 * Available only if "error" interrupt was specified in DT.
+		 */
+		if (port->error_irq > 0)
+			timer_setup(&port->link_irq_timer,
+				    mvebu_pcie_link_irq_handler,
+				    TIMER_IRQSAFE);
+
 		/*
 		 * PCIe topology exported by mvebu hw is quite complicated. In
 		 * reality has something like N fully independent host bridges
@@ -1932,6 +2074,9 @@  static int mvebu_pcie_remove(struct platform_device *pdev)
 			irq_domain_remove(port->rp_irq_domain);
 		}
 
+		if (port->error_irq > 0)
+			del_timer_sync(&port->link_irq_timer);
+
 		/* Free config space for emulated root bridge. */
 		pci_bridge_emul_cleanup(&port->bridge);