Linux SPI subsystem development
 help / color / mirror / Atom feed
From: Shih-Yuan Lee <fourdollars@debian.org>
To: Mark Brown <broonie@kernel.org>
Cc: linux-spi@vger.kernel.org, linux-kernel@vger.kernel.org,
	Shih-Yuan Lee <fourdollars@debian.org>
Subject: [PATCH v8 2/2] spi: pxa2xx: restore LPSS private register state on S3 resume
Date: Sat, 18 Jul 2026 11:22:39 +0800	[thread overview]
Message-ID: <20260718032239.19136-3-fourdollars@debian.org> (raw)
In-Reply-To: <20260718032239.19136-1-fourdollars@debian.org>

Intel LPSS SPI controllers lose all private register state across S3
suspend because the LPSS power domain is fully removed. On resume the
driver only re-enables the SSP clock, leaving the LPSS private registers
in their power-on-reset state, which causes two problems:

1. LPSS_PRIV_RESETS (offset 0x04 within the LPSS private space) stays
   zero, keeping the functional block in reset. Any MMIO access while
   the block is held in reset causes a PCIe Completion Timeout and a
   watchdog-triggered system reset. LPSS_PRIV_RESETS_FUNC and
   LPSS_PRIV_RESETS_IDMA must be de-asserted before any other register
   access on resume.

2. The LPSS software chip-select control register must not be blindly
   restored from its suspend-time snapshot: if CS was asserted at the
   moment of suspend, restoring that state corrupts the first
   post-resume SPI transaction. Instead, call lpss_ssp_setup() which
   unconditionally writes SW_MODE | CS_HIGH (idle/deasserted), matching
   the state established at probe time.

To resolve these issues safely:
- Wrap S3 suspend/resume with pm_runtime_resume_and_get() and
  pm_runtime_put_autosuspend() respectively. This ensures that if the
  device was runtime-suspended, it is temporarily resumed to active state
  prior to suspend. This guarantees that the clock and power domain are
  active during MMIO register access, and that the private registers are
  consistently saved and restored across S3 sleep cycles.
  This also ensures that the unconditional MMIO register access in
  pxa2xx_spi_suspend() (specifically pxa_ssp_disable()) is safe from triggering
  PCIe Completion Timeouts.
- On S3 suspend success path, return 0 directly without dropping the PM
  reference. This preserves the acquired PM reference across suspend.
  On S3 resume, release it via pm_runtime_put_autosuspend(), and ensure
  all error paths in resume (clock enable failure or spi_controller_resume
  failure) jump to out_put to correctly release the reference, preventing
  reference count underflow and leaks.
- Save and restore LPSS private registers only on LPT, BYT, and BSW platforms
  (via helper pxa2xx_spi_need_lpss_restore()). These platforms operate directly
  as PCI/platform devices without parent MFD drivers (intel-lpss). For newer
  platforms (SPT, BXT, CNL), private registers are already saved/restored by
  the parent MFD driver intel-lpss.c; accessing hardcoded offsets 0x00..0x14 on
  newer platforms where offsets 0x08/0x0c are absent/reserved causes PCIe
  Completion Timeouts and system freezes.
- Save the first 6 LPSS private registers (offsets 0x00 to 0x14) via
  drv_data->lpss_base during suspend for LPT/BYT/BSW platforms. Offsets beyond
  0x14 (except CS control at 0x18, which is re-initialised by lpss_ssp_setup())
  are reserved/unimplemented on LPT platforms, and writing to them triggers a
  PCIe Completion Timeout causing a system halt.
- Clear drv_data->suspended only after de-asserting the resets and
  restoring the private registers on resume. This prevents shared interrupt
  handlers from performing unclocked/held-in-reset MMIO accesses if an
  interrupt fires during the resume process.
- Revert drv_data->suspended to true and call synchronize_irq() on
  spi_controller_resume() failure to ensure subsequent interrupts do not
  attempt register reads after the clock is disabled.
- Add spi_controller_resume() recovery to the error path of
  spi_controller_suspend() in pxa2xx_spi_suspend() to prevent the controller
  from remaining permanently disabled in the event system suspend is aborted.
- Store the saved context in drv_data->lpss_priv_ctx[6] (inside struct
  driver_data) which is private to the core driver. This avoids changing
  the layout of struct pxa2xx_spi_controller, preventing ABI symbol
  version mismatches with uncompiled platform drivers (e.g.,
  spi-pxa2xx-platform.ko).

On resume, de-assert resets first, restore all other saved registers,
then call lpss_ssp_setup() to re-initialise CS.

Link: https://bugzilla.kernel.org/show_bug.cgi?id=108331
Signed-off-by: Shih-Yuan Lee <fourdollars@debian.org>
---
 drivers/spi/spi-pxa2xx.c | 60 ++++++++++++++++++++++++++++++++++++++++
 drivers/spi/spi-pxa2xx.h |  1 +
 2 files changed, 61 insertions(+)

