* [PATCH v3 0/3] spi: tegra210-quad: Improve interrupt handling for loaded systems
@ 2026-05-20 19:24 Vishwaroop A
2026-05-20 19:24 ` [PATCH v3 1/3] spi: tegra210-quad: Convert to hard IRQ with high-priority workqueue Vishwaroop A
` (2 more replies)
0 siblings, 3 replies; 4+ messages in thread
From: Vishwaroop A @ 2026-05-20 19:24 UTC (permalink / raw)
To: Thierry Reding, Jonathan Hunter, Mark Brown
Cc: Vishwaroop A, Breno Leitao, Laxman Dewangan, Sowjanya Komatineni,
Suresh Mangipudi, Krishna Yarlagadda, linux-tegra, linux-spi,
linux-kernel
The current threaded IRQ implementation in spi-tegra210-quad suffers from
scheduler-induced latency on heavily loaded systems. Because threaded IRQ
handlers are subject to CFS scheduling, they can be delayed long enough to
trigger transfer timeouts even though hardware completes in microseconds.
This results in false timeout errors and WARN_ON splats during normal
operation.
This series addresses the problem in three steps:
1. Convert the threaded IRQ handler to a hard IRQ + high-priority unbound
workqueue model. The hard IRQ does the minimum: verify interrupt
ownership, cache status registers, clear and mask interrupts. The
workqueue bottom-half handles the rest in process context and can run
on any available CPU, avoiding the CPU0 bottleneck inherent in threaded
IRQs.
2. Cache QSPI_TRANS_STATUS in the ISR before clearing it. This allows the
timeout handler to distinguish between a real hardware timeout (QSPI_RDY
not set) and a delayed workqueue (QSPI_RDY set), preventing false
timeout errors when hardware has already completed.
3. Process small PIO transfers (≤ FIFO depth, 256 bytes) directly in
hard IRQ context. This eliminates workqueue scheduling overhead for
latency-sensitive devices like TPMs, reducing completion latency from
potentially seconds to microseconds.
Changes since v2:
- Added cancel_work_sync() in remove to flush pending work before devm
tears down the workqueue (Jon Hunter)
- Rewrote patch 2 commit message to describe the race in terms of the
workqueue model rather than referencing the old threaded IRQ (Jon)
- s/NULLed/cleared/ in code comment (Jon)
Changes since v1:
- Switched to devm_alloc_workqueue() and devm_request_irq() for resource
management, simplifying error paths and remove (Jon Hunter)
- Improved patch 2 commit message to explain the timeout race scenario
and clarify that the issue pre-exists the workqueue conversion (Jon)
- Removed unnecessary local variable in tegra_qspi_handle_timeout (Jon)
- Moved "workqueue was delayed" comment updates from patch 2 to patch 1,
since patch 1 introduces the workqueue (Jon)
Tested on TH500 with TPM and QSPI flash devices under sustained load.
Vishwaroop A (3):
spi: tegra210-quad: Convert to hard IRQ with high-priority workqueue
spi: tegra210-quad: Cache TRANS_STATUS in ISR for timeout handler
spi: tegra210-quad: Process small PIO transfers in hard IRQ context
drivers/spi/spi-tegra210-quad.c | 166 +++++++++++++++++++++-----------
1 file changed, 112 insertions(+), 54 deletions(-)
--
2.17.1
^ permalink raw reply [flat|nested] 4+ messages in thread
* [PATCH v3 1/3] spi: tegra210-quad: Convert to hard IRQ with high-priority workqueue
2026-05-20 19:24 [PATCH v3 0/3] spi: tegra210-quad: Improve interrupt handling for loaded systems Vishwaroop A
@ 2026-05-20 19:24 ` Vishwaroop A
2026-05-20 19:24 ` [PATCH v3 2/3] spi: tegra210-quad: Cache TRANS_STATUS in ISR for timeout handler Vishwaroop A
2026-05-20 19:24 ` [PATCH v3 3/3] spi: tegra210-quad: Process small PIO transfers in hard IRQ context Vishwaroop A
2 siblings, 0 replies; 4+ messages in thread
From: Vishwaroop A @ 2026-05-20 19:24 UTC (permalink / raw)
To: Thierry Reding, Jonathan Hunter, Mark Brown
Cc: Vishwaroop A, Breno Leitao, Laxman Dewangan, Sowjanya Komatineni,
Suresh Mangipudi, Krishna Yarlagadda, linux-tegra, linux-spi,
linux-kernel
Threaded IRQ handlers suffer from scheduler latency on heavily loaded
systems, causing false transfer timeouts. Convert to hard IRQ handler
that schedules work on a high-priority unbound workqueue.
The hard IRQ handler verifies the interrupt, caches FIFO status,
clears and masks interrupts, then schedules bottom-half processing.
The workqueue handler runs in process context (can sleep for DMA)
and can execute on any CPU, avoiding CPU0 bottlenecks.
Signed-off-by: Vishwaroop A <va@nvidia.com>
---
drivers/spi/spi-tegra210-quad.c | 129 +++++++++++++++++++++-----------
1 file changed, 85 insertions(+), 44 deletions(-)
diff --git a/drivers/spi/spi-tegra210-quad.c b/drivers/spi/spi-tegra210-quad.c
index db28dd556484..b37c1b9816f7 100644
--- a/drivers/spi/spi-tegra210-quad.c
+++ b/drivers/spi/spi-tegra210-quad.c
@@ -191,6 +191,8 @@ struct tegra_qspi {
void __iomem *base;
phys_addr_t phys;
unsigned int irq;
+ struct work_struct irq_work;
+ struct workqueue_struct *wq;
u32 cur_speed;
unsigned int cur_pos;
@@ -1225,9 +1227,9 @@ static int tegra_qspi_combined_seq_xfer(struct tegra_qspi *tqspi,
if (ret == 0) {
/*
- * Check if hardware completed the transfer
- * even though interrupt was lost or delayed.
- * If so, process the completion and continue.
+ * Check if hardware completed the transfer even though
+ * workqueue was delayed. If so, process completion and
+ * continue.
*/
ret = tegra_qspi_handle_timeout(tqspi);
if (ret < 0) {
@@ -1344,8 +1346,8 @@ static int tegra_qspi_non_combined_seq_xfer(struct tegra_qspi *tqspi,
if (ret == 0) {
/*
* Check if hardware completed the transfer even though
- * interrupt was lost or delayed. If so, process the
- * completion and continue.
+ * workqueue was delayed. If so, process completion and
+ * continue.
*/
ret = tegra_qspi_handle_timeout(tqspi);
if (ret < 0) {
@@ -1574,46 +1576,40 @@ static irqreturn_t handle_dma_based_xfer(struct tegra_qspi *tqspi)
return IRQ_HANDLED;
}
-static irqreturn_t tegra_qspi_isr_thread(int irq, void *context_data)
+/**
+ * tegra_qspi_work_handler - Workqueue handler for interrupt bottom-half
+ * @work: work_struct embedded in tegra_qspi
+ *
+ * Runs in process context and can sleep (needed for DMA completion waits).
+ * Can run on any available CPU, avoiding CPU0 bottleneck that occurs with
+ * threaded IRQ handlers which are pinned to the IRQ's CPU.
+ *
+ * The hard IRQ handler has already:
+ * - Verified this is our interrupt (QSPI_RDY was set)
+ * - Cached FIFO status in tqspi->status_reg
+ * - Parsed tx_status / rx_status from FIFO status
+ * - Masked further interrupts
+ */
+static void tegra_qspi_work_handler(struct work_struct *work)
{
- struct tegra_qspi *tqspi = context_data;
+ struct tegra_qspi *tqspi = container_of(work, struct tegra_qspi, irq_work);
unsigned long flags;
- u32 status;
- /*
- * Read transfer status to check if interrupt was triggered by transfer
- * completion
- */
- status = tegra_qspi_readl(tqspi, QSPI_TRANS_STATUS);
+ spin_lock_irqsave(&tqspi->lock, flags);
/*
- * Occasionally the IRQ thread takes a long time to wake up (usually
- * when the CPU that it's running on is excessively busy) and we have
- * already reached the timeout before and cleaned up the timed out
- * transfer. Avoid any processing in that case and bail out early.
- *
- * If no transfer is in progress, check if this was a real interrupt
- * that the timeout handler already processed, or a spurious one.
+ * Check if timeout handler already processed this transfer.
+ * Can happen if work was delayed and timeout fired first. If
+ * so, we must unmask interrupts before returning, otherwise
+ * they remain masked from the hard IRQ handler and the next
+ * transfer will timeout.
*/
- spin_lock_irqsave(&tqspi->lock, flags);
if (!tqspi->curr_xfer) {
spin_unlock_irqrestore(&tqspi->lock, flags);
- /* Spurious interrupt - transfer not ready */
- if (!(status & QSPI_RDY))
- return IRQ_NONE;
- /* Real interrupt, already handled by timeout path */
- return IRQ_HANDLED;
+ tegra_qspi_unmask_irq(tqspi);
+ return;
}
- tqspi->status_reg = tegra_qspi_readl(tqspi, QSPI_FIFO_STATUS);
-
- if (tqspi->cur_direction & DATA_DIR_TX)
- tqspi->tx_status = tqspi->status_reg & (QSPI_TX_FIFO_UNF | QSPI_TX_FIFO_OVF);
-
- if (tqspi->cur_direction & DATA_DIR_RX)
- tqspi->rx_status = tqspi->status_reg & (QSPI_RX_FIFO_OVF | QSPI_RX_FIFO_UNF);
-
- tegra_qspi_mask_clear_irq(tqspi);
spin_unlock_irqrestore(&tqspi->lock, flags);
/*
@@ -1623,9 +1619,46 @@ static irqreturn_t tegra_qspi_isr_thread(int irq, void *context_data)
* cannot be done while holding spinlock.
*/
if (!tqspi->is_curr_dma_xfer)
- return handle_cpu_based_xfer(tqspi);
+ handle_cpu_based_xfer(tqspi);
+ else
+ handle_dma_based_xfer(tqspi);
+}
+
+/**
+ * tegra_qspi_isr - Hard IRQ handler
+ * @irq: IRQ number
+ * @context_data: QSPI controller instance
+ *
+ * Runs in hard IRQ context with minimal latency. Cannot sleep.
+ *
+ * Return: IRQ_NONE if not our interrupt, IRQ_HANDLED if handled
+ */
+static irqreturn_t tegra_qspi_isr(int irq, void *context_data)
+{
+ struct tegra_qspi *tqspi = context_data;
+ u32 status;
+
+ status = tegra_qspi_readl(tqspi, QSPI_TRANS_STATUS);
+ if (!(status & QSPI_RDY))
+ return IRQ_NONE;
+
+ spin_lock(&tqspi->lock);
+ tqspi->status_reg = tegra_qspi_readl(tqspi, QSPI_FIFO_STATUS);
+ tegra_qspi_mask_clear_irq(tqspi);
- return handle_dma_based_xfer(tqspi);
+ if (tqspi->cur_direction & DATA_DIR_TX)
+ tqspi->tx_status = tqspi->status_reg &
+ (QSPI_TX_FIFO_UNF | QSPI_TX_FIFO_OVF);
+
+ if (tqspi->cur_direction & DATA_DIR_RX)
+ tqspi->rx_status = tqspi->status_reg &
+ (QSPI_RX_FIFO_OVF | QSPI_RX_FIFO_UNF);
+
+ spin_unlock(&tqspi->lock);
+
+ queue_work(tqspi->wq, &tqspi->irq_work);
+
+ return IRQ_HANDLED;
}
static struct tegra_qspi_soc_data tegra210_qspi_soc_data = {
@@ -1793,9 +1826,19 @@ static int tegra_qspi_probe(struct platform_device *pdev)
pm_runtime_put_autosuspend(&pdev->dev);
- ret = request_threaded_irq(tqspi->irq, NULL,
- tegra_qspi_isr_thread, IRQF_ONESHOT,
- dev_name(&pdev->dev), tqspi);
+ tqspi->wq = devm_alloc_workqueue(&pdev->dev, "%s",
+ WQ_HIGHPRI | WQ_UNBOUND, 0,
+ dev_name(&pdev->dev));
+ if (!tqspi->wq) {
+ dev_err(&pdev->dev, "failed to allocate workqueue\n");
+ ret = -ENOMEM;
+ goto exit_pm_disable;
+ }
+
+ INIT_WORK(&tqspi->irq_work, tegra_qspi_work_handler);
+
+ ret = devm_request_irq(&pdev->dev, tqspi->irq, tegra_qspi_isr,
+ IRQF_SHARED, dev_name(&pdev->dev), tqspi);
if (ret < 0) {
dev_err(&pdev->dev, "failed to request IRQ#%u: %d\n", tqspi->irq, ret);
goto exit_pm_disable;
@@ -1804,13 +1847,11 @@ static int tegra_qspi_probe(struct platform_device *pdev)
ret = spi_register_controller(host);
if (ret < 0) {
dev_err(&pdev->dev, "failed to register host: %d\n", ret);
- goto exit_free_irq;
+ goto exit_pm_disable;
}
return 0;
-exit_free_irq:
- free_irq(qspi_irq, tqspi);
exit_pm_disable:
pm_runtime_dont_use_autosuspend(&pdev->dev);
pm_runtime_force_suspend(&pdev->dev);
@@ -1824,7 +1865,7 @@ static void tegra_qspi_remove(struct platform_device *pdev)
struct tegra_qspi *tqspi = spi_controller_get_devdata(host);
spi_unregister_controller(host);
- free_irq(tqspi->irq, tqspi);
+ cancel_work_sync(&tqspi->irq_work);
pm_runtime_dont_use_autosuspend(&pdev->dev);
pm_runtime_force_suspend(&pdev->dev);
tegra_qspi_deinit_dma(tqspi);
--
2.17.1
^ permalink raw reply related [flat|nested] 4+ messages in thread
* [PATCH v3 2/3] spi: tegra210-quad: Cache TRANS_STATUS in ISR for timeout handler
2026-05-20 19:24 [PATCH v3 0/3] spi: tegra210-quad: Improve interrupt handling for loaded systems Vishwaroop A
2026-05-20 19:24 ` [PATCH v3 1/3] spi: tegra210-quad: Convert to hard IRQ with high-priority workqueue Vishwaroop A
@ 2026-05-20 19:24 ` Vishwaroop A
2026-05-20 19:24 ` [PATCH v3 3/3] spi: tegra210-quad: Process small PIO transfers in hard IRQ context Vishwaroop A
2 siblings, 0 replies; 4+ messages in thread
From: Vishwaroop A @ 2026-05-20 19:24 UTC (permalink / raw)
To: Thierry Reding, Jonathan Hunter, Mark Brown
Cc: Vishwaroop A, Breno Leitao, Laxman Dewangan, Sowjanya Komatineni,
Suresh Mangipudi, Krishna Yarlagadda, linux-tegra, linux-spi,
linux-kernel
On heavily loaded systems, the workqueue bottom-half can be delayed
long enough for wait_for_completion_timeout() to expire first. When
the timeout handler reads QSPI_TRANS_STATUS directly from hardware,
it may see a completed transfer but race with the workqueue handler,
leading to double completion or use-after-free on curr_xfer.
Cache QSPI_TRANS_STATUS in the ISR before clearing it, allowing the
timeout handler to check the cached value under spinlock. Also guard
against curr_xfer being cleared by a concurrent workqueue completion.
Signed-off-by: Vishwaroop A <va@nvidia.com>
---
drivers/spi/spi-tegra210-quad.c | 30 +++++++++++++++++++-----------
1 file changed, 19 insertions(+), 11 deletions(-)
diff --git a/drivers/spi/spi-tegra210-quad.c b/drivers/spi/spi-tegra210-quad.c
index b37c1b9816f7..64ad17d38b84 100644
--- a/drivers/spi/spi-tegra210-quad.c
+++ b/drivers/spi/spi-tegra210-quad.c
@@ -214,6 +214,7 @@ struct tegra_qspi {
u32 tx_status;
u32 rx_status;
u32 status_reg;
+ u32 trans_status;
bool is_packed;
bool use_dma;
@@ -854,6 +855,7 @@ static u32 tegra_qspi_setup_transfer_one(struct spi_device *spi, struct spi_tran
tqspi->cur_rx_pos = 0;
tqspi->cur_tx_pos = 0;
tqspi->curr_xfer = t;
+ tqspi->trans_status = 0;
spin_unlock_irqrestore(&tqspi->lock, flags);
if (is_first_of_msg) {
@@ -1068,26 +1070,30 @@ static irqreturn_t handle_dma_based_xfer(struct tegra_qspi *tqspi);
*/
static int tegra_qspi_handle_timeout(struct tegra_qspi *tqspi)
{
+ unsigned long flags;
irqreturn_t ret;
- u32 status;
- /* Check if hardware actually completed the transfer */
- status = tegra_qspi_readl(tqspi, QSPI_TRANS_STATUS);
- if (!(status & QSPI_RDY))
+ spin_lock_irqsave(&tqspi->lock, flags);
+
+ if (!(tqspi->trans_status & QSPI_RDY)) {
+ spin_unlock_irqrestore(&tqspi->lock, flags);
return -ETIMEDOUT;
+ }
/*
- * Hardware completed but interrupt was lost/delayed. Manually
- * process the completion by calling the appropriate handler.
+ * ISR or workqueue may have already completed the transfer
+ * and cleared curr_xfer between the completion timeout and now.
*/
+ if (!tqspi->curr_xfer) {
+ spin_unlock_irqrestore(&tqspi->lock, flags);
+ return 0;
+ }
+
+ spin_unlock_irqrestore(&tqspi->lock, flags);
+
dev_warn_ratelimited(tqspi->dev,
"QSPI interrupt timeout, but transfer complete\n");
- /* Clear the transfer status */
- status = tegra_qspi_readl(tqspi, QSPI_TRANS_STATUS);
- tegra_qspi_writel(tqspi, status, QSPI_TRANS_STATUS);
-
- /* Manually trigger completion handler */
if (!tqspi->is_curr_dma_xfer)
ret = handle_cpu_based_xfer(tqspi);
else
@@ -1642,6 +1648,8 @@ static irqreturn_t tegra_qspi_isr(int irq, void *context_data)
if (!(status & QSPI_RDY))
return IRQ_NONE;
+ tqspi->trans_status = status;
+
spin_lock(&tqspi->lock);
tqspi->status_reg = tegra_qspi_readl(tqspi, QSPI_FIFO_STATUS);
tegra_qspi_mask_clear_irq(tqspi);
--
2.17.1
^ permalink raw reply related [flat|nested] 4+ messages in thread
* [PATCH v3 3/3] spi: tegra210-quad: Process small PIO transfers in hard IRQ context
2026-05-20 19:24 [PATCH v3 0/3] spi: tegra210-quad: Improve interrupt handling for loaded systems Vishwaroop A
2026-05-20 19:24 ` [PATCH v3 1/3] spi: tegra210-quad: Convert to hard IRQ with high-priority workqueue Vishwaroop A
2026-05-20 19:24 ` [PATCH v3 2/3] spi: tegra210-quad: Cache TRANS_STATUS in ISR for timeout handler Vishwaroop A
@ 2026-05-20 19:24 ` Vishwaroop A
2 siblings, 0 replies; 4+ messages in thread
From: Vishwaroop A @ 2026-05-20 19:24 UTC (permalink / raw)
To: Thierry Reding, Jonathan Hunter, Mark Brown
Cc: Vishwaroop A, Breno Leitao, Laxman Dewangan, Sowjanya Komatineni,
Suresh Mangipudi, Krishna Yarlagadda, linux-tegra, linux-spi,
linux-kernel
On heavily loaded systems, workqueue scheduling delays can exceed
transfer timeouts even for high-priority queues, causing false timeouts
for latency-sensitive devices like TPM despite hardware completing in
microseconds.
Process small PIO transfers (≤256 bytes) directly in hard IRQ context
instead of deferring to workqueue. This reduces completion latency
from 1000ms+ to microseconds and matches the pattern used by other
SPI drivers.
The 256-byte threshold (FIFO depth) ensures small transfers for devices
like TPMs use the fast path, while larger transfers continue using
workqueue.
Signed-off-by: Vishwaroop A <va@nvidia.com>
---
drivers/spi/spi-tegra210-quad.c | 9 +++++++++
1 file changed, 9 insertions(+)
diff --git a/drivers/spi/spi-tegra210-quad.c b/drivers/spi/spi-tegra210-quad.c
index 64ad17d38b84..e3681f06b0ec 100644
--- a/drivers/spi/spi-tegra210-quad.c
+++ b/drivers/spi/spi-tegra210-quad.c
@@ -1664,6 +1664,15 @@ static irqreturn_t tegra_qspi_isr(int irq, void *context_data)
spin_unlock(&tqspi->lock);
+ /*
+ * For small PIO transfers (e.g., TPM), process directly in hard IRQ
+ * context unless there was a FIFO error. Error recovery calls
+ * device_reset() which can sleep, so must be deferred to workqueue.
+ */
+ if (!tqspi->is_curr_dma_xfer && tqspi->curr_dma_words <= QSPI_FIFO_DEPTH &&
+ !tqspi->tx_status && !tqspi->rx_status)
+ return handle_cpu_based_xfer(tqspi);
+
queue_work(tqspi->wq, &tqspi->irq_work);
return IRQ_HANDLED;
--
2.17.1
^ permalink raw reply related [flat|nested] 4+ messages in thread
end of thread, other threads:[~2026-05-20 19:24 UTC | newest]
Thread overview: 4+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-05-20 19:24 [PATCH v3 0/3] spi: tegra210-quad: Improve interrupt handling for loaded systems Vishwaroop A
2026-05-20 19:24 ` [PATCH v3 1/3] spi: tegra210-quad: Convert to hard IRQ with high-priority workqueue Vishwaroop A
2026-05-20 19:24 ` [PATCH v3 2/3] spi: tegra210-quad: Cache TRANS_STATUS in ISR for timeout handler Vishwaroop A
2026-05-20 19:24 ` [PATCH v3 3/3] spi: tegra210-quad: Process small PIO transfers in hard IRQ context Vishwaroop A
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox