From: Vishwaroop A <va@nvidia.com>
To: Thierry Reding <thierry.reding@kernel.org>,
Jon Hunter <jonathanh@nvidia.com>,
Mark Brown <broonie@kernel.org>
Cc: Vishwaroop A <va@nvidia.com>,
Laxman Dewangan <ldewangan@nvidia.com>,
Sowjanya Komatineni <skomatineni@nvidia.com>,
Breno Leitao <leitao@debian.org>,
Suresh Mangipudi <smangipudi@nvidia.com>,
"Krishna Yarlagadda" <kyarlagadda@nvidia.com>,
<linux-tegra@vger.kernel.org>, <linux-spi@vger.kernel.org>,
<linux-kernel@vger.kernel.org>
Subject: [PATCH v5 0/3] spi: tegra210-quad: Improve interrupt handling for loaded systems
Date: Wed, 8 Jul 2026 01:12:54 +0000 [thread overview]
Message-ID: <20260708011257.1712961-1-va@nvidia.com> (raw)
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: capture FIFO status,
mask and clear the controller IRQ, then schedule the bottom half. The
workqueue handler runs in process context (can sleep for DMA
completion) and can execute on any CPU, avoiding the CPU0 bottleneck
inherent in threaded IRQs.
2. Cache QSPI_TRANS_STATUS in the ISR before clearing it. This lets the
timeout handler 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. Pair the cache
publication with smp_store_release()/smp_load_acquire() so the timeout
handler observes a coherent set of cached fields on weakly-ordered
architectures.
3. Process small PIO transfers (those that complete the whole spi_transfer
in a single chunk) directly in hard IRQ context, eliminating workqueue
scheduling latency for TPM-style short reads.
Runtime PM lifetime note (unchanged from v4): the work handler only
touches QSPI MMIO when curr_xfer is non-NULL. While curr_xfer is set,
the transfer thread is blocked in wait_for_completion_timeout() with
the SPI core's runtime PM reference held, so the clocks are guaranteed
on. When the work handler runs late after the timeout path has already
processed the transfer, it sees curr_xfer == NULL and returns without
any MMIO. With this invariant no additional PM reference handoff
between the ISR and the work handler is needed.
Changes since v4 (addressing review by Mark Brown [1]):
Patch 1 ("Convert to hard IRQ with high-priority workqueue"):
- Rewrote the tegra_qspi_work_handler() comment to describe the
new serialisation invariant honestly: with the cancel_work_sync()
added to tegra_qspi_handle_timeout() in patch 2, the work handler
is never concurrent with the timeout path. The curr_xfer NULL
check that remains simply catches the case where the timeout
path already tore the transfer down before this work got a CPU.
Addresses Mark's "Can't the timeout handler also be running at
the same time as this?" comment.
- Converted the tegra_qspi_isr() header comment to a proper
kernel-doc block, with @irq / @context_data / Return: fields.
No functional change.
Patch 2 ("Cache TRANS_STATUS in ISR for timeout handler"):
- Serialise tegra_qspi_handle_timeout() against the workqueue.
Mask the controller IRQ (tegra_qspi_mask_clear_irq()) and then
cancel_work_sync(&tqspi->irq_work) at the top of the recovery
path, and re-check curr_xfer under the spinlock afterwards.
Once cancel_work_sync() returns the bottom half is neither
running nor pending, so the subsequent
handle_{cpu,dma}_based_xfer() call from the timeout handler
cannot race with the same call from the work handler. Addresses
Mark's "This can be called from both tegra_qspi_work_handler()
and tegra_qspi_handle_timeout() - I can't see what stops them
both handling and completing the same transfer simultaneously?"
review comment.
- Clear the cached trans_status per chunk. Add
smp_store_release(&trans_status, 0) immediately before
tegra_qspi_unmask_irq() in both tegra_qspi_start_cpu_based_
transfer() and tegra_qspi_start_dma_based_transfer(), so a
multi-chunk DMA transfer (or the DMA -> PIO tail-chunk
transition) cannot leave a stale RDY from chunk N in the cache
when chunk N+1's completion times out. The store happens-before
the unmask, and the new chunk's IRQ cannot fire until the
controller register write below the unmask, so ordering with
smp_load_acquire() in tegra_qspi_handle_timeout() is preserved.
Addresses Mark's "It looks like the CPU based transfer function
supports multiple interrupts per transfer ... don't we need to
clear trans_status when we handle the interrupt as well?" review
comment.
- Take tqspi->lock across the ISR's status snapshot and cache
publish sequence, and move tegra_qspi_mask_clear_irq() inside
the locked region. This closes the QSPI_INTR_MASK RMW race
Mark had already flagged on v3: setup_transfer_one()'s
mask_clear_irq() and the ISR's mask_clear_irq() no longer race
on the shared INTR_MASK register, and the cached status /
status_reg fields are published atomically with respect to
other tqspi->lock holders.
Patch 3 ("Process small PIO transfers in hard IRQ context"):
- Comment-only reflows to match the surrounding text and clarify
that the fastpath cannot recurse into
tegra_qspi_start_cpu_based_transfer() from hard IRQ context
(multi-chunk PIO is only set on the final chunk, so the
continuation path is intrinsically single-chunk).
No functional changes in patch 3.
Changes since v3:
Patch 1 ("Convert to hard IRQ with high-priority workqueue"):
- Dropped IRQF_SHARED. Tegra QSPI uses a dedicated GIC SPI line on
every SoC that uses this driver, so the ISR does not need a
runtime PM reference. Addresses Mark Brown's "Since we now have
IRQF_SHARED we need to take a runtime PM reference here" comment.
- Switched from devm_request_irq() to plain request_irq() in
probe() and added explicit free_irq() in remove(), in the order:
spi_unregister_controller -> free_irq -> destroy_workqueue ->
pm_runtime_dont_use_autosuspend -> pm_runtime_force_suspend ->
tegra_qspi_deinit_dma. Addresses Mark's "devm + non-devm mix
seems likely to be racy" comments on probe() and remove().
- Removed the tegra_qspi_unmask_irq() call from the work_handler
NULL-bail path. Addresses Mark's "unmask after dropping the lock
feels like it opens up races" comment. The next transfer's setup
path re-arms the IRQ.
- Snapshot tqspi->curr_xfer under tqspi->lock at the top of
handle_dma_based_xfer() so the (potentially long) DMA waits
operate on a stable transfer pointer even if the timeout path
clears curr_xfer concurrently. Integrates cleanly with Breno
Leitao's recently merged protect-curr_xfer series.
Patch 2 ("Cache TRANS_STATUS in ISR for timeout handler"):
- Cached status_reg / tx_status / rx_status / trans_status are now
published with WRITE_ONCE() and smp_store_release() and consumed
with smp_load_acquire() in tegra_qspi_handle_timeout(), so a
timeout handler that observes non-zero trans_status also sees a
coherent snapshot of the other fields on weakly-ordered
architectures. Live MMIO fallback via tegra_qspi_readl() only
runs when the cache is still zero (the ISR never ran).
- Kept the WRITE_ONCE / dev_warn_ratelimited() / dev_err()
diagnostics scaffolding around the wait_for_completion_timeout()
calls in tegra_qspi_{combined,non_combined}_seq_xfer(). With
the recovery path in place a timeout is no longer a programming
error; the existing dev_warn_ratelimited("QSPI interrupt
timeout, but transfer complete") and dev_err("transfer
timeout") diagnostics are preserved.
Patch 3 ("Process small PIO transfers in hard IRQ context"):
- Replaced the previous "curr_dma_words <= QSPI_FIFO_DEPTH" check
with a tqspi->is_last_pio_chunk scalar computed in
tegra_qspi_start_cpu_based_transfer() before it unmasks the IRQ.
Addresses Mark Brown's "Is cur_dma_words always in the same
units as QSPI_FIFO_DEPTH - I see there's packed transfer support
in the driver?" comment: the scalar comparison is units-correct
(cur_pos + curr_dma_words * bytes_per_word >= t->len) and the
hard-IRQ fastpath no longer dereferences the spi_transfer
object, so it stays safe against any teardown race that could
clear curr_xfer concurrently.
- Fastpath additionally gates on tx_status == 0 && rx_status == 0
because handle_cpu_based_xfer()'s error path calls
tegra_qspi_handle_error() -> device_reset(), which can sleep and
must not run from hard IRQ context.
- is_curr_dma_xfer and is_last_pio_chunk are written from process
context (the transfer-start functions) and read lock-free from
the hard IRQ handler and the workqueue handler, so the writers
use WRITE_ONCE() and the readers use READ_ONCE() to prevent
compiler tearing and silence KCSAN.
Changes since v2:
- Added cancel_work_sync() in remove to flush pending work before
devm tore down the workqueue (Jon Hunter). v4 has replaced devm
altogether per Mark Brown's comment, so the explicit teardown now
relies on free_irq() preventing new work being queued, followed by
destroy_workqueue() draining what is in-flight.
- 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 (Jon Hunter). v4 has since reverted to
non-devm for the IRQ and workqueue per Mark Brown's review, so
teardown order can be made explicit.
- 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 Tegra234 (Ferrix P3960) under sustained CPU stress (stress-ng
--cpu $(nproc) --timeout 3600s) while running dd if=/dev/mtd0 of=/dev/null
bs=1M count=100 in a loop; no QSPI timeouts, no softlockups, no lockdep
warnings, and mtd read/write + md5sum round-trip clean.
The series is based on linux-next (next-20260706).
[1] https://lore.kernel.org/linux-spi/cover.1781037385.git.va@nvidia.com/
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 | 324 ++++++++++++++++++++++++++------
1 file changed, 265 insertions(+), 59 deletions(-)
--
2.17.1
next reply other threads:[~2026-07-08 1:13 UTC|newest]
Thread overview: 6+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-07-08 1:12 Vishwaroop A [this message]
2026-07-08 1:12 ` [PATCH v5 1/3] spi: tegra210-quad: Convert to hard IRQ with high-priority workqueue Vishwaroop A
2026-07-08 12:15 ` Breno Leitao
2026-07-09 3:00 ` Vishwaroop A
2026-07-08 1:12 ` [PATCH v5 2/3] spi: tegra210-quad: Cache TRANS_STATUS in ISR for timeout handler Vishwaroop A
2026-07-08 1:12 ` [PATCH v5 3/3] spi: tegra210-quad: Process small PIO transfers in hard IRQ context Vishwaroop A
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=20260708011257.1712961-1-va@nvidia.com \
--to=va@nvidia.com \
--cc=broonie@kernel.org \
--cc=jonathanh@nvidia.com \
--cc=kyarlagadda@nvidia.com \
--cc=ldewangan@nvidia.com \
--cc=leitao@debian.org \
--cc=linux-kernel@vger.kernel.org \
--cc=linux-spi@vger.kernel.org \
--cc=linux-tegra@vger.kernel.org \
--cc=skomatineni@nvidia.com \
--cc=smangipudi@nvidia.com \
--cc=thierry.reding@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