From: Joseph CT Chan <jctchan@gmail.com>
To: Mark Brown <broonie@kernel.org>
Cc: linux-spi@vger.kernel.org, David Lechner <dlechner@baylibre.com>,
Bart Van Assche <bvanassche@acm.org>,
Joseph CT Chan <josephctchan@google.com>
Subject: [PATCH v2 2/2] spi: spi-ti-qspi: add KUnit tests for transfer error finalization
Date: Tue, 3 Nov 2026 22:51:16 +0000 [thread overview]
Message-ID: <20261103225116.3624270-3-jctchan@gmail.com> (raw)
In-Reply-To: <20261103225116.3624270-1-jctchan@gmail.com>
From: Joseph CT Chan <josephctchan@google.com>
Add KUnit tests for ti_qspi_start_transfer_one() covering the transfer
error handling path.
On ARCH=um, emulate the TI QSPI controller MMIO via logic_iomem:
- ti_qspi_test: verify that queued and synchronous transfers hitting a
ti_qspi_poll_wc() write completion timeout complete with -ETIMEDOUT,
that QSPI_INVAL is written to end the frame, and that subsequent
transfers succeed cleanly once QSPI_SPI_STATUS_REG reports QSPI_WC.
Developed with help from the Claude and Gemini AI assistants.
Signed-off-by: Joseph CT Chan <josephctchan@google.com>
---
v2: Trimmed the comments.
drivers/spi/Kconfig | 12 ++
drivers/spi/Makefile | 1 +
drivers/spi/tests/.kunitconfig | 4 +
drivers/spi/tests/Makefile | 1 +
drivers/spi/tests/spi-ti-qspi-kunit.c | 188 ++++++++++++++++++++++++++
5 files changed, 206 insertions(+)
create mode 100644 drivers/spi/tests/spi-ti-qspi-kunit.c
diff --git a/drivers/spi/Kconfig b/drivers/spi/Kconfig
index a7b343d124a9..feb9265b7d0f 100644
--- a/drivers/spi/Kconfig
+++ b/drivers/spi/Kconfig
@@ -1258,6 +1258,18 @@ config SPI_TI_QSPI
This device supports single, dual and quad read support, while
it only supports single write mode.
+config SPI_TI_QSPI_KUNIT_TEST
+ bool "KUnit test for TI QSPI controller driver" if !KUNIT_ALL_TESTS
+ depends on KUNIT=y && SPI_TI_QSPI=y && INDIRECT_IOMEM && !HAVE_CLK
+ default KUNIT_ALL_TESTS
+ help
+ This option builds the KUnit test suite for the TI QSPI controller
+ driver. It verifies that transfer timeout errors are properly
+ finalized with -ETIMEDOUT and that QSPI_INVAL is written to the
+ command register.
+
+ If unsure, say N.
+
config SPI_TLE62X0
tristate "Infineon TLE62X0 (for power switching)"
depends on SYSFS
diff --git a/drivers/spi/Makefile b/drivers/spi/Makefile
index d4a806b46b2c..24497d3936f6 100644
--- a/drivers/spi/Makefile
+++ b/drivers/spi/Makefile
@@ -15,6 +15,7 @@ obj-$(CONFIG_SPI_SPIDEV) += spidev.o
obj-$(CONFIG_SPI_LOOPBACK_TEST) += spi-loopback-test.o
obj-$(CONFIG_SPI_DMA_KUNIT_TEST) += tests/
obj-$(CONFIG_SPI_CORE_KUNIT_TEST) += tests/
+obj-$(CONFIG_SPI_TI_QSPI_KUNIT_TEST) += tests/
# SPI master controller drivers (bus)
obj-$(CONFIG_SPI_AIROHA_SNFI) += spi-airoha-snfi.o
diff --git a/drivers/spi/tests/.kunitconfig b/drivers/spi/tests/.kunitconfig
index f781425d258e..354bafc42c2b 100644
--- a/drivers/spi/tests/.kunitconfig
+++ b/drivers/spi/tests/.kunitconfig
@@ -1,4 +1,8 @@
CONFIG_KUNIT=y
+CONFIG_KUNIT_UML_PCI=y
CONFIG_SPI=y
CONFIG_SPI_MASTER=y
CONFIG_SPI_CORE_KUNIT_TEST=y
+CONFIG_COMPILE_TEST=y
+CONFIG_SPI_TI_QSPI=y
+CONFIG_SPI_TI_QSPI_KUNIT_TEST=y
diff --git a/drivers/spi/tests/Makefile b/drivers/spi/tests/Makefile
index 6dbf53643619..a5a8fc9435d4 100644
--- a/drivers/spi/tests/Makefile
+++ b/drivers/spi/tests/Makefile
@@ -2,3 +2,4 @@
obj-$(CONFIG_SPI_DMA_KUNIT_TEST) += spi-dma-kunit.o
obj-$(CONFIG_SPI_CORE_KUNIT_TEST) += spi-core-kunit.o
+obj-$(CONFIG_SPI_TI_QSPI_KUNIT_TEST) += spi-ti-qspi-kunit.o
diff --git a/drivers/spi/tests/spi-ti-qspi-kunit.c b/drivers/spi/tests/spi-ti-qspi-kunit.c
new file mode 100644
index 000000000000..fd75d5d3ab89
--- /dev/null
+++ b/drivers/spi/tests/spi-ti-qspi-kunit.c
@@ -0,0 +1,188 @@
+// SPDX-License-Identifier: GPL-2.0
+#include <kunit/test.h>
+#include <linux/completion.h>
+#include <linux/jiffies.h>
+#include <linux/kthread.h>
+#include <linux/logic_iomem.h>
+#include <linux/platform_device.h>
+#include <linux/pm_runtime.h>
+#include <linux/spi/spi.h>
+
+#define QSPI_SPI_CMD_REG 0x48
+#define QSPI_SPI_STATUS_REG 0x4c
+#define QSPI_WC BIT(1)
+#define QSPI_INVAL (4 << 16)
+
+struct ti_qspi_test_ctx {
+ struct platform_device *pdev;
+ struct spi_device *spi;
+ struct spi_message msg;
+ struct spi_transfer xfer;
+ struct completion done;
+ u8 tx[4];
+};
+
+static bool g_err, g_inval, g_region_registered;
+static struct resource g_iomem_res = DEFINE_RES_MEM(0x30000000, 0x2000);
+
+static unsigned long ti_qspi_io_r(void *p, unsigned int off, int sz)
+{
+ return (off == QSPI_SPI_STATUS_REG && !g_err) ? QSPI_WC : 0;
+}
+
+static void ti_qspi_io_w(void *p, unsigned int off, int sz, unsigned long val)
+{
+ if (off == QSPI_SPI_CMD_REG && (val & QSPI_INVAL))
+ g_inval = true;
+}
+
+static const struct logic_iomem_ops g_iops = {
+ .read = ti_qspi_io_r,
+ .write = ti_qspi_io_w,
+};
+
+static long ti_qspi_map(unsigned long o, size_t s, const struct logic_iomem_ops **ops, void **p)
+{
+ *ops = &g_iops;
+ return 0;
+}
+
+static const struct logic_iomem_region_ops g_rops = {
+ .map = ti_qspi_map,
+};
+
+static int ti_qspi_find_ctlr(struct device *d, void *data)
+{
+ *(struct spi_controller **)data = container_of(d, struct spi_controller, dev);
+ return 1;
+}
+
+static void kunit_done(void *p)
+{
+ complete(p);
+}
+
+static int ti_qspi_init(struct kunit *t)
+{
+ const struct resource res[] = {
+ DEFINE_RES_MEM(0x30000000, 0x1000),
+ DEFINE_RES_MEM(0x30001000, 0x1000),
+ DEFINE_RES_IRQ(1),
+ };
+ struct ti_qspi_test_ctx *ctx;
+ struct spi_board_info info = { .modalias = "ti-qspi-test" };
+ struct spi_controller *ctlr = NULL;
+ struct platform_device *pdev;
+
+ g_err = false;
+ g_inval = false;
+ ctx = kunit_kzalloc(t, sizeof(*ctx), GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(t, ctx);
+ t->priv = ctx;
+
+ pdev = platform_device_register_simple("ti-qspi", -1, res, ARRAY_SIZE(res));
+ KUNIT_ASSERT_NOT_ERR_OR_NULL(t, pdev);
+ ctx->pdev = pdev;
+
+ device_for_each_child(&pdev->dev, &ctlr, ti_qspi_find_ctlr);
+ if (!ctlr) {
+ platform_device_unregister(pdev);
+ ctx->pdev = NULL;
+ KUNIT_FAIL_AND_ABORT(t, "TI QSPI controller device not found");
+ }
+
+ ctlr->max_speed_hz = 50000000;
+ ctx->spi = spi_new_device(ctlr, &info);
+ if (!ctx->spi) {
+ pm_runtime_get_noresume(&pdev->dev);
+ platform_device_unregister(pdev);
+ ctx->pdev = NULL;
+ KUNIT_FAIL_AND_ABORT(t, "Failed to create spi child device");
+ }
+
+ init_completion(&ctx->done);
+ memcpy(ctx->tx, "ABCD", sizeof(ctx->tx));
+ ctx->xfer.tx_buf = ctx->tx;
+ ctx->xfer.len = sizeof(ctx->tx);
+ spi_message_init_with_transfers(&ctx->msg, &ctx->xfer, 1);
+ return 0;
+}
+
+static void ti_qspi_exit(struct kunit *t)
+{
+ struct ti_qspi_test_ctx *ctx = t->priv;
+
+ if (!ctx)
+ return;
+ if (ctx->spi)
+ spi_unregister_device(ctx->spi);
+ if (ctx->pdev) {
+ pm_runtime_get_noresume(&ctx->pdev->dev);
+ platform_device_unregister(ctx->pdev);
+ }
+}
+
+static void check_sync(struct kunit *t, struct spi_device *s, struct spi_message *m, int exp)
+{
+ g_inval = false;
+ KUNIT_EXPECT_EQ(t, spi_sync(s, m), exp);
+ KUNIT_EXPECT_EQ(t, m->status, exp);
+ /* Message must be finalized and unoptimized even when a transfer fails. */
+ KUNIT_EXPECT_FALSE(t, m->optimized);
+ /* QSPI_INVAL must be written to terminate the frame on both success and error. */
+ KUNIT_EXPECT_TRUE(t, g_inval);
+}
+
+static void ti_qspi_test(struct kunit *t)
+{
+ struct ti_qspi_test_ctx *ctx = t->priv;
+ unsigned long timeout;
+
+ g_err = true;
+ check_sync(t, ctx->spi, &ctx->msg, -ETIMEDOUT);
+
+ /* Exercise the queued kworker path via spi_async() on transfer timeout. */
+ ctx->msg.complete = kunit_done;
+ ctx->msg.context = &ctx->done;
+ g_inval = false;
+ reinit_completion(&ctx->done);
+ KUNIT_ASSERT_EQ(t, spi_async(ctx->spi, &ctx->msg), 0);
+
+ timeout = wait_for_completion_timeout(&ctx->done, msecs_to_jiffies(4000));
+ kthread_flush_worker(ctx->spi->controller->kworker);
+ if (!timeout)
+ KUNIT_FAIL_AND_ABORT(t, "ti-qspi async transfer timed out");
+
+ KUNIT_EXPECT_EQ(t, ctx->msg.status, -ETIMEDOUT);
+ KUNIT_EXPECT_FALSE(t, ctx->msg.optimized);
+ KUNIT_EXPECT_TRUE(t, g_inval);
+ /* Clear the injected fault and verify a subsequent transfer succeeds. */
+ g_err = false;
+ check_sync(t, ctx->spi, &ctx->msg, 0);
+}
+
+static int ti_qspi_suite_init(struct kunit_suite *s)
+{
+ int ret = g_region_registered ? 0 : logic_iomem_add_region(&g_iomem_res, &g_rops);
+
+ g_region_registered = !ret;
+ return ret;
+}
+
+static struct kunit_case ti_qspi_test_cases[] = {
+ KUNIT_CASE_SLOW(ti_qspi_test),
+ {}
+};
+
+static struct kunit_suite ti_qspi_test_suite = {
+ .name = "spi_ti_qspi",
+ .suite_init = ti_qspi_suite_init,
+ .init = ti_qspi_init,
+ .exit = ti_qspi_exit,
+ .test_cases = ti_qspi_test_cases,
+};
+
+kunit_test_suite(ti_qspi_test_suite);
+
+MODULE_DESCRIPTION("KUnit tests for TI QSPI controller driver");
+MODULE_LICENSE("GPL");
--
2.56.0.385.gac671c819a-goog
prev parent reply other threads:[~2026-11-03 22:51 UTC|newest]
Thread overview: 3+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-11-03 22:51 [PATCH v2 0/2] spi: spi-ti-qspi: finalize message on transfer error Joseph CT Chan
2026-11-03 22:51 ` [PATCH v2 1/2] " Joseph CT Chan
2026-11-03 22:51 ` Joseph CT Chan [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=20261103225116.3624270-3-jctchan@gmail.com \
--to=jctchan@gmail.com \
--cc=broonie@kernel.org \
--cc=bvanassche@acm.org \
--cc=dlechner@baylibre.com \
--cc=josephctchan@google.com \
--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