diff --git a/drivers/spi/spi-pxa2xx.c b/drivers/spi/spi-pxa2xx.c
index f6cc39633191..c200930a2fc5 100644
--- a/drivers/spi/spi-pxa2xx.c
+++ b/drivers/spi/spi-pxa2xx.c
@@ -194,6 +194,17 @@ static bool is_lpss_ssp(const struct driver_data *drv_data)
 	}
 }
 
+static bool pxa2xx_spi_need_lpss_restore(const struct driver_data *drv_data)
+{
+	switch (drv_data->ssp_type) {
+	case LPSS_LPT_SSP:
+	case LPSS_BYT_SSP:
+	case LPSS_BSW_SSP:
+		return true;
+	default:
+		return false;
+	}
+}
 
 static bool is_quark_x1000_ssp(const struct driver_data *drv_data)
 {
@@ -1561,6 +1572,22 @@ static int pxa2xx_spi_suspend(struct device *dev)
 	drv_data->suspended = true;
 	synchronize_irq(ssp->irq);
 
+	if (pxa2xx_spi_need_lpss_restore(drv_data)) {
+		unsigned int i;
+
+		/*
+		 * Save the first 6 LPSS private registers (offsets 0x00 to 0x14)
+		 * while the clock is still enabled.  They are lost when the LPSS
+		 * power domain is removed across S3 and must be restored on resume.
+		 * Use drv_data->lpss_base so the correct per-platform offset
+		 * is applied regardless of LPSS IP revision.
+		 * Registers beyond 0x14 (except CS control at 0x18) are reserved
+		 * or unimplemented on LPT, and accessing them triggers a PCIe
+		 * Completion Timeout causing a system halt.
+		 */
+		for (i = 0; i < 6; i++)
+			drv_data->lpss_priv_ctx[i] = readl(drv_data->lpss_base + i * 4);
+	}
 
 	pxa2xx_spi_clk_disable(drv_data);
 	return 0;
@@ -1581,6 +1608,39 @@ static int pxa2xx_spi_resume(struct device *dev)
 	if (status)
 		goto out_put;
 
+	if (pxa2xx_spi_need_lpss_restore(drv_data)) {
+		unsigned int i;
+
+		/*
+		 * The LPSS power domain is removed across S3, taking
+		 * all private registers with it.  De-assert the
+		 * functional block and IDMA resets first; any MMIO
+		 * access while the block is held in reset causes a
+		 * PCIe Completion Timeout and a watchdog-triggered
+		 * system reset.
+		 */
+		writel(LPSS_PRIV_RESETS_FUNC | LPSS_PRIV_RESETS_IDMA,
+		       drv_data->lpss_base + LPSS_PRIV_RESETS);
+
+		/* Restore the other 5 saved private registers */
+		for (i = 0; i < 6; i++) {
+			if (i == LPSS_PRIV_RESETS / 4)
+				continue;
+			writel(drv_data->lpss_priv_ctx[i],
+			       drv_data->lpss_base + i * 4);
+		}
+	}
+
+	if (is_lpss_ssp(drv_data)) {
+		/*
+		 * Re-initialise the SW chip-select control register so
+		 * CS starts deasserted (SW_MODE | CS_HIGH), regardless
+		 * of the state it was in at suspend time.  A stale
+		 * asserted CS on the first post-resume transaction
+		 * corrupts the write-status response from the device.
+		 */
+		lpss_ssp_setup(drv_data);
+	}
 
 	/*
 	 * Now that resets are de-asserted and registers are restored,
diff --git a/drivers/spi/spi-pxa2xx.h b/drivers/spi/spi-pxa2xx.h
index 44f37bf9c519..48169494f74e 100644
--- a/drivers/spi/spi-pxa2xx.h
+++ b/drivers/spi/spi-pxa2xx.h
@@ -71,6 +71,7 @@ struct driver_data {
 	irqreturn_t (*transfer_handler)(struct driver_data *drv_data);
 
 	void __iomem *lpss_base;
+	u32 lpss_priv_ctx[6];
 
 	bool suspended;
 	bool clk_enabled;
-- 
2.39.5


      parent reply	other threads:[~2026-07-18  3:22 UTC|newest]

Thread overview: 3+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-07-18  3:22 [PATCH v8 0/2] spi: pxa2xx: MacBook8,1 quirk and LPSS S3 resume state fixes Shih-Yuan Lee
2026-07-18  3:22 ` [PATCH v8 1/2] spi: pxa2xx: disable DMA and fix runtime PM for Apple MacBook8,1 Shih-Yuan Lee
2026-07-18  3:22 ` Shih-Yuan Lee [this message]

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20260718032239.19136-3-fourdollars@debian.org \
    --to=fourdollars@debian.org \
    --cc=broonie@kernel.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=linux-spi@vger.kernel.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox