[PATCH v2 3/5] hw/riscv/k230: Instantiate DesignWare SSI controllers

Kangjie Huang <[email protected]> Sun, 2 Aug 2026 03:28:46 +0800
Newsgroups org.nongnu.qemu-riscv,org.nongnu.qemu-devel
Message-ID <[email protected]>
Instantiate the three DesignWare SSI profiles used by K230 and map their
controller MMIO regions at the documented addresses.

Configure the QSPI0, QSPI1, and SPI-OPI instances with their chip-select,
FIFO-depth, and interrupt-mask reset profiles.

Add K230 SSI qtests for Standard PIO transfers, register masks, FIFO
behaviour, interrupt state, unsupported-register semantics, and reset
profiles. K230 is used as the hardware carrier for the generic controller
tests; PLIC routing is added by the next patch.

Signed-off-by: Kangjie Huang <[email protected]>
---
 docs/system/riscv/k230.rst     |   1 +
 hw/riscv/Kconfig               |   1 +
 hw/riscv/k230.c                |  79 ++++--
 include/hw/riscv/k230.h        |   2 +
 tests/qtest/k230-dw-ssi-test.c | 469 +++++++++++++++++++++++++++++++++
 tests/qtest/meson.build        |   4 +-
 6 files changed, 540 insertions(+), 16 deletions(-)
 create mode 100644 tests/qtest/k230-dw-ssi-test.c

diff --git a/docs/system/riscv/k230.rst b/docs/system/riscv/k230.rst
index cea8202e55..7a49cac95b 100644
--- a/docs/system/riscv/k230.rst
+++ b/docs/system/riscv/k230.rst
@@ -20,6 +20,7 @@ The ``k230`` machine supports the following devices:
 * Platform-Level Interrupt Controller (PLIC)
 * 2 K230 Watchdog Timer
 * 5 UART
+* 3 K230 SSI controllers for SPI only
 
 Boot options
 ------------
diff --git a/hw/riscv/Kconfig b/hw/riscv/Kconfig
index de37c08cae..60a260691c 100644
--- a/hw/riscv/Kconfig
+++ b/hw/riscv/Kconfig
@@ -162,3 +162,4 @@ config K230
     select SERIAL_MM
     select UNIMP
     select K230_WDT
+    select DW_SSI
diff --git a/hw/riscv/k230.c b/hw/riscv/k230.c
index 656f28190c..4c321002cd 100644
--- a/hw/riscv/k230.c
+++ b/hw/riscv/k230.c
@@ -21,7 +21,9 @@
 #include "system/device_tree.h"
 #include "system/system.h"
 #include "system/memory.h"
+#include "system/blockdev.h"
 #include "target/riscv/cpu.h"
+#include "hw/block/flash.h"
 #include "hw/core/loader.h"
 #include "hw/core/sysbus.h"
 #include "hw/riscv/k230.h"
@@ -102,6 +104,40 @@ static const MemMapEntry memmap[] = {
     [K230_DEV_CLINT] =        { 0xF04000000, 0x00400000 },
 };
 
+typedef struct K230DwSsiProfile {
+    uint32_t num_cs;
+    uint32_t fifo_depth;
+    uint32_t imr_reset;
+} K230DwSsiProfile;
+
+static const K230DwSsiProfile k230_dw_ssi_profiles[] = {
+    [0] = { /* QSPI0, SDK spi1 */
+        .num_cs = 5,
+        .fifo_depth = 256,
+        .imr_reset = 0x0000001f,
+    },
+    [1] = { /* QSPI1, SDK spi2 */
+        .num_cs = 5,
+        .fifo_depth = 256,
+        .imr_reset = 0x0000001f,
+    },
+    [2] = { /* SPI-OPI/FMC, SDK spi0 */
+        .num_cs = 1,
+        .fifo_depth = 256,
+        .imr_reset = 0x0000003f,
+    },
+};
+
+static void k230_configure_dw_ssi(DwSsiState *ssi,
+                                  const K230DwSsiProfile *profile)
+{
+    DeviceState *dev = DEVICE(ssi);
+
+    qdev_prop_set_uint32(dev, "num-cs", profile->num_cs);
+    qdev_prop_set_uint32(dev, "fifo-depth", profile->fifo_depth);
+    qdev_prop_set_uint32(dev, "imr-reset", profile->imr_reset);
+}
+
 static void k230_soc_init(Object *obj)
 {
     K230SoCState *s = RISCV_K230_SOC(obj);
@@ -110,6 +146,12 @@ static void k230_soc_init(Object *obj)
     object_initialize_child(obj, "c908-cpu", cpu0, TYPE_RISCV_HART_ARRAY);
     object_initialize_child(obj, "k230-wdt0", &s->wdt[0], TYPE_K230_WDT);
     object_initialize_child(obj, "k230-wdt1", &s->wdt[1], TYPE_K230_WDT);
+    object_initialize_child(obj, "k230-qspi0", &s->dw_ssi[0],
+                            TYPE_DW_SSI);
+    object_initialize_child(obj, "k230-qspi1", &s->dw_ssi[1],
+                            TYPE_DW_SSI);
+    object_initialize_child(obj, "k230-spi-opi", &s->dw_ssi[2],
+                            TYPE_DW_SSI);
 
     qdev_prop_set_uint32(DEVICE(cpu0), "hartid-base", 0);
     qdev_prop_set_string(DEVICE(cpu0), "cpu-type", TYPE_RISCV_CPU_THEAD_C908);
@@ -198,6 +240,13 @@ static void k230_soc_realize(DeviceState *dev, Error **errp)
         }
     }
 
+    for (int i = 0; i < ARRAY_SIZE(s->dw_ssi); i++) {
+        k230_configure_dw_ssi(&s->dw_ssi[i], &k230_dw_ssi_profiles[i]);
+        if (!sysbus_realize(SYS_BUS_DEVICE(&s->dw_ssi[i]), errp)) {
+            return;
+        }
+    }
+
     sysbus_mmio_map(SYS_BUS_DEVICE(&s->wdt[0]), 0, memmap[K230_DEV_WDT0].base);
     sysbus_connect_irq(SYS_BUS_DEVICE(&s->wdt[0]), 0,
                        qdev_get_gpio_in(DEVICE(s->c908_plic), K230_WDT0_IRQ));
@@ -206,6 +255,20 @@ static void k230_soc_realize(DeviceState *dev, Error **errp)
     sysbus_connect_irq(SYS_BUS_DEVICE(&s->wdt[1]), 0,
                        qdev_get_gpio_in(DEVICE(s->c908_plic), K230_WDT1_IRQ));
 
+    sysbus_mmio_map(SYS_BUS_DEVICE(&s->dw_ssi[0]), 0,
+                    memmap[K230_DEV_QSPI0].base);
+    sysbus_mmio_map(SYS_BUS_DEVICE(&s->dw_ssi[1]), 0,
+                    memmap[K230_DEV_QSPI1].base);
+    sysbus_mmio_map(SYS_BUS_DEVICE(&s->dw_ssi[2]), 0,
+                    memmap[K230_DEV_SPI].base);
+
+    create_unimplemented_device("spi-flash-xip",
+                                memmap[K230_DEV_FLASH].base,
+                                memmap[K230_DEV_FLASH].size);
+
+    create_unimplemented_device("hi_sys", memmap[K230_DEV_HI_SYS_CFG].base,
+                                memmap[K230_DEV_HI_SYS_CFG].size);
+
     /* unimplemented devices */
     create_unimplemented_device("kpu.l2-cache",
                                 memmap[K230_DEV_KPU_L2_CACHE].base,
@@ -349,23 +412,8 @@ static void k230_soc_realize(DeviceState *dev, Error **errp)
     create_unimplemented_device("sd1", memmap[K230_DEV_SD1].base,
                                 memmap[K230_DEV_SD1].size);
 
-    create_unimplemented_device("qspi0", memmap[K230_DEV_QSPI0].base,
-                                memmap[K230_DEV_QSPI0].size);
-
-    create_unimplemented_device("qspi1", memmap[K230_DEV_QSPI1].base,
-                                memmap[K230_DEV_QSPI1].size);
-
-    create_unimplemented_device("spi", memmap[K230_DEV_SPI].base,
-                                memmap[K230_DEV_SPI].size);
-
-    create_unimplemented_device("hi_sys_cfg", memmap[K230_DEV_HI_SYS_CFG].base,
-                                memmap[K230_DEV_HI_SYS_CFG].size);
-
     create_unimplemented_device("ddrc_cfg", memmap[K230_DEV_DDRC_CFG].base,
                                 memmap[K230_DEV_DDRC_CFG].size);
-
-    create_unimplemented_device("flash", memmap[K230_DEV_FLASH].base,
-                                memmap[K230_DEV_FLASH].size);
 }
 
 static void k230_soc_class_init(ObjectClass *oc, const void *data)
@@ -511,6 +559,7 @@ static void k230_machine_class_init(ObjectClass *oc, const void *data)
     mc->default_cpus = 1;
     mc->default_ram_id = "riscv.K230.ram"; /* DDR */
     mc->default_ram_size = memmap[K230_DEV_DDRC].size;
