[PATCH 04/11] hw/ssi: Add K230 SSI interrupt controller

Kangjie Huang <[email protected]> Sun, 26 Jul 2026 20:28:22 +0800
Newsgroups org.nongnu.qemu-riscv,org.nongnu.qemu-devel
Message-ID <087a0aba5b55336b11fe599e01da59cc6c5aba96.1785064313.git.flamboyant.h.01@gmail.com>
Implement the SSI interrupt state and GPIO outputs.

Derive TXE and RXF from the FIFO thresholds. Keep RXU, TXO, and RXO as
latched causes with their documented read-clear behaviour.

Cover TXE level signalling, RXU latching, interrupt masking, and cause
clearing in qtest. PLIC routing is added by the next patch.

Signed-off-by: Kangjie Huang <[email protected]>
---
 hw/ssi/k230_dw_ssi.c           | 128 ++++++++++++++++++++++++++++++++-
 include/hw/ssi/k230_dw_ssi.h   |  17 +++++
 tests/qtest/k230-dw-ssi-test.c |  36 ++++++++++
 3 files changed, 179 insertions(+), 2 deletions(-)

diff --git a/hw/ssi/k230_dw_ssi.c b/hw/ssi/k230_dw_ssi.c
index d7315f28ef..26ac052a63 100644
--- a/hw/ssi/k230_dw_ssi.c
+++ b/hw/ssi/k230_dw_ssi.c
@@ -38,6 +38,7 @@
 #define K230_DW_SSI_AXIARLEN_RESET          0x00000700
 #define K230_DW_SSI_VERSION                 0x3130332a
 #define K230_DW_SSI_PIO_TX_BATCH            64
+#define K230_DW_SSI_IRQ_VALID_MASK          0x000009bf
 
 REG32(CTRLR0, 0x000)
     FIELD(CTRLR0, DFS, 0, 5)
@@ -258,12 +259,64 @@ REG32(XIP_WRITE_CTRL, 0x148)
 #define K230_DW_SSI_AXIAR0_WRITABLE_MASK R_AXIAR0_AXIAR_0_31_MASK
 #define K230_DW_SSI_AXIAR1_WRITABLE_MASK R_AXIAR1_AXIAR_32_63_MASK
 
+static const uint32_t k230_dw_ssi_irq_status_mask[
+    K230_DW_SSI_IRQ_COUNT] = {
+    [K230_DW_SSI_IRQ_TXE] = R_RISR_TXEIR_MASK,
+    [K230_DW_SSI_IRQ_TXO] = R_RISR_TXOIR_MASK,
+    [K230_DW_SSI_IRQ_RXF] = R_RISR_RXFIR_MASK,
+    [K230_DW_SSI_IRQ_RXO] = R_RISR_RXOIR_MASK,
+    [K230_DW_SSI_IRQ_TXU] = R_RISR_TXUIR_MASK,
+    [K230_DW_SSI_IRQ_RXU] = R_RISR_RXUIR_MASK,
+    [K230_DW_SSI_IRQ_MST] = R_RISR_MSTIR_MASK,
+    [K230_DW_SSI_IRQ_DONE] = R_RISR_DONER_MASK,
+    [K230_DW_SSI_IRQ_AXIE] = R_RISR_AXIER_MASK,
+};
+
 static void k230_dw_ssi_write_masked(K230DwSsiState *s, unsigned int reg,
                                      uint32_t value, uint32_t mask)
 {
     s->regs[reg] = (s->regs[reg] & ~mask) | (value & mask);
 }
 
+static uint32_t k230_dw_ssi_irq_raw_status(K230DwSsiState *s)
+{
+    uint32_t status = s->irq_latched;
+    uint32_t tx_used = fifo32_num_used(&s->tx_fifo);
+    uint32_t rx_used = fifo32_num_used(&s->rx_fifo);
+    uint32_t tx_threshold =
+        FIELD_EX32(s->regs[R_TXFTLR], TXFTLR, TFT);
+    uint32_t rx_threshold =
+        FIELD_EX32(s->regs[R_RXFTLR], RXFTLR, RFT);
+
+    if (tx_used <= tx_threshold) {
+        status |= R_RISR_TXEIR_MASK;
+    }
+    if (rx_used > rx_threshold) {
+        status |= R_RISR_RXFIR_MASK;
+    }
+    return status & K230_DW_SSI_IRQ_VALID_MASK;
+}
+
+static void k230_dw_ssi_update_irq(K230DwSsiState *s)
+{
+    uint32_t status = k230_dw_ssi_irq_raw_status(s) &
+                      s->regs[R_IMR] & K230_DW_SSI_IRQ_VALID_MASK;
+
+    for (int i = 0; i < K230_DW_SSI_IRQ_COUNT; i++) {
+        qemu_set_irq(s->irqs[i], !!(status & k230_dw_ssi_irq_status_mask[i]));
+    }
+}
+
+static uint32_t k230_dw_ssi_irq_read_clear(K230DwSsiState *s,
+                                            uint32_t clear_mask)
+{
+    uint32_t active = s->irq_latched & clear_mask;
+
+    s->irq_latched &= ~clear_mask;
+    k230_dw_ssi_update_irq(s);
+    return !!active;
+}
+
 static uint32_t k230_dw_ssi_frame_masked(K230DwSsiState *s)
 {
     unsigned int bits = FIELD_EX32(s->regs[R_CTRLR0], CTRLR0, DFS) + 1;
@@ -333,6 +386,7 @@ static void k230_dw_ssi_abort_transfer(K230DwSsiState *s)
     fifo32_reset(&s->rx_fifo);
     s->phase = K230_DW_SSI_PHASE_IDLE;
     s->remaining_frames = 0;
+    k230_dw_ssi_update_irq(s);
 }
 
 static uint32_t k230_dw_ssi_status(K230DwSsiState *s)
@@ -360,6 +414,8 @@ static void k230_dw_ssi_push_tx(K230DwSsiState *s, uint32_t tx)
     }
 
     if (fifo32_is_full(&s->tx_fifo)) {
+        s->irq_latched |= R_RISR_TXOIR_MASK;
+        k230_dw_ssi_update_irq(s);
         return;
     }
 
@@ -368,6 +424,7 @@ static void k230_dw_ssi_push_tx(K230DwSsiState *s, uint32_t tx)
     if (s->phase != K230_DW_SSI_PHASE_STANDARD_TX_ONLY) {
         k230_dw_ssi_run_transfer(s);
     }
+    k230_dw_ssi_update_irq(s);
 }
 
 static uint32_t k230_dw_ssi_send_frame(K230DwSsiState *s,
@@ -410,6 +467,12 @@ static void k230_dw_ssi_run_transfer(K230DwSsiState *s)
             uint32_t rx = k230_dw_ssi_send_frame(s, tx);
             if (!fifo32_is_full(&s->rx_fifo)) {
                 fifo32_push(&s->rx_fifo, rx);
+            } else {
+                s->irq_latched |= R_RISR_RXOIR_MASK;
+                qemu_log_mask(LOG_GUEST_ERROR,
+                              "%s: RX FIFO full, dropping frame\n",
+                              DEVICE(s)->canonical_path);
+                break;
             }
         }
         break;
@@ -549,9 +612,13 @@ static uint64_t k230_dw_ssi_read(void *opaque, hwaddr addr, unsigned int size)
     if (k230_dw_ssi_is_dr(addr)) {
         if (!fifo32_is_empty(&s->rx_fifo)) {
             value = fifo32_pop(&s->rx_fifo) & k230_dw_ssi_frame_masked(s);
+        } else {
+            value = 0;
+            s->irq_latched |= R_RISR_RXUIR_MASK;
         }
 
         k230_dw_ssi_run_transfer(s);
+        k230_dw_ssi_update_irq(s);
         return value;
     }
 
@@ -592,6 +659,7 @@ static uint64_t k230_dw_ssi_read(void *opaque, hwaddr addr, unsigned int size)
         value = fifo32_num_used(&s->tx_fifo);
         if (s->phase == K230_DW_SSI_PHASE_STANDARD_TX_ONLY) {
             k230_dw_ssi_run_transfer(s);
+            k230_dw_ssi_update_irq(s);
         }
         break;
     case A_RXFLR:
@@ -601,18 +669,33 @@ static uint64_t k230_dw_ssi_read(void *opaque, hwaddr addr, unsigned int size)
         value = k230_dw_ssi_status(s);
         if (s->phase == K230_DW_SSI_PHASE_STANDARD_TX_ONLY) {
             k230_dw_ssi_run_transfer(s);
+            k230_dw_ssi_update_irq(s);
         }
         break;
     case A_ISR:
+        value = k230_dw_ssi_irq_raw_status(s) & s->regs[R_IMR] &
+                K230_DW_SSI_IRQ_VALID_MASK;
+        break;
     case A_RISR:
-        value = 0;
+        value = k230_dw_ssi_irq_raw_status(s);
         break;
     case A_TXEICR:
+        value = k230_dw_ssi_irq_read_clear(
+            s, R_RISR_TXOIR_MASK | R_RISR_TXUIR_MASK);
+        break;
     case A_RXOICR:
