[PATCH 08/11] hw/ssi: Implement K230 SSI internal DMA transfers

Kangjie Huang <[email protected]> Sun, 26 Jul 2026 20:28:26 +0800
Newsgroups org.nongnu.qemu-riscv,org.nongnu.qemu-devel
Message-ID <8f3b84c222bc1d4cc5d5704b13cd70e9af39cce4.1785064313.git.flamboyant.h.01@gmail.com>
Implement synchronous internal DMA for 8-bit SDR Dual and Quad transfer
modes. The K230 SDK driver polls DONE before touching the buffer, so a
synchronous model is sufficient and avoids racing guest memory. Only the
8-bit SDR Dual and Quad modes are supported, matching what that driver
uses.

Build enhanced commands from SPIDR and SPIAR, move data through AXIAR0/1,
and report completion or guest-memory failures through the DONE and AXIE
interrupt causes.

Keep DR accesses out of the FIFO while IDMA is enabled. Implement the
read-clear status registers, terminate each transaction with SSI
disabled and chip select inactive, and migrate the completed-frame
count.

Cover a Quad read into guest RAM, completed-frame reporting, DONE
routing and clearing, and the AXIE path for an invalid guest address.

Exercise QSPI read and write commands from U-Boot and Linux. With spi0
configured for QSPI in the device tree, boot a Linux image from QSPI
flash.

Signed-off-by: Kangjie Huang <[email protected]>
---
 docs/system/riscv/k230.rst     |   1 +
 hw/ssi/k230_dw_ssi.c           | 203 +++++++++++++++++++++++++++++++--
 include/hw/ssi/k230_dw_ssi.h   |   1 +
 tests/qtest/k230-dw-ssi-test.c |  95 +++++++++++++++
 4 files changed, 290 insertions(+), 10 deletions(-)

diff --git a/docs/system/riscv/k230.rst b/docs/system/riscv/k230.rst
index 0d2617c0f9..3067c1b5d7 100644
--- a/docs/system/riscv/k230.rst
+++ b/docs/system/riscv/k230.rst
@@ -22,6 +22,7 @@ The ``k230`` machine supports the following devices:
 * 5 UART
 * 3 K230 SSI controllers for SPI and QSPI
 * Optional SPI NOR flash on spi0 CS0
+* QSPI IDMA transfers for 8-bit SDR Dual and Quad modes
 
 Boot options
 ------------
diff --git a/hw/ssi/k230_dw_ssi.c b/hw/ssi/k230_dw_ssi.c
index 31da3a06a3..65b2a8c245 100644
--- a/hw/ssi/k230_dw_ssi.c
+++ b/hw/ssi/k230_dw_ssi.c
@@ -25,6 +25,7 @@
 #include "qemu/bitops.h"
 #include "qemu/log.h"
 #include "qemu/module.h"
+#include "system/dma.h"
 
 #define K230_DW_SSI_FIFO_CAPACITY 256
 
@@ -331,7 +332,7 @@ static uint32_t k230_dw_ssi_frame_masked(K230DwSsiState *s)
     return bits == 32 ? UINT32_MAX : MAKE_64BIT_MASK(0, bits);
 }
 
-static bool k230_dw_ssi_enabled(K230DwSsiState *s)
+static bool k230_dw_ssi_enabled(const K230DwSsiState *s)
 {
     return FIELD_EX32(s->regs[R_SSIENR], SSIENR, SSIC_EN);
 }
@@ -408,6 +409,7 @@ static uint32_t k230_dw_ssi_status(K230DwSsiState *s)
     sr = FIELD_DP32(sr, SR, RFNE, rx_used != 0);
     sr = FIELD_DP32(sr, SR, RFF,
                     rx_used == K230_DW_SSI_FIFO_CAPACITY);
+    sr = FIELD_DP32(sr, SR, CMPLTD_DF, s->idma_completed_frames);
 
     return sr;
 }
@@ -623,6 +625,172 @@ static uint32_t k230_dw_ssi_dummy_bytes(uint32_t spi_frf,
     return DIV_ROUND_UP(wait_cycles * lines, 8);
 }
 