+
 }
 
 static const TypeInfo k230_machine_typeinfo = {
diff --git a/include/hw/riscv/k230.h b/include/hw/riscv/k230.h
index 592e1c26bf..99996a93b1 100644
--- a/include/hw/riscv/k230.h
+++ b/include/hw/riscv/k230.h
@@ -17,6 +17,7 @@
 
 #include "hw/core/boards.h"
 #include "hw/riscv/riscv_hart.h"
+#include "hw/ssi/dw_ssi.h"
 #include "hw/watchdog/k230_wdt.h"
 
 #define C908_CPU_HARTID   (0)
@@ -33,6 +34,7 @@ typedef struct K230SoCState {
     RISCVHartArrayState c908_cpu; /* Small core */
 
     K230WdtState wdt[2];
+    DwSsiState dw_ssi[3];
     MemoryRegion sram;
     MemoryRegion bootrom;
 
diff --git a/tests/qtest/k230-dw-ssi-test.c b/tests/qtest/k230-dw-ssi-test.c
new file mode 100644
index 0000000000..6361c2b5ee
--- /dev/null
+++ b/tests/qtest/k230-dw-ssi-test.c
@@ -0,0 +1,469 @@
+/*
+ * QTest for the Kendryte K230 DesignWare SSI
+ *
+ * Copyright (c) 2026 Kangjie Huang <[email protected]>
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+#include "qemu/osdep.h"
+#include "libqtest.h"
+#include "qemu/bitops.h"
+#include "qemu/units.h"
+
+#define K230_SPI0_BASE          0x91584000ULL
+#define K230_SPI1_BASE          0x91582000ULL
+#define K230_SPI2_BASE          0x91583000ULL
+#define K230_SSI_CTRLR0          0x000
+#define K230_SSI_CTRLR1          0x004
+#define K230_SSI_SSIENR          0x008
+#define K230_SSI_SER             0x010
+#define K230_SSI_BAUDR           0x014
+#define K230_SSI_TXFTLR          0x018
+#define K230_SSI_TXFLR           0x020
+#define K230_SSI_RXFLR           0x024
+#define K230_SSI_SR              0x028
+#define K230_SSI_IMR             0x02c
+#define K230_SSI_ISR             0x030
+#define K230_SSI_RISR            0x034
+#define K230_SSI_RXUICR          0x040
+#define K230_SSI_IDR             0x058
+#define K230_SSI_VERSION_ID      0x05c
+#define K230_SSI_DR0             0x060
+#define K230_SSI_SPI_CTRLR0      0x0f4
+
+#define K230_SSI_CTRLR0_RESET           0x00004007U
+#define K230_SSI_IDR_RESET              0xa1b2c3d5U
+#define K230_SSI_VERSION_RESET          0x3130332aU
+
+#define K230_SSI_CTRLR0_WRITABLE_MASK   0x000f7f1fU
+#define K230_SSI_BAUDR_WRITABLE_MASK    0x0000fffeU
+
+#define K230_SSI_CTRLR0_DFS_MASK        0x1fU
+#define K230_SSI_CTRLR0_TMOD_SHIFT      10
+#define K230_SSI_CTRLR0_SRL             BIT(13)
+#define K230_SSI_TXFTLR_TXFTHR_SHIFT     16
+#define K230_SSI_TMOD_TR                0
+#define K230_SSI_TMOD_TO                1
+#define K230_SSI_TMOD_RO                2
+#define K230_SSI_TMOD_EEPROM_READ       3
+
+#define K230_SSI_SR_BUSY                BIT(0)
+#define K230_SSI_SR_TFNF                BIT(1)
+#define K230_SSI_SR_TFE                 BIT(2)
+#define K230_SSI_SR_RFNE                BIT(3)
+
+#define K230_SSI_INT_TXE                BIT(0)
+#define K230_SSI_INT_TXO                BIT(1)
+#define K230_SSI_INT_RXO                BIT(3)
+#define K230_SSI_INT_RXU                BIT(2)
+
+#define K230_SSI_RXFTLR                 0x01c
+#define K230_SSI_RXOICR                 0x03c
+#define K230_SSI_TXEICR                 0x038
+#define K230_SSI_ICR                    0x048
+#define K230_SSI_DMACR                  0x04c
+#define K230_SSI_XIP_MODE_BITS          0x0fc
+
+#define K230_SSI_FIFO_DEPTH             256
+
+typedef struct K230SsiInstance {
+    uint64_t base;
+    uint32_t num_cs;
+    uint32_t imr_reset;
+} K230SsiInstance;
+
+static const K230SsiInstance k230_ssi_instances[3] = {
+    {
+        .base = K230_SPI0_BASE,
+        .num_cs = 1,
+        .imr_reset = 0x0000003fU,
+    }, {
+        .base = K230_SPI1_BASE,
+        .num_cs = 5,
+        .imr_reset = 0x0000001fU,
+    }, {
+        .base = K230_SPI2_BASE,
+        .num_cs = 5,
+        .imr_reset = 0x0000001fU,
+    },
+};
+
+static QTestState *k230_ssi_start(void)
+{
+    return qtest_init("-machine k230");
+}
+
+static uint32_t k230_ssi_readl(QTestState *qts, uint64_t base,
+                               uint32_t offset)
+{
+    return qtest_readl(qts, base + offset);
+}
+
+static void k230_ssi_writel(QTestState *qts, uint64_t base,
+                            uint32_t offset, uint32_t value)
+{
+    qtest_writel(qts, base + offset, value);
+}
+
+static void k230_ssi_disable(QTestState *qts, uint64_t base)
+{
+    k230_ssi_writel(qts, base, K230_SSI_SSIENR, 0);
+}
+
+static void k230_ssi_configure(QTestState *qts, uint64_t base,
+                               uint32_t tmod, uint32_t dfs_bits,
+                               uint32_t ndf)
+{
+    uint32_t ctrlr0;
+
+    g_assert_cmpuint(dfs_bits, >=, 4);
+    g_assert_cmpuint(dfs_bits, <=, 32);
+    g_assert_cmpuint(tmod, <=, K230_SSI_TMOD_EEPROM_READ);
+
+    k230_ssi_disable(qts, base);
+    ctrlr0 = (dfs_bits - 1) & K230_SSI_CTRLR0_DFS_MASK;
+    ctrlr0 |= tmod << K230_SSI_CTRLR0_TMOD_SHIFT;
+    k230_ssi_writel(qts, base, K230_SSI_CTRLR0, ctrlr0);
+    k230_ssi_writel(qts, base, K230_SSI_CTRLR1, ndf);
+    k230_ssi_writel(qts, base, K230_SSI_BAUDR, 2);
+}
+
+static void k230_ssi_enable_cs(QTestState *qts, uint64_t base, uint32_t ser)
+{
+    k230_ssi_writel(qts, base, K230_SSI_SER, ser);
+    k230_ssi_writel(qts, base, K230_SSI_SSIENR, 1);
+}
+
+static void k230_ssi_write_frame(QTestState *qts, uint64_t base,
+                                 uint32_t value)
+{
+    k230_ssi_writel(qts, base, K230_SSI_DR0, value);
+}
+
+static uint32_t k230_ssi_read_frame(QTestState *qts, uint64_t base)
+{
+    return k230_ssi_readl(qts, base, K230_SSI_DR0);
+}
+
+static void k230_ssi_wait_mask(QTestState *qts, uint64_t base,
+                               uint32_t offset, uint32_t mask,
+                               uint32_t expected)
+{
+    for (int i = 0; i < 1000; i++) {
+        uint32_t value = k230_ssi_readl(qts, base, offset);
+
+        if ((value & mask) == expected) {
+            return;
+        }
+        qtest_clock_step(qts, 1000);
+    }
+
+    g_assert_cmphex(k230_ssi_readl(qts, base, offset) & mask,
+                    ==, expected);
+}
+
+static void configure_loopback(QTestState *qts, uint32_t tmod,
+                               uint32_t ndf)
+{
+    uint32_t ctrlr0;
+
+    k230_ssi_configure(qts, K230_SPI1_BASE, tmod, 8, ndf);
+    ctrlr0 = k230_ssi_readl(qts, K230_SPI1_BASE, K230_SSI_CTRLR0);
+    k230_ssi_writel(qts, K230_SPI1_BASE, K230_SSI_CTRLR0,
+                    ctrlr0 | K230_SSI_CTRLR0_SRL);
+}
+
+static void test_register_contract(void)
+{
+    QTestState *qts = k230_ssi_start();
+
+    for (int i = 0; i < ARRAY_SIZE(k230_ssi_instances); i++) {
+        const K230SsiInstance *inst = &k230_ssi_instances[i];
+
+        g_assert_cmphex(k230_ssi_readl(qts, inst->base, K230_SSI_CTRLR0),
+                        ==, K230_SSI_CTRLR0_RESET);
+        g_assert_cmphex(k230_ssi_readl(qts, inst->base, K230_SSI_SSIENR),
+                        ==, 0);
+        g_assert_cmphex(k230_ssi_readl(qts, inst->base,
+                                      K230_SSI_SPI_CTRLR0),
+                        ==, 0);
+        g_assert_cmphex(k230_ssi_readl(qts, inst->base, K230_SSI_IMR),
+                        ==, inst->imr_reset);
+        k230_ssi_writel(qts, inst->base, K230_SSI_SER, UINT32_MAX);
+        g_assert_cmphex(k230_ssi_readl(qts, inst->base, K230_SSI_SER),
+                        ==, MAKE_64BIT_MASK(0, inst->num_cs));
+    }
+
+    g_assert_cmphex(k230_ssi_readl(qts, K230_SPI0_BASE, K230_SSI_IDR),
+                    ==, K230_SSI_IDR_RESET);
+    g_assert_cmphex(k230_ssi_readl(qts, K230_SPI0_BASE,
+                                  K230_SSI_VERSION_ID),
+                    ==, K230_SSI_VERSION_RESET);
+    k230_ssi_writel(qts, K230_SPI1_BASE, K230_SSI_CTRLR0, UINT32_MAX);
+    g_assert_cmphex(k230_ssi_readl(qts, K230_SPI1_BASE, K230_SSI_CTRLR0),
+                    ==, K230_SSI_CTRLR0_WRITABLE_MASK);
+    /* SSTE is R/W per TRM; the frame-boundary toggle is not modelled. */
+    k230_ssi_writel(qts, K230_SPI1_BASE, K230_SSI_CTRLR0,
+                    K230_SSI_CTRLR0_RESET & ~BIT(14));
+    g_assert_cmphex(k230_ssi_readl(qts, K230_SPI1_BASE, K230_SSI_CTRLR0) &
+                    BIT(14), ==, 0);
+    k230_ssi_writel(qts, K230_SPI1_BASE, K230_SSI_BAUDR, UINT32_MAX);
+    g_assert_cmphex(k230_ssi_readl(qts, K230_SPI1_BASE, K230_SSI_BAUDR),
+                    ==, K230_SSI_BAUDR_WRITABLE_MASK);
+
+    qtest_system_reset(qts);
+    g_assert_cmphex(k230_ssi_readl(qts, K230_SPI0_BASE, K230_SSI_CTRLR0),
+                    ==, K230_SSI_CTRLR0_RESET);
+    qtest_quit(qts);
+}
+
+static void test_pio_data_path(void)
+{
+    QTestState *qts = k230_ssi_start();
+    uint32_t status;
+
+    configure_loopback(qts, K230_SSI_TMOD_TR, 0);
+    k230_ssi_enable_cs(qts, K230_SPI1_BASE, BIT(0));
+    k230_ssi_write_frame(qts, K230_SPI1_BASE, 0xa5);
+    k230_ssi_wait_mask(qts, K230_SPI1_BASE, K230_SSI_SR,
+                       K230_SSI_SR_RFNE, K230_SSI_SR_RFNE);
+    g_assert_cmphex(k230_ssi_read_frame(qts, K230_SPI1_BASE), ==, 0xa5);
+
+    configure_loopback(qts, K230_SSI_TMOD_RO, 3);
+    k230_ssi_enable_cs(qts, K230_SPI1_BASE, BIT(0));
+    k230_ssi_write_frame(qts, K230_SPI1_BASE, 0);
+    k230_ssi_wait_mask(qts, K230_SPI1_BASE, K230_SSI_RXFLR,
+                       UINT32_MAX, 4);
+    for (int i = 0; i < 4; i++) {
+        g_assert_cmphex(k230_ssi_read_frame(qts, K230_SPI1_BASE), ==, 0);
+    }
+
+    k230_ssi_disable(qts, K230_SPI1_BASE);
+    status = k230_ssi_readl(qts, K230_SPI1_BASE, K230_SSI_SR);
+    g_assert_cmpuint(k230_ssi_readl(qts, K230_SPI1_BASE, K230_SSI_RXFLR),
+                     ==, 0);
+    g_assert_cmphex(status & (K230_SSI_SR_BUSY | K230_SSI_SR_TFNF |
+                              K230_SSI_SR_TFE | K230_SSI_SR_RFNE),
+                    ==, K230_SSI_SR_TFNF | K230_SSI_SR_TFE);
+    qtest_quit(qts);
+}
+
+static void test_interrupt_controller(void)
+{
+    QTestState *qts = k230_ssi_start();
+
+    k230_ssi_configure(qts, K230_SPI1_BASE, K230_SSI_TMOD_TR, 8, 0);
+    k230_ssi_writel(qts, K230_SPI1_BASE, K230_SSI_TXFTLR, 0);
+    k230_ssi_writel(qts, K230_SPI1_BASE, K230_SSI_IMR, 0);
+    k230_ssi_writel(qts, K230_SPI1_BASE, K230_SSI_SSIENR, 1);
+    g_assert_cmphex(k230_ssi_readl(qts, K230_SPI1_BASE, K230_SSI_RISR) &
+                    K230_SSI_INT_TXE, ==, K230_SSI_INT_TXE);
+    g_assert_cmphex(k230_ssi_readl(qts, K230_SPI1_BASE, K230_SSI_ISR) &
+                    K230_SSI_INT_TXE, ==, 0);
+    k230_ssi_writel(qts, K230_SPI1_BASE, K230_SSI_IMR,
+                    K230_SSI_INT_TXE);
+    g_assert_cmphex(k230_ssi_readl(qts, K230_SPI1_BASE, K230_SSI_ISR) &
+                    K230_SSI_INT_TXE, ==, K230_SSI_INT_TXE);
+
+    k230_ssi_writel(qts, K230_SPI1_BASE, K230_SSI_IMR,
+                    K230_SSI_INT_RXU);
+    (void)k230_ssi_read_frame(qts, K230_SPI1_BASE);
+    g_assert_cmphex(k230_ssi_readl(qts, K230_SPI1_BASE, K230_SSI_RISR) &
+                    K230_SSI_INT_RXU, ==, K230_SSI_INT_RXU);
+    g_assert_cmphex(k230_ssi_readl(qts, K230_SPI1_BASE, K230_SSI_ISR) &
+                    K230_SSI_INT_RXU, ==, K230_SSI_INT_RXU);
+    g_assert_cmphex(k230_ssi_readl(qts, K230_SPI1_BASE, K230_SSI_RXUICR),
+                    ==, 1);
+    g_assert_cmphex(k230_ssi_readl(qts, K230_SPI1_BASE, K230_SSI_RISR) &
+                    K230_SSI_INT_RXU, ==, 0);
+    qtest_quit(qts);
+}
+
+
+static void test_tx_only_mode(void)
+{
+    QTestState *qts = k230_ssi_start();
+
+    configure_loopback(qts, K230_SSI_TMOD_TO, 0);
+    k230_ssi_enable_cs(qts, K230_SPI1_BASE, BIT(0));
+    k230_ssi_write_frame(qts, K230_SPI1_BASE, 0x55);
+    g_assert_cmphex(k230_ssi_readl(qts, K230_SPI1_BASE, K230_SSI_SR) &
+                    K230_SSI_SR_TFE, ==, K230_SSI_SR_TFE);
+    g_assert_cmphex(k230_ssi_readl(qts, K230_SPI1_BASE, K230_SSI_TXFLR),
+                    ==, 0);
+    qtest_quit(qts);
+}
+
+static void test_eeprom_read_contract(void)
+{
+    QTestState *qts = k230_ssi_start();
+
+    configure_loopback(qts, K230_SSI_TMOD_EEPROM_READ, 3);
+    k230_ssi_enable_cs(qts, K230_SPI1_BASE, BIT(0));
+    k230_ssi_write_frame(qts, K230_SPI1_BASE, 0x03);
+    k230_ssi_wait_mask(qts, K230_SPI1_BASE, K230_SSI_RXFLR,
+                       UINT32_MAX, 4);
+    for (int i = 0; i < 4; i++) {
+        g_assert_cmphex(k230_ssi_read_frame(qts, K230_SPI1_BASE), ==, 0);
+    }
+
+    configure_loopback(qts, K230_SSI_TMOD_EEPROM_READ, 0);
+    k230_ssi_enable_cs(qts, K230_SPI1_BASE, BIT(0));
+    k230_ssi_write_frame(qts, K230_SPI1_BASE, 0x03);
+    k230_ssi_wait_mask(qts, K230_SPI1_BASE, K230_SSI_RXFLR,
+                       UINT32_MAX, 1);
+    g_assert_cmphex(k230_ssi_read_frame(qts, K230_SPI1_BASE), ==, 0);
+    qtest_quit(qts);
+}
+
+static void test_rx_fifo_overflow(void)
+{
+    QTestState *qts = k230_ssi_start();
+
+    configure_loopback(qts, K230_SSI_TMOD_TR, 0);
+    k230_ssi_enable_cs(qts, K230_SPI1_BASE, BIT(0));
+    for (int i = 0; i < K230_SSI_FIFO_DEPTH + 1; i++) {
+        k230_ssi_write_frame(qts, K230_SPI1_BASE, 0xa5);
+    }
+    g_assert_cmphex(k230_ssi_readl(qts, K230_SPI1_BASE, K230_SSI_RISR) &
+                    K230_SSI_INT_RXO, ==, K230_SSI_INT_RXO);
+    g_assert_cmphex(k230_ssi_readl(qts, K230_SPI1_BASE,
+                                  K230_SSI_RXOICR), ==, 1);
+    g_assert_cmphex(k230_ssi_readl(qts, K230_SPI1_BASE, K230_SSI_RISR) &
+                    K230_SSI_INT_RXO, ==, 0);
+    qtest_quit(qts);
+}
+
+static void test_txfthr_start_gate(void)
+{
+    QTestState *qts = k230_ssi_start();
+
+    /* TXFTHR=4: transfer starts only once 5+ frames are queued. */
+    k230_ssi_configure(qts, K230_SPI1_BASE, K230_SSI_TMOD_TR, 8, 0);
+    k230_ssi_writel(qts, K230_SPI1_BASE, K230_SSI_TXFTLR, 4 << 16);
+    k230_ssi_enable_cs(qts, K230_SPI1_BASE, BIT(0));
+
+    k230_ssi_write_frame(qts, K230_SPI1_BASE, 0x11);
+    g_assert_cmphex(k230_ssi_readl(qts, K230_SPI1_BASE, K230_SSI_TXFLR),
+                    ==, 1);
+    g_assert_cmphex(k230_ssi_readl(qts, K230_SPI1_BASE, K230_SSI_RXFLR),
+                    ==, 0);
+
+    for (int i = 0; i < 4; i++) {
+        k230_ssi_write_frame(qts, K230_SPI1_BASE, 0x22 + i);
+    }
+    g_assert_cmphex(k230_ssi_readl(qts, K230_SPI1_BASE, K230_SSI_TXFLR),
+                    ==, 0);
+    g_assert_cmphex(k230_ssi_readl(qts, K230_SPI1_BASE, K230_SSI_RXFLR),
+                    ==, 5);
+    qtest_quit(qts);
+}
+
+static void test_baudr_zero(void)
+{
+    QTestState *qts = k230_ssi_start();
+
+    /* SCKDV=0 disables sclk_out: queued frames must not be shifted out. */
+    k230_ssi_configure(qts, K230_SPI1_BASE, K230_SSI_TMOD_TR, 8, 0);
+    k230_ssi_writel(qts, K230_SPI1_BASE, K230_SSI_BAUDR, 0);
+    k230_ssi_enable_cs(qts, K230_SPI1_BASE, BIT(0));
+
+    k230_ssi_write_frame(qts, K230_SPI1_BASE, 0xa5);
+    g_assert_cmphex(k230_ssi_readl(qts, K230_SPI1_BASE, K230_SSI_TXFLR),
+                    ==, 1);
+    g_assert_cmphex(k230_ssi_readl(qts, K230_SPI1_BASE, K230_SSI_RXFLR),
+                    ==, 0);
+    qtest_quit(qts);
+}
+
+static void test_ser_terminates_ro(void)
+{
+    QTestState *qts = k230_ssi_start();
+
+    /*
+     * RO transfer with NDF larger than the RX FIFO: after the FIFO
+     * fills, SER=0 must terminate the pending transaction.  Re-selecting
+     * the slave must not resume receiving the stale remaining frames
+     * without a new dummy word.
+     */
+    k230_ssi_configure(qts, K230_SPI1_BASE, K230_SSI_TMOD_RO, 8, 0xffff);
+    k230_ssi_enable_cs(qts, K230_SPI1_BASE, BIT(0));
+    k230_ssi_write_frame(qts, K230_SPI1_BASE, 0);
+    k230_ssi_wait_mask(qts, K230_SPI1_BASE, K230_SSI_RXFLR,
+                       UINT32_MAX, K230_SSI_FIFO_DEPTH);
+
+    k230_ssi_writel(qts, K230_SPI1_BASE, K230_SSI_SER, 0);
+    (void)k230_ssi_read_frame(qts, K230_SPI1_BASE);
+    g_assert_cmphex(k230_ssi_readl(qts, K230_SPI1_BASE, K230_SSI_RXFLR),
+                    ==, K230_SSI_FIFO_DEPTH - 1);
+
+    k230_ssi_writel(qts, K230_SPI1_BASE, K230_SSI_SER, BIT(0));
+    g_assert_cmphex(k230_ssi_readl(qts, K230_SPI1_BASE, K230_SSI_RXFLR),
+                    ==, K230_SSI_FIFO_DEPTH - 1);
+
+    /* A direct BIT(0) -> BIT(1) switch must also terminate the tx. */
+    k230_ssi_writel(qts, K230_SPI1_BASE, K230_SSI_SER, BIT(1));
+    g_assert_cmphex(k230_ssi_readl(qts, K230_SPI1_BASE, K230_SSI_RXFLR),
+                    ==, K230_SSI_FIFO_DEPTH - 1);
+    qtest_quit(qts);
+}
+
+static void test_icr_total_clear(void)
+{
+    QTestState *qts = k230_ssi_start();
+
+    configure_loopback(qts, K230_SSI_TMOD_TR, 0);
+    k230_ssi_enable_cs(qts, K230_SPI1_BASE, BIT(0));
+    (void)k230_ssi_read_frame(qts, K230_SPI1_BASE); /* latch RXU */
+    for (int i = 0; i < K230_SSI_FIFO_DEPTH + 1; i++) {
+        k230_ssi_write_frame(qts, K230_SPI1_BASE, 0xa5);
+    }
+    g_assert_cmphex(k230_ssi_readl(qts, K230_SPI1_BASE, K230_SSI_RISR) &
+                    K230_SSI_INT_RXU, ==, K230_SSI_INT_RXU);
+    g_assert_cmphex(k230_ssi_readl(qts, K230_SPI1_BASE,
+                                  K230_SSI_ICR), ==, 1);
+    g_assert_cmphex(k230_ssi_readl(qts, K230_SPI1_BASE, K230_SSI_RISR) &
+                    K230_SSI_INT_RXU, ==, 0);
+    g_assert_cmphex(k230_ssi_readl(qts, K230_SPI1_BASE, K230_SSI_RISR) &
+                    K230_SSI_INT_RXO, ==, 0);
+    qtest_quit(qts);
+}
+
+static void test_unsupported_registers(void)
+{
+    QTestState *qts = k230_ssi_start();
+    static const uint32_t unsupported[] = {
+        K230_SSI_DMACR, K230_SSI_XIP_MODE_BITS, K230_SSI_SPI_CTRLR0,
+    };
+
+    for (size_t i = 0; i < ARRAY_SIZE(unsupported); i++) {
+        uint32_t off = unsupported[i];
+
+        g_assert_cmphex(k230_ssi_readl(qts, K230_SPI1_BASE, off), ==, 0);
+        k230_ssi_writel(qts, K230_SPI1_BASE, off, UINT32_MAX);
+        g_assert_cmphex(k230_ssi_readl(qts, K230_SPI1_BASE, off), ==, 0);
+    }
+    qtest_quit(qts);
+}
+
+int main(int argc, char **argv)
+{
+    g_test_init(&argc, &argv, NULL);
+
+    qtest_add_func("/k230-dw-ssi/register-contract", test_register_contract);
+    qtest_add_func("/k230-dw-ssi/pio-data-path", test_pio_data_path);
+    qtest_add_func("/k230-dw-ssi/interrupt-controller",
+                   test_interrupt_controller);
+    qtest_add_func("/k230-dw-ssi/tx-only-mode", test_tx_only_mode);
+    qtest_add_func("/k230-dw-ssi/eeprom-read-contract",
+                   test_eeprom_read_contract);
+    qtest_add_func("/k230-dw-ssi/rx-fifo-overflow", test_rx_fifo_overflow);
+    qtest_add_func("/k230-dw-ssi/txfthr-start-gate", test_txfthr_start_gate);
+    qtest_add_func("/k230-dw-ssi/baudr-zero", test_baudr_zero);
+    qtest_add_func("/k230-dw-ssi/ser-terminates-ro", test_ser_terminates_ro);
+    qtest_add_func("/k230-dw-ssi/icr-total-clear", test_icr_total_clear);
+    qtest_add_func("/k230-dw-ssi/unsupported-registers",
+                   test_unsupported_registers);
+    return g_test_run();
+}
diff --git a/tests/qtest/meson.build b/tests/qtest/meson.build
index 56ff860e21..f5bcc7bece 100644
--- a/tests/qtest/meson.build
+++ b/tests/qtest/meson.build
@@ -297,7 +297,8 @@ qtests_riscv64 = ['riscv-csr-test'] + \
   (config_all_devices.has_key('CONFIG_IOMMU_TESTDEV') and
    config_all_devices.has_key('CONFIG_RISCV_IOMMU') ?
    ['iommu-riscv-test'] : []) + \
-  (config_all_devices.has_key('CONFIG_K230') ? ['k230-wdt-test'] : [])
+  (config_all_devices.has_key('CONFIG_K230') ?
+   ['k230-wdt-test', 'k230-dw-ssi-test'] : [])
 
 qtests_hexagon = ['boot-serial-test']
 
@@ -400,6 +401,7 @@ qtests = {
                        [gio],
   'erst-test': files('erst-test.c'),
   'ivshmem-test': [rt, '../../contrib/ivshmem-server/ivshmem-server.c'],
+  'k230-dw-ssi-test': files('k230-dw-ssi-test.c'),
   'migration-test': test_migration_files + migration_tls_files + migration_colo_files,
   'pxe-test': files('boot-sector.c'),
   'pnv-xive2-test': files('pnv-xive2-common.c', 'pnv-xive2-flush-sync.c',
-- 
2.43.0