+        value = k230_dw_ssi_irq_read_clear(s, R_RISR_RXOIR_MASK);
+        break;
     case A_RXUICR:
+        value = k230_dw_ssi_irq_read_clear(s, R_RISR_RXUIR_MASK);
+        break;
     case A_MSTICR:
+        value = k230_dw_ssi_irq_read_clear(s, R_RISR_MSTIR_MASK);
+        break;
     case A_ICR:
-        value = 0;
+        value = k230_dw_ssi_irq_read_clear(
+            s, R_RISR_TXOIR_MASK | R_RISR_RXUIR_MASK |
+               R_RISR_RXOIR_MASK | R_RISR_MSTIR_MASK);
         break;
     case A_AXIECR:
     case A_DONECR:
@@ -678,6 +761,7 @@ static void k230_dw_ssi_write(void *opaque, hwaddr addr,
 
         k230_dw_ssi_update_cs(s);
         k230_dw_ssi_run_transfer(s);
+        k230_dw_ssi_update_irq(s);
         break;
     }
     case A_MWCR:
@@ -695,6 +779,7 @@ static void k230_dw_ssi_write(void *opaque, hwaddr addr,
 
         k230_dw_ssi_update_cs(s);
         k230_dw_ssi_run_transfer(s);
+        k230_dw_ssi_update_irq(s);
         break;
     }
     case A_BAUDR:
@@ -704,10 +789,12 @@ static void k230_dw_ssi_write(void *opaque, hwaddr addr,
     case A_TXFTLR:
         k230_dw_ssi_write_masked(s, R_TXFTLR, value,
                                  K230_DW_SSI_TXFTLR_WRITABLE_MASK);
+        k230_dw_ssi_update_irq(s);
         break;
     case A_RXFTLR:
         k230_dw_ssi_write_masked(s, R_RXFTLR, value,
                                  K230_DW_SSI_RXFTLR_WRITABLE_MASK);
+        k230_dw_ssi_update_irq(s);
         break;
     case A_TXFLR:
     case A_RXFLR:
@@ -716,6 +803,7 @@ static void k230_dw_ssi_write(void *opaque, hwaddr addr,
     case A_IMR:
         k230_dw_ssi_write_masked(s, R_IMR, value,
                                  K230_DW_SSI_IMR_WRITABLE_MASK);
+        k230_dw_ssi_update_irq(s);
         break;
     case A_ISR:
     case A_RISR:
@@ -831,6 +919,7 @@ static void k230_dw_ssi_enter_reset(Object *obj, ResetType type)
     fifo32_reset(&s->rx_fifo);
     s->phase = K230_DW_SSI_PHASE_IDLE;
     s->remaining_frames = 0;
+    s->irq_latched = 0;
 
     s->regs[R_CTRLR0] = K230_DW_SSI_CTRLR0_RESET;
     s->regs[R_SR] = K230_DW_SSI_SR_RESET;
@@ -842,6 +931,8 @@ static void k230_dw_ssi_enter_reset(Object *obj, ResetType type)
     s->regs[R_SPI_CTRLR0] = s->max_lines == 8 ?
         K230_DW_SSI_SPI_CTRLR0_FMC_RESET :
         K230_DW_SSI_SPI_CTRLR0_SPI_RESET;
+
+    k230_dw_ssi_update_irq(s);
 }
 
 static void k230_dw_ssi_hold_reset(Object *obj, ResetType type)
@@ -856,12 +947,40 @@ static void k230_dw_ssi_hold_reset(Object *obj, ResetType type)
     }
 }
 
+static void k230_dw_ssi_exit_reset(Object *obj, ResetType type)
+{
+    K230DwSsiState *s = K230_DW_SSI(obj);
+
+    k230_dw_ssi_update_irq(s);
+}
+
+static int k230_dw_ssi_post_load(void *opaque, int version_id)
+{
+    K230DwSsiState *s = opaque;
+
+    if (s->active_cs < -1 || s->active_cs >= (int)s->num_cs) {
+        return -EINVAL;
+    }
+
+    for (int i = 0; i < s->num_cs; i++) {
+        qemu_irq_raise(s->cs_lines[i]);
+    }
+    if (s->active_cs >= 0) {
+        qemu_irq_lower(s->cs_lines[s->active_cs]);
+    }
+
+    k230_dw_ssi_update_irq(s);
+    return 0;
+}
+
 static const VMStateDescription vmstate_k230_dw_ssi = {
     .name = TYPE_K230_DW_SSI,
+    .post_load = k230_dw_ssi_post_load,
     .fields = (const VMStateField[]) {
         VMSTATE_UINT32_ARRAY(regs, K230DwSsiState, K230_DW_SSI_NUM_REGS),
         VMSTATE_FIFO32(tx_fifo, K230DwSsiState),
         VMSTATE_FIFO32(rx_fifo, K230DwSsiState),
+        VMSTATE_UINT32(irq_latched, K230DwSsiState),
         VMSTATE_UINT32(phase, K230DwSsiState),
         VMSTATE_UINT32(remaining_frames, K230DwSsiState),
         VMSTATE_INT32(active_cs, K230DwSsiState),
@@ -881,6 +1000,10 @@ static void k230_dw_ssi_init(Object *obj)
                           TYPE_K230_DW_SSI, K230_DW_SSI_MMIO_SIZE);
     sysbus_init_mmio(sbd, &s->mmio);
 
+    for (int i = 0; i < K230_DW_SSI_IRQ_COUNT; i++) {
+        sysbus_init_irq(sbd, &s->irqs[i]);
+    }
+
     fifo32_create(&s->tx_fifo, K230_DW_SSI_FIFO_CAPACITY);
     fifo32_create(&s->rx_fifo, K230_DW_SSI_FIFO_CAPACITY);
     s->active_cs = -1;
@@ -930,6 +1053,7 @@ static void k230_dw_ssi_class_init(ObjectClass *klass, const void *data)
     device_class_set_props(dc, k230_dw_ssi_properties);
     rc->phases.enter = k230_dw_ssi_enter_reset;
     rc->phases.hold = k230_dw_ssi_hold_reset;
+    rc->phases.exit = k230_dw_ssi_exit_reset;
 }
 
 static const TypeInfo k230_dw_ssi_info = {
diff --git a/include/hw/ssi/k230_dw_ssi.h b/include/hw/ssi/k230_dw_ssi.h
index 993cb19745..404215245b 100644
--- a/include/hw/ssi/k230_dw_ssi.h
+++ b/include/hw/ssi/k230_dw_ssi.h
@@ -31,6 +31,20 @@ OBJECT_DECLARE_SIMPLE_TYPE(K230DwSsiState, K230_DW_SSI)
 #define K230_DW_SSI_NUM_REGS \
     (K230_DW_SSI_REGS_SIZE / sizeof(uint32_t))
 
+/* SSI GPIO output ordering differs from RISR/ISR bit ordering. */
+typedef enum K230DwSsiIrq {
+    K230_DW_SSI_IRQ_TXE,
+    K230_DW_SSI_IRQ_TXO,
+    K230_DW_SSI_IRQ_RXF,
+    K230_DW_SSI_IRQ_RXO,
+    K230_DW_SSI_IRQ_TXU,
+    K230_DW_SSI_IRQ_RXU,
+    K230_DW_SSI_IRQ_MST,
+    K230_DW_SSI_IRQ_DONE,
+    K230_DW_SSI_IRQ_AXIE,
+    K230_DW_SSI_IRQ_COUNT,
+} K230DwSsiIrq;
+
 typedef enum K230DwSsiPhase {
     K230_DW_SSI_PHASE_IDLE,
     K230_DW_SSI_PHASE_STANDARD_TX_ONLY,
@@ -45,11 +59,14 @@ struct K230DwSsiState {
     MemoryRegion mmio;
     SSIBus *spi;
     qemu_irq *cs_lines;
+    qemu_irq irqs[K230_DW_SSI_IRQ_COUNT];
 
     Fifo32 tx_fifo;
     Fifo32 rx_fifo;
     uint32_t regs[K230_DW_SSI_NUM_REGS];
 
+    uint32_t irq_latched;
+
     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 a62b1a7ff6..0a3d03d6f3 100644
--- a/tests/qtest/k230-dw-ssi-test.c
+++ b/tests/qtest/k230-dw-ssi-test.c
@@ -25,6 +25,7 @@
 #define K230_SSI_IMR             0x02c
 #define K230_SSI_ISR             0x030
 #define K230_SSI_RISR            0x034
+#define K230_SSI_RXUICR          0x040
 #define K230_SSI_DMACR           0x04c
 #define K230_SSI_IDR             0x058
 #define K230_SSI_VERSION_ID      0x05c
@@ -63,6 +64,8 @@
 #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_RXU                BIT(2)
 #define K230_SSI_INT_AXIE               BIT(8)
 #define K230_SSI_INT_DONE               BIT(11)
 
@@ -245,11 +248,44 @@ static void test_pio_data_path(void)
     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);
+}
+
 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);
     return g_test_run();
 }
-- 
2.43.0