+static bool k230_dw_ssi_idma_enabled(const K230DwSsiState *s)
+{
+    return FIELD_EX32(s->regs[R_DMACR], DMACR, IDMAE);
+}
+
+static uint64_t k230_dw_ssi_idma_address(const K230DwSsiState *s)
+{
+    return s->regs[R_AXIAR0] | ((uint64_t)s->regs[R_AXIAR1] << 32);
+}
+
+static bool k230_dw_ssi_idma_triggered(const K230DwSsiState *s)
+{
+    uint32_t ser = s->regs[R_SER];
+
+    return k230_dw_ssi_idma_enabled(s) &&
+           k230_dw_ssi_enabled(s) && ser &&
+           !(ser & (ser - 1)) &&
+           s->phase == K230_DW_SSI_PHASE_IDLE;
+}
+
+static void k230_dw_ssi_idma_end(K230DwSsiState *s, uint32_t cause)
+{
+    s->regs[R_SSIENR] = 0;
+    s->phase = K230_DW_SSI_PHASE_IDLE;
+    s->remaining_frames = 0;
+    k230_dw_ssi_deselect(s);
+    s->irq_latched |= cause;
+    k230_dw_ssi_update_irq(s);
+}
+
+static void k230_dw_ssi_idma_fail(K230DwSsiState *s, const char *operation)
+{
+    qemu_log_mask(LOG_GUEST_ERROR,
+                  "%s: IDMA %s memory access failed\n",
+                  DEVICE(s)->canonical_path, operation);
+    s->idma_completed_frames = 0;
+    k230_dw_ssi_idma_end(s, R_RISR_AXIER_MASK);
+}
+
+/*
+ * Supported SDK paths observe the final memory contents and DONE/AXIE,
+ * so complete IDMA synchronously without modeling AXI timing or FIFO
+ * backpressure.
+ */
+static void k230_dw_ssi_try_idma(K230DwSsiState *s)
+{
+    K230DwSsiEnhancedCommand command = { 0 };
+    g_autofree uint8_t *buffer = NULL;
+    uint64_t address;
+    uint32_t dummy_bytes;
+    uint32_t length;
+    MemTxResult result;
+
+    if (!k230_dw_ssi_idma_triggered(s)) {
+        return;
+    }
+
+    if (!FIELD_EX32(s->regs[R_DMACR], DMACR, AINC)) {
+        qemu_log_mask(LOG_UNIMP,
+                      "%s: fixed-address IDMA is unsupported\n",
+                      DEVICE(s)->canonical_path);
+        s->idma_completed_frames = 0;
+        k230_dw_ssi_idma_end(s, 0);
+        return;
+    }
+
+    if (!fifo32_is_empty(&s->tx_fifo) ||
+        !fifo32_is_empty(&s->rx_fifo)) {
+        qemu_log_mask(LOG_GUEST_ERROR,
+                      "%s: IDMA requires empty TX and RX FIFOs\n",
+                      DEVICE(s)->canonical_path);
+        s->idma_completed_frames = 0;
+        k230_dw_ssi_idma_end(s, 0);
+        return;
+    }
+
+    if (FIELD_EX32(s->regs[R_CTRLR0], CTRLR0, DFS) != 7) {
+        qemu_log_mask(LOG_UNIMP,
+                      "%s: IDMA only supports 8-bit data frames\n",
+                      DEVICE(s)->canonical_path);
+        s->idma_completed_frames = 0;
+        k230_dw_ssi_idma_end(s, 0);
+        return;
+    }
+
+    if (!k230_dw_ssi_decode_enhanced_command(s, &command)) {
+        s->idma_completed_frames = 0;
+        k230_dw_ssi_idma_end(s, 0);
+        return;
+    }
+
+    command.instruction = s->regs[R_SPIDR] &
+        (uint32_t)MAKE_64BIT_MASK(0, command.instruction_bits);
+    command.address = s->regs[R_SPIAR] &
+        (uint32_t)MAKE_64BIT_MASK(0, command.address_bits);
+    length = command.data_frames;
+    address = k230_dw_ssi_idma_address(s);
+    s->idma_completed_frames = 0;
+
+    if (address > UINT64_MAX - (length - 1)) {
+        k230_dw_ssi_idma_fail(s, "address range");
+        return;
+    }
+
+    buffer = g_malloc(length);
+    if (command.tmod == K230_DW_SSI_TMOD_TO) {
+        result = dma_memory_read(&address_space_memory, address, buffer,
+                                 length, MEMTXATTRS_UNSPECIFIED);
+        if (result != MEMTX_OK) {
+            k230_dw_ssi_idma_fail(s, "source");
+            return;
+        }
+    }
+
+    k230_dw_ssi_update_cs(s);
+    if (s->active_cs < 0) {
+        k230_dw_ssi_idma_end(s, 0);
+        return;
+    }
+
+    if (command.instruction_bits != 0) {
+        k230_dw_ssi_send_enhanced_field(s, command.instruction,
+                                         command.instruction_bits);
+    }
+    if (command.address_bits != 0) {
+        k230_dw_ssi_send_enhanced_field(s, command.address,
+                                         command.address_bits);
+    }
+    if (command.mode_bits_enabled) {
+        k230_dw_ssi_send_enhanced_field(s, command.mode,
+                                         command.mode_bits);
+    } else if (command.trans_type == 1 && command.wait_cycles >= 2) {
+        /*
+         * The SDK's 1-4-4 read supplies its mode byte through XIP_MODE_BITS
+         * without XIP_MD_BIT_EN; one Quad byte consumes two wait cycles.
+         */
+        k230_dw_ssi_send_enhanced_field(s, s->regs[R_XIP_MODE_BITS], 8);
+        command.wait_cycles -= 2;
+    }
+
+    dummy_bytes = k230_dw_ssi_dummy_bytes(
+        command.spi_frf, command.trans_type, command.wait_cycles);
+    for (uint32_t i = 0; i < dummy_bytes; i++) {
+        ssi_transfer(s->spi, 0);
+    }
+
+    if (command.tmod == K230_DW_SSI_TMOD_RO) {
+        for (uint32_t i = 0; i < length; i++) {
+            buffer[i] = ssi_transfer(s->spi, 0);
+        }
+        result = dma_memory_write(&address_space_memory, address, buffer,
+                                  length, MEMTXATTRS_UNSPECIFIED);
+        if (result != MEMTX_OK) {
+            k230_dw_ssi_idma_fail(s, "destination");
+            return;
+        }
+    } else {
+        for (uint32_t i = 0; i < length; i++) {
+            ssi_transfer(s->spi, buffer[i]);
+        }
+    }
+
+    s->idma_completed_frames = length;
+    k230_dw_ssi_idma_end(s, R_RISR_DONER_MASK);
+}
+
 static void k230_dw_ssi_run_enhanced_rx_data(K230DwSsiState *s)
 {
     while (!fifo32_is_full(&s->rx_fifo) &&
@@ -878,6 +1046,10 @@ static uint64_t k230_dw_ssi_read(void *opaque, hwaddr addr, unsigned int size)
     uint32_t value = 0;
 
     if (k230_dw_ssi_is_dr(addr)) {
+        if (k230_dw_ssi_idma_enabled(s)) {
+            return 0;
+        }
+
         if (!fifo32_is_empty(&s->rx_fifo)) {
             value = fifo32_pop(&s->rx_fifo) & k230_dw_ssi_frame_masked(s);
         } else {
@@ -966,8 +1138,10 @@ static uint64_t k230_dw_ssi_read(void *opaque, hwaddr addr, unsigned int size)
                R_RISR_RXOIR_MASK | R_RISR_MSTIR_MASK);
         break;
     case A_AXIECR:
+        value = k230_dw_ssi_irq_read_clear(s, R_RISR_AXIER_MASK);
+        break;
     case A_DONECR:
-        value = 0;
+        value = k230_dw_ssi_irq_read_clear(s, R_RISR_DONER_MASK);
         break;
     default:
         if (addr >= K230_DW_SSI_REGS_SIZE || (addr & 0x3) != 0) {
@@ -987,6 +1161,10 @@ static void k230_dw_ssi_write(void *opaque, hwaddr addr,
     K230DwSsiState *s = K230_DW_SSI(opaque);
 
     if (k230_dw_ssi_is_dr(addr)) {
+        if (k230_dw_ssi_idma_enabled(s)) {
+            return;
+        }
+
         k230_dw_ssi_push_tx(s, value);
         return;
     }
@@ -1028,7 +1206,11 @@ static void k230_dw_ssi_write(void *opaque, hwaddr addr,
         }
 
         k230_dw_ssi_update_cs(s);
-        k230_dw_ssi_run_transfer(s);
+        if (k230_dw_ssi_idma_enabled(s)) {
+            k230_dw_ssi_try_idma(s);
+        } else {
+            k230_dw_ssi_run_transfer(s);
+        }
         k230_dw_ssi_update_irq(s);
         break;
     }
@@ -1046,7 +1228,11 @@ static void k230_dw_ssi_write(void *opaque, hwaddr addr,
         }
 
         k230_dw_ssi_update_cs(s);
-        k230_dw_ssi_run_transfer(s);
+        if (k230_dw_ssi_idma_enabled(s)) {
+            k230_dw_ssi_try_idma(s);
+        } else {
+            k230_dw_ssi_run_transfer(s);
+        }
         k230_dw_ssi_update_irq(s);
         break;
     }
@@ -1085,12 +1271,7 @@ static void k230_dw_ssi_write(void *opaque, hwaddr addr,
     case A_DMACR:
         k230_dw_ssi_write_masked(s, R_DMACR, value,
                                  K230_DW_SSI_DMACR_WRITABLE_MASK);
-        if (FIELD_EX32(s->regs[R_DMACR], DMACR, IDMAE)) {
-            qemu_log_mask(LOG_UNIMP,
-                          "%s: DMACR.IDMAE enabled, internal DMA is not "
-                          "implemented\n",
-                          DEVICE(s)->canonical_path);
-        }
+        k230_dw_ssi_try_idma(s);
         break;
     case A_AXIAWLEN:
         k230_dw_ssi_write_masked(s, R_AXIAWLEN, value,
@@ -1188,6 +1369,7 @@ static void k230_dw_ssi_enter_reset(Object *obj, ResetType type)
     s->phase = K230_DW_SSI_PHASE_IDLE;
     s->remaining_frames = 0;
     s->irq_latched = 0;
+    s->idma_completed_frames = 0;
     memset(&s->enhanced, 0, sizeof(s->enhanced));
 
     s->regs[R_CTRLR0] = K230_DW_SSI_CTRLR0_RESET;
@@ -1250,6 +1432,7 @@ static const VMStateDescription vmstate_k230_dw_ssi = {
         VMSTATE_FIFO32(tx_fifo, K230DwSsiState),
         VMSTATE_FIFO32(rx_fifo, K230DwSsiState),
         VMSTATE_UINT32(irq_latched, K230DwSsiState),
+        VMSTATE_UINT32(idma_completed_frames, K230DwSsiState),
         VMSTATE_UINT32(phase, K230DwSsiState),
         VMSTATE_UINT32(remaining_frames, K230DwSsiState),
         VMSTATE_UINT32(enhanced.instruction, K230DwSsiState),
diff --git a/include/hw/ssi/k230_dw_ssi.h b/include/hw/ssi/k230_dw_ssi.h
index b692a616d6..3a1b2c08cd 100644
--- a/include/hw/ssi/k230_dw_ssi.h
+++ b/include/hw/ssi/k230_dw_ssi.h
@@ -87,6 +87,7 @@ struct K230DwSsiState {
     uint32_t regs[K230_DW_SSI_NUM_REGS];
 
     uint32_t irq_latched;
+    uint32_t idma_completed_frames;
 
     uint32_t phase;
     uint32_t remaining_frames;
diff --git a/tests/qtest/k230-dw-ssi-test.c b/tests/qtest/k230-dw-ssi-test.c
index 658b1f4cbf..ddf5e13145 100644
--- a/tests/qtest/k230-dw-ssi-test.c
+++ b/tests/qtest/k230-dw-ssi-test.c
@@ -79,12 +79,18 @@
 #define K230_SSI_SR_TFNF                BIT(1)
 #define K230_SSI_SR_TFE                 BIT(2)
 #define K230_SSI_SR_RFNE                BIT(3)
+#define K230_SSI_SR_CMPLTD_DF_SHIFT     15
+#define K230_SSI_SR_CMPLTD_DF_MASK      (0x1ffffU << 15)
 
 #define K230_SSI_INT_TXE                BIT(0)
 #define K230_SSI_INT_RXU                BIT(2)
 #define K230_SSI_INT_AXIE               BIT(8)
 #define K230_SSI_INT_DONE               BIT(11)
 
+#define K230_SSI_IDMAE                  BIT(2)
+#define K230_SSI_AINC                   BIT(6)
+#define K230_SSI_DMA_ADDR               0x80201000ULL
+
 #define K230_SSI_IRQ_TXE                0
 #define K230_SSI_IRQ_RXU                5
 #define K230_SSI_IRQ_DONE               7
@@ -414,6 +420,44 @@ static void read_enhanced_result(QTestState *qts, uint8_t *data, size_t len)
     k230_ssi_disable(qts, K230_SPI0_BASE);
 }
 
+static void configure_idma(QTestState *qts, uint32_t tmod,
+                           uint8_t opcode, uint32_t flash_address,
+                           uint64_t dma_address, size_t length)
+{
+    uint32_t ctrlr0;
+    uint32_t spi_ctrlr0;
+
+    k230_ssi_configure(qts, K230_SPI0_BASE, tmod, 8, length - 1);
+    k230_ssi_writel(qts, K230_SPI0_BASE, K230_SSI_SER, 0);
+    ctrlr0 = k230_ssi_readl(qts, K230_SPI0_BASE, K230_SSI_CTRLR0);
+    ctrlr0 |= K230_SSI_FRF_QUAD << K230_SSI_CTRLR0_SPI_FRF_SHIFT;
+    k230_ssi_writel(qts, K230_SPI0_BASE, K230_SSI_CTRLR0, ctrlr0);
+    spi_ctrlr0 = K230_SSI_SPI_CTRLR0_TRANS_TYPE(0) |
+                 K230_SSI_SPI_CTRLR0_ADDR_L(24) |
+                 K230_SSI_SPI_CTRLR0_INST_L_8 |
+                 K230_SSI_SPI_CTRLR0_WAIT(8);
+    k230_ssi_writel(qts, K230_SPI0_BASE, K230_SSI_SPI_CTRLR0, spi_ctrlr0);
+    k230_ssi_writel(qts, K230_SPI0_BASE, K230_SSI_DMACR,
+                    K230_SSI_IDMAE | K230_SSI_AINC);
+    k230_ssi_writel(qts, K230_SPI0_BASE, K230_SSI_SPIDR, opcode);
+    k230_ssi_writel(qts, K230_SPI0_BASE, K230_SSI_SPIAR, flash_address);
+    k230_ssi_writel(qts, K230_SPI0_BASE, K230_SSI_AXIAR0, dma_address);
+    k230_ssi_writel(qts, K230_SPI0_BASE, K230_SSI_AXIAR1,
+                    dma_address >> 32);
+    k230_ssi_writel(qts, K230_SPI0_BASE, K230_SSI_SSIENR, 1);
+    k230_ssi_writel(qts, K230_SPI0_BASE, K230_SSI_SER, BIT(0));
+}
+
+static void assert_idma_stopped(QTestState *qts, size_t completed)
+{
+    uint32_t status = k230_ssi_readl(qts, K230_SPI0_BASE, K230_SSI_SR);
+
+    g_assert_cmphex(k230_ssi_readl(qts, K230_SPI0_BASE, K230_SSI_SSIENR),
+                    ==, 0);
+    g_assert_cmpuint((status & K230_SSI_SR_CMPLTD_DF_MASK) >>
+                     K230_SSI_SR_CMPLTD_DF_SHIFT, ==, completed);
+}
+
 static void test_register_contract(void)
 {
     QTestState *qts = k230_ssi_start();
@@ -673,6 +717,56 @@ static void test_qspi_sdr(void)
     k230_ssi_flash_image_clear(&image);
 }
 
+static void test_idma(void)
+{
+    static const uint8_t expected[] = { 0xa5, 0x5a, 0x3c, 0xc3 };
+    K230SsiFlashImage image;
+    QTestState *qts = k230_ssi_start_with_flash(&image);
+    uint8_t actual[ARRAY_SIZE(expected)];
+
+    qtest_memset(qts, K230_SSI_DMA_ADDR, 0, sizeof(actual));
+    k230_ssi_writel(qts, K230_SPI0_BASE, K230_SSI_IMR, 0);
+    configure_idma(qts, K230_SSI_TMOD_RO, FLASH_CMD_QUAD_OUT,
+                   K230_SSI_FLASH_PATTERN_ADDR, K230_SSI_DMA_ADDR,
+                   sizeof(actual));
+    g_assert_cmphex(k230_ssi_readl(qts, K230_SPI0_BASE, K230_SSI_RISR) &
+                    K230_SSI_INT_DONE, ==, K230_SSI_INT_DONE);
+    g_assert_cmphex(k230_ssi_readl(qts, K230_SPI0_BASE, K230_SSI_ISR) &
+                    K230_SSI_INT_DONE, ==, 0);
+    k230_ssi_writel(qts, K230_SPI0_BASE, K230_SSI_IMR,
+                    K230_SSI_INT_DONE);
+    g_assert_true(k230_ssi_plic_pending(
+        qts, k230_ssi_instances[0].first_irq + K230_SSI_IRQ_DONE));
+    assert_idma_stopped(qts, sizeof(actual));
+    qtest_memread(qts, K230_SSI_DMA_ADDR, actual, sizeof(actual));
+    g_assert_cmpmem(actual, sizeof(actual), expected, sizeof(expected));
+    k230_ssi_writel(qts, K230_SPI0_BASE, K230_SSI_DONECR, 1);
+    g_assert_cmphex(k230_ssi_readl(qts, K230_SPI0_BASE, K230_SSI_DONECR),
+                    ==, 1);
+    g_assert_cmphex(k230_ssi_readl(qts, K230_SPI0_BASE, K230_SSI_RISR) &
+                    K230_SSI_INT_DONE, ==, 0);
+
+    k230_ssi_writel(qts, K230_SPI0_BASE, K230_SSI_IMR, 0);
+    configure_idma(qts, K230_SSI_TMOD_RO, FLASH_CMD_QUAD_OUT,
+                   K230_SSI_FLASH_PATTERN_ADDR, 0x100000000ULL,
+                   sizeof(actual));
+    g_assert_cmphex(k230_ssi_readl(qts, K230_SPI0_BASE, K230_SSI_RISR) &
+                    K230_SSI_INT_AXIE, ==, K230_SSI_INT_AXIE);
+    assert_idma_stopped(qts, 0);
+    k230_ssi_writel(qts, K230_SPI0_BASE, K230_SSI_IMR,
+                    K230_SSI_INT_AXIE);
+    g_assert_true(k230_ssi_plic_pending(
+        qts, k230_ssi_instances[0].first_irq + K230_SSI_IRQ_AXIE));
+    k230_ssi_writel(qts, K230_SPI0_BASE, K230_SSI_AXIECR, 1);
+    g_assert_cmphex(k230_ssi_readl(qts, K230_SPI0_BASE, K230_SSI_AXIECR),
+                    ==, 1);
+    g_assert_cmphex(k230_ssi_readl(qts, K230_SPI0_BASE, K230_SSI_RISR) &
+                    K230_SSI_INT_AXIE, ==, 0);
+
+    qtest_quit(qts);
+    k230_ssi_flash_image_clear(&image);
+}
+
 int main(int argc, char **argv)
 {
     g_test_init(&argc, &argv, NULL);
@@ -685,5 +779,6 @@ int main(int argc, char **argv)
     qtest_add_func("/k230-dw-ssi/qspi-config", test_qspi_config);
     qtest_add_func("/k230-dw-ssi/spi-nor", test_spi_nor);
     qtest_add_func("/k230-dw-ssi/qspi-sdr", test_qspi_sdr);
+    qtest_add_func("/k230-dw-ssi/idma", test_idma);
     return g_test_run();
 }
-- 
2.